fastapi-mongo-base 0.8.30__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.
- fastapi_mongo_base/__init__.py +0 -0
- fastapi_mongo_base/cached.py +115 -0
- fastapi_mongo_base/core/__init__.py +0 -0
- fastapi_mongo_base/core/app_factory.py +184 -0
- fastapi_mongo_base/core/config.py +84 -0
- fastapi_mongo_base/core/db.py +45 -0
- fastapi_mongo_base/core/enums.py +36 -0
- fastapi_mongo_base/core/exceptions.py +86 -0
- fastapi_mongo_base/core/middlewares.py +22 -0
- fastapi_mongo_base/handlers.py +39 -0
- fastapi_mongo_base/models.py +384 -0
- fastapi_mongo_base/routes.py +293 -0
- fastapi_mongo_base/schemas.py +107 -0
- fastapi_mongo_base/tasks.py +291 -0
- fastapi_mongo_base/utils/__init__.py +0 -0
- fastapi_mongo_base/utils/aionetwork.py +90 -0
- fastapi_mongo_base/utils/basic.py +75 -0
- fastapi_mongo_base/utils/bsontools.py +25 -0
- fastapi_mongo_base/utils/imagetools.py +261 -0
- fastapi_mongo_base/utils/texttools.py +207 -0
- fastapi_mongo_base-0.8.30.dist-info/LICENSE.txt +19 -0
- fastapi_mongo_base-0.8.30.dist-info/METADATA +69 -0
- fastapi_mongo_base-0.8.30.dist-info/RECORD +25 -0
- fastapi_mongo_base-0.8.30.dist-info/WHEEL +5 -0
- fastapi_mongo_base-0.8.30.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from beanie import Document, Insert, Replace, Save, SaveChanges, Update, before_event
|
|
6
|
+
from beanie.odm.queries.find import FindMany
|
|
7
|
+
from pydantic import ConfigDict
|
|
8
|
+
from pymongo import ASCENDING, IndexModel
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from server.config import Settings
|
|
12
|
+
except ImportError:
|
|
13
|
+
from .core.config import Settings
|
|
14
|
+
|
|
15
|
+
from .schemas import (
|
|
16
|
+
BaseEntitySchema,
|
|
17
|
+
BusinessEntitySchema,
|
|
18
|
+
BusinessOwnedEntitySchema,
|
|
19
|
+
OwnedEntitySchema,
|
|
20
|
+
)
|
|
21
|
+
from .tasks import TaskMixin
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseEntity(BaseEntitySchema, Document):
|
|
25
|
+
class Settings:
|
|
26
|
+
__abstract__ = True
|
|
27
|
+
|
|
28
|
+
keep_nulls = False
|
|
29
|
+
validate_on_save = True
|
|
30
|
+
|
|
31
|
+
indexes = [
|
|
32
|
+
IndexModel([("uid", ASCENDING)], unique=True),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def is_abstract(cls):
|
|
37
|
+
# Use `__dict__` to check if `__abstract__` is defined in the class itself
|
|
38
|
+
return "__abstract__" in cls.__dict__ and cls.__dict__["__abstract__"]
|
|
39
|
+
|
|
40
|
+
@before_event([Insert, Replace, Save, SaveChanges, Update])
|
|
41
|
+
async def pre_save(self):
|
|
42
|
+
self.updated_at = datetime.now()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_queryset(
|
|
46
|
+
cls,
|
|
47
|
+
user_id: uuid.UUID = None,
|
|
48
|
+
business_name: str = None,
|
|
49
|
+
is_deleted: bool = False,
|
|
50
|
+
uid: uuid.UUID = None,
|
|
51
|
+
*args,
|
|
52
|
+
**kwargs,
|
|
53
|
+
) -> list[dict]:
|
|
54
|
+
"""Build a MongoDB query filter based on provided parameters.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
user_id: Filter by user ID if the model has user_id field
|
|
58
|
+
business_name: Filter by business name if the model has business_name field
|
|
59
|
+
is_deleted: Filter by deletion status
|
|
60
|
+
uid: Filter by unique identifier
|
|
61
|
+
**kwargs: Additional filters that can include range queries with _from/_to suffixes
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of MongoDB query conditions
|
|
65
|
+
"""
|
|
66
|
+
# Start with basic filters
|
|
67
|
+
base_query = []
|
|
68
|
+
|
|
69
|
+
# Add standard filters if applicable
|
|
70
|
+
base_query.append({"is_deleted": is_deleted})
|
|
71
|
+
|
|
72
|
+
if hasattr(cls, "user_id") and user_id:
|
|
73
|
+
base_query.append({"user_id": user_id})
|
|
74
|
+
|
|
75
|
+
if hasattr(cls, "business_name"):
|
|
76
|
+
base_query.append({"business_name": business_name})
|
|
77
|
+
|
|
78
|
+
if uid:
|
|
79
|
+
base_query.append({"uid": uid})
|
|
80
|
+
|
|
81
|
+
# Process additional filters from kwargs
|
|
82
|
+
for key, value in kwargs.items():
|
|
83
|
+
if value is None:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Extract base field name without _from/_to suffix
|
|
87
|
+
base_field = cls._get_base_field_name(key)
|
|
88
|
+
|
|
89
|
+
# Validate field is allowed for searching
|
|
90
|
+
if cls.search_field_set() and base_field not in cls.search_field_set():
|
|
91
|
+
continue
|
|
92
|
+
if cls.search_exclude_set() and base_field in cls.search_exclude_set():
|
|
93
|
+
continue
|
|
94
|
+
if not hasattr(cls, base_field):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Handle range queries and normal filters
|
|
98
|
+
if (
|
|
99
|
+
key.endswith("_from") or key.endswith("_to")
|
|
100
|
+
) and cls._is_valid_range_value(value):
|
|
101
|
+
if key.endswith("_from"):
|
|
102
|
+
base_query.append({base_field: {"$gte": value}})
|
|
103
|
+
elif key.endswith("_to"):
|
|
104
|
+
base_query.append({base_field: {"$lte": value}})
|
|
105
|
+
else:
|
|
106
|
+
base_query.append({key: value})
|
|
107
|
+
|
|
108
|
+
return base_query
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def _get_base_field_name(cls, field: str) -> str:
|
|
112
|
+
"""Extract the base field name by removing _from/_to suffixes."""
|
|
113
|
+
if field.endswith("_from"):
|
|
114
|
+
return field[:-5]
|
|
115
|
+
elif field.endswith("_to"):
|
|
116
|
+
return field[:-3]
|
|
117
|
+
return field
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def _is_valid_range_value(cls, value) -> bool:
|
|
121
|
+
"""Check if value is valid for range comparison."""
|
|
122
|
+
return isinstance(value, (int, float, Decimal, datetime, date, str))
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def get_query(
|
|
126
|
+
cls,
|
|
127
|
+
user_id: uuid.UUID = None,
|
|
128
|
+
business_name: str = None,
|
|
129
|
+
is_deleted: bool = False,
|
|
130
|
+
uid: uuid.UUID = None,
|
|
131
|
+
created_at_from: datetime = None,
|
|
132
|
+
created_at_to: datetime = None,
|
|
133
|
+
*args,
|
|
134
|
+
**kwargs,
|
|
135
|
+
) -> FindMany:
|
|
136
|
+
base_query = cls.get_queryset(
|
|
137
|
+
user_id=user_id,
|
|
138
|
+
business_name=business_name,
|
|
139
|
+
is_deleted=is_deleted,
|
|
140
|
+
uid=uid,
|
|
141
|
+
created_at_from=created_at_from,
|
|
142
|
+
created_at_to=created_at_to,
|
|
143
|
+
*args,
|
|
144
|
+
**kwargs,
|
|
145
|
+
)
|
|
146
|
+
query = cls.find(*base_query)
|
|
147
|
+
return query
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
async def get_item(
|
|
151
|
+
cls,
|
|
152
|
+
uid: uuid.UUID,
|
|
153
|
+
user_id: uuid.UUID = None,
|
|
154
|
+
business_name: str = None,
|
|
155
|
+
is_deleted: bool = False,
|
|
156
|
+
*args,
|
|
157
|
+
**kwargs,
|
|
158
|
+
) -> "BaseEntity":
|
|
159
|
+
query = cls.get_query(
|
|
160
|
+
user_id=user_id,
|
|
161
|
+
business_name=business_name,
|
|
162
|
+
is_deleted=is_deleted,
|
|
163
|
+
uid=uid,
|
|
164
|
+
*args,
|
|
165
|
+
**kwargs,
|
|
166
|
+
)
|
|
167
|
+
items = await query.to_list()
|
|
168
|
+
if not items:
|
|
169
|
+
return None
|
|
170
|
+
if len(items) > 1:
|
|
171
|
+
raise ValueError("Multiple items found")
|
|
172
|
+
return items[0]
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def adjust_pagination(cls, offset: int, limit: int):
|
|
176
|
+
from fastapi import params
|
|
177
|
+
|
|
178
|
+
if isinstance(offset, params.Query):
|
|
179
|
+
offset = offset.default
|
|
180
|
+
if isinstance(limit, params.Query):
|
|
181
|
+
limit = limit.default
|
|
182
|
+
|
|
183
|
+
offset = max(offset or 0, 0)
|
|
184
|
+
limit = max(1, min(limit or 10, Settings.page_max_limit))
|
|
185
|
+
return offset, limit
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
async def list_items(
|
|
189
|
+
cls,
|
|
190
|
+
user_id: uuid.UUID = None,
|
|
191
|
+
business_name: str = None,
|
|
192
|
+
offset: int = 0,
|
|
193
|
+
limit: int = 10,
|
|
194
|
+
is_deleted: bool = False,
|
|
195
|
+
*args,
|
|
196
|
+
**kwargs,
|
|
197
|
+
):
|
|
198
|
+
offset, limit = cls.adjust_pagination(offset, limit)
|
|
199
|
+
|
|
200
|
+
query = cls.get_query(
|
|
201
|
+
user_id=user_id,
|
|
202
|
+
business_name=business_name,
|
|
203
|
+
is_deleted=is_deleted,
|
|
204
|
+
*args,
|
|
205
|
+
**kwargs,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
items_query = query.sort("-created_at").skip(offset).limit(limit)
|
|
209
|
+
items = await items_query.to_list()
|
|
210
|
+
return items
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
async def total_count(
|
|
214
|
+
cls,
|
|
215
|
+
user_id: uuid.UUID = None,
|
|
216
|
+
business_name: str = None,
|
|
217
|
+
is_deleted: bool = False,
|
|
218
|
+
*args,
|
|
219
|
+
**kwargs,
|
|
220
|
+
):
|
|
221
|
+
query = cls.get_query(
|
|
222
|
+
user_id=user_id,
|
|
223
|
+
business_name=business_name,
|
|
224
|
+
is_deleted=is_deleted,
|
|
225
|
+
*args,
|
|
226
|
+
**kwargs,
|
|
227
|
+
)
|
|
228
|
+
return await query.count()
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
async def list_total_combined(
|
|
232
|
+
cls,
|
|
233
|
+
user_id: uuid.UUID = None,
|
|
234
|
+
business_name: str = None,
|
|
235
|
+
offset: int = 0,
|
|
236
|
+
limit: int = 10,
|
|
237
|
+
is_deleted: bool = False,
|
|
238
|
+
*args,
|
|
239
|
+
**kwargs,
|
|
240
|
+
) -> tuple[list["BaseEntity"], int]:
|
|
241
|
+
items = await cls.list_items(
|
|
242
|
+
user_id=user_id,
|
|
243
|
+
business_name=business_name,
|
|
244
|
+
offset=offset,
|
|
245
|
+
limit=limit,
|
|
246
|
+
is_deleted=is_deleted,
|
|
247
|
+
**kwargs,
|
|
248
|
+
)
|
|
249
|
+
total = await cls.total_count(
|
|
250
|
+
user_id=user_id,
|
|
251
|
+
business_name=business_name,
|
|
252
|
+
is_deleted=is_deleted,
|
|
253
|
+
**kwargs,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return items, total
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
async def create_item(cls, data: dict):
|
|
260
|
+
# for key in data.keys():
|
|
261
|
+
# if cls.create_exclude_set() and key not in cls.create_field_set():
|
|
262
|
+
# data.pop(key, None)
|
|
263
|
+
# elif cls.create_exclude_set() and key in cls.create_exclude_set():
|
|
264
|
+
# data.pop(key, None)
|
|
265
|
+
|
|
266
|
+
item = cls(**data)
|
|
267
|
+
await item.save()
|
|
268
|
+
return item
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
async def update_item(cls, item: "BaseEntity", data: dict):
|
|
272
|
+
for key, value in data.items():
|
|
273
|
+
if cls.update_field_set() and key not in cls.update_field_set():
|
|
274
|
+
continue
|
|
275
|
+
if cls.update_exclude_set() and key in cls.update_exclude_set():
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if hasattr(item, key):
|
|
279
|
+
setattr(item, key, value)
|
|
280
|
+
|
|
281
|
+
await item.save()
|
|
282
|
+
return item
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
async def delete_item(cls, item: "BaseEntity"):
|
|
286
|
+
item.is_deleted = True
|
|
287
|
+
await item.save()
|
|
288
|
+
return item
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class OwnedEntity(OwnedEntitySchema, BaseEntity):
|
|
292
|
+
|
|
293
|
+
class Settings(BaseEntity.Settings):
|
|
294
|
+
__abstract__ = True
|
|
295
|
+
|
|
296
|
+
indexes = BaseEntity.Settings.indexes + [IndexModel([("user_id", ASCENDING)])]
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
async def get_item(cls, uid, user_id, *args, **kwargs) -> "OwnedEntity":
|
|
300
|
+
if user_id == None and kwargs.get("ignore_user_id") != True:
|
|
301
|
+
raise ValueError("user_id is required")
|
|
302
|
+
return await super().get_item(uid, user_id=user_id, *args, **kwargs)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class BusinessEntity(BusinessEntitySchema, BaseEntity):
|
|
306
|
+
|
|
307
|
+
class Settings(BaseEntity.Settings):
|
|
308
|
+
__abstract__ = True
|
|
309
|
+
|
|
310
|
+
indexes = BaseEntity.Settings.indexes + [
|
|
311
|
+
IndexModel([("business_name", ASCENDING)])
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
async def get_item(cls, uid, business_name, *args, **kwargs) -> "BusinessEntity":
|
|
316
|
+
if business_name == None:
|
|
317
|
+
raise ValueError("business_name is required")
|
|
318
|
+
return await super().get_item(uid, business_name=business_name, *args, **kwargs)
|
|
319
|
+
|
|
320
|
+
async def get_business(self):
|
|
321
|
+
raise NotImplementedError
|
|
322
|
+
from apps.business_mongo.models import Business
|
|
323
|
+
|
|
324
|
+
return await Business.get_by_name(self.business_name)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class BusinessOwnedEntity(BusinessOwnedEntitySchema, BaseEntity):
|
|
328
|
+
|
|
329
|
+
class Settings(BusinessEntity.Settings):
|
|
330
|
+
__abstract__ = True
|
|
331
|
+
|
|
332
|
+
indexes = BusinessEntity.Settings.indexes + [
|
|
333
|
+
IndexModel([("user_id", ASCENDING)])
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
async def get_item(
|
|
338
|
+
cls, uid, business_name, user_id, *args, **kwargs
|
|
339
|
+
) -> "BusinessOwnedEntity":
|
|
340
|
+
if business_name == None:
|
|
341
|
+
raise ValueError("business_name is required")
|
|
342
|
+
# if user_id == None:
|
|
343
|
+
# raise ValueError("user_id is required")
|
|
344
|
+
return await super().get_item(
|
|
345
|
+
uid, business_name=business_name, user_id=user_id, *args, **kwargs
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class BaseEntityTaskMixin(BaseEntity, TaskMixin):
|
|
350
|
+
class Settings(BaseEntity.Settings):
|
|
351
|
+
__abstract__ = True
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ImmutableBase(BaseEntity):
|
|
355
|
+
model_config = ConfigDict(frozen=True)
|
|
356
|
+
|
|
357
|
+
class Settings(BaseEntity.Settings):
|
|
358
|
+
__abstract__ = True
|
|
359
|
+
|
|
360
|
+
@classmethod
|
|
361
|
+
async def update_item(cls, item: "BaseEntity", data: dict):
|
|
362
|
+
raise ValueError("Immutable items cannot be updated")
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
async def delete_item(cls, item: "BaseEntity"):
|
|
366
|
+
raise ValueError("Immutable items cannot be deleted")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class ImmutableOwnedEntity(ImmutableBase, OwnedEntity):
|
|
370
|
+
|
|
371
|
+
class Settings(OwnedEntity.Settings):
|
|
372
|
+
__abstract__ = True
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class ImmutableBusinessEntity(ImmutableBase, BusinessEntity):
|
|
376
|
+
|
|
377
|
+
class Settings(BusinessEntity.Settings):
|
|
378
|
+
__abstract__ = True
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class ImmutableBusinessOwnedEntity(ImmutableBase, BusinessOwnedEntity):
|
|
382
|
+
|
|
383
|
+
class Settings(BusinessOwnedEntity.Settings):
|
|
384
|
+
__abstract__ = True
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Generic, Type, TypeVar
|
|
5
|
+
|
|
6
|
+
import singleton
|
|
7
|
+
from fastapi import APIRouter, BackgroundTasks, Query, Request
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from core.exceptions import BaseHTTPException
|
|
11
|
+
except ImportError:
|
|
12
|
+
from .core.exceptions import BaseHTTPException
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from server.config import Settings
|
|
16
|
+
except ImportError:
|
|
17
|
+
from .core.config import Settings
|
|
18
|
+
|
|
19
|
+
from .handlers import create_dto
|
|
20
|
+
from .models import BaseEntity, BaseEntityTaskMixin
|
|
21
|
+
from .schemas import BaseEntitySchema, PaginatedResponse
|
|
22
|
+
|
|
23
|
+
# Define a type variable
|
|
24
|
+
T = TypeVar("T", bound=BaseEntity)
|
|
25
|
+
TE = TypeVar("TE", bound=BaseEntityTaskMixin)
|
|
26
|
+
TS = TypeVar("TS", bound=BaseEntitySchema)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AbstractBaseRouter(Generic[T, TS], metaclass=singleton.Singleton):
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
model: Type[T],
|
|
34
|
+
user_dependency: Any,
|
|
35
|
+
*args,
|
|
36
|
+
prefix: str = None,
|
|
37
|
+
tags: list[str] = None,
|
|
38
|
+
schema: Type[TS] = None,
|
|
39
|
+
**kwargs,
|
|
40
|
+
):
|
|
41
|
+
self.model = model
|
|
42
|
+
if schema is None:
|
|
43
|
+
schema = self.model
|
|
44
|
+
self.schema = schema
|
|
45
|
+
self.user_dependency = user_dependency
|
|
46
|
+
if prefix is None:
|
|
47
|
+
prefix = f"/{self.model.__name__.lower()}s"
|
|
48
|
+
if tags is None:
|
|
49
|
+
tags = [self.model.__name__]
|
|
50
|
+
self.router = APIRouter(prefix=prefix, tags=tags, **kwargs)
|
|
51
|
+
|
|
52
|
+
self.config_schemas(self.schema, **kwargs)
|
|
53
|
+
self.config_routes(**kwargs)
|
|
54
|
+
|
|
55
|
+
def config_schemas(self, schema, **kwargs):
|
|
56
|
+
self.schema = schema
|
|
57
|
+
self.list_response_schema = PaginatedResponse[schema]
|
|
58
|
+
self.list_item_schema = schema
|
|
59
|
+
self.retrieve_response_schema = schema
|
|
60
|
+
self.create_response_schema = schema
|
|
61
|
+
self.update_response_schema = schema
|
|
62
|
+
self.delete_response_schema = schema
|
|
63
|
+
|
|
64
|
+
self.create_request_schema = schema
|
|
65
|
+
self.update_request_schema = schema
|
|
66
|
+
|
|
67
|
+
def config_routes(self, **kwargs):
|
|
68
|
+
prefix: str = kwargs.get("prefix", "")
|
|
69
|
+
prefix = prefix.strip("/")
|
|
70
|
+
prefix = f"/{prefix}" if prefix else ""
|
|
71
|
+
|
|
72
|
+
if kwargs.get("list_route", True):
|
|
73
|
+
self.router.add_api_route(
|
|
74
|
+
f"{prefix}/",
|
|
75
|
+
self.list_items,
|
|
76
|
+
methods=["GET"],
|
|
77
|
+
response_model=self.list_response_schema,
|
|
78
|
+
status_code=200,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if kwargs.get("retrieve_route", True):
|
|
82
|
+
self.router.add_api_route(
|
|
83
|
+
f"{prefix}/{{uid:uuid}}",
|
|
84
|
+
self.retrieve_item,
|
|
85
|
+
methods=["GET"],
|
|
86
|
+
response_model=self.retrieve_response_schema,
|
|
87
|
+
status_code=200,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if kwargs.get("create_route", True):
|
|
91
|
+
self.router.add_api_route(
|
|
92
|
+
f"{prefix}/",
|
|
93
|
+
self.create_item,
|
|
94
|
+
methods=["POST"],
|
|
95
|
+
response_model=self.create_response_schema,
|
|
96
|
+
status_code=201,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if kwargs.get("update_route", True):
|
|
100
|
+
self.router.add_api_route(
|
|
101
|
+
f"{prefix}/{{uid:uuid}}",
|
|
102
|
+
self.update_item,
|
|
103
|
+
methods=["PATCH"],
|
|
104
|
+
response_model=self.update_response_schema,
|
|
105
|
+
status_code=200,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if kwargs.get("delete_route", True):
|
|
109
|
+
self.router.add_api_route(
|
|
110
|
+
f"{prefix}/{{uid:uuid}}",
|
|
111
|
+
self.delete_item,
|
|
112
|
+
methods=["DELETE"],
|
|
113
|
+
response_model=self.delete_response_schema,
|
|
114
|
+
# status_code=204,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def get_item(
|
|
118
|
+
self,
|
|
119
|
+
uid: uuid.UUID,
|
|
120
|
+
user_id: uuid.UUID = None,
|
|
121
|
+
business_name: str = None,
|
|
122
|
+
**kwargs,
|
|
123
|
+
):
|
|
124
|
+
item = await self.model.get_item(
|
|
125
|
+
uid, user_id=user_id, business_name=business_name, **kwargs
|
|
126
|
+
)
|
|
127
|
+
if item is None:
|
|
128
|
+
raise BaseHTTPException(
|
|
129
|
+
status_code=404,
|
|
130
|
+
error="item_not_found",
|
|
131
|
+
message=f"{self.model.__name__.capitalize()} not found",
|
|
132
|
+
)
|
|
133
|
+
return item
|
|
134
|
+
|
|
135
|
+
async def get_user(self, request: Request, *args, **kwargs):
|
|
136
|
+
if self.user_dependency is None:
|
|
137
|
+
return None
|
|
138
|
+
if asyncio.iscoroutinefunction(self.user_dependency):
|
|
139
|
+
return await self.user_dependency(request)
|
|
140
|
+
return self.user_dependency(request)
|
|
141
|
+
|
|
142
|
+
async def get_user_id(self, request: Request, *args, **kwargs):
|
|
143
|
+
user = await self.get_user(request)
|
|
144
|
+
user_id = user.uid if user else None
|
|
145
|
+
return user_id
|
|
146
|
+
|
|
147
|
+
async def list_items(
|
|
148
|
+
self,
|
|
149
|
+
request: Request,
|
|
150
|
+
offset: int = Query(0, ge=0),
|
|
151
|
+
limit: int = Query(10, ge=1, le=Settings.page_max_limit),
|
|
152
|
+
created_at_from: datetime | None = None,
|
|
153
|
+
created_at_to: datetime | None = None,
|
|
154
|
+
):
|
|
155
|
+
user_id = await self.get_user_id(request)
|
|
156
|
+
limit = max(1, min(limit, Settings.page_max_limit))
|
|
157
|
+
|
|
158
|
+
items, total = await self.model.list_total_combined(
|
|
159
|
+
user_id=user_id,
|
|
160
|
+
offset=offset,
|
|
161
|
+
limit=limit,
|
|
162
|
+
created_at_from=created_at_from,
|
|
163
|
+
created_at_to=created_at_to,
|
|
164
|
+
)
|
|
165
|
+
items_in_schema = [self.list_item_schema(**item.model_dump()) for item in items]
|
|
166
|
+
|
|
167
|
+
return PaginatedResponse(
|
|
168
|
+
items=items_in_schema,
|
|
169
|
+
total=total,
|
|
170
|
+
offset=offset,
|
|
171
|
+
limit=limit,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def retrieve_item(
|
|
175
|
+
self,
|
|
176
|
+
request: Request,
|
|
177
|
+
uid: uuid.UUID,
|
|
178
|
+
):
|
|
179
|
+
user_id = await self.get_user_id(request)
|
|
180
|
+
item = await self.get_item(uid, user_id=user_id)
|
|
181
|
+
return item
|
|
182
|
+
|
|
183
|
+
async def create_item(
|
|
184
|
+
self,
|
|
185
|
+
request: Request,
|
|
186
|
+
data: dict,
|
|
187
|
+
):
|
|
188
|
+
user_id = await self.get_user_id(request)
|
|
189
|
+
item_data: TS = await create_dto(self.create_response_schema)(
|
|
190
|
+
request, user_id=user_id
|
|
191
|
+
)
|
|
192
|
+
item = await self.model.create_item(item_data.model_dump())
|
|
193
|
+
# item: T = await create_dto(self.create_request_schema)(request, user)
|
|
194
|
+
await item.save()
|
|
195
|
+
return item
|
|
196
|
+
|
|
197
|
+
async def update_item(
|
|
198
|
+
self,
|
|
199
|
+
request: Request,
|
|
200
|
+
uid: uuid.UUID,
|
|
201
|
+
data: dict,
|
|
202
|
+
):
|
|
203
|
+
user_id = await self.get_user_id(request)
|
|
204
|
+
item = await self.get_item(uid, user_id=user_id)
|
|
205
|
+
# item = await update_dto(self.model)(request, user)
|
|
206
|
+
item = await self.model.update_item(item, data)
|
|
207
|
+
return item
|
|
208
|
+
|
|
209
|
+
async def delete_item(
|
|
210
|
+
self,
|
|
211
|
+
request: Request,
|
|
212
|
+
uid: uuid.UUID,
|
|
213
|
+
):
|
|
214
|
+
user_id = await self.get_user_id(request)
|
|
215
|
+
item = await self.get_item(uid, user_id=user_id)
|
|
216
|
+
|
|
217
|
+
item = await self.model.delete_item(item)
|
|
218
|
+
return item
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AbstractTaskRouter(AbstractBaseRouter[TE, TS]):
|
|
222
|
+
def __init__(
|
|
223
|
+
self,
|
|
224
|
+
model: Type[TE],
|
|
225
|
+
user_dependency: Any,
|
|
226
|
+
schema: TS,
|
|
227
|
+
draftable: bool = True,
|
|
228
|
+
*args,
|
|
229
|
+
**kwargs,
|
|
230
|
+
):
|
|
231
|
+
super().__init__(model, user_dependency, schema=schema, *args, **kwargs)
|
|
232
|
+
self.draftable = draftable
|
|
233
|
+
|
|
234
|
+
def config_routes(self, **kwargs):
|
|
235
|
+
super().config_routes(**kwargs)
|
|
236
|
+
|
|
237
|
+
self.router.add_api_route(
|
|
238
|
+
"/{uid:uuid}/start",
|
|
239
|
+
self.start_item,
|
|
240
|
+
methods=["POST"],
|
|
241
|
+
response_model=self.retrieve_response_schema,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
async def create_item(
|
|
245
|
+
self, request: Request, data: dict, background_tasks: BackgroundTasks
|
|
246
|
+
):
|
|
247
|
+
if not self.draftable:
|
|
248
|
+
data["task_status"] = "init"
|
|
249
|
+
|
|
250
|
+
item: TE = await super().create_item(request, data)
|
|
251
|
+
|
|
252
|
+
if item.task_status == "init" or not self.draftable:
|
|
253
|
+
background_tasks.add_task(item.start_processing)
|
|
254
|
+
return item
|
|
255
|
+
|
|
256
|
+
async def start_item(
|
|
257
|
+
self, request: Request, uid: uuid.UUID, background_tasks: BackgroundTasks
|
|
258
|
+
):
|
|
259
|
+
user_id = await self.get_user_id(request)
|
|
260
|
+
item: TE = await self.get_item(uid, user_id=user_id)
|
|
261
|
+
background_tasks.add_task(item.start_processing)
|
|
262
|
+
return item.model_dump()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def copy_router(router: APIRouter, new_prefix: str):
|
|
266
|
+
new_router = APIRouter(prefix=new_prefix)
|
|
267
|
+
for route in router.routes:
|
|
268
|
+
new_router.add_api_route(
|
|
269
|
+
route.path.replace(router.prefix, ""),
|
|
270
|
+
route.endpoint,
|
|
271
|
+
methods=[
|
|
272
|
+
method
|
|
273
|
+
for method in route.methods
|
|
274
|
+
if method in ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
275
|
+
],
|
|
276
|
+
name=route.name,
|
|
277
|
+
response_class=route.response_class,
|
|
278
|
+
status_code=route.status_code,
|
|
279
|
+
tags=route.tags,
|
|
280
|
+
dependencies=route.dependencies,
|
|
281
|
+
summary=route.summary,
|
|
282
|
+
description=route.description,
|
|
283
|
+
response_description=route.response_description,
|
|
284
|
+
responses=route.responses,
|
|
285
|
+
deprecated=route.deprecated,
|
|
286
|
+
include_in_schema=route.include_in_schema,
|
|
287
|
+
response_model=route.response_model,
|
|
288
|
+
response_model_include=route.response_model_include,
|
|
289
|
+
response_model_exclude=route.response_model_exclude,
|
|
290
|
+
response_model_by_alias=route.response_model_by_alias,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return new_router
|