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.
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/PKG-INFO +1 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/manager.py +1 -2
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/utils.py +1 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/constant/__init__.py +1 -0
- fastlib_py-0.2.3/fastlib/constant/constant.py +150 -0
- fastlib_py-0.2.3/fastlib/decorator/base.py +237 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/enums/__init__.py +1 -1
- fastlib_py-0.2.3/fastlib/enums/base.py +71 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/exception/__init__.py +1 -1
- fastlib_py-0.2.1/fastlib/exception/custom_exception.py → fastlib_py-0.2.3/fastlib/exception/base.py +10 -2
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/exception/exception_handler.py +8 -7
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/exception/exception_manager.py +1 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/i18n/manager.py +4 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/i18n/messages.py +23 -18
- fastlib_py-0.2.3/fastlib/i18n/types.py +106 -0
- fastlib_py-0.2.3/fastlib/i18n/utils.py +43 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/middleware/db_session.py +4 -4
- fastlib_py-0.2.3/fastlib/middleware/i18n.py +40 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/middleware/jwt.py +34 -8
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/router/__init__.py +1 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/schema/__init__.py +2 -2
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/schema/response.py +2 -2
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/security/security.py +2 -24
- fastlib_py-0.2.3/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/session/db_engine.py +0 -4
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/PKG-INFO +1 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/SOURCES.txt +6 -8
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/pyproject.toml +1 -1
- fastlib_py-0.2.1/fastlib/constant/constant.py +0 -31
- fastlib_py-0.2.1/fastlib/enums/base_error_code.py +0 -15
- fastlib_py-0.2.1/fastlib/enums/enum.py +0 -33
- fastlib_py-0.2.1/fastlib/i18n/types.py +0 -12
- fastlib_py-0.2.1/fastlib/i18n/utils.py +0 -86
- fastlib_py-0.2.1/fastlib/middleware/i18n.py +0 -57
- fastlib_py-0.2.1/fastlib/middleware/log.py +0 -49
- fastlib_py-0.2.1/fastlib/model/__init__.py +0 -5
- fastlib_py-0.2.1/fastlib/model/base_model.py +0 -62
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/LICENSE +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/README.md +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/base.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/disk_cache.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/cache/redis_cache.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/base.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/database_config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/loader.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/manager.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/registry.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/security_config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/config/server_config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/context/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/context/contextvars.py +0 -0
- {fastlib_py-0.2.1/fastlib/mapper → fastlib_py-0.2.3/fastlib/decorator}/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/logging/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/logging/config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/logging/handlers.py +0 -0
- {fastlib_py-0.2.1/fastlib/mapper/impl → fastlib_py-0.2.3/fastlib/mapper}/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/mapper/base_mapper.py +0 -0
- {fastlib_py-0.2.1/fastlib/service → fastlib_py-0.2.3/fastlib/mapper/impl}/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/middleware/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/migrate/migrate.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/openapi/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/openapi/offline.py +0 -0
- /fastlib_py-0.2.1/fastlib/router/router_v1.py → /fastlib_py-0.2.3/fastlib/router/base.py +0 -0
- /fastlib_py-0.2.1/fastlib/schema/schema.py → /fastlib_py-0.2.3/fastlib/schema/base.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/security/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/security/utils.py +0 -0
- {fastlib_py-0.2.1/fastlib/service/impl → fastlib_py-0.2.3/fastlib/service}/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/service/base_service.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/service/impl/base_service_impl.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/session/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/session/db_session.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/excel_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/file_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/http_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/snowflake_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/requires.txt +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/fastlib_py.egg-info/top_level.txt +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.3}/setup.cfg +0 -0
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
fastlib_py-0.2.1/fastlib/exception/custom_exception.py → fastlib_py-0.2.3/fastlib/exception/base.py
RENAMED
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
from typing import Any, Optional
|
|
5
5
|
|
|
6
|
-
from
|
|
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:
|
|
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":
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
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":
|
|
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.
|
|
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
|
|
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)
|