fastlib-py 0.5.2__tar.gz → 0.5.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.
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/PKG-INFO +1 -1
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/exception_handler.py +9 -7
- fastlib_py-0.5.4/fastlib/logging/__init__.py +15 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/logging/handlers.py +43 -20
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/db_session.py +30 -15
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/migrate.py +5 -2
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/snowflake_util.py +6 -2
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/PKG-INFO +1 -1
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/pyproject.toml +1 -1
- fastlib_py-0.5.2/fastlib/logging/__init__.py +0 -10
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/LICENSE +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/README.md +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/_memony_cache.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/_redis_cache.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/manager.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_database_config.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_log_config.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_security_config.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_server_config.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/base.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/decorator.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/loader.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/manager.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/registry.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/utils.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/constants.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/contextvars.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/db_engine.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/enums.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/base.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/exception_manager.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/manager.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/messages.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/types.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/utils.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/base_mapper.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/impl/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/i18n.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/jwt.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/openapi.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/py.typed +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/request.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/response.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/router.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/schema.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/security.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/base_service.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/impl/base_service_impl.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/enums.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/event.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/handler.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/schema.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/sse.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/excel_util.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/file_util.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/http_util.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/security_util.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/validate_util.py +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/SOURCES.txt +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/requires.txt +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/top_level.txt +0 -0
- {fastlib_py-0.5.2 → fastlib_py-0.5.4}/setup.cfg +0 -0
|
@@ -179,19 +179,21 @@ async def custom_exception_handler(request: Request, exc: BaseException):
|
|
|
179
179
|
log_exception(exc, request_info)
|
|
180
180
|
error_info = exc.code
|
|
181
181
|
error_code = error_info.code
|
|
182
|
+
error_payload: dict = {
|
|
183
|
+
"code": error_code,
|
|
184
|
+
"message": exc.message,
|
|
185
|
+
}
|
|
186
|
+
if getattr(exc, "details", None) is not None:
|
|
187
|
+
error_payload["details"] = exc.details
|
|
188
|
+
|
|
182
189
|
if is_auth_errors_code(error_code):
|
|
183
190
|
return JSONResponse(
|
|
184
191
|
status_code=HTTPStatus.UNAUTHORIZED.value,
|
|
185
|
-
content=
|
|
192
|
+
content=error_payload,
|
|
186
193
|
)
|
|
187
194
|
|
|
188
195
|
return JSONResponse(
|
|
189
|
-
content={
|
|
190
|
-
"error": {
|
|
191
|
-
"code": error_code,
|
|
192
|
-
"message": exc.message,
|
|
193
|
-
}
|
|
194
|
-
}
|
|
196
|
+
content={"error": error_payload}
|
|
195
197
|
)
|
|
196
198
|
|
|
197
199
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Export logging module"""
|
|
3
|
+
|
|
4
|
+
from .handlers import Logger
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Logger",
|
|
8
|
+
"logger",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name: str) -> Logger:
|
|
13
|
+
if name == "logger":
|
|
14
|
+
return Logger.initialize()
|
|
15
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -14,7 +14,7 @@ from fastlib.config.manager import ConfigManager
|
|
|
14
14
|
|
|
15
15
|
class Logger:
|
|
16
16
|
"""
|
|
17
|
-
Enhanced logging tool
|
|
17
|
+
Enhanced logging tool with loguru-compatible ``str.format`` messages.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
_instances: dict[str, "Logger"] = {}
|
|
@@ -137,30 +137,53 @@ class Logger:
|
|
|
137
137
|
|
|
138
138
|
return config.default_format
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"""Log debug message."""
|
|
143
|
-
logger.bind(logger_name=self.name).debug(message, **kwargs)
|
|
140
|
+
def _bound(self):
|
|
141
|
+
"""Return a loguru logger bound to this instance.
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
``opt(depth=1)`` skips this wrapper so ``{name}:{function}:{line}``
|
|
144
|
+
points at the caller, matching loguru's own logging methods.
|
|
145
|
+
"""
|
|
146
|
+
return logger.bind(logger_name=self.name).opt(depth=1)
|
|
147
|
+
|
|
148
|
+
def bind(self, **kwargs):
|
|
149
|
+
"""Bind extra fields onto the loguru ``extra`` dict, like ``logger.bind()``."""
|
|
150
|
+
return logger.bind(logger_name=self.name, **kwargs)
|
|
151
|
+
|
|
152
|
+
def trace(self, message: str, *args, **kwargs) -> None:
|
|
153
|
+
"""Log ``message.format(*args, **kwargs)`` with severity ``TRACE``."""
|
|
154
|
+
self._bound().trace(message, *args, **kwargs)
|
|
155
|
+
|
|
156
|
+
def debug(self, message: str, *args, **kwargs) -> None:
|
|
157
|
+
"""Log ``message.format(*args, **kwargs)`` with severity ``DEBUG``."""
|
|
158
|
+
self._bound().debug(message, *args, **kwargs)
|
|
159
|
+
|
|
160
|
+
def info(self, message: str, *args, **kwargs) -> None:
|
|
161
|
+
"""Log ``message.format(*args, **kwargs)`` with severity ``INFO``."""
|
|
162
|
+
self._bound().info(message, *args, **kwargs)
|
|
163
|
+
|
|
164
|
+
def success(self, message: str, *args, **kwargs) -> None:
|
|
165
|
+
"""Log ``message.format(*args, **kwargs)`` with severity ``SUCCESS``."""
|
|
166
|
+
self._bound().success(message, *args, **kwargs)
|
|
167
|
+
|
|
168
|
+
def warning(self, message: str, *args, **kwargs) -> None:
|
|
169
|
+
"""Log ``message.format(*args, **kwargs)`` with severity ``WARNING``."""
|
|
170
|
+
self._bound().warning(message, *args, **kwargs)
|
|
148
171
|
|
|
149
|
-
def
|
|
150
|
-
"""Log
|
|
151
|
-
|
|
172
|
+
def error(self, message: str, *args, **kwargs) -> None:
|
|
173
|
+
"""Log ``message.format(*args, **kwargs)`` with severity ``ERROR``."""
|
|
174
|
+
self._bound().error(message, *args, **kwargs)
|
|
152
175
|
|
|
153
|
-
def
|
|
154
|
-
"""Log
|
|
155
|
-
|
|
176
|
+
def critical(self, message: str, *args, **kwargs) -> None:
|
|
177
|
+
"""Log ``message.format(*args, **kwargs)`` with severity ``CRITICAL``."""
|
|
178
|
+
self._bound().critical(message, *args, **kwargs)
|
|
156
179
|
|
|
157
|
-
def
|
|
158
|
-
"""Log
|
|
159
|
-
|
|
180
|
+
def exception(self, message: str, *args, **kwargs) -> None:
|
|
181
|
+
"""Log ``message.format(*args, **kwargs)`` as ``ERROR`` with traceback."""
|
|
182
|
+
self._bound().exception(message, *args, **kwargs)
|
|
160
183
|
|
|
161
|
-
def
|
|
162
|
-
"""Log
|
|
163
|
-
|
|
184
|
+
def log(self, level, message: str, *args, **kwargs) -> None:
|
|
185
|
+
"""Log ``message.format(*args, **kwargs)`` with the given severity."""
|
|
186
|
+
self._bound().log(level, message, *args, **kwargs)
|
|
164
187
|
|
|
165
188
|
@classmethod
|
|
166
189
|
def reset(cls) -> None:
|
|
@@ -7,12 +7,7 @@ from sqlalchemy.engine import Engine
|
|
|
7
7
|
from sqlalchemy.engine.url import URL
|
|
8
8
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
9
9
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
10
|
-
from starlette.
|
|
11
|
-
BaseHTTPMiddleware,
|
|
12
|
-
RequestResponseEndpoint,
|
|
13
|
-
)
|
|
14
|
-
from starlette.requests import Request
|
|
15
|
-
from starlette.types import ASGIApp
|
|
10
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
16
11
|
|
|
17
12
|
try:
|
|
18
13
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
@@ -25,8 +20,15 @@ def create_middleware_and_session_proxy():
|
|
|
25
20
|
_Session: async_sessionmaker | None = None
|
|
26
21
|
_session: ContextVar[AsyncSession | None] = ContextVar("_session", default=None)
|
|
27
22
|
|
|
28
|
-
class SQLAlchemyMiddleware
|
|
29
|
-
"""
|
|
23
|
+
class SQLAlchemyMiddleware:
|
|
24
|
+
"""
|
|
25
|
+
Pure ASGI middleware that keeps one AsyncSession alive for the full
|
|
26
|
+
request — including StreamingResponse / SSE body iteration.
|
|
27
|
+
|
|
28
|
+
BaseHTTPMiddleware closes the session as soon as response.start is
|
|
29
|
+
sent, which races with generators still using db.session (SQLAlchemy
|
|
30
|
+
isce: close() while _connection_for_bind() is in progress).
|
|
31
|
+
"""
|
|
30
32
|
|
|
31
33
|
def __init__(
|
|
32
34
|
self,
|
|
@@ -38,7 +40,7 @@ def create_middleware_and_session_proxy():
|
|
|
38
40
|
commit_on_exit: bool = True,
|
|
39
41
|
):
|
|
40
42
|
"""Initialize the middleware with database configuration."""
|
|
41
|
-
|
|
43
|
+
self.app = app
|
|
42
44
|
self.commit_on_exit = commit_on_exit
|
|
43
45
|
engine_args = engine_args or {}
|
|
44
46
|
session_args = session_args or {}
|
|
@@ -60,10 +62,13 @@ def create_middleware_and_session_proxy():
|
|
|
60
62
|
**session_args,
|
|
61
63
|
)
|
|
62
64
|
|
|
63
|
-
async def
|
|
64
|
-
""
|
|
65
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
66
|
+
if scope["type"] != "http":
|
|
67
|
+
await self.app(scope, receive, send)
|
|
68
|
+
return
|
|
69
|
+
|
|
65
70
|
async with DBSession(commit_on_exit=self.commit_on_exit):
|
|
66
|
-
|
|
71
|
+
await self.app(scope, receive, send)
|
|
67
72
|
|
|
68
73
|
class DBSessionMeta(type):
|
|
69
74
|
"""Metaclass for DBSession providing session property."""
|
|
@@ -86,6 +91,7 @@ def create_middleware_and_session_proxy():
|
|
|
86
91
|
def __init__(self, session_args: dict = None, commit_on_exit: bool = False):
|
|
87
92
|
"""Initialize session context manager."""
|
|
88
93
|
self.token = None
|
|
94
|
+
self._owned_session: AsyncSession | None = None
|
|
89
95
|
self.session_args = session_args or {}
|
|
90
96
|
self.commit_on_exit = commit_on_exit
|
|
91
97
|
|
|
@@ -94,12 +100,19 @@ def create_middleware_and_session_proxy():
|
|
|
94
100
|
if _Session is None:
|
|
95
101
|
raise RuntimeError("Session is not initialised")
|
|
96
102
|
|
|
97
|
-
self.
|
|
103
|
+
self._owned_session = _Session(**self.session_args) # type: ignore
|
|
104
|
+
self.token = _session.set(self._owned_session)
|
|
98
105
|
return type(self)
|
|
99
106
|
|
|
100
107
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
101
108
|
"""Exit session context, handling commit/rollback."""
|
|
102
|
-
session
|
|
109
|
+
# Always close the session this context created — never the nested
|
|
110
|
+
# current ContextVar value (which may belong to an inner async with db()).
|
|
111
|
+
session = self._owned_session
|
|
112
|
+
if session is None:
|
|
113
|
+
if self.token is not None:
|
|
114
|
+
_session.reset(self.token)
|
|
115
|
+
return
|
|
103
116
|
|
|
104
117
|
try:
|
|
105
118
|
if exc_type is not None:
|
|
@@ -108,7 +121,9 @@ def create_middleware_and_session_proxy():
|
|
|
108
121
|
await session.commit()
|
|
109
122
|
finally:
|
|
110
123
|
await session.close()
|
|
111
|
-
|
|
124
|
+
if self.token is not None:
|
|
125
|
+
_session.reset(self.token)
|
|
126
|
+
self._owned_session = None
|
|
112
127
|
|
|
113
128
|
return SQLAlchemyMiddleware, DBSession
|
|
114
129
|
|
|
@@ -15,10 +15,13 @@ for parent in current_path.parents:
|
|
|
15
15
|
if not src_parent:
|
|
16
16
|
raise FileNotFoundError("Can not found src dir")
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# Legacy layout: src/main/app/model
|
|
19
|
+
# Domain layout (ginvest): src/main/app/domains/*/model
|
|
20
|
+
legacy_model_path = src_parent / "src" / "main" / "app" / "model"
|
|
21
|
+
domains_model_path = src_parent / "src" / "main" / "app" / "domains"
|
|
19
22
|
|
|
20
23
|
# List of directories to scan for model files
|
|
21
|
-
MODEL_PACKAGES = [
|
|
24
|
+
MODEL_PACKAGES = [legacy_model_path, domains_model_path]
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
def import_sql_models(
|
|
@@ -75,10 +75,14 @@ def generator(
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
global_generator = generator()
|
|
78
|
+
# A generator can't be resumed by two threads at once ("generator already executing");
|
|
79
|
+
# the lock inside generator() only guards its body, so serialize next() here.
|
|
80
|
+
_global_lock = threading.Lock()
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
def snowflake_id() -> int:
|
|
81
84
|
"""
|
|
82
|
-
Returns a unique snowflake ID.
|
|
85
|
+
Returns a unique snowflake ID. Thread-safe.
|
|
83
86
|
"""
|
|
84
|
-
|
|
87
|
+
with _global_lock:
|
|
88
|
+
return next(global_generator)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|