fastlib-py 0.2.0__tar.gz → 0.2.2__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.0/fastlib_py.egg-info → fastlib_py-0.2.2}/PKG-INFO +1 -1
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/cache/__init__.py +1 -1
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/cache/disk_cache.py +2 -2
- fastlib_py-0.2.0/fastlib/cache/cache_manager.py → fastlib_py-0.2.2/fastlib/cache/manager.py +2 -2
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/cache/redis_cache.py +6 -7
- fastlib_py-0.2.2/fastlib/config/base.py +17 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/utils.py +0 -2
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/constant/__init__.py +1 -0
- fastlib_py-0.2.2/fastlib/constant/constant.py +150 -0
- fastlib_py-0.2.2/fastlib/i18n/manager.py +99 -0
- fastlib_py-0.2.2/fastlib/i18n/messages.py +93 -0
- fastlib_py-0.2.2/fastlib/i18n/types.py +106 -0
- fastlib_py-0.2.2/fastlib/i18n/utils.py +43 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/logging/config.py +9 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/mapper/impl/base_mapper_impl.py +1 -1
- fastlib_py-0.2.2/fastlib/middleware/__init__.py +7 -0
- fastlib_py-0.2.2/fastlib/middleware/i18n.py +57 -0
- fastlib_py-0.2.0/fastlib/middleware/jwt_middleware.py → fastlib_py-0.2.2/fastlib/middleware/jwt.py +33 -7
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/model/base_model.py +2 -2
- {fastlib_py-0.2.0 → fastlib_py-0.2.2/fastlib_py.egg-info}/PKG-INFO +1 -1
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib_py.egg-info/SOURCES.txt +11 -10
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/pyproject.toml +1 -1
- fastlib_py-0.2.0/fastlib/config/base.py +0 -6
- fastlib_py-0.2.0/fastlib/constant/constant.py +0 -31
- fastlib_py-0.2.0/fastlib/i18n/i18n_context.py +0 -215
- fastlib_py-0.2.0/fastlib/i18n/i18n_messages.py +0 -99
- fastlib_py-0.2.0/fastlib/i18n/i18n_types.py +0 -12
- fastlib_py-0.2.0/fastlib/i18n/i18n_util.py +0 -233
- fastlib_py-0.2.0/fastlib/middleware/__init__.py +0 -7
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/LICENSE +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/README.md +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/__init__.py +0 -0
- /fastlib_py-0.2.0/fastlib/cache/base_cache.py → /fastlib_py-0.2.2/fastlib/cache/base.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/database_config.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/loader.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/manager.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/registry.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/security_config.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/config/server_config.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/context/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/context/contextvars.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/enums/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/enums/base_error_code.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/enums/enum.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/exception/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/exception/custom_exception.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/exception/exception_handler.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/exception/exception_manager.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/logging/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/logging/handlers.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/mapper/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/mapper/base_mapper.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/mapper/impl/__init__.py +0 -0
- /fastlib_py-0.2.0/fastlib/middleware/db_session_middleware.py → /fastlib_py-0.2.2/fastlib/middleware/db_session.py +0 -0
- /fastlib_py-0.2.0/fastlib/middleware/log_middleware.py → /fastlib_py-0.2.2/fastlib/middleware/log.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/migrate/migrate.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/model/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/openapi/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/openapi/offline.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/router/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/router/router_v1.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/schema/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/schema/response.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/schema/schema.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/security/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/security/security.py +0 -0
- /fastlib_py-0.2.0/fastlib/utils/security_util.py → /fastlib_py-0.2.2/fastlib/security/utils.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/service/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/service/base_service.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/service/impl/base_service_impl.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/session/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/session/db_engine.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/session/db_session.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/utils/excel_util.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/utils/file_util.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/utils/http_util.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/utils/snowflake_util.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib_py.egg-info/requires.txt +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/fastlib_py.egg-info/top_level.txt +0 -0
- {fastlib_py-0.2.0 → fastlib_py-0.2.2}/setup.cfg +0 -0
|
@@ -5,11 +5,11 @@ import json
|
|
|
5
5
|
import threading
|
|
6
6
|
import time
|
|
7
7
|
from contextlib import asynccontextmanager
|
|
8
|
-
from typing import Any, Optional
|
|
8
|
+
from typing import Any, Optional
|
|
9
9
|
|
|
10
10
|
import diskcache
|
|
11
11
|
|
|
12
|
-
from fastlib.cache.
|
|
12
|
+
from fastlib.cache.base import Cache, CacheError
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class PageCache(Cache):
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
2
|
"""Cache Client manager to instantiate the appropriate cache client"""
|
|
3
3
|
|
|
4
|
-
from fastlib.cache.
|
|
5
|
-
from fastlib.cache.redis_cache import RedisManager
|
|
4
|
+
from fastlib.cache.base import Cache
|
|
6
5
|
from fastlib.config.manager import ConfigManager
|
|
7
6
|
|
|
8
7
|
|
|
@@ -12,6 +11,7 @@ async def get_cache_client() -> Cache:
|
|
|
12
11
|
Returns:
|
|
13
12
|
Cache: Redis client if Redis is enabled in config, otherwise returns page cache.
|
|
14
13
|
"""
|
|
14
|
+
from fastlib.cache.redis_cache import RedisManager
|
|
15
15
|
|
|
16
16
|
config = ConfigManager.get_database_config()
|
|
17
17
|
if config.enable_redis:
|
|
@@ -5,17 +5,18 @@ import asyncio
|
|
|
5
5
|
import time
|
|
6
6
|
import uuid
|
|
7
7
|
from contextlib import asynccontextmanager
|
|
8
|
-
from typing import Any, Optional
|
|
8
|
+
from typing import Any, Optional
|
|
9
9
|
|
|
10
10
|
from loguru import logger
|
|
11
11
|
|
|
12
|
-
from fastlib
|
|
12
|
+
from fastlib import ConfigManager
|
|
13
|
+
from fastlib.cache.base import Cache, CacheError
|
|
13
14
|
|
|
14
15
|
try:
|
|
15
16
|
import redis.asyncio as redis
|
|
16
17
|
from redis.exceptions import RedisError
|
|
17
18
|
except ModuleNotFoundError:
|
|
18
|
-
logger.
|
|
19
|
+
logger.warning(
|
|
19
20
|
"Cannot find redis module, please install it via `uv add redis[hiredis] and then uv remove diskcache`"
|
|
20
21
|
)
|
|
21
22
|
redis = None
|
|
@@ -25,8 +26,6 @@ except Exception:
|
|
|
25
26
|
redis = None
|
|
26
27
|
RedisError = Exception
|
|
27
28
|
|
|
28
|
-
from fastlib.config.manager import load_config
|
|
29
|
-
|
|
30
29
|
|
|
31
30
|
class RedisCache(Cache):
|
|
32
31
|
def __init__(self, redis_client):
|
|
@@ -347,9 +346,9 @@ class RedisManager:
|
|
|
347
346
|
if cls._instance is None:
|
|
348
347
|
async with cls._lock:
|
|
349
348
|
if cls._connection_pool is None:
|
|
350
|
-
|
|
349
|
+
database_config = ConfigManager.get_database_config()
|
|
351
350
|
cls._connection_pool = redis.ConnectionPool.from_url(
|
|
352
|
-
f"redis://:{
|
|
351
|
+
f"redis://:{database_config.cache_pass}@{database_config.cache_host}:{database_config.cache_port}/{database_config.db_num}",
|
|
353
352
|
decode_responses=True,
|
|
354
353
|
)
|
|
355
354
|
if cls._instance is None:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Base configuration classes."""
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseConfig(ABC):
|
|
8
|
+
"""Base class for all configuration classes."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def __str__(self) -> str:
|
|
12
|
+
"""Return string representation of the configuration.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: String representation of the configuration instance.
|
|
16
|
+
"""
|
|
17
|
+
pass
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
2
|
"""Configuration module util"""
|
|
4
3
|
|
|
5
4
|
import configparser
|
|
@@ -60,7 +59,6 @@ class ProjectInfo:
|
|
|
60
59
|
path = Path(file_util.find_project_root()) / "pyproject.toml"
|
|
61
60
|
else:
|
|
62
61
|
path = Path(pyproject_path)
|
|
63
|
-
print(f"{path}")
|
|
64
62
|
if not path.exists():
|
|
65
63
|
raise FileNotFoundError(f"{pyproject_path} does not exist")
|
|
66
64
|
|
|
@@ -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,99 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""
|
|
3
|
+
Internationalization context manager
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import contextvars
|
|
7
|
+
import functools
|
|
8
|
+
from collections.abc import Awaitable
|
|
9
|
+
from typing import Any, Callable, TypeVar
|
|
10
|
+
|
|
11
|
+
from fastlib.i18n.types import Language
|
|
12
|
+
|
|
13
|
+
_language_context = contextvars.ContextVar("language", default=Language.ENGLISH)
|
|
14
|
+
|
|
15
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_language(language: Language) -> None:
|
|
19
|
+
"""Set current language"""
|
|
20
|
+
_language_context.set(language)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_language() -> Language:
|
|
24
|
+
"""Get current language"""
|
|
25
|
+
return _language_context.get()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_language_code() -> str:
|
|
29
|
+
"""Get current language code, e.g. 'en', 'zh'"""
|
|
30
|
+
return get_language().value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def reset_language() -> None:
|
|
34
|
+
"""Reset language to default"""
|
|
35
|
+
_language_context.set(Language.ENGLISH)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_language(language: Language) -> bool:
|
|
39
|
+
"""Check if current language matches"""
|
|
40
|
+
return get_language() == language
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class I18nContext:
|
|
44
|
+
"""Context manager to temporarily switch language"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, language: Language):
|
|
47
|
+
self.language = language
|
|
48
|
+
self._token: contextvars.Token | None = None
|
|
49
|
+
|
|
50
|
+
def __enter__(self):
|
|
51
|
+
self._token = _language_context.set(self.language)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
55
|
+
if self._token is not None:
|
|
56
|
+
_language_context.reset(self._token)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def with_language(language: Language) -> I18nContext:
|
|
60
|
+
"""Helper to create context manager"""
|
|
61
|
+
return I18nContext(language)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _language_context_decorator(
|
|
65
|
+
language: Language, is_async: bool = False
|
|
66
|
+
) -> Callable[[F], F]:
|
|
67
|
+
"""Internal generator for sync/async language context decorators"""
|
|
68
|
+
|
|
69
|
+
def decorator(func: F) -> F:
|
|
70
|
+
if is_async:
|
|
71
|
+
|
|
72
|
+
@functools.wraps(func)
|
|
73
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
74
|
+
with I18nContext(language):
|
|
75
|
+
return await func(*args, **kwargs)
|
|
76
|
+
|
|
77
|
+
return async_wrapper
|
|
78
|
+
else:
|
|
79
|
+
|
|
80
|
+
@functools.wraps(func)
|
|
81
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
82
|
+
with I18nContext(language):
|
|
83
|
+
return func(*args, **kwargs)
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
return decorator
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def language_context(language: Language) -> Callable[[F], F]:
|
|
91
|
+
"""Decorator: run a sync function within a language context"""
|
|
92
|
+
return _language_context_decorator(language, is_async=False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def async_language_context(
|
|
96
|
+
language: Language,
|
|
97
|
+
) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]:
|
|
98
|
+
"""Decorator: run an async function within a language context"""
|
|
99
|
+
return _language_context_decorator(language, is_async=True)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Load i18n data and export get message method"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
from fastlib.constant import RESOURCE_DIR
|
|
9
|
+
from fastlib.i18n.types import Language
|
|
10
|
+
|
|
11
|
+
from .manager import get_language
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_DIR = Path(RESOURCE_DIR) / "i18n"
|
|
14
|
+
TRANSLATIONS = {
|
|
15
|
+
"zh": {"debug": {"test_message": "这是一条调试测试消息"}},
|
|
16
|
+
"en": {"debug": {"test_message": "This is a debug test message"}},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_translations(base_dirs: Union[Path, list[Path], None] = None):
|
|
21
|
+
"""Load translation files from specified directories.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_dirs: Path or list of paths to translation directories.
|
|
25
|
+
|
|
26
|
+
File structure:
|
|
27
|
+
base_dir/{lang}/{namespace}.json
|
|
28
|
+
Example: i18n/en/error.json
|
|
29
|
+
|
|
30
|
+
Note: Later directories override earlier ones for duplicate keys.
|
|
31
|
+
"""
|
|
32
|
+
if base_dirs is None:
|
|
33
|
+
base_dirs = [DEFAULT_BASE_DIR]
|
|
34
|
+
elif isinstance(base_dirs, Path):
|
|
35
|
+
base_dirs = [base_dirs]
|
|
36
|
+
|
|
37
|
+
for base_dir in base_dirs:
|
|
38
|
+
if not base_dir.exists():
|
|
39
|
+
continue # Skip if directory doesn't exist
|
|
40
|
+
|
|
41
|
+
for lang in [Language.CHINESE, Language.ENGLISH]:
|
|
42
|
+
lang_dir = base_dir / lang.value
|
|
43
|
+
if not lang_dir.exists():
|
|
44
|
+
continue # Skip if language directory doesn't exist
|
|
45
|
+
|
|
46
|
+
# Initialize language dict if not exists
|
|
47
|
+
if lang.value not in TRANSLATIONS:
|
|
48
|
+
TRANSLATIONS[lang.value] = {}
|
|
49
|
+
|
|
50
|
+
for file in lang_dir.glob("*.json"):
|
|
51
|
+
namespace = (
|
|
52
|
+
file.stem
|
|
53
|
+
) # Determine message_type from filename, e.g., error, success
|
|
54
|
+
with open(file, encoding="utf-8") as f:
|
|
55
|
+
translations_data = json.load(f)
|
|
56
|
+
|
|
57
|
+
# Merge with existing namespace data or create new
|
|
58
|
+
if namespace in TRANSLATIONS[lang.value]:
|
|
59
|
+
TRANSLATIONS[lang.value][namespace].update(translations_data)
|
|
60
|
+
else:
|
|
61
|
+
TRANSLATIONS[lang.value][namespace] = translations_data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_message(
|
|
65
|
+
message_type: str, key: str, language: Optional[Language] = None
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Get translated message for given type, key and language.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
message_type: Namespace/category of the message (e.g., 'error', 'success')
|
|
71
|
+
key: Message identifier key
|
|
72
|
+
language: Target language
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Translated message or fallback to default language if not found
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
if not language:
|
|
79
|
+
language = get_language()
|
|
80
|
+
# Try to get message from requested language
|
|
81
|
+
lang_dict = TRANSLATIONS.get(language.value, {})
|
|
82
|
+
msg = lang_dict.get(message_type, {}).get(key)
|
|
83
|
+
print(f"lang_dict: {lang_dict}")
|
|
84
|
+
if msg:
|
|
85
|
+
return msg
|
|
86
|
+
|
|
87
|
+
default_language = Language.ENGLISH.value
|
|
88
|
+
# Fallback to default language
|
|
89
|
+
return (
|
|
90
|
+
TRANSLATIONS.get(default_language, {})
|
|
91
|
+
.get(message_type, {})
|
|
92
|
+
.get(key, f"Unknown {message_type} message: {key}")
|
|
93
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""
|
|
3
|
+
Internationalization type definitions with extensible language support
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Language(Enum):
|
|
11
|
+
"""Extensible language enumeration with metadata support"""
|
|
12
|
+
|
|
13
|
+
CHINESE = "zh"
|
|
14
|
+
ENGLISH = "en"
|
|
15
|
+
|
|
16
|
+
# Language metadata (can be extended)
|
|
17
|
+
_metadata: dict[str, dict[str, str]] = {
|
|
18
|
+
"zh": {"name": "Chinese", "native_name": "中文", "direction": "ltr"},
|
|
19
|
+
"en": {"name": "English", "native_name": "English", "direction": "ltr"},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def display_name(self) -> str:
|
|
24
|
+
"""Get language display name"""
|
|
25
|
+
return self._metadata[self.value]["name"]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def native_name(self) -> str:
|
|
29
|
+
"""Get native language name"""
|
|
30
|
+
return self._metadata[self.value]["native_name"]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def direction(self) -> str:
|
|
34
|
+
"""Get text direction (ltr/rtl)"""
|
|
35
|
+
return self._metadata[self.value]["direction"]
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def register(
|
|
39
|
+
cls, code: str, name: str, native_name: str, direction: str = "ltr"
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Register a new language dynamically
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
code: Language code (e.g., 'es', 'fr')
|
|
45
|
+
name: English display name
|
|
46
|
+
native_name: Native language name
|
|
47
|
+
direction: Text direction ('ltr' or 'rtl')
|
|
48
|
+
"""
|
|
49
|
+
if hasattr(cls, code.upper()):
|
|
50
|
+
raise ValueError(f"Language {code} already registered")
|
|
51
|
+
|
|
52
|
+
# Create new enum member
|
|
53
|
+
new_member = Enum.value.__new__(cls)
|
|
54
|
+
new_member._value_ = code
|
|
55
|
+
cls._member_map_[code.upper()] = new_member
|
|
56
|
+
cls._member_names_.append(code.upper())
|
|
57
|
+
|
|
58
|
+
# Add metadata
|
|
59
|
+
cls._metadata[code] = {
|
|
60
|
+
"name": name,
|
|
61
|
+
"native_name": native_name,
|
|
62
|
+
"direction": direction,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_code(cls, code: str) -> Optional["Language"]:
|
|
67
|
+
"""Get Language enum from language code
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
code: Language code (e.g., 'zh', 'en')
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Language enum or None if not found
|
|
74
|
+
"""
|
|
75
|
+
code_upper = code.upper()
|
|
76
|
+
return cls._member_map_.get(code_upper)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def supported_codes(cls) -> set[str]:
|
|
80
|
+
"""Get set of all supported language codes"""
|
|
81
|
+
return {member.value for member in cls}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def is_supported(cls, code: str) -> bool:
|
|
85
|
+
"""Check if language code is supported"""
|
|
86
|
+
return code in cls.supported_codes()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# if __name__ == "__main__":
|
|
90
|
+
# # Register new languages dynamically
|
|
91
|
+
# Language.register("es", "Spanish", "Español")
|
|
92
|
+
# Language.register("ja", "Japanese", "日本語")
|
|
93
|
+
# Language.register("ar", "Arabic", "العربية", "rtl")
|
|
94
|
+
|
|
95
|
+
# # Access new languages
|
|
96
|
+
# spanish = Language.ES
|
|
97
|
+
# print(f"Spanish code: {spanish.value}") # es
|
|
98
|
+
# print(f"Spanish native: {spanish.native_name}") # Español
|
|
99
|
+
|
|
100
|
+
# # Check support
|
|
101
|
+
# print(Language.is_supported("fr")) # False
|
|
102
|
+
# print(Language.is_supported("ja")) # True
|
|
103
|
+
|
|
104
|
+
# # Get from code
|
|
105
|
+
# japanese = Language.from_code("ja")
|
|
106
|
+
# print(japanese.display_name) # Japanese
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""
|
|
3
|
+
Internationalization utilities
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .manager import I18nContext
|
|
9
|
+
from .messages import get_message
|
|
10
|
+
from .types import Language
|
|
11
|
+
|
|
12
|
+
# Default language
|
|
13
|
+
DEFAULT_LANGUAGE = Language.ENGLISH
|
|
14
|
+
|
|
15
|
+
# Language alias mapping
|
|
16
|
+
LANGUAGE_MAPPING = {
|
|
17
|
+
"zh": Language.CHINESE,
|
|
18
|
+
"zh_cn": Language.CHINESE,
|
|
19
|
+
"en": Language.ENGLISH,
|
|
20
|
+
"en_us": Language.ENGLISH,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_language(language: Optional[Language]) -> Language:
|
|
25
|
+
"""Return given language or current context default"""
|
|
26
|
+
return language or I18nContext.get_language()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_language(language_str: Optional[str]) -> Language:
|
|
30
|
+
"""Parse string like 'zh-CN', 'en-US' to Language enum"""
|
|
31
|
+
if not language_str:
|
|
32
|
+
return DEFAULT_LANGUAGE
|
|
33
|
+
return LANGUAGE_MAPPING.get(
|
|
34
|
+
language_str.lower().replace("-", "_"), DEFAULT_LANGUAGE
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_localized_message(
|
|
39
|
+
category: str, key: str, language: Optional[Language] = None
|
|
40
|
+
) -> str:
|
|
41
|
+
"""Get localized message by category ('error', 'success', 'business')"""
|
|
42
|
+
lang = _resolve_language(language)
|
|
43
|
+
return get_message(category, key, lang)
|
|
@@ -64,3 +64,12 @@ class LogConfig(BaseConfig):
|
|
|
64
64
|
def error_log_file_pattern(self) -> str:
|
|
65
65
|
"""Returns the error log file pattern with timestamp."""
|
|
66
66
|
return f"{{time:{self.time_format}}}_error_{self.app_name}.log"
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Returns a string representation of the database configuration.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A string representation of the DatabaseConfig instance.
|
|
74
|
+
"""
|
|
75
|
+
return f"{self.__class__.__name__}({self.__dict__})"
|
|
@@ -11,7 +11,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
11
11
|
from fastlib.constant import FilterOperators, constant
|
|
12
12
|
from fastlib.enums import SortEnum
|
|
13
13
|
from fastlib.mapper.base_mapper import BaseMapper
|
|
14
|
-
from fastlib.middleware.
|
|
14
|
+
from fastlib.middleware.db_session import db
|
|
15
15
|
from fastlib.schema import SortItem
|
|
16
16
|
|
|
17
17
|
IDType = TypeVar("IDType", int, str)
|