nlbone 0.6.8__py3-none-any.whl → 0.6.9__py3-none-any.whl

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.
@@ -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)
@@ -1,5 +1,6 @@
1
+ from decimal import Decimal
1
2
  from enum import Enum
2
- from typing import List, Literal, Optional
3
+ from typing import Dict, List, Literal, Optional, Union
3
4
 
4
5
  import httpx
5
6
  import requests
@@ -32,7 +33,7 @@ class Product(BaseModel):
32
33
 
33
34
 
34
35
  class Pricing(BaseModel):
35
- source: Literal["formula", "static"]
36
+ source: Optional[Literal["formula", "static"]] = None
36
37
  price: Optional[float] = None
37
38
  discount: Optional[float] = None
38
39
  discount_type: Optional[DiscountType] = None
@@ -61,7 +62,7 @@ class PricingRule(BaseModel):
61
62
  pricing: Pricing
62
63
 
63
64
 
64
- class CalculatePriceOut(RootModel[List[PricingRule]]):
65
+ class CalculatePriceOut(RootModel[Union[List[PricingRule], Dict[str, PricingRule]]]):
65
66
  pass
66
67
 
67
68
 
@@ -98,3 +99,18 @@ class PricingService:
98
99
  return CalculatePriceOut.model_validate(root=[])
99
100
 
100
101
  return CalculatePriceOut.model_validate(r.json())
102
+
103
+ def exchange_rates(self) -> Dict[str, Decimal]:
104
+ r = self._client.get(
105
+ f"{self._base_url}/variables/key/exchange_rates",
106
+ headers=auth_headers(self._token_provider.get_access_token()),
107
+ timeout=self._timeout,
108
+ verify=False,
109
+ )
110
+
111
+ if r.status_code != 200:
112
+ raise PricingError(r.status_code, r.text)
113
+
114
+ values = r.json().get("values")
115
+
116
+ return {f"{value['key']}": Decimal(value["value"]) for value in values}
nlbone/container.py CHANGED
@@ -15,6 +15,7 @@ from nlbone.adapters.http_clients import PricingService
15
15
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
16
16
  from nlbone.adapters.http_clients.uploadchi.uploadchi_async import UploadchiAsyncClient
17
17
  from nlbone.adapters.messaging import InMemoryEventBus
18
+ from nlbone.config.settings import Settings
18
19
  from nlbone.core.ports import EventBusPort
19
20
  from nlbone.core.ports.cache import AsyncCachePort, CachePort
20
21
  from nlbone.core.ports.files import AsyncFileServicePort, FileServicePort
@@ -62,12 +63,12 @@ class Container(containers.DeclarativeContainer):
62
63
  def create_container(settings: Optional[Any] = None) -> Container:
63
64
  c = Container()
64
65
  if settings is not None:
65
- if hasattr(settings, "model_dump"):
66
+ if isinstance(settings, Settings):
67
+ c.config.override(settings)
68
+ elif hasattr(settings, "model_dump"):
66
69
  c.config.from_dict(settings.model_dump()) # Pydantic v2
67
70
  elif hasattr(settings, "dict"):
68
71
  c.config.from_dict(settings.dict()) # Pydantic v1
69
72
  elif isinstance(settings, Mapping):
70
73
  c.config.from_dict(dict(settings))
71
- else:
72
- c.config.override(settings)
73
74
  return c
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from math import ceil
3
- from typing import Any, Optional
3
+ from typing import Any, Optional, List
4
4
 
5
5
  from fastapi import Query
6
6
 
@@ -13,16 +13,18 @@ class PaginateRequest:
13
13
  """
14
14
 
15
15
  def __init__(
16
- self,
17
- limit: int = 10,
18
- offset: int = 0,
19
- sort: Optional[str] = None,
20
- filters: Optional[str] = Query(None, description="e.g. title:abc"),
16
+ self,
17
+ limit: int = 10,
18
+ offset: int = 0,
19
+ sort: Optional[str] = None,
20
+ filters: Optional[str] = Query(None, description="e.g. title:abc"),
21
+ include: Optional[str] = None,
21
22
  ) -> None:
22
23
  self.limit = max(0, limit)
23
24
  self.offset = max(0, offset)
24
25
  self.sort = self._parse_sort(sort)
25
26
  self.filters = self._parse_filters(filters or "")
27
+ self.include_ids: List[int] = ([int(x) for x in include.split(",") if x.strip().isdigit()] if include else [])[:50]
26
28
 
27
29
  @staticmethod
28
30
  def _parse_sort(sort_str: Optional[str]) -> list[dict[str, str]]:
@@ -81,12 +83,12 @@ class PaginateResponse:
81
83
  """
82
84
 
83
85
  def __init__(
84
- self,
85
- data: list[Any],
86
- total_count: int | None,
87
- limit: int,
88
- offset: int,
89
- use_data_key: bool = True,
86
+ self,
87
+ data: list[Any],
88
+ total_count: int | None,
89
+ limit: int,
90
+ offset: int,
91
+ use_data_key: bool = True,
90
92
  ) -> None:
91
93
  self.data = data
92
94
  self.total_count = total_count
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.6.8
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
@@ -1,5 +1,5 @@
1
1
  nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- nlbone/container.py,sha256=ZD5sF1aByHUR_SoYo2Cw0_LMgGtw1pQluiG1afbzgRI,3242
2
+ nlbone/container.py,sha256=VPGkfQO0HS4SbQy2ja3HVKyuMjD94p5ZmDPSqYHGhNo,3317
3
3
  nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  nlbone/adapters/__init__.py,sha256=NzUmk4XPyp3GJOw7VSE86xkQMZLtG3MrOoXLeoB551M,41
5
5
  nlbone/adapters/auth/__init__.py,sha256=hkDHvsFhw_UiOHG9ZSMqjiAhK4wumEforitveSZswVw,42
@@ -15,7 +15,7 @@ nlbone/adapters/db/postgres/__init__.py,sha256=6JYJH0xZs3aR-zuyMpRhsdzFugmqz8npr
15
15
  nlbone/adapters/db/postgres/audit.py,sha256=8f5XOuW7_ybJyy_STam1FNzqmZAAVAu7tmMRUkCGJOM,4594
16
16
  nlbone/adapters/db/postgres/base.py,sha256=kha9xmklzhuQAK8QEkNBn-mAHq8dUKbOM-3abaBpWmQ,71
17
17
  nlbone/adapters/db/postgres/engine.py,sha256=UCegauVB1gvo42ThytYnn5VIcQBwR-5xhcXYFApRFNk,3448
18
- nlbone/adapters/db/postgres/query_builder.py,sha256=U5pqpCfJKuMIxIEHyodoHuPgE8jf53slC1ScKZR5xa4,8653
18
+ nlbone/adapters/db/postgres/query_builder.py,sha256=rJ9J3vl0ikELwq5JujFn7jZYPmF6rM5wHTbIiUHOs1k,9381
19
19
  nlbone/adapters/db/postgres/repository.py,sha256=J_DBE73JhHPYCk90c5-O7lQtZbxDgqjjN9OcWy4Omvs,1660
20
20
  nlbone/adapters/db/postgres/schema.py,sha256=NlE7Rr8uXypsw4oWkdZhZwcIBHQEPIpoHLxcUo98i6s,1039
21
21
  nlbone/adapters/db/postgres/uow.py,sha256=nRxNpY-WoWHpym-XeZ8VHm0MYvtB9wuopOeNdV_ebk8,2088
@@ -23,7 +23,7 @@ nlbone/adapters/db/redis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
23
23
  nlbone/adapters/db/redis/client.py,sha256=5SUnwP2-GrueSFimUbiqDvrQsumvIE2aeozk8l-vOfQ,466
24
24
  nlbone/adapters/http_clients/__init__.py,sha256=w-Yr9CLuXMU71N0Ada5HbvP1DB53wqeP6B-i5rALlTo,150
25
25
  nlbone/adapters/http_clients/pricing/__init__.py,sha256=ElA9NFcAR9u4cqb_w3PPqKU3xGeyjNLQ8veJ0ql2iz0,81
26
- nlbone/adapters/http_clients/pricing/pricing_service.py,sha256=KzLRfNxrFjFi74EXahRVS7EH9qx8dbWoFB9WsmG15Og,2710
26
+ nlbone/adapters/http_clients/pricing/pricing_service.py,sha256=PDG6CbLg_SL-BsrhNwfyypywcuZIsEyj5mpQGSPH4e4,3300
27
27
  nlbone/adapters/http_clients/uploadchi/__init__.py,sha256=uBzEOuVtY22teWW2b36Pitkdk5yVdSqa6xbg22JfTNg,105
28
28
  nlbone/adapters/http_clients/uploadchi/uploadchi.py,sha256=ABFiH3bLsxFtB-4Si4SEedE2bMUVz5hWXGwD4RkV3ws,4816
29
29
  nlbone/adapters/http_clients/uploadchi/uploadchi_async.py,sha256=PQbVNeaYde5CmgT3vcnQoI1PGeSs9AxHlPFuB8biOmU,4717
@@ -70,7 +70,7 @@ nlbone/interfaces/api/middleware/access_log.py,sha256=vIkxxxfy2HcjqqKb8XCfGCcSri
70
70
  nlbone/interfaces/api/middleware/add_request_context.py,sha256=av-qs0biOYuF9R6RJOo2eYsFqDL9WRYWcjVakFhbt-w,1834
71
71
  nlbone/interfaces/api/middleware/authentication.py,sha256=ze7vCm492QsX9nPL6A-PqZCmC1C5ZgUE-OWI6fCLpsU,1809
72
72
  nlbone/interfaces/api/pagination/__init__.py,sha256=sWKKQFa2Z-1SlprQOqImOa2c9exq4wueKpUL_9QM7wc,417
73
- nlbone/interfaces/api/pagination/offset_base.py,sha256=B6rHxzDsxQbm-d2snM6tjgnhWyZw7zvs7fcehV0gpa0,3621
73
+ nlbone/interfaces/api/pagination/offset_base.py,sha256=BPG8veDRUA-67Z_JqLQTqT2nqCalxPblzSiQv_HZXUc,3838
74
74
  nlbone/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  nlbone/interfaces/cli/init_db.py,sha256=C67n2MsJ1vzkJxC8zfUYOxFdd6mEB_vT9agxN6jWoG8,790
76
76
  nlbone/interfaces/cli/main.py,sha256=pNldsTgplVyXa-Hx96dySO2I9gFRi49nDXv7J_dO73s,686
@@ -84,8 +84,8 @@ nlbone/utils/context.py,sha256=MmclJ24BG2uvSTg1IK7J-Da9BhVFDQ5ag4Ggs2FF1_w,1600
84
84
  nlbone/utils/http.py,sha256=UXUoXgQdTRNT08ho8zl-C5ekfDsD8uf-JiMQ323ooqw,872
85
85
  nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
86
86
  nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
87
- nlbone-0.6.8.dist-info/METADATA,sha256=GRppq6YJdTQl_op7vf2oWBZP3IX2TNznHWhTFtvXJOM,2194
88
- nlbone-0.6.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
- nlbone-0.6.8.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
90
- nlbone-0.6.8.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
- nlbone-0.6.8.dist-info/RECORD,,
87
+ nlbone-0.6.9.dist-info/METADATA,sha256=uE-MksB9505zd0epl9bh3S7QmnanB2hvnoppyU_0mH4,2194
88
+ nlbone-0.6.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
+ nlbone-0.6.9.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
90
+ nlbone-0.6.9.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
+ nlbone-0.6.9.dist-info/RECORD,,
File without changes