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/field.py ADDED
@@ -0,0 +1,554 @@
1
+ import enum
2
+ from datetime import datetime
3
+ from typing import TYPE_CHECKING, Any, List, Optional, Tuple
4
+
5
+ from attrs import define, field
6
+ from pydantic import BaseModel
7
+
8
+ from exdrf.constants import (
9
+ FIELD_TYPE_BOOL,
10
+ FIELD_TYPE_DATE,
11
+ FIELD_TYPE_DT,
12
+ FIELD_TYPE_FLOAT,
13
+ FIELD_TYPE_FLOAT_LIST,
14
+ FIELD_TYPE_INT_LIST,
15
+ FIELD_TYPE_INTEGER,
16
+ FIELD_TYPE_REF_MANY_TO_MANY,
17
+ FIELD_TYPE_REF_MANY_TO_ONE,
18
+ FIELD_TYPE_REF_ONE_TO_MANY,
19
+ FIELD_TYPE_REF_ONE_TO_ONE,
20
+ FIELD_TYPE_STRING,
21
+ FIELD_TYPE_STRING_LIST,
22
+ )
23
+ from exdrf.utils import doc_lines, inflect_e
24
+
25
+ if TYPE_CHECKING:
26
+ from exdrf.dataset import ExDataset # noqa: F401
27
+ from exdrf.resource import ExResource # noqa: F401
28
+ from exdrf.visitor import ExVisitor # noqa: F401
29
+
30
+ NO_DIACRITICS = "no_diacritics"
31
+
32
+
33
+ @define
34
+ class ExFieldBase:
35
+ """The minimal set of attributes for a field.
36
+
37
+ Attributes:
38
+ name: The name of the field inside the resource. This is expected to be
39
+ in snake_case.
40
+ title: A string suitable to be used as a title for the field.
41
+ description: A longer description of the field.
42
+ category: The category of the field. This should be a short
43
+ string; nested categories using the dot notation are not supported.
44
+ type_name: The unique type name of the field.
45
+ nullable: Whether the field is nullable.
46
+ """
47
+
48
+ name: str = field(default="")
49
+ title: str = field(default="")
50
+ description: str = field(default="")
51
+ category: str = field(default="")
52
+ type_name: str = field(default="")
53
+ nullable: bool = field(default=True)
54
+
55
+
56
+ @define
57
+ class ExField(ExFieldBase):
58
+ """A class representing a field in a resource.
59
+
60
+ Attributes:
61
+ name: The name of the field inside the resource. This is expected to be
62
+ in snake_case.
63
+ resource: The resource that the field belongs to.
64
+ src: The source from which this field was derived. If the field
65
+ was created from SqlAlchemy, this would be the SqlAlchemy column.
66
+ title: A string suitable to be used as a title for the field.
67
+ description: A longer description of the field.
68
+ category: The category of the field. This should be a short
69
+ string; nested categories using the dot notation are not supported.
70
+ type_name: The unique type name of the field.
71
+ is_list: Whether field contains multiple items (like when there are
72
+ many-to-many relations or one-to-many relations).
73
+ primary: Whether this filed contributes to constructing the identity
74
+ of a record.
75
+ visible: Whether the field is visible to the user. You may want to
76
+ set this to `False` for password hashes or other sensitive data.
77
+ read_only: An alternative to `visible` that shows the content of the
78
+ field but does not allow the user to edit it.
79
+ nullable: Whether the field is nullable.
80
+ sortable: Whether the user can sort list results by this field.
81
+ filterable: Whether the user can filter list results by this field.
82
+ exportable: Whether the field is user exportable.
83
+ qsearch: Whether the field is part of the quick search set.
84
+ resizable: Whether the user can resize the column in the list view.
85
+ fk_to: if this field is a foreign key, this property is the field
86
+ representing the resolved resource (if this field is `parent_id`,
87
+ the fk_to is `parent`).
88
+ fk_from: if this field points to a resource, this property is the
89
+ field representing the foreign key (if this field is `parent`,
90
+ the fk_from field is `parent_id`).
91
+ derived: If the field is derived from another field, this property
92
+ holds the name of that field and the type of derivation.
93
+ For now the only supported type is NO_DIACRITICS which indicates
94
+ that the value of this field results from the text value of another
95
+ field without diacritics (unidecode is used to convert the text).
96
+ pos_hint: A hint for the position of the field in the UI. This is
97
+ used to determine the order of the fields in the UI. The value
98
+ is a string that may be used to map the field to a position in the
99
+ UI. After the sort value the string may also include
100
+ the [after:xxx] pattern and the [before:xxx] pattern, where xxx
101
+ is the name of another field in this resource.
102
+ By default the sort key used is the `category.field-name` string,
103
+ but pos_hint will replace the field-name part if provided.
104
+ """
105
+
106
+ resource: "ExResource" = field(default=None)
107
+ src: Any = field(default=None)
108
+
109
+ is_list: bool = field(default=False)
110
+ primary: bool = field(default=False)
111
+ visible: bool = field(default=True)
112
+ read_only: bool = field(default=False)
113
+ sortable: bool = field(default=True)
114
+ filterable: bool = field(default=True)
115
+ exportable: bool = field(default=True)
116
+ qsearch: bool = field(default=True)
117
+ resizable: bool = field(default=True)
118
+ fk_to: Optional["ExField"] = field(default=None)
119
+ fk_from: Optional["ExField"] = field(default=None)
120
+ derived: Optional[Tuple[str, str]] = field(default=None)
121
+ pos_hint: Optional[str] = field(default=None)
122
+
123
+ def field_properties(self, explicit: bool = False) -> dict[str, Any]:
124
+ """Get the properties of the field.
125
+
126
+ Args:
127
+ explicit: Whether to include explicit properties.
128
+ """
129
+ if explicit:
130
+ return {
131
+ "name": self.name,
132
+ "resource": self.resource.name,
133
+ "title": self.title,
134
+ "description": self.description,
135
+ "category": self.category,
136
+ "type_name": self.type_name,
137
+ "is_list": self.is_list,
138
+ "primary": self.primary,
139
+ "visible": self.visible,
140
+ "read_only": self.read_only,
141
+ "nullable": self.nullable,
142
+ "sortable": self.sortable,
143
+ "filterable": self.filterable,
144
+ "exportable": self.exportable,
145
+ "qsearch": self.qsearch,
146
+ "resizable": self.resizable,
147
+ "fk_to": self.fk_to.name if self.fk_to else None,
148
+ "fk_from": self.fk_from.name if self.fk_from else None,
149
+ "derived": self.derived,
150
+ }
151
+ else:
152
+ result: dict[str, Any] = {
153
+ "name": self.name,
154
+ "resource": self.resource.name,
155
+ "type_name": self.type_name,
156
+ "nullable": self.nullable,
157
+ }
158
+ if self.title:
159
+ result["title"] = self.title
160
+ if self.description:
161
+ result["description"] = self.description
162
+ if self.category:
163
+ result["category"] = self.category
164
+ if self.is_list:
165
+ result["is_list"] = self.is_list
166
+ if self.primary:
167
+ result["primary"] = self.primary
168
+ if not self.visible:
169
+ result["visible"] = self.visible
170
+ if not self.read_only:
171
+ result["read_only"] = self.read_only
172
+ if not self.sortable:
173
+ result["sortable"] = self.sortable
174
+ if not self.filterable:
175
+ result["filterable"] = self.filterable
176
+ if not self.exportable:
177
+ result["exportable"] = self.exportable
178
+ if not self.qsearch:
179
+ result["qsearch"] = self.qsearch
180
+ if not self.resizable:
181
+ result["resizable"] = self.resizable
182
+ if self.fk_to:
183
+ result["fk_to"] = self.fk_to.name
184
+ if self.fk_from:
185
+ result["fk_from"] = self.fk_from.name
186
+ if self.derived:
187
+ result["derived"] = self.derived
188
+ return result
189
+
190
+ def __hash__(self):
191
+ return hash(f"{self.resource.name}.{self.name}")
192
+
193
+ def __str__(self) -> str:
194
+ return self.__repr__()
195
+
196
+ def __repr__(self) -> str:
197
+ return f"{self.resource.name}.{self.name}"
198
+
199
+ @property
200
+ def pascal_case_name(self) -> str:
201
+ """Return the name of the resource in PascalCase."""
202
+ return "".join([c.title() for c in self.name.split("_")])
203
+
204
+ @property
205
+ def snake_case_name(self) -> str:
206
+ """Return the name of the resource in snake_case."""
207
+ return self.name
208
+
209
+ @property
210
+ def snake_case_name_plural(self) -> str:
211
+ """Return the name of the resource in snake_case."""
212
+ parts = self.name.split("_")
213
+ parts[-1] = inflect_e.plural(parts[-1]) # type: ignore
214
+ return "_".join(parts)
215
+
216
+ @property
217
+ def camel_case_name(self) -> str:
218
+ """Return the name of the resource in camelCase."""
219
+ return self.name[0].lower() + self.pascal_case_name[1:]
220
+
221
+ @property
222
+ def text_name(self) -> str:
223
+ """Return the name of the resource in `Text case`."""
224
+ parts = self.name.split("_")
225
+ parts[0] = parts[0].title()
226
+ return " ".join(parts)
227
+
228
+ @property
229
+ def doc_lines(self) -> List[str]:
230
+ """Get the docstring of the field as a set of lines.
231
+
232
+ Returns:
233
+ The docstring of the field as a set of lines.
234
+ """
235
+ return doc_lines(self.description)
236
+
237
+ @property
238
+ def is_ref_type(self) -> bool:
239
+ """Check if the field is a reference type.
240
+
241
+ Returns:
242
+ True if the field is a reference type, False otherwise.
243
+ """
244
+ return self.type_name in (
245
+ FIELD_TYPE_REF_ONE_TO_MANY,
246
+ FIELD_TYPE_REF_ONE_TO_ONE,
247
+ FIELD_TYPE_REF_MANY_TO_MANY,
248
+ FIELD_TYPE_REF_MANY_TO_ONE,
249
+ )
250
+
251
+ @property
252
+ def is_one_to_many_type(self) -> bool:
253
+ """Check if the field is a one-to-many type.
254
+
255
+ In this type of relation there is one item of the present resource
256
+ that is related to many items of the related resource.
257
+
258
+ It is asserted that in this case the `is_list` attribute is set to
259
+ `True`.
260
+
261
+ Returns:
262
+ True if the field is a one-to-many type, False otherwise.
263
+ """
264
+ return self.type_name == FIELD_TYPE_REF_ONE_TO_MANY
265
+
266
+ @property
267
+ def is_one_to_one_type(self) -> bool:
268
+ """Check if the field is a one-to-one type.
269
+
270
+ In this type of relation there is one item of the present resource
271
+ that is related to one item of the related resource.
272
+
273
+ It is asserted that in this case the `is_list` attribute is set to
274
+ `False`.
275
+
276
+ Returns:
277
+ True if the field is a one-to-one type, False otherwise.
278
+ """
279
+ return self.type_name == FIELD_TYPE_REF_ONE_TO_ONE
280
+
281
+ @property
282
+ def is_many_to_many_type(self) -> bool:
283
+ """Check if the field is a many-to-many type.
284
+
285
+ In this type of relation there are many items of the present
286
+ resource that are related to many items of the related resource.
287
+
288
+ It is asserted that in this case the `is_list` attribute is set to
289
+ `True`.
290
+
291
+ Returns:
292
+ True if the field is a many-to-many type, False otherwise.
293
+ """
294
+ return self.type_name == FIELD_TYPE_REF_MANY_TO_MANY
295
+
296
+ @property
297
+ def is_many_to_one_type(self) -> bool:
298
+ """Check if the field is a many-to-one type.
299
+
300
+ In this type of relation there are many items of the present
301
+ resource that are related to one item of the related resource.
302
+
303
+ It is asserted that in this case the `is_list` attribute is set to
304
+ `False`.
305
+
306
+ Returns:
307
+ True if the field is a many-to-one type, False otherwise.
308
+ """
309
+ return self.type_name == FIELD_TYPE_REF_MANY_TO_ONE
310
+
311
+ @property
312
+ def related_resource(self) -> Optional["ExResource"]:
313
+ """Get the resource that this field is related to.
314
+
315
+ Returns:
316
+ The resource that this field is related to.
317
+ """
318
+ return self.ref if hasattr(self, "ref") else None # type: ignore
319
+
320
+ @property
321
+ def is_derived(self) -> bool:
322
+ """Check if the field is derived from another field.
323
+
324
+ Returns:
325
+ True if the field is derived from another field, False otherwise.
326
+ """
327
+ return self.derived is not None
328
+
329
+ @property
330
+ def derived_from(self) -> Optional[str]:
331
+ """Get the name of the field that this field is derived from.
332
+
333
+ Returns:
334
+ The name of the field that this field is derived from.
335
+ """
336
+ return self.derived[0] if self.derived else None
337
+
338
+ @property
339
+ def derived_type(self) -> Optional[str]:
340
+ """Get the type of derivation of the field.
341
+
342
+ Returns:
343
+ The type of derivation of the field.
344
+ """
345
+ return self.derived[1] if self.derived else None
346
+
347
+ def visit(self: "ExField", visitor: "ExVisitor") -> bool:
348
+ """Visit the resource and its fields.
349
+
350
+ Args:
351
+ visitor: The visitor to use.
352
+
353
+ Returns:
354
+ bool: True if the visit should continue, False otherwise.
355
+ """
356
+ return visitor.visit_field(self) # type: ignore
357
+
358
+ def extra_ref(self, d_set: "ExDataset") -> List["ExResource"]:
359
+ """Additional dependencies of this field.
360
+
361
+ Usually only dependencies that reference other fields generate
362
+ dependencies (other resources that are used by a particular resource).
363
+
364
+ This method allows the field to specify additional dependencies that
365
+ are not automatically detected.
366
+
367
+ See `Resource.get_dependencies()` for more details.
368
+
369
+ Args:
370
+ d_set: The dataset to which the resource belongs.
371
+
372
+ Returns:
373
+ A list of resources that this field depends on.
374
+ """
375
+ return []
376
+
377
+ def value_to_str(self, value: Any) -> str:
378
+ """Convert a value of this type to a string.
379
+
380
+ Args:
381
+ value: The value to convert.
382
+
383
+ Returns:
384
+ A string representation of the value.
385
+ """
386
+ if isinstance(value, (list, tuple)):
387
+ return ", ".join(str(v) for v in value)
388
+ elif isinstance(value, dict):
389
+ return ", ".join(f"{k}: {v}" for k, v in value.items())
390
+ else:
391
+ return str(value)
392
+
393
+ def value_from_str(self, value: str) -> Any:
394
+ """Convert a string to a value of this type."""
395
+ if self.type_name == FIELD_TYPE_STRING:
396
+ return value
397
+ elif self.type_name == FIELD_TYPE_INTEGER:
398
+ return int(value)
399
+ elif self.type_name == FIELD_TYPE_FLOAT:
400
+ return float(value)
401
+ elif self.type_name == FIELD_TYPE_BOOL:
402
+ return bool(value)
403
+ elif self.type_name == FIELD_TYPE_DATE:
404
+ return datetime.strptime(value, "%Y-%m-%d").date()
405
+ elif self.type_name == FIELD_TYPE_DT:
406
+ return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S")
407
+ elif self.type_name == FIELD_TYPE_REF_ONE_TO_MANY:
408
+ if isinstance(value, (tuple, list)):
409
+ return list(value)
410
+ elif isinstance(value, str):
411
+ return value.split(",")
412
+ else:
413
+ raise ValueError(
414
+ f"Invalid value for one-to-many: {value} of type {type(value)}"
415
+ )
416
+ elif self.type_name == FIELD_TYPE_REF_MANY_TO_MANY:
417
+ if isinstance(value, (tuple, list)):
418
+ return list(value)
419
+ elif isinstance(value, str):
420
+ return value.split(",")
421
+ else:
422
+ raise ValueError(
423
+ f"Invalid value for many-to-many: {value} of type {type(value)}"
424
+ )
425
+ elif self.type_name == FIELD_TYPE_INT_LIST:
426
+ if isinstance(value, (tuple, list)):
427
+ return list(value)
428
+ elif isinstance(value, str):
429
+ return [int(v) for v in value.split(",")]
430
+ else:
431
+ raise ValueError(
432
+ f"Invalid value for int list: {value} of type {type(value)}"
433
+ )
434
+ elif self.type_name == FIELD_TYPE_FLOAT_LIST:
435
+ if isinstance(value, (tuple, list)):
436
+ return list(value)
437
+ elif isinstance(value, str):
438
+ return [float(v) for v in value.split(",")]
439
+ else:
440
+ raise ValueError(
441
+ f"Invalid value for float list: {value} of type {type(value)}"
442
+ )
443
+ elif self.type_name == FIELD_TYPE_STRING_LIST:
444
+ if isinstance(value, (tuple, list)):
445
+ return list(value)
446
+ elif isinstance(value, str):
447
+ return value.split(",")
448
+ else:
449
+ raise ValueError(
450
+ f"Invalid value for string list: {value} of type {type(value)}"
451
+ )
452
+ else:
453
+ return value
454
+
455
+
456
+ class FieldInfo(BaseModel):
457
+ """Base parser for information about a field.
458
+
459
+ We use this mechanism when the information extracted from the source of the
460
+ field needs to be supplemented with additional information.
461
+
462
+ The attributes have exactly the same names as those in the `Field` class,
463
+ so that they can be used to create a `Field` object.
464
+
465
+ Attributes:
466
+ title: A string suitable to be used as a title for the field. If
467
+ not provided the default is the name of the field capitalized
468
+ and with underscores replaced by spaces.
469
+ description: A longer description of the field.
470
+ category: The category of the field. This should be a short
471
+ string; nested categories using the dot notation are not supported.
472
+ For common cases the implementation may subclass the `Resource`
473
+ class and reimplement the `get_default_category()` method.
474
+ pos_hint: A hint for the position of the field in the UI. This is
475
+ used to determine the order of the fields in the UI. The value
476
+ is a string that may be used to map the field to a position in the
477
+ UI. After the sort value the string may also include
478
+ the [after:xxx] pattern and the [before:xxx] pattern, where xxx
479
+ is the name of another field in this resource.
480
+ By default the sort key used is the `category.field-name` string,
481
+ but pos_hint will replace the field-name part if provided.
482
+ type_name: The unique type name of the field. If provided, it overrides
483
+ the internal logic that determines the type name of the field.
484
+ It should be one of the `FIELD_TYPE_*` constants defined in the
485
+ `exdrf.constants` module.
486
+ primary: Whether this filed contributes to constructing the identity
487
+ of a record.
488
+ visible: Whether the field is visible to the user. You may want to
489
+ set this to `False` for password hashes or other sensitive data.
490
+ read_only: An alternative to `visible` that shows the content of the
491
+ field but does not allow the user to edit it.
492
+ nullable: Whether the field is nullable.
493
+ sortable: Whether the user can sort list results by this field.
494
+ filterable: Whether the user can filter list results by this field.
495
+ exportable: Whether the field is user exportable.
496
+ qsearch: Whether the field is part of the quick search set.
497
+ resizable: Whether the user can resize the column in the list view.
498
+ use_rel: When True it is a hint for the ui to use a transfer list
499
+ instead of a simple select list.
500
+ """
501
+
502
+ title: Optional[str] = None
503
+ description: Optional[str] = None
504
+ category: Optional[str] = None
505
+ pos_hint: Optional[str] = None
506
+ type_name: Optional[str] = None
507
+ primary: Optional[bool] = None
508
+ visible: Optional[bool] = None
509
+ read_only: Optional[bool] = None
510
+ nullable: Optional[bool] = None
511
+ sortable: Optional[bool] = None
512
+ filterable: Optional[bool] = None
513
+ exportable: Optional[bool] = None
514
+ qsearch: Optional[bool] = None
515
+ resizable: Optional[bool] = None
516
+ derived: Optional[Tuple[str, str]] = None
517
+ use_rel: Optional[bool] = None
518
+
519
+ @staticmethod
520
+ def validate_enum_with_type(v, value_type: type) -> List[Tuple[Any, str]]:
521
+ """Validate the enum values.
522
+
523
+ Accepts either a list of (value_type, str) tuples or an Enum class.
524
+ """
525
+ if isinstance(v, type) and issubclass(v, enum.Enum):
526
+ # Convert Enum class to list of (value, name) tuples
527
+ return [
528
+ (
529
+ value_type(member.value),
530
+ member.name.replace("_", " ").title(),
531
+ )
532
+ for member in v
533
+ ]
534
+ elif isinstance(v, list):
535
+ # Ensure all elements are (value_type, str) tuples
536
+ for item in v:
537
+ if (
538
+ not isinstance(item, tuple)
539
+ or len(item) != 2
540
+ or not isinstance(item[0], value_type)
541
+ or not isinstance(item[1], str)
542
+ ):
543
+ raise TypeError(
544
+ "Each item in enum_values must be a tuple of "
545
+ f"({value_type}, str)"
546
+ )
547
+ return v
548
+ elif v is None:
549
+ return []
550
+ else:
551
+ raise TypeError(
552
+ f"enum_values must be a list of ({value_type}, str) "
553
+ "tuples or an Enum class"
554
+ )
File without changes
@@ -0,0 +1,78 @@
1
+ from exdrf.constants import (
2
+ FIELD_TYPE_BLOB,
3
+ FIELD_TYPE_BOOL,
4
+ FIELD_TYPE_DATE,
5
+ FIELD_TYPE_DT,
6
+ FIELD_TYPE_DURATION,
7
+ FIELD_TYPE_ENUM,
8
+ FIELD_TYPE_FILTER,
9
+ FIELD_TYPE_FLOAT,
10
+ FIELD_TYPE_FLOAT_LIST,
11
+ FIELD_TYPE_FORMATTED,
12
+ FIELD_TYPE_INT_LIST,
13
+ FIELD_TYPE_INTEGER,
14
+ FIELD_TYPE_REF_MANY_TO_MANY,
15
+ FIELD_TYPE_REF_MANY_TO_ONE,
16
+ FIELD_TYPE_REF_ONE_TO_MANY,
17
+ FIELD_TYPE_REF_ONE_TO_ONE,
18
+ FIELD_TYPE_SORT,
19
+ FIELD_TYPE_STRING,
20
+ FIELD_TYPE_STRING_LIST,
21
+ FIELD_TYPE_TIME,
22
+ )
23
+ from exdrf.field_types.blob_field import BlobField, BlobInfo # noqa: F401
24
+ from exdrf.field_types.bool_field import BoolField, BoolInfo # noqa: F401
25
+ from exdrf.field_types.date_field import DateField, DateInfo # noqa: F401
26
+ from exdrf.field_types.date_time import ( # noqa: F401
27
+ DateTimeField,
28
+ DateTimeInfo,
29
+ )
30
+ from exdrf.field_types.dur_field import ( # noqa: F401
31
+ DurationField,
32
+ DurationInfo,
33
+ )
34
+ from exdrf.field_types.enum_field import EnumField, EnumInfo # noqa: F401
35
+ from exdrf.field_types.filter_field import FilterField # noqa: F401
36
+ from exdrf.field_types.float_field import FloatField, FloatInfo # noqa: F401
37
+ from exdrf.field_types.float_list import ( # noqa: F401
38
+ FloatListField,
39
+ FloatListInfo,
40
+ )
41
+ from exdrf.field_types.formatted import ( # noqa: F401
42
+ FormattedField,
43
+ FormattedInfo,
44
+ )
45
+ from exdrf.field_types.int_field import IntField, IntInfo # noqa: F401
46
+ from exdrf.field_types.int_list import IntListField, IntListInfo # noqa: F401
47
+ from exdrf.field_types.ref_base import RefBaseField, RelExtraInfo # noqa: F401
48
+ from exdrf.field_types.ref_m2m import RefManyToManyField # noqa: F401
49
+ from exdrf.field_types.ref_m2o import RefManyToOneField # noqa: F401
50
+ from exdrf.field_types.ref_o2m import RefOneToManyField # noqa: F401
51
+ from exdrf.field_types.ref_o2o import RefOneToOneField # noqa: F401
52
+ from exdrf.field_types.sort_field import SortField # noqa: F401
53
+ from exdrf.field_types.str_field import StrField, StrInfo # noqa: F401
54
+ from exdrf.field_types.str_list import StrListField, StrListInfo # noqa: F401
55
+ from exdrf.field_types.time_field import TimeField, TimeInfo # noqa: F401
56
+
57
+ field_type_to_class = {
58
+ FIELD_TYPE_BLOB: BlobField,
59
+ FIELD_TYPE_BOOL: BoolField,
60
+ FIELD_TYPE_DATE: DateField,
61
+ FIELD_TYPE_DT: DateTimeField,
62
+ FIELD_TYPE_DURATION: DurationField,
63
+ FIELD_TYPE_ENUM: EnumField,
64
+ FIELD_TYPE_FILTER: FilterField,
65
+ FIELD_TYPE_FLOAT: FloatField,
66
+ FIELD_TYPE_FLOAT_LIST: FloatListField,
67
+ FIELD_TYPE_FORMATTED: FormattedField,
68
+ FIELD_TYPE_INTEGER: IntField,
69
+ FIELD_TYPE_INT_LIST: IntListField,
70
+ FIELD_TYPE_REF_MANY_TO_MANY: RefManyToManyField,
71
+ FIELD_TYPE_REF_MANY_TO_ONE: RefManyToOneField,
72
+ FIELD_TYPE_REF_ONE_TO_MANY: RefOneToManyField,
73
+ FIELD_TYPE_REF_ONE_TO_ONE: RefOneToOneField,
74
+ FIELD_TYPE_SORT: SortField,
75
+ FIELD_TYPE_STRING: StrField,
76
+ FIELD_TYPE_STRING_LIST: StrListField,
77
+ FIELD_TYPE_TIME: TimeField,
78
+ }
@@ -0,0 +1,44 @@
1
+ from typing import Any, Optional
2
+
3
+ from attrs import define, field
4
+
5
+ from exdrf.constants import FIELD_TYPE_BLOB
6
+ from exdrf.field import ExField, FieldInfo
7
+
8
+
9
+ @define
10
+ class BlobField(ExField):
11
+ """A field that stores binary data.
12
+
13
+ The field cannot be used for filtering or sorting, and it is not usually
14
+ visible to the user.
15
+
16
+ Attributes:
17
+ mime_type: The MIME type of the data stored in the field.
18
+ """
19
+
20
+ type_name: str = field(default=FIELD_TYPE_BLOB)
21
+ visible: bool = field(default=False)
22
+ sortable: bool = field(default=False)
23
+ filterable: bool = field(default=False)
24
+
25
+ mime_type: str = field(default="")
26
+
27
+ def __repr__(self) -> str:
28
+ return f"BlobF({self.resource.name}.{self.name})"
29
+
30
+ def field_properties(self, explicit: bool = False) -> dict[str, Any]:
31
+ result = super().field_properties(explicit)
32
+ if self.mime_type or explicit:
33
+ result["mime_type"] = self.mime_type
34
+ return result
35
+
36
+
37
+ class BlobInfo(FieldInfo):
38
+ """Parser for information about a blob field.
39
+
40
+ Attributes:
41
+ mime_type: The MIME type of the data stored in the field.
42
+ """
43
+
44
+ mime_type: Optional[str] = None