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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xn-model
3
- Version: 0.11.3
3
+ Version: 1.0.0
4
4
  Summary: Base model for xn-api
5
5
  Author-email: Mike Artemiev <mixartemev@gmail.com>
6
6
  License: MIT
@@ -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
- model_config = ConfigDict(arbitrary_types_allowed=True)
10
- data: list[RootModelType]
11
- total: int
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xn-model
3
- Version: 0.11.3
3
+ Version: 1.0.0
4
4
  Summary: Base model for xn-api
5
5
  Author-email: Mike Artemiev <mixartemev@gmail.com>
6
6
  License: MIT
@@ -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