1"""vRequestStatus is a subclass of vText with convenience methods."""
2
3from __future__ import annotations
4
5import re
6from typing import TYPE_CHECKING, Any
7from xml.etree.ElementTree import Element, SubElement
8
9from icalendar.error import InvalidCalendar, JCalParsingError
10from icalendar.parser.parameter import Parameters
11from icalendar.parser.xcal.wrapper import from_xcal_wrapper
12
13from .text import vText
14
15if TYPE_CHECKING:
16 from icalendar.compatibility import Self
17 from icalendar.parser.xcal.base import XCalParser
18
19CODE_REGEX = r"^[0-9]\.[0-9](?:\.[0-9])?$"
20CODE_REGEX_PATTERN = re.compile(CODE_REGEX)
21
22REQUEST_STATUS_GRAMMAR = re.compile(
23 r"^(?P<code>[0-9]\.[0-9](?:\.[0-9])?)"
24 r";(?P<description>(?:\\;|[^;])*)"
25 r"(?:;(?P<data>.*))?$",
26 re.MULTILINE,
27)
28UNESCAPE = re.compile(r"\\(.)")
29ESCAPE = re.compile(r"([\\,;])")
30
31
32class vRequestStatus(vText):
33 """A text specifically for REQUEST-STATUS.
34
35 See :rfc:`5545#section-3.8.8.3`,
36 :rfc:`6321#appendix-A` in subsection 3.8.8.3 Request Status and,
37 :rfc:`7265#section-3.4.1.2`.
38
39 Examples:
40
41 The attributes of a valid request status return the parsed data.
42
43 .. code-block:: pycon
44
45 >>> from icalendar.prop import vRequestStatus
46 >>> r = vRequestStatus("2.0;Success")
47 >>> r.code
48 (2, 0)
49 >>> r.description
50 'Success'
51 >>> r.data is None
52 True
53
54 An invalid :attr:`code` is indicated by an empty tuple and
55 an empty description.
56
57 .. code-block:: pycon
58
59 >>> r = vRequestStatus("2.10;invalid")
60 >>> r.code
61 ()
62 >>> r.description
63 ''
64 >>> r.data is None
65 True
66
67 """
68
69 __match: re.Match | None = None
70
71 def _get_match(self) -> re.Match | None:
72 """Return a match for parsing."""
73 if self.__match is None:
74 self.__match = REQUEST_STATUS_GRAMMAR.match(self)
75 return self.__match
76
77 @property
78 def code(self) -> tuple[int, int] | tuple[int, int, int] | tuple[()]:
79 """Return the status code as a tuple.
80
81 Returns:
82 A tuple of integers or ``()`` if the status code could not be parsed.
83 """
84 s = self.code_string.split(".")
85 if len(s) == 2:
86 return int(s[0]), int(s[1])
87 if len(s) == 3:
88 return int(s[0]), int(s[1]), int(s[2])
89 return ()
90
91 @property
92 def code_string(self) -> str:
93 """Return the status code as a string.
94
95 Returns:
96 The status string or ``""`` if the request status could not be parsed.
97 """
98 match = self._get_match()
99 if not match:
100 return ""
101 return match.group("code")
102
103 def _unescape(self, string: str):
104 """Unescape a string."""
105 return UNESCAPE.sub(r"\1", string)
106
107 @property
108 def description(self) -> str:
109 """Return the description of the request status.
110
111 Returns:
112 The description of the request status or ``""``
113 if the request status could not be parsed.
114 """
115 match = self._get_match()
116 if not match:
117 return ""
118 description: str = match.group("description") or ""
119 return self._unescape(description)
120
121 @property
122 def data(self) -> str | None:
123 """Return the data of the request status.
124
125 Returns:
126 The data of the request status or ``None``.
127 """
128 match = self._get_match()
129 if not match:
130 return None
131 data: str = match.group("data")
132 if data is None:
133 return None
134 return self._unescape(data)
135
136 @classmethod
137 def new(
138 cls,
139 code: tuple[int, int] | tuple[int, int, int] | str,
140 description: str = "",
141 data: str | None = None,
142 *,
143 params: dict[str, Any] | None = None,
144 validate: bool = True,
145 ) -> Self:
146 """Create a new request status object.
147
148 Parameters:
149 code: The status code.
150 description: The description of the request status.
151 data: The data of the request status.
152 validate: Turn validation for the ``code`` string on and off.
153 ``code`` tuples are always validated.
154
155 Raises:
156 InvalidCalendar: If the code is not valid.
157 TypeError: If the code is neither a tuple of integers nor string.
158
159 Returns:
160 A new request status object.
161 """
162 if not isinstance(code, (tuple, str)):
163 raise TypeError("code must be tuple or string")
164 if isinstance(code, str):
165 code_string = code
166 if validate and CODE_REGEX_PATTERN.match(code_string) is None:
167 raise InvalidCalendar(
168 f"code must be either 2 or 3 numbers, each in the range 0-9, separated by `.`. Got {code_string!r}."
169 )
170 else:
171 if not all(isinstance(x, int) for x in code):
172 raise TypeError(
173 f"code must be a tuple of either 2 or 3 numbers, each in the range 0-9, separated by `.`. Got {code!r}."
174 )
175 if len(code) not in (2, 3) or not all(0 <= x <= 9 for x in code):
176 raise InvalidCalendar(
177 f"code must be either 2 or 3 numbers, each in the range 0-9, separated by `.`. Got {code!r}."
178 )
179 code_string = f"{code[0]}.{code[1]}"
180 if len(code) == 3:
181 code_string += f".{code[2]}"
182 description = ESCAPE.sub(r"\\\1", description)
183 request_status = f"{code_string};{description}"
184 if data is not None:
185 data = ESCAPE.sub(r"\\\1", data)
186 request_status += f";{data}"
187 return cls(request_status, params=params)
188
189 @classmethod
190 def examples(cls) -> list[Self]: # pyright: ignore[reportIncompatibleMethodOverride]
191 """Examples of vRequestStatus."""
192 return [
193 cls("2.0;Success"),
194 cls("3.7;Invalid calendar user;ATTENDEE:mailto:jsmith@example.org"),
195 ]
196
197 def to_jcal(self, name: str) -> list:
198 """The jCal representation of this property according to :rfc:`7265`."""
199 status = [self.code_string, self.description, self.data]
200 if status[-1] is None:
201 status = status[:-1]
202 return [name, self.params.to_jcal(), self.VALUE.lower(), status]
203
204 @classmethod
205 def from_jcal(cls, jcal_property: list) -> Self:
206 """Parse jCal from :rfc:`7265`.
207
208 Parameters:
209 jcal_property: The jCal property to parse.
210
211 Raises:
212 ~error.JCalParsingError: If the provided jCal is invalid.
213 """
214 JCalParsingError.validate_property(jcal_property, cls)
215 status_list = jcal_property[3]
216 JCalParsingError.validate_list_type(status_list, str, cls, 3)
217 return cls.new(
218 code=status_list[0] if len(status_list) > 0 else "",
219 description=status_list[1] if len(status_list) > 1 else "",
220 data=";".join(status_list[2:]) if len(status_list) > 2 else None,
221 params=Parameters.from_jcal_property(jcal_property),
222 validate=False,
223 )
224
225 def to_xcal(self, element: Element) -> None:
226 """Add the xCal representation of this property according to :rfc:`6321`."""
227 self.params.to_xcal(element)
228 SubElement(element, "code").text = self.code_string
229 SubElement(element, "description").text = self.description
230 if self.data is not None:
231 SubElement(element, "data").text = self.data
232
233 @classmethod
234 @from_xcal_wrapper
235 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
236 """Parse xCal from :rfc:`6321`.
237
238 Parameters:
239 xml: The XML to parse or a parser.
240
241 Raises:
242 ~error.XCalParsingError: If the provided xCal is invalid.
243 """
244 code = parser.parse_tag("code")
245 description = parser.parse_tag("description")
246 data = parser.parse_optional_tag("data")
247 return cls.new(
248 code.get_xsd_token(),
249 description.get_xsd_string(),
250 None if data is None else data.get_xsd_string(),
251 params=params,
252 validate=False,
253 )
254
255
256__all__ = ["vRequestStatus"]