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
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from pymongo import UpdateOne
|
|
6
|
+
|
|
7
|
+
from .models import BaseEntity
|
|
8
|
+
from .tasks import TaskStatusEnum
|
|
9
|
+
from .utils import bsontools
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from server.config import Settings
|
|
13
|
+
except ImportError:
|
|
14
|
+
from .core.config import Settings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from server.db import redis
|
|
19
|
+
except ImportError:
|
|
20
|
+
from redis import Redis
|
|
21
|
+
|
|
22
|
+
redis = Redis()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CachedMixin(BaseEntity):
|
|
26
|
+
def is_done(self):
|
|
27
|
+
return (
|
|
28
|
+
getattr(self, "task_status", "done") in TaskStatusEnum.Finishes()
|
|
29
|
+
or self.is_deleted
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
async def is_cached(self):
|
|
33
|
+
return await redis.hexists(
|
|
34
|
+
f"{Settings.project_name}:{self.__class__.__name__}_updates_hash",
|
|
35
|
+
str(self.uid),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def save(self, *args, **kwargs):
|
|
39
|
+
if self.is_done():
|
|
40
|
+
result = await super().save(*args, **kwargs)
|
|
41
|
+
await redis.hdel(
|
|
42
|
+
f"{Settings.project_name}:{self.__class__.__name__}_updates_hash",
|
|
43
|
+
str(self.uid),
|
|
44
|
+
)
|
|
45
|
+
return result
|
|
46
|
+
else:
|
|
47
|
+
await redis.hset(
|
|
48
|
+
f"{Settings.project_name}:{self.__class__.__name__}_updates_hash",
|
|
49
|
+
str(self.uid),
|
|
50
|
+
self.model_dump_json(),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
async def flush_queue_to_db(cls):
|
|
55
|
+
# Get all items from the Redis hash in a single operation
|
|
56
|
+
items_data: dict[bytes, bytes] = await redis.hgetall(
|
|
57
|
+
f"{Settings.project_name}:{cls.__name__}_updates_hash"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Clear the Redis hash after a successful batch write
|
|
61
|
+
await redis.delete(f"{Settings.project_name}:{cls.__name__}_updates_hash")
|
|
62
|
+
|
|
63
|
+
if items_data:
|
|
64
|
+
# Create a list of MongoDB upsert operations for the bulk update/insert
|
|
65
|
+
bulk_operations = []
|
|
66
|
+
for uid_bytes, item_data in items_data.items():
|
|
67
|
+
item_dict = json.loads(item_data)
|
|
68
|
+
item = cls(**item_dict)
|
|
69
|
+
uid = uuid.UUID(uid_bytes.decode("utf-8"))
|
|
70
|
+
filter_query = {"uid": bsontools.get_bson_value(uid)}
|
|
71
|
+
# Assuming the unique identifier is stored in _id
|
|
72
|
+
logging.info(
|
|
73
|
+
f"Flushing item {uid} to DB {bsontools.get_bson_value(item.model_dump())}"
|
|
74
|
+
)
|
|
75
|
+
update_query = {"$set": bsontools.get_bson_value(item.model_dump())}
|
|
76
|
+
bulk_operations.append(
|
|
77
|
+
UpdateOne(filter_query, update_query, upsert=True)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Perform the bulk upsert operation in a single call
|
|
81
|
+
if bulk_operations:
|
|
82
|
+
res = await cls.get_motor_collection().bulk_write(bulk_operations)
|
|
83
|
+
logging.info(f"Flushed {len(bulk_operations)} items to DB \n{res}")
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
async def get_item(
|
|
87
|
+
cls,
|
|
88
|
+
uid,
|
|
89
|
+
user_id: uuid.UUID = None,
|
|
90
|
+
business_name: str = None,
|
|
91
|
+
is_deleted: bool = False,
|
|
92
|
+
*args,
|
|
93
|
+
**kwargs,
|
|
94
|
+
) -> BaseEntity:
|
|
95
|
+
if user_id == None and kwargs.get("ignore_user_id") != True:
|
|
96
|
+
raise ValueError("user_id is required")
|
|
97
|
+
item_data = await redis.hget(
|
|
98
|
+
f"{Settings.project_name}:{cls.__name__}_updates_hash", str(uid)
|
|
99
|
+
)
|
|
100
|
+
if item_data:
|
|
101
|
+
item_dict = json.loads(item_data)
|
|
102
|
+
item = cls(**item_dict)
|
|
103
|
+
if user_id and getattr(item, "user_id", None) != user_id:
|
|
104
|
+
return None
|
|
105
|
+
if getattr(item, "business_name", None) != business_name:
|
|
106
|
+
return None
|
|
107
|
+
return item
|
|
108
|
+
return await super().get_item(
|
|
109
|
+
uid,
|
|
110
|
+
business_name=business_name,
|
|
111
|
+
user_id=user_id,
|
|
112
|
+
is_deleted=is_deleted,
|
|
113
|
+
*args,
|
|
114
|
+
**kwargs,
|
|
115
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from collections import deque
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
|
|
6
|
+
import fastapi
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from fastapi.staticfiles import StaticFiles
|
|
9
|
+
|
|
10
|
+
from fastapi_mongo_base.core import db, exceptions
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from server.config import Settings
|
|
14
|
+
except ImportError:
|
|
15
|
+
from .config import Settings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def health(request: fastapi.Request):
|
|
19
|
+
return {
|
|
20
|
+
"status": "up",
|
|
21
|
+
"host": request.url.hostname,
|
|
22
|
+
# "host2": request.base_url.hostname,
|
|
23
|
+
# "original_host":request.headers.get("x-original-host", "!not found!"),
|
|
24
|
+
# "forwarded_host": request.headers.get("X-Forwarded-Host", "forwarded_host"),
|
|
25
|
+
# "forwarded_proto": request.headers.get("X-Forwarded-Proto", "forwarded_proto"),
|
|
26
|
+
# "forwarded_for": request.headers.get("X-Forwarded-For", "forwarded_for"),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@asynccontextmanager
|
|
31
|
+
async def lifespan(app: fastapi.FastAPI, worker=None, init_functions=[], settings: Settings = None): # type: ignore
|
|
32
|
+
"""Initialize application services."""
|
|
33
|
+
await db.init_mongo_db()
|
|
34
|
+
|
|
35
|
+
if worker:
|
|
36
|
+
app.state.worker = asyncio.create_task(worker())
|
|
37
|
+
|
|
38
|
+
for function in init_functions:
|
|
39
|
+
if asyncio.iscoroutinefunction(function):
|
|
40
|
+
await function()
|
|
41
|
+
else:
|
|
42
|
+
function()
|
|
43
|
+
|
|
44
|
+
logging.info("Startup complete")
|
|
45
|
+
yield
|
|
46
|
+
app.state.worker.cancel()
|
|
47
|
+
logging.info("Shutdown complete")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def setup_exception_handlers(
|
|
51
|
+
app: fastapi.FastAPI,
|
|
52
|
+
usso_handler: bool = True,
|
|
53
|
+
ufaas_handler: bool = True,
|
|
54
|
+
**kwargs,
|
|
55
|
+
):
|
|
56
|
+
exception_handlers = exceptions.EXCEPTION_HANDLERS
|
|
57
|
+
if usso_handler:
|
|
58
|
+
from usso.fastapi.integration import (
|
|
59
|
+
EXCEPTION_HANDLERS as USSO_EXCEPTION_HANDLERS,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
exception_handlers.update(USSO_EXCEPTION_HANDLERS)
|
|
63
|
+
if ufaas_handler:
|
|
64
|
+
from ufaas.fastapi.integration import (
|
|
65
|
+
EXCEPTION_HANDLERS as UFAAS_EXCEPTION_HANDLERS,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
exception_handlers.update(UFAAS_EXCEPTION_HANDLERS)
|
|
69
|
+
|
|
70
|
+
for exc_class, handler in exception_handlers.items():
|
|
71
|
+
app.exception_handler(exc_class)(handler)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def setup_middlewares(
|
|
75
|
+
app: fastapi.FastAPI,
|
|
76
|
+
origins: list = None,
|
|
77
|
+
original_host_middleware: bool = False,
|
|
78
|
+
request_log_middleware: bool = False,
|
|
79
|
+
**kwargs,
|
|
80
|
+
):
|
|
81
|
+
if origins:
|
|
82
|
+
app.add_middleware(
|
|
83
|
+
CORSMiddleware,
|
|
84
|
+
allow_origins=origins,
|
|
85
|
+
allow_credentials=True,
|
|
86
|
+
allow_methods=["*"],
|
|
87
|
+
allow_headers=["*"],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if original_host_middleware:
|
|
91
|
+
from ufaas_fastapi_business.core.middlewares import OriginalHostMiddleware
|
|
92
|
+
|
|
93
|
+
app.add_middleware(OriginalHostMiddleware)
|
|
94
|
+
if request_log_middleware:
|
|
95
|
+
from .middlewares import RequestLoggingMiddleware
|
|
96
|
+
|
|
97
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_app(
|
|
101
|
+
*,
|
|
102
|
+
settings: Settings = None,
|
|
103
|
+
title=None,
|
|
104
|
+
description=None,
|
|
105
|
+
version="0.1.0",
|
|
106
|
+
serve_coverage: bool = False,
|
|
107
|
+
origins: list = None,
|
|
108
|
+
lifespan_func=None,
|
|
109
|
+
worker=None,
|
|
110
|
+
init_functions: list = [],
|
|
111
|
+
contact={
|
|
112
|
+
"name": "Mahdi Kiani",
|
|
113
|
+
"url": "https://github.com/mahdikiani/FastAPILaunchpad",
|
|
114
|
+
"email": "mahdikiany@gmail.com",
|
|
115
|
+
},
|
|
116
|
+
license_info={
|
|
117
|
+
"name": "MIT License",
|
|
118
|
+
"url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
|
|
119
|
+
},
|
|
120
|
+
usso_handler: bool = True,
|
|
121
|
+
ufaas_handler: bool = True,
|
|
122
|
+
original_host_middleware: bool = False,
|
|
123
|
+
request_log_middleware: bool = False,
|
|
124
|
+
**kwargs,
|
|
125
|
+
) -> fastapi.FastAPI:
|
|
126
|
+
settings.config_logger()
|
|
127
|
+
|
|
128
|
+
"""Create a FastAPI app with shared configurations."""
|
|
129
|
+
if settings is None:
|
|
130
|
+
settings = Settings()
|
|
131
|
+
if title is None:
|
|
132
|
+
title = settings.project_name.replace("-", " ").title()
|
|
133
|
+
if description is None:
|
|
134
|
+
description = getattr(settings, "project_description", None)
|
|
135
|
+
if version is None:
|
|
136
|
+
version = getattr(settings, "project_version", "0.1.0")
|
|
137
|
+
base_path: str = settings.base_path
|
|
138
|
+
|
|
139
|
+
if origins is None:
|
|
140
|
+
origins = ["http://localhost:8000"]
|
|
141
|
+
|
|
142
|
+
if lifespan_func is None:
|
|
143
|
+
lifespan_func = lambda app: lifespan(app, worker, init_functions, settings)
|
|
144
|
+
|
|
145
|
+
docs_url = f"{base_path}/docs"
|
|
146
|
+
openapi_url = f"{base_path}/openapi.json"
|
|
147
|
+
|
|
148
|
+
app = fastapi.FastAPI(
|
|
149
|
+
title=title,
|
|
150
|
+
version=version,
|
|
151
|
+
description=description,
|
|
152
|
+
lifespan=lifespan_func,
|
|
153
|
+
contact=contact,
|
|
154
|
+
license_info=license_info,
|
|
155
|
+
docs_url=docs_url,
|
|
156
|
+
openapi_url=openapi_url,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
setup_exception_handlers(app, usso_handler, ufaas_handler, **kwargs)
|
|
160
|
+
setup_middlewares(
|
|
161
|
+
app,
|
|
162
|
+
origins,
|
|
163
|
+
original_host_middleware,
|
|
164
|
+
request_log_middleware,
|
|
165
|
+
**kwargs,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async def logs():
|
|
169
|
+
with open(settings.get_log_config()["info_log_path"], "rb") as f:
|
|
170
|
+
last_100_lines = deque(f, maxlen=100)
|
|
171
|
+
|
|
172
|
+
return [line.decode("utf-8") for line in last_100_lines]
|
|
173
|
+
|
|
174
|
+
app.get(f"{base_path}/health")(health)
|
|
175
|
+
app.get(f"{base_path}/logs", include_in_schema=False)(logs)
|
|
176
|
+
|
|
177
|
+
if serve_coverage:
|
|
178
|
+
app.mount(
|
|
179
|
+
f"{settings.base_path}/coverage",
|
|
180
|
+
StaticFiles(directory=settings.get_coverage_dir()),
|
|
181
|
+
name="coverage",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return app
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""FastAPI server configuration."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
import logging.config
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import dotenv
|
|
9
|
+
from singleton import Singleton
|
|
10
|
+
|
|
11
|
+
dotenv.load_dotenv()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class Settings(metaclass=Singleton):
|
|
16
|
+
"""Server config settings."""
|
|
17
|
+
|
|
18
|
+
# base_dir: Path = Path(__file__).resolve().parent.parent
|
|
19
|
+
root_url: str = os.getenv("DOMAIN", default="http://localhost:8000")
|
|
20
|
+
project_name: str = os.getenv("PROJECT_NAME", default="PROJECT")
|
|
21
|
+
base_path: str = "/api/v1"
|
|
22
|
+
worker_update_time: int = int(os.getenv("WORKER_UPDATE_TIME", default=180))
|
|
23
|
+
testing: bool = os.getenv("DEBUG", default=False)
|
|
24
|
+
|
|
25
|
+
page_max_limit: int = 100
|
|
26
|
+
|
|
27
|
+
mongo_uri: str = os.getenv("MONGO_URI", default="mongodb://localhost:27017/")
|
|
28
|
+
redis_uri: str = os.getenv("REDIS_URI", default="redis://localhost:6379/0")
|
|
29
|
+
|
|
30
|
+
app_id: str = os.getenv("APP_ID")
|
|
31
|
+
app_secret: str = os.getenv("APP_SECRET")
|
|
32
|
+
|
|
33
|
+
JWT_CONFIG: str = os.getenv(
|
|
34
|
+
"USSO_JWT_CONFIG",
|
|
35
|
+
default='{"jwk_url": "https://sso.usso.io/website/jwks.json","type": "RS256","header": {"type": "Cookie", "name": "usso_access_token"} }',
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_coverage_dir(cls):
|
|
40
|
+
return cls.base_dir / "htmlcov"
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get_log_config(cls, console_level: str = "INFO", file_level: str = "INFO", **kwargs):
|
|
44
|
+
log_config = {
|
|
45
|
+
"formatters": {
|
|
46
|
+
"standard": {
|
|
47
|
+
"format": "[{levelname} : {filename}:{lineno} : {asctime} -> {funcName:10}] {message}",
|
|
48
|
+
"style": "{",
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"handlers": {
|
|
52
|
+
"console": {
|
|
53
|
+
"class": "logging.StreamHandler",
|
|
54
|
+
"level": console_level,
|
|
55
|
+
"formatter": "standard",
|
|
56
|
+
},
|
|
57
|
+
"file": {
|
|
58
|
+
"class": "logging.FileHandler",
|
|
59
|
+
"level": file_level,
|
|
60
|
+
"filename": cls.base_dir / "logs" / "info.log",
|
|
61
|
+
"formatter": "standard",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
"loggers": {
|
|
65
|
+
"": {
|
|
66
|
+
"handlers": ["console", "file"],
|
|
67
|
+
"level": "INFO",
|
|
68
|
+
"propagate": True,
|
|
69
|
+
},
|
|
70
|
+
"httpx": {
|
|
71
|
+
"handlers": ["console", "file"],
|
|
72
|
+
"level": "WARNING",
|
|
73
|
+
"propagate": False,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"version": 1,
|
|
77
|
+
}
|
|
78
|
+
return log_config
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def config_logger(cls):
|
|
82
|
+
(cls.base_dir / "logs").mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
logging.config.dictConfig(cls.get_log_config())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from beanie import init_beanie
|
|
4
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
5
|
+
|
|
6
|
+
from fastapi_mongo_base.models import BaseEntity
|
|
7
|
+
from fastapi_mongo_base.utils import basic
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from server.config import Settings
|
|
11
|
+
except ImportError:
|
|
12
|
+
from .config import Settings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def init_mongo_db():
|
|
16
|
+
client = AsyncIOMotorClient(Settings.mongo_uri)
|
|
17
|
+
db = client.get_database(Settings.project_name)
|
|
18
|
+
await init_beanie(
|
|
19
|
+
database=db,
|
|
20
|
+
document_models=[
|
|
21
|
+
cls
|
|
22
|
+
for cls in basic.get_all_subclasses(BaseEntity)
|
|
23
|
+
if not (
|
|
24
|
+
hasattr(cls, "Settings")
|
|
25
|
+
and getattr(cls.Settings, "__abstract__", False)
|
|
26
|
+
)
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
return db
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def init_redis():
|
|
33
|
+
try:
|
|
34
|
+
from redis import Redis as RedisSync
|
|
35
|
+
from redis.asyncio.client import Redis
|
|
36
|
+
|
|
37
|
+
if getattr(Settings, "redis_uri"):
|
|
38
|
+
redis_sync: RedisSync = RedisSync.from_url(Settings.redis_uri)
|
|
39
|
+
redis: Redis = Redis.from_url(Settings.redis_uri)
|
|
40
|
+
except ImportError or AttributeError or Exception as e:
|
|
41
|
+
logging.error(f"Error initializing Redis: {e}")
|
|
42
|
+
redis_sync = None
|
|
43
|
+
redis = None
|
|
44
|
+
|
|
45
|
+
return redis_sync, redis
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Language(str, Enum):
|
|
5
|
+
English = "English"
|
|
6
|
+
Persian = "Persian"
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def has_value(cls, value):
|
|
10
|
+
return value in cls._value2member_map_
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def fa(self):
|
|
14
|
+
return {
|
|
15
|
+
Language.English: "انگلیسی",
|
|
16
|
+
Language.Persian: "فارسی",
|
|
17
|
+
}[self]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def abbreviation(self):
|
|
21
|
+
return {
|
|
22
|
+
Language.English: "en",
|
|
23
|
+
Language.Persian: "fa",
|
|
24
|
+
}[self]
|
|
25
|
+
|
|
26
|
+
def get_dict(self):
|
|
27
|
+
return {
|
|
28
|
+
"en": self.name,
|
|
29
|
+
"fa": self.fa,
|
|
30
|
+
"value": self.value,
|
|
31
|
+
"abbreviation": self.abbreviation,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_choices(cls):
|
|
36
|
+
return [item.get_dict() for item in cls]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
import json_advanced
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
from fastapi.exceptions import (
|
|
8
|
+
HTTPException,
|
|
9
|
+
RequestValidationError,
|
|
10
|
+
ResponseValidationError,
|
|
11
|
+
)
|
|
12
|
+
from fastapi.responses import JSONResponse
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
|
|
15
|
+
error_messages = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseHTTPException(HTTPException):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
status_code: int,
|
|
22
|
+
error: str,
|
|
23
|
+
message: str = None,
|
|
24
|
+
detail: str = None,
|
|
25
|
+
**kwargs,
|
|
26
|
+
):
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
self.error = error
|
|
29
|
+
self.message = message
|
|
30
|
+
if message is None:
|
|
31
|
+
self.message = error_messages.get(error, error)
|
|
32
|
+
if detail is None:
|
|
33
|
+
detail = self.message
|
|
34
|
+
self.detail = detail
|
|
35
|
+
super().__init__(status_code, detail=detail, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def base_http_exception_handler(request: Request, exc: BaseHTTPException):
|
|
39
|
+
return JSONResponse(
|
|
40
|
+
status_code=exc.status_code,
|
|
41
|
+
content={"message": exc.message, "error": exc.error},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def pydantic_exception_handler(request: Request, exc: ValidationError):
|
|
46
|
+
return JSONResponse(
|
|
47
|
+
status_code=500,
|
|
48
|
+
content={
|
|
49
|
+
"message": str(exc),
|
|
50
|
+
"error": "Exception",
|
|
51
|
+
"errors": json.loads(json_advanced.dumps(exc.errors())),
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def request_validation_exception_handler(
|
|
57
|
+
request: Request, exc: RequestValidationError
|
|
58
|
+
):
|
|
59
|
+
logging.error(
|
|
60
|
+
f"request_validation_exception: {request.url} {exc}\n{(await request.body())[:100]}"
|
|
61
|
+
)
|
|
62
|
+
from fastapi.exception_handlers import (
|
|
63
|
+
request_validation_exception_handler as default_handler,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return await default_handler(request, exc)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def general_exception_handler(request: Request, exc: Exception):
|
|
70
|
+
traceback_str = "".join(traceback.format_tb(exc.__traceback__))
|
|
71
|
+
logging.error(f"Exception: {traceback_str} {exc}")
|
|
72
|
+
logging.error(f"Exception on request: {request.url}")
|
|
73
|
+
return JSONResponse(
|
|
74
|
+
status_code=500,
|
|
75
|
+
content={"message": str(exc), "error": "Exception"},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# A dictionary for dynamic registration
|
|
80
|
+
EXCEPTION_HANDLERS = {
|
|
81
|
+
BaseHTTPException: base_http_exception_handler,
|
|
82
|
+
ValidationError: pydantic_exception_handler,
|
|
83
|
+
ResponseValidationError: pydantic_exception_handler,
|
|
84
|
+
RequestValidationError: request_validation_exception_handler,
|
|
85
|
+
Exception: general_exception_handler,
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Create logging middleware
|
|
8
|
+
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
9
|
+
async def dispatch(self, request: Request, call_next):
|
|
10
|
+
# Log the request details
|
|
11
|
+
# logging.info(f"Request: {request.method} {request.url}")
|
|
12
|
+
# logging.info(f"Headers: {request.headers}")
|
|
13
|
+
|
|
14
|
+
# You can also log other request details like body, client IP, etc.
|
|
15
|
+
# Accessing the request body requires it to be async, managing it carefully since it is a stream
|
|
16
|
+
|
|
17
|
+
response = await call_next(request)
|
|
18
|
+
|
|
19
|
+
# You can also log response details here if needed
|
|
20
|
+
logging.info(f"request: {request.method} {request.url} {response.status_code}")
|
|
21
|
+
|
|
22
|
+
return response
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from fastapi import Request
|
|
5
|
+
|
|
6
|
+
from .schemas import BaseEntitySchema, OwnedEntitySchema
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T", bound=BaseEntitySchema)
|
|
9
|
+
OT = TypeVar("OT", bound=OwnedEntitySchema)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_dto(cls: OT):
|
|
13
|
+
async def dto(
|
|
14
|
+
request: Request,
|
|
15
|
+
*,
|
|
16
|
+
user_id: uuid.UUID = None,
|
|
17
|
+
business_name: str = None,
|
|
18
|
+
**kwargs,
|
|
19
|
+
):
|
|
20
|
+
form_data = await request.json()
|
|
21
|
+
|
|
22
|
+
if hasattr(cls, "create_field_set") and cls.create_field_set():
|
|
23
|
+
for key in form_data.keys():
|
|
24
|
+
if key not in cls.create_field_set():
|
|
25
|
+
form_data.pop(key, None)
|
|
26
|
+
|
|
27
|
+
if hasattr(cls, "create_exclude_set") and cls.create_exclude_set():
|
|
28
|
+
for key in cls.create_exclude_set():
|
|
29
|
+
form_data.pop(key, None)
|
|
30
|
+
|
|
31
|
+
if user_id:
|
|
32
|
+
form_data["user_id"] = user_id
|
|
33
|
+
|
|
34
|
+
if business_name:
|
|
35
|
+
form_data["business_name"] = business_name
|
|
36
|
+
|
|
37
|
+
return cls(**form_data)
|
|
38
|
+
|
|
39
|
+
return dto
|