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.
Files changed (77) hide show
  1. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/PKG-INFO +1 -1
  2. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/exception_handler.py +9 -7
  3. fastlib_py-0.5.4/fastlib/logging/__init__.py +15 -0
  4. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/logging/handlers.py +43 -20
  5. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/db_session.py +30 -15
  6. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/migrate.py +5 -2
  7. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/snowflake_util.py +6 -2
  8. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/PKG-INFO +1 -1
  9. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/pyproject.toml +1 -1
  10. fastlib_py-0.5.2/fastlib/logging/__init__.py +0 -10
  11. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/LICENSE +0 -0
  12. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/README.md +0 -0
  13. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/__init__.py +0 -0
  14. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/__init__.py +0 -0
  15. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/_memony_cache.py +0 -0
  16. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/_redis_cache.py +0 -0
  17. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/cache/manager.py +0 -0
  18. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/__init__.py +0 -0
  19. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_database_config.py +0 -0
  20. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_log_config.py +0 -0
  21. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_security_config.py +0 -0
  22. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/_server_config.py +0 -0
  23. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/base.py +0 -0
  24. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/decorator.py +0 -0
  25. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/loader.py +0 -0
  26. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/manager.py +0 -0
  27. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/registry.py +0 -0
  28. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/config/utils.py +0 -0
  29. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/constants.py +0 -0
  30. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/contextvars.py +0 -0
  31. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/db_engine.py +0 -0
  32. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/enums.py +0 -0
  33. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/__init__.py +0 -0
  34. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/base.py +0 -0
  35. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/exception/exception_manager.py +0 -0
  36. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/__init__.py +0 -0
  37. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/manager.py +0 -0
  38. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/messages.py +0 -0
  39. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/types.py +0 -0
  40. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/i18n/utils.py +0 -0
  41. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/__init__.py +0 -0
  42. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/base_mapper.py +0 -0
  43. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/impl/__init__.py +0 -0
  44. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
  45. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/__init__.py +0 -0
  46. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/i18n.py +0 -0
  47. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/middleware/jwt.py +0 -0
  48. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/openapi.py +0 -0
  49. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/py.typed +0 -0
  50. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/request.py +0 -0
  51. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/response.py +0 -0
  52. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/router.py +0 -0
  53. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/schema.py +0 -0
  54. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/security.py +0 -0
  55. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/__init__.py +0 -0
  56. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/base_service.py +0 -0
  57. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/impl/__init__.py +0 -0
  58. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/service/impl/base_service_impl.py +0 -0
  59. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/__init__.py +0 -0
  60. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/enums.py +0 -0
  61. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/event.py +0 -0
  62. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/handler.py +0 -0
  63. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/schema.py +0 -0
  64. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/stream/sse.py +0 -0
  65. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/__init__.py +0 -0
  66. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/excel_util.py +0 -0
  67. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/file_util.py +0 -0
  68. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/http_util.py +0 -0
  69. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/ip_util.py +0 -0
  70. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/security_util.py +0 -0
  71. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/str_util.py +0 -0
  72. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib/utils/validate_util.py +0 -0
  73. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/SOURCES.txt +0 -0
  74. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/dependency_links.txt +0 -0
  75. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/requires.txt +0 -0
  76. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/fastlib_py.egg-info/top_level.txt +0 -0
  77. {fastlib_py-0.5.2 → fastlib_py-0.5.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Fastlib, high performance, easy to use, fast to code, ready for production
5
5
  Author-email: tyvekZhang <tyvekzhang@gmail.com>
6
6
  Maintainer-email: tyvekZhang <tyvekzhang@gmail.com>
@@ -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={"code": error_code, "message": exc.message},
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 support structured logging.
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
- # Convenience methods for logging
141
- def debug(self, message: str, **kwargs) -> None:
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
- def info(self, message: str, **kwargs) -> None:
146
- """Log info message."""
147
- logger.bind(logger_name=self.name).info(message, **kwargs)
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 warning(self, message: str, **kwargs) -> None:
150
- """Log warning message."""
151
- logger.bind(logger_name=self.name).warning(message, **kwargs)
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 error(self, message: str, **kwargs) -> None:
154
- """Log error message."""
155
- logger.bind(logger_name=self.name).error(message, **kwargs)
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 critical(self, message: str, **kwargs) -> None:
158
- """Log critical message."""
159
- logger.bind(logger_name=self.name).critical(message, **kwargs)
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 exception(self, message: str, **kwargs) -> None:
162
- """Log exception with traceback."""
163
- logger.bind(logger_name=self.name).exception(message, **kwargs)
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.middleware.base import (
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(BaseHTTPMiddleware):
29
- """Middleware for managing SQLAlchemy sessions in FastAPI applications."""
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
- super().__init__(app)
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 dispatch(self, request: Request, call_next: RequestResponseEndpoint):
64
- """Manage database session for each request."""
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
- return await call_next(request)
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.token = _session.set(_Session(**self.session_args)) # type: ignore
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 = _session.get()
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
- _session.reset(self.token)
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
- model_path = src_parent / "src" / "main" / "app" / "model"
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 = [model_path]
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
- return next(global_generator)
87
+ with _global_lock:
88
+ return next(global_generator)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Fastlib, high performance, easy to use, fast to code, ready for production
5
5
  Author-email: tyvekZhang <tyvekzhang@gmail.com>
6
6
  Maintainer-email: tyvekZhang <tyvekzhang@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastlib-py"
3
- version = "0.5.2"
3
+ version = "0.5.4"
4
4
  description = "Fastlib, high performance, easy to use, fast to code, ready for production"
5
5
  readme = "README.md"
6
6
  keywords = ["fastapi", "library", "sqlmodel", "web"]
@@ -1,10 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """Export logging module"""
3
-
4
- from .handlers import Logger
5
-
6
- logger = Logger.initialize()
7
-
8
- __all__ = [
9
- "logger",
10
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes