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,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
|