fastapi-mongo-base 0.8.30__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.8.30/LICENSE.txt +19 -0
- fastapi_mongo_base-0.8.30/PKG-INFO +69 -0
- fastapi_mongo_base-0.8.30/README.md +7 -0
- fastapi_mongo_base-0.8.30/pyproject.toml +48 -0
- fastapi_mongo_base-0.8.30/setup.cfg +4 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/__init__.py +0 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/cached.py +115 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/core/__init__.py +0 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/core/app_factory.py +184 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/core/config.py +84 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/core/db.py +45 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/core/enums.py +36 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/core/exceptions.py +86 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/core/middlewares.py +22 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/handlers.py +39 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/models.py +384 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/routes.py +293 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/schemas.py +107 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/tasks.py +291 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/utils/__init__.py +0 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/utils/aionetwork.py +90 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/utils/basic.py +75 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/utils/bsontools.py +25 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/utils/imagetools.py +261 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base/utils/texttools.py +207 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base.egg-info/PKG-INFO +69 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base.egg-info/SOURCES.txt +28 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base.egg-info/dependency_links.txt +1 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base.egg-info/requires.txt +21 -0
- fastapi_mongo_base-0.8.30/src/fastapi_mongo_base.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2016 The Python Packaging Authority (PyPA)
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: fastapi-mongo-base
|
|
3
|
+
Version: 0.8.30
|
|
4
|
+
Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
|
|
5
|
+
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
6
|
+
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
7
|
+
License: Copyright (c) 2016 The Python Packaging Authority (PyPA)
|
|
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
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/mahdikiani/fastapi-mongo-base-app
|
|
28
|
+
Project-URL: Bug Reports, https://github.com/mahdikiani/fastapi-mongo-base-app/issues
|
|
29
|
+
Project-URL: Funding, https://github.com/mahdikiani/fastapi-mongo-base-app
|
|
30
|
+
Project-URL: Say Thanks!, https://saythanks.io/to/mahdikiani
|
|
31
|
+
Project-URL: Source, https://github.com/mahdikiani/fastapi-mongo-base-app
|
|
32
|
+
Keywords: fastapi,mongodb,beanie
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
42
|
+
Requires-Python: >=3.9
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
License-File: LICENSE.txt
|
|
45
|
+
Requires-Dist: pydantic>=1.8.2
|
|
46
|
+
Requires-Dist: requests>=2.26.0
|
|
47
|
+
Requires-Dist: pyjwt[crypto]
|
|
48
|
+
Requires-Dist: singleton_package
|
|
49
|
+
Requires-Dist: fastapi>=0.65.0
|
|
50
|
+
Requires-Dist: uvicorn[standard]>=0.13.0
|
|
51
|
+
Provides-Extra: image
|
|
52
|
+
Requires-Dist: Pillow>=9.0.0; extra == "image"
|
|
53
|
+
Provides-Extra: test
|
|
54
|
+
Requires-Dist: coverage; extra == "test"
|
|
55
|
+
Provides-Extra: image-similarity
|
|
56
|
+
Requires-Dist: numpy>=1.21.0; extra == "image-similarity"
|
|
57
|
+
Requires-Dist: imagededup>=0.3.0; extra == "image-similarity"
|
|
58
|
+
Provides-Extra: all
|
|
59
|
+
Requires-Dist: Pillow>=9.0.0; extra == "all"
|
|
60
|
+
Requires-Dist: numpy>=1.21.0; extra == "all"
|
|
61
|
+
Requires-Dist: imagededup>=0.3.0; extra == "all"
|
|
62
|
+
|
|
63
|
+
# Fastapi Mongo
|
|
64
|
+
|
|
65
|
+
## Contributing
|
|
66
|
+
Contributions are welcome! See CONTRIBUTING.md for more details on how to get involved.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
Distributed under the MIT License. See LICENSE for more information.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastapi-mongo-base"
|
|
7
|
+
version = "0.8.30"
|
|
8
|
+
description = "A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {file = "LICENSE.txt"}
|
|
12
|
+
keywords = ["fastapi", "mongodb", "beanie"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Mahdi Kiani", email = "mahdikiany@gmail.com"}
|
|
15
|
+
]
|
|
16
|
+
maintainers = [
|
|
17
|
+
{name = "Mahdi Kiani", email = "mahdikiany@gmail.com"}
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Topic :: Software Development :: Build Tools",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"pydantic>=1.8.2",
|
|
32
|
+
"requests>=2.26.0",
|
|
33
|
+
"pyjwt[crypto]",
|
|
34
|
+
"singleton_package",
|
|
35
|
+
"fastapi>=0.65.0",
|
|
36
|
+
"uvicorn[standard]>=0.13.0"
|
|
37
|
+
]
|
|
38
|
+
optional-dependencies = {"image" = ["Pillow>=9.0.0"],"test" = ["coverage"], "image-similarity" = ["numpy>=1.21.0", "imagededup>=0.3.0"], "all" = ["Pillow>=9.0.0", "numpy>=1.21.0", "imagededup>=0.3.0"]}
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
"Homepage" = "https://github.com/mahdikiani/fastapi-mongo-base-app"
|
|
42
|
+
"Bug Reports" = "https://github.com/mahdikiani/fastapi-mongo-base-app/issues"
|
|
43
|
+
"Funding" = "https://github.com/mahdikiani/fastapi-mongo-base-app"
|
|
44
|
+
"Say Thanks!" = "https://saythanks.io/to/mahdikiani"
|
|
45
|
+
"Source" = "https://github.com/mahdikiani/fastapi-mongo-base-app"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools]
|
|
48
|
+
package-data = {"usso" = ["*.dat"]}
|
|
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]
|