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