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.
@@ -0,0 +1,107 @@
1
+ import uuid
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from typing import Generic, TypeVar
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ try:
9
+ from server.config import Settings
10
+ except ImportError:
11
+ from .core.config import Settings
12
+
13
+
14
+ class CoreEntitySchema(BaseModel):
15
+ created_at: datetime = Field(
16
+ default_factory=datetime.now, json_schema_extra={"index": True}
17
+ )
18
+ updated_at: datetime = Field(default_factory=datetime.now)
19
+ is_deleted: bool = False
20
+ meta_data: dict | None = None
21
+
22
+
23
+ class BaseEntitySchema(CoreEntitySchema):
24
+ uid: uuid.UUID = Field(
25
+ default_factory=uuid.uuid4, json_schema_extra={"index": True, "unique": True}
26
+ )
27
+
28
+ @property
29
+ def item_url(self):
30
+ return f"https://{Settings.root_url}/{Settings.base_path}/{self.__class__.__name__.lower()}s/{self.uid}"
31
+
32
+ @classmethod
33
+ def create_exclude_set(cls) -> list[str]:
34
+ return ["uid", "created_at", "updated_at", "is_deleted"]
35
+
36
+ @classmethod
37
+ def create_field_set(cls) -> list:
38
+ return []
39
+
40
+ @classmethod
41
+ def update_exclude_set(cls) -> list:
42
+ return ["uid", "created_at", "updated_at"]
43
+
44
+ @classmethod
45
+ def update_field_set(cls) -> list:
46
+ return []
47
+
48
+ @classmethod
49
+ def search_exclude_set(cls) -> list[str]:
50
+ return ["meta_data"]
51
+
52
+ @classmethod
53
+ def search_field_set(cls) -> list:
54
+ return []
55
+
56
+ def expired(self, days: int = 3):
57
+ return (datetime.now() - self.updated_at).days > days
58
+
59
+
60
+ class OwnedEntitySchema(BaseEntitySchema):
61
+ user_id: uuid.UUID
62
+
63
+ @classmethod
64
+ def create_exclude_set(cls) -> list[str]:
65
+ return super().create_exclude_set() + ["user_id"]
66
+
67
+ @classmethod
68
+ def update_exclude_set(cls) -> list[str]:
69
+ return super().update_exclude_set() + ["user_id"]
70
+
71
+
72
+ class BusinessEntitySchema(BaseEntitySchema):
73
+ business_name: str
74
+
75
+ @classmethod
76
+ def create_exclude_set(cls) -> list[str]:
77
+ return super().create_exclude_set() + ["business_name"]
78
+
79
+ @classmethod
80
+ def update_exclude_set(cls) -> list[str]:
81
+ return super().update_exclude_set() + ["business_name"]
82
+
83
+
84
+ class BusinessOwnedEntitySchema(OwnedEntitySchema, BusinessEntitySchema):
85
+
86
+ @classmethod
87
+ def create_exclude_set(cls) -> list[str]:
88
+ return list(set(super().create_exclude_set() + ["business_name", "user_id"]))
89
+
90
+ @classmethod
91
+ def update_exclude_set(cls) -> list[str]:
92
+ return list(set(super().update_exclude_set() + ["business_name", "user_id"]))
93
+
94
+
95
+ class Language(str, Enum):
96
+ English = "English"
97
+ Persian = "Persian"
98
+
99
+
100
+ T = TypeVar("T", bound=BaseEntitySchema)
101
+
102
+
103
+ class PaginatedResponse(BaseModel, Generic[T]):
104
+ items: list[T]
105
+ total: int
106
+ offset: int
107
+ limit: int
@@ -0,0 +1,291 @@
1
+ import asyncio
2
+ import logging
3
+ import uuid
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import Any, Callable, Coroutine, Literal, Union
7
+
8
+ import json_advanced as json
9
+ from pydantic import BaseModel, Field, field_serializer, field_validator
10
+ from singleton import Singleton
11
+
12
+ from .schemas import BaseEntitySchema
13
+ from .utils import aionetwork, basic
14
+
15
+
16
+ class TaskStatusEnum(str, Enum):
17
+ none = "null"
18
+ draft = "draft"
19
+ init = "init"
20
+ processing = "processing"
21
+ paused = "paused"
22
+ completed = "completed"
23
+ done = "done"
24
+ error = "error"
25
+
26
+ @classmethod
27
+ def Finishes(cls):
28
+ return [cls.done, cls.error, cls.completed]
29
+
30
+ @property
31
+ def is_done(self):
32
+ return self in self.Finishes()
33
+
34
+
35
+ class SignalRegistry(metaclass=Singleton):
36
+ def __init__(self):
37
+ self.signal_map: dict[
38
+ str,
39
+ list[Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]],
40
+ ] = {}
41
+
42
+
43
+ class TaskLogRecord(BaseModel):
44
+ reported_at: datetime = Field(default_factory=datetime.now)
45
+ message: str
46
+ task_status: TaskStatusEnum
47
+ duration: int = 0
48
+ log_type: str | None = None
49
+ # data: dict | None = None
50
+
51
+ def __eq__(self, other):
52
+ if isinstance(other, TaskLogRecord):
53
+ return (
54
+ self.reported_at == other.reported_at
55
+ and self.message == other.message
56
+ and self.task_status == other.task_status
57
+ and self.duration == other.duration
58
+ # and self.data == other.data
59
+ )
60
+ return False
61
+
62
+ def __hash__(self):
63
+ return hash((self.reported_at, self.message, self.task_status, self.duration))
64
+
65
+
66
+ class TaskReference(BaseModel):
67
+ task_id: uuid.UUID
68
+ task_type: str
69
+
70
+ def __eq__(self, other):
71
+ if isinstance(other, TaskReference):
72
+ return self.task_id == other.task_id and self.task_type == other.task_type
73
+ return False
74
+
75
+ def __hash__(self):
76
+ return hash((self.task_id, self.task_type))
77
+
78
+ async def get_task_item(self) -> BaseEntitySchema | None:
79
+ task_classes = {
80
+ subclass.__name__: subclass
81
+ for subclass in basic.get_all_subclasses(TaskMixin)
82
+ if issubclass(subclass, BaseEntitySchema)
83
+ }
84
+ # task_classes = self._get_all_task_classes()
85
+
86
+ task_class = task_classes.get(self.task_type)
87
+ if not task_class:
88
+ raise ValueError(f"Task type {self.task_type} is not supported.")
89
+
90
+ task_item = await task_class.find_one(task_class.uid == self.task_id)
91
+ if not task_item:
92
+ raise ValueError(
93
+ f"No task found with id {self.task_id} of type {self.task_type}."
94
+ )
95
+
96
+ return task_item
97
+
98
+
99
+ class TaskReferenceList(BaseModel):
100
+ tasks: list[Union[TaskReference, "TaskReferenceList"]] = []
101
+ mode: Literal["serial", "parallel"] = "serial"
102
+
103
+ async def list_processing(self):
104
+ task_items = [task.get_task_item() for task in self.tasks]
105
+ match self.mode:
106
+ case "serial":
107
+ for task_item in task_items:
108
+ await task_item.start_processing()
109
+ case "parallel":
110
+ await asyncio.gather(*[task.start_processing() for task in task_items])
111
+
112
+
113
+ class TaskMixin(BaseModel):
114
+ task_status: TaskStatusEnum = TaskStatusEnum.draft
115
+ task_report: str | None = None
116
+ task_progress: int = -1
117
+ task_logs: list[TaskLogRecord] = []
118
+ task_references: TaskReferenceList | None = None
119
+ webhook_url: str | None = None
120
+
121
+ @property
122
+ def item_webhook_url(self):
123
+ return f"{self.item_url}/webhook"
124
+
125
+ @field_validator("task_status", mode="before")
126
+ def validate_task_status(cls, value):
127
+ if isinstance(value, str):
128
+ return TaskStatusEnum(value)
129
+ return value
130
+
131
+ @field_serializer("task_status")
132
+ def serialize_task_status(self, value):
133
+ if isinstance(value, TaskStatusEnum):
134
+ return value.value
135
+ return value
136
+
137
+ @classmethod
138
+ def signals(cls):
139
+ registry = SignalRegistry()
140
+ if cls.__name__ not in registry.signal_map:
141
+ registry.signal_map[cls.__name__] = []
142
+ return registry.signal_map[cls.__name__]
143
+
144
+ @classmethod
145
+ def add_signal(
146
+ cls,
147
+ signal: Callable[..., None] | Callable[..., Coroutine[Any, Any, None]],
148
+ ):
149
+ cls.signals().append(signal)
150
+
151
+ @classmethod
152
+ async def emit_signals(cls, task_instance: "TaskMixin", *, sync=False, **kwargs):
153
+
154
+ async def webhook_call(*args, **kwargs):
155
+ try:
156
+ await aionetwork.aio_request(*args, **kwargs)
157
+ except Exception as e:
158
+ import traceback
159
+
160
+ traceback_str = "".join(traceback.format_tb(e.__traceback__))
161
+ await task_instance.save_report(
162
+ f"An error occurred in webhook_call: {e}",
163
+ emit=False,
164
+ log_type="webhook_error",
165
+ )
166
+ await task_instance.save()
167
+ logging.error(
168
+ "\n".join(
169
+ ["An error occurred in webhook_call:", traceback_str, str(e)]
170
+ )
171
+ )
172
+
173
+ def webhook_task(webhook_url: str):
174
+ return
175
+
176
+ signals = []
177
+ meta_data = getattr(task_instance, "meta_data") or {}
178
+ task_dict = task_instance.model_dump()
179
+ task_dict.update({"task_type": task_instance.__class__.__name__})
180
+ task_dict.update(kwargs)
181
+
182
+ for webhook_url in [
183
+ task_instance.webhook_url,
184
+ meta_data.get("webhook"),
185
+ meta_data.get("webhook_url"),
186
+ ]:
187
+ if not webhook_url:
188
+ continue
189
+ signals.append(
190
+ webhook_call(
191
+ method="post",
192
+ url=webhook_url,
193
+ headers={"Content-Type": "application/json"},
194
+ data=json.dumps(task_dict),
195
+ )
196
+ )
197
+
198
+ signals += [
199
+ (
200
+ signal(task_instance)
201
+ if asyncio.iscoroutinefunction(signal)
202
+ else asyncio.to_thread(signal, task_instance)
203
+ )
204
+ for signal in cls.signals()
205
+ ]
206
+
207
+ if not sync:
208
+ await asyncio.gather(*signals)
209
+ return
210
+
211
+ for signal in signals:
212
+ await signal
213
+
214
+ async def save_status(
215
+ self,
216
+ status: TaskStatusEnum,
217
+ **kwargs,
218
+ ):
219
+ self.task_status = status
220
+ await self.add_log(
221
+ TaskLogRecord(
222
+ task_status=self.task_status,
223
+ message=f"Status changed to {status}",
224
+ log_type=kwargs.get("log_type", "status_update"),
225
+ ),
226
+ **kwargs,
227
+ )
228
+
229
+ async def add_reference(self, task_id: uuid.UUID, **kwargs):
230
+ self.task_references.append(task_id)
231
+ await self.add_log(
232
+ TaskLogRecord(
233
+ task_status=self.task_status,
234
+ message=f"Added reference to task {task_id}",
235
+ log_type=kwargs.get("log_type", "add_reference"),
236
+ ),
237
+ **kwargs,
238
+ )
239
+
240
+ async def save_report(self, report: str, **kwargs):
241
+ self.task_report = report
242
+ await self.add_log(
243
+ TaskLogRecord(
244
+ task_status=self.task_status,
245
+ message=report,
246
+ log_type=kwargs.get("log_type", "report"),
247
+ ),
248
+ **kwargs,
249
+ )
250
+
251
+ async def add_log(self, log_record: TaskLogRecord, *, emit: bool = True, **kwargs):
252
+ self.task_logs.append(log_record)
253
+ if emit:
254
+ # await self.emit_signals(self)
255
+ await self.save_and_emit()
256
+
257
+ async def start_processing(self):
258
+ if self.task_references is None:
259
+ raise NotImplementedError("Subclasses should implement this method")
260
+
261
+ await self.task_references.list_processing()
262
+
263
+ @basic.try_except_wrapper
264
+ async def save_and_emit(self, **kwargs):
265
+ if kwargs.get("sync"):
266
+ await self.save()
267
+ await self.emit_signals(self, **kwargs)
268
+ else:
269
+ await asyncio.gather(self.save(), self.emit_signals(self, **kwargs))
270
+
271
+ async def update_and_emit(self, **kwargs):
272
+ if kwargs.get("task_status") in [
273
+ TaskStatusEnum.done,
274
+ TaskStatusEnum.error,
275
+ TaskStatusEnum.completed,
276
+ ]:
277
+ kwargs["task_progress"] = kwargs.get("task_progress", 100)
278
+ # kwargs["task_report"] = kwargs.get("task_report")
279
+ for key, value in kwargs.items():
280
+ if hasattr(self, key):
281
+ setattr(self, key, value)
282
+ if kwargs.get("task_report"):
283
+ await self.add_log(
284
+ TaskLogRecord(
285
+ task_status=self.task_status,
286
+ message=kwargs["task_report"],
287
+ log_type=kwargs.get("log_type", "status_update"),
288
+ ),
289
+ emit=False,
290
+ )
291
+ await self.save_and_emit()
File without changes
@@ -0,0 +1,90 @@
1
+ import logging
2
+ from io import BytesIO
3
+ from typing import Literal
4
+
5
+ import aiofiles
6
+ import httpx
7
+
8
+
9
+ async def prepare_url(url: str) -> str:
10
+ """Ensure the URL is valid and starts with http or https."""
11
+ if url is None:
12
+ raise ValueError("url is required")
13
+ return url if url.startswith("http") else f"https://{url}"
14
+
15
+
16
+ async def log_error(url: str, status_code: int, text: str, **kwargs):
17
+ """Log request errors."""
18
+ data = kwargs.get("data")
19
+ if not isinstance(data, str):
20
+ data = str(data)
21
+
22
+ logging.error(f"Error in aio_request {url} {data[:100]}: {status_code} {text}")
23
+
24
+
25
+ async def handle_binary_response(response: httpx.Response) -> BytesIO:
26
+ """Handle binary response."""
27
+ resp_bytes = BytesIO(await response.aread())
28
+ resp_bytes.seek(0)
29
+ return resp_bytes
30
+
31
+
32
+ async def aio_request_client(
33
+ client: httpx.AsyncClient,
34
+ *,
35
+ method: str = "get",
36
+ url: str = None,
37
+ response_type: Literal["json", "binary", "text", "bytes"] = "json",
38
+ **kwargs,
39
+ ):
40
+ """Perform an HTTP request and return the desired response type."""
41
+ url = await prepare_url(url)
42
+
43
+ raise_exception = kwargs.pop("raise_exception", True)
44
+
45
+ response = await client.request(method, url, **kwargs)
46
+
47
+ try:
48
+ response.raise_for_status()
49
+ except Exception as e:
50
+ await log_error(url, response.status_code, response.text, **kwargs)
51
+ if raise_exception:
52
+ raise e
53
+
54
+ if response_type == "binary":
55
+ return await handle_binary_response(response)
56
+ if response_type == "bytes":
57
+ return response.content
58
+ if response_type == "json":
59
+ return response.json()
60
+ return response.text
61
+
62
+
63
+ async def aio_request(*, method: str = "get", url: str = None, **kwargs) -> dict:
64
+ async with httpx.AsyncClient() as client:
65
+ return await aio_request_client(client, method=method, url=url, **kwargs)
66
+
67
+
68
+ async def aio_request_binary(
69
+ *, method: str = "get", url: str = None, **kwargs
70
+ ) -> BytesIO:
71
+ return await aio_request(method=method, url=url, response_type="binary", **kwargs)
72
+
73
+
74
+ async def aio_download(
75
+ url: str, filename: str, *, chunk_size: int = 8192, **kwargs
76
+ ) -> str:
77
+ """Download a file and save it locally."""
78
+ url = await prepare_url(url)
79
+ raise_exception = kwargs.pop("raise_exception", True)
80
+
81
+ async with httpx.AsyncClient() as client:
82
+ async with client.stream("GET", url, **kwargs) as response:
83
+ if raise_exception:
84
+ response.raise_for_status()
85
+
86
+ async with aiofiles.open(filename, "wb") as f:
87
+ async for chunk in response.aiter_bytes(chunk_size=chunk_size):
88
+ await f.write(chunk)
89
+
90
+ return filename
@@ -0,0 +1,75 @@
1
+ import asyncio
2
+ import functools
3
+ import logging
4
+
5
+
6
+ def get_all_subclasses(cls: type):
7
+ subclasses = cls.__subclasses__()
8
+ return subclasses + [
9
+ sub for subclass in subclasses for sub in get_all_subclasses(subclass)
10
+ ]
11
+
12
+
13
+ def try_except_wrapper(func):
14
+ @functools.wraps(func)
15
+ async def wrapped_func(*args, **kwargs):
16
+ try:
17
+ if asyncio.iscoroutinefunction(func):
18
+ return await func(*args, **kwargs)
19
+ return await asyncio.to_thread(func, *args, **kwargs)
20
+ except Exception as e:
21
+ import inspect
22
+ import traceback
23
+
24
+ func_name = func.__name__
25
+ if len(args) > 0:
26
+ if inspect.ismethod(func) or inspect.isfunction(func):
27
+ if hasattr(args[0], "__class__"):
28
+ class_name = args[0].__class__.__name__
29
+ func_name = f"{class_name}.{func_name}"
30
+
31
+ traceback_str = "".join(traceback.format_tb(e.__traceback__))
32
+ logging.error(f"An error occurred in {func_name}:\n{traceback_str}\n{e}")
33
+ return None
34
+
35
+ return wrapped_func
36
+
37
+
38
+ def delay_execution(seconds):
39
+ def decorator(func):
40
+ @functools.wraps(func)
41
+ async def wrapped_func(*args, **kwargs):
42
+ await asyncio.sleep(seconds)
43
+ if asyncio.iscoroutinefunction(func):
44
+ return await func(*args, **kwargs)
45
+ return await asyncio.to_thread(func, *args, **kwargs)
46
+
47
+ return wrapped_func
48
+
49
+ return decorator
50
+
51
+
52
+ def retry_execution(attempts, delay=0):
53
+ def decorator(func):
54
+ @functools.wraps(func)
55
+ async def wrapped_func(*args, **kwargs):
56
+ last_exception = None
57
+ for attempt in range(attempts):
58
+ try:
59
+ if asyncio.iscoroutinefunction(func):
60
+ return await func(*args, **kwargs)
61
+ return await asyncio.to_thread(func, *args, **kwargs)
62
+ except Exception as e:
63
+ last_exception = e
64
+ logging.warning(
65
+ f"Attempt {attempt + 1} failed for {func.__name__}: {e}"
66
+ )
67
+ if delay > 0 and attempt < attempts - 1:
68
+ await asyncio.sleep(delay)
69
+ # If the loop finishes and the function didn't return successfully
70
+ logging.error(f"All {attempts} attempts failed for {func.__name__}")
71
+ raise last_exception
72
+
73
+ return wrapped_func
74
+
75
+ return decorator
@@ -0,0 +1,25 @@
1
+ import uuid
2
+ from decimal import Decimal
3
+
4
+ from bson import Binary
5
+ from bson.decimal128 import Decimal128
6
+
7
+
8
+ def decimal_amount(value):
9
+ if type(value) == Decimal128:
10
+ return Decimal(value.to_decimal())
11
+ return value
12
+
13
+
14
+ def get_bson_value(value):
15
+ if isinstance(value, Decimal):
16
+ return Decimal128(value)
17
+ if isinstance(value, bytes):
18
+ return Binary(value)
19
+ if isinstance(value, uuid.UUID):
20
+ return Binary.from_uuid(value)
21
+ if isinstance(value, dict):
22
+ return {k: get_bson_value(v) for k, v in value.items()}
23
+ if isinstance(value, list):
24
+ return [get_bson_value(v) for v in value]
25
+ return value