fastlib-py 0.3.6__tar.gz → 0.3.8__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 (81) hide show
  1. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/PKG-INFO +3 -4
  2. fastlib_py-0.3.8/fastlib/_logging/config.py +109 -0
  3. fastlib_py-0.3.8/fastlib/cache/_memony_cache.py +35 -0
  4. fastlib_py-0.3.8/fastlib/cache/_redis_cache.py +58 -0
  5. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/cache/manager.py +8 -7
  6. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/logging.py +1 -1
  7. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/router.py +0 -1
  8. {fastlib_py-0.3.6/fastlib/security → fastlib_py-0.3.8/fastlib}/security.py +124 -13
  9. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/stream/handler.py +3 -4
  10. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib_py.egg-info/PKG-INFO +3 -4
  11. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib_py.egg-info/SOURCES.txt +2 -4
  12. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib_py.egg-info/requires.txt +2 -3
  13. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/pyproject.toml +5 -6
  14. fastlib_py-0.3.6/fastlib/_logging/config.py +0 -102
  15. fastlib_py-0.3.6/fastlib/cache/_memony_cache.py +0 -267
  16. fastlib_py-0.3.6/fastlib/cache/_redis_cache.py +0 -370
  17. fastlib_py-0.3.6/fastlib/cache/base.py +0 -175
  18. fastlib_py-0.3.6/fastlib/security/__init__.py +0 -23
  19. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/LICENSE +0 -0
  20. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/README.md +0 -0
  21. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/__init__.py +0 -0
  22. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/_logging/__init__.py +0 -0
  23. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/_logging/handlers.py +0 -0
  24. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/cache/__init__.py +0 -0
  25. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/__init__.py +0 -0
  26. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/_database_config.py +0 -0
  27. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/_security_config.py +0 -0
  28. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/_server_config.py +0 -0
  29. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/base.py +0 -0
  30. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/loader.py +0 -0
  31. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/manager.py +0 -0
  32. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/registry.py +0 -0
  33. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/config/utils.py +0 -0
  34. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/constants.py +0 -0
  35. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/contextvars.py +0 -0
  36. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/db_engine.py +0 -0
  37. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/enums.py +0 -0
  38. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/exception/__init__.py +0 -0
  39. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/exception/base.py +0 -0
  40. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/exception/exception_handler.py +0 -0
  41. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/exception/exception_manager.py +0 -0
  42. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/i18n/__init__.py +0 -0
  43. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/i18n/manager.py +0 -0
  44. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/i18n/messages.py +0 -0
  45. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/i18n/types.py +0 -0
  46. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/i18n/utils.py +0 -0
  47. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/mapper/__init__.py +0 -0
  48. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/mapper/base_mapper.py +0 -0
  49. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/mapper/impl/__init__.py +0 -0
  50. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
  51. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/middleware/__init__.py +0 -0
  52. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/middleware/db_session.py +0 -0
  53. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/middleware/i18n.py +0 -0
  54. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/middleware/jwt.py +0 -0
  55. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/migrate.py +0 -0
  56. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/openapi.py +0 -0
  57. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/py.typed +0 -0
  58. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/request.py +0 -0
  59. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/response.py +0 -0
  60. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/schema.py +0 -0
  61. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/service/__init__.py +0 -0
  62. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/service/base_service.py +0 -0
  63. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/service/impl/__init__.py +0 -0
  64. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/service/impl/base_service_impl.py +0 -0
  65. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/stream/__init__.py +0 -0
  66. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/stream/enums.py +0 -0
  67. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/stream/event.py +0 -0
  68. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/stream/schema.py +0 -0
  69. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/stream/sse.py +0 -0
  70. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/__init__.py +0 -0
  71. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/excel_util.py +0 -0
  72. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/file_util.py +0 -0
  73. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/http_util.py +0 -0
  74. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/ip_util.py +0 -0
  75. /fastlib_py-0.3.6/fastlib/security/utils.py → /fastlib_py-0.3.8/fastlib/utils/security_util.py +0 -0
  76. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/snowflake_util.py +0 -0
  77. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/str_util.py +0 -0
  78. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib/utils/validate_util.py +0 -0
  79. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib_py.egg-info/dependency_links.txt +0 -0
  80. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/fastlib_py.egg-info/top_level.txt +0 -0
  81. {fastlib_py-0.3.6 → fastlib_py-0.3.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.3.6
3
+ Version: 0.3.8
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>
@@ -21,13 +21,12 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: aiosqlite>=0.21
23
23
  Requires-Dist: alembic>=1.15.2
24
- Requires-Dist: cryptography>=45.0.6
24
+ Requires-Dist: fakeredis>=2.32
25
25
  Requires-Dist: fastapi[standard]>=0.115.12
26
26
  Requires-Dist: loguru>=0.7.3
27
27
  Requires-Dist: openpyxl>=3.1.5
28
28
  Requires-Dist: pandas>=2.2.3
29
- Requires-Dist: passlib[bcrypt]>=1.7.4
30
- Requires-Dist: pyjwt>=2.10.1
29
+ Requires-Dist: pyjwt[crypto]>=2.10.1
31
30
  Requires-Dist: sqlmodel>=0.0.24
32
31
  Dynamic: license-file
33
32
 
@@ -0,0 +1,109 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ Log configuration and logging utility for the application.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ from fastlib.config.base import BaseConfig
11
+ from fastlib.config.manager import config_class
12
+ from fastlib.config.utils import ProjectInfo
13
+
14
+ LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
15
+
16
+
17
+ @config_class("log")
18
+ @dataclass
19
+ class LogConfig(BaseConfig):
20
+ """
21
+ Configuration for logging settings.
22
+
23
+ Attributes:
24
+ log_dir (str): Directory where log files will be stored.
25
+ app_name (str): Name of the application for log file naming.
26
+ log_level (LogLevel): Minimum log level to record. Possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL.
27
+ log_rotation (str): Frequency of log file rotation. Default is "1 day".
28
+ log_retention (str): How long to keep log files. Default is "90 days".
29
+ enable_console_log (bool): Whether to output logs to console. Default is True.
30
+ enable_json_logs (bool): Whether to use JSON format for logs. Default is False.
31
+ error_log_rotation (str): Frequency of error log rotation. Default is "1 day".
32
+ error_log_retention (str): How long to keep error log files. Default is "90 days".
33
+ enable_async (bool): Whether to use asynchronous logging. Default is True.
34
+ max_file_size (Optional[str]): Maximum size for log files before rotation. Default is "10 MB".
35
+ time_format (str): Format string for timestamps in log file names. Default is "YYYY-MM-DD".
36
+ """
37
+
38
+ log_dir: str = "/var/log"
39
+ app_name: str | None = None
40
+ log_level: LogLevel = "INFO"
41
+ log_rotation: str = "1 day"
42
+ log_retention: str = "90 days"
43
+ enable_console_log: bool = True
44
+ enable_json_logs: bool = False
45
+ error_log_rotation: str = "1 day"
46
+ error_log_retention: str = "90 days"
47
+ enable_async: bool = True
48
+ max_file_size: str | None = "10 MB"
49
+ time_format: str = "YYYY-MM-DD"
50
+
51
+ def __post_init__(self):
52
+ """
53
+ Set default app_name if not provided.
54
+
55
+ If `app_name` is not provided, it will be set to the name of the project
56
+ retrieved from the `pyproject.toml` file.
57
+ """
58
+ if self.app_name is None:
59
+ self.app_name = ProjectInfo.from_pyproject().name
60
+
61
+ @property
62
+ def default_format(self) -> str:
63
+ """
64
+ Default log format string.
65
+
66
+ Returns:
67
+ str: The default format used for log messages.
68
+ """
69
+ return "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message}"
70
+
71
+ @property
72
+ def log_file_pattern(self) -> str:
73
+ """
74
+ Returns the main log file pattern with timestamp.
75
+
76
+ Returns:
77
+ str: The pattern used to name the main log file.
78
+ """
79
+ return f"{{time:{self.time_format}}}_{self.app_name}.log"
80
+
81
+ @property
82
+ def error_log_file_pattern(self) -> str:
83
+ """
84
+ Returns the error log file pattern with timestamp.
85
+
86
+ Returns:
87
+ str: The pattern used to name the error log file.
88
+ """
89
+ return f"{{time:{self.time_format}}}_error_{self.app_name}.log"
90
+
91
+ @property
92
+ def log_file_path(self) -> Path:
93
+ """
94
+ Returns the full path for the main log file.
95
+
96
+ Returns:
97
+ Path: The full path to the main log file.
98
+ """
99
+ return Path(self.log_dir) / self.log_file_pattern
100
+
101
+ @property
102
+ def error_log_file_path(self) -> Path:
103
+ """
104
+ Returns the full path for the error log file.
105
+
106
+ Returns:
107
+ Path: The full path to the error log file.
108
+ """
109
+ return Path(self.log_dir) / self.error_log_file_pattern
@@ -0,0 +1,35 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Memory (FakeRedis) Cache Manager for in-memory caching"""
3
+
4
+ import asyncio
5
+
6
+ from fakeredis.aioredis import FakeRedis
7
+
8
+
9
+ class MemoryCacheManager:
10
+ """Singleton manager for in-memory (FakeRedis) cache client."""
11
+
12
+ _instance: FakeRedis | None = None
13
+ _lock = asyncio.Lock()
14
+
15
+ @classmethod
16
+ async def get_instance(cls) -> FakeRedis:
17
+ """Get a singleton FakeRedis instance."""
18
+ if cls._instance is None:
19
+ async with cls._lock:
20
+ if cls._instance is None:
21
+ cls._instance = FakeRedis()
22
+ return cls._instance
23
+
24
+ @classmethod
25
+ async def close(cls):
26
+ """Close the FakeRedis instance (for cleanup in tests)."""
27
+ if cls._instance is not None:
28
+ await cls._instance.close()
29
+ cls._instance = None
30
+
31
+ @classmethod
32
+ async def reset(cls):
33
+ """Clear all cached data without destroying the instance."""
34
+ if cls._instance is not None:
35
+ await cls._instance.flushall()
@@ -0,0 +1,58 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Redis cache implementation"""
3
+
4
+ import asyncio
5
+ from typing import Optional
6
+
7
+ from loguru import logger
8
+
9
+ from fastlib import ConfigManager
10
+
11
+ try:
12
+ import redis.asyncio as redis
13
+ except ModuleNotFoundError:
14
+ logger.warning(
15
+ "Cannot find redis module, please install it via `uv add redis[hiredis]`"
16
+ )
17
+ redis = None
18
+ RedisError = Exception
19
+ except Exception:
20
+ logger.error("Error importing redis module")
21
+ redis = None
22
+ RedisError = Exception
23
+
24
+
25
+ class RedisCacheManager:
26
+ _instance: Optional["redis.Redis"] = None
27
+ _connection_pool: Optional["redis.ConnectionPool"] = None
28
+ _lock = asyncio.Lock()
29
+
30
+ @classmethod
31
+ async def get_instance(cls) -> "redis.Redis":
32
+ """
33
+ Get redis instance
34
+ """
35
+ if redis is None:
36
+ raise RuntimeError(
37
+ "Redis module is not available. Please install it via 'uv add redis[hiredis]'"
38
+ )
39
+
40
+ if cls._instance is None:
41
+ async with cls._lock:
42
+ if cls._connection_pool is None:
43
+ database_config = ConfigManager.get_database_config()
44
+ cls._connection_pool = redis.ConnectionPool.from_url(
45
+ f"redis://:{database_config.cache_pass}@{database_config.cache_host}:{database_config.cache_port}/{database_config.db_num}",
46
+ decode_responses=True,
47
+ )
48
+ if cls._instance is None:
49
+ cls._instance = redis.Redis(connection_pool=cls._connection_pool)
50
+ return cls._instance
51
+
52
+ @classmethod
53
+ async def close_pool(cls):
54
+ """Close the connection pool."""
55
+ if cls._connection_pool:
56
+ await cls._connection_pool.disconnect()
57
+ cls._connection_pool = None
58
+ cls._instance = None
@@ -1,23 +1,24 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  """Cache Client manager to instantiate the appropriate cache client"""
3
3
 
4
- from fastlib.cache.base import Cache
5
4
  from fastlib.config.manager import ConfigManager
6
5
 
7
6
 
8
- async def get_cache_client() -> Cache:
7
+ async def get_cache_client():
9
8
  """Initialize and return the appropriate cache client based on configuration.
10
9
 
11
10
  Returns:
12
- Cache: Redis client if Redis is enabled in config, otherwise returns page cache.
11
+ Cache: Redis client if Redis is enabled in config, otherwise returns FakeRedis client.
13
12
  """
14
13
  config = ConfigManager.get_database_config()
14
+
15
15
  if config.enable_redis:
16
- from fastlib.cache._redis_cache import RedisCache, RedisManager
16
+ from fastlib.cache._redis_cache import RedisManager
17
17
 
18
18
  redis_client = await RedisManager.get_instance()
19
- return RedisCache(redis_client)
19
+ return redis_client
20
20
  else:
21
- from fastlib.cache._memony_cache import MemoryCache
21
+ from fastlib.cache._memony_cache import MemoryCacheManager
22
22
 
23
- return MemoryCache()
23
+ fake_client = await MemoryCacheManager.get_instance()
24
+ return fake_client
@@ -3,6 +3,6 @@
3
3
  Logging module
4
4
  """
5
5
 
6
- from ._logging.handlers import Logger
6
+ from _logging.handlers import Logger
7
7
 
8
8
  logger = Logger.initialize()
@@ -25,7 +25,6 @@ def register_router(
25
25
  api_version: str = "/v1",
26
26
  module_base_prefix: str = "src",
27
27
  module_separator: str = ".",
28
- controller_file_pattern_template: str = "*_{}.py",
29
28
  ) -> APIRouter:
30
29
  """Register routers recursively from controller directories.
31
30
 
@@ -1,4 +1,7 @@
1
1
  # SPDX-License-Identifier: MIT
2
+ import base64
3
+ import hashlib
4
+ import secrets
2
5
  from collections.abc import Callable
3
6
  from datetime import datetime, timedelta
4
7
  from typing import Any
@@ -7,7 +10,6 @@ import jwt
7
10
  from fastapi import Depends, HTTPException, status
8
11
  from fastapi.security import OAuth2PasswordBearer
9
12
  from jwt.exceptions import InvalidTokenError
10
- from passlib.context import CryptContext
11
13
 
12
14
  from fastlib.config.manager import ConfigManager
13
15
  from fastlib.schema import CurrentUser
@@ -16,8 +18,6 @@ from fastlib.schema import CurrentUser
16
18
  security_config = ConfigManager.get_security_config()
17
19
  server_config = ConfigManager.get_server_config()
18
20
 
19
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
20
-
21
21
 
22
22
  def decode_jwt_token(token: str) -> dict[str, Any]:
23
23
  """Decode and validate JWT token.
@@ -50,6 +50,11 @@ def decode_jwt_token(token: str) -> dict[str, Any]:
50
50
 
51
51
 
52
52
  def get_oauth2_scheme() -> OAuth2PasswordBearer:
53
+ """Create OAuth2 password bearer scheme.
54
+
55
+ Returns:
56
+ OAuth2PasswordBearer instance
57
+ """
53
58
  oauth2_scheme = OAuth2PasswordBearer(
54
59
  tokenUrl=f"{server_config.api_prefix}/v1/auth/auth:signInWithEmailAndPassword"
55
60
  )
@@ -57,8 +62,7 @@ def get_oauth2_scheme() -> OAuth2PasswordBearer:
57
62
 
58
63
 
59
64
  def get_current_user() -> Callable[[], CurrentUser]:
60
- """
61
- Acquire current info through access_token
65
+ """Get current user from access token.
62
66
 
63
67
  Returns:
64
68
  CurrentUser instance
@@ -114,29 +118,86 @@ def create_token(
114
118
  )
115
119
 
116
120
 
117
- def verify_password(plain_password: str, hashed_password: str) -> bool:
118
- """Verify password against hashed version.
121
+ def generate_salt(length: int = 16) -> str:
122
+ """Generate a cryptographically secure random salt.
123
+
124
+ Args:
125
+ length: Length of the salt in bytes
126
+
127
+ Returns:
128
+ Base64 encoded salt string
129
+ """
130
+ return base64.b64encode(secrets.token_bytes(length)).decode("utf-8")
131
+
132
+
133
+ def hash_password_sha256(password: str, salt: str | None = None) -> tuple[str, str]:
134
+ """Hash password using SHA-256 with salt.
135
+
136
+ Args:
137
+ password: Plain text password to hash
138
+ salt: Salt for hashing. If None, generates random salt.
139
+
140
+ Returns:
141
+ Tuple of (hashed_password, salt)
142
+ """
143
+ if salt is None:
144
+ salt = generate_salt()
145
+
146
+ # Decode base64 salt
147
+ salt_bytes = base64.b64decode(salt.encode("utf-8"))
148
+
149
+ # Create hash with salt and password
150
+ hash_obj = hashlib.sha256()
151
+ hash_obj.update(salt_bytes + password.encode("utf-8"))
152
+
153
+ # Multiple iterations for increased security (optional)
154
+ for _ in range(1000): # 1000 iterations
155
+ hash_obj = hashlib.sha256(
156
+ hash_obj.digest() + salt_bytes + password.encode("utf-8")
157
+ )
158
+
159
+ hashed_password = base64.b64encode(hash_obj.digest()).decode("utf-8")
160
+ return hashed_password, salt
161
+
162
+
163
+ def verify_password(
164
+ plain_password: str, hashed_password: str, salt: str | None = None
165
+ ) -> bool:
166
+ """Verify password against hashed version using SHA-256.
119
167
 
120
168
  Args:
121
- plain_password: Input password
169
+ plain_password: Input password to verify
122
170
  hashed_password: Stored hashed password
171
+ salt: Salt used for the original hash. If None, uses a default salt.
123
172
 
124
173
  Returns:
125
174
  True if passwords match
126
175
  """
127
- return pwd_context.verify(plain_password, hashed_password)
176
+ try:
177
+ # Use default salt if none provided (for backward compatibility)
178
+ if salt is None:
179
+ # Default salt - in production, this should be stored with the hash
180
+ salt = "default_salt_placeholder"
181
+
182
+ # Recalculate hash with the same salt and algorithm
183
+ calculated_hash, _ = hash_password_sha256(plain_password, salt)
184
+
185
+ # Use timing-safe comparison function
186
+ return secrets.compare_digest(calculated_hash, hashed_password)
187
+ except Exception:
188
+ return False
128
189
 
129
190
 
130
- def get_password_hash(password: str) -> str:
131
- """Generate password hash.
191
+ def get_password_hash(password: str) -> tuple[str, str]:
192
+ """Generate password hash using SHA-256.
132
193
 
133
194
  Args:
134
195
  password: Plain text password
135
196
 
136
197
  Returns:
137
- Hashed password string
198
+ Tuple of (hashed_password, salt)
138
199
  """
139
- return pwd_context.hash(password)
200
+ return hash_password_sha256(password)
140
201
 
141
202
 
142
203
  def validate_token(token: str) -> bool:
@@ -177,3 +238,53 @@ def get_user_id(token: str) -> int:
177
238
  """
178
239
  payload = decode_jwt_token(token)
179
240
  return int(payload["sub"])
241
+
242
+
243
+ # Backward compatibility wrapper functions
244
+ def verify_password_compat(plain_password: str, hashed_password: str) -> bool:
245
+ """Compatibility version of password verification with default salt.
246
+
247
+ Args:
248
+ plain_password: Input password to verify
249
+ hashed_password: Stored hashed password
250
+
251
+ Returns:
252
+ True if passwords match
253
+ """
254
+ return verify_password(plain_password, hashed_password, salt=None)
255
+
256
+
257
+ def get_password_hash_compat(password: str) -> str:
258
+ """Compatibility version of password hash function.
259
+
260
+ Args:
261
+ password: Plain text password
262
+
263
+ Returns:
264
+ Formatted string containing hash and salt (hash:salt)
265
+ """
266
+ hashed_password, salt = get_password_hash(password)
267
+ return f"{hashed_password}:{salt}"
268
+
269
+
270
+ def parse_hashed_password(hashed_data: str) -> tuple[str, str]:
271
+ """Parse hashed password data in format 'hash:salt'.
272
+
273
+ Args:
274
+ hashed_data: String containing hash and salt separated by colon
275
+
276
+ Returns:
277
+ Tuple of (hashed_password, salt)
278
+
279
+ Raises:
280
+ ValueError: If format is invalid
281
+ """
282
+ if ":" not in hashed_data:
283
+ # If no salt is present, use default salt
284
+ return hashed_data, "default_salt_placeholder"
285
+
286
+ parts = hashed_data.split(":", 1)
287
+ if len(parts) != 2:
288
+ raise ValueError("Invalid hashed password format")
289
+
290
+ return parts[0], parts[1]
@@ -9,12 +9,11 @@ import asyncio
9
9
  from abc import ABC, abstractmethod
10
10
  from collections.abc import AsyncIterable
11
11
  from datetime import datetime
12
- from typing import Generic, TypeVar
12
+ from typing import Any, Generic, TypeVar
13
13
 
14
14
  from loguru import logger
15
15
  from pydantic import BaseModel
16
16
 
17
- from fastlib.cache.base import Cache
18
17
  from fastlib.cache.manager import get_cache_client
19
18
 
20
19
  from .schema import (
@@ -37,7 +36,7 @@ class StreamHandler(ABC, Generic[T]):
37
36
  self,
38
37
  message: T,
39
38
  source: AsyncIterable[V],
40
- storage: Cache | None,
39
+ storage: Any,
41
40
  ):
42
41
  self._message = message
43
42
  self._source = source
@@ -79,7 +78,7 @@ class AsyncStreamHandler(StreamHandler[T]):
79
78
  self,
80
79
  message: T,
81
80
  source: AsyncIterable[V],
82
- storage: Cache | None,
81
+ storage: Any,
83
82
  buffer_size: int = 100,
84
83
  ):
85
84
  super().__init__(message, source, storage)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.3.6
3
+ Version: 0.3.8
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>
@@ -21,13 +21,12 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: aiosqlite>=0.21
23
23
  Requires-Dist: alembic>=1.15.2
24
- Requires-Dist: cryptography>=45.0.6
24
+ Requires-Dist: fakeredis>=2.32
25
25
  Requires-Dist: fastapi[standard]>=0.115.12
26
26
  Requires-Dist: loguru>=0.7.3
27
27
  Requires-Dist: openpyxl>=3.1.5
28
28
  Requires-Dist: pandas>=2.2.3
29
- Requires-Dist: passlib[bcrypt]>=1.7.4
30
- Requires-Dist: pyjwt>=2.10.1
29
+ Requires-Dist: pyjwt[crypto]>=2.10.1
31
30
  Requires-Dist: sqlmodel>=0.0.24
32
31
  Dynamic: license-file
33
32
 
@@ -14,13 +14,13 @@ fastlib/request.py
14
14
  fastlib/response.py
15
15
  fastlib/router.py
16
16
  fastlib/schema.py
17
+ fastlib/security.py
17
18
  fastlib/_logging/__init__.py
18
19
  fastlib/_logging/config.py
19
20
  fastlib/_logging/handlers.py
20
21
  fastlib/cache/__init__.py
21
22
  fastlib/cache/_memony_cache.py
22
23
  fastlib/cache/_redis_cache.py
23
- fastlib/cache/base.py
24
24
  fastlib/cache/manager.py
25
25
  fastlib/config/__init__.py
26
26
  fastlib/config/_database_config.py
@@ -48,9 +48,6 @@ fastlib/middleware/__init__.py
48
48
  fastlib/middleware/db_session.py
49
49
  fastlib/middleware/i18n.py
50
50
  fastlib/middleware/jwt.py
51
- fastlib/security/__init__.py
52
- fastlib/security/security.py
53
- fastlib/security/utils.py
54
51
  fastlib/service/__init__.py
55
52
  fastlib/service/base_service.py
56
53
  fastlib/service/impl/__init__.py
@@ -66,6 +63,7 @@ fastlib/utils/excel_util.py
66
63
  fastlib/utils/file_util.py
67
64
  fastlib/utils/http_util.py
68
65
  fastlib/utils/ip_util.py
66
+ fastlib/utils/security_util.py
69
67
  fastlib/utils/snowflake_util.py
70
68
  fastlib/utils/str_util.py
71
69
  fastlib/utils/validate_util.py
@@ -1,10 +1,9 @@
1
1
  aiosqlite>=0.21
2
2
  alembic>=1.15.2
3
- cryptography>=45.0.6
3
+ fakeredis>=2.32
4
4
  fastapi[standard]>=0.115.12
5
5
  loguru>=0.7.3
6
6
  openpyxl>=3.1.5
7
7
  pandas>=2.2.3
8
- passlib[bcrypt]>=1.7.4
9
- pyjwt>=2.10.1
8
+ pyjwt[crypto]>=2.10.1
10
9
  sqlmodel>=0.0.24
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastlib-py"
3
- version = "0.3.6"
3
+ version = "0.3.8"
4
4
  description = "Fastlib, high performance, easy to use, fast to code, ready for production"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -31,13 +31,12 @@ classifiers = [
31
31
  dependencies = [
32
32
  "aiosqlite>=0.21",
33
33
  "alembic>=1.15.2",
34
- "cryptography>=45.0.6",
34
+ "fakeredis>=2.32",
35
35
  "fastapi[standard]>=0.115.12",
36
36
  "loguru>=0.7.3",
37
37
  "openpyxl>=3.1.5",
38
38
  "pandas>=2.2.3",
39
- "passlib[bcrypt]>=1.7.4",
40
- "pyjwt>=2.10.1",
39
+ "pyjwt[crypto]>=2.10.1",
41
40
  "sqlmodel>=0.0.24",
42
41
  ]
43
42
 
@@ -119,6 +118,6 @@ title = "FastLib Coverage Report"
119
118
  strict = true
120
119
 
121
120
  [[tool.uv.index]]
122
- name = "tsinghua"
123
- url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
121
+ name = "ustc"
122
+ url = "https://pypi.mirrors.ustc.edu.cn/simple/"
124
123
  default = true