fastlib-py 0.2.1__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 (86) hide show
  1. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/PKG-INFO +1 -1
  2. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/manager.py +1 -2
  3. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/utils.py +1 -1
  4. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/constant/__init__.py +1 -0
  5. fastlib_py-0.2.3/fastlib/constant/constant.py +150 -0
  6. fastlib_py-0.2.3/fastlib/decorator/base.py +237 -0
  7. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/enums/__init__.py +1 -1
  8. fastlib_py-0.2.3/fastlib/enums/base.py +71 -0
  9. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/exception/__init__.py +1 -1
  10. fastlib_py-0.2.1/fastlib/exception/custom_exception.py → fastlib_py-0.2.3/fastlib/exception/base.py +10 -2
  11. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/exception/exception_handler.py +8 -7
  12. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/exception/exception_manager.py +1 -1
  13. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/i18n/manager.py +4 -1
  14. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/i18n/messages.py +23 -18
  15. fastlib_py-0.2.3/fastlib/i18n/types.py +106 -0
  16. fastlib_py-0.2.3/fastlib/i18n/utils.py +43 -0
  17. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/middleware/db_session.py +4 -4
  18. fastlib_py-0.2.3/fastlib/middleware/i18n.py +40 -0
  19. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/middleware/jwt.py +34 -8
  20. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/router/__init__.py +1 -1
  21. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/schema/__init__.py +2 -2
  22. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/schema/response.py +2 -2
  23. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/security/security.py +2 -24
  24. fastlib_py-0.2.3/fastlib/service/impl/__init__.py +0 -0
  25. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/session/db_engine.py +0 -4
  26. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/PKG-INFO +1 -1
  27. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/SOURCES.txt +6 -8
  28. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/pyproject.toml +1 -1
  29. fastlib_py-0.2.1/fastlib/constant/constant.py +0 -31
  30. fastlib_py-0.2.1/fastlib/enums/base_error_code.py +0 -15
  31. fastlib_py-0.2.1/fastlib/enums/enum.py +0 -33
  32. fastlib_py-0.2.1/fastlib/i18n/types.py +0 -12
  33. fastlib_py-0.2.1/fastlib/i18n/utils.py +0 -86
  34. fastlib_py-0.2.1/fastlib/middleware/i18n.py +0 -57
  35. fastlib_py-0.2.1/fastlib/middleware/log.py +0 -49
  36. fastlib_py-0.2.1/fastlib/model/__init__.py +0 -5
  37. fastlib_py-0.2.1/fastlib/model/base_model.py +0 -62
  38. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/LICENSE +0 -0
  39. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/README.md +0 -0
  40. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/__init__.py +0 -0
  41. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/__init__.py +0 -0
  42. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/base.py +0 -0
  43. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/disk_cache.py +0 -0
  44. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/redis_cache.py +0 -0
  45. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/__init__.py +0 -0
  46. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/base.py +0 -0
  47. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/database_config.py +0 -0
  48. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/loader.py +0 -0
  49. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/manager.py +0 -0
  50. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/registry.py +0 -0
  51. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/security_config.py +0 -0
  52. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/server_config.py +0 -0
  53. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/context/__init__.py +0 -0
  54. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/context/contextvars.py +0 -0
  55. {fastlib_py-0.2.1/fastlib/mapper → fastlib_py-0.2.3/fastlib/decorator}/__init__.py +0 -0
  56. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/logging/__init__.py +0 -0
  57. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/logging/config.py +0 -0
  58. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/logging/handlers.py +0 -0
  59. {fastlib_py-0.2.1/fastlib/mapper/impl → fastlib_py-0.2.3/fastlib/mapper}/__init__.py +0 -0
  60. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/mapper/base_mapper.py +0 -0
  61. {fastlib_py-0.2.1/fastlib/service → fastlib_py-0.2.3/fastlib/mapper/impl}/__init__.py +0 -0
  62. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
  63. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/middleware/__init__.py +0 -0
  64. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/migrate/migrate.py +0 -0
  65. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/openapi/__init__.py +0 -0
  66. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/openapi/offline.py +0 -0
  67. /fastlib_py-0.2.1/fastlib/router/router_v1.py → /fastlib_py-0.2.3/fastlib/router/base.py +0 -0
  68. /fastlib_py-0.2.1/fastlib/schema/schema.py → /fastlib_py-0.2.3/fastlib/schema/base.py +0 -0
  69. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/security/__init__.py +0 -0
  70. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/security/utils.py +0 -0
  71. {fastlib_py-0.2.1/fastlib/service/impl → fastlib_py-0.2.3/fastlib/service}/__init__.py +0 -0
  72. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/service/base_service.py +0 -0
  73. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/service/impl/base_service_impl.py +0 -0
  74. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/session/__init__.py +0 -0
  75. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/session/db_session.py +0 -0
  76. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/__init__.py +0 -0
  77. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/excel_util.py +0 -0
  78. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/file_util.py +0 -0
  79. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/http_util.py +0 -0
  80. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/ip_util.py +0 -0
  81. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/snowflake_util.py +0 -0
  82. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/str_util.py +0 -0
  83. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/dependency_links.txt +0 -0
  84. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/requires.txt +0 -0
  85. {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/top_level.txt +0 -0
  86. {fastlib_py-0.2.1 → 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.1
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>
@@ -2,7 +2,6 @@
2
2
  """Cache Client manager to instantiate the appropriate cache client"""
3
3
 
4
4
  from fastlib.cache.base import Cache
5
-
6
5
  from fastlib.config.manager import ConfigManager
7
6
 
8
7
 
@@ -13,7 +12,7 @@ async def get_cache_client() -> Cache:
13
12
  Cache: Redis client if Redis is enabled in config, otherwise returns page cache.
14
13
  """
15
14
  from fastlib.cache.redis_cache import RedisManager
16
-
15
+
17
16
  config = ConfigManager.get_database_config()
18
17
  if config.enable_redis:
19
18
  from fastlib.cache.redis_cache import RedisCache
@@ -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:
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: MIT
1
2
  """Constant for the application"""
2
3
 
3
4
  from .constant import (
@@ -0,0 +1,150 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Common constant"""
3
+
4
+ import os
5
+ from typing import Any
6
+
7
+ from fastlib.utils import file_util
8
+
9
+ # Default constant values
10
+ _DEFAULT_CONSTANTS: dict[str, Any] = {
11
+ "RESOURCE_DIR": os.path.abspath(
12
+ os.path.join(file_util.find_project_root(), "src", "main", "resource")
13
+ ),
14
+ "ADMIN_ID": 9,
15
+ "ROOT_PARENT_ID": 0,
16
+ "MAX_PAGE_SIZE": 1000,
17
+ "ENV": "env",
18
+ "PARENT_ID": "parent_id",
19
+ "CONFIG_FILE": "config_file",
20
+ "CONFIG_FILE_NAME": "config.yml",
21
+ "AUTHORIZATION": "Authorization",
22
+ }
23
+
24
+ # Current constant values (initially defaults)
25
+ _CURRENT_CONSTANTS = _DEFAULT_CONSTANTS.copy()
26
+
27
+ # Exported constants (maintain original interface)
28
+ RESOURCE_DIR: str = _CURRENT_CONSTANTS["RESOURCE_DIR"]
29
+ ADMIN_ID = _CURRENT_CONSTANTS["ADMIN_ID"]
30
+ ROOT_PARENT_ID = _CURRENT_CONSTANTS["ROOT_PARENT_ID"]
31
+ MAX_PAGE_SIZE = _CURRENT_CONSTANTS["MAX_PAGE_SIZE"]
32
+ ENV = _CURRENT_CONSTANTS["ENV"]
33
+ PARENT_ID = _CURRENT_CONSTANTS["PARENT_ID"]
34
+ CONFIG_FILE = _CURRENT_CONSTANTS["CONFIG_FILE"]
35
+ CONFIG_FILE_NAME = _CURRENT_CONSTANTS["CONFIG_FILE_NAME"]
36
+ AUTHORIZATION = _CURRENT_CONSTANTS["AUTHORIZATION"]
37
+
38
+
39
+ def set_constant(name: str, value: Any) -> None:
40
+ """Set constant value.
41
+
42
+ Args:
43
+ name: Constant name
44
+ value: New value
45
+
46
+ Raises:
47
+ ValueError: If constant doesn't exist
48
+ """
49
+ if name not in _DEFAULT_CONSTANTS:
50
+ raise ValueError(f"Constant '{name}' doesn't exist")
51
+
52
+ _CURRENT_CONSTANTS[name] = value
53
+ _update_exported_constants()
54
+
55
+
56
+ def reset_constant(name: str = None) -> None:
57
+ """Reset constant(s) to default values.
58
+
59
+ Args:
60
+ name: Constant name to reset, None resets all
61
+
62
+ Raises:
63
+ ValueError: If constant doesn't exist
64
+ """
65
+ if name is None:
66
+ _CURRENT_CONSTANTS.clear()
67
+ _CURRENT_CONSTANTS.update(_DEFAULT_CONSTANTS)
68
+ else:
69
+ if name not in _DEFAULT_CONSTANTS:
70
+ raise ValueError(f"Constant '{name}' doesn't exist")
71
+ _CURRENT_CONSTANTS[name] = _DEFAULT_CONSTANTS[name]
72
+
73
+ _update_exported_constants()
74
+
75
+
76
+ def get_constant(name: str) -> Any:
77
+ """Get current constant value.
78
+
79
+ Args:
80
+ name: Constant name
81
+
82
+ Returns:
83
+ Current constant value
84
+
85
+ Raises:
86
+ ValueError: If constant doesn't exist
87
+ """
88
+ if name not in _CURRENT_CONSTANTS:
89
+ raise ValueError(f"Constant '{name}' doesn't exist")
90
+ return _CURRENT_CONSTANTS[name]
91
+
92
+
93
+ def get_default_constant(name: str) -> Any:
94
+ """Get default constant value.
95
+
96
+ Args:
97
+ name: Constant name
98
+
99
+ Returns:
100
+ Default constant value
101
+
102
+ Raises:
103
+ ValueError: If constant doesn't exist
104
+ """
105
+ if name not in _DEFAULT_CONSTANTS:
106
+ raise ValueError(f"Constant '{name}' doesn't exist")
107
+ return _DEFAULT_CONSTANTS[name]
108
+
109
+
110
+ def list_constants() -> dict[str, dict[str, Any]]:
111
+ """List all constants with current and default values.
112
+
113
+ Returns:
114
+ Dictionary with constant information
115
+ """
116
+ return {
117
+ name: {
118
+ "current": _CURRENT_CONSTANTS[name],
119
+ "default": _DEFAULT_CONSTANTS[name],
120
+ "is_modified": _CURRENT_CONSTANTS[name] != _DEFAULT_CONSTANTS[name],
121
+ }
122
+ for name in _DEFAULT_CONSTANTS
123
+ }
124
+
125
+
126
+ def _update_exported_constants() -> None:
127
+ """Update exported constant variables."""
128
+ global RESOURCE_DIR, ADMIN_ID, ROOT_PARENT_ID, MAX_PAGE_SIZE
129
+ global ENV, PARENT_ID, CONFIG_FILE, CONFIG_FILE_NAME, AUTHORIZATION
130
+
131
+ RESOURCE_DIR = _CURRENT_CONSTANTS["RESOURCE_DIR"]
132
+ ADMIN_ID = _CURRENT_CONSTANTS["ADMIN_ID"]
133
+ ROOT_PARENT_ID = _CURRENT_CONSTANTS["ROOT_PARENT_ID"]
134
+ MAX_PAGE_SIZE = _CURRENT_CONSTANTS["MAX_PAGE_SIZE"]
135
+ ENV = _CURRENT_CONSTANTS["ENV"]
136
+ PARENT_ID = _CURRENT_CONSTANTS["PARENT_ID"]
137
+ CONFIG_FILE = _CURRENT_CONSTANTS["CONFIG_FILE"]
138
+ CONFIG_FILE_NAME = _CURRENT_CONSTANTS["CONFIG_FILE_NAME"]
139
+ AUTHORIZATION = _CURRENT_CONSTANTS["AUTHORIZATION"]
140
+
141
+
142
+ class FilterOperators:
143
+ EQ = "EQ"
144
+ NE = "NE"
145
+ GT = "GT"
146
+ GE = "GE"
147
+ LT = "LT"
148
+ LE = "LE"
149
+ BETWEEN = "BETWEEN"
150
+ LIKE = "LIKE"
@@ -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:
@@ -1,10 +1,13 @@
1
+ # SPDX-License-Identifier: MIT
1
2
  """
2
3
  Internationalization context manager
3
4
  """
4
5
 
5
6
  import contextvars
6
7
  import functools
7
- from typing import Any, Callable, TypeVar, Awaitable
8
+ from collections.abc import Awaitable
9
+ from typing import Any, Callable, TypeVar
10
+
8
11
  from fastlib.i18n.types import Language
9
12
 
10
13
  _language_context = contextvars.ContextVar("language", default=Language.ENGLISH)