xn-model 0.10.3.dev1__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.
- x_model/__init__.py +34 -0
- x_model/enum.py +31 -0
- x_model/field.py +95 -0
- x_model/func.py +11 -0
- x_model/model.py +330 -0
- x_model/pydantic.py +54 -0
- xn_model-0.10.3.dev1.dist-info/METADATA +43 -0
- xn_model-0.10.3.dev1.dist-info/RECORD +10 -0
- xn_model-0.10.3.dev1.dist-info/WHEEL +5 -0
- xn_model-0.10.3.dev1.dist-info/top_level.txt +1 -0
x_model/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from types import ModuleType
|
|
2
|
+
from tortoise import Tortoise, connections, ConfigurationError
|
|
3
|
+
from tortoise.backends.asyncpg import AsyncpgDBClient
|
|
4
|
+
from tortoise.exceptions import DBConnectionError
|
|
5
|
+
|
|
6
|
+
from .enum import FieldType
|
|
7
|
+
from .field import PointField, RangeField, PolygonField, CollectionField, ListField, DatetimeSecField
|
|
8
|
+
from .model import Model, TsModel, User
|
|
9
|
+
from .func import Array
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"FieldType",
|
|
13
|
+
"PointField",
|
|
14
|
+
"RangeField",
|
|
15
|
+
"PolygonField",
|
|
16
|
+
"CollectionField",
|
|
17
|
+
"ListField",
|
|
18
|
+
"DatetimeSecField",
|
|
19
|
+
"Model",
|
|
20
|
+
"TsModel",
|
|
21
|
+
"User",
|
|
22
|
+
"Array",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def init_db(dsn: str, models: ModuleType, create_tables: bool = False) -> AsyncpgDBClient | str:
|
|
27
|
+
try:
|
|
28
|
+
await Tortoise.init(db_url=dsn, modules={"models": [models]})
|
|
29
|
+
if create_tables:
|
|
30
|
+
await Tortoise.generate_schemas()
|
|
31
|
+
cn: AsyncpgDBClient = connections.get("default")
|
|
32
|
+
except (ConfigurationError, DBConnectionError) as ce:
|
|
33
|
+
return ce.args[0]
|
|
34
|
+
return cn
|
x_model/enum.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FieldType(IntEnum):
|
|
5
|
+
input = 1
|
|
6
|
+
checkbox = 2
|
|
7
|
+
select = 3
|
|
8
|
+
textarea = 4
|
|
9
|
+
collection = 5
|
|
10
|
+
list = 6
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UserStatus(IntEnum):
|
|
14
|
+
BANNED = 0
|
|
15
|
+
WAIT = 1 # waiting for approve
|
|
16
|
+
TEST = 2 # trial
|
|
17
|
+
ACTIVE = 3
|
|
18
|
+
PREMIUM = 4
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Scope(IntEnum):
|
|
22
|
+
READ = 4
|
|
23
|
+
WRITE = 2
|
|
24
|
+
ALL = 1 # not only my
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UserRole(IntEnum):
|
|
28
|
+
CLIENT = Scope.READ # 4
|
|
29
|
+
AUTHOR = Scope.WRITE # 2
|
|
30
|
+
MANAGER = Scope.READ + Scope.WRITE # 6
|
|
31
|
+
ADMIN = Scope.READ + Scope.WRITE + Scope.ALL # 7
|
x_model/field.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from asyncpg import Point, Polygon, Range, Box
|
|
5
|
+
from tortoise import Model
|
|
6
|
+
from tortoise.contrib.postgres.fields import ArrayField
|
|
7
|
+
from tortoise.fields import Field, SmallIntField, IntField, FloatField, DatetimeField
|
|
8
|
+
from tortoise.fields.base import VALUE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ListField(Field[VALUE]):
|
|
12
|
+
base_field = Field[VALUE]
|
|
13
|
+
labels: tuple
|
|
14
|
+
|
|
15
|
+
def to_python_value(self, value):
|
|
16
|
+
if value is not None and not isinstance(value, self.field_type):
|
|
17
|
+
value = self.field_type(*value)
|
|
18
|
+
self.validate(value)
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CollectionField(ListField[VALUE]):
|
|
23
|
+
labels: tuple
|
|
24
|
+
step: str = None
|
|
25
|
+
|
|
26
|
+
def __new__(cls, precision: int = 0, *args, **kwargs):
|
|
27
|
+
if precision:
|
|
28
|
+
cls.step = f"0.{'0'*(precision-1)}1"
|
|
29
|
+
cls.base_field = FloatField if precision else IntField
|
|
30
|
+
return super().__new__(cls)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RangeField(CollectionField[Range]):
|
|
34
|
+
field_type = Range
|
|
35
|
+
labels = ("from", "to")
|
|
36
|
+
|
|
37
|
+
def __new__(cls, precision: int = 0, *args, **kwargs):
|
|
38
|
+
cls.SQL_TYPE = "numrange" if precision else "int4range"
|
|
39
|
+
return super().__new__(cls)
|
|
40
|
+
|
|
41
|
+
def to_python_value(self, value: tuple):
|
|
42
|
+
if value is not None and not isinstance(value, self.field_type):
|
|
43
|
+
value = self.field_type(*[float(v) for v in value])
|
|
44
|
+
self.validate(value)
|
|
45
|
+
return (value.lower, value.upper) if value else None
|
|
46
|
+
|
|
47
|
+
def to_db_value(self, value: Any, instance: Model) -> Any:
|
|
48
|
+
if value is not None and not isinstance(value, self.field_type):
|
|
49
|
+
value = self.field_type(*value) # pylint: disable=E1102
|
|
50
|
+
self.validate(value)
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PointField(CollectionField[Point]):
|
|
55
|
+
SQL_TYPE = "POINT"
|
|
56
|
+
field_type = Point
|
|
57
|
+
base_field = FloatField
|
|
58
|
+
labels = ("lat", "lon")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PolygonField(ListField[Polygon]):
|
|
62
|
+
SQL_TYPE = "POLYGON"
|
|
63
|
+
field_type = Polygon
|
|
64
|
+
base_field = PointField
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BoxField(ListField[Box]):
|
|
68
|
+
SQL_TYPE = "BOX"
|
|
69
|
+
field_type = Box
|
|
70
|
+
base_field = PointField
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DatetimeSecField(DatetimeField):
|
|
74
|
+
class _db_postgres:
|
|
75
|
+
SQL_TYPE = "TIMESTAMPTZ(0)"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SetField(ListField[IntEnum]):
|
|
79
|
+
SQL_TYPE = "smallint[]"
|
|
80
|
+
field_type = ArrayField
|
|
81
|
+
base_field = SmallIntField
|
|
82
|
+
enum_type: type[IntEnum]
|
|
83
|
+
|
|
84
|
+
def __init__(self, enum_type: type[IntEnum], **kwargs: Any):
|
|
85
|
+
super().__init__(**kwargs)
|
|
86
|
+
self.enum_type = enum_type
|
|
87
|
+
self.field_type = enum_type
|
|
88
|
+
self.base_field = enum_type
|
|
89
|
+
|
|
90
|
+
def to_python_value(self, value):
|
|
91
|
+
for val in value:
|
|
92
|
+
if val is not None and not isinstance(val, self.enum_type):
|
|
93
|
+
val = self.enum_type(val)
|
|
94
|
+
self.validate(val)
|
|
95
|
+
return value
|
x_model/func.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pypika.terms import AggregateFunction
|
|
2
|
+
from tortoise.expressions import Function
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ArrayAgg(AggregateFunction):
|
|
6
|
+
def __init__(self, term, alias=None):
|
|
7
|
+
super(ArrayAgg, self).__init__("array_agg", term, alias=alias)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Array(Function):
|
|
11
|
+
database_func = ArrayAgg
|
x_model/model.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from passlib.context import CryptContext
|
|
3
|
+
from pydantic import create_model
|
|
4
|
+
from tortoise import Model as BaseModel
|
|
5
|
+
from tortoise.contrib.postgres.fields import ArrayField
|
|
6
|
+
from tortoise.contrib.pydantic import pydantic_model_creator, PydanticModel
|
|
7
|
+
from tortoise.fields import (
|
|
8
|
+
Field,
|
|
9
|
+
CharField,
|
|
10
|
+
IntField,
|
|
11
|
+
SmallIntField,
|
|
12
|
+
BigIntField,
|
|
13
|
+
DecimalField,
|
|
14
|
+
FloatField,
|
|
15
|
+
TextField,
|
|
16
|
+
BooleanField,
|
|
17
|
+
DatetimeField,
|
|
18
|
+
DateField,
|
|
19
|
+
TimeField,
|
|
20
|
+
JSONField,
|
|
21
|
+
ForeignKeyRelation,
|
|
22
|
+
OneToOneRelation,
|
|
23
|
+
ManyToManyRelation,
|
|
24
|
+
ForeignKeyNullableRelation,
|
|
25
|
+
OneToOneNullableRelation,
|
|
26
|
+
IntEnumField,
|
|
27
|
+
)
|
|
28
|
+
from tortoise.fields.data import IntEnumFieldInstance, CharEnumFieldInstance
|
|
29
|
+
from tortoise.fields.relational import (
|
|
30
|
+
BackwardFKRelation,
|
|
31
|
+
ForeignKeyFieldInstance,
|
|
32
|
+
ManyToManyFieldInstance,
|
|
33
|
+
OneToOneFieldInstance,
|
|
34
|
+
BackwardOneToOneRelation,
|
|
35
|
+
RelationalField,
|
|
36
|
+
)
|
|
37
|
+
from tortoise.models import MetaInfo
|
|
38
|
+
from tortoise.queryset import QuerySet
|
|
39
|
+
|
|
40
|
+
from x_model import FieldType, PointField, PolygonField, RangeField
|
|
41
|
+
from x_model.enum import UserStatus, UserRole
|
|
42
|
+
from x_model.field import DatetimeSecField, SetField
|
|
43
|
+
from x_model.pydantic import PydList
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Model(BaseModel):
|
|
47
|
+
id: int = IntField(pk=True)
|
|
48
|
+
_name: set[str] = {"name"}
|
|
49
|
+
_icon: str = "" # https://unpkg.com/@tabler/icons@2.30.0/icons/icon_name.svg
|
|
50
|
+
_sorts: list[str] = ["-id"]
|
|
51
|
+
_ownable_fields: dict[str, str | None] = {"one": None, "list": None, "in": None}
|
|
52
|
+
_pydIn: type[PydanticModel] = None
|
|
53
|
+
_pyd: type[PydanticModel] = None
|
|
54
|
+
_pydListItem: type[PydanticModel] = None
|
|
55
|
+
_permissions: tuple[bool, bool, bool] = True, True, True
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def cols(cls) -> list[dict]:
|
|
59
|
+
meta = cls._meta
|
|
60
|
+
return [
|
|
61
|
+
{"data": c, "orderable": c not in meta.fetch_fields or c in meta.fk_fields}
|
|
62
|
+
for c in meta.fields_map
|
|
63
|
+
if not c.endswith("_id")
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def pyd(cls) -> type[PydanticModel]:
|
|
68
|
+
cls._pyd = cls._pyd or pydantic_model_creator(cls)
|
|
69
|
+
return cls._pyd
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def pydIn(cls) -> type[PydanticModel]:
|
|
73
|
+
if not cls._pydIn:
|
|
74
|
+
opts = tuple(k for k, v in cls._meta.fields_map.items() if not v.required)
|
|
75
|
+
cls._pydIn = pydantic_model_creator(
|
|
76
|
+
cls,
|
|
77
|
+
name=cls.__name__ + "In",
|
|
78
|
+
meta_override=cls.PydanticMetaIn,
|
|
79
|
+
optional=opts,
|
|
80
|
+
exclude_readonly=True,
|
|
81
|
+
exclude=("created_at", "updated_at"),
|
|
82
|
+
)
|
|
83
|
+
if m2ms := cls._meta.m2m_fields: # hack for direct inserting m2m values
|
|
84
|
+
cls._pydIn = create_model(
|
|
85
|
+
cls._pydIn.__name__, __base__=cls._pydIn, **{m2m: (list[int] | None, None) for m2m in m2ms}
|
|
86
|
+
)
|
|
87
|
+
return cls._pydIn
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def pydListItem(cls) -> type[PydanticModel]:
|
|
91
|
+
if not cls._pydListItem:
|
|
92
|
+
cls._pydListItem = pydantic_model_creator(
|
|
93
|
+
cls, name=cls.__name__ + "ListItem", meta_override=cls.PydanticMetaListItem
|
|
94
|
+
)
|
|
95
|
+
return cls._pydListItem
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def pydsList(cls) -> type[PydList]:
|
|
99
|
+
return create_model(
|
|
100
|
+
cls.__name__ + "List",
|
|
101
|
+
data=(list[cls.pydListItem()], []),
|
|
102
|
+
total=(int, 0),
|
|
103
|
+
filtered=(int | None, None),
|
|
104
|
+
__base__=PydList[cls.pydListItem()],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
async def one(cls, uid: int, owner: int = None, **kwargs) -> PydanticModel:
|
|
109
|
+
if owner and (of := cls._ownable_fields.get("one")):
|
|
110
|
+
kwargs.update({of: owner})
|
|
111
|
+
q = cls.get(id=uid, **kwargs)
|
|
112
|
+
return await cls.pyd().from_queryset_single(q)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def pageQuery(
|
|
116
|
+
cls, sorts: list[str], limit: int = 1000, offset: int = 0, q: str = None, owner: int = None, **kwargs
|
|
117
|
+
) -> QuerySet:
|
|
118
|
+
rels, keys = [], ["id"]
|
|
119
|
+
for nam in cls._name:
|
|
120
|
+
parts = nam.split("__")
|
|
121
|
+
if len(parts) > 1:
|
|
122
|
+
rels.append("__".join(parts[:-1]))
|
|
123
|
+
keys.append(nam)
|
|
124
|
+
query = (
|
|
125
|
+
cls.filter(**kwargs)
|
|
126
|
+
.order_by(*sorts)
|
|
127
|
+
.limit(limit)
|
|
128
|
+
.offset(offset)
|
|
129
|
+
.prefetch_related(*(cls._meta.fetch_fields & set(kwargs)), *rels)
|
|
130
|
+
)
|
|
131
|
+
if q:
|
|
132
|
+
query = query.filter(**{f"{cls._name}__icontains": q})
|
|
133
|
+
if owner and (of := cls._ownable_fields.get("list")):
|
|
134
|
+
query = query.filter(**{of: owner})
|
|
135
|
+
return query
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
async def pagePyd(
|
|
139
|
+
cls, sorts: list[str], limit: int = 1000, offset: int = 0, q: str = None, owner: int = None, **kwargs
|
|
140
|
+
) -> PydList:
|
|
141
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
142
|
+
pyd = cls.pydListItem()
|
|
143
|
+
query = cls.pageQuery(sorts, limit, offset, q, owner, **kwargs)
|
|
144
|
+
await query
|
|
145
|
+
data = await pyd.from_queryset(query)
|
|
146
|
+
if limit - (li := len(data)):
|
|
147
|
+
filtered = total = li + offset
|
|
148
|
+
else:
|
|
149
|
+
total = await cls.all().count()
|
|
150
|
+
filtered_query = cls.filter(**kwargs)
|
|
151
|
+
if q:
|
|
152
|
+
filtered_query = filtered_query.filter(**{f"{cls._name}__icontains": q})
|
|
153
|
+
filtered = await filtered_query.count()
|
|
154
|
+
pyds = cls.pydsList()
|
|
155
|
+
return pyds(data=data, total=total, filtered=filtered)
|
|
156
|
+
|
|
157
|
+
def repr(self) -> str:
|
|
158
|
+
if self._name in self._meta.db_fields:
|
|
159
|
+
return " ".join(getattr(self, name_fragment) for name_fragment in self._name)
|
|
160
|
+
return self.__repr__()
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
async def getOrCreateByName(cls, name: str, attr_name: str = None, def_dict: dict = None) -> BaseModel:
|
|
164
|
+
attr_name = attr_name or list(cls._name)[0]
|
|
165
|
+
if not (obj := await cls.get_or_none(**{attr_name: name})):
|
|
166
|
+
next_id = (await cls.all().order_by("-id").first()).id + 1
|
|
167
|
+
obj = await cls.create(id=next_id, **{attr_name: name}, **(def_dict or {}))
|
|
168
|
+
return obj
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
async def upsert(cls, data: dict, oid=None):
|
|
172
|
+
meta: MetaInfo = cls._meta
|
|
173
|
+
|
|
174
|
+
# pop fields for relations from general data dict
|
|
175
|
+
m2ms = {k: data.pop(k) for k in meta.m2m_fields if k in data}
|
|
176
|
+
# bfks = {k: data.pop(k) for k in meta.backward_fk_fields if k in data}
|
|
177
|
+
# bo2os = {k: data.pop(k) for k in meta.backward_o2o_fields if k in data}
|
|
178
|
+
|
|
179
|
+
# save general model
|
|
180
|
+
# if pk := meta.pk_attr in data.keys():
|
|
181
|
+
# unq = {pk: data.pop(pk)}
|
|
182
|
+
# else:
|
|
183
|
+
# unq = {key: data.pop(key) for key, ft in meta.fields_map.items() if ft.unique and key in data.keys()}
|
|
184
|
+
# # unq = meta.unique_together
|
|
185
|
+
# obj, is_created = await cls.update_or_create(data, **unq)
|
|
186
|
+
obj = (await cls.update_or_create(data, **{meta.pk_attr: oid}))[0] if oid else await cls.create(**data)
|
|
187
|
+
|
|
188
|
+
# save relations
|
|
189
|
+
for k, ids in m2ms.items():
|
|
190
|
+
if ids:
|
|
191
|
+
m2m_rel: ManyToManyRelation = getattr(obj, k)
|
|
192
|
+
items = [await m2m_rel.remote_model[i] for i in ids]
|
|
193
|
+
await m2m_rel.clear() # for updating, not just adding
|
|
194
|
+
await m2m_rel.add(*items)
|
|
195
|
+
# for k, ids in bfks.items():
|
|
196
|
+
# bfk_rel: ReverseRelation = getattr(obj, k)
|
|
197
|
+
# items = [await bfk_rel.remote_model[i] for i in ids]
|
|
198
|
+
# [await item.update_from_dict({bfk_rel.relation_field: obj.pk}).save() for item in items]
|
|
199
|
+
# for k, oid in bo2os.items():
|
|
200
|
+
# bo2o_rel: QuerySet = getattr(obj, k)
|
|
201
|
+
# item = await bo2o_rel.model[oid]
|
|
202
|
+
# await item.update_from_dict({obj._meta.db_table: obj}).save()
|
|
203
|
+
|
|
204
|
+
await obj.fetch_related(*cls._meta.fetch_fields)
|
|
205
|
+
return obj
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def field_input_map(cls) -> dict:
|
|
209
|
+
def type2input(ft: type[Field]):
|
|
210
|
+
dry = {
|
|
211
|
+
"base_field": hasattr(ft, "base_field") and {**type2input(ft.base_field)},
|
|
212
|
+
"step": hasattr(ft, "step") and ft.step,
|
|
213
|
+
"labels": hasattr(ft, "labels") and ft.labels,
|
|
214
|
+
}
|
|
215
|
+
type2inputs: {Field: dict} = {
|
|
216
|
+
CharField: {"input": FieldType.input.name},
|
|
217
|
+
IntField: {"input": FieldType.input.name, "type": "number"},
|
|
218
|
+
SmallIntField: {"input": FieldType.input.name, "type": "number"},
|
|
219
|
+
BigIntField: {"input": FieldType.input.name, "type": "number"},
|
|
220
|
+
DecimalField: {"input": FieldType.input.name, "type": "number", "step": "0.01"},
|
|
221
|
+
FloatField: {"input": FieldType.input.name, "type": "number", "step": "0.001"},
|
|
222
|
+
TextField: {"input": FieldType.textarea.name, "rows": "2"},
|
|
223
|
+
BooleanField: {"input": FieldType.checkbox.name},
|
|
224
|
+
DatetimeField: {"input": FieldType.input.name, "type": "datetime"},
|
|
225
|
+
DatetimeSecField: {"input": FieldType.input.name, "type": "datetime"},
|
|
226
|
+
DateField: {"input": FieldType.input.name, "type": "date"},
|
|
227
|
+
TimeField: {"input": FieldType.input.name, "type": "time"},
|
|
228
|
+
JSONField: {"input": FieldType.input.name},
|
|
229
|
+
IntEnumFieldInstance: {"input": FieldType.select.name},
|
|
230
|
+
CharEnumFieldInstance: {"input": FieldType.select.name},
|
|
231
|
+
ForeignKeyFieldInstance: {"input": FieldType.select.name},
|
|
232
|
+
OneToOneFieldInstance: {"input": FieldType.select.name},
|
|
233
|
+
ManyToManyFieldInstance: {"input": FieldType.select.name, "multiple": True},
|
|
234
|
+
ForeignKeyRelation: {"input": FieldType.select.name, "multiple": True},
|
|
235
|
+
OneToOneRelation: {"input": FieldType.select.name},
|
|
236
|
+
BackwardOneToOneRelation: {"input": FieldType.select.name},
|
|
237
|
+
ManyToManyRelation: {"input": FieldType.select.name, "multiple": True},
|
|
238
|
+
ForeignKeyNullableRelation: {"input": FieldType.select.name, "multiple": True},
|
|
239
|
+
BackwardFKRelation: {"input": FieldType.select.name, "multiple": True},
|
|
240
|
+
ArrayField: {"input": FieldType.select.name, "multiple": True},
|
|
241
|
+
SetField: {"input": FieldType.select.name, "multiple": True},
|
|
242
|
+
OneToOneNullableRelation: {"input": FieldType.select.name},
|
|
243
|
+
PointField: {"input": FieldType.collection.name, **dry},
|
|
244
|
+
PolygonField: {"input": FieldType.list.name, **dry},
|
|
245
|
+
RangeField: {"input": FieldType.collection.name, **dry},
|
|
246
|
+
}
|
|
247
|
+
return type2inputs[ft]
|
|
248
|
+
|
|
249
|
+
def field2input(_key: str, field: Field):
|
|
250
|
+
attrs: dict = {"required": not field.null}
|
|
251
|
+
if isinstance(field, CharEnumFieldInstance):
|
|
252
|
+
attrs.update({"options": {en.name: en.value for en in field.enum_type}})
|
|
253
|
+
elif isinstance(field, IntEnumFieldInstance) or isinstance(field, SetField):
|
|
254
|
+
attrs.update({"options": {en.value: en.name.replace("_", " ") for en in field.enum_type}})
|
|
255
|
+
elif isinstance(field, RelationalField):
|
|
256
|
+
attrs.update({"source_field": field.source_field}) # 'table': attrs[key]['multiple'],
|
|
257
|
+
elif field.generated or ("auto_now" in field.__dict__ and (field.auto_now or field.auto_now_add)): # noqa
|
|
258
|
+
attrs.update({"auto": True})
|
|
259
|
+
return {**type2input(type(field)), **attrs}
|
|
260
|
+
|
|
261
|
+
return {key: field2input(key, field) for key, field in cls._meta.fields_map.items() if not key.endswith("_id")}
|
|
262
|
+
|
|
263
|
+
class Meta:
|
|
264
|
+
abstract = True
|
|
265
|
+
|
|
266
|
+
class PydanticMeta:
|
|
267
|
+
#: If not empty, only fields this property contains will be in the pydantic model
|
|
268
|
+
# include: tuple[str, ...] = ()
|
|
269
|
+
# #: Fields listed in this property will be excluded from pydantic model
|
|
270
|
+
# exclude: tuple[str, ...] = ()
|
|
271
|
+
# #: Computed fields can be listed here to use in pydantic model
|
|
272
|
+
# computed: tuple[str, ...] = ()
|
|
273
|
+
|
|
274
|
+
exclude_raw_fields = False # default: True
|
|
275
|
+
max_recursion: int = 1 # default: 3
|
|
276
|
+
|
|
277
|
+
class PydanticMetaIn:
|
|
278
|
+
max_recursion: int = 0 # default: 3
|
|
279
|
+
backward_relations: bool = False # no need to disable when max_recursion=0 # default: True
|
|
280
|
+
exclude_raw_fields: bool = False # default: True
|
|
281
|
+
|
|
282
|
+
class PydanticMetaListItem:
|
|
283
|
+
max_recursion: int = 0 # default: 3
|
|
284
|
+
backward_relations: bool = False # default: True
|
|
285
|
+
exclude_raw_fields = False # default: True
|
|
286
|
+
sort_alphabetically: bool = True # default: False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class TsModel(Model):
|
|
290
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
291
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
292
|
+
|
|
293
|
+
class Meta:
|
|
294
|
+
abstract = True
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class User(TsModel):
|
|
298
|
+
status: UserStatus = IntEnumField(UserStatus, default=UserStatus.WAIT)
|
|
299
|
+
username: str | None = CharField(95, unique=True, null=True)
|
|
300
|
+
email: str | None = CharField(100, unique=True, null=True)
|
|
301
|
+
password: str | None = CharField(60, null=True)
|
|
302
|
+
phone: int | None = BigIntField(null=True)
|
|
303
|
+
role: UserRole = IntEnumField(UserRole, default=UserRole.CLIENT)
|
|
304
|
+
|
|
305
|
+
_icon = "user"
|
|
306
|
+
_name = {"username"}
|
|
307
|
+
|
|
308
|
+
class Meta:
|
|
309
|
+
table_description = "Users"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class UserPasswordTrait(TsModel):
|
|
313
|
+
password: str | None = CharField(60, null=True)
|
|
314
|
+
|
|
315
|
+
__cc = CryptContext(schemes=["bcrypt"])
|
|
316
|
+
|
|
317
|
+
def pwd_vrf(self, pwd: str) -> bool:
|
|
318
|
+
return self.__cc.verify(pwd, self.password)
|
|
319
|
+
|
|
320
|
+
@classmethod
|
|
321
|
+
async def create(cls, using_db=None, **kwargs) -> "User":
|
|
322
|
+
user: "User" | Model = await super().create(using_db, **kwargs)
|
|
323
|
+
if pwd := kwargs.get("password"):
|
|
324
|
+
# noinspection PyUnresolvedReferences
|
|
325
|
+
await user.set_pwd(pwd)
|
|
326
|
+
return user
|
|
327
|
+
|
|
328
|
+
async def set_pwd(self, pwd: str = password) -> None:
|
|
329
|
+
self.password = self.__cc.hash(pwd)
|
|
330
|
+
await self.save()
|
x_model/pydantic.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import TypeVar, Generic
|
|
2
|
+
from pydantic import BaseModel, ConfigDict
|
|
3
|
+
|
|
4
|
+
from x_model.enum import UserStatus, UserRole
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
RootModelType = TypeVar("RootModelType")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PydList(BaseModel, Generic[RootModelType]):
|
|
11
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
12
|
+
data: list[RootModelType]
|
|
13
|
+
total: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserPwd(BaseModel):
|
|
17
|
+
password: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UserReg(UserPwd):
|
|
21
|
+
username: str
|
|
22
|
+
email: str | None = None
|
|
23
|
+
phone: int | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UserUpdate(BaseModel):
|
|
27
|
+
username: str
|
|
28
|
+
status: UserStatus
|
|
29
|
+
email: str | None
|
|
30
|
+
phone: int | None
|
|
31
|
+
role: UserRole
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UserAuth(UserUpdate):
|
|
35
|
+
id: int
|
|
36
|
+
username: str
|
|
37
|
+
status: UserStatus
|
|
38
|
+
role: UserRole
|
|
39
|
+
# ref_id: int | None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Names(BaseModel):
|
|
43
|
+
# models for name endpoint for select2 inputs
|
|
44
|
+
class Name(BaseModel):
|
|
45
|
+
id: int
|
|
46
|
+
text: str
|
|
47
|
+
logo: str | None = None
|
|
48
|
+
selected: bool | None = None
|
|
49
|
+
|
|
50
|
+
class Pagination(BaseModel):
|
|
51
|
+
more: bool
|
|
52
|
+
|
|
53
|
+
results: list[Name]
|
|
54
|
+
pagination: Pagination
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: xn-model
|
|
3
|
+
Version: 0.10.3.dev1
|
|
4
|
+
Summary: Base model for xn-api
|
|
5
|
+
Author-email: Mike Artemiev <mixartemev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/XyncNet/x-model
|
|
8
|
+
Project-URL: Repository, https://github.com/XyncNet/x-model
|
|
9
|
+
Keywords: tortoise,model,crud,generator,api,admin
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: tortoise-orm[accel,asyncpg]
|
|
13
|
+
Requires-Dist: passlib[bcrypt]
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Requires-Dist: python-dotenv
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
18
|
+
Requires-Dist: build ; extra == 'dev'
|
|
19
|
+
Requires-Dist: twine ; extra == 'dev'
|
|
20
|
+
Requires-Dist: setuptools-scm ; extra == 'dev'
|
|
21
|
+
|
|
22
|
+
## INSTALL
|
|
23
|
+
```bash
|
|
24
|
+
# Create python virtual environment
|
|
25
|
+
python3 -m venv venv
|
|
26
|
+
# Activate this environment
|
|
27
|
+
source venv/bin/activate
|
|
28
|
+
# Install dependencies
|
|
29
|
+
pip install .
|
|
30
|
+
|
|
31
|
+
# Create pg db
|
|
32
|
+
createdb --U username -W dbname
|
|
33
|
+
## set password for db user
|
|
34
|
+
|
|
35
|
+
# Copy .env file from sample template
|
|
36
|
+
cp .env.sample .env
|
|
37
|
+
## set your pg creds in .env file
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## TEST
|
|
41
|
+
```bash
|
|
42
|
+
pytest
|
|
43
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
x_model/__init__.py,sha256=LrDAUX7IsIS0HvdPAyefqzNknvnn3-3XVk-UDLBukwA,1007
|
|
2
|
+
x_model/enum.py,sha256=yrTIzMVnqG-kAD4ifhJhMSnYI-DIDLkB236w-Ol2I-M,543
|
|
3
|
+
x_model/field.py,sha256=S461M94ryQG7yu8lreXtWnZo3YdCP97xhbcCJ3BzXsY,2751
|
|
4
|
+
x_model/func.py,sha256=E7jDoHJGaFpKvxbHnT_lyBxUZeMo-GRd5gv9dLw7B9s,289
|
|
5
|
+
x_model/model.py,sha256=rzKwyk6Uu9c6hYCuVFbB4DHgYaMrGUkwIK1Zq0jCwwE,13856
|
|
6
|
+
x_model/pydantic.py,sha256=lTQ4eGNg_jsyGNLu8B1fTO-Ky8C3F057abhyAdhmVoQ,1047
|
|
7
|
+
xn_model-0.10.3.dev1.dist-info/METADATA,sha256=yQ4EovBcmvABOeff2zdqiwpxYxvSaIuMVEJKQaP7MQY,1062
|
|
8
|
+
xn_model-0.10.3.dev1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
9
|
+
xn_model-0.10.3.dev1.dist-info/top_level.txt,sha256=QCYyfv5AA_8jPPtCpShkBXzQRUCGuuW7Ro0mqysDE8E,8
|
|
10
|
+
xn_model-0.10.3.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
x_model
|