fastlib-py 0.2.6__tar.gz → 0.2.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.2.6 → fastlib_py-0.2.8}/PKG-INFO +1 -1
  2. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/loader.py +5 -1
  3. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/manager.py +0 -3
  4. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/utils.py +5 -1
  5. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/mapper/impl/base_mapper_impl.py +1 -1
  6. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/security/utils.py +262 -116
  7. fastlib_py-0.2.8/fastlib/service/base_service.py +187 -0
  8. fastlib_py-0.2.8/fastlib/service/impl/__init__.py +0 -0
  9. fastlib_py-0.2.8/fastlib/service/impl/base_service_impl.py +304 -0
  10. fastlib_py-0.2.8/fastlib/session/__init__.py +6 -0
  11. fastlib_py-0.2.8/fastlib/stream/__init__.py +1 -0
  12. fastlib_py-0.2.8/fastlib/stream/enums.py +36 -0
  13. fastlib_py-0.2.8/fastlib/stream/event.py +101 -0
  14. fastlib_py-0.2.8/fastlib/stream/handler.py +187 -0
  15. fastlib_py-0.2.8/fastlib/stream/schema.py +140 -0
  16. fastlib_py-0.2.8/fastlib/stream/sse.py +272 -0
  17. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib_py.egg-info/PKG-INFO +1 -1
  18. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib_py.egg-info/SOURCES.txt +7 -0
  19. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/pyproject.toml +127 -126
  20. fastlib_py-0.2.6/fastlib/service/base_service.py +0 -75
  21. fastlib_py-0.2.6/fastlib/service/impl/base_service_impl.py +0 -79
  22. fastlib_py-0.2.6/fastlib/session/__init__.py +0 -5
  23. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/LICENSE +0 -0
  24. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/README.md +0 -0
  25. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/__init__.py +0 -0
  26. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/cache/__init__.py +0 -0
  27. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/cache/base.py +0 -0
  28. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/cache/disk_cache.py +0 -0
  29. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/cache/manager.py +0 -0
  30. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/cache/redis_cache.py +0 -0
  31. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/__init__.py +0 -0
  32. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/base.py +0 -0
  33. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/database_config.py +0 -0
  34. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/registry.py +0 -0
  35. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/security_config.py +0 -0
  36. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/config/server_config.py +0 -0
  37. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/constant/__init__.py +0 -0
  38. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/constant/constant.py +0 -0
  39. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/contextvars.py +0 -0
  40. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/enums.py +0 -0
  41. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/exception/__init__.py +0 -0
  42. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/exception/base.py +0 -0
  43. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/exception/exception_handler.py +0 -0
  44. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/exception/exception_manager.py +0 -0
  45. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/i18n/__init__.py +0 -0
  46. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/i18n/manager.py +0 -0
  47. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/i18n/messages.py +0 -0
  48. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/i18n/types.py +0 -0
  49. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/i18n/utils.py +0 -0
  50. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/logging/__init__.py +0 -0
  51. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/logging/config.py +0 -0
  52. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/logging/handlers.py +0 -0
  53. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/mapper/__init__.py +0 -0
  54. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/mapper/base_mapper.py +0 -0
  55. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/mapper/impl/__init__.py +0 -0
  56. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/middleware/__init__.py +0 -0
  57. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/middleware/db_session.py +0 -0
  58. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/middleware/i18n.py +0 -0
  59. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/middleware/jwt.py +0 -0
  60. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/migrate.py +0 -0
  61. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/openapi.py +0 -0
  62. /fastlib_py-0.2.6/fastlib/service/__init__.py → /fastlib_py-0.2.8/fastlib/py.typed +0 -0
  63. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/request.py +0 -0
  64. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/response.py +0 -0
  65. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/router.py +0 -0
  66. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/schema.py +0 -0
  67. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/security/__init__.py +0 -0
  68. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/security/security.py +0 -0
  69. {fastlib_py-0.2.6/fastlib/service/impl → fastlib_py-0.2.8/fastlib/service}/__init__.py +0 -0
  70. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/session/db_engine.py +0 -0
  71. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/utils/__init__.py +0 -0
  72. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/utils/excel_util.py +0 -0
  73. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/utils/file_util.py +0 -0
  74. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/utils/http_util.py +0 -0
  75. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/utils/ip_util.py +0 -0
  76. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/utils/snowflake_util.py +0 -0
  77. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib/utils/str_util.py +0 -0
  78. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib_py.egg-info/dependency_links.txt +0 -0
  79. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib_py.egg-info/requires.txt +0 -0
  80. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/fastlib_py.egg-info/top_level.txt +0 -0
  81. {fastlib_py-0.2.6 → fastlib_py-0.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastlib-py
3
- Version: 0.2.6
3
+ Version: 0.2.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>
@@ -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
- raise FileNotFoundError(f"Config file not found: {file_path}")
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
- return get_alembic_db_info(file_util.get_file_path("alembic.ini")).full_url
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:
@@ -550,7 +550,7 @@ class SqlModelMapper(BaseMapper, Generic[ModelType]):
550
550
 
551
551
  # Query children for the current level
552
552
  stmt = select(self.model).where(self.model.parent_id.in_(parent_ids))
553
- result = await db_session.execute(stmt)
553
+ result = await db_session.exec(stmt)
554
554
  children = result.scalars().all()
555
555
 
556
556
  # Convert ORM children to SchemaType instances
@@ -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 using the best available method.
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 if cryptography is available, SimpleSymmetricEncryption otherwise
683
+ SymmetricEncryption instance
684
+
685
+ Raises:
686
+ ImportError: If cryptography package is not available
548
687
  """
549
- if HAS_CRYPTOGRAPHY:
550
- return SymmetricEncryption(key)
551
- else:
552
- return SimpleSymmetricEncryption(key)
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 the best available symmetric encryption.
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 the best available symmetric encryption.
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)
@@ -0,0 +1,187 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Abstract service with common database operations."""
3
+
4
+ from abc import ABC, abstractmethod
5
+ from typing import Any, Generic, Optional, TypeVar
6
+
7
+ from pydantic import BaseModel
8
+ from sqlmodel import SQLModel
9
+ from sqlmodel.ext.asyncio.session import AsyncSession
10
+
11
+ from fastlib.schema import SortItem
12
+
13
+ IDType = TypeVar("IDType", int, str)
14
+ ModelType = TypeVar("ModelType", bound=SQLModel)
15
+
16
+
17
+ class BaseService(Generic[ModelType], ABC):
18
+ """Base service interface with common CRUD operations"""
19
+
20
+ @abstractmethod
21
+ async def save(
22
+ self, data: BaseModel, db_session: Optional[AsyncSession] = None
23
+ ) -> BaseModel:
24
+ """Insert a single record"""
25
+ pass
26
+
27
+ @abstractmethod
28
+ async def save_batch(
29
+ self, data_list: list[BaseModel], db_session: Optional[AsyncSession] = None
30
+ ) -> int:
31
+ """Batch insert records"""
32
+ pass
33
+
34
+ @abstractmethod
35
+ async def save_or_update(
36
+ self, data: BaseModel, db_session: Optional[AsyncSession] = None
37
+ ) -> bool:
38
+ """Insert or update a record"""
39
+ pass
40
+
41
+ @abstractmethod
42
+ async def save_or_update_batch(
43
+ self, data_list: list[BaseModel], db_session: Optional[AsyncSession] = None
44
+ ) -> bool:
45
+ """Batch insert or update records"""
46
+ pass
47
+
48
+ @abstractmethod
49
+ async def remove_by_id(
50
+ self, id: IDType, db_session: Optional[AsyncSession] = None
51
+ ) -> bool:
52
+ """Delete record by ID"""
53
+ pass
54
+
55
+ @abstractmethod
56
+ async def remove_by_ids(
57
+ self, ids: list[IDType], db_session: Optional[AsyncSession] = None
58
+ ) -> bool:
59
+ """Batch delete records by IDs"""
60
+ pass
61
+
62
+ @abstractmethod
63
+ async def remove(self, db_session: Optional[AsyncSession] = None, **kwargs) -> bool:
64
+ """Delete records by conditions"""
65
+ pass
66
+
67
+ @abstractmethod
68
+ async def update_by_id(
69
+ self, data: BaseModel, db_session: Optional[AsyncSession] = None
70
+ ) -> bool:
71
+ """Update record by ID"""
72
+ pass
73
+
74
+ @abstractmethod
75
+ async def update_batch_by_ids(
76
+ self,
77
+ ids: list[IDType],
78
+ data: dict[str, Any],
79
+ db_session: Optional[AsyncSession] = None,
80
+ ) -> bool:
81
+ """Batch update records by IDs"""
82
+ pass
83
+
84
+ @abstractmethod
85
+ async def batch_update(
86
+ self, data_list: list[BaseModel], db_session: Optional[AsyncSession] = None
87
+ ) -> bool:
88
+ """Batch update records"""
89
+ pass
90
+
91
+ @abstractmethod
92
+ async def update(
93
+ self, data: BaseModel, db_session: Optional[AsyncSession] = None, **kwargs
94
+ ) -> bool:
95
+ """Update records by conditions"""
96
+ pass
97
+
98
+ @abstractmethod
99
+ async def get_by_id(
100
+ self,
101
+ id: IDType,
102
+ fields: Optional[list[str]] = None,
103
+ db_session: Optional[AsyncSession] = None,
104
+ ) -> Optional[BaseModel]:
105
+ """Get record by ID"""
106
+ pass
107
+
108
+ @abstractmethod
109
+ async def get_one(
110
+ self, db_session: Optional[AsyncSession] = None, **kwargs
111
+ ) -> Optional[BaseModel]:
112
+ """Get one record by conditions"""
113
+ pass
114
+
115
+ @abstractmethod
116
+ async def list_by_ids(
117
+ self,
118
+ ids: list[IDType],
119
+ fields: Optional[list[str]] = None,
120
+ db_session: Optional[AsyncSession] = None,
121
+ ) -> list[BaseModel]:
122
+ """Get records by IDs"""
123
+ pass
124
+
125
+ @abstractmethod
126
+ async def list_all(
127
+ self,
128
+ fields: Optional[list[str]] = None,
129
+ db_session: Optional[AsyncSession] = None,
130
+ **kwargs,
131
+ ) -> list[BaseModel]:
132
+ """Get all records with optional filtering"""
133
+ pass
134
+
135
+ @abstractmethod
136
+ async def list(
137
+ self, db_session: Optional[AsyncSession] = None, **kwargs
138
+ ) -> list[BaseModel]:
139
+ """Get records by conditions"""
140
+ pass
141
+
142
+ @abstractmethod
143
+ async def page(
144
+ self,
145
+ current: int = 1,
146
+ page_size: int = 100,
147
+ count: bool = True,
148
+ fields: Optional[list[str]] = None,
149
+ db_session: Optional[AsyncSession] = None,
150
+ **kwargs,
151
+ ) -> tuple[list[BaseModel], int]:
152
+ """Paginated query"""
153
+ pass
154
+
155
+ @abstractmethod
156
+ async def page_ordered(
157
+ self,
158
+ current: int = 1,
159
+ page_size: int = 100,
160
+ count: bool = True,
161
+ sort_list: Optional[list[SortItem]] = None,
162
+ fields: Optional[list[str]] = None,
163
+ db_session: Optional[AsyncSession] = None,
164
+ **kwargs,
165
+ ) -> tuple[list[BaseModel], int]:
166
+ """Paginated query with ordering"""
167
+ pass
168
+
169
+ @abstractmethod
170
+ async def count(self, db_session: Optional[AsyncSession] = None, **kwargs) -> int:
171
+ """Count records by conditions"""
172
+ pass
173
+
174
+ @abstractmethod
175
+ async def exists(self, db_session: Optional[AsyncSession] = None, **kwargs) -> bool:
176
+ """Check if records exist by conditions"""
177
+ pass
178
+
179
+ @abstractmethod
180
+ async def get_children_tree(
181
+ self,
182
+ parent_data: list[BaseModel],
183
+ max_level: int = 5,
184
+ db_session: Optional[AsyncSession] = None,
185
+ ) -> list[BaseModel]:
186
+ """Get children tree recursively"""
187
+ pass
File without changes