fastlib-py 0.2.1__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.1 → fastlib_py-0.2.2}/PKG-INFO +1 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/cache/manager.py +1 -2
- {fastlib_py-0.2.1 → 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.1 → fastlib_py-0.2.2}/fastlib/i18n/manager.py +4 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/i18n/messages.py +23 -18
- 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.1 → fastlib_py-0.2.2}/fastlib/middleware/i18n.py +10 -10
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/middleware/jwt.py +33 -7
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib_py.egg-info/PKG-INFO +1 -1
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/pyproject.toml +1 -1
- fastlib_py-0.2.1/fastlib/constant/constant.py +0 -31
- 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_py-0.2.2}/LICENSE +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/README.md +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/cache/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/cache/base.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/cache/disk_cache.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/cache/redis_cache.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/base.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/database_config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/loader.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/manager.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/registry.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/security_config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/server_config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/config/utils.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/context/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/context/contextvars.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/enums/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/enums/base_error_code.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/enums/enum.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/exception/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/exception/custom_exception.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/exception/exception_handler.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/exception/exception_manager.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/logging/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/logging/config.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/logging/handlers.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/mapper/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/mapper/base_mapper.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/mapper/impl/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/middleware/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/middleware/db_session.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/middleware/log.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/migrate/migrate.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/model/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/model/base_model.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/openapi/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/openapi/offline.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/router/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/router/router_v1.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/schema/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/schema/response.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/schema/schema.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/security/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/security/security.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/security/utils.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/service/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/service/base_service.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/service/impl/base_service_impl.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/session/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/session/db_engine.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/session/db_session.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/utils/excel_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/utils/file_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/utils/http_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/utils/snowflake_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib_py.egg-info/SOURCES.txt +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib_py.egg-info/requires.txt +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/fastlib_py.egg-info/top_level.txt +0 -0
- {fastlib_py-0.2.1 → fastlib_py-0.2.2}/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"
|
|
@@ -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)
|
|
@@ -1,32 +1,33 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Load i18n data and export get message method"""
|
|
3
|
+
|
|
1
4
|
import json
|
|
2
5
|
from pathlib import Path
|
|
3
|
-
from typing import
|
|
4
|
-
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
from fastlib.constant import RESOURCE_DIR
|
|
5
9
|
from fastlib.i18n.types import Language
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
DEFAULT_BASE_DIR = Path(RESOURCE_DIR) / "i18n"
|
|
11
|
+
from .manager import get_language
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
DEFAULT_BASE_DIR = Path(RESOURCE_DIR) / "i18n"
|
|
11
14
|
TRANSLATIONS = {
|
|
12
15
|
"zh": {"debug": {"test_message": "这是一条调试测试消息"}},
|
|
13
16
|
"en": {"debug": {"test_message": "This is a debug test message"}},
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
|
|
17
|
-
def load_translations(base_dirs: Union[Path,
|
|
18
|
-
"""Load
|
|
20
|
+
def load_translations(base_dirs: Union[Path, list[Path], None] = None):
|
|
21
|
+
"""Load translation files from specified directories.
|
|
19
22
|
|
|
20
23
|
Args:
|
|
21
|
-
base_dirs:
|
|
22
|
-
If not provided, uses the default DEFAULT_BASE_DIR.
|
|
24
|
+
base_dirs: Path or list of paths to translation directories.
|
|
23
25
|
|
|
24
|
-
File structure
|
|
25
|
-
|
|
26
|
-
Example: i18n/
|
|
26
|
+
File structure:
|
|
27
|
+
base_dir/{lang}/{namespace}.json
|
|
28
|
+
Example: i18n/en/error.json
|
|
27
29
|
|
|
28
|
-
Note: Later directories
|
|
29
|
-
if the same language and namespace exist.
|
|
30
|
+
Note: Later directories override earlier ones for duplicate keys.
|
|
30
31
|
"""
|
|
31
32
|
if base_dirs is None:
|
|
32
33
|
base_dirs = [DEFAULT_BASE_DIR]
|
|
@@ -50,7 +51,7 @@ def load_translations(base_dirs: Union[Path, List[Path], None] = None):
|
|
|
50
51
|
namespace = (
|
|
51
52
|
file.stem
|
|
52
53
|
) # Determine message_type from filename, e.g., error, success
|
|
53
|
-
with open(file,
|
|
54
|
+
with open(file, encoding="utf-8") as f:
|
|
54
55
|
translations_data = json.load(f)
|
|
55
56
|
|
|
56
57
|
# Merge with existing namespace data or create new
|
|
@@ -60,7 +61,9 @@ def load_translations(base_dirs: Union[Path, List[Path], None] = None):
|
|
|
60
61
|
TRANSLATIONS[lang.value][namespace] = translations_data
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def get_message(
|
|
64
|
+
def get_message(
|
|
65
|
+
message_type: str, key: str, language: Optional[Language] = None
|
|
66
|
+
) -> str:
|
|
64
67
|
"""Get translated message for given type, key and language.
|
|
65
68
|
|
|
66
69
|
Args:
|
|
@@ -71,15 +74,17 @@ def get_message(message_type: str, key: str, language: Language) -> str:
|
|
|
71
74
|
Returns:
|
|
72
75
|
Translated message or fallback to default language if not found
|
|
73
76
|
"""
|
|
74
|
-
default_language = Language.CHINESE.value
|
|
75
77
|
|
|
78
|
+
if not language:
|
|
79
|
+
language = get_language()
|
|
76
80
|
# Try to get message from requested language
|
|
77
81
|
lang_dict = TRANSLATIONS.get(language.value, {})
|
|
78
82
|
msg = lang_dict.get(message_type, {}).get(key)
|
|
79
|
-
|
|
83
|
+
print(f"lang_dict: {lang_dict}")
|
|
80
84
|
if msg:
|
|
81
85
|
return msg
|
|
82
86
|
|
|
87
|
+
default_language = Language.ENGLISH.value
|
|
83
88
|
# Fallback to default language
|
|
84
89
|
return (
|
|
85
90
|
TRANSLATIONS.get(default_language, {})
|
|
@@ -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)
|
|
@@ -7,40 +7,40 @@ from fastlib.i18n.types import Language
|
|
|
7
7
|
class I18nMiddleware(BaseHTTPMiddleware):
|
|
8
8
|
"""
|
|
9
9
|
Middleware for automatic language detection and setting based on HTTP headers.
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
This middleware inspects the Accept-Language header from incoming requests
|
|
12
12
|
and sets the appropriate language in the request state for use throughout
|
|
13
13
|
the application.
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
The language preference is stored in request.state.language and can be
|
|
16
16
|
accessed in route handlers and other middleware.
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Example:
|
|
19
19
|
```python
|
|
20
20
|
from fastapi import FastAPI, Request
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
app = FastAPI()
|
|
23
23
|
app.add_middleware(I18nMiddleware)
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
@app.get("/")
|
|
26
26
|
async def root(request: Request):
|
|
27
27
|
language = request.state.language
|
|
28
28
|
return {"language": language.value}
|
|
29
29
|
```
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
Note:
|
|
32
32
|
Currently supports only English and Chinese. Defaults to English
|
|
33
33
|
if the Accept-Language header is not present or unrecognized.
|
|
34
34
|
"""
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
async def dispatch(self, request, call_next):
|
|
37
37
|
"""
|
|
38
38
|
Process the request and set the language based on Accept-Language header.
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
Args:
|
|
41
41
|
request: The incoming HTTP request
|
|
42
42
|
call_next: The next middleware or route handler in the chain
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
Returns:
|
|
45
45
|
The HTTP response from the next handler
|
|
46
46
|
"""
|
|
@@ -54,4 +54,4 @@ class I18nMiddleware(BaseHTTPMiddleware):
|
|
|
54
54
|
request.state.language = Language.CHINESE
|
|
55
55
|
|
|
56
56
|
response = await call_next(request)
|
|
57
|
-
return response
|
|
57
|
+
return response
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""JWT middleware for FastAPI authentication"""
|
|
3
3
|
|
|
4
4
|
import http
|
|
5
|
+
from enum import Enum
|
|
5
6
|
|
|
6
7
|
from fastapi import Request
|
|
7
8
|
from jwt.exceptions import PyJWTError
|
|
@@ -11,7 +12,6 @@ from starlette.responses import JSONResponse
|
|
|
11
12
|
from fastlib import ConfigManager, constant, security
|
|
12
13
|
from fastlib.context.contextvars import current_user_id
|
|
13
14
|
from fastlib.enums.enum import MediaTypeEnum
|
|
14
|
-
from fastlib.enums import AuthErrorCode
|
|
15
15
|
from fastlib.schema import UserCredential
|
|
16
16
|
|
|
17
17
|
# Load configuration
|
|
@@ -19,6 +19,32 @@ server_config = ConfigManager.get_server_config()
|
|
|
19
19
|
security_config = ConfigManager.get_security_config()
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class JWTErrorCode(Enum):
|
|
23
|
+
"""Authentication error codes with code and message"""
|
|
24
|
+
|
|
25
|
+
OPENAPI_FORBIDDEN = (403, "OpenAPI documentation is disabled")
|
|
26
|
+
TOKEN_EXPIRED = (401, "Authorization token has expired")
|
|
27
|
+
MISSING_TOKEN = (401, "Authorization token is missing")
|
|
28
|
+
|
|
29
|
+
def __init__(self, code: int, message: str):
|
|
30
|
+
self._code = code
|
|
31
|
+
self._message = message
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def code(self) -> int:
|
|
35
|
+
"""Get error code"""
|
|
36
|
+
return self._code
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def message(self) -> str:
|
|
40
|
+
"""Get error message"""
|
|
41
|
+
return self._message
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
"""Convert to dictionary format for JSON response"""
|
|
45
|
+
return {"code": self.code, "message": self.message}
|
|
46
|
+
|
|
47
|
+
|
|
22
48
|
async def jwt_middleware(request: Request, call_next):
|
|
23
49
|
ctx_token: UserCredential | None = None
|
|
24
50
|
try:
|
|
@@ -32,8 +58,8 @@ async def jwt_middleware(request: Request, call_next):
|
|
|
32
58
|
return JSONResponse(
|
|
33
59
|
status_code=http.HTTPStatus.FORBIDDEN,
|
|
34
60
|
content={
|
|
35
|
-
"code":
|
|
36
|
-
"message":
|
|
61
|
+
"code": JWTErrorCode.OPENAPI_FORBIDDEN.code,
|
|
62
|
+
"message": JWTErrorCode.OPENAPI_FORBIDDEN.message,
|
|
37
63
|
},
|
|
38
64
|
)
|
|
39
65
|
|
|
@@ -63,16 +89,16 @@ async def jwt_middleware(request: Request, call_next):
|
|
|
63
89
|
return JSONResponse(
|
|
64
90
|
status_code=http.HTTPStatus.UNAUTHORIZED,
|
|
65
91
|
content={
|
|
66
|
-
"code":
|
|
67
|
-
"message":
|
|
92
|
+
"code": JWTErrorCode.TOKEN_EXPIRED.code,
|
|
93
|
+
"message": JWTErrorCode.TOKEN_EXPIRED.message,
|
|
68
94
|
},
|
|
69
95
|
)
|
|
70
96
|
else:
|
|
71
97
|
return JSONResponse(
|
|
72
98
|
status_code=http.HTTPStatus.UNAUTHORIZED,
|
|
73
99
|
content={
|
|
74
|
-
"code":
|
|
75
|
-
"message":
|
|
100
|
+
"code": JWTErrorCode.MISSING_TOKEN.code,
|
|
101
|
+
"message": JWTErrorCode.MISSING_TOKEN.message,
|
|
76
102
|
},
|
|
77
103
|
)
|
|
78
104
|
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
-
"""Common constant"""
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
from fastlib.utils import file_util
|
|
7
|
-
|
|
8
|
-
RESOURCE_DIR: str = os.path.abspath(
|
|
9
|
-
os.path.join(file_util.find_project_root(), "src", "main", "resource")
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
ADMIN_ID = 9
|
|
13
|
-
ROOT_PARENT_ID = 0
|
|
14
|
-
MAX_PAGE_SIZE = 1000
|
|
15
|
-
|
|
16
|
-
ENV = "env"
|
|
17
|
-
PARENT_ID = "parent_id"
|
|
18
|
-
CONFIG_FILE = "config_file"
|
|
19
|
-
CONFIG_FILE_NAME = "config.yml"
|
|
20
|
-
AUTHORIZATION = "Authorization"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class FilterOperators:
|
|
24
|
-
EQ = "EQ"
|
|
25
|
-
NE = "NE"
|
|
26
|
-
GT = "GT"
|
|
27
|
-
GE = "GE"
|
|
28
|
-
LT = "LT"
|
|
29
|
-
LE = "LE"
|
|
30
|
-
BETWEEN = "BETWEEN"
|
|
31
|
-
LIKE = "LIKE"
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Internationalization utilities
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Any, Optional
|
|
6
|
-
|
|
7
|
-
from .manager import I18nContext
|
|
8
|
-
from .messages import get_message
|
|
9
|
-
from .types import Language
|
|
10
|
-
|
|
11
|
-
# Default language
|
|
12
|
-
DEFAULT_LANGUAGE = Language.ENGLISH
|
|
13
|
-
|
|
14
|
-
# Language alias mapping
|
|
15
|
-
LANGUAGE_MAPPING = {
|
|
16
|
-
"zh": Language.CHINESE,
|
|
17
|
-
"zh_cn": Language.CHINESE,
|
|
18
|
-
"en": Language.ENGLISH,
|
|
19
|
-
"en_us": Language.ENGLISH,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
# --- Helpers ---
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _resolve_language(language: Optional[Language]) -> Language:
|
|
26
|
-
"""Return given language or current context default"""
|
|
27
|
-
return language or I18nContext.get_language()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# --- Core utilities ---
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def parse_language(language_str: Optional[str]) -> Language:
|
|
34
|
-
"""Parse string like 'zh-CN', 'en-US' to Language enum"""
|
|
35
|
-
if not language_str:
|
|
36
|
-
return DEFAULT_LANGUAGE
|
|
37
|
-
return LANGUAGE_MAPPING.get(
|
|
38
|
-
language_str.lower().replace("-", "_"), DEFAULT_LANGUAGE
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def get_localized_message(
|
|
43
|
-
category: str, key: str, language: Optional[Language] = None
|
|
44
|
-
) -> str:
|
|
45
|
-
"""Get localized message by category ('error', 'success', 'business')"""
|
|
46
|
-
lang = _resolve_language(language)
|
|
47
|
-
return get_message(category, key, lang)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def get_error_message(key: str, language: Optional[Language] = None) -> str:
|
|
51
|
-
return get_localized_message("error", key, language)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def get_success_message(key: str, language: Optional[Language] = None) -> str:
|
|
55
|
-
return get_localized_message("success", key, language)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def create_error_response(
|
|
59
|
-
error_key: str,
|
|
60
|
-
language: Optional[Language] = None,
|
|
61
|
-
details: Optional[str] = None,
|
|
62
|
-
error_code: int = 400,
|
|
63
|
-
) -> dict[str, Any]:
|
|
64
|
-
lang = _resolve_language(language)
|
|
65
|
-
error = {
|
|
66
|
-
"code": error_code,
|
|
67
|
-
"message": get_error_message(error_key, lang),
|
|
68
|
-
"language": lang.value,
|
|
69
|
-
}
|
|
70
|
-
if details:
|
|
71
|
-
error["details"] = details
|
|
72
|
-
return {"success": False, "error": error}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def create_success_response(
|
|
76
|
-
data: Any,
|
|
77
|
-
language: Optional[Language] = None,
|
|
78
|
-
message_key: str = "search_success",
|
|
79
|
-
) -> dict[str, Any]:
|
|
80
|
-
lang = _resolve_language(language)
|
|
81
|
-
return {
|
|
82
|
-
"success": True,
|
|
83
|
-
"data": data,
|
|
84
|
-
"message": get_success_message(message_key, lang),
|
|
85
|
-
"language": lang.value,
|
|
86
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|