fastapi-mongo-base 0.12.2__tar.gz → 0.12.4__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.
- fastapi_mongo_base-0.12.4/MANIFEST.in +1 -0
- {fastapi_mongo_base-0.12.2/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-0.12.4}/PKG-INFO +4 -22
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/pyproject.toml +30 -12
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/app_factory.py +37 -16
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/config.py +1 -1
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/db.py +2 -2
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/enums.py +2 -2
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/exceptions.py +5 -2
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/models.py +86 -41
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/routes.py +32 -13
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/schemas.py +5 -16
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/sql.py +24 -16
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/tasks.py +43 -18
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/basic.py +12 -5
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/bsontools.py +1 -1
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4/src/fastapi_mongo_base.egg-info}/PKG-INFO +4 -22
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/SOURCES.txt +1 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/requires.txt +2 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/LICENSE.txt +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/README.md +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/setup.cfg +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/__init__.py +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/__init__.py +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/middlewares.py +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/timezone.py +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/__init__.py +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/conditions.py +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/dependency_links.txt +0 -0
- {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include pytest.ini
|
{fastapi_mongo_base-0.12.2/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-0.12.4}/PKG-INFO
RENAMED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-mongo-base
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.4
|
|
4
4
|
Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
|
|
5
5
|
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
6
6
|
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
7
|
-
License:
|
|
8
|
-
|
|
9
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
10
|
-
this software and associated documentation files (the "Software"), to deal in
|
|
11
|
-
the Software without restriction, including without limitation the rights to
|
|
12
|
-
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
13
|
-
of the Software, and to permit persons to whom the Software is furnished to do
|
|
14
|
-
so, subject to the following conditions:
|
|
15
|
-
|
|
16
|
-
The above copyright notice and this permission notice shall be included in all
|
|
17
|
-
copies or substantial portions of the Software.
|
|
18
|
-
|
|
19
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
-
SOFTWARE.
|
|
26
|
-
|
|
7
|
+
License-Expression: MIT
|
|
27
8
|
Project-URL: Homepage, https://github.com/mahdikiani/fastapi-mongo-base-app
|
|
28
9
|
Project-URL: Bug Reports, https://github.com/mahdikiani/fastapi-mongo-base-app/issues
|
|
29
10
|
Project-URL: Funding, https://github.com/mahdikiani/fastapi-mongo-base-app
|
|
@@ -33,7 +14,6 @@ Keywords: fastapi,mongodb,beanie
|
|
|
33
14
|
Classifier: Development Status :: 3 - Alpha
|
|
34
15
|
Classifier: Intended Audience :: Developers
|
|
35
16
|
Classifier: Topic :: Software Development :: Build Tools
|
|
36
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
37
17
|
Classifier: Programming Language :: Python :: 3
|
|
38
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
39
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -53,6 +33,8 @@ Requires-Dist: uvicorn[standard]>=0.13.0
|
|
|
53
33
|
Provides-Extra: image
|
|
54
34
|
Requires-Dist: Pillow>=9.0.0; extra == "image"
|
|
55
35
|
Provides-Extra: test
|
|
36
|
+
Requires-Dist: pytest; extra == "test"
|
|
37
|
+
Requires-Dist: mongomock_motor; extra == "test"
|
|
56
38
|
Requires-Dist: coverage; extra == "test"
|
|
57
39
|
Dynamic: license-file
|
|
58
40
|
|
|
@@ -4,23 +4,19 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-mongo-base"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.4"
|
|
8
8
|
description = "A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
|
-
license =
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE.txt"]
|
|
12
13
|
keywords = ["fastapi", "mongodb", "beanie"]
|
|
13
|
-
authors = [
|
|
14
|
-
|
|
15
|
-
]
|
|
16
|
-
maintainers = [
|
|
17
|
-
{name = "Mahdi Kiani", email = "mahdikiany@gmail.com"}
|
|
18
|
-
]
|
|
14
|
+
authors = [{ name = "Mahdi Kiani", email = "mahdikiany@gmail.com" }]
|
|
15
|
+
maintainers = [{ name = "Mahdi Kiani", email = "mahdikiany@gmail.com" }]
|
|
19
16
|
classifiers = [
|
|
20
17
|
"Development Status :: 3 - Alpha",
|
|
21
18
|
"Intended Audience :: Developers",
|
|
22
19
|
"Topic :: Software Development :: Build Tools",
|
|
23
|
-
"License :: OSI Approved :: MIT License",
|
|
24
20
|
"Programming Language :: Python :: 3",
|
|
25
21
|
"Programming Language :: Python :: 3.10",
|
|
26
22
|
"Programming Language :: Python :: 3.11",
|
|
@@ -35,9 +31,18 @@ dependencies = [
|
|
|
35
31
|
"uuid6",
|
|
36
32
|
"pytz",
|
|
37
33
|
"fastapi>=0.65.0",
|
|
38
|
-
"uvicorn[standard]>=0.13.0"
|
|
34
|
+
"uvicorn[standard]>=0.13.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
image = [
|
|
39
|
+
"Pillow>=9.0.0",
|
|
40
|
+
]
|
|
41
|
+
test = [
|
|
42
|
+
"pytest",
|
|
43
|
+
"mongomock_motor",
|
|
44
|
+
"coverage",
|
|
39
45
|
]
|
|
40
|
-
optional-dependencies = {"image" = ["Pillow>=9.0.0"],"test" = ["coverage"]}
|
|
41
46
|
|
|
42
47
|
[project.urls]
|
|
43
48
|
"Homepage" = "https://github.com/mahdikiani/fastapi-mongo-base-app"
|
|
@@ -47,4 +52,17 @@ optional-dependencies = {"image" = ["Pillow>=9.0.0"],"test" = ["coverage"]}
|
|
|
47
52
|
"Source" = "https://github.com/mahdikiani/fastapi-mongo-base-app"
|
|
48
53
|
|
|
49
54
|
[tool.setuptools]
|
|
50
|
-
package-data = {"usso" = ["*.dat"]}
|
|
55
|
+
package-data = { "usso" = ["*.dat"] }
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 79
|
|
59
|
+
target-version = "py313"
|
|
60
|
+
fix = true
|
|
61
|
+
unsafe-fixes = true
|
|
62
|
+
preview = true
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "W", "I", "UP", "B"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.format]
|
|
68
|
+
quote-style = "double"
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/app_factory.py
RENAMED
|
@@ -18,9 +18,15 @@ async def health(request: fastapi.Request):
|
|
|
18
18
|
|
|
19
19
|
@asynccontextmanager
|
|
20
20
|
async def lifespan(
|
|
21
|
-
*,
|
|
21
|
+
*,
|
|
22
|
+
app: fastapi.FastAPI,
|
|
23
|
+
worker=None,
|
|
24
|
+
init_functions=None,
|
|
25
|
+
settings: Settings = None,
|
|
22
26
|
):
|
|
23
27
|
"""Initialize application services."""
|
|
28
|
+
if init_functions is None:
|
|
29
|
+
init_functions = []
|
|
24
30
|
await db.init_mongo_db()
|
|
25
31
|
|
|
26
32
|
if worker:
|
|
@@ -39,7 +45,9 @@ async def lifespan(
|
|
|
39
45
|
logging.info("Shutdown complete")
|
|
40
46
|
|
|
41
47
|
|
|
42
|
-
def setup_exception_handlers(
|
|
48
|
+
def setup_exception_handlers(
|
|
49
|
+
*, app: fastapi.FastAPI, handlers: dict = None, **kwargs
|
|
50
|
+
):
|
|
43
51
|
exception_handlers = exceptions.EXCEPTION_HANDLERS
|
|
44
52
|
if handlers:
|
|
45
53
|
exception_handlers.update(handlers)
|
|
@@ -69,14 +77,18 @@ def get_app_kwargs(
|
|
|
69
77
|
version="0.1.0",
|
|
70
78
|
lifespan_func=None,
|
|
71
79
|
worker=None,
|
|
72
|
-
init_functions: list =
|
|
80
|
+
init_functions: list = None,
|
|
73
81
|
contact=None,
|
|
74
|
-
license_info=
|
|
75
|
-
"name": "MIT License",
|
|
76
|
-
"url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
|
|
77
|
-
},
|
|
82
|
+
license_info=None,
|
|
78
83
|
**kwargs,
|
|
79
84
|
):
|
|
85
|
+
if license_info is None:
|
|
86
|
+
license_info = {
|
|
87
|
+
"name": "MIT License",
|
|
88
|
+
"url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
|
|
89
|
+
}
|
|
90
|
+
if init_functions is None:
|
|
91
|
+
init_functions = []
|
|
80
92
|
settings.config_logger()
|
|
81
93
|
|
|
82
94
|
"""Create a FastAPI app with shared configurations."""
|
|
@@ -92,9 +104,14 @@ def get_app_kwargs(
|
|
|
92
104
|
base_path: str = settings.base_path
|
|
93
105
|
|
|
94
106
|
if lifespan_func is None:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
|
|
108
|
+
def lifespan_func(app):
|
|
109
|
+
return lifespan(
|
|
110
|
+
app=app,
|
|
111
|
+
worker=worker,
|
|
112
|
+
init_functions=init_functions,
|
|
113
|
+
settings=settings,
|
|
114
|
+
)
|
|
98
115
|
|
|
99
116
|
docs_url = f"{base_path}/docs"
|
|
100
117
|
openapi_url = f"{base_path}/openapi.json"
|
|
@@ -122,18 +139,22 @@ def create_app(
|
|
|
122
139
|
origins: list = None,
|
|
123
140
|
lifespan_func=None,
|
|
124
141
|
worker=None,
|
|
125
|
-
init_functions: list =
|
|
142
|
+
init_functions: list = None,
|
|
126
143
|
contact=None,
|
|
127
|
-
license_info=
|
|
128
|
-
"name": "MIT License",
|
|
129
|
-
"url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
|
|
130
|
-
},
|
|
144
|
+
license_info=None,
|
|
131
145
|
exception_handlers: dict = None,
|
|
132
146
|
log_route: bool = False,
|
|
133
147
|
health_route: bool = True,
|
|
134
148
|
index_route: bool = True,
|
|
135
149
|
**kwargs,
|
|
136
150
|
) -> fastapi.FastAPI:
|
|
151
|
+
if license_info is None:
|
|
152
|
+
license_info = {
|
|
153
|
+
"name": "MIT License",
|
|
154
|
+
"url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
|
|
155
|
+
}
|
|
156
|
+
if init_functions is None:
|
|
157
|
+
init_functions = []
|
|
137
158
|
data = get_app_kwargs(
|
|
138
159
|
settings=settings,
|
|
139
160
|
title=title,
|
|
@@ -197,7 +218,7 @@ def configure_app(
|
|
|
197
218
|
if log_route:
|
|
198
219
|
app.get(f"{base_path}/logs", include_in_schema=False)(logs)
|
|
199
220
|
if index_route:
|
|
200
|
-
app.get(
|
|
221
|
+
app.get("/", include_in_schema=False)(index)
|
|
201
222
|
|
|
202
223
|
if serve_coverage:
|
|
203
224
|
app.mount(
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/config.py
RENAMED
|
@@ -36,7 +36,7 @@ class Settings(metaclass=Singleton):
|
|
|
36
36
|
log_config = {
|
|
37
37
|
"formatters": {
|
|
38
38
|
"standard": {
|
|
39
|
-
"format": "[{levelname} : {filename}:{lineno} : {asctime} -> {funcName:10}] {message}",
|
|
39
|
+
"format": "[{levelname} : {filename}:{lineno} : {asctime} -> {funcName:10}] {message}", # noqa: E501
|
|
40
40
|
"style": "{",
|
|
41
41
|
}
|
|
42
42
|
},
|
|
@@ -34,10 +34,10 @@ def init_redis():
|
|
|
34
34
|
from redis import Redis as RedisSync
|
|
35
35
|
from redis.asyncio.client import Redis
|
|
36
36
|
|
|
37
|
-
if
|
|
37
|
+
if Settings.redis_uri:
|
|
38
38
|
redis_sync: RedisSync = RedisSync.from_url(Settings.redis_uri)
|
|
39
39
|
redis: Redis = Redis.from_url(Settings.redis_uri)
|
|
40
|
-
except ImportError
|
|
40
|
+
except (ImportError, AttributeError, Exception) as e:
|
|
41
41
|
logging.error(f"Error initializing Redis: {e}")
|
|
42
42
|
redis_sync = None
|
|
43
43
|
redis = None
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/exceptions.py
RENAMED
|
@@ -36,7 +36,9 @@ class BaseHTTPException(HTTPException):
|
|
|
36
36
|
super().__init__(status_code, detail=detail)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
async def base_http_exception_handler(
|
|
39
|
+
async def base_http_exception_handler(
|
|
40
|
+
request: Request, exc: BaseHTTPException
|
|
41
|
+
):
|
|
40
42
|
return JSONResponse(
|
|
41
43
|
status_code=exc.status_code,
|
|
42
44
|
content={"message": exc.message, "error": exc.error},
|
|
@@ -58,7 +60,8 @@ async def request_validation_exception_handler(
|
|
|
58
60
|
request: Request, exc: RequestValidationError
|
|
59
61
|
):
|
|
60
62
|
logging.error(
|
|
61
|
-
f"request_validation_exception: {request.url} {exc}\n
|
|
63
|
+
f"request_validation_exception: {request.url} {exc}\n"
|
|
64
|
+
f"{(await request.body())[:100]}"
|
|
62
65
|
)
|
|
63
66
|
from fastapi.exception_handlers import (
|
|
64
67
|
request_validation_exception_handler as default_handler,
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
|
|
3
|
-
from beanie import
|
|
3
|
+
from beanie import (
|
|
4
|
+
Document,
|
|
5
|
+
Insert,
|
|
6
|
+
Replace,
|
|
7
|
+
Save,
|
|
8
|
+
SaveChanges,
|
|
9
|
+
Update,
|
|
10
|
+
before_event,
|
|
11
|
+
)
|
|
4
12
|
from beanie.odm.queries.find import FindMany
|
|
5
13
|
from pydantic import ConfigDict
|
|
6
14
|
from pymongo import ASCENDING, IndexModel
|
|
@@ -29,8 +37,11 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
29
37
|
|
|
30
38
|
@classmethod
|
|
31
39
|
def is_abstract(cls):
|
|
32
|
-
# Use `__dict__` to check if `__abstract__` is defined
|
|
33
|
-
|
|
40
|
+
# Use `__dict__` to check if `__abstract__` is defined
|
|
41
|
+
# in the class itself
|
|
42
|
+
return (
|
|
43
|
+
"__abstract__" in cls.__dict__ and cls.__dict__["__abstract__"]
|
|
44
|
+
)
|
|
34
45
|
|
|
35
46
|
@before_event([Insert, Replace, Save, SaveChanges, Update])
|
|
36
47
|
async def pre_save(self):
|
|
@@ -39,13 +50,13 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
39
50
|
@classmethod
|
|
40
51
|
def get_queryset(
|
|
41
52
|
cls,
|
|
53
|
+
*,
|
|
42
54
|
user_id: str | None = None,
|
|
43
55
|
tenant_id: str | None = None,
|
|
44
56
|
is_deleted: bool = False,
|
|
45
57
|
uid: str | None = None,
|
|
46
|
-
*args,
|
|
47
58
|
**kwargs,
|
|
48
|
-
) ->
|
|
59
|
+
) -> dict:
|
|
49
60
|
"""Build a MongoDB query filter based on provided parameters.
|
|
50
61
|
|
|
51
62
|
Args:
|
|
@@ -53,25 +64,26 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
53
64
|
tenant_id: Filter by tenant ID if the model has tenant_id field
|
|
54
65
|
is_deleted: Filter by deletion status
|
|
55
66
|
uid: Filter by unique identifier
|
|
56
|
-
**kwargs: Additional filters that can include range queries
|
|
67
|
+
**kwargs: Additional filters that can include range queries
|
|
68
|
+
with _from/_to suffixes
|
|
57
69
|
|
|
58
70
|
Returns:
|
|
59
71
|
List of MongoDB query conditions
|
|
60
72
|
"""
|
|
61
73
|
# Start with basic filters
|
|
62
|
-
base_query =
|
|
74
|
+
base_query = {}
|
|
63
75
|
|
|
64
76
|
# Add standard filters if applicable
|
|
65
|
-
base_query.
|
|
66
|
-
|
|
67
|
-
if hasattr(cls, "user_id") and user_id:
|
|
68
|
-
base_query.append({"user_id": user_id})
|
|
77
|
+
base_query.update({"is_deleted": is_deleted})
|
|
69
78
|
|
|
70
79
|
if hasattr(cls, "tenant_id"):
|
|
71
|
-
base_query.
|
|
80
|
+
base_query.update({"tenant_id": tenant_id})
|
|
81
|
+
|
|
82
|
+
if hasattr(cls, "user_id") and user_id:
|
|
83
|
+
base_query.update({"user_id": user_id})
|
|
72
84
|
|
|
73
85
|
if uid:
|
|
74
|
-
base_query.
|
|
86
|
+
base_query.update({"uid": uid})
|
|
75
87
|
|
|
76
88
|
# Process additional filters from kwargs
|
|
77
89
|
for key, value in kwargs.items():
|
|
@@ -82,9 +94,15 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
82
94
|
base_field = basic.get_base_field_name(key)
|
|
83
95
|
|
|
84
96
|
# Validate field is allowed for searching
|
|
85
|
-
if
|
|
97
|
+
if (
|
|
98
|
+
cls.search_field_set()
|
|
99
|
+
and base_field not in cls.search_field_set()
|
|
100
|
+
):
|
|
86
101
|
continue
|
|
87
|
-
if
|
|
102
|
+
if (
|
|
103
|
+
cls.search_exclude_set()
|
|
104
|
+
and base_field in cls.search_exclude_set()
|
|
105
|
+
):
|
|
88
106
|
continue
|
|
89
107
|
if not hasattr(cls, base_field):
|
|
90
108
|
continue
|
|
@@ -92,29 +110,27 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
92
110
|
# Handle range queries and array operators
|
|
93
111
|
if key.endswith("_from") or key.endswith("_to"):
|
|
94
112
|
if basic.is_valid_range_value(value):
|
|
95
|
-
if key.endswith("_from")
|
|
96
|
-
|
|
97
|
-
elif key.endswith("_to"):
|
|
98
|
-
base_query.append({base_field: {"$lte": value}})
|
|
113
|
+
op = "$gte" if key.endswith("_from") else "$lte"
|
|
114
|
+
base_query.setdefault(base_field, {}).update({op: value})
|
|
99
115
|
elif key.endswith("_in") or key.endswith("_nin"):
|
|
100
116
|
value_list = basic.parse_array_parameter(value)
|
|
101
117
|
operator = "$in" if key.endswith("_in") else "$nin"
|
|
102
|
-
base_query.
|
|
118
|
+
base_query.update({base_field: {operator: value_list}})
|
|
103
119
|
else:
|
|
104
|
-
base_query.
|
|
120
|
+
base_query.update({key: value})
|
|
105
121
|
|
|
106
122
|
return base_query
|
|
107
123
|
|
|
108
124
|
@classmethod
|
|
109
125
|
def get_query(
|
|
110
126
|
cls,
|
|
127
|
+
*,
|
|
111
128
|
user_id: str | None = None,
|
|
112
129
|
tenant_id: str | None = None,
|
|
113
130
|
is_deleted: bool = False,
|
|
114
131
|
uid: str | None = None,
|
|
115
132
|
created_at_from: datetime = None,
|
|
116
133
|
created_at_to: datetime = None,
|
|
117
|
-
*args,
|
|
118
134
|
**kwargs,
|
|
119
135
|
) -> FindMany:
|
|
120
136
|
base_query = cls.get_queryset(
|
|
@@ -124,7 +140,6 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
124
140
|
uid=uid,
|
|
125
141
|
created_at_from=created_at_from,
|
|
126
142
|
created_at_to=created_at_to,
|
|
127
|
-
*args,
|
|
128
143
|
**kwargs,
|
|
129
144
|
)
|
|
130
145
|
query = cls.find({"$and": base_query})
|
|
@@ -145,7 +160,7 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
145
160
|
tenant_id=tenant_id,
|
|
146
161
|
is_deleted=is_deleted,
|
|
147
162
|
uid=uid,
|
|
148
|
-
*args,
|
|
163
|
+
*args, # noqa: B026
|
|
149
164
|
**kwargs,
|
|
150
165
|
)
|
|
151
166
|
items = await query.to_list()
|
|
@@ -185,7 +200,6 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
185
200
|
user_id=user_id,
|
|
186
201
|
tenant_id=tenant_id,
|
|
187
202
|
is_deleted=is_deleted,
|
|
188
|
-
*args,
|
|
189
203
|
**kwargs,
|
|
190
204
|
)
|
|
191
205
|
|
|
@@ -206,7 +220,7 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
206
220
|
user_id=user_id,
|
|
207
221
|
tenant_id=tenant_id,
|
|
208
222
|
is_deleted=is_deleted,
|
|
209
|
-
*args,
|
|
223
|
+
*args, # noqa: B026
|
|
210
224
|
**kwargs,
|
|
211
225
|
)
|
|
212
226
|
return await query.count()
|
|
@@ -285,55 +299,86 @@ class BaseEntity(BaseEntitySchema, Document):
|
|
|
285
299
|
|
|
286
300
|
|
|
287
301
|
class UserOwnedEntity(UserOwnedEntitySchema, BaseEntity):
|
|
288
|
-
|
|
289
302
|
class Settings(BaseEntity.Settings):
|
|
290
303
|
__abstract__ = True
|
|
291
304
|
|
|
292
|
-
indexes = BaseEntity.Settings.indexes + [
|
|
305
|
+
indexes = BaseEntity.Settings.indexes + [
|
|
306
|
+
IndexModel([("user_id", ASCENDING), ("uid", ASCENDING)]),
|
|
307
|
+
]
|
|
293
308
|
|
|
294
309
|
@classmethod
|
|
295
|
-
async def get_item(
|
|
296
|
-
|
|
310
|
+
async def get_item(
|
|
311
|
+
cls,
|
|
312
|
+
uid,
|
|
313
|
+
user_id,
|
|
314
|
+
*args,
|
|
315
|
+
**kwargs,
|
|
316
|
+
) -> "UserOwnedEntity":
|
|
317
|
+
if user_id is None and not kwargs.get("ignore_user_id"):
|
|
297
318
|
raise ValueError("user_id is required")
|
|
298
|
-
return await super().get_item(
|
|
319
|
+
return await super().get_item(
|
|
320
|
+
uid,
|
|
321
|
+
user_id=user_id,
|
|
322
|
+
*args, # noqa: B026
|
|
323
|
+
**kwargs,
|
|
324
|
+
)
|
|
299
325
|
|
|
300
326
|
|
|
301
327
|
class TenantScopedEntity(TenantScopedEntitySchema, BaseEntity):
|
|
302
|
-
|
|
303
328
|
class Settings(BaseEntity.Settings):
|
|
304
329
|
__abstract__ = True
|
|
305
330
|
|
|
306
|
-
indexes = BaseEntity.Settings.indexes + [
|
|
331
|
+
indexes = BaseEntity.Settings.indexes + [
|
|
332
|
+
IndexModel([("tenant_id", ASCENDING), ("uid", ASCENDING)]),
|
|
333
|
+
]
|
|
307
334
|
|
|
308
335
|
@classmethod
|
|
309
|
-
async def get_item(
|
|
310
|
-
|
|
336
|
+
async def get_item(
|
|
337
|
+
cls,
|
|
338
|
+
uid,
|
|
339
|
+
tenant_id,
|
|
340
|
+
*args,
|
|
341
|
+
**kwargs,
|
|
342
|
+
) -> "TenantScopedEntity":
|
|
343
|
+
if tenant_id is None:
|
|
311
344
|
raise ValueError("tenant_id is required")
|
|
312
|
-
return await super().get_item(
|
|
345
|
+
return await super().get_item(
|
|
346
|
+
uid,
|
|
347
|
+
tenant_id=tenant_id,
|
|
348
|
+
*args, # noqa: B026
|
|
349
|
+
**kwargs,
|
|
350
|
+
)
|
|
313
351
|
|
|
314
352
|
async def get_tenant(self):
|
|
315
353
|
raise NotImplementedError
|
|
316
354
|
|
|
317
355
|
|
|
318
356
|
class TenantUserEntity(TenantUserEntitySchema, BaseEntity):
|
|
319
|
-
|
|
320
357
|
class Settings(TenantScopedEntity.Settings):
|
|
321
358
|
__abstract__ = True
|
|
322
359
|
|
|
323
|
-
indexes =
|
|
324
|
-
IndexModel([
|
|
360
|
+
indexes = UserOwnedEntity.Settings.indexes + [
|
|
361
|
+
IndexModel([
|
|
362
|
+
("tenant_id", ASCENDING),
|
|
363
|
+
("user_id", ASCENDING),
|
|
364
|
+
("uid", ASCENDING),
|
|
365
|
+
]),
|
|
325
366
|
]
|
|
326
367
|
|
|
327
368
|
@classmethod
|
|
328
369
|
async def get_item(
|
|
329
370
|
cls, uid, tenant_id, user_id, *args, **kwargs
|
|
330
371
|
) -> "TenantUserEntity":
|
|
331
|
-
if tenant_id
|
|
372
|
+
if tenant_id is None:
|
|
332
373
|
raise ValueError("tenant_id is required")
|
|
333
374
|
# if user_id == None:
|
|
334
375
|
# raise ValueError("user_id is required")
|
|
335
376
|
return await super().get_item(
|
|
336
|
-
uid,
|
|
377
|
+
uid,
|
|
378
|
+
tenant_id=tenant_id,
|
|
379
|
+
user_id=user_id,
|
|
380
|
+
*args, # noqa: B026
|
|
381
|
+
**kwargs,
|
|
337
382
|
)
|
|
338
383
|
|
|
339
384
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, TypeVar
|
|
4
4
|
|
|
5
5
|
import singleton
|
|
6
6
|
from fastapi import APIRouter, BackgroundTasks, Query, Request
|
|
@@ -21,15 +21,14 @@ TS = TypeVar("TS", bound=BaseEntitySchema)
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class AbstractBaseRouter(metaclass=singleton.Singleton):
|
|
24
|
-
|
|
25
24
|
def __init__(
|
|
26
25
|
self,
|
|
27
|
-
model:
|
|
26
|
+
model: type[T],
|
|
28
27
|
*args,
|
|
29
28
|
user_dependency=None,
|
|
30
29
|
prefix: str = None,
|
|
31
30
|
tags: list[str] = None,
|
|
32
|
-
schema:
|
|
31
|
+
schema: type[TS] = None,
|
|
33
32
|
**kwargs,
|
|
34
33
|
):
|
|
35
34
|
self.model = model
|
|
@@ -52,13 +51,25 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
|
|
|
52
51
|
self.list_response_schema = kwargs.get(
|
|
53
52
|
"list_response_schema", PaginatedResponse[self.list_item_schema]
|
|
54
53
|
)
|
|
55
|
-
self.retrieve_response_schema = kwargs.get(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.
|
|
54
|
+
self.retrieve_response_schema = kwargs.get(
|
|
55
|
+
"retrieve_response_schema", schema
|
|
56
|
+
)
|
|
57
|
+
self.create_response_schema = kwargs.get(
|
|
58
|
+
"create_response_schema", schema
|
|
59
|
+
)
|
|
60
|
+
self.update_response_schema = kwargs.get(
|
|
61
|
+
"update_response_schema", schema
|
|
62
|
+
)
|
|
63
|
+
self.delete_response_schema = kwargs.get(
|
|
64
|
+
"delete_response_schema", schema
|
|
65
|
+
)
|
|
59
66
|
|
|
60
|
-
self.create_request_schema = kwargs.get(
|
|
61
|
-
|
|
67
|
+
self.create_request_schema = kwargs.get(
|
|
68
|
+
"create_request_schema", schema
|
|
69
|
+
)
|
|
70
|
+
self.update_request_schema = kwargs.get(
|
|
71
|
+
"update_request_schema", schema
|
|
72
|
+
)
|
|
62
73
|
|
|
63
74
|
def config_routes(self, **kwargs):
|
|
64
75
|
prefix: str = kwargs.get("prefix", "")
|
|
@@ -174,7 +185,9 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
|
|
|
174
185
|
limit=limit,
|
|
175
186
|
**kwargs,
|
|
176
187
|
)
|
|
177
|
-
items_in_schema = [
|
|
188
|
+
items_in_schema = [
|
|
189
|
+
self.list_item_schema(**item.model_dump()) for item in items
|
|
190
|
+
]
|
|
178
191
|
|
|
179
192
|
return PaginatedResponse(
|
|
180
193
|
items=items_in_schema,
|
|
@@ -244,7 +257,7 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
|
|
|
244
257
|
class AbstractTaskRouter(AbstractBaseRouter):
|
|
245
258
|
def __init__(
|
|
246
259
|
self,
|
|
247
|
-
model:
|
|
260
|
+
model: type[T],
|
|
248
261
|
user_dependency: Any,
|
|
249
262
|
schema: TS,
|
|
250
263
|
draftable: bool = True,
|
|
@@ -252,7 +265,13 @@ class AbstractTaskRouter(AbstractBaseRouter):
|
|
|
252
265
|
**kwargs,
|
|
253
266
|
):
|
|
254
267
|
self.draftable = draftable
|
|
255
|
-
super().__init__(
|
|
268
|
+
super().__init__(
|
|
269
|
+
model,
|
|
270
|
+
user_dependency,
|
|
271
|
+
schema=schema,
|
|
272
|
+
*args, # noqa: B026
|
|
273
|
+
**kwargs,
|
|
274
|
+
)
|
|
256
275
|
|
|
257
276
|
def config_routes(self, **kwargs):
|
|
258
277
|
super().config_routes(**kwargs)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import TypeVar
|
|
3
3
|
|
|
4
4
|
import uuid6
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
@@ -71,10 +71,6 @@ class BaseEntitySchema(BaseModel):
|
|
|
71
71
|
class UserOwnedEntitySchema(BaseEntitySchema):
|
|
72
72
|
user_id: str
|
|
73
73
|
|
|
74
|
-
@classmethod
|
|
75
|
-
def create_exclude_set(cls) -> list[str]:
|
|
76
|
-
return super().create_exclude_set() + ["user_id"]
|
|
77
|
-
|
|
78
74
|
@classmethod
|
|
79
75
|
def update_exclude_set(cls) -> list[str]:
|
|
80
76
|
return super().update_exclude_set() + ["user_id"]
|
|
@@ -83,30 +79,23 @@ class UserOwnedEntitySchema(BaseEntitySchema):
|
|
|
83
79
|
class TenantScopedEntitySchema(BaseEntitySchema):
|
|
84
80
|
tenant_id: str
|
|
85
81
|
|
|
86
|
-
@classmethod
|
|
87
|
-
def create_exclude_set(cls) -> list[str]:
|
|
88
|
-
return super().create_exclude_set() + ["tenant_id"]
|
|
89
|
-
|
|
90
82
|
@classmethod
|
|
91
83
|
def update_exclude_set(cls) -> list[str]:
|
|
92
84
|
return super().update_exclude_set() + ["tenant_id"]
|
|
93
85
|
|
|
94
86
|
|
|
95
87
|
class TenantUserEntitySchema(TenantScopedEntitySchema, UserOwnedEntitySchema):
|
|
96
|
-
|
|
97
|
-
@classmethod
|
|
98
|
-
def create_exclude_set(cls) -> list[str]:
|
|
99
|
-
return list(set(super().create_exclude_set() + ["tenant_id", "user_id"]))
|
|
100
|
-
|
|
101
88
|
@classmethod
|
|
102
89
|
def update_exclude_set(cls) -> list[str]:
|
|
103
|
-
return list(
|
|
90
|
+
return list(
|
|
91
|
+
set(super().update_exclude_set() + ["tenant_id", "user_id"])
|
|
92
|
+
)
|
|
104
93
|
|
|
105
94
|
|
|
106
95
|
T = TypeVar("T", bound=BaseEntitySchema)
|
|
107
96
|
|
|
108
97
|
|
|
109
|
-
class PaginatedResponse
|
|
98
|
+
class PaginatedResponse[T: BaseEntitySchema](BaseModel):
|
|
110
99
|
items: list[T]
|
|
111
100
|
total: int
|
|
112
101
|
offset: int
|
|
@@ -68,10 +68,13 @@ class BaseEntity:
|
|
|
68
68
|
return (datetime.now(timezone.tz) - self.updated_at).days > days
|
|
69
69
|
|
|
70
70
|
def dump(
|
|
71
|
-
self,
|
|
71
|
+
self,
|
|
72
|
+
include_fields: list[str] = None,
|
|
73
|
+
exclude_fields: list[str] = None,
|
|
72
74
|
) -> dict:
|
|
73
75
|
"""
|
|
74
|
-
Dump the object into a dictionary.
|
|
76
|
+
Dump the object into a dictionary.
|
|
77
|
+
It includes all the fields of the object.
|
|
75
78
|
"""
|
|
76
79
|
result = {}
|
|
77
80
|
for key, value in (include_fields or self.__dict__).items():
|
|
@@ -112,7 +115,8 @@ class BaseEntity:
|
|
|
112
115
|
tenant_id: Filter by tenant ID if the model has tenant_id field
|
|
113
116
|
is_deleted: Filter by deletion status
|
|
114
117
|
uid: Filter by unique identifier
|
|
115
|
-
**kwargs: Additional filters that can include range queries
|
|
118
|
+
**kwargs: Additional filters that can include range queries
|
|
119
|
+
with _from/_to suffixes
|
|
116
120
|
|
|
117
121
|
Returns:
|
|
118
122
|
List of SQLAlchemy query conditions
|
|
@@ -141,9 +145,15 @@ class BaseEntity:
|
|
|
141
145
|
base_field = basic.get_base_field_name(key)
|
|
142
146
|
|
|
143
147
|
# Validate field is allowed for searching
|
|
144
|
-
if
|
|
148
|
+
if (
|
|
149
|
+
cls.search_field_set()
|
|
150
|
+
and base_field not in cls.search_field_set()
|
|
151
|
+
):
|
|
145
152
|
continue
|
|
146
|
-
if
|
|
153
|
+
if (
|
|
154
|
+
cls.search_exclude_set()
|
|
155
|
+
and base_field in cls.search_exclude_set()
|
|
156
|
+
):
|
|
147
157
|
continue
|
|
148
158
|
if not hasattr(cls, base_field):
|
|
149
159
|
continue
|
|
@@ -177,7 +187,6 @@ class BaseEntity:
|
|
|
177
187
|
uid: str | None = None,
|
|
178
188
|
created_at_from: datetime | None = None,
|
|
179
189
|
created_at_to: datetime | None = None,
|
|
180
|
-
*args,
|
|
181
190
|
**kwargs,
|
|
182
191
|
):
|
|
183
192
|
base_query = cls.get_queryset(
|
|
@@ -187,7 +196,6 @@ class BaseEntity:
|
|
|
187
196
|
uid=uid,
|
|
188
197
|
created_at_from=created_at_from,
|
|
189
198
|
created_at_to=created_at_to,
|
|
190
|
-
*args,
|
|
191
199
|
**kwargs,
|
|
192
200
|
)
|
|
193
201
|
return base_query
|
|
@@ -225,7 +233,6 @@ class BaseEntity:
|
|
|
225
233
|
limit: int = 10,
|
|
226
234
|
**kwargs,
|
|
227
235
|
):
|
|
228
|
-
|
|
229
236
|
base_query = cls.get_query(
|
|
230
237
|
user_id=user_id,
|
|
231
238
|
tenant_id=tenant_id,
|
|
@@ -254,7 +261,6 @@ class BaseEntity:
|
|
|
254
261
|
is_deleted: bool = False,
|
|
255
262
|
**kwargs,
|
|
256
263
|
):
|
|
257
|
-
|
|
258
264
|
base_query = cls.get_query(
|
|
259
265
|
user_id=user_id,
|
|
260
266
|
tenant_id=tenant_id,
|
|
@@ -263,7 +269,9 @@ class BaseEntity:
|
|
|
263
269
|
)
|
|
264
270
|
|
|
265
271
|
# Query for getting the total count of items
|
|
266
|
-
total_count_query = select(func.count()).filter(
|
|
272
|
+
total_count_query = select(func.count()).filter(
|
|
273
|
+
*base_query
|
|
274
|
+
) # .subquery()
|
|
267
275
|
|
|
268
276
|
async with async_session() as session:
|
|
269
277
|
total_result = await session.execute(total_count_query)
|
|
@@ -299,7 +307,6 @@ class BaseEntity:
|
|
|
299
307
|
|
|
300
308
|
@classmethod
|
|
301
309
|
async def get_by_uid(cls, uid: str):
|
|
302
|
-
|
|
303
310
|
async with async_session() as session:
|
|
304
311
|
query = select(cls).filter(cls.uid == uid)
|
|
305
312
|
result = await session.execute(query)
|
|
@@ -308,7 +315,6 @@ class BaseEntity:
|
|
|
308
315
|
|
|
309
316
|
@classmethod
|
|
310
317
|
async def create_item(cls, data: dict):
|
|
311
|
-
|
|
312
318
|
item = cls(**data)
|
|
313
319
|
async with async_session() as session:
|
|
314
320
|
session.add(item)
|
|
@@ -318,7 +324,6 @@ class BaseEntity:
|
|
|
318
324
|
|
|
319
325
|
@classmethod
|
|
320
326
|
async def update_item(cls, item: "BaseEntity", data: dict):
|
|
321
|
-
|
|
322
327
|
for key, value in data.items():
|
|
323
328
|
if cls.update_field_set() and key not in cls.update_field_set():
|
|
324
329
|
continue
|
|
@@ -335,7 +340,6 @@ class BaseEntity:
|
|
|
335
340
|
|
|
336
341
|
@classmethod
|
|
337
342
|
async def delete_item(cls, item: "BaseEntity"):
|
|
338
|
-
|
|
339
343
|
item.is_deleted = True
|
|
340
344
|
async with async_session() as session:
|
|
341
345
|
session.add(item)
|
|
@@ -377,11 +381,15 @@ class TenantUserEntity(TenantScopedEntity, UserOwnedEntity):
|
|
|
377
381
|
|
|
378
382
|
@classmethod
|
|
379
383
|
def create_exclude_set(cls) -> list[str]:
|
|
380
|
-
return list(
|
|
384
|
+
return list(
|
|
385
|
+
set(super().create_exclude_set() + ["tenant_id", "user_id"])
|
|
386
|
+
)
|
|
381
387
|
|
|
382
388
|
@classmethod
|
|
383
389
|
def update_exclude_set(cls) -> list[str]:
|
|
384
|
-
return list(
|
|
390
|
+
return list(
|
|
391
|
+
set(super().update_exclude_set() + ["tenant_id", "user_id"])
|
|
392
|
+
)
|
|
385
393
|
|
|
386
394
|
|
|
387
395
|
class ImmutableMixin(BaseEntity):
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from collections.abc import Callable, Coroutine
|
|
3
4
|
from datetime import datetime
|
|
4
|
-
from enum import
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any, Literal, Union
|
|
6
7
|
|
|
7
8
|
import json_advanced as json
|
|
8
9
|
from pydantic import BaseModel, Field, field_serializer, field_validator
|
|
@@ -13,7 +14,7 @@ from .schemas import BaseEntitySchema
|
|
|
13
14
|
from .utils import basic
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class TaskStatusEnum(
|
|
17
|
+
class TaskStatusEnum(StrEnum):
|
|
17
18
|
none = "null"
|
|
18
19
|
draft = "draft"
|
|
19
20
|
init = "init"
|
|
@@ -36,12 +37,16 @@ class SignalRegistry(metaclass=Singleton):
|
|
|
36
37
|
def __init__(self):
|
|
37
38
|
self.signal_map: dict[
|
|
38
39
|
str,
|
|
39
|
-
list[
|
|
40
|
+
list[
|
|
41
|
+
Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]
|
|
42
|
+
],
|
|
40
43
|
] = {}
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
class TaskLogRecord(BaseModel):
|
|
44
|
-
reported_at: datetime = Field(
|
|
47
|
+
reported_at: datetime = Field(
|
|
48
|
+
default_factory=lambda: datetime.now(timezone.tz)
|
|
49
|
+
)
|
|
45
50
|
message: str
|
|
46
51
|
task_status: TaskStatusEnum
|
|
47
52
|
duration: int = 0
|
|
@@ -60,7 +65,12 @@ class TaskLogRecord(BaseModel):
|
|
|
60
65
|
return False
|
|
61
66
|
|
|
62
67
|
def __hash__(self):
|
|
63
|
-
return hash((
|
|
68
|
+
return hash((
|
|
69
|
+
self.reported_at,
|
|
70
|
+
self.message,
|
|
71
|
+
self.task_status,
|
|
72
|
+
self.duration,
|
|
73
|
+
))
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
class TaskReference(BaseModel):
|
|
@@ -69,7 +79,10 @@ class TaskReference(BaseModel):
|
|
|
69
79
|
|
|
70
80
|
def __eq__(self, other):
|
|
71
81
|
if isinstance(other, TaskReference):
|
|
72
|
-
return
|
|
82
|
+
return (
|
|
83
|
+
self.task_id == other.task_id
|
|
84
|
+
and self.task_type == other.task_type
|
|
85
|
+
)
|
|
73
86
|
return False
|
|
74
87
|
|
|
75
88
|
def __hash__(self):
|
|
@@ -90,7 +103,8 @@ class TaskReference(BaseModel):
|
|
|
90
103
|
task_item = await task_class.find_one(task_class.uid == self.task_id)
|
|
91
104
|
if not task_item:
|
|
92
105
|
raise ValueError(
|
|
93
|
-
f"No task found with id {self.task_id} of type
|
|
106
|
+
f"No task found with id {self.task_id} of type "
|
|
107
|
+
f"{self.task_type}."
|
|
94
108
|
)
|
|
95
109
|
|
|
96
110
|
return task_item
|
|
@@ -107,7 +121,9 @@ class TaskReferenceList(BaseModel):
|
|
|
107
121
|
for task_item in task_items:
|
|
108
122
|
await task_item.start_processing()
|
|
109
123
|
case "parallel":
|
|
110
|
-
await asyncio.gather(*[
|
|
124
|
+
await asyncio.gather(*[
|
|
125
|
+
task.start_processing() for task in task_items
|
|
126
|
+
])
|
|
111
127
|
|
|
112
128
|
|
|
113
129
|
class TaskMixin(BaseModel):
|
|
@@ -165,8 +181,9 @@ class TaskMixin(BaseModel):
|
|
|
165
181
|
cls.signals().append(signal)
|
|
166
182
|
|
|
167
183
|
@classmethod
|
|
168
|
-
async def emit_signals(
|
|
169
|
-
|
|
184
|
+
async def emit_signals(
|
|
185
|
+
cls, task_instance: "TaskMixin", *, sync=False, **kwargs
|
|
186
|
+
):
|
|
170
187
|
async def webhook_call(*args, **kwargs):
|
|
171
188
|
import httpx
|
|
172
189
|
|
|
@@ -183,16 +200,18 @@ class TaskMixin(BaseModel):
|
|
|
183
200
|
)
|
|
184
201
|
await task_instance.save()
|
|
185
202
|
logging.error(
|
|
186
|
-
"\n".join(
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
"\n".join([
|
|
204
|
+
"An error occurred in webhook_call:",
|
|
205
|
+
traceback_str,
|
|
206
|
+
str(e),
|
|
207
|
+
])
|
|
189
208
|
)
|
|
190
209
|
|
|
191
210
|
def webhook_task(webhook_url: str):
|
|
192
211
|
return
|
|
193
212
|
|
|
194
213
|
signals = []
|
|
195
|
-
meta_data =
|
|
214
|
+
meta_data = task_instance.meta_data or {}
|
|
196
215
|
task_dict = task_instance.model_dump()
|
|
197
216
|
task_dict.update({"task_type": task_instance.__class__.__name__})
|
|
198
217
|
task_dict.update(kwargs)
|
|
@@ -269,7 +288,9 @@ class TaskMixin(BaseModel):
|
|
|
269
288
|
**kwargs,
|
|
270
289
|
)
|
|
271
290
|
|
|
272
|
-
async def add_log(
|
|
291
|
+
async def add_log(
|
|
292
|
+
self, log_record: TaskLogRecord, *, emit: bool = True, **kwargs
|
|
293
|
+
):
|
|
273
294
|
self.task_logs.append(log_record)
|
|
274
295
|
if emit:
|
|
275
296
|
# await self.emit_signals(self)
|
|
@@ -277,7 +298,9 @@ class TaskMixin(BaseModel):
|
|
|
277
298
|
|
|
278
299
|
async def start_processing(self, **kwargs):
|
|
279
300
|
if self.task_references is None:
|
|
280
|
-
raise NotImplementedError(
|
|
301
|
+
raise NotImplementedError(
|
|
302
|
+
"Subclasses should implement this method"
|
|
303
|
+
)
|
|
281
304
|
|
|
282
305
|
await self.task_references.list_processing()
|
|
283
306
|
|
|
@@ -297,7 +320,9 @@ class TaskMixin(BaseModel):
|
|
|
297
320
|
await self.save()
|
|
298
321
|
await self.emit_signals(self, **kwargs)
|
|
299
322
|
else:
|
|
300
|
-
await asyncio.gather(
|
|
323
|
+
await asyncio.gather(
|
|
324
|
+
self.save(), self.emit_signals(self, **kwargs)
|
|
325
|
+
)
|
|
301
326
|
|
|
302
327
|
async def update_and_emit(self, **kwargs):
|
|
303
328
|
if kwargs.get("task_status") in [
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/basic.py
RENAMED
|
@@ -81,7 +81,8 @@ def try_except_wrapper(func, sync_to_thread=False):
|
|
|
81
81
|
func_name = f"{class_name}.{func_name}"
|
|
82
82
|
traceback_str = "".join(traceback.format_tb(e.__traceback__))
|
|
83
83
|
logging.error(
|
|
84
|
-
f"An error occurred in {func_name} ({args=}, {kwargs=}):\n
|
|
84
|
+
f"An error occurred in {func_name} ({args=}, {kwargs=}):\n"
|
|
85
|
+
f"{traceback_str}\n{type(e)}: {e}"
|
|
85
86
|
)
|
|
86
87
|
return None
|
|
87
88
|
|
|
@@ -140,12 +141,15 @@ def retry_execution(attempts, delay=0, sync_to_thread=False):
|
|
|
140
141
|
except Exception as e:
|
|
141
142
|
last_exception = e
|
|
142
143
|
logging.warning(
|
|
143
|
-
f"Attempt {attempt + 1} failed for {func.__name__}:
|
|
144
|
+
f"Attempt {attempt + 1} failed for {func.__name__}: "
|
|
145
|
+
f"{e}"
|
|
144
146
|
)
|
|
145
147
|
if delay > 0 and attempt < attempts - 1:
|
|
146
148
|
await asyncio.sleep(delay)
|
|
147
149
|
# If the loop finishes and the function didn't return successfully
|
|
148
|
-
logging.error(
|
|
150
|
+
logging.error(
|
|
151
|
+
f"All {attempts} attempts failed for {func.__name__}"
|
|
152
|
+
)
|
|
149
153
|
raise last_exception
|
|
150
154
|
|
|
151
155
|
@functools.wraps(func)
|
|
@@ -157,12 +161,15 @@ def retry_execution(attempts, delay=0, sync_to_thread=False):
|
|
|
157
161
|
except Exception as e:
|
|
158
162
|
last_exception = e
|
|
159
163
|
logging.warning(
|
|
160
|
-
f"Attempt {attempt + 1} failed for {func.__name__}:
|
|
164
|
+
f"Attempt {attempt + 1} failed for {func.__name__}: "
|
|
165
|
+
f"{e}"
|
|
161
166
|
)
|
|
162
167
|
if delay > 0 and attempt < attempts - 1:
|
|
163
168
|
time.sleep(delay)
|
|
164
169
|
# If the loop finishes and the function didn't return successfully
|
|
165
|
-
logging.error(
|
|
170
|
+
logging.error(
|
|
171
|
+
f"All {attempts} attempts failed for {func.__name__}"
|
|
172
|
+
)
|
|
166
173
|
raise last_exception
|
|
167
174
|
|
|
168
175
|
if sync_to_thread or asyncio.iscoroutinefunction(func):
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4/src/fastapi_mongo_base.egg-info}/PKG-INFO
RENAMED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-mongo-base
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.4
|
|
4
4
|
Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
|
|
5
5
|
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
6
6
|
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
7
|
-
License:
|
|
8
|
-
|
|
9
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
10
|
-
this software and associated documentation files (the "Software"), to deal in
|
|
11
|
-
the Software without restriction, including without limitation the rights to
|
|
12
|
-
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
13
|
-
of the Software, and to permit persons to whom the Software is furnished to do
|
|
14
|
-
so, subject to the following conditions:
|
|
15
|
-
|
|
16
|
-
The above copyright notice and this permission notice shall be included in all
|
|
17
|
-
copies or substantial portions of the Software.
|
|
18
|
-
|
|
19
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
-
SOFTWARE.
|
|
26
|
-
|
|
7
|
+
License-Expression: MIT
|
|
27
8
|
Project-URL: Homepage, https://github.com/mahdikiani/fastapi-mongo-base-app
|
|
28
9
|
Project-URL: Bug Reports, https://github.com/mahdikiani/fastapi-mongo-base-app/issues
|
|
29
10
|
Project-URL: Funding, https://github.com/mahdikiani/fastapi-mongo-base-app
|
|
@@ -33,7 +14,6 @@ Keywords: fastapi,mongodb,beanie
|
|
|
33
14
|
Classifier: Development Status :: 3 - Alpha
|
|
34
15
|
Classifier: Intended Audience :: Developers
|
|
35
16
|
Classifier: Topic :: Software Development :: Build Tools
|
|
36
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
37
17
|
Classifier: Programming Language :: Python :: 3
|
|
38
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
39
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -53,6 +33,8 @@ Requires-Dist: uvicorn[standard]>=0.13.0
|
|
|
53
33
|
Provides-Extra: image
|
|
54
34
|
Requires-Dist: Pillow>=9.0.0; extra == "image"
|
|
55
35
|
Provides-Extra: test
|
|
36
|
+
Requires-Dist: pytest; extra == "test"
|
|
37
|
+
Requires-Dist: mongomock_motor; extra == "test"
|
|
56
38
|
Requires-Dist: coverage; extra == "test"
|
|
57
39
|
Dynamic: license-file
|
|
58
40
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/__init__.py
RENAMED
|
File without changes
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/middlewares.py
RENAMED
|
File without changes
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/timezone.py
RENAMED
|
File without changes
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/__init__.py
RENAMED
|
File without changes
|
{fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/conditions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|