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/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
|
exdrf/field_types/api.py
ADDED
|
@@ -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
|