xn-model 0.11.3__tar.gz → 1.0.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.11.3/xn_model.egg-info → xn_model-1.0.0}/PKG-INFO +1 -1
- {xn_model-0.11.3 → xn_model-1.0.0}/pyproject.toml +1 -3
- xn_model-1.0.0/x_model/models.py +67 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/x_model/pydantic.py +9 -9
- {xn_model-0.11.3 → xn_model-1.0.0/xn_model.egg-info}/PKG-INFO +1 -1
- xn_model-0.11.3/x_model/models.py +0 -175
- {xn_model-0.11.3 → xn_model-1.0.0}/.env.sample +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/.gitignore +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/.pre-commit-config.yaml +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/README.md +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/makefile +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/setup.cfg +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/tests/__init__.py +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/tests/test_db.py +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/x_model/__init__.py +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/x_model/field.py +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/x_model/func.py +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/xn_model.egg-info/SOURCES.txt +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/xn_model.egg-info/dependency_links.txt +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/xn_model.egg-info/requires.txt +0 -0
- {xn_model-0.11.3 → xn_model-1.0.0}/xn_model.egg-info/top_level.txt +0 -0
|
@@ -4,9 +4,7 @@ requires-python = ">=3.12"
|
|
|
4
4
|
authors = [
|
|
5
5
|
{name = "Mike Artemiev", email = "mixartemev@gmail.com"},
|
|
6
6
|
]
|
|
7
|
-
dependencies = [
|
|
8
|
-
'tortoise-orm[accel,asyncpg]'
|
|
9
|
-
]
|
|
7
|
+
dependencies = ['tortoise-orm[accel,asyncpg]']
|
|
10
8
|
keywords = ["tortoise", "model", "crud", "generator", "api", "admin"]
|
|
11
9
|
description = 'Base model for xn-api'
|
|
12
10
|
readme = "README.md"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import ConfigDict
|
|
4
|
+
from tortoise import Model as BaseModel
|
|
5
|
+
from tortoise.contrib.pydantic import pydantic_model_creator, PydanticModel
|
|
6
|
+
from tortoise.fields import DatetimeField, IntField
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatetimeSecField(DatetimeField):
|
|
10
|
+
class _db_postgres:
|
|
11
|
+
SQL_TYPE = "TIMESTAMPTZ(0)"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TsTrait:
|
|
15
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
16
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Model(BaseModel):
|
|
20
|
+
id: int = IntField(True)
|
|
21
|
+
|
|
22
|
+
_name: tuple[str] = ("name",)
|
|
23
|
+
_sorts: tuple[str] = ("-id",)
|
|
24
|
+
|
|
25
|
+
def repr(self) -> str:
|
|
26
|
+
return " ".join(getattr(self, name_fragment) for name_fragment in self._name)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def _pyd(cls, suffix: str, **kwargs) -> type[PydanticModel]:
|
|
30
|
+
return pydantic_model_creator(cls, name=cls.__name__ + suffix, **kwargs)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def pyd(cls):
|
|
34
|
+
return cls._pyd("Root")
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def pyd_in(cls):
|
|
38
|
+
return cls._pyd("In", exclude_readonly=True)
|
|
39
|
+
|
|
40
|
+
# # # CRUD Methods # # #
|
|
41
|
+
@classmethod
|
|
42
|
+
async def one_pyd(cls, uid: int, **filters) -> PydanticModel:
|
|
43
|
+
q = cls.get(id=uid, **filters)
|
|
44
|
+
return await cls.pyd().from_queryset_single(q)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
async def get_or_create_by_name(cls, name: str, attr_name: str = None, def_dict: dict = None) -> "Model":
|
|
48
|
+
attr_name = attr_name or list(cls._name)[0]
|
|
49
|
+
if not (obj := await cls.get_or_none(**{attr_name: name})):
|
|
50
|
+
next_id = (await cls.all().order_by("-id").first()).id + 1
|
|
51
|
+
obj = await cls.create(id=next_id, **{attr_name: name}, **(def_dict or {}))
|
|
52
|
+
return obj
|
|
53
|
+
|
|
54
|
+
class PydanticMeta:
|
|
55
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
56
|
+
# include: tuple[str, ...] = ()
|
|
57
|
+
# exclude: tuple[str, ...] = ("Meta",)
|
|
58
|
+
# computed: tuple[str, ...] = ()
|
|
59
|
+
# backward_relations: bool = True
|
|
60
|
+
max_recursion: int = 1 # default: 3
|
|
61
|
+
# allow_cycles: bool = False
|
|
62
|
+
# exclude_raw_fields: bool = True
|
|
63
|
+
# sort_alphabetically: bool = False
|
|
64
|
+
# model_config: ConfigDict | None = None
|
|
65
|
+
|
|
66
|
+
class Meta:
|
|
67
|
+
abstract = True
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
from typing import TypeVar, Generic
|
|
2
|
-
from pydantic import BaseModel, ConfigDict
|
|
1
|
+
# from typing import TypeVar, Generic
|
|
2
|
+
from pydantic import BaseModel # , ConfigDict
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
RootModelType = TypeVar("RootModelType")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class PydList(BaseModel, Generic[RootModelType]):
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
# RootModelType = TypeVar("RootModelType")
|
|
6
|
+
#
|
|
7
|
+
#
|
|
8
|
+
# class PydList(BaseModel, Generic[RootModelType]):
|
|
9
|
+
# model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
10
|
+
# data: list[RootModelType]
|
|
11
|
+
# total: int
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Names(BaseModel):
|
|
@@ -1,175 +0,0 @@
|
|
|
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.exceptions import FieldError
|
|
7
|
-
from tortoise.models import MetaInfo
|
|
8
|
-
from tortoise.queryset import QuerySet
|
|
9
|
-
from x_model import HTTPException, FailReason
|
|
10
|
-
|
|
11
|
-
from x_model.field import DatetimeSecField
|
|
12
|
-
from x_model.pydantic import PydList
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class BaseModel(TortoiseModel):
|
|
16
|
-
# todo: resolve ownable + add only own list method
|
|
17
|
-
# todo: refact: clean old garbage
|
|
18
|
-
id: int = fields.IntField(True)
|
|
19
|
-
|
|
20
|
-
_name: tuple[str] = ("name",)
|
|
21
|
-
_sorts: tuple[str] = ("-id",)
|
|
22
|
-
|
|
23
|
-
def repr(self, sep: str = " ") -> str:
|
|
24
|
-
return sep.join(getattr(self, name_fragment) for name_fragment in self._name)
|
|
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[0]}__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
|
-
try:
|
|
158
|
-
data = await pyd_item.from_queryset(query)
|
|
159
|
-
except FieldError as e:
|
|
160
|
-
raise HTTPException(FailReason.body, e)
|
|
161
|
-
if limit - (li := len(data)):
|
|
162
|
-
filtered = total = li + offset
|
|
163
|
-
else:
|
|
164
|
-
total = await cls.all().count()
|
|
165
|
-
filtered_query = cls.filter(**filters)
|
|
166
|
-
if q:
|
|
167
|
-
filtered_query = filtered_query.filter(**{f"{cls._name}__icontains": q})
|
|
168
|
-
filtered = await filtered_query.count()
|
|
169
|
-
pyds = cls.pyds_list()
|
|
170
|
-
return pyds(data=data, total=total, filtered=filtered)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class TsTrait:
|
|
174
|
-
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
175
|
-
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|