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.
Files changed (57) hide show
  1. exdrf/__init__.py +0 -0
  2. exdrf/__version__.py +24 -0
  3. exdrf/api.py +51 -0
  4. exdrf/constants.py +30 -0
  5. exdrf/dataset.py +197 -0
  6. exdrf/field.py +554 -0
  7. exdrf/field_types/__init__.py +0 -0
  8. exdrf/field_types/api.py +78 -0
  9. exdrf/field_types/blob_field.py +44 -0
  10. exdrf/field_types/bool_field.py +47 -0
  11. exdrf/field_types/date_field.py +49 -0
  12. exdrf/field_types/date_time.py +52 -0
  13. exdrf/field_types/dur_field.py +44 -0
  14. exdrf/field_types/enum_field.py +41 -0
  15. exdrf/field_types/filter_field.py +11 -0
  16. exdrf/field_types/float_field.py +85 -0
  17. exdrf/field_types/float_list.py +18 -0
  18. exdrf/field_types/formatted.py +39 -0
  19. exdrf/field_types/int_field.py +70 -0
  20. exdrf/field_types/int_list.py +18 -0
  21. exdrf/field_types/ref_base.py +105 -0
  22. exdrf/field_types/ref_m2m.py +39 -0
  23. exdrf/field_types/ref_m2o.py +23 -0
  24. exdrf/field_types/ref_o2m.py +36 -0
  25. exdrf/field_types/ref_o2o.py +32 -0
  26. exdrf/field_types/sort_field.py +18 -0
  27. exdrf/field_types/str_field.py +77 -0
  28. exdrf/field_types/str_list.py +18 -0
  29. exdrf/field_types/time_field.py +49 -0
  30. exdrf/filter.py +653 -0
  31. exdrf/filter_dsl.py +950 -0
  32. exdrf/filter_op_catalog.py +222 -0
  33. exdrf/label_dsl.py +691 -0
  34. exdrf/moment.py +496 -0
  35. exdrf/py.typed +0 -0
  36. exdrf/py_support.py +21 -0
  37. exdrf/resource.py +901 -0
  38. exdrf/sa_fi_item.py +69 -0
  39. exdrf/sa_filter_op.py +324 -0
  40. exdrf/utils.py +17 -0
  41. exdrf/validator.py +45 -0
  42. exdrf/var_bag.py +328 -0
  43. exdrf/visitor.py +58 -0
  44. exdrf-0.0.1.dev0.dist-info/METADATA +42 -0
  45. exdrf-0.0.1.dev0.dist-info/RECORD +57 -0
  46. exdrf-0.0.1.dev0.dist-info/WHEEL +5 -0
  47. exdrf-0.0.1.dev0.dist-info/top_level.txt +3 -0
  48. exdrf_tests/__init__.py +0 -0
  49. exdrf_tests/test_dataset.py +422 -0
  50. exdrf_tests/test_field.py +109 -0
  51. exdrf_tests/test_filter.py +425 -0
  52. exdrf_tests/test_filter_dsl.py +556 -0
  53. exdrf_tests/test_label_dsl.py +234 -0
  54. exdrf_tests/test_resource.py +107 -0
  55. exdrf_tests/test_utils.py +43 -0
  56. exdrf_tests/test_visitor.py +31 -0
  57. 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