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