fastlib-py 0.2.6__tar.gz → 0.2.7__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.6 → fastlib_py-0.2.7}/PKG-INFO +1 -1
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/loader.py +5 -1
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/manager.py +0 -3
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/utils.py +5 -1
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/security/utils.py +262 -116
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib_py.egg-info/PKG-INFO +1 -1
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/pyproject.toml +127 -126
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/LICENSE +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/README.md +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/cache/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/cache/base.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/cache/disk_cache.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/cache/manager.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/cache/redis_cache.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/base.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/database_config.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/registry.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/security_config.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/config/server_config.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/constant/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/constant/constant.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/contextvars.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/enums.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/exception/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/exception/base.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/exception/exception_handler.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/exception/exception_manager.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/i18n/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/i18n/manager.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/i18n/messages.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/i18n/types.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/i18n/utils.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/logging/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/logging/config.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/logging/handlers.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/mapper/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/mapper/base_mapper.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/mapper/impl/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/mapper/impl/base_mapper_impl.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/middleware/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/middleware/db_session.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/middleware/i18n.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/middleware/jwt.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/migrate.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/openapi.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/request.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/response.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/router.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/schema.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/security/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/security/security.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/service/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/service/base_service.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/service/impl/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/service/impl/base_service_impl.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/session/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/session/db_engine.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/utils/__init__.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/utils/excel_util.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/utils/file_util.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/utils/http_util.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/utils/ip_util.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/utils/snowflake_util.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib/utils/str_util.py +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib_py.egg-info/SOURCES.txt +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib_py.egg-info/dependency_links.txt +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib_py.egg-info/requires.txt +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/fastlib_py.egg-info/top_level.txt +0 -0
- {fastlib_py-0.2.6 → fastlib_py-0.2.7}/setup.cfg +0 -0
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
|
+
from loguru import logger
|
|
8
9
|
|
|
9
10
|
from fastlib import constant
|
|
10
11
|
from fastlib.config import utils as config_util
|
|
@@ -47,7 +48,10 @@ class ConfigLoader:
|
|
|
47
48
|
yaml.YAMLError: If there is an error parsing the YAML
|
|
48
49
|
"""
|
|
49
50
|
if not os.path.exists(file_path):
|
|
50
|
-
|
|
51
|
+
logger.warning(
|
|
52
|
+
f"Config file not found: {file_path}, will use the default config"
|
|
53
|
+
)
|
|
54
|
+
return {}
|
|
51
55
|
|
|
52
56
|
with open(file_path, encoding="utf-8") as file:
|
|
53
57
|
try:
|
|
@@ -123,9 +123,6 @@ class ConfigManager:
|
|
|
123
123
|
for config_name in ConfigRegistry.list_registered():
|
|
124
124
|
config_data = ConfigManager._global_config_dict.get(config_name, {})
|
|
125
125
|
|
|
126
|
-
if not config_data:
|
|
127
|
-
continue
|
|
128
|
-
|
|
129
126
|
try:
|
|
130
127
|
instance = ConfigRegistry.create_instance(
|
|
131
128
|
config_name, {config_name: config_data}
|
|
@@ -172,7 +172,11 @@ def get_alembic_db_info(
|
|
|
172
172
|
|
|
173
173
|
def get_db_url() -> str:
|
|
174
174
|
"""Get the database connection URL from alembic.ini configuration."""
|
|
175
|
-
|
|
175
|
+
try:
|
|
176
|
+
return get_alembic_db_info(file_util.get_file_path("alembic.ini")).full_url
|
|
177
|
+
except FileNotFoundError:
|
|
178
|
+
logger.warning("Can not found alembic.ini in the project, use default db url")
|
|
179
|
+
return "sqlite+aiosqlite:///./fastlib.db"
|
|
176
180
|
|
|
177
181
|
|
|
178
182
|
def get_sqlite_db_path() -> str:
|
|
@@ -10,7 +10,7 @@ from typing import Optional, Union
|
|
|
10
10
|
try:
|
|
11
11
|
from cryptography.fernet import Fernet
|
|
12
12
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
13
|
-
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa
|
|
14
14
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
15
15
|
|
|
16
16
|
HAS_CRYPTOGRAPHY = True
|
|
@@ -137,110 +137,6 @@ class SymmetricEncryption:
|
|
|
137
137
|
return decrypted.decode("utf-8")
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
class SimpleSymmetricEncryption:
|
|
141
|
-
"""Simple symmetric encryption using XOR and HMAC (when cryptography is not available)."""
|
|
142
|
-
|
|
143
|
-
def __init__(self, key: Optional[bytes] = None) -> None:
|
|
144
|
-
"""
|
|
145
|
-
Initialize simple symmetric encryption with a key.
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
key: Encryption key. If None, generates a new key.
|
|
149
|
-
"""
|
|
150
|
-
if key is None:
|
|
151
|
-
self._key = os.urandom(32)
|
|
152
|
-
else:
|
|
153
|
-
self._key = key[:32] if len(key) > 32 else key.ljust(32, b"\x00")
|
|
154
|
-
|
|
155
|
-
def get_key(self) -> bytes:
|
|
156
|
-
"""Get the encryption key."""
|
|
157
|
-
return self._key
|
|
158
|
-
|
|
159
|
-
def _xor_encrypt_decrypt(self, data: bytes) -> bytes:
|
|
160
|
-
"""XOR encryption/decryption."""
|
|
161
|
-
key_len = len(self._key)
|
|
162
|
-
return bytes(data[i] ^ self._key[i % key_len] for i in range(len(data)))
|
|
163
|
-
|
|
164
|
-
def encrypt(self, plaintext: Union[str, bytes]) -> bytes:
|
|
165
|
-
"""
|
|
166
|
-
Encrypt plaintext data using XOR + HMAC.
|
|
167
|
-
|
|
168
|
-
Args:
|
|
169
|
-
plaintext: Data to encrypt
|
|
170
|
-
|
|
171
|
-
Returns:
|
|
172
|
-
Encrypted data with HMAC
|
|
173
|
-
"""
|
|
174
|
-
if isinstance(plaintext, str):
|
|
175
|
-
plaintext = plaintext.encode("utf-8")
|
|
176
|
-
|
|
177
|
-
# Add random IV
|
|
178
|
-
iv = os.urandom(16)
|
|
179
|
-
|
|
180
|
-
# XOR encrypt
|
|
181
|
-
encrypted = self._xor_encrypt_decrypt(plaintext)
|
|
182
|
-
|
|
183
|
-
# Create HMAC for integrity
|
|
184
|
-
mac = hmac.new(self._key, iv + encrypted, hashlib.sha256).digest()
|
|
185
|
-
|
|
186
|
-
return iv + mac + encrypted
|
|
187
|
-
|
|
188
|
-
def decrypt(self, ciphertext: bytes) -> bytes:
|
|
189
|
-
"""
|
|
190
|
-
Decrypt ciphertext data.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
ciphertext: Encrypted data to decrypt
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
Decrypted data
|
|
197
|
-
|
|
198
|
-
Raises:
|
|
199
|
-
ValueError: If decryption fails or HMAC verification fails
|
|
200
|
-
"""
|
|
201
|
-
if len(ciphertext) < 48: # 16 (IV) + 32 (HMAC) = 48 minimum
|
|
202
|
-
raise ValueError("Invalid ciphertext length")
|
|
203
|
-
|
|
204
|
-
iv = ciphertext[:16]
|
|
205
|
-
mac = ciphertext[16:48]
|
|
206
|
-
encrypted = ciphertext[48:]
|
|
207
|
-
|
|
208
|
-
# Verify HMAC
|
|
209
|
-
expected_mac = hmac.new(self._key, iv + encrypted, hashlib.sha256).digest()
|
|
210
|
-
if not hmac.compare_digest(mac, expected_mac):
|
|
211
|
-
raise ValueError("HMAC verification failed")
|
|
212
|
-
|
|
213
|
-
# XOR decrypt
|
|
214
|
-
return self._xor_encrypt_decrypt(encrypted)
|
|
215
|
-
|
|
216
|
-
def encrypt_string(self, plaintext: str) -> str:
|
|
217
|
-
"""
|
|
218
|
-
Encrypt string and return base64 encoded result.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
plaintext: String to encrypt
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
Base64 encoded encrypted string
|
|
225
|
-
"""
|
|
226
|
-
encrypted = self.encrypt(plaintext)
|
|
227
|
-
return base64.b64encode(encrypted).decode("utf-8")
|
|
228
|
-
|
|
229
|
-
def decrypt_string(self, ciphertext: str) -> str:
|
|
230
|
-
"""
|
|
231
|
-
Decrypt base64 encoded string.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
ciphertext: Base64 encoded encrypted string
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
Decrypted string
|
|
238
|
-
"""
|
|
239
|
-
encrypted_bytes = base64.b64decode(ciphertext.encode("utf-8"))
|
|
240
|
-
decrypted = self.decrypt(encrypted_bytes)
|
|
241
|
-
return decrypted.decode("utf-8")
|
|
242
|
-
|
|
243
|
-
|
|
244
140
|
class HMACSigner:
|
|
245
141
|
"""HMAC-based message signing and verification."""
|
|
246
142
|
|
|
@@ -463,6 +359,248 @@ class RSASigner:
|
|
|
463
359
|
return False
|
|
464
360
|
|
|
465
361
|
|
|
362
|
+
class Ed25519Signer:
|
|
363
|
+
"""Ed25519 elliptic curve digital signing and verification."""
|
|
364
|
+
|
|
365
|
+
def __init__(
|
|
366
|
+
self, private_key: Optional[bytes] = None, public_key: Optional[bytes] = None
|
|
367
|
+
) -> None:
|
|
368
|
+
"""
|
|
369
|
+
Initialize Ed25519 signer.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
private_key: PEM encoded private key for signing
|
|
373
|
+
public_key: PEM encoded public key for verification
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ImportError: If cryptography package is not available
|
|
377
|
+
"""
|
|
378
|
+
if not HAS_CRYPTOGRAPHY:
|
|
379
|
+
raise ImportError(
|
|
380
|
+
"cryptography package is required for Ed25519Signer. Install it with: uv add cryptography"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
self._private_key = None
|
|
384
|
+
self._public_key = None
|
|
385
|
+
|
|
386
|
+
if private_key:
|
|
387
|
+
self._private_key = serialization.load_pem_private_key(
|
|
388
|
+
private_key, password=None
|
|
389
|
+
)
|
|
390
|
+
# Validate that it's an Ed25519 key
|
|
391
|
+
if not isinstance(self._private_key, ed25519.Ed25519PrivateKey):
|
|
392
|
+
raise ValueError("Private key is not an Ed25519 key")
|
|
393
|
+
|
|
394
|
+
if public_key:
|
|
395
|
+
self._public_key = serialization.load_pem_public_key(public_key)
|
|
396
|
+
# Validate that it's an Ed25519 key
|
|
397
|
+
if not isinstance(self._public_key, ed25519.Ed25519PublicKey):
|
|
398
|
+
raise ValueError("Public key is not an Ed25519 key")
|
|
399
|
+
|
|
400
|
+
@classmethod
|
|
401
|
+
def generate_keypair(cls) -> tuple[bytes, bytes]:
|
|
402
|
+
"""
|
|
403
|
+
Generate Ed25519 key pair.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Tuple of (private_key_pem, public_key_pem)
|
|
407
|
+
|
|
408
|
+
Raises:
|
|
409
|
+
ImportError: If cryptography package is not available
|
|
410
|
+
"""
|
|
411
|
+
if not HAS_CRYPTOGRAPHY:
|
|
412
|
+
raise ImportError(
|
|
413
|
+
"cryptography package is required for Ed25519Signer. Install it with: uv add cryptography"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Generate private key
|
|
417
|
+
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
418
|
+
|
|
419
|
+
# Serialize private key
|
|
420
|
+
private_pem = private_key.private_bytes(
|
|
421
|
+
encoding=serialization.Encoding.PEM,
|
|
422
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
423
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Serialize public key
|
|
427
|
+
public_pem = private_key.public_key().public_bytes(
|
|
428
|
+
encoding=serialization.Encoding.PEM,
|
|
429
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
return private_pem, public_pem
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def from_private_key_bytes(cls, private_key_bytes: bytes) -> "Ed25519Signer":
|
|
436
|
+
"""
|
|
437
|
+
Create Ed25519Signer from raw private key bytes.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
private_key_bytes: Raw 32-byte private key
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Ed25519Signer instance
|
|
444
|
+
"""
|
|
445
|
+
if not HAS_CRYPTOGRAPHY:
|
|
446
|
+
raise ImportError(
|
|
447
|
+
"cryptography package is required for Ed25519Signer. Install it with: uv add cryptography"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
|
|
451
|
+
private_pem = private_key.private_bytes(
|
|
452
|
+
encoding=serialization.Encoding.PEM,
|
|
453
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
454
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
455
|
+
)
|
|
456
|
+
return cls(private_key=private_pem)
|
|
457
|
+
|
|
458
|
+
def get_private_key_bytes(self) -> bytes:
|
|
459
|
+
"""
|
|
460
|
+
Get raw private key bytes (32 bytes).
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Raw private key bytes
|
|
464
|
+
|
|
465
|
+
Raises:
|
|
466
|
+
ValueError: If private key is not available
|
|
467
|
+
"""
|
|
468
|
+
if not self._private_key:
|
|
469
|
+
raise ValueError("Private key not available")
|
|
470
|
+
|
|
471
|
+
# Extract raw private key bytes from PEM
|
|
472
|
+
private_bytes = self._private_key.private_bytes(
|
|
473
|
+
encoding=serialization.Encoding.Raw,
|
|
474
|
+
format=serialization.PrivateFormat.Raw,
|
|
475
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
476
|
+
)
|
|
477
|
+
return private_bytes
|
|
478
|
+
|
|
479
|
+
def get_public_key_bytes(self) -> bytes:
|
|
480
|
+
"""
|
|
481
|
+
Get raw public key bytes (32 bytes).
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Raw public key bytes
|
|
485
|
+
|
|
486
|
+
Raises:
|
|
487
|
+
ValueError: If public key is not available
|
|
488
|
+
"""
|
|
489
|
+
if self._public_key:
|
|
490
|
+
public_key = self._public_key
|
|
491
|
+
elif self._private_key:
|
|
492
|
+
public_key = self._private_key.public_key()
|
|
493
|
+
else:
|
|
494
|
+
raise ValueError("No public key available")
|
|
495
|
+
|
|
496
|
+
public_bytes = public_key.public_bytes(
|
|
497
|
+
encoding=serialization.Encoding.Raw,
|
|
498
|
+
format=serialization.PublicFormat.Raw,
|
|
499
|
+
)
|
|
500
|
+
return public_bytes
|
|
501
|
+
|
|
502
|
+
def sign(self, message: Union[str, bytes]) -> str:
|
|
503
|
+
"""
|
|
504
|
+
Create Ed25519 signature for message.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
message: Message to sign
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Base64 encoded signature
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
ValueError: If private key is not available
|
|
514
|
+
"""
|
|
515
|
+
if not self._private_key:
|
|
516
|
+
raise ValueError("Private key required for signing")
|
|
517
|
+
|
|
518
|
+
if isinstance(message, str):
|
|
519
|
+
message = message.encode("utf-8")
|
|
520
|
+
|
|
521
|
+
# Ed25519 signing is straightforward, no padding needed
|
|
522
|
+
signature = self._private_key.sign(message)
|
|
523
|
+
|
|
524
|
+
return base64.b64encode(signature).decode("utf-8")
|
|
525
|
+
|
|
526
|
+
def verify(self, message: Union[str, bytes], signature: str) -> bool:
|
|
527
|
+
"""
|
|
528
|
+
Verify Ed25519 signature.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
message: Original message
|
|
532
|
+
signature: Base64 encoded signature to verify
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
True if signature is valid, False otherwise
|
|
536
|
+
"""
|
|
537
|
+
public_key = self._public_key
|
|
538
|
+
if not public_key and self._private_key:
|
|
539
|
+
public_key = self._private_key.public_key()
|
|
540
|
+
|
|
541
|
+
if not public_key:
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
if isinstance(message, str):
|
|
546
|
+
message = message.encode("utf-8")
|
|
547
|
+
|
|
548
|
+
signature_bytes = base64.b64decode(signature.encode("utf-8"))
|
|
549
|
+
|
|
550
|
+
public_key.verify(signature_bytes, message)
|
|
551
|
+
return True
|
|
552
|
+
except Exception:
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
def sign_detached(self, message: Union[str, bytes]) -> bytes:
|
|
556
|
+
"""
|
|
557
|
+
Create detached Ed25519 signature (raw bytes).
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
message: Message to sign
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Raw signature bytes (64 bytes)
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
ValueError: If private key is not available
|
|
567
|
+
"""
|
|
568
|
+
if not self._private_key:
|
|
569
|
+
raise ValueError("Private key required for signing")
|
|
570
|
+
|
|
571
|
+
if isinstance(message, str):
|
|
572
|
+
message = message.encode("utf-8")
|
|
573
|
+
|
|
574
|
+
return self._private_key.sign(message)
|
|
575
|
+
|
|
576
|
+
def verify_detached(self, message: Union[str, bytes], signature: bytes) -> bool:
|
|
577
|
+
"""
|
|
578
|
+
Verify detached Ed25519 signature.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
message: Original message
|
|
582
|
+
signature: Raw signature bytes to verify
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
True if signature is valid, False otherwise
|
|
586
|
+
"""
|
|
587
|
+
public_key = self._public_key
|
|
588
|
+
if not public_key and self._private_key:
|
|
589
|
+
public_key = self._private_key.public_key()
|
|
590
|
+
|
|
591
|
+
if not public_key:
|
|
592
|
+
return False
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
if isinstance(message, str):
|
|
596
|
+
message = message.encode("utf-8")
|
|
597
|
+
|
|
598
|
+
public_key.verify(signature, message)
|
|
599
|
+
return True
|
|
600
|
+
except Exception:
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
|
|
466
604
|
def generate_random_key(length: int = 32) -> str:
|
|
467
605
|
"""
|
|
468
606
|
Generate a random key for symmetric encryption.
|
|
@@ -534,29 +672,31 @@ def verify_password(password: str, hashed_password: str, salt: str) -> bool:
|
|
|
534
672
|
return False
|
|
535
673
|
|
|
536
674
|
|
|
537
|
-
def create_symmetric_encryption(
|
|
538
|
-
key: Optional[bytes] = None,
|
|
539
|
-
) -> Union[SymmetricEncryption, SimpleSymmetricEncryption]:
|
|
675
|
+
def create_symmetric_encryption(key: Optional[bytes] = None) -> SymmetricEncryption:
|
|
540
676
|
"""
|
|
541
|
-
Create a symmetric encryption instance
|
|
677
|
+
Create a symmetric encryption instance.
|
|
542
678
|
|
|
543
679
|
Args:
|
|
544
680
|
key: Encryption key. If None, generates a new key.
|
|
545
681
|
|
|
546
682
|
Returns:
|
|
547
|
-
SymmetricEncryption
|
|
683
|
+
SymmetricEncryption instance
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
ImportError: If cryptography package is not available
|
|
548
687
|
"""
|
|
549
|
-
if HAS_CRYPTOGRAPHY:
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
688
|
+
if not HAS_CRYPTOGRAPHY:
|
|
689
|
+
raise ImportError(
|
|
690
|
+
"cryptography package is required for encryption. Install it with: uv add cryptography"
|
|
691
|
+
)
|
|
692
|
+
return SymmetricEncryption(key)
|
|
553
693
|
|
|
554
694
|
|
|
555
695
|
def encrypt_data(
|
|
556
696
|
data: Union[str, bytes], key: Optional[bytes] = None
|
|
557
697
|
) -> tuple[bytes, bytes]:
|
|
558
698
|
"""
|
|
559
|
-
Encrypt data using
|
|
699
|
+
Encrypt data using symmetric encryption.
|
|
560
700
|
|
|
561
701
|
Args:
|
|
562
702
|
data: Data to encrypt
|
|
@@ -564,6 +704,9 @@ def encrypt_data(
|
|
|
564
704
|
|
|
565
705
|
Returns:
|
|
566
706
|
Tuple of (encrypted_data, key_used)
|
|
707
|
+
|
|
708
|
+
Raises:
|
|
709
|
+
ImportError: If cryptography package is not available
|
|
567
710
|
"""
|
|
568
711
|
encryptor = create_symmetric_encryption(key)
|
|
569
712
|
encrypted = encryptor.encrypt(data)
|
|
@@ -572,7 +715,7 @@ def encrypt_data(
|
|
|
572
715
|
|
|
573
716
|
def decrypt_data(encrypted_data: bytes, key: bytes) -> bytes:
|
|
574
717
|
"""
|
|
575
|
-
Decrypt data using
|
|
718
|
+
Decrypt data using symmetric encryption.
|
|
576
719
|
|
|
577
720
|
Args:
|
|
578
721
|
encrypted_data: Encrypted data
|
|
@@ -580,6 +723,9 @@ def decrypt_data(encrypted_data: bytes, key: bytes) -> bytes:
|
|
|
580
723
|
|
|
581
724
|
Returns:
|
|
582
725
|
Decrypted data
|
|
726
|
+
|
|
727
|
+
Raises:
|
|
728
|
+
ImportError: If cryptography package is not available
|
|
583
729
|
"""
|
|
584
730
|
encryptor = create_symmetric_encryption(key)
|
|
585
731
|
return encryptor.decrypt(encrypted_data)
|
|
@@ -1,126 +1,127 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "fastlib-py"
|
|
3
|
-
version = "0.2.
|
|
4
|
-
description = "Fastlib, high performance, easy to use, fast to code, ready for production"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
keywords = [
|
|
7
|
-
"fastapi",
|
|
8
|
-
"library",
|
|
9
|
-
"sqlmodel",
|
|
10
|
-
"web",
|
|
11
|
-
]
|
|
12
|
-
license = "MIT"
|
|
13
|
-
maintainers = [
|
|
14
|
-
{ name = "tyvekZhang", email = "tyvekzhang@gmail.com" },
|
|
15
|
-
]
|
|
16
|
-
authors = [
|
|
17
|
-
{ name = "tyvekZhang", email = "tyvekzhang@gmail.com" },
|
|
18
|
-
]
|
|
19
|
-
requires-python = ">=3.9"
|
|
20
|
-
classifiers = [
|
|
21
|
-
"Development Status :: 4 - Beta",
|
|
22
|
-
"Programming Language :: Python",
|
|
23
|
-
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
-
"Programming Language :: Python :: 3.9",
|
|
25
|
-
"Programming Language :: Python :: 3.10",
|
|
26
|
-
"Programming Language :: Python :: 3.11",
|
|
27
|
-
"Programming Language :: Python :: 3.12",
|
|
28
|
-
"Programming Language :: Python :: 3.13",
|
|
29
|
-
"Programming Language :: Python :: Implementation :: CPython",
|
|
30
|
-
"Programming Language :: Python :: Implementation :: PyPy",
|
|
31
|
-
]
|
|
32
|
-
dependencies = [
|
|
33
|
-
"aiosqlite>=0.21",
|
|
34
|
-
"alembic>=1.15.2",
|
|
35
|
-
"asgi-correlation-id>=4.3.4",
|
|
36
|
-
"cryptography>=45.0.6",
|
|
37
|
-
"diskcache>=5.6.3",
|
|
38
|
-
"fastapi[standard]>=0.115.12",
|
|
39
|
-
"loguru>=0.7.3",
|
|
40
|
-
"openpyxl>=3.1.5",
|
|
41
|
-
"pandas>=2.2.3",
|
|
42
|
-
"passlib[bcrypt]>=1.7.4",
|
|
43
|
-
"pyjwt>=2.10.1",
|
|
44
|
-
"sqlmodel>=0.0.24",
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
urls.Homepage = "https://github.com/tyvekzhang/fastlib"
|
|
48
|
-
|
|
49
|
-
[dependency-groups]
|
|
50
|
-
dev = [
|
|
51
|
-
"coverage>=7.8",
|
|
52
|
-
"httpx>=0.28.1",
|
|
53
|
-
"pre-commit>=4.2",
|
|
54
|
-
"pytest>=8.3.5",
|
|
55
|
-
"pytest-asyncio>=1.1",
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
docs = [
|
|
59
|
-
"sphinx>=7.2.6",
|
|
60
|
-
"sphinx-rtd-theme>=2",
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
[tool.ruff]
|
|
64
|
-
line-length = 88
|
|
65
|
-
|
|
66
|
-
include = [ "fastlib/**/*.py", "main.py" ]
|
|
67
|
-
exclude = [ "doc/**/*.py" ]
|
|
68
|
-
|
|
69
|
-
lint.select = [
|
|
70
|
-
"B", # flake8-bugbear
|
|
71
|
-
"C4", # flake8-comprehensions
|
|
72
|
-
"E", # pycodestyle errors
|
|
73
|
-
"F", # pyflakes
|
|
74
|
-
"I", # isort
|
|
75
|
-
"UP", # pyupgrade
|
|
76
|
-
"W", # pycodestyle warnings
|
|
77
|
-
]
|
|
78
|
-
lint.ignore = [
|
|
79
|
-
"B008", # do not perform function calls in argument defaults
|
|
80
|
-
"C901", # too complex
|
|
81
|
-
"E501", # line too long, handled by black
|
|
82
|
-
"W191", # indentation contains tabs
|
|
83
|
-
]
|
|
84
|
-
lint.isort.known-third-party = [
|
|
85
|
-
"aiosqlite",
|
|
86
|
-
"alembic",
|
|
87
|
-
"diskcache",
|
|
88
|
-
"fastapi",
|
|
89
|
-
"loguru",
|
|
90
|
-
"openpyxl",
|
|
91
|
-
"pandas",
|
|
92
|
-
"passlib",
|
|
93
|
-
"pyjwt",
|
|
94
|
-
"sqlmodel",
|
|
95
|
-
"starlette",
|
|
96
|
-
"pydantic",
|
|
97
|
-
"bcrypt",
|
|
98
|
-
"cryptography",
|
|
99
|
-
]
|
|
100
|
-
# Preserve types, even if a file imports `from __future__ import annotations`.
|
|
101
|
-
lint.pyupgrade.keep-runtime-typing = true
|
|
102
|
-
|
|
103
|
-
[tool.coverage.run]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastlib-py"
|
|
3
|
+
version = "0.2.7"
|
|
4
|
+
description = "Fastlib, high performance, easy to use, fast to code, ready for production"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
keywords = [
|
|
7
|
+
"fastapi",
|
|
8
|
+
"library",
|
|
9
|
+
"sqlmodel",
|
|
10
|
+
"web",
|
|
11
|
+
]
|
|
12
|
+
license = "MIT"
|
|
13
|
+
maintainers = [
|
|
14
|
+
{ name = "tyvekZhang", email = "tyvekzhang@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
authors = [
|
|
17
|
+
{ name = "tyvekZhang", email = "tyvekzhang@gmail.com" },
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.9"
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 4 - Beta",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
30
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"aiosqlite>=0.21",
|
|
34
|
+
"alembic>=1.15.2",
|
|
35
|
+
"asgi-correlation-id>=4.3.4",
|
|
36
|
+
"cryptography>=45.0.6",
|
|
37
|
+
"diskcache>=5.6.3",
|
|
38
|
+
"fastapi[standard]>=0.115.12",
|
|
39
|
+
"loguru>=0.7.3",
|
|
40
|
+
"openpyxl>=3.1.5",
|
|
41
|
+
"pandas>=2.2.3",
|
|
42
|
+
"passlib[bcrypt]>=1.7.4",
|
|
43
|
+
"pyjwt>=2.10.1",
|
|
44
|
+
"sqlmodel>=0.0.24",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
urls.Homepage = "https://github.com/tyvekzhang/fastlib"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"coverage>=7.8",
|
|
52
|
+
"httpx>=0.28.1",
|
|
53
|
+
"pre-commit>=4.2",
|
|
54
|
+
"pytest>=8.3.5",
|
|
55
|
+
"pytest-asyncio>=1.1",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
docs = [
|
|
59
|
+
"sphinx>=7.2.6",
|
|
60
|
+
"sphinx-rtd-theme>=2",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 88
|
|
65
|
+
|
|
66
|
+
include = [ "fastlib/**/*.py", "main.py" ]
|
|
67
|
+
exclude = [ "doc/**/*.py" ]
|
|
68
|
+
|
|
69
|
+
lint.select = [
|
|
70
|
+
"B", # flake8-bugbear
|
|
71
|
+
"C4", # flake8-comprehensions
|
|
72
|
+
"E", # pycodestyle errors
|
|
73
|
+
"F", # pyflakes
|
|
74
|
+
"I", # isort
|
|
75
|
+
"UP", # pyupgrade
|
|
76
|
+
"W", # pycodestyle warnings
|
|
77
|
+
]
|
|
78
|
+
lint.ignore = [
|
|
79
|
+
"B008", # do not perform function calls in argument defaults
|
|
80
|
+
"C901", # too complex
|
|
81
|
+
"E501", # line too long, handled by black
|
|
82
|
+
"W191", # indentation contains tabs
|
|
83
|
+
]
|
|
84
|
+
lint.isort.known-third-party = [
|
|
85
|
+
"aiosqlite",
|
|
86
|
+
"alembic",
|
|
87
|
+
"diskcache",
|
|
88
|
+
"fastapi",
|
|
89
|
+
"loguru",
|
|
90
|
+
"openpyxl",
|
|
91
|
+
"pandas",
|
|
92
|
+
"passlib",
|
|
93
|
+
"pyjwt",
|
|
94
|
+
"sqlmodel",
|
|
95
|
+
"starlette",
|
|
96
|
+
"pydantic",
|
|
97
|
+
"bcrypt",
|
|
98
|
+
"cryptography",
|
|
99
|
+
]
|
|
100
|
+
# Preserve types, even if a file imports `from __future__ import annotations`.
|
|
101
|
+
lint.pyupgrade.keep-runtime-typing = true
|
|
102
|
+
|
|
103
|
+
[tool.coverage.run]
|
|
104
|
+
data_file = "coverage/.coverage"
|
|
105
|
+
source = [ "fastlib" ]
|
|
106
|
+
context = '${CONTEXT}'
|
|
107
|
+
dynamic_context = "test_function"
|
|
108
|
+
omit = [
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[tool.coverage.report]
|
|
112
|
+
fail_under = 90
|
|
113
|
+
show_missing = true
|
|
114
|
+
sort = "-Cover"
|
|
115
|
+
|
|
116
|
+
[tool.coverage.html]
|
|
117
|
+
show_contexts = true
|
|
118
|
+
directory = "coverage/html"
|
|
119
|
+
title = "FastLib Coverage Report"
|
|
120
|
+
|
|
121
|
+
[tool.mypy]
|
|
122
|
+
strict = true
|
|
123
|
+
|
|
124
|
+
[[tool.uv.index]]
|
|
125
|
+
name = "tsinghua"
|
|
126
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
|
|
127
|
+
default = true
|
|
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
|