nlbone 0.6.0__tar.gz → 0.6.9__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 (95) hide show
  1. {nlbone-0.6.0 → nlbone-0.6.9}/PKG-INFO +1 -1
  2. {nlbone-0.6.0 → nlbone-0.6.9}/pyproject.toml +2 -2
  3. nlbone-0.6.9/src/nlbone/adapters/__init__.py +1 -0
  4. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/auth/keycloak.py +1 -1
  5. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/auth/token_provider.py +1 -1
  6. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/cache/async_redis.py +18 -8
  7. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/cache/memory.py +21 -11
  8. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/cache/pubsub_listener.py +3 -0
  9. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/cache/redis.py +23 -8
  10. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/__init__.py +0 -1
  11. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/audit.py +14 -11
  12. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/query_builder.py +103 -86
  13. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/redis/client.py +1 -4
  14. nlbone-0.6.9/src/nlbone/adapters/http_clients/__init__.py +2 -0
  15. nlbone-0.6.9/src/nlbone/adapters/http_clients/pricing/__init__.py +1 -0
  16. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/http_clients/pricing/pricing_service.py +40 -20
  17. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/http_clients/uploadchi/__init__.py +1 -1
  18. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/http_clients/uploadchi/uploadchi.py +12 -12
  19. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +14 -15
  20. nlbone-0.6.9/src/nlbone/adapters/percolation/__init__.py +1 -0
  21. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/percolation/connection.py +2 -1
  22. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/config/logging.py +54 -24
  23. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/container.py +14 -9
  24. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/application/base_worker.py +1 -1
  25. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/domain/models.py +4 -2
  26. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/cache.py +25 -9
  27. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/dependencies/auth.py +26 -0
  28. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/pagination/offset_base.py +14 -12
  29. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/cli/init_db.py +1 -1
  30. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/cli/main.py +6 -5
  31. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/utils/cache.py +10 -0
  32. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/utils/cache_keys.py +6 -0
  33. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/utils/cache_registry.py +5 -2
  34. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/utils/http.py +1 -1
  35. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/utils/redactor.py +2 -1
  36. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/utils/time.py +1 -1
  37. nlbone-0.6.0/src/nlbone/adapters/http_clients/__init__.py +0 -2
  38. nlbone-0.6.0/src/nlbone/adapters/http_clients/pricing/__init__.py +0 -1
  39. nlbone-0.6.0/src/nlbone/adapters/percolation/__init__.py +0 -1
  40. nlbone-0.6.0/src/nlbone/utils/__init__.py +0 -0
  41. {nlbone-0.6.0 → nlbone-0.6.9}/.gitignore +0 -0
  42. {nlbone-0.6.0 → nlbone-0.6.9}/LICENSE +0 -0
  43. {nlbone-0.6.0 → nlbone-0.6.9}/README.md +0 -0
  44. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/__init__.py +0 -0
  45. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/auth/__init__.py +0 -0
  46. {nlbone-0.6.0/src/nlbone/adapters → nlbone-0.6.9/src/nlbone/adapters/cache}/__init__.py +0 -0
  47. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
  48. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/base.py +0 -0
  49. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/engine.py +0 -0
  50. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/repository.py +0 -0
  51. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/schema.py +0 -0
  52. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/db/postgres/uow.py +0 -0
  53. {nlbone-0.6.0/src/nlbone/adapters/cache → nlbone-0.6.9/src/nlbone/adapters/db/redis}/__init__.py +0 -0
  54. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/messaging/__init__.py +0 -0
  55. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/messaging/event_bus.py +0 -0
  56. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/adapters/messaging/redis.py +0 -0
  57. {nlbone-0.6.0/src/nlbone/adapters/db/redis → nlbone-0.6.9/src/nlbone/config}/__init__.py +0 -0
  58. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/config/settings.py +0 -0
  59. {nlbone-0.6.0/src/nlbone/config → nlbone-0.6.9/src/nlbone/core}/__init__.py +0 -0
  60. {nlbone-0.6.0/src/nlbone/core → nlbone-0.6.9/src/nlbone/core/application}/__init__.py +0 -0
  61. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/application/events.py +0 -0
  62. {nlbone-0.6.0/src/nlbone/core/application → nlbone-0.6.9/src/nlbone/core/application/services}/__init__.py +0 -0
  63. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/application/services.py +0 -0
  64. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/application/use_case.py +0 -0
  65. {nlbone-0.6.0/src/nlbone/core/application/services → nlbone-0.6.9/src/nlbone/core/domain}/__init__.py +0 -0
  66. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/domain/base.py +0 -0
  67. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/domain/events.py +0 -0
  68. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/__init__.py +0 -0
  69. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/auth.py +0 -0
  70. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/event_bus.py +0 -0
  71. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/files.py +0 -0
  72. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/messaging.py +0 -0
  73. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/repo.py +0 -0
  74. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/core/ports/uow.py +0 -0
  75. {nlbone-0.6.0/src/nlbone/core/domain → nlbone-0.6.9/src/nlbone/interfaces}/__init__.py +0 -0
  76. {nlbone-0.6.0/src/nlbone/interfaces → nlbone-0.6.9/src/nlbone/interfaces/api}/__init__.py +0 -0
  77. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/dependencies/__init__.py +0 -0
  78. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -0
  79. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
  80. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
  81. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
  82. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/exceptions.py +0 -0
  83. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
  84. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
  85. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
  86. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
  87. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
  88. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/routers.py +0 -0
  89. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/api/schemas.py +0 -0
  90. {nlbone-0.6.0/src/nlbone/interfaces/api → nlbone-0.6.9/src/nlbone/interfaces/cli}/__init__.py +0 -0
  91. {nlbone-0.6.0/src/nlbone/interfaces/cli → nlbone-0.6.9/src/nlbone/interfaces/jobs}/__init__.py +0 -0
  92. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
  93. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/types.py +0 -0
  94. {nlbone-0.6.0/src/nlbone/interfaces/jobs → nlbone-0.6.9/src/nlbone/utils}/__init__.py +0 -0
  95. {nlbone-0.6.0 → nlbone-0.6.9}/src/nlbone/utils/context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.6.0
3
+ Version: 0.6.9
4
4
  Summary: Backbone package for interfaces and infrastructure in Python projects
5
5
  Author-email: Amir Hosein Kahkbazzadeh <a.khakbazzadeh@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nlbone"
7
- version = "0.6.0"
7
+ version = "0.6.9"
8
8
  description = "Backbone package for interfaces and infrastructure in Python projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -63,4 +63,4 @@ dev = [
63
63
  ]
64
64
 
65
65
  [project.scripts]
66
- nlbone = "nlbone.interfaces.cli.main:main"
66
+ nlbone = "nlbone.interfaces.cli.main:main"
@@ -0,0 +1 @@
1
+ import nlbone.adapters.db.postgres.audit
@@ -75,4 +75,4 @@ class KeycloakAuthService(AuthService):
75
75
  def client_has_access(self, token: str, permissions: list[str], allowed_clients: set[str] | None = None) -> bool:
76
76
  if not self.is_client_token(token, allowed_clients):
77
77
  return False
78
- return self.has_access(token, permissions)
78
+ return self.has_access(token, permissions)
@@ -1,6 +1,6 @@
1
1
  import threading
2
2
  import time
3
- from typing import Optional, Dict, Any
3
+ from typing import Any, Dict, Optional
4
4
 
5
5
  from nlbone.adapters.auth.keycloak import KeycloakAuthService
6
6
 
@@ -1,14 +1,20 @@
1
1
  import asyncio
2
2
  import json
3
3
  import os
4
- from typing import Optional, Iterable, Any, Mapping, Sequence, List
4
+ from typing import Any, Iterable, Mapping, Optional, Sequence
5
5
 
6
6
  from redis.asyncio import Redis
7
+
7
8
  from nlbone.core.ports.cache import AsyncCachePort
8
9
 
9
10
 
10
- def _nsver_key(ns: str) -> str: return f"nsver:{ns}"
11
- def _tag_key(tag: str) -> str: return f"tag:{tag}"
11
+ def _nsver_key(ns: str) -> str:
12
+ return f"nsver:{ns}"
13
+
14
+
15
+ def _tag_key(tag: str) -> str:
16
+ return f"tag:{tag}"
17
+
12
18
 
13
19
  class AsyncRedisCache(AsyncCachePort):
14
20
  def __init__(self, url: str, *, invalidate_channel: str | None = None):
@@ -36,7 +42,9 @@ class AsyncRedisCache(AsyncCachePort):
36
42
  fk = await self._full_key(key)
37
43
  return await self._r.get(fk)
38
44
 
39
- async def set(self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None:
45
+ async def set(
46
+ self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
47
+ ) -> None:
40
48
  fk = await self._full_key(key)
41
49
  if ttl is None:
42
50
  await self._r.set(fk, value)
@@ -66,8 +74,9 @@ class AsyncRedisCache(AsyncCachePort):
66
74
  fks = [await self._full_key(k) for k in keys]
67
75
  return await self._r.mget(fks)
68
76
 
69
- async def mset(self, items: Mapping[str, bytes], *, ttl: Optional[int] = None,
70
- tags: Optional[Iterable[str]] = None) -> None:
77
+ async def mset(
78
+ self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
79
+ ) -> None:
71
80
  pipe = self._r.pipeline()
72
81
  if ttl is None:
73
82
  for k, v in items.items():
@@ -93,8 +102,9 @@ class AsyncRedisCache(AsyncCachePort):
93
102
  b = await self.get(key)
94
103
  return None if b is None else json.loads(b)
95
104
 
96
- async def set_json(self, key: str, value: Any, *, ttl: Optional[int] = None,
97
- tags: Optional[Iterable[str]] = None) -> None:
105
+ async def set_json(
106
+ self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
107
+ ) -> None:
98
108
  await self.set(key, json.dumps(value).encode("utf-8"), ttl=ttl, tags=tags)
99
109
 
100
110
  # -------- invalidation --------
@@ -1,5 +1,8 @@
1
- import json, threading, time
2
- from typing import Optional, Iterable, Any, Mapping, Sequence, Dict, Set
1
+ import json
2
+ import threading
3
+ import time
4
+ from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Set
5
+
3
6
  from nlbone.core.ports.cache import CachePort
4
7
 
5
8
 
@@ -12,7 +15,8 @@ class InMemoryCache(CachePort):
12
15
 
13
16
  def _expired(self, key: str) -> bool:
14
17
  v = self._data.get(key)
15
- if not v: return True
18
+ if not v:
19
+ return True
16
20
  _, exp = v
17
21
  return exp is not None and time.time() > exp
18
22
 
@@ -21,7 +25,8 @@ class InMemoryCache(CachePort):
21
25
  self._data.pop(key, None)
22
26
 
23
27
  def _attach_tags(self, key: str, tags: Optional[Iterable[str]]) -> None:
24
- if not tags: return
28
+ if not tags:
29
+ return
25
30
  for t in tags:
26
31
  self._tags.setdefault(t, set()).add(key)
27
32
 
@@ -50,17 +55,20 @@ class InMemoryCache(CachePort):
50
55
  with self._lock:
51
56
  self._gc(key)
52
57
  v = self._data.get(key)
53
- if not v: return None
58
+ if not v:
59
+ return None
54
60
  _, exp = v
55
- if exp is None: return None
61
+ if exp is None:
62
+ return None
56
63
  rem = int(exp - time.time())
57
64
  return rem if rem >= 0 else 0
58
65
 
59
66
  def mget(self, keys: Sequence[str]) -> list[Optional[bytes]]:
60
67
  return [self.get(k) for k in keys]
61
68
 
62
- def mset(self, items: Mapping[str, bytes], *, ttl: Optional[int] = None,
63
- tags: Optional[Iterable[str]] = None) -> None:
69
+ def mset(
70
+ self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
71
+ ) -> None:
64
72
  for k, v in items.items():
65
73
  self.set(k, v, ttl=ttl, tags=tags)
66
74
 
@@ -68,8 +76,9 @@ class InMemoryCache(CachePort):
68
76
  b = self.get(key)
69
77
  return None if b is None else json.loads(b)
70
78
 
71
- def set_json(self, key: str, value: Any, *, ttl: Optional[int] = None,
72
- tags: Optional[Iterable[str]] = None) -> None:
79
+ def set_json(
80
+ self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
81
+ ) -> None:
73
82
  self.set(key, json.dumps(value).encode("utf-8"), ttl=ttl, tags=tags)
74
83
 
75
84
  def invalidate_tags(self, tags: Iterable[str]) -> int:
@@ -91,7 +100,8 @@ class InMemoryCache(CachePort):
91
100
  def clear_namespace(self, namespace: str) -> int:
92
101
  with self._lock:
93
102
  keys = [k for k in self._data.keys() if k.startswith(namespace + ":")]
94
- for k in keys: self.delete(k)
103
+ for k in keys:
104
+ self.delete(k)
95
105
  return len(keys)
96
106
 
97
107
  def get_or_set(self, key: str, producer, *, ttl: int, tags=None) -> bytes:
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
+
2
3
  import asyncio
3
4
  import json
4
5
  from typing import Awaitable, Callable, Optional
6
+
5
7
  from redis.asyncio import Redis
6
8
 
9
+
7
10
  async def run_cache_invalidation_listener(
8
11
  redis: Redis,
9
12
  channel: str = "cache:invalidate",
@@ -1,13 +1,22 @@
1
1
  from __future__ import annotations
2
- import json, os, time
3
- from typing import Optional, Iterable, Any, Mapping, Sequence, List, Set
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from typing import Any, Iterable, Mapping, Optional, Sequence
7
+
4
8
  import redis # redis-py (sync)
9
+
5
10
  from nlbone.core.ports.cache import CachePort
6
11
 
7
12
 
13
+ def _nsver_key(ns: str) -> str:
14
+ return f"nsver:{ns}"
15
+
16
+
17
+ def _tag_key(tag: str) -> str:
18
+ return f"tag:{tag}"
8
19
 
9
- def _nsver_key(ns: str) -> str: return f"nsver:{ns}"
10
- def _tag_key(tag: str) -> str: return f"tag:{tag}"
11
20
 
12
21
  class RedisCache(CachePort):
13
22
  def __init__(self, url: str):
@@ -57,7 +66,9 @@ class RedisCache(CachePort):
57
66
  fks = [self._full_key(k) for k in keys]
58
67
  return self.r.mget(fks)
59
68
 
60
- def mset(self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None:
69
+ def mset(
70
+ self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
71
+ ) -> None:
61
72
  pipe = self.r.pipeline()
62
73
  if ttl is None:
63
74
  for k, v in items.items():
@@ -77,7 +88,9 @@ class RedisCache(CachePort):
77
88
  b = self.get(key)
78
89
  return None if b is None else json.loads(b)
79
90
 
80
- def set_json(self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None:
91
+ def set_json(
92
+ self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
93
+ ) -> None:
81
94
  self.set(key, json.dumps(value).encode("utf-8"), ttl=ttl, tags=tags)
82
95
 
83
96
  def invalidate_tags(self, tags: Iterable[str]) -> int:
@@ -109,8 +122,10 @@ class RedisCache(CachePort):
109
122
  while True:
110
123
  cursor, keys = self.r.scan(cursor=cursor, match=pattern, count=1000)
111
124
  if keys:
112
- self.r.delete(*keys); cnt += len(keys)
113
- if cursor == 0: break
125
+ self.r.delete(*keys)
126
+ cnt += len(keys)
127
+ if cursor == 0:
128
+ break
114
129
  return cnt
115
130
 
116
131
  def get_or_set(self, key: str, producer, *, ttl: int, tags=None) -> bytes:
@@ -1,4 +1,3 @@
1
1
  from .postgres import apply_pagination, get_paginated_response
2
2
  from .postgres.base import Base
3
3
  from .postgres.engine import async_ping, async_session, init_async_engine, init_sync_engine, sync_ping, sync_session
4
- import nlbone.adapters.db.postgres.audit
@@ -1,10 +1,12 @@
1
1
  import uuid
2
2
  from datetime import date, datetime
3
+ from decimal import Decimal
4
+ from enum import Enum as _Enum
3
5
  from typing import Any
4
- from sqlalchemy import event, inspect as sa_inspect
6
+
7
+ from sqlalchemy import event
8
+ from sqlalchemy import inspect as sa_inspect
5
9
  from sqlalchemy.orm import Session as SASession
6
- from enum import Enum as _Enum
7
- from decimal import Decimal
8
10
 
9
11
  from nlbone.core.domain.models import AuditLog
10
12
  from nlbone.utils.context import current_context_dict
@@ -54,8 +56,11 @@ def _ser(val):
54
56
 
55
57
 
56
58
  def _entity_name(obj: Any) -> str:
57
- return getattr(getattr(obj, "__table__", None), "name", None) or getattr(obj, "__tablename__",
58
- None) or obj.__class__.__name__
59
+ return (
60
+ getattr(getattr(obj, "__table__", None), "name", None)
61
+ or getattr(obj, "__tablename__", None)
62
+ or obj.__class__.__name__
63
+ )
59
64
 
60
65
 
61
66
  def _entity_id(obj: Any) -> str:
@@ -84,13 +89,15 @@ def _changes_for_update(obj: any) -> dict[str, dict[str, any]]:
84
89
  except KeyError:
85
90
  continue
86
91
 
87
- hist = state.history # History object
92
+ hist = state.history # History object
88
93
  if hist.has_changes():
89
94
  old = hist.deleted[0] if hist.deleted else None
90
95
  new = hist.added[0] if hist.added else None
91
96
  if old != new:
92
97
  changes[key] = {"old": _ser(old), "new": _ser(new)}
93
98
  return changes
99
+
100
+
94
101
  @event.listens_for(SASession, "before_flush")
95
102
  def before_flush(session: SASession, flush_context, instances):
96
103
  entries = session.info.setdefault("_audit_entries", [])
@@ -107,11 +114,7 @@ def before_flush(session: SASession, flush_context, instances):
107
114
  if key in exclude:
108
115
  continue
109
116
  row[key] = _ser(getattr(obj, key, None))
110
- entries.append({
111
- "obj": obj,
112
- "op": "INSERT",
113
- "changes": {k: {"old": None, "new": v} for k, v in row.items()}
114
- })
117
+ entries.append({"obj": obj, "op": "INSERT", "changes": {k: {"old": None, "new": v} for k, v in row.items()}})
115
118
 
116
119
  # UPDATE
117
120
  for obj in session.dirty:
@@ -1,6 +1,6 @@
1
1
  from typing import Any, Callable, Optional, Sequence, Type, Union
2
2
 
3
- from sqlalchemy import asc, desc, or_
3
+ from sqlalchemy import asc, desc, or_, and_, case, literal
4
4
  from sqlalchemy.dialects.postgresql import ENUM as PGEnum
5
5
  from sqlalchemy.orm import Query, Session
6
6
  from sqlalchemy.orm.interfaces import LoaderOption
@@ -29,8 +29,15 @@ class _InvalidEnum(Exception):
29
29
 
30
30
 
31
31
  def _apply_order(pagination: PaginateRequest, entity, query):
32
+ order_clauses = []
33
+
34
+ include_ids = getattr(pagination, "include_ids", []) or []
35
+ if include_ids and hasattr(entity, "id"):
36
+ id_col = getattr(entity, "id")
37
+ whens = [(id_col == _id, idx) for idx, _id in enumerate(include_ids)]
38
+ order_clauses.append(asc(case(*whens, else_=literal(999_999))))
39
+
32
40
  if pagination.sort:
33
- order_clauses = []
34
41
  for sort in pagination.sort:
35
42
  field = sort["field"]
36
43
  order = sort["order"]
@@ -42,8 +49,8 @@ def _apply_order(pagination: PaginateRequest, entity, query):
42
49
  else:
43
50
  order_clauses.append(desc(column))
44
51
 
45
- if order_clauses:
46
- query = query.order_by(*order_clauses)
52
+ if order_clauses:
53
+ query = query.order_by(*order_clauses)
47
54
  return query
48
55
 
49
56
 
@@ -101,89 +108,100 @@ def _parse_field_and_op(field: str):
101
108
 
102
109
 
103
110
  def _apply_filters(pagination, entity, query):
104
- if not getattr(pagination, "filters", None):
111
+ if not getattr(pagination, "filters", None) and not getattr(pagination, "include_ids", None):
105
112
  return query
106
113
 
107
- for raw_field, value in pagination.filters.items():
108
- if value is None or value in NULL_SENTINELS or value == [] or value == {}:
109
- value = None
110
-
111
- field, op_hint = _parse_field_and_op(raw_field)
112
-
113
- if not hasattr(entity, field):
114
- continue
115
-
116
- col = getattr(entity, field)
117
- coltype = getattr(col, "type", None)
118
-
119
- def coerce(v):
120
- if v is None:
121
- return None
122
- # Enums
123
- if isinstance(coltype, (SAEnum, PGEnum)):
124
- return _coerce_enum(coltype, v)
125
- # Text
126
- if _is_text_type(coltype):
127
- return str(v)
128
- # Numbers
129
- if isinstance(coltype, (Integer, BigInteger, SmallInteger)):
130
- return int(v)
131
- if isinstance(coltype, (Float, Numeric)):
132
- return float(v)
133
- # Booleans
134
- if isinstance(coltype, Boolean):
135
- if isinstance(v, bool):
136
- return v
137
- if isinstance(v, (int, float)):
138
- return bool(v)
139
- if isinstance(v, str):
140
- vl = v.strip().lower()
141
- if vl in {"true", "1", "yes", "y", "t"}:
114
+ predicates = []
115
+
116
+ if getattr(pagination, "filters", None):
117
+ for raw_field, value in pagination.filters.items():
118
+ if value is None or value in NULL_SENTINELS or value == [] or value == {}:
119
+ value = None
120
+
121
+ field, op_hint = _parse_field_and_op(raw_field)
122
+
123
+ if not hasattr(entity, field):
124
+ continue
125
+
126
+ col = getattr(entity, field)
127
+ coltype = getattr(col, "type", None)
128
+
129
+ def coerce(v):
130
+ if v is None:
131
+ return None
132
+ # Enums
133
+ if isinstance(coltype, (SAEnum, PGEnum)):
134
+ return _coerce_enum(coltype, v)
135
+ # Text
136
+ if _is_text_type(coltype):
137
+ return str(v)
138
+ # Numbers
139
+ if isinstance(coltype, (Integer, BigInteger, SmallInteger)):
140
+ return int(v)
141
+ if isinstance(coltype, (Float, Numeric)):
142
+ return float(v)
143
+ # Booleans
144
+ if isinstance(coltype, Boolean):
145
+ if isinstance(v, bool):
146
+ return v
147
+ if isinstance(v, (int, float)):
148
+ return bool(v)
149
+ if isinstance(v, str):
150
+ vl = v.strip().lower()
151
+ if vl in {"true", "1", "yes", "y", "t"}:
152
+ return True
153
+ if vl in {"false", "0", "no", "n", "f"}:
154
+ return False
155
+ return None
156
+ return v
157
+
158
+ try:
159
+ def _use_ilike(v) -> bool:
160
+ if op_hint == "ilike":
161
+ return True
162
+ if _is_text_type(coltype) and isinstance(v, str) and _looks_like_wildcard(v):
142
163
  return True
143
- if vl in {"false", "0", "no", "n", "f"}:
144
- return False
145
- return None
146
- # fallback
147
- return v
164
+ return False
148
165
 
149
- try:
150
- # Decide operator: explicit __ilike OR automatic if wildcard on text
151
- def _use_ilike(v) -> bool:
152
- if op_hint == "ilike":
153
- return True
154
- if _is_text_type(coltype) and isinstance(v, str) and _looks_like_wildcard(v):
155
- return True
156
- return False
157
-
158
- if isinstance(value, (list, tuple, set)):
159
- vals = [v for v in value if v not in (None, "", "null", "None")]
160
- if not vals:
161
- continue
162
-
163
- # if any value signals ilike, apply OR of ilike; else IN / EQs
164
- if any(_use_ilike(v) for v in vals) and _is_text_type(coltype):
165
- patterns = [_to_sql_like_pattern(str(v)) for v in vals]
166
- query = query.filter(or_(*[col.ilike(p) for p in patterns]))
167
- else:
168
- coerced = [coerce(v) for v in vals]
169
- if not coerced:
166
+ if isinstance(value, (list, tuple, set)):
167
+ vals = [v for v in value if v not in (None, "", "null", "None")]
168
+ if not vals:
170
169
  continue
171
- query = query.filter(col.in_(coerced))
172
- else:
173
- if _use_ilike(value) and _is_text_type(coltype):
174
- pattern = _to_sql_like_pattern(str(value))
175
- query = query.filter(col.ilike(pattern))
170
+
171
+ if any(_use_ilike(v) for v in vals) and _is_text_type(coltype):
172
+ patterns = [_to_sql_like_pattern(str(v)) for v in vals]
173
+ predicates.append(or_(*[col.ilike(p) for p in patterns]))
174
+ else:
175
+ coerced = [coerce(v) for v in vals]
176
+ if not coerced:
177
+ continue
178
+ predicates.append(col.in_(coerced))
176
179
  else:
177
- v = coerce(value)
178
- if v is None:
179
- query = query.filter(col.is_(None))
180
+ if _use_ilike(value) and _is_text_type(coltype):
181
+ pattern = _to_sql_like_pattern(str(value))
182
+ predicates.append(col.ilike(pattern))
180
183
  else:
181
- query = query.filter(col == v)
182
-
183
- except _InvalidEnum as e:
184
- # Surface validation error like before
185
- raise UnprocessableEntityException(str(e), loc=["query", "filters", raw_field]) from e
186
-
184
+ v = coerce(value)
185
+ if v is None:
186
+ predicates.append(col.is_(None))
187
+ else:
188
+ predicates.append(col == v)
189
+
190
+ except _InvalidEnum as e:
191
+ raise UnprocessableEntityException(str(e), loc=["query", "filters", raw_field]) from e
192
+
193
+ include_ids = getattr(pagination, "include_ids", []) or []
194
+ if include_ids and hasattr(entity, "id"):
195
+ id_col = getattr(entity, "id")
196
+ include_pred = id_col.in_(include_ids)
197
+ if predicates:
198
+ final_pred = or_(and_(*predicates), include_pred)
199
+ else:
200
+ final_pred = or_(and_(*[1 == 1]), include_pred)
201
+ return query.filter(final_pred)
202
+
203
+ if predicates:
204
+ query = query.filter(and_(*predicates))
187
205
  return query
188
206
 
189
207
 
@@ -210,24 +228,24 @@ def _serialize_item(item: Any, output_cls: OutputType) -> Any:
210
228
 
211
229
  if hasattr(output_cls, "model_validate"):
212
230
  try:
213
- model = output_cls.model_validate(item, from_attributes=True) # type: ignore[attr-defined]
231
+ model = output_cls.model_validate(item, from_attributes=True)
214
232
  if hasattr(model, "model_dump"):
215
- return model.model_dump() # type: ignore[attr-defined]
233
+ return model.model_dump()
216
234
  return model
217
235
  except Exception:
218
236
  pass
219
237
 
220
238
  if hasattr(output_cls, "from_orm"):
221
239
  try:
222
- model = output_cls.from_orm(item) # type: ignore[attr-defined]
240
+ model = output_cls.from_orm(item)
223
241
  if hasattr(model, "dict"):
224
- return model.dict() # type: ignore[attr-defined]
242
+ return model.dict()
225
243
  return model
226
244
  except Exception:
227
245
  pass
228
246
 
229
247
  try:
230
- obj = output_cls(item) # type: ignore[call-arg]
248
+ obj = output_cls(item)
231
249
  try:
232
250
  from dataclasses import asdict, is_dataclass
233
251
 
@@ -249,7 +267,6 @@ def get_paginated_response(
249
267
  output_cls: Optional[Type] = None,
250
268
  eager_options: Optional[Sequence[LoaderOption]] = None,
251
269
  ) -> dict:
252
- # پایه‌ی کوئری
253
270
  query = session.query(entity)
254
271
  if eager_options:
255
272
  query = query.options(*eager_options)
@@ -9,10 +9,7 @@ class RedisClient:
9
9
  @classmethod
10
10
  def get_client(cls) -> redis.Redis:
11
11
  if cls._client is None:
12
- cls._client = redis.from_url(
13
- get_settings().REDIS_URL,
14
- decode_responses=True
15
- )
12
+ cls._client = redis.from_url(get_settings().REDIS_URL, decode_responses=True)
16
13
  return cls._client
17
14
 
18
15
  @classmethod
@@ -0,0 +1,2 @@
1
+ from .pricing import CalculatePriceIn, CalculatePriceOut, PricingService
2
+ from .uploadchi import UploadchiAsyncClient, UploadchiClient, UploadchiError
@@ -0,0 +1 @@
1
+ from .pricing_service import CalculatePriceIn, CalculatePriceOut, PricingService