1"""Factory class for all the property types."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING, Any, ClassVar
6
7from icalendar.caselessdict import CaselessDict
8from icalendar.prop.adr import vAdr
9from icalendar.prop.binary import vBinary
10from icalendar.prop.boolean import vBoolean
11from icalendar.prop.broken import vBroken
12from icalendar.prop.cal_address import vCalAddress
13from icalendar.prop.categories import vCategory
14from icalendar.prop.dt import (
15 vDate,
16 vDatetime,
17 vDDDLists,
18 vDDDTypes,
19 vDuration,
20 vPeriod,
21 vTime,
22 vUTCOffset,
23)
24from icalendar.prop.float import vFloat
25from icalendar.prop.geo import vGeo
26from icalendar.prop.inline import vInline
27from icalendar.prop.n import vN
28from icalendar.prop.org import vOrg
29from icalendar.prop.recur import vFrequency, vRecur, vWeekday
30from icalendar.prop.text import vText
31from icalendar.prop.uid import vUid
32from icalendar.prop.unknown import vUnknown
33from icalendar.prop.uri import vUri
34from icalendar.prop.xml_reference import vXmlReference
35
36from .integer import vInt
37
38if TYPE_CHECKING:
39 from icalendar.prop import VPROPERTY
40
41
42class TypesFactory(CaselessDict):
43 """Factory for all value types defined in :rfc:`5545` and subsequent.
44
45 The value and parameter names don't overlap. So one factory is enough for
46 both kinds.
47 """
48
49 _instance: ClassVar[TypesFactory | None] = None
50
51 @staticmethod
52 def instance() -> TypesFactory:
53 """Return a singleton instance of this class."""
54 if TypesFactory._instance is None:
55 TypesFactory._instance = TypesFactory()
56 return TypesFactory._instance
57
58 def __init__(self, *args: Any, **kwargs: Any) -> None:
59 """Set keys to upper for initial dict"""
60 super().__init__(*args, **kwargs)
61 self.all_types = (
62 vBinary,
63 vBoolean,
64 vCalAddress,
65 vDDDLists,
66 vDDDTypes,
67 vDate,
68 vDatetime,
69 vDuration,
70 vFloat,
71 vFrequency,
72 vGeo,
73 vInline,
74 vInt,
75 vPeriod,
76 vRecur,
77 vText,
78 vTime,
79 vUTCOffset,
80 vUri,
81 vWeekday,
82 vCategory,
83 vAdr,
84 vN,
85 vOrg,
86 vUid,
87 vXmlReference,
88 vUnknown,
89 vBroken,
90 )
91 self["binary"] = vBinary
92 self["boolean"] = vBoolean
93 self["cal-address"] = vCalAddress
94 self["date"] = vDDDTypes
95 self["date-time"] = vDDDTypes
96 self["duration"] = vDDDTypes
97 self["float"] = vFloat
98 self["integer"] = vInt
99 self["period"] = vPeriod
100 self["recur"] = vRecur
101 self["text"] = vText
102 self["time"] = vTime
103 self["uri"] = vUri
104 self["utc-offset"] = vUTCOffset
105 self["geo"] = vGeo
106 self["inline"] = vInline
107 self["date-time-list"] = vDDDLists
108 self["categories"] = vCategory
109 self["adr"] = vAdr # RFC 6350 vCard
110 self["n"] = vN # RFC 6350 vCard
111 self["org"] = vOrg # RFC 6350 vCard
112 self["unknown"] = vUnknown # RFC 7265
113 self["uid"] = vUid # RFC 9253
114 self["xml-reference"] = vXmlReference # RFC 9253
115 self["x-broken"] = vBroken
116
117 #################################################
118 # Property types
119
120 # These are the default types
121 types_map = CaselessDict(
122 {
123 ####################################
124 # Property value types
125 # Calendar Properties
126 "calscale": "text",
127 "method": "text",
128 "prodid": "text",
129 "version": "text",
130 # Descriptive Component Properties
131 "attach": "uri",
132 "categories": "categories",
133 "class": "text",
134 # vCard Properties (RFC 6350)
135 "adr": "adr",
136 "n": "n",
137 "org": "org",
138 "comment": "text",
139 "description": "text",
140 "geo": "geo",
141 "location": "text",
142 "percent-complete": "integer",
143 "priority": "integer",
144 "resources": "text",
145 "status": "text",
146 "summary": "text",
147 # RFC 9253
148 # link should be uri, xml-reference or uid
149 # uri is likely most helpful if people forget to set VALUE
150 "link": "uri",
151 "concept": "uri",
152 "refid": "text",
153 # Date and Time Component Properties
154 "completed": "date-time",
155 "dtend": "date-time",
156 "due": "date-time",
157 "dtstart": "date-time",
158 "duration": "duration",
159 "freebusy": "period",
160 "transp": "text",
161 "refresh-interval": "duration", # RFC 7986
162 # Time Zone Component Properties
163 "tzid": "text",
164 "tzname": "text",
165 "tzoffsetfrom": "utc-offset",
166 "tzoffsetto": "utc-offset",
167 "tzurl": "uri",
168 # Relationship Component Properties
169 "attendee": "cal-address",
170 "contact": "text",
171 "organizer": "cal-address",
172 "recurrence-id": "date-time",
173 "related-to": "text",
174 "url": "uri",
175 "conference": "uri", # RFC 7986
176 "image": "unknown", # RFC 7986 has no default value type
177 "source": "uri",
178 "uid": "text",
179 # Recurrence Component Properties
180 "exdate": "date-time-list",
181 "exrule": "recur",
182 "rdate": "date-time-list",
183 "rrule": "recur",
184 # Alarm Component Properties
185 "action": "text",
186 "repeat": "integer",
187 "trigger": "duration",
188 "acknowledged": "date-time",
189 "proximity": "text", # RFC 9074
190 # Change Management Component Properties
191 "created": "date-time",
192 "dtstamp": "date-time",
193 "last-modified": "date-time",
194 "sequence": "integer",
195 # Miscellaneous Component Properties
196 "request-status": "text",
197 ####################################
198 # parameter types (luckily there is no name overlap)
199 "altrep": "uri",
200 "cn": "text",
201 "cutype": "text",
202 "delegated-from": "cal-address",
203 "delegated-to": "cal-address",
204 "dir": "uri",
205 "encoding": "text",
206 "fmttype": "text",
207 "fbtype": "text",
208 "language": "text",
209 "member": "cal-address",
210 "partstat": "text",
211 "range": "text",
212 "related": "text",
213 "reltype": "text",
214 "role": "text",
215 "rsvp": "boolean",
216 "sent-by": "cal-address",
217 "value": "text",
218 # rfc 9253 parameters
219 "label": "text",
220 "linkrel": "text",
221 "gap": "duration",
222 }
223 )
224
225 def for_property(self, name, value_param: str | None = None) -> type[VPROPERTY]:
226 """Returns the type class for a property or parameter.
227
228 Parameters:
229 name: Property or parameter name
230 value_param: Optional ``VALUE`` parameter, for example,
231 "DATE", "DATE-TIME", or other string.
232
233 Returns:
234 The appropriate value type class.
235 """
236 name = name.upper()
237 value_param = value_param.upper() if value_param else None
238 # RFC 7265's UNKNOWN type is always represented verbatim, even for
239 # properties such as RDATE/EXDATE that normally use list parsing.
240 if value_param and value_param == "UNKNOWN":
241 return self["unknown"]
242
243 # Special case: RDATE and EXDATE always use vDDDLists to support list values
244 # regardless of the VALUE parameter
245 if name in ("RDATE", "EXDATE") and (
246 value_param is None or value_param in vDDDTypes.VALUE_MAP
247 ):
248 return self["date-time-list"]
249 # Only use VALUE parameter for known properties that support multiple value
250 # types (like DTSTART, DTEND, etc. which can be DATE or DATE-TIME)
251 # For unknown/custom properties, always use the default type from types_map
252 if value_param and name in self.types_map and value_param in self:
253 # vCategory also has TEXT as VALUE.
254 # It should be preferred over TEXT
255 # This checks the case that the value type actually matches.
256 value_type: VPROPERTY = self[self.types_map[name]]
257 if value_type.default_value == value_param:
258 return value_type
259 return self[value_param]
260 if value_param and (value_param in self) and value_param != "IMAGE":
261 return self[value_param]
262
263 return self[self.types_map.get(name, "unknown")]
264
265 #: jCal value types defined by :rfc:`7265` that are used verbatim as the
266 #: type field. Internal type keys outside of this set (for example
267 #: ``"categories"`` or ``"date-time-list"``) are mapped to their concrete
268 #: jCal type via the value class.
269 _jcal_value_types = frozenset(
270 {
271 "binary",
272 "boolean",
273 "cal-address",
274 "date",
275 "date-time",
276 "duration",
277 "float",
278 "integer",
279 "period",
280 "recur",
281 "text",
282 "time",
283 "uri",
284 "utc-offset",
285 "unknown",
286 }
287 )
288
289 def default_value_type(self, name: str) -> str:
290 """Return the default jCal value type for a property.
291
292 The result is a lowercase :rfc:`7265` value type, for example
293 ``"date-time"`` for ``DTSTART`` or ``"duration"`` for ``TRIGGER``.
294 This is the value type a property uses when no explicit ``VALUE``
295 parameter is given, so it tells :func:`Component.from_jcal` whether a
296 ``VALUE`` parameter has to be restored from the jCal type field.
297
298 Parameters:
299 name: Property name.
300
301 Returns:
302 The default jCal value type as a lowercase string.
303 """
304 internal = self.types_map.get(name.lower(), "unknown")
305 if internal in self._jcal_value_types:
306 return internal
307 # Internal keys such as ``categories`` or ``date-time-list`` are not
308 # jCal value types themselves; ask the value class for the type it
309 # actually serialises to.
310 try:
311 return self[internal].examples()[0].VALUE.lower()
312 except (KeyError, IndexError, AttributeError):
313 return "unknown"
314
315 def to_ical(self, name, value):
316 """Encodes a named value from a primitive python type to an icalendar
317 encoded string.
318 """
319 type_class = self.for_property(name)
320 return type_class(value).to_ical()
321
322 def from_ical(self, name, value):
323 """Decodes a named property or parameter value from an icalendar
324 encoded string to a primitive python type.
325 """
326 type_class = self.for_property(name)
327 return type_class.from_ical(value)
328
329
330__all__ = ["TypesFactory"]