1"""INT values from :rfc:`5545`."""
2
3from typing import Any, ClassVar
4from xml.etree.ElementTree import Element, SubElement
5
6from icalendar.compatibility import Self
7from icalendar.error import JCalParsingError, XCalParsingError
8from icalendar.parser import Parameters
9from icalendar.parser.xcal.adapter import ElementAdapter
10from icalendar.parser.xcal.base import XCalParser
11from icalendar.parser.xcal.wrapper import from_xcal_wrapper
12from icalendar.parser_tools import ICAL_TYPE
13
14
15class vInt(int):
16 """Integer
17
18 Value Name:
19 INTEGER
20
21 Purpose:
22 This value type is used to identify properties that contain a
23 signed integer value.
24
25 Format Definition:
26 This value type is defined by the following notation:
27
28 .. code-block:: text
29
30 integer = (["+"] / "-") 1*DIGIT
31
32 Description:
33 If the property permits, multiple "integer" values are
34 specified by a COMMA-separated list of values. The valid range
35 for "integer" is -2147483648 to 2147483647. If the sign is not
36 specified, then the value is assumed to be positive.
37
38 The ``__new__`` method creates a vInt instance:
39
40 Parameters:
41 value: Integer value to encode. Must be within :attr:`min` to :attr:`max`.
42 params: Optional parameter dictionary for the property.
43
44 Returns:
45 vInt instance
46
47 Raises:
48 ValueError: If the value is outside the RFC 5545 signed 32-bit integer range
49 as defined in :attr:`min` and :attr:`max`.
50
51 Examples:
52
53 .. code-block:: text
54
55 1234567890
56 -1234567890
57 +1234567890
58 432109876
59
60 .. code-block:: pycon
61
62 >>> from icalendar.prop import vInt
63 >>> integer = vInt.from_ical('1234567890')
64 >>> integer
65 1234567890
66 >>> integer = vInt.from_ical('-1234567890')
67 >>> integer
68 -1234567890
69 >>> integer = vInt.from_ical('+1234567890')
70 >>> integer
71 1234567890
72 >>> integer = vInt.from_ical('432109876')
73 >>> integer
74 432109876
75
76 Create a PRIORITY property (1 = highest priority):
77
78 .. code-block:: pycon
79
80 >>> priority = vInt(1)
81 >>> priority
82 1
83 >>> priority.to_ical()
84 b'1'
85
86 Create SEQUENCE property (for versioning):
87
88 .. code-block:: pycon
89
90 >>> sequence = vInt(3)
91 >>> sequence.to_ical()
92 b'3'
93 """
94
95 default_value: ClassVar[str] = "INTEGER"
96 params: Parameters
97
98 min: ClassVar[int] = -2_147_483_648
99 """min: The minimum valid value per :rfc:`5545#section-3.3.8` (``-2147483648``)."""
100 max: ClassVar[int] = 2_147_483_647
101 """max: The maximum valid value per :rfc:`5545#section-3.3.8` (``2147483647``)."""
102
103 def __new__(
104 cls,
105 *args,
106 params: dict[str, Any] | None = None,
107 _validate_range: bool = True,
108 **kwargs,
109 ):
110 self = super().__new__(cls, *args, **kwargs)
111 self.params = Parameters(params)
112 if not (cls.min <= self <= cls.max) and _validate_range:
113 raise ValueError(
114 f"Integer {self} is outside the RFC 5545 range [{cls.min}, {cls.max}]"
115 )
116 return self
117
118 def to_ical(self) -> bytes:
119 return str(self).encode("utf-8")
120
121 @property
122 def ical_value(self) -> int:
123 """INTEGER property type according to :rfc:`5545#section-3.3.8`"""
124 return int(self)
125
126 @classmethod
127 def from_ical(cls, ical: ICAL_TYPE):
128 try:
129 value = int(ical)
130 except Exception as e:
131 raise ValueError(f"Expected int, got: {ical}") from e
132 return cls(value)
133
134 @classmethod
135 def examples(cls) -> list[Self]:
136 """Examples of vInt."""
137 return [cls(1000), cls(-42)]
138
139 from icalendar.param import VALUE
140
141 def to_jcal(self, name: str) -> list:
142 """The jCal representation of this property according to :rfc:`7265`."""
143 return [name, self.params.to_jcal(), self.VALUE.lower(), int(self)]
144
145 @classmethod
146 def from_jcal(cls, jcal_property: list) -> Self:
147 """Parse jCal from :rfc:`7265`.
148
149 Parameters:
150 jcal_property: The jCal property to parse.
151
152 Raises:
153 ~error.JCalParsingError: If the provided jCal is invalid.
154 """
155 JCalParsingError.validate_property(jcal_property, cls)
156 JCalParsingError.validate_value_type(jcal_property[3], int, cls, 3)
157 return cls(
158 jcal_property[3],
159 params=Parameters.from_jcal_property(jcal_property),
160 )
161
162 @classmethod
163 def parse_jcal_value(cls, value: Any) -> int:
164 """Parse a jCal value for vInt.
165
166 Raises:
167 ~error.JCalParsingError: If the value is not an int.
168 """
169 JCalParsingError.validate_value_type(value, int, cls)
170 return cls(value)
171
172 @classmethod
173 @from_xcal_wrapper
174 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
175 """Parse xCal from :rfc:`6321`.
176
177 Parameters:
178 element: The xCal element to parse.
179
180 Raises:
181 ~error.XCalParsingError: If the provided xCal is invalid.
182 """
183 element = parser.parse_tag(cls.default_value)
184 return cls.from_xcal_element(element, params)
185
186 XCAL_TYPE = "xsd:integer"
187
188 @classmethod
189 def from_xcal_element(cls, element: ElementAdapter, params: Parameters) -> Self:
190 """Parse xCal from :rfc:`6321`.
191
192 Parameters:
193 element: The xCal element to parse.
194
195 Raises:
196 ~error.XCalParsingError: If the provided xCal is invalid.
197 """
198 try:
199 value = int(element.get_xsd_token())
200 except (TypeError, ValueError) as e:
201 raise XCalParsingError(
202 f"Expected {cls.XCAL_TYPE}",
203 element.get_xsd_token(),
204 element,
205 ) from e
206 # xCal integers are xsd:integers. They do not have a value range.
207 return cls(value, params=params, _validate_range=False)
208
209 @classmethod
210 @from_xcal_wrapper
211 def from_xcal_in_recur(cls, parser: XCalParser, params: Parameters) -> Self:
212 """Parse xCal from :rfc:`6321`.
213
214 Parameters:
215 element: The xCal element to parse.
216
217 Raises:
218 ~error.XCalParsingError: If the provided xCal is invalid.
219 """
220 element = parser.parse_tag()
221 return cls.from_xcal_element(element, params)
222
223 def to_xcal(self, element: Element) -> None:
224 """Add the xCal representation of this property according to :rfc:`6321`."""
225 self.params.to_xcal(element)
226 element = SubElement(element, self.default_value.lower())
227 element.text = self.to_ical().decode()
228
229
230class vNonNegativeInt(vInt):
231 """This is an integer that is supposed to be at least 0.
232
233 See https://www.rfc-editor.org/rfc/inline-errata/rfc6321.html#btn_3050
234 """
235
236 min: ClassVar[int] = 0
237
238 @classmethod
239 def examples(cls) -> list[vInt]:
240 """Examples of vPositiveInt."""
241 return [cls(1000), cls(0)]
242
243 XCAL_TYPE = "xsd:nonNegativeInteger"
244
245
246__all__ = ["vInt", "vNonNegativeInt"]