xn-model 0.10.3.dev1__tar.gz → 0.11.0__tar.gz
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.
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/.env.sample +2 -1
- {xn_model-0.10.3.dev1/xn_model.egg-info → xn_model-0.11.0}/PKG-INFO +1 -3
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/makefile +6 -6
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/pyproject.toml +0 -2
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/x_model/__init__.py +0 -19
- xn_model-0.11.0/x_model/model.py +172 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/x_model/pydantic.py +0 -28
- {xn_model-0.10.3.dev1 → xn_model-0.11.0/xn_model.egg-info}/PKG-INFO +1 -3
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/xn_model.egg-info/SOURCES.txt +0 -1
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/xn_model.egg-info/requires.txt +0 -2
- xn_model-0.10.3.dev1/x_model/enum.py +0 -31
- xn_model-0.10.3.dev1/x_model/model.py +0 -330
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/.gitignore +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/.pre-commit-config.yaml +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/README.md +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/setup.cfg +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/tests/__init__.py +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/tests/test_db.py +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/x_model/field.py +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/x_model/func.py +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/xn_model.egg-info/dependency_links.txt +0 -0
- {xn_model-0.10.3.dev1 → xn_model-0.11.0}/xn_model.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: xn-model
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Base model for xn-api
|
|
5
5
|
Author-email: Mike Artemiev <mixartemev@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -10,8 +10,6 @@ Keywords: tortoise,model,crud,generator,api,admin
|
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
Requires-Dist: tortoise-orm[accel,asyncpg]
|
|
13
|
-
Requires-Dist: passlib[bcrypt]
|
|
14
|
-
Requires-Dist: pydantic
|
|
15
13
|
Requires-Dist: python-dotenv
|
|
16
14
|
Provides-Extra: dev
|
|
17
15
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
include .env
|
|
1
2
|
PACKAGE := x_model
|
|
2
|
-
VENV := venv
|
|
3
3
|
VPYTHON := . $(VENV)/bin/activate && python
|
|
4
4
|
|
|
5
5
|
.PHONY: all install pre-commit test clean build twine patch
|
|
@@ -8,20 +8,20 @@ all:
|
|
|
8
8
|
make install test clean build
|
|
9
9
|
|
|
10
10
|
install: $(VENV)
|
|
11
|
-
$(VPYTHON) -m pip install
|
|
11
|
+
$(VPYTHON) -m pip install .[dev]; make pre-commit
|
|
12
12
|
pre-commit: .pre-commit-config.yaml
|
|
13
13
|
pre-commit install -t pre-commit -t post-commit -t pre-push
|
|
14
14
|
|
|
15
|
-
test:
|
|
15
|
+
test: $(VENV)
|
|
16
16
|
$(VPYTHON) -m pytest
|
|
17
17
|
|
|
18
18
|
clean: .pytest_cache dist $(PACKAGE).egg-info
|
|
19
19
|
rm -rf .pytest_cache dist/* $(PACKAGE).egg-info $(PACKAGE)/__pycache__ dist/__pycache__
|
|
20
20
|
|
|
21
|
-
build:
|
|
21
|
+
build: $(VENV)
|
|
22
22
|
$(VPYTHON) -m build; make twine
|
|
23
|
-
twine: dist
|
|
23
|
+
twine: $(VENV) dist
|
|
24
24
|
$(VPYTHON) -m twine upload dist/* --skip-existing
|
|
25
25
|
|
|
26
|
-
patch:
|
|
26
|
+
patch: $(VENV)
|
|
27
27
|
git tag `$(VPYTHON) -m setuptools_scm --strip-dev`; git push --tags --prune -f
|
|
@@ -3,25 +3,6 @@ from tortoise import Tortoise, connections, ConfigurationError
|
|
|
3
3
|
from tortoise.backends.asyncpg import AsyncpgDBClient
|
|
4
4
|
from tortoise.exceptions import DBConnectionError
|
|
5
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
6
|
|
|
26
7
|
async def init_db(dsn: str, models: ModuleType, create_tables: bool = False) -> AsyncpgDBClient | str:
|
|
27
8
|
try:
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pydantic import create_model
|
|
3
|
+
from tortoise import Model as TortoiseModel
|
|
4
|
+
from tortoise.contrib.pydantic import pydantic_model_creator, PydanticModel
|
|
5
|
+
from tortoise import fields
|
|
6
|
+
from tortoise.models import MetaInfo
|
|
7
|
+
from tortoise.queryset import QuerySet
|
|
8
|
+
|
|
9
|
+
from x_model.field import DatetimeSecField
|
|
10
|
+
from x_model.pydantic import PydList
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseModel(TortoiseModel):
|
|
14
|
+
# todo: resolve ownable + add only own list method
|
|
15
|
+
# todo: refact: clean old garbage
|
|
16
|
+
id: int = fields.IntField(True)
|
|
17
|
+
|
|
18
|
+
_name: tuple[str] = {"name"}
|
|
19
|
+
_sorts: tuple[str] = ["-id"]
|
|
20
|
+
|
|
21
|
+
def repr(self, sep: str = " ") -> str:
|
|
22
|
+
if self._name in self._meta.db_fields:
|
|
23
|
+
return sep.join(getattr(self, name_fragment) for name_fragment in self._name)
|
|
24
|
+
return self.__repr__()
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
async def get_or_create_by_name(cls, name: str, attr_name: str = None, def_dict: dict = None) -> TortoiseModel:
|
|
28
|
+
attr_name = attr_name or list(cls._name)[0]
|
|
29
|
+
if not (obj := await cls.get_or_none(**{attr_name: name})):
|
|
30
|
+
next_id = (await cls.all().order_by("-id").first()).id + 1
|
|
31
|
+
obj = await cls.create(id=next_id, **{attr_name: name}, **(def_dict or {}))
|
|
32
|
+
return obj
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _page_query(cls, sorts: tuple[str], limit: int = 1000, offset: int = 0, q: str = None, **filters) -> QuerySet:
|
|
36
|
+
query = cls.filter(**filters).order_by(*sorts).limit(limit).offset(offset)
|
|
37
|
+
if q:
|
|
38
|
+
query = query.filter(**{f"{cls._name}__icontains": q})
|
|
39
|
+
return query
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
async def upsert(cls, data: dict, oid=None):
|
|
43
|
+
meta: MetaInfo = cls._meta
|
|
44
|
+
|
|
45
|
+
# pop fields for relations from general data dict # todo: add backwards fields for save
|
|
46
|
+
m2ms = {k: data.pop(k) for k in meta.m2m_fields if k in data}
|
|
47
|
+
# bfks = {k: data.pop(k) for k in meta.backward_fk_fields if k in data}
|
|
48
|
+
# bo2os = {k: data.pop(k) for k in meta.backward_o2o_fields if k in data}
|
|
49
|
+
|
|
50
|
+
# save general model
|
|
51
|
+
# if pk := meta.pk_attr in data.keys():
|
|
52
|
+
# unq = {pk: data.pop(pk)}
|
|
53
|
+
# else:
|
|
54
|
+
# unq = {key: data.pop(key) for key, ft in meta.fields_map.items() if ft.unique and key in data.keys()}
|
|
55
|
+
# # unq = meta.unique_together
|
|
56
|
+
# obj, is_created = await cls.update_or_create(data, **unq)
|
|
57
|
+
obj = (await cls.update_or_create(data, id=oid))[0] if oid else await cls.create(**data)
|
|
58
|
+
|
|
59
|
+
# save relations
|
|
60
|
+
for k, ids in m2ms.items():
|
|
61
|
+
if ids:
|
|
62
|
+
m2m_rel: fields.ManyToManyRelation = getattr(obj, k)
|
|
63
|
+
items = [await m2m_rel.remote_model[i] for i in ids]
|
|
64
|
+
await m2m_rel.clear() # for updating, not just adding
|
|
65
|
+
await m2m_rel.add(*items)
|
|
66
|
+
# for k, ids in bfks.items():
|
|
67
|
+
# bfk_rel: ReverseRelation = getattr(obj, k)
|
|
68
|
+
# items = [await bfk_rel.remote_model[i] for i in ids]
|
|
69
|
+
# [await item.update_from_dict({bfk_rel.relation_field: obj.pk}).save() for item in items]
|
|
70
|
+
# for k, oid in bo2os.items():
|
|
71
|
+
# bo2o_rel: QuerySet = getattr(obj, k)
|
|
72
|
+
# item = await bo2o_rel.model[oid]
|
|
73
|
+
# await item.update_from_dict({obj._meta.db_table: obj}).save()
|
|
74
|
+
|
|
75
|
+
await obj.fetch_related(*cls._meta.fetch_fields)
|
|
76
|
+
return obj
|
|
77
|
+
|
|
78
|
+
class Meta:
|
|
79
|
+
abstract = True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Model(BaseModel):
|
|
83
|
+
_pyd: type[PydanticModel] = None
|
|
84
|
+
_pydIn: type[PydanticModel] = None
|
|
85
|
+
_pydListItem: type[PydanticModel] = None
|
|
86
|
+
|
|
87
|
+
class PydanticMeta:
|
|
88
|
+
# include: tuple[str, ...] = ()
|
|
89
|
+
# exclude: tuple[str, ...] = ()
|
|
90
|
+
# computed: tuple[str, ...] = ()
|
|
91
|
+
exclude_raw_fields = False # default: True
|
|
92
|
+
max_recursion: int = 1 # default: 3
|
|
93
|
+
|
|
94
|
+
class PydanticMetaIn:
|
|
95
|
+
max_recursion: int = 0 # default: 3
|
|
96
|
+
backward_relations: bool = False # no need to disable when max_recursion=0 # default: True
|
|
97
|
+
exclude_raw_fields: bool = False # default: True
|
|
98
|
+
|
|
99
|
+
class PydanticMetaListItem:
|
|
100
|
+
max_recursion: int = 0 # default: 3
|
|
101
|
+
backward_relations: bool = False # default: True
|
|
102
|
+
exclude_raw_fields = False # default: True
|
|
103
|
+
sort_alphabetically: bool = True # default: False
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def pyd(cls) -> type[PydanticModel]:
|
|
107
|
+
cls._pyd = cls._pyd or pydantic_model_creator(cls)
|
|
108
|
+
return cls._pyd
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def pyd_in(cls) -> type[PydanticModel]:
|
|
112
|
+
if not cls._pydIn:
|
|
113
|
+
opts = tuple(k for k, v in cls._meta.fields_map.items() if not v.required)
|
|
114
|
+
cls._pydIn = pydantic_model_creator(
|
|
115
|
+
cls,
|
|
116
|
+
name=cls.__name__ + "In",
|
|
117
|
+
meta_override=cls.PydanticMetaIn,
|
|
118
|
+
optional=opts,
|
|
119
|
+
exclude_readonly=True,
|
|
120
|
+
exclude=("created_at", "updated_at"),
|
|
121
|
+
)
|
|
122
|
+
if m2ms := cls._meta.m2m_fields: # hack for direct inserting m2m values
|
|
123
|
+
cls._pydIn = create_model(
|
|
124
|
+
cls._pydIn.__name__, __base__=cls._pydIn, **{m2m: (list[int] | None, None) for m2m in m2ms}
|
|
125
|
+
)
|
|
126
|
+
return cls._pydIn
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def pyd_list_item(cls) -> type[PydanticModel]:
|
|
130
|
+
if not cls._pydListItem:
|
|
131
|
+
cls._pydListItem = pydantic_model_creator(
|
|
132
|
+
cls, name=cls.__name__ + "ListItem", meta_override=cls.PydanticMetaListItem
|
|
133
|
+
)
|
|
134
|
+
return cls._pydListItem
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def pyds_list(cls) -> type[PydList]:
|
|
138
|
+
return create_model(
|
|
139
|
+
cls.__name__ + "List",
|
|
140
|
+
data=(list[cls.pyd_list_item()], []),
|
|
141
|
+
total=(int, 0),
|
|
142
|
+
filtered=(int | None, None),
|
|
143
|
+
__base__=PydList[cls.pyd_list_item()],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# # # CRUD Methods # # #
|
|
147
|
+
@classmethod
|
|
148
|
+
async def one_pyd(cls, uid: int, **filters) -> PydanticModel:
|
|
149
|
+
q = cls.get(id=uid, **filters)
|
|
150
|
+
return await cls.pyd().from_queryset_single(q)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
async def page_pyd(cls, sorts: tuple[str], limit: int = 1000, offset: int = 0, q: str = None, **filters) -> PydList:
|
|
154
|
+
filters = {k: v for k, v in filters.items() if v is not None}
|
|
155
|
+
pyd_item = cls.pyd_list_item()
|
|
156
|
+
query = cls._page_query(sorts, limit, offset, q, **filters)
|
|
157
|
+
data = await pyd_item.from_queryset(query)
|
|
158
|
+
if limit - (li := len(data)):
|
|
159
|
+
filtered = total = li + offset
|
|
160
|
+
else:
|
|
161
|
+
total = await cls.all().count()
|
|
162
|
+
filtered_query = cls.filter(**filters)
|
|
163
|
+
if q:
|
|
164
|
+
filtered_query = filtered_query.filter(**{f"{cls._name}__icontains": q})
|
|
165
|
+
filtered = await filtered_query.count()
|
|
166
|
+
pyds = cls.pyds_list()
|
|
167
|
+
return pyds(data=data, total=total, filtered=filtered)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TsTrait:
|
|
171
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
172
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from typing import TypeVar, Generic
|
|
2
2
|
from pydantic import BaseModel, ConfigDict
|
|
3
3
|
|
|
4
|
-
from x_model.enum import UserStatus, UserRole
|
|
5
|
-
|
|
6
4
|
|
|
7
5
|
RootModelType = TypeVar("RootModelType")
|
|
8
6
|
|
|
@@ -13,32 +11,6 @@ class PydList(BaseModel, Generic[RootModelType]):
|
|
|
13
11
|
total: int
|
|
14
12
|
|
|
15
13
|
|
|
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
14
|
class Names(BaseModel):
|
|
43
15
|
# models for name endpoint for select2 inputs
|
|
44
16
|
class Name(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: xn-model
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Base model for xn-api
|
|
5
5
|
Author-email: Mike Artemiev <mixartemev@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -10,8 +10,6 @@ Keywords: tortoise,model,crud,generator,api,admin
|
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
Requires-Dist: tortoise-orm[accel,asyncpg]
|
|
13
|
-
Requires-Dist: passlib[bcrypt]
|
|
14
|
-
Requires-Dist: pydantic
|
|
15
13
|
Requires-Dist: python-dotenv
|
|
16
14
|
Provides-Extra: dev
|
|
17
15
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
|
@@ -1,330 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|