fastlib-py 0.2.2__tar.gz → 0.2.3__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 (83) hide show
  1. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/PKG-INFO +1 -1
  2. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/utils.py +1 -1
  3. fastlib_py-0.2.3/fastlib/decorator/base.py +237 -0
  4. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/enums/__init__.py +1 -1
  5. fastlib_py-0.2.3/fastlib/enums/base.py +71 -0
  6. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/exception/__init__.py +1 -1
  7. fastlib_py-0.2.2/fastlib/exception/custom_exception.py → fastlib_py-0.2.3/fastlib/exception/base.py +10 -2
  8. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/exception/exception_handler.py +8 -7
  9. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/exception/exception_manager.py +1 -1
  10. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/middleware/db_session.py +4 -4
  11. fastlib_py-0.2.3/fastlib/middleware/i18n.py +40 -0
  12. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/middleware/jwt.py +1 -1
  13. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/router/__init__.py +1 -1
  14. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/schema/__init__.py +2 -2
  15. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/schema/response.py +2 -2
  16. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/security/security.py +2 -24
  17. fastlib_py-0.2.3/fastlib/service/impl/__init__.py +0 -0
  18. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/session/db_engine.py +0 -4
  19. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib_py.egg-info/PKG-INFO +1 -1
  20. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib_py.egg-info/SOURCES.txt +6 -8
  21. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/pyproject.toml +1 -1
  22. fastlib_py-0.2.2/fastlib/enums/base_error_code.py +0 -15
  23. fastlib_py-0.2.2/fastlib/enums/enum.py +0 -33
  24. fastlib_py-0.2.2/fastlib/middleware/i18n.py +0 -57
  25. fastlib_py-0.2.2/fastlib/middleware/log.py +0 -49
  26. fastlib_py-0.2.2/fastlib/model/__init__.py +0 -5
  27. fastlib_py-0.2.2/fastlib/model/base_model.py +0 -62
  28. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/LICENSE +0 -0
  29. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/README.md +0 -0
  30. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/__init__.py +0 -0
  31. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/cache/__init__.py +0 -0
  32. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/cache/base.py +0 -0
  33. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/cache/disk_cache.py +0 -0
  34. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/cache/manager.py +0 -0
  35. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/cache/redis_cache.py +0 -0
  36. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/__init__.py +0 -0
  37. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/base.py +0 -0
  38. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/database_config.py +0 -0
  39. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/loader.py +0 -0
  40. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/manager.py +0 -0
  41. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/registry.py +0 -0
  42. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/security_config.py +0 -0
  43. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/config/server_config.py +0 -0
  44. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/constant/__init__.py +0 -0
  45. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/constant/constant.py +0 -0
  46. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/context/__init__.py +0 -0
  47. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/context/contextvars.py +0 -0
  48. {fastlib_py-0.2.2/fastlib/mapper → fastlib_py-0.2.3/fastlib/decorator}/__init__.py +0 -0
  49. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/i18n/manager.py +0 -0
  50. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/i18n/messages.py +0 -0
  51. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/i18n/types.py +0 -0
  52. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/i18n/utils.py +0 -0
  53. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/logging/__init__.py +0 -0
  54. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/logging/config.py +0 -0
  55. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/logging/handlers.py +0 -0
  56. {fastlib_py-0.2.2/fastlib/mapper/impl → fastlib_py-0.2.3/fastlib/mapper}/__init__.py +0 -0
  57. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/mapper/base_mapper.py +0 -0
  58. {fastlib_py-0.2.2/fastlib/service → fastlib_py-0.2.3/fastlib/mapper/impl}/__init__.py +0 -0
  59. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
  60. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/middleware/__init__.py +0 -0
  61. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/migrate/migrate.py +0 -0
  62. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/openapi/__init__.py +0 -0
  63. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/openapi/offline.py +0 -0
  64. /fastlib_py-0.2.2/fastlib/router/router_v1.py → /fastlib_py-0.2.3/fastlib/router/base.py +0 -0
  65. /fastlib_py-0.2.2/fastlib/schema/schema.py → /fastlib_py-0.2.3/fastlib/schema/base.py +0 -0
  66. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/security/__init__.py +0 -0
  67. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/security/utils.py +0 -0
  68. {fastlib_py-0.2.2/fastlib/service/impl → fastlib_py-0.2.3/fastlib/service}/__init__.py +0 -0
  69. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/service/base_service.py +0 -0
  70. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/service/impl/base_service_impl.py +0 -0
  71. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/session/__init__.py +0 -0
  72. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/session/db_session.py +0 -0
  73. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/utils/__init__.py +0 -0
  74. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/utils/excel_util.py +0 -0
  75. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/utils/file_util.py +0 -0
  76. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/utils/http_util.py +0 -0
  77. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/utils/ip_util.py +0 -0
  78. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/utils/snowflake_util.py +0 -0
  79. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib/utils/str_util.py +0 -0
  80. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib_py.egg-info/dependency_links.txt +0 -0
  81. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib_py.egg-info/requires.txt +0 -0
  82. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/fastlib_py.egg-info/top_level.txt +0 -0
  83. {fastlib_py-0.2.2 → fastlib_py-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.2.2
3
+ Version: 0.2.3
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>
@@ -8,7 +8,7 @@ from urllib.parse import urlparse
8
8
 
9
9
  from loguru import logger
10
10
 
11
- from fastlib.enums.enum import DBTypeEnum
11
+ from fastlib.enums.base import DBTypeEnum
12
12
  from fastlib.utils import file_util
13
13
 
14
14
  try:
@@ -0,0 +1,237 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Commonly used decorator modules"""
3
+ import asyncio
4
+ from functools import wraps
5
+ import functools
6
+ import hashlib
7
+ import json
8
+ import time
9
+ from typing import Any, Callable, Optional
10
+ from loguru import Logger
11
+
12
+ from fastlib.cache.manager import get_cache_client
13
+
14
+
15
+ def log_request_time(logger_name: Optional[str] = None):
16
+ """
17
+ Decorator to log API request execution time.
18
+
19
+ Args:
20
+ logger_name: Name of the logger instance to use. If None, uses default logger.
21
+
22
+ Returns:
23
+ Decorator function
24
+ """
25
+
26
+ def decorator(func: Callable):
27
+ @wraps(func)
28
+ async def async_wrapper(*args, **kwargs):
29
+ return await _time_request_async(func, logger_name, *args, **kwargs)
30
+
31
+ @wraps(func)
32
+ def sync_wrapper(*args, **kwargs):
33
+ return _time_request_sync(func, logger_name, *args, **kwargs)
34
+
35
+ # Return appropriate wrapper based on function type
36
+ if asyncio.iscoroutinefunction(func):
37
+ return async_wrapper
38
+ return sync_wrapper
39
+
40
+ return decorator
41
+
42
+
43
+ async def _time_request_async(func: Callable, logger_name: str, *args, **kwargs) -> Any:
44
+ """
45
+ Internal function to time and log async request execution.
46
+
47
+ Args:
48
+ func: The decorated function
49
+ logger_name: Logger instance name
50
+ *args: Function arguments
51
+ **kwargs: Function keyword arguments
52
+
53
+ Returns:
54
+ Result of the function execution
55
+ """
56
+ # Get logger instance
57
+ logger_instance = Logger.get_instance(logger_name)
58
+
59
+ # Record start time
60
+ start_time = time.time()
61
+
62
+ try:
63
+ # Execute the function
64
+ result = await func(*args, **kwargs)
65
+
66
+ # Calculate execution time
67
+ execution_time = time.time() - start_time
68
+
69
+ # Log success
70
+ logger_instance.info(
71
+ f"API endpoint {func.__name__} executed successfully",
72
+ execution_time_ms=round(execution_time * 1000, 2),
73
+ status="success",
74
+ endpoint=func.__name__,
75
+ )
76
+
77
+ return result
78
+
79
+ except Exception as e:
80
+ # Calculate execution time (even if error occurred)
81
+ execution_time = time.time() - start_time
82
+
83
+ # Log error
84
+ logger_instance.error(
85
+ f"API endpoint {func.__name__} execution failed: {str(e)}",
86
+ execution_time_ms=round(execution_time * 1000, 2),
87
+ status="error",
88
+ endpoint=func.__name__,
89
+ exc_info=e,
90
+ )
91
+
92
+ # Re-raise the exception
93
+ raise
94
+
95
+
96
+ def _time_request_sync(func: Callable, logger_name: str, *args, **kwargs) -> Any:
97
+ """
98
+ Internal function to time and log synchronous request execution.
99
+
100
+ Args:
101
+ func: The decorated function
102
+ logger_name: Logger instance name
103
+ *args: Function arguments
104
+ **kwargs: Function keyword arguments
105
+
106
+ Returns:
107
+ Result of the function execution
108
+ """
109
+ # Get logger instance
110
+ logger_instance = Logger.get_instance(logger_name)
111
+
112
+ # Record start time
113
+ start_time = time.time()
114
+
115
+ try:
116
+ # Execute the function
117
+ result = func(*args, **kwargs)
118
+
119
+ # Calculate execution time
120
+ execution_time = time.time() - start_time
121
+
122
+ # Log success
123
+ logger_instance.info(
124
+ f"API endpoint {func.__name__} executed successfully",
125
+ execution_time_ms=round(execution_time * 1000, 2),
126
+ status="success",
127
+ endpoint=func.__name__,
128
+ )
129
+
130
+ return result
131
+
132
+ except Exception as e:
133
+ # Calculate execution time (even if error occurred)
134
+ execution_time = time.time() - start_time
135
+
136
+ # Log error
137
+ logger_instance.error(
138
+ f"API endpoint {func.__name__} execution failed: {str(e)}",
139
+ execution_time_ms=round(execution_time * 1000, 2),
140
+ status="error",
141
+ endpoint=func.__name__,
142
+ exc_info=e,
143
+ )
144
+
145
+ # Re-raise the exception
146
+ raise
147
+
148
+
149
+ def prevent_duplicate_submission(
150
+ key_prefix: str = "duplicate_submission",
151
+ ttl: int = 60,
152
+ include_args: bool = True,
153
+ include_kwargs: bool = True,
154
+ custom_key_func: Optional[Callable] = None,
155
+ ):
156
+ """Decorator to prevent duplicate submissions within a specified time window.
157
+
158
+ Args:
159
+ key_prefix: Prefix for the cache key
160
+ ttl: Time to live in seconds for the duplicate check
161
+ include_args: Whether to include function args in the cache key
162
+ include_kwargs: Whether to include function kwargs in the cache key
163
+ custom_key_func: Optional custom function to generate cache key
164
+
165
+ Returns:
166
+ Decorated function that prevents duplicate submissions
167
+ """
168
+
169
+ def decorator(func: Callable) -> Callable:
170
+ @functools.wraps(func)
171
+ async def async_wrapper(*args, **kwargs):
172
+ # Generate cache key
173
+ if custom_key_func:
174
+ cache_key = custom_key_func(*args, **kwargs)
175
+ else:
176
+ key_parts = [key_prefix, func.__name__]
177
+
178
+ if include_args and args:
179
+ # Skip 'self' for instance methods
180
+ args_to_hash = (
181
+ args[1:] if args and hasattr(args[0], "__class__") else args
182
+ )
183
+ key_parts.append(str(args_to_hash))
184
+
185
+ if include_kwargs and kwargs:
186
+ # Sort kwargs for consistent key generation
187
+ sorted_kwargs = json.dumps(kwargs, sort_keys=True)
188
+ key_parts.append(sorted_kwargs)
189
+
190
+ # Create hash of the key parts for a shorter key
191
+ key_content = ":".join(key_parts)
192
+ cache_key = (
193
+ f"{key_prefix}:{hashlib.md5(key_content.encode()).hexdigest()}"
194
+ )
195
+
196
+ # Get cache client
197
+ cache = await get_cache_client()
198
+
199
+ # Check if request is duplicate
200
+ existing = await cache.get(cache_key)
201
+ if existing is not None:
202
+ raise DuplicateSubmissionError(
203
+ f"Duplicate submission detected. Please wait {ttl} seconds before retrying."
204
+ )
205
+
206
+ # Set lock in cache
207
+ await cache.set(cache_key, "locked", ttl=ttl)
208
+
209
+ try:
210
+ # Execute the original function
211
+ result = await func(*args, **kwargs)
212
+ return result
213
+ except Exception as e:
214
+ # On error, remove the lock to allow retry
215
+ await cache.delete(cache_key)
216
+ raise e
217
+
218
+ @functools.wraps(func)
219
+ def sync_wrapper(*args, **kwargs):
220
+ raise NotImplementedError(
221
+ "Synchronous functions are not supported. "
222
+ "Please use async functions with prevent_duplicate_submission decorator."
223
+ )
224
+
225
+ # Return appropriate wrapper based on function type
226
+ if asyncio.iscoroutinefunction(func):
227
+ return async_wrapper
228
+ else:
229
+ return sync_wrapper
230
+
231
+ return decorator
232
+
233
+
234
+ class DuplicateSubmissionError(Exception):
235
+ """Exception raised when a duplicate submission is detected."""
236
+
237
+ pass
@@ -1,7 +1,7 @@
1
1
  """Enum for the application"""
2
2
 
3
+ from .base import DBTypeEnum, MediaTypeEnum, SortEnum
3
4
  from .base_error_code import ExceptionCode
4
- from .enum import DBTypeEnum, MediaTypeEnum, SortEnum
5
5
 
6
6
  __all__ = [
7
7
  ExceptionCode,
@@ -0,0 +1,71 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Enumerations module. Contains commonly used enum types for the application."""
3
+
4
+ from enum import Enum
5
+
6
+
7
+ class SortEnum(str, Enum):
8
+ """Enumeration for sorting directions."""
9
+
10
+ ascending = "asc"
11
+ descending = "desc"
12
+
13
+
14
+ class DBTypeEnum(str, Enum):
15
+ """Enumeration for supported database types."""
16
+
17
+ PGSQL = "postgresql"
18
+ MYSQL = "mysql"
19
+ SQLITE = "sqlite"
20
+
21
+
22
+ class MediaTypeEnum(str, Enum):
23
+ """Enumeration for media/content types."""
24
+
25
+ # === Text Formats ===
26
+ JSON = "application/json"
27
+ JSON_PATCH = "application/json-patch+json"
28
+ XML = "application/xml"
29
+ HTML = "text/html"
30
+ TEXT = "text/plain"
31
+ CSS = "text/css"
32
+ JAVASCRIPT = "application/javascript"
33
+ CSV = "text/csv"
34
+ YAML = "application/yaml"
35
+ YML = "application/yaml" # Alias
36
+
37
+ # === Form Data ===
38
+ FORM_URL_ENCODED = "application/x-www-form-urlencoded"
39
+ MULTIPART_FORM = "multipart/form-data"
40
+
41
+ # === Binary/Data Formats ===
42
+ PDF = "application/pdf"
43
+ ZIP = "application/zip"
44
+ GZIP = "application/gzip"
45
+ OCTET_STREAM = "application/octet-stream"
46
+
47
+ # === Images ===
48
+ PNG = "image/png"
49
+ JPEG = "image/jpeg"
50
+ JPG = "image/jpeg" # Alias
51
+ GIF = "image/gif"
52
+ WEBP = "image/webp"
53
+ SVG = "image/svg+xml"
54
+
55
+ # === Audio ===
56
+ MP3 = "audio/mpeg"
57
+ WAV = "audio/wav"
58
+ WEBM_AUDIO = "audio/webm"
59
+
60
+ # === Video ===
61
+ MP4 = "video/mp4"
62
+ WEBM_VIDEO = "video/webm"
63
+ MPEG = "video/mpeg"
64
+
65
+ # === Protobuf & Other Binary Formats ===
66
+ PROTOBUF = "application/protobuf"
67
+ MSGPACK = "application/msgpack"
68
+ CBOR = "application/cbor"
69
+
70
+ def __str__(self) -> str:
71
+ return self.value
@@ -1,6 +1,6 @@
1
1
  """Exception handling."""
2
2
 
3
- from .custom_exception import HTTPException
3
+ from .base import HTTPException
4
4
  from .exception_manager import register_exception_handlers
5
5
 
6
6
  __all__ = [HTTPException, register_exception_handlers]
@@ -3,7 +3,15 @@
3
3
 
4
4
  from typing import Any, Optional
5
5
 
6
- from fastlib.enums import ExceptionCode
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class ErrorDetail(BaseModel):
10
+ """Base class for error code.
11
+ """
12
+
13
+ code: int
14
+ message: str
7
15
 
8
16
 
9
17
  class HTTPException(Exception):
@@ -18,7 +26,7 @@ class HTTPException(Exception):
18
26
 
19
27
  def __init__(
20
28
  self,
21
- code: ExceptionCode,
29
+ code: ErrorDetail,
22
30
  message: Optional[str] = None,
23
31
  details: Optional[Any] = None,
24
32
  ):
@@ -16,7 +16,6 @@ from pydantic_core._pydantic_core import ValidationError # noqa
16
16
  from starlette.exceptions import HTTPException as StarletteHTTPException
17
17
 
18
18
  from fastlib import ConfigManager
19
- from fastlib.enums.enum import CommonErrorCode
20
19
  from fastlib.exception import HTTPException
21
20
 
22
21
 
@@ -69,9 +68,9 @@ def collect_request_info(request: Request) -> dict[str, Any]:
69
68
  "method": request.method,
70
69
  "query_params": dict(request.query_params),
71
70
  "headers": dict(request.headers),
72
- "client": f"{request.client.host}:{request.client.port}"
73
- if request.client
74
- else None,
71
+ "client": (
72
+ f"{request.client.host}:{request.client.port}" if request.client else None
73
+ ),
75
74
  }
76
75
 
77
76
 
@@ -80,13 +79,15 @@ def log_exception(exc: Exception, request_info: dict[str, Any]) -> None:
80
79
  Log exception with full context information.
81
80
  """
82
81
  logger.error(
83
- textwrap.dedent(f"""\
82
+ textwrap.dedent(
83
+ f"""\
84
84
  Unhandled exception,
85
85
  exception_type: {type(exc).__name__},
86
86
  exception_message: {str(exc)},
87
87
  traceback: {traceback.format_exc()},
88
88
  request: {request_info},
89
- """)
89
+ """
90
+ )
90
91
  )
91
92
 
92
93
 
@@ -109,7 +110,7 @@ def build_error_response(
109
110
  return JSONResponse(
110
111
  content={
111
112
  "error": {
112
- "code": CommonErrorCode.INTERNAL_SERVER_ERROR.code,
113
+ "code": -1,
113
114
  "message": error_message,
114
115
  }
115
116
  }
@@ -8,7 +8,7 @@ from pydantic_core._pydantic_core import ValidationError # noqa
8
8
  from starlette.exceptions import HTTPException as StarletteHTTPException
9
9
 
10
10
  from fastlib.exception import exception_handler
11
- from fastlib.exception.custom_exception import HTTPException
11
+ from fastlib.exception.base import HTTPException
12
12
 
13
13
 
14
14
  def register_exception_handlers(app: FastAPI) -> None:
@@ -45,7 +45,7 @@ def create_middleware_and_session_proxy():
45
45
  session_args = session_args or {}
46
46
 
47
47
  if not custom_engine and not db_url:
48
- raise ValueError(
48
+ raise RuntimeError(
49
49
  "You need to pass a db_url or a custom_engine parameter."
50
50
  )
51
51
  if not custom_engine:
@@ -73,11 +73,11 @@ def create_middleware_and_session_proxy():
73
73
  def session(self) -> AsyncSession:
74
74
  """Get the current async context session."""
75
75
  if _Session is None:
76
- raise ValueError("Session is not initialised")
76
+ raise RuntimeError("Session is not initialised")
77
77
 
78
78
  session = _session.get()
79
79
  if session is None:
80
- raise ValueError("Session is not initialised")
80
+ raise RuntimeError("Session is not initialised")
81
81
 
82
82
  return session
83
83
 
@@ -93,7 +93,7 @@ def create_middleware_and_session_proxy():
93
93
  async def __aenter__(self):
94
94
  """Enter session context."""
95
95
  if _Session is None:
96
- raise ValueError("Session is not initialised")
96
+ raise RuntimeError("Session is not initialised")
97
97
 
98
98
  self.token = _session.set(_Session(**self.session_args)) # type: ignore
99
99
  return type(self)
@@ -0,0 +1,40 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from starlette.middleware.base import BaseHTTPMiddleware
3
+
4
+ from fastlib.i18n.manager import reset_language, set_language
5
+ from fastlib.i18n.types import Language
6
+
7
+
8
+ class I18nMiddleware(BaseHTTPMiddleware):
9
+ """
10
+ Middleware for automatic language detection and setting based on HTTP headers.
11
+ """
12
+
13
+ async def dispatch(self, request, call_next):
14
+ """
15
+ Process the request and set the language based on Accept-Language header.
16
+
17
+ Args:
18
+ request: The incoming HTTP request
19
+ call_next: The next middleware or route handler in the chain
20
+
21
+ Returns:
22
+ The HTTP response from the next handler
23
+ """
24
+ # Get language preference from request headers
25
+ accept_lang = request.headers.get("Accept-Language", "en")
26
+
27
+ # Set the language for current request
28
+ if accept_lang.startswith("en"):
29
+ language = Language.CHINESE
30
+ elif accept_lang.startswith("zh"):
31
+ language = Language.CHINESE
32
+
33
+ set_language(language)
34
+ try:
35
+ response = await call_next(request)
36
+ finally:
37
+ # Always reset to prevent leaking context between requests
38
+ reset_language()
39
+
40
+ return response
@@ -11,7 +11,7 @@ from starlette.responses import JSONResponse
11
11
 
12
12
  from fastlib import ConfigManager, constant, security
13
13
  from fastlib.context.contextvars import current_user_id
14
- from fastlib.enums.enum import MediaTypeEnum
14
+ from fastlib.enums.base import MediaTypeEnum
15
15
  from fastlib.schema import UserCredential
16
16
 
17
17
  # Load configuration
@@ -1,5 +1,5 @@
1
1
  """Export router module."""
2
2
 
3
- from fastlib.router.router_v1 import register_router
3
+ from fastlib.router.base import register_router
4
4
 
5
5
  __all__ = [register_router]
@@ -1,13 +1,13 @@
1
1
  """Export the core schemas' symbols."""
2
2
 
3
- from .response import HttpResponse
4
- from .schema import (
3
+ from .base import (
5
4
  CurrentUser,
6
5
  ListResult,
7
6
  PaginationRequest,
8
7
  SortItem,
9
8
  UserCredential,
10
9
  )
10
+ from .response import HttpResponse
11
11
 
12
12
  __all__ = [
13
13
  HttpResponse,
@@ -5,7 +5,7 @@ from typing import Any, Generic, Optional, TypeVar, Union
5
5
 
6
6
  from pydantic import BaseModel, model_serializer
7
7
 
8
- from fastlib.enums import ExceptionCode
8
+ from fastlib.exception.base import ErrorDetail
9
9
 
10
10
  # Define generic type variables for response data
11
11
  DataType = TypeVar("DataType")
@@ -77,7 +77,7 @@ class HttpResponse(BaseModel, Generic[T]):
77
77
 
78
78
  @staticmethod
79
79
  def fail_with_error(
80
- error: Union[ExceptionCode, tuple[int, str]], # Supports multiple error types
80
+ error: Union[ErrorDetail, tuple[int, str]], # Supports multiple error types
81
81
  extra_msg: Optional[str] = None,
82
82
  ) -> "HttpResponse[Any]":
83
83
  """Constructs an error response from various error type inputs.
@@ -1,6 +1,5 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from datetime import datetime, timedelta
3
- from functools import wraps
4
3
  from typing import Any, Callable, Optional, Union
5
4
 
6
5
  import jwt
@@ -13,8 +12,8 @@ from fastlib.config.manager import ConfigManager
13
12
  from fastlib.schema import CurrentUser
14
13
 
15
14
  # Configuration
16
- security_config = ConfigManager.get_security_config
17
- server_config = ConfigManager.get_server_config
15
+ security_config = ConfigManager.get_security_config()
16
+ server_config = ConfigManager.get_server_config()
18
17
 
19
18
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
20
19
 
@@ -177,24 +176,3 @@ def get_user_id(token: str) -> int:
177
176
  """
178
177
  payload = decode_jwt_token(token)
179
178
  return int(payload["sub"])
180
-
181
-
182
- def role_required(required_role: str):
183
- def decorator(func):
184
- @wraps(func)
185
- async def wrapper(current_user: CurrentUser, *args, **kwargs):
186
- # Get the current user
187
- pass
188
- # user_id = current_user.user_id
189
- #
190
- # # Get user role info or permission info
191
- # if user.get("role") != required_role:
192
- # raise HTTPException(
193
- # status_code=status.HTTP_403_FORBIDDEN,
194
- # detail=f"Requires {required_role} role"
195
- # )
196
- # return await func(request, *args, **kwargs)
197
-
198
- return wrapper
199
-
200
- return decorator
File without changes
@@ -7,10 +7,6 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
7
7
 
8
8
  from fastlib import ConfigManager
9
9
 
10
- # Global engine cache with thread safety
11
- _engine_map: dict[str, AsyncEngine] = {}
12
- _lock = Lock()
13
-
14
10
  async_engine: AsyncEngine
15
11
 
16
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.2.2
3
+ Version: 0.2.3
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>
@@ -20,11 +20,12 @@ fastlib/constant/__init__.py
20
20
  fastlib/constant/constant.py
21
21
  fastlib/context/__init__.py
22
22
  fastlib/context/contextvars.py
23
+ fastlib/decorator/__init__.py
24
+ fastlib/decorator/base.py
23
25
  fastlib/enums/__init__.py
24
- fastlib/enums/base_error_code.py
25
- fastlib/enums/enum.py
26
+ fastlib/enums/base.py
26
27
  fastlib/exception/__init__.py
27
- fastlib/exception/custom_exception.py
28
+ fastlib/exception/base.py
28
29
  fastlib/exception/exception_handler.py
29
30
  fastlib/exception/exception_manager.py
30
31
  fastlib/i18n/manager.py
@@ -42,17 +43,14 @@ fastlib/middleware/__init__.py
42
43
  fastlib/middleware/db_session.py
43
44
  fastlib/middleware/i18n.py
44
45
  fastlib/middleware/jwt.py
45
- fastlib/middleware/log.py
46
46
  fastlib/migrate/migrate.py
47
- fastlib/model/__init__.py
48
- fastlib/model/base_model.py
49
47
  fastlib/openapi/__init__.py
50
48
  fastlib/openapi/offline.py
51
49
  fastlib/router/__init__.py
52
- fastlib/router/router_v1.py
50
+ fastlib/router/base.py
53
51
  fastlib/schema/__init__.py
52
+ fastlib/schema/base.py
54
53
  fastlib/schema/response.py
55
- fastlib/schema/schema.py
56
54
  fastlib/security/__init__.py
57
55
  fastlib/security/security.py
58
56
  fastlib/security/utils.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastlib-py"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Fastlib, high performance, easy to use, fast to code, ready for production"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -1,15 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """Base error code enumerations for API responses."""
3
-
4
- from pydantic import BaseModel
5
-
6
-
7
- class ExceptionCode(BaseModel):
8
- """Base class for error code enumerations.
9
-
10
- Provides common interface for all error codes where each enum member is defined as a tuple of
11
- (code, message). This enables consistent error handling across the codebase.
12
- """
13
-
14
- code: int
15
- message: str
@@ -1,33 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """Enumerations module. Contains commonly used enum types for the application."""
3
-
4
- from enum import Enum
5
-
6
- from fastlib.enums import ExceptionCode
7
-
8
-
9
- class SortEnum(str, Enum):
10
- """Enumeration for sorting directions."""
11
-
12
- ascending = "asc"
13
- descending = "desc"
14
-
15
-
16
- class DBTypeEnum(str, Enum):
17
- """Enumeration for supported database types."""
18
-
19
- PGSQL = "postgresql"
20
- MYSQL = "mysql"
21
- SQLITE = "sqlite"
22
-
23
-
24
- class MediaTypeEnum(str, Enum):
25
- """Enumeration for media/content types."""
26
-
27
- JSON = ".json"
28
-
29
-
30
- class CommonErrorCode:
31
- """Error codes for core domain."""
32
-
33
- INTERNAL_SERVER_ERROR = ExceptionCode(code=-1, message="Internal server exception")
@@ -1,57 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- from starlette.middleware.base import BaseHTTPMiddleware
3
-
4
- from fastlib.i18n.types import Language
5
-
6
-
7
- class I18nMiddleware(BaseHTTPMiddleware):
8
- """
9
- Middleware for automatic language detection and setting based on HTTP headers.
10
-
11
- This middleware inspects the Accept-Language header from incoming requests
12
- and sets the appropriate language in the request state for use throughout
13
- the application.
14
-
15
- The language preference is stored in request.state.language and can be
16
- accessed in route handlers and other middleware.
17
-
18
- Example:
19
- ```python
20
- from fastapi import FastAPI, Request
21
-
22
- app = FastAPI()
23
- app.add_middleware(I18nMiddleware)
24
-
25
- @app.get("/")
26
- async def root(request: Request):
27
- language = request.state.language
28
- return {"language": language.value}
29
- ```
30
-
31
- Note:
32
- Currently supports only English and Chinese. Defaults to English
33
- if the Accept-Language header is not present or unrecognized.
34
- """
35
-
36
- async def dispatch(self, request, call_next):
37
- """
38
- Process the request and set the language based on Accept-Language header.
39
-
40
- Args:
41
- request: The incoming HTTP request
42
- call_next: The next middleware or route handler in the chain
43
-
44
- Returns:
45
- The HTTP response from the next handler
46
- """
47
- # Get language preference from request headers
48
- accept_lang = request.headers.get("Accept-Language", "en")
49
-
50
- # Set the language for current request
51
- if accept_lang.startswith("en"):
52
- request.state.language = Language.ENGLISH
53
- else:
54
- request.state.language = Language.CHINESE
55
-
56
- response = await call_next(request)
57
- return response
@@ -1,49 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """FastAPI Request Logging Middleware"""
3
-
4
- import time
5
- import uuid
6
-
7
- from fastapi import Request
8
- from loguru import logger
9
-
10
-
11
- async def log_requests(request: Request, call_next):
12
- """Middleware to log all incoming requests and responses"""
13
- # Generate unique request ID
14
- request_id = str(uuid.uuid4())
15
- request.state.request_id = request_id
16
-
17
- # Log request start information
18
- start_time = time.time()
19
-
20
- # Request started log
21
- logger.info(
22
- f"Request started - {request.method} {request.url.path} "
23
- f"(ID: {request_id}, IP: {request.client.host if request.client else None}, "
24
- f"UA: {request.headers.get('user-agent')})"
25
- )
26
-
27
- try:
28
- response = await call_next(request)
29
- except Exception as exc:
30
- # Log exception information
31
- logger.error(
32
- f"Request failed - {request.method} {request.url.path} (ID: {request_id}, Error: {str(exc)})",
33
- exc_info=True,
34
- )
35
- raise
36
-
37
- # Calculate processing time
38
- process_time = round((time.time() - start_time) * 1000, 2)
39
-
40
- # Request completed log
41
- logger.info(
42
- f"Request completed - {request.method} {request.url.path} "
43
- f"(ID: {request_id}, Status: {response.status_code}, "
44
- f"Time: {process_time}ms)"
45
- )
46
-
47
- # Add request ID to response headers
48
- response.headers["X-Request-ID"] = request_id
49
- return response
@@ -1,5 +0,0 @@
1
- """Export model symbols"""
2
-
3
- from .base_model import BaseModel, ModelExt
4
-
5
- __all__ = [BaseModel, ModelExt]
@@ -1,62 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """Database Model Base Classes"""
3
-
4
- from datetime import datetime
5
- from typing import Optional
6
-
7
- from sqlalchemy import BigInteger, DateTime, Integer
8
- from sqlmodel import Field
9
- from sqlmodel import SQLModel as _SQLModel
10
-
11
- from fastlib.utils.snowflake_util import snowflake_id
12
-
13
-
14
- class BaseModel(_SQLModel):
15
- """
16
- Identifier for a model
17
- """
18
-
19
- id: int = Field(
20
- default_factory=snowflake_id,
21
- primary_key=True,
22
- sa_type=BigInteger,
23
- sa_column_kwargs={"comment": "主键"},
24
- )
25
-
26
-
27
- class TreeModel(BaseModel):
28
- """
29
- Identifier for a model
30
- """
31
-
32
- parent_id: int = Field(
33
- index=True,
34
- sa_type=BigInteger,
35
- sa_column_kwargs={"comment": "父id"},
36
- )
37
-
38
- sort: int = Field(
39
- index=True,
40
- sa_type=Integer,
41
- sa_column_kwargs={"comment": "排序"},
42
- )
43
-
44
-
45
- class ModelExt(_SQLModel):
46
- """
47
- Create time and update time for a model, can be automatically generated
48
- """
49
-
50
- create_at: Optional[datetime] = Field(
51
- sa_type=DateTime,
52
- default_factory=datetime.utcnow,
53
- sa_column_kwargs={"comment": "创建时间"},
54
- )
55
- update_at: Optional[datetime] = Field(
56
- sa_type=DateTime,
57
- default_factory=datetime.utcnow,
58
- sa_column_kwargs={
59
- "onupdate": datetime.utcnow,
60
- "comment": "更新时间",
61
- },
62
- )
File without changes
File without changes
File without changes