exdrf 0.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exdrf/__init__.py +0 -0
- exdrf/__version__.py +24 -0
- exdrf/api.py +51 -0
- exdrf/constants.py +30 -0
- exdrf/dataset.py +197 -0
- exdrf/field.py +554 -0
- exdrf/field_types/__init__.py +0 -0
- exdrf/field_types/api.py +78 -0
- exdrf/field_types/blob_field.py +44 -0
- exdrf/field_types/bool_field.py +47 -0
- exdrf/field_types/date_field.py +49 -0
- exdrf/field_types/date_time.py +52 -0
- exdrf/field_types/dur_field.py +44 -0
- exdrf/field_types/enum_field.py +41 -0
- exdrf/field_types/filter_field.py +11 -0
- exdrf/field_types/float_field.py +85 -0
- exdrf/field_types/float_list.py +18 -0
- exdrf/field_types/formatted.py +39 -0
- exdrf/field_types/int_field.py +70 -0
- exdrf/field_types/int_list.py +18 -0
- exdrf/field_types/ref_base.py +105 -0
- exdrf/field_types/ref_m2m.py +39 -0
- exdrf/field_types/ref_m2o.py +23 -0
- exdrf/field_types/ref_o2m.py +36 -0
- exdrf/field_types/ref_o2o.py +32 -0
- exdrf/field_types/sort_field.py +18 -0
- exdrf/field_types/str_field.py +77 -0
- exdrf/field_types/str_list.py +18 -0
- exdrf/field_types/time_field.py +49 -0
- exdrf/filter.py +653 -0
- exdrf/filter_dsl.py +950 -0
- exdrf/filter_op_catalog.py +222 -0
- exdrf/label_dsl.py +691 -0
- exdrf/moment.py +496 -0
- exdrf/py.typed +0 -0
- exdrf/py_support.py +21 -0
- exdrf/resource.py +901 -0
- exdrf/sa_fi_item.py +69 -0
- exdrf/sa_filter_op.py +324 -0
- exdrf/utils.py +17 -0
- exdrf/validator.py +45 -0
- exdrf/var_bag.py +328 -0
- exdrf/visitor.py +58 -0
- exdrf-0.0.1.dev0.dist-info/METADATA +42 -0
- exdrf-0.0.1.dev0.dist-info/RECORD +57 -0
- exdrf-0.0.1.dev0.dist-info/WHEEL +5 -0
- exdrf-0.0.1.dev0.dist-info/top_level.txt +3 -0
- exdrf_tests/__init__.py +0 -0
- exdrf_tests/test_dataset.py +422 -0
- exdrf_tests/test_field.py +109 -0
- exdrf_tests/test_filter.py +425 -0
- exdrf_tests/test_filter_dsl.py +556 -0
- exdrf_tests/test_label_dsl.py +234 -0
- exdrf_tests/test_resource.py +107 -0
- exdrf_tests/test_utils.py +43 -0
- exdrf_tests/test_visitor.py +31 -0
- exdrf_tests/var_bag_test.py +502 -0
exdrf/moment.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
from datetime import date, datetime, time
|
|
2
|
+
from typing import Any, List, TypeVar, cast
|
|
3
|
+
|
|
4
|
+
from attrs import define, field
|
|
5
|
+
from dateutil.relativedelta import relativedelta # type: ignore[import-untyped]
|
|
6
|
+
|
|
7
|
+
from exdrf.validator import ValidationResult
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", date, datetime)
|
|
10
|
+
|
|
11
|
+
labels = {
|
|
12
|
+
"YYYY": ("cmn.year", "Year"),
|
|
13
|
+
"MM": ("cmn.month", "Month"),
|
|
14
|
+
"DD": ("cmn.day", "Day"),
|
|
15
|
+
"HH": ("cmn.hour", "Hour"),
|
|
16
|
+
"mm": ("cmn.minute", "Minute"),
|
|
17
|
+
"ss": ("cmn.second", "Second"),
|
|
18
|
+
"SSS": ("cmn.millisecond", "Millisecond"),
|
|
19
|
+
"literal": ("cmn.literal", "Text"),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@define
|
|
24
|
+
class Bit:
|
|
25
|
+
"""Represents one component of a date/time value.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
start: The 0-based start index of the component in the date string.
|
|
29
|
+
size: The size of the component in the date string.
|
|
30
|
+
pattern: The string used to detect this component in the format string.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
start: int = field(default=0)
|
|
34
|
+
size: int = field(default=0, init=False)
|
|
35
|
+
pattern: str = field(default="", init=False)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def end(self) -> int:
|
|
39
|
+
"""Returns the end index of the component."""
|
|
40
|
+
return self.start + self.size
|
|
41
|
+
|
|
42
|
+
def set_part(self, value: T, part: str) -> T:
|
|
43
|
+
"""Modifies the value with the bit from the part."""
|
|
44
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
45
|
+
|
|
46
|
+
def get_part(self, value: T) -> str:
|
|
47
|
+
"""Returns the value of the component from the given value."""
|
|
48
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
49
|
+
|
|
50
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
51
|
+
"""Applies an offset to the component value."""
|
|
52
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
53
|
+
|
|
54
|
+
def validate(self, value: str) -> bool:
|
|
55
|
+
"""Validates the value of the component."""
|
|
56
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@define
|
|
60
|
+
class YearBit(Bit):
|
|
61
|
+
"""Represents the year component of a date."""
|
|
62
|
+
|
|
63
|
+
size: int = field(default=4, init=False)
|
|
64
|
+
pattern: str = field(default="YYYY", init=False)
|
|
65
|
+
|
|
66
|
+
def set_part(self, value: T, part: str) -> T:
|
|
67
|
+
return value.replace(year=int(part))
|
|
68
|
+
|
|
69
|
+
def get_part(self, value: T) -> str:
|
|
70
|
+
return str(value.year)
|
|
71
|
+
|
|
72
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
73
|
+
return value.replace(year=value.year + offset)
|
|
74
|
+
|
|
75
|
+
def validate(self, value: str) -> bool:
|
|
76
|
+
return (
|
|
77
|
+
len(value) == self.size and value.isdigit() and 1900 <= int(value) <= 2100
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@define
|
|
82
|
+
class MonthBit(Bit):
|
|
83
|
+
"""Represents the month component of a date."""
|
|
84
|
+
|
|
85
|
+
size: int = field(default=2, init=False)
|
|
86
|
+
pattern: str = field(default="MM", init=False)
|
|
87
|
+
|
|
88
|
+
def set_part(self, value: T, part: str) -> T:
|
|
89
|
+
month = int(part)
|
|
90
|
+
if month < 1 or month > 12:
|
|
91
|
+
raise ValueError("Month must be between 1 and 12.")
|
|
92
|
+
try:
|
|
93
|
+
return value.replace(month=month)
|
|
94
|
+
except ValueError:
|
|
95
|
+
# Handle the case where the day is invalid for the new month
|
|
96
|
+
return value.replace(month=month, day=1)
|
|
97
|
+
|
|
98
|
+
def get_part(self, value: T) -> str:
|
|
99
|
+
return str(value.month).zfill(2)
|
|
100
|
+
|
|
101
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
102
|
+
return value + relativedelta(months=offset)
|
|
103
|
+
|
|
104
|
+
def validate(self, value: str) -> bool:
|
|
105
|
+
return len(value) == self.size and value.isdigit() and 1 <= int(value) <= 12
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@define
|
|
109
|
+
class DayBit(Bit):
|
|
110
|
+
"""Represents the day component of a date."""
|
|
111
|
+
|
|
112
|
+
size: int = field(default=2, init=False)
|
|
113
|
+
pattern: str = field(default="DD", init=False)
|
|
114
|
+
|
|
115
|
+
def set_part(self, value: T, part: str) -> T:
|
|
116
|
+
day = int(part)
|
|
117
|
+
if day < 1 or day > 31:
|
|
118
|
+
raise ValueError("Day must be between 1 and 31.")
|
|
119
|
+
return value.replace(day=day)
|
|
120
|
+
|
|
121
|
+
def get_part(self, value: T) -> str:
|
|
122
|
+
return str(value.day).zfill(2)
|
|
123
|
+
|
|
124
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
125
|
+
return value + relativedelta(days=offset)
|
|
126
|
+
|
|
127
|
+
def validate(self, value: str) -> bool:
|
|
128
|
+
return len(value) == self.size and value.isdigit() and 1 <= int(value) <= 31
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@define
|
|
132
|
+
class HourBit(Bit):
|
|
133
|
+
"""Represents the hour component of a date."""
|
|
134
|
+
|
|
135
|
+
size: int = field(default=2, init=False)
|
|
136
|
+
pattern: str = field(default="HH", init=False)
|
|
137
|
+
|
|
138
|
+
def set_part(self, value: T, part: str) -> T:
|
|
139
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
140
|
+
raise ValueError("HourBit requires a datetime value.")
|
|
141
|
+
hour = int(part)
|
|
142
|
+
if hour < 0 or hour > 23:
|
|
143
|
+
raise ValueError("Hour must be between 0 and 23.")
|
|
144
|
+
if isinstance(value, datetime):
|
|
145
|
+
return value.replace(hour=hour)
|
|
146
|
+
return time(
|
|
147
|
+
hour=hour,
|
|
148
|
+
minute=value.minute,
|
|
149
|
+
second=value.second,
|
|
150
|
+
microsecond=value.microsecond,
|
|
151
|
+
) # type: ignore[return-value]
|
|
152
|
+
|
|
153
|
+
def get_part(self, value: T) -> str:
|
|
154
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
155
|
+
raise ValueError("HourBit requires a datetime value.")
|
|
156
|
+
return str(value.hour).zfill(2)
|
|
157
|
+
|
|
158
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
159
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
160
|
+
raise ValueError("HourBit requires a datetime value.")
|
|
161
|
+
return value + relativedelta(hours=offset)
|
|
162
|
+
|
|
163
|
+
def validate(self, value: str) -> bool:
|
|
164
|
+
return len(value) == self.size and value.isdigit() and 0 <= int(value) <= 23
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@define
|
|
168
|
+
class MinuteBit(Bit):
|
|
169
|
+
"""Represents the minute component of a date."""
|
|
170
|
+
|
|
171
|
+
size: int = field(default=2, init=False)
|
|
172
|
+
pattern: str = field(default="mm", init=False)
|
|
173
|
+
|
|
174
|
+
def set_part(self, value: T, part: str) -> T:
|
|
175
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
176
|
+
raise ValueError("MinuteBit requires a datetime value.")
|
|
177
|
+
minute = int(part)
|
|
178
|
+
if minute < 0 or minute > 59:
|
|
179
|
+
raise ValueError("Minute must be between 0 and 59.")
|
|
180
|
+
if isinstance(value, datetime):
|
|
181
|
+
return value.replace(minute=minute)
|
|
182
|
+
return time(
|
|
183
|
+
hour=value.hour,
|
|
184
|
+
minute=minute,
|
|
185
|
+
second=value.second,
|
|
186
|
+
microsecond=value.microsecond,
|
|
187
|
+
) # type: ignore[return-value]
|
|
188
|
+
|
|
189
|
+
def get_part(self, value: T) -> str:
|
|
190
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
191
|
+
raise ValueError("MinuteBit requires a datetime value.")
|
|
192
|
+
return str(value.minute).zfill(2)
|
|
193
|
+
|
|
194
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
195
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
196
|
+
raise ValueError("MinuteBit requires a datetime value.")
|
|
197
|
+
return value + relativedelta(minutes=offset)
|
|
198
|
+
|
|
199
|
+
def validate(self, value: str) -> bool:
|
|
200
|
+
return len(value) == self.size and value.isdigit() and 0 <= int(value) <= 59
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@define
|
|
204
|
+
class SecondBit(Bit):
|
|
205
|
+
"""Represents the second component of a date."""
|
|
206
|
+
|
|
207
|
+
size: int = field(default=2, init=False)
|
|
208
|
+
pattern: str = field(default="ss", init=False)
|
|
209
|
+
|
|
210
|
+
def set_part(self, value: T, part: str) -> T:
|
|
211
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
212
|
+
raise ValueError("SecondBit requires a datetime value.")
|
|
213
|
+
second = int(part)
|
|
214
|
+
if second < 0 or second > 59:
|
|
215
|
+
raise ValueError("Second must be between 0 and 59.")
|
|
216
|
+
if isinstance(value, datetime):
|
|
217
|
+
return value.replace(second=second)
|
|
218
|
+
return time(
|
|
219
|
+
hour=value.hour,
|
|
220
|
+
minute=value.minute,
|
|
221
|
+
second=second,
|
|
222
|
+
microsecond=value.microsecond,
|
|
223
|
+
) # type: ignore[return-value]
|
|
224
|
+
|
|
225
|
+
def get_part(self, value: T) -> str:
|
|
226
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
227
|
+
raise ValueError("SecondBit requires a datetime value.")
|
|
228
|
+
return str(value.second).zfill(2)
|
|
229
|
+
|
|
230
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
231
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
232
|
+
raise ValueError("SecondBit requires a datetime value.")
|
|
233
|
+
return value + relativedelta(seconds=offset)
|
|
234
|
+
|
|
235
|
+
def validate(self, value: str) -> bool:
|
|
236
|
+
return len(value) == self.size and value.isdigit() and 0 <= int(value) <= 59
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@define
|
|
240
|
+
class MillisecondBit(Bit):
|
|
241
|
+
"""Represents the millisecond component of a date."""
|
|
242
|
+
|
|
243
|
+
size: int = field(default=3, init=False)
|
|
244
|
+
pattern: str = field(default="SSS", init=False)
|
|
245
|
+
|
|
246
|
+
def set_part(self, value: T, part: str) -> T:
|
|
247
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
248
|
+
raise ValueError("MillisecondBit requires a datetime value.")
|
|
249
|
+
millisecond = int(part)
|
|
250
|
+
if millisecond < 0 or millisecond > 999:
|
|
251
|
+
raise ValueError("Millisecond must be between 0 and 999.")
|
|
252
|
+
if isinstance(value, datetime):
|
|
253
|
+
return value.replace(microsecond=millisecond * 1000)
|
|
254
|
+
return time(
|
|
255
|
+
hour=value.hour,
|
|
256
|
+
minute=value.minute,
|
|
257
|
+
second=value.second,
|
|
258
|
+
microsecond=millisecond * 1000,
|
|
259
|
+
) # type: ignore[return-value]
|
|
260
|
+
|
|
261
|
+
def get_part(self, value: T) -> str:
|
|
262
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
263
|
+
raise ValueError("MillisecondBit requires a datetime value.")
|
|
264
|
+
return str(value.microsecond // 1000).zfill(3)
|
|
265
|
+
|
|
266
|
+
def apply_offset(self, value: T, offset: int) -> T:
|
|
267
|
+
if not isinstance(value, datetime) and not isinstance(value, time):
|
|
268
|
+
raise ValueError("MillisecondBit requires a datetime value.")
|
|
269
|
+
return value + relativedelta(microseconds=offset * 1000)
|
|
270
|
+
|
|
271
|
+
def validate(self, value: str) -> bool:
|
|
272
|
+
return len(value) == self.size and value.isdigit() and 0 <= int(value) <= 999
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@define
|
|
276
|
+
class LiteralBit(Bit):
|
|
277
|
+
"""Represents a literal component of a date."""
|
|
278
|
+
|
|
279
|
+
value: str = field(default="")
|
|
280
|
+
|
|
281
|
+
size: int = field(init=False)
|
|
282
|
+
|
|
283
|
+
def __attrs_post_init__(self):
|
|
284
|
+
"""Sets the size of the literal component."""
|
|
285
|
+
self.size = len(self.value)
|
|
286
|
+
|
|
287
|
+
def set_part(self, value: T, part: str) -> T:
|
|
288
|
+
return value
|
|
289
|
+
|
|
290
|
+
def get_part(self, value: T) -> str:
|
|
291
|
+
return self.value
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@define
|
|
295
|
+
class MomentFormat:
|
|
296
|
+
"""Represents a format string for date/time values.
|
|
297
|
+
|
|
298
|
+
The format string is parsed into components, each of which represents a
|
|
299
|
+
part of the date/time value. The components can be literals or specific
|
|
300
|
+
date/time parts (year, month, day, hour, minute, second, millisecond).
|
|
301
|
+
|
|
302
|
+
Each bit stores the position in the string where it starts and the size of
|
|
303
|
+
the component. The `length` property returns the total length of the string.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
components: List[Bit] = field(factory=list)
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def length(self) -> int:
|
|
310
|
+
"""Returns the total length of the format string."""
|
|
311
|
+
return sum(component.size for component in self.components)
|
|
312
|
+
|
|
313
|
+
def _load_moment(self, value: str, result: T) -> T:
|
|
314
|
+
for component in self.components:
|
|
315
|
+
content = value[component.start : component.end] # noqa: E203
|
|
316
|
+
if not content:
|
|
317
|
+
raise ValueError(f"Invalid date format: {value}")
|
|
318
|
+
if isinstance(component, LiteralBit):
|
|
319
|
+
if content != component.value:
|
|
320
|
+
raise ValueError(f"Invalid date format: {value}")
|
|
321
|
+
else:
|
|
322
|
+
result = component.set_part(result, content)
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
def string_to_date(self, value: str) -> date:
|
|
326
|
+
"""Parse a string into a date value using the format string.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
value: The string to parse.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
A date object representing the parsed date.
|
|
333
|
+
|
|
334
|
+
Throws:
|
|
335
|
+
ValueError: If the string cannot be parsed into a date.
|
|
336
|
+
"""
|
|
337
|
+
return self._load_moment(value, date.today())
|
|
338
|
+
|
|
339
|
+
def string_to_datetime(self, value: str) -> datetime:
|
|
340
|
+
"""Parse a string into a date-time value using the format string.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
value: The string to parse.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
A datetime object representing the parsed date-time.
|
|
347
|
+
|
|
348
|
+
Throws:
|
|
349
|
+
ValueError: If the string cannot be parsed into a date-time.
|
|
350
|
+
"""
|
|
351
|
+
return self._load_moment(value, datetime.now())
|
|
352
|
+
|
|
353
|
+
def moment_to_string(self, value: T) -> str:
|
|
354
|
+
"""Converts a date/time value to a string using the format string."""
|
|
355
|
+
result = ""
|
|
356
|
+
for component in self.components:
|
|
357
|
+
result += component.get_part(value)
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
def bit_at_position(self, position: int, inclusive: bool = False) -> Bit:
|
|
361
|
+
"""Returns the component at the given position.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
position: The 0-based position in the format string.
|
|
365
|
+
"""
|
|
366
|
+
if inclusive:
|
|
367
|
+
for component in self.components:
|
|
368
|
+
if position <= component.end:
|
|
369
|
+
return component
|
|
370
|
+
else:
|
|
371
|
+
for component in self.components:
|
|
372
|
+
if position < component.end:
|
|
373
|
+
return component
|
|
374
|
+
raise ValueError(f"No component found at position {position}.")
|
|
375
|
+
|
|
376
|
+
def apply_offset(self, value: T, position: int, offset: int) -> T:
|
|
377
|
+
"""Applies an offset to the component at the given position.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
value: The date or date/time value to modify.
|
|
381
|
+
position: The 0-based position of the bit in the format string.
|
|
382
|
+
offset: The offset to apply to the component.
|
|
383
|
+
"""
|
|
384
|
+
component = self.bit_at_position(position, inclusive=True)
|
|
385
|
+
if isinstance(component, LiteralBit):
|
|
386
|
+
return value
|
|
387
|
+
return component.apply_offset(value, offset)
|
|
388
|
+
|
|
389
|
+
def validate(self, value: str, t: Any) -> ValidationResult:
|
|
390
|
+
result = ValidationResult(reason="FORMAT", value=datetime.now())
|
|
391
|
+
expected_size = 0
|
|
392
|
+
for component in self.components:
|
|
393
|
+
expected_size += component.size
|
|
394
|
+
content = value[component.start : component.end] # noqa: E203
|
|
395
|
+
if isinstance(component, LiteralBit):
|
|
396
|
+
if content == component.value:
|
|
397
|
+
continue
|
|
398
|
+
result.error = t(
|
|
399
|
+
"cmn.err.date.str",
|
|
400
|
+
"Expecting '{expect}' ({size}) at position {pos} but got `{found}`",
|
|
401
|
+
expect=component.value,
|
|
402
|
+
pos=component.start,
|
|
403
|
+
found=content,
|
|
404
|
+
size=component.size,
|
|
405
|
+
)
|
|
406
|
+
return result
|
|
407
|
+
|
|
408
|
+
if component.validate(content):
|
|
409
|
+
cur_dt = cast(datetime, result.value)
|
|
410
|
+
cur_dt = component.set_part(cur_dt, content)
|
|
411
|
+
result.value = cur_dt
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
trk, def_lbl = labels[component.pattern]
|
|
415
|
+
result.error = t(
|
|
416
|
+
"cmn.err.date.int",
|
|
417
|
+
"Expecting <{expect}> ({size}) at position {pos} but got `{found}`",
|
|
418
|
+
expect=t(trk, def_lbl),
|
|
419
|
+
pos=component.start,
|
|
420
|
+
found=content,
|
|
421
|
+
size=component.size,
|
|
422
|
+
)
|
|
423
|
+
return result
|
|
424
|
+
|
|
425
|
+
if len(value) != expected_size:
|
|
426
|
+
result.error = t(
|
|
427
|
+
"cmn.err.date.size",
|
|
428
|
+
"Extra characters at the end of the string: {extra}",
|
|
429
|
+
extra=value[expected_size:],
|
|
430
|
+
)
|
|
431
|
+
return result
|
|
432
|
+
return result
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def from_string(cls, fmt: str, **kwargs) -> "MomentFormat":
|
|
436
|
+
"""Parses a format string and returns a MomentFormat object.
|
|
437
|
+
|
|
438
|
+
The function accepts the following components:
|
|
439
|
+
|
|
440
|
+
- YYYY: Year (4 digits)
|
|
441
|
+
- MM: Month (2 digits)
|
|
442
|
+
- DD: Day (2 digits)
|
|
443
|
+
- HH: Hour (2 digits, 24-hour format)
|
|
444
|
+
- mm: Minute (2 digits)
|
|
445
|
+
- ss: Second (2 digits)
|
|
446
|
+
- SSS: Millisecond (3 digits)
|
|
447
|
+
|
|
448
|
+
Anything else is treated as a literal string.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
fmt: The format string to parse.
|
|
452
|
+
"""
|
|
453
|
+
result = cls(**kwargs)
|
|
454
|
+
|
|
455
|
+
accumulator = ""
|
|
456
|
+
|
|
457
|
+
def add_component(component: Bit) -> None:
|
|
458
|
+
"""Adds a component to the result."""
|
|
459
|
+
nonlocal accumulator
|
|
460
|
+
|
|
461
|
+
if len(accumulator) > 0:
|
|
462
|
+
result.components.append(
|
|
463
|
+
LiteralBit(start=result.length, value=accumulator)
|
|
464
|
+
)
|
|
465
|
+
accumulator = ""
|
|
466
|
+
|
|
467
|
+
component.start = result.length
|
|
468
|
+
result.components.append(component)
|
|
469
|
+
|
|
470
|
+
while fmt:
|
|
471
|
+
if fmt.startswith("YYYY"):
|
|
472
|
+
add_component(YearBit())
|
|
473
|
+
fmt = fmt[4:]
|
|
474
|
+
elif fmt.startswith("MM"):
|
|
475
|
+
add_component(MonthBit())
|
|
476
|
+
fmt = fmt[2:]
|
|
477
|
+
elif fmt.startswith("DD"):
|
|
478
|
+
add_component(DayBit())
|
|
479
|
+
fmt = fmt[2:]
|
|
480
|
+
elif fmt.startswith("HH"):
|
|
481
|
+
add_component(HourBit())
|
|
482
|
+
fmt = fmt[2:]
|
|
483
|
+
elif fmt.startswith("mm"):
|
|
484
|
+
add_component(MinuteBit())
|
|
485
|
+
fmt = fmt[2:]
|
|
486
|
+
elif fmt.startswith("ss"):
|
|
487
|
+
add_component(SecondBit())
|
|
488
|
+
fmt = fmt[2:]
|
|
489
|
+
elif fmt.startswith("SSS"):
|
|
490
|
+
add_component(MillisecondBit())
|
|
491
|
+
fmt = fmt[3:]
|
|
492
|
+
else:
|
|
493
|
+
accumulator += fmt[0]
|
|
494
|
+
fmt = fmt[1:]
|
|
495
|
+
|
|
496
|
+
return result
|
exdrf/py.typed
ADDED
|
File without changes
|
exdrf/py_support.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from importlib import import_module
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_symbol_from_path(path: str) -> object:
|
|
5
|
+
"""Given a `module.path:name`, load the python module and return the symbol.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
path: The module path and symbol name, e.g. `module.path:name`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
if ":" in path:
|
|
12
|
+
module_path, symbol = path.split(":")
|
|
13
|
+
else:
|
|
14
|
+
module_path = path
|
|
15
|
+
symbol = None
|
|
16
|
+
|
|
17
|
+
module = import_module(module_path)
|
|
18
|
+
|
|
19
|
+
if symbol is not None:
|
|
20
|
+
return getattr(module, symbol)
|
|
21
|
+
return module
|