nlbone 0.7.22__py3-none-any.whl → 0.7.24__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.
Files changed (35) hide show
  1. nlbone/adapters/auth/auth_service.py +10 -16
  2. nlbone/adapters/db/postgres/query_builder.py +183 -78
  3. nlbone/adapters/db/postgres/types.py +3 -1
  4. nlbone/adapters/http_clients/pricing/pricing_service.py +1 -1
  5. nlbone/adapters/i18n/__init__.py +0 -0
  6. nlbone/adapters/i18n/engine.py +28 -0
  7. nlbone/adapters/i18n/loaders.py +58 -0
  8. nlbone/adapters/i18n/locales/fa.json +12 -0
  9. nlbone/adapters/messaging/rabbitmq.py +2 -6
  10. nlbone/adapters/outbox/outbox_consumer.py +21 -21
  11. nlbone/adapters/outbox/outbox_repo.py +6 -5
  12. nlbone/adapters/snowflake.py +9 -6
  13. nlbone/adapters/ticketing/client.py +10 -5
  14. nlbone/config/settings.py +4 -4
  15. nlbone/container.py +5 -2
  16. nlbone/core/domain/base.py +3 -7
  17. nlbone/core/domain/models.py +2 -1
  18. nlbone/core/ports/outbox.py +33 -33
  19. nlbone/core/ports/translation.py +8 -0
  20. nlbone/interfaces/api/dependencies/async_auth.py +2 -1
  21. nlbone/interfaces/api/dependencies/auth.py +3 -1
  22. nlbone/interfaces/api/dependencies/client_credential.py +3 -2
  23. nlbone/interfaces/api/middleware/add_request_context.py +2 -1
  24. nlbone/interfaces/api/middleware/authentication.py +7 -4
  25. nlbone/interfaces/api/pagination/offset_base.py +1 -1
  26. nlbone/interfaces/api/schema/base_response_model.py +3 -0
  27. nlbone/interfaces/cli/ticket.py +16 -13
  28. nlbone/utils/context.py +8 -0
  29. nlbone/utils/flatten_sqlalchemy_result.py +26 -0
  30. nlbone/utils/http.py +1 -1
  31. {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/METADATA +1 -1
  32. {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/RECORD +35 -29
  33. {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/WHEEL +0 -0
  34. {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/entry_points.txt +0 -0
  35. {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/licenses/LICENSE +0 -0
@@ -36,7 +36,7 @@ class SQLAlchemyOutboxRepository(OutboxRepository):
36
36
  return row
37
37
 
38
38
  def enqueue_many(
39
- self, items: Iterable[Tuple[str, Dict[str, Any]]], *, headers=None, available_at=None
39
+ self, items: Iterable[Tuple[str, Dict[str, Any]]], *, headers=None, available_at=None
40
40
  ) -> List[Outbox]:
41
41
  rows = [
42
42
  Outbox(topic=t, payload=p, headers=headers or {}, available_at=available_at or self._now())
@@ -46,8 +46,9 @@ class SQLAlchemyOutboxRepository(OutboxRepository):
46
46
  self.session.flush()
47
47
  return [r for r in rows]
48
48
 
49
- def claim_batch(self, *, topics: list[str] = None, limit: int = 100, now: Optional[datetime] = None) -> List[
50
- Outbox]:
49
+ def claim_batch(
50
+ self, *, topics: list[str] = None, limit: int = 100, now: Optional[datetime] = None
51
+ ) -> List[Outbox]:
51
52
  now = now or self._now()
52
53
  # Select candidates eligible to process
53
54
  stmt = (
@@ -113,7 +114,7 @@ class SQLAlchemyAsyncOutboxRepository(AsyncOutboxRepository):
113
114
  return datetime.now(timezone.utc)
114
115
 
115
116
  async def enqueue(
116
- self, topic: str, payload: Dict[str, Any], *, headers=None, key=None, available_at=None
117
+ self, topic: str, payload: Dict[str, Any], *, headers=None, key=None, available_at=None
117
118
  ) -> Outbox:
118
119
  row = Outbox(
119
120
  topic=topic,
@@ -127,7 +128,7 @@ class SQLAlchemyAsyncOutboxRepository(AsyncOutboxRepository):
127
128
  return row
128
129
 
129
130
  async def enqueue_many(
130
- self, items: Iterable[Tuple[str, Dict[str, Any]]], *, headers=None, available_at=None
131
+ self, items: Iterable[Tuple[str, Dict[str, Any]]], *, headers=None, available_at=None
131
132
  ) -> List[Outbox]:
132
133
  rows = [
133
134
  Outbox(topic=t, payload=p, headers=headers or {}, available_at=available_at or self._now())
@@ -10,9 +10,9 @@ class Snowflake:
10
10
  DATACENTER_ID_BITS = 5
11
11
  SEQUENCE_BITS = 12
12
12
 
13
- MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1 # 31
13
+ MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1 # 31
14
14
  MAX_DATACENTER_ID = (1 << DATACENTER_ID_BITS) - 1 # 31
15
- SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1 # 4095
15
+ SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1 # 4095
16
16
 
17
17
  WORKER_ID_SHIFT = SEQUENCE_BITS
18
18
  DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS
@@ -58,12 +58,15 @@ class Snowflake:
58
58
 
59
59
  self.last_ts = ts
60
60
 
61
- id64 = ((ts - self.EPOCH) << self.TIMESTAMP_SHIFT) | \
62
- (self.datacenter_id << self.DATACENTER_ID_SHIFT) | \
63
- (self.worker_id << self.WORKER_ID_SHIFT) | \
64
- self.sequence
61
+ id64 = (
62
+ ((ts - self.EPOCH) << self.TIMESTAMP_SHIFT)
63
+ | (self.datacenter_id << self.DATACENTER_ID_SHIFT)
64
+ | (self.worker_id << self.WORKER_ID_SHIFT)
65
+ | self.sequence
66
+ )
65
67
  return id64
66
68
 
69
+
67
70
  setting = get_settings()
68
71
  _DC_ID = int(setting.SNOWFLAKE_DATACENTER_ID)
69
72
  _WORKER_ID = int(setting.SNOWFLAKE_WORKER_ID)
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, Optional
1
+ from typing import Optional
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
@@ -28,12 +28,17 @@ class TicketingClient:
28
28
  self._exchange = settings.RABBITMQ_TICKETING_EXCHANGE
29
29
  self._rk_create_v1 = settings.RABBITMQ_TICKETING_ROUTING_KEY_CREATE_V1
30
30
 
31
- async def create_ticket(self, payload: CreateTicketIn, created_by_id: int, *,
32
- override_exchange: Optional[str] = None,
33
- override_routing_key: Optional[str] = None) -> None:
31
+ async def create_ticket(
32
+ self,
33
+ payload: CreateTicketIn,
34
+ created_by_id: int,
35
+ *,
36
+ override_exchange: Optional[str] = None,
37
+ override_routing_key: Optional[str] = None,
38
+ ) -> None:
34
39
  exchange = override_exchange or self._exchange
35
40
  routing_key = override_routing_key or self._rk_create_v1
36
41
  payload = payload.model_dump()
37
- payload.update({'created_by_id': created_by_id})
42
+ payload.update({"created_by_id": created_by_id})
38
43
  print(payload)
39
44
  await self._bus.publish(exchange=exchange, routing_key=routing_key, payload=payload)
nlbone/config/settings.py CHANGED
@@ -3,7 +3,7 @@ from functools import lru_cache
3
3
  from pathlib import Path
4
4
  from typing import Literal
5
5
 
6
- from pydantic import AnyHttpUrl, Field, SecretStr, AliasChoices
6
+ from pydantic import AliasChoices, AnyHttpUrl, Field, SecretStr
7
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
8
8
 
9
9
 
@@ -43,7 +43,7 @@ class Settings(BaseSettings):
43
43
  LOG_LEVEL: Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] = Field(default="INFO")
44
44
  LOG_JSON: bool = Field(default=True)
45
45
 
46
- SNOWFLAKE_WORKER_ID : int = Field(default=1)
46
+ SNOWFLAKE_WORKER_ID: int = Field(default=1)
47
47
  SNOWFLAKE_DATACENTER_ID: int = Field(default=1)
48
48
 
49
49
  AUDIT_DEFAULT_ENABLE: bool = False
@@ -60,9 +60,9 @@ class Settings(BaseSettings):
60
60
  KEYCLOAK_REALM_NAME: str = Field(default="numberland")
61
61
  KEYCLOAK_CLIENT_ID: str = Field(default="nlbone")
62
62
  KEYCLOAK_CLIENT_SECRET: SecretStr = Field(default=SecretStr("dev-secret"))
63
- PRICING_API_SECRET: str = ''
63
+ PRICING_API_SECRET: str = ""
64
64
 
65
- AUTH_SERVICE_URL: AnyHttpUrl = Field(default="https://auth.numberland.ir")
65
+ AUTH_SERVICE_URL: AnyHttpUrl = Field(default="https://auth.numberland.ir")
66
66
  CLIENT_ID: str = Field(default="", validation_alias=AliasChoices("KEYCLOAK_CLIENT_ID"))
67
67
  CLIENT_SECRET: SecretStr = Field(default="", validation_alias=AliasChoices("KEYCLOAK_CLIENT_SECRET"))
68
68
 
nlbone/container.py CHANGED
@@ -6,8 +6,6 @@ from dependency_injector import containers, providers
6
6
  from pydantic_settings import BaseSettings
7
7
 
8
8
  from nlbone.adapters.auth.auth_service import AuthService as AuthService_IMP
9
- from nlbone.core.ports.auth import AuthService
10
- from nlbone.adapters.auth.keycloak import KeycloakAuthService
11
9
  from nlbone.adapters.auth.token_provider import ClientTokenProvider
12
10
  from nlbone.adapters.cache.async_redis import AsyncRedisCache
13
11
  from nlbone.adapters.cache.memory import InMemoryCache
@@ -16,6 +14,9 @@ from nlbone.adapters.db.postgres.engine import get_async_session_factory, get_sy
16
14
  from nlbone.adapters.http_clients import PricingService
17
15
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
18
16
  from nlbone.adapters.http_clients.uploadchi.uploadchi_async import UploadchiAsyncClient
17
+ from nlbone.adapters.i18n.engine import I18nAdapter
18
+ from nlbone.adapters.i18n.loaders import JSONFileLoader
19
+ from nlbone.core.ports.auth import AuthService
19
20
  from nlbone.core.ports.cache import AsyncCachePort, CachePort
20
21
  from nlbone.core.ports.files import AsyncFileServicePort, FileServicePort
21
22
 
@@ -54,6 +55,8 @@ class Container(containers.DeclarativeContainer):
54
55
  redis=providers.Singleton(AsyncRedisCache, url=config.REDIS_URL),
55
56
  )
56
57
 
58
+ translator = I18nAdapter(loader=JSONFileLoader(locales_path="path/to/locales"), default_locale="fa")
59
+
57
60
 
58
61
  def create_container(settings: Optional[BaseSettings | Dict] = None) -> Container:
59
62
  c = Container()
@@ -4,11 +4,9 @@ from _decimal import Decimal
4
4
  from dataclasses import dataclass
5
5
  from datetime import datetime, timezone
6
6
  from enum import Enum
7
- from typing import Any, Generic, List, TypeVar, Callable, ClassVar
7
+ from typing import Any, Callable, ClassVar, Generic, List, TypeVar
8
8
  from uuid import uuid4
9
9
 
10
- from pydantic import BaseModel
11
-
12
10
  from nlbone.adapters.snowflake import SNOWFLAKE
13
11
 
14
12
  TId = TypeVar("TId")
@@ -18,12 +16,10 @@ class DomainError(Exception):
18
16
  """Base domain exception."""
19
17
 
20
18
 
21
-
22
19
  class Message:
23
20
  pass
24
21
 
25
22
 
26
-
27
23
  class DomainEvent(Message):
28
24
  """Immutable domain event."""
29
25
 
@@ -84,9 +80,9 @@ class BaseEnum(Enum):
84
80
  pass
85
81
 
86
82
 
87
-
88
83
  ID = TypeVar("ID", bound="BaseId")
89
84
 
85
+
90
86
  @dataclass(frozen=True)
91
87
  class BaseId(ValueObject):
92
88
  value: int
@@ -118,4 +114,4 @@ class BaseId(ValueObject):
118
114
  return cls(v)
119
115
 
120
116
  def __repr__(self):
121
- return f"<{self.value}>"
117
+ return f"<{self.value}>"
@@ -1,6 +1,6 @@
1
1
  import enum
2
2
  import uuid
3
- from datetime import datetime, timezone, timedelta
3
+ from datetime import datetime, timedelta, timezone
4
4
  from typing import Any, Dict, Optional
5
5
 
6
6
  from sqlalchemy import JSON, DateTime, Index, Integer, String, Text
@@ -80,5 +80,6 @@ class Outbox(Base):
80
80
  self.status = OutboxStatus.PUBLISHED
81
81
  self.next_attempt_at = None
82
82
 
83
+
83
84
  def to_outbox_row(evt) -> Outbox:
84
85
  return Outbox(topic=evt.topic, payload=evt.__dict__)
@@ -8,29 +8,29 @@ from nlbone.core.domain.models import Outbox, OutboxStatus
8
8
 
9
9
  class OutboxRepository(Protocol):
10
10
  def enqueue(
11
- self,
12
- topic: str,
13
- payload: Dict[str, Any],
14
- *,
15
- headers: Optional[Dict[str, Any]] = None,
16
- key: Optional[str] = None,
17
- available_at: Optional[datetime] = None,
11
+ self,
12
+ topic: str,
13
+ payload: Dict[str, Any],
14
+ *,
15
+ headers: Optional[Dict[str, Any]] = None,
16
+ key: Optional[str] = None,
17
+ available_at: Optional[datetime] = None,
18
18
  ) -> Outbox: ...
19
19
 
20
20
  def enqueue_many(
21
- self,
22
- items: Iterable[Tuple[str, Dict[str, Any]]],
23
- *,
24
- headers: Optional[Dict[str, Any]] = None,
25
- available_at: Optional[datetime] = None,
21
+ self,
22
+ items: Iterable[Tuple[str, Dict[str, Any]]],
23
+ *,
24
+ headers: Optional[Dict[str, Any]] = None,
25
+ available_at: Optional[datetime] = None,
26
26
  ) -> List[Outbox]: ...
27
27
 
28
28
  def claim_batch(
29
- self,
30
- *,
31
- topics: list[str] = None,
32
- limit: int = 100,
33
- now: Optional[datetime] = None,
29
+ self,
30
+ *,
31
+ topics: list[str] = None,
32
+ limit: int = 100,
33
+ now: Optional[datetime] = None,
34
34
  ) -> List[Outbox]: ...
35
35
 
36
36
  def mark_published(self, ids: Iterable[int]) -> None: ...
@@ -42,28 +42,28 @@ class OutboxRepository(Protocol):
42
42
 
43
43
  class AsyncOutboxRepository(Protocol):
44
44
  async def enqueue(
45
- self,
46
- topic: str,
47
- payload: Dict[str, Any],
48
- *,
49
- headers: Optional[Dict[str, Any]] = None,
50
- key: Optional[str] = None,
51
- available_at: Optional[datetime] = None,
45
+ self,
46
+ topic: str,
47
+ payload: Dict[str, Any],
48
+ *,
49
+ headers: Optional[Dict[str, Any]] = None,
50
+ key: Optional[str] = None,
51
+ available_at: Optional[datetime] = None,
52
52
  ) -> Outbox: ...
53
53
 
54
54
  async def enqueue_many(
55
- self,
56
- items: Iterable[Tuple[str, Dict[str, Any]]],
57
- *,
58
- headers: Optional[Dict[str, Any]] = None,
59
- available_at: Optional[datetime] = None,
55
+ self,
56
+ items: Iterable[Tuple[str, Dict[str, Any]]],
57
+ *,
58
+ headers: Optional[Dict[str, Any]] = None,
59
+ available_at: Optional[datetime] = None,
60
60
  ) -> List[Outbox]: ...
61
61
 
62
62
  async def claim_batch(
63
- self,
64
- *,
65
- limit: int = 100,
66
- now: Optional[datetime] = None,
63
+ self,
64
+ *,
65
+ limit: int = 100,
66
+ now: Optional[datetime] = None,
67
67
  ) -> List[Outbox]: ...
68
68
 
69
69
  async def mark_published(self, ids: Iterable[int]) -> None: ...
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+
5
+ class TranslationPort(ABC):
6
+ @abstractmethod
7
+ def translate(self, key: str, locale: Optional[str] = None, **kwargs) -> str:
8
+ pass
@@ -1,10 +1,11 @@
1
1
  import functools
2
2
 
3
- from .auth import client_has_access_func
4
3
  from nlbone.adapters.auth import KeycloakAuthService
5
4
  from nlbone.interfaces.api.exceptions import ForbiddenException, UnauthorizedException
6
5
  from nlbone.utils.context import current_request
7
6
 
7
+ from .auth import client_has_access_func
8
+
8
9
 
9
10
  async def current_user_id() -> int:
10
11
  user_id = current_request().state.user_id
@@ -6,12 +6,14 @@ from nlbone.config.settings import get_settings
6
6
  from nlbone.interfaces.api.exceptions import ForbiddenException, UnauthorizedException
7
7
  from nlbone.utils.context import current_request
8
8
 
9
+
9
10
  @functools.lru_cache()
10
11
  def bypass_authz() -> bool:
11
- if get_settings().ENV != 'prod':
12
+ if get_settings().ENV != "prod":
12
13
  return True
13
14
  return False
14
15
 
16
+
15
17
  def current_user_id() -> int:
16
18
  user_id = current_request().state.user_id
17
19
  if user_id is not None:
@@ -1,11 +1,10 @@
1
1
  import asyncio
2
- import functools
3
2
  from typing import Callable
4
3
 
5
4
  from makefun import wraps as mf_wraps
6
5
 
7
6
  from nlbone.adapters.auth.auth_service import AuthService
8
- from nlbone.interfaces.api.exceptions import UnauthorizedException, ForbiddenException
7
+ from nlbone.interfaces.api.exceptions import ForbiddenException, UnauthorizedException
9
8
  from nlbone.utils.context import current_request
10
9
 
11
10
 
@@ -28,6 +27,7 @@ def client_has_access(*, permissions=None):
28
27
  is_async_func = asyncio.iscoroutinefunction(func)
29
28
 
30
29
  if is_async_func:
30
+
31
31
  @mf_wraps(func)
32
32
  async def aw(*args, **kwargs):
33
33
  client_has_access_func(permissions=permissions)
@@ -41,4 +41,5 @@ def client_has_access(*, permissions=None):
41
41
  return func(*args, **kwargs)
42
42
 
43
43
  return sw
44
+
44
45
  return deco
@@ -45,8 +45,9 @@ class AddRequestContextMiddleware(BaseHTTPMiddleware):
45
45
  user_id = getattr(getattr(request, "state", None), "user_id", None) or request.headers.get("X-User-Id")
46
46
  ip = request.client.host if request.client else None
47
47
  ua = request.headers.get("user-agent")
48
+ locale = request.headers.get("Accept-Language")
48
49
 
49
- tokens = bind_context(request=request, request_id=req_id, user_id=user_id, ip=ip, user_agent=ua)
50
+ tokens = bind_context(request=request, request_id=req_id, user_id=user_id, ip=ip, user_agent=ua, locale=locale)
50
51
  try:
51
52
  response = await call_next(request)
52
53
  response.headers.setdefault("X-Request-ID", req_id)
@@ -3,8 +3,8 @@ from typing import Callable, Optional, Union
3
3
  from fastapi import Request
4
4
  from starlette.middleware.base import BaseHTTPMiddleware
5
5
 
6
- from nlbone.config.settings import get_settings
7
6
  from nlbone.adapters.auth.auth_service import AuthService
7
+ from nlbone.config.settings import get_settings
8
8
 
9
9
  try:
10
10
  from dependency_injector import providers
@@ -50,8 +50,11 @@ def authenticate_admin_user(request, auth_service):
50
50
  except Exception:
51
51
  pass
52
52
 
53
+
53
54
  def authenticate_user(request):
54
- token = request.cookies.get("access_token") or request.cookies.get("j_token") or request.headers.get("Authorization")
55
+ token = (
56
+ request.cookies.get("access_token") or request.cookies.get("j_token") or request.headers.get("Authorization")
57
+ )
55
58
  if request.headers.get("Authorization"):
56
59
  scheme, token = request.headers.get("Authorization").split(" ", 1)
57
60
 
@@ -75,11 +78,11 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
75
78
  request.state.client_id = None
76
79
  request.state.user_id = None
77
80
  request.state.token = None
78
- if request.headers.get('X-Client-Id') and request.headers.get("X-Api-Key"):
81
+ if request.headers.get("X-Client-Id") and request.headers.get("X-Api-Key"):
79
82
  if request.headers.get("X-Api-Key") == get_settings().PRICING_API_SECRET:
80
83
  request.state.client_id = request.headers.get("X-Client-Id")
81
84
  return await call_next(request)
82
- if request.headers.get('X-Client-Id') == 'website' and request.headers.get("Authorization"):
85
+ if request.headers.get("X-Client-Id") == "website" and request.headers.get("Authorization"):
83
86
  authenticate_user(request)
84
87
  elif request.cookies.get("access_token"):
85
88
  authenticate_user(request)
@@ -26,7 +26,7 @@ class PaginateRequest:
26
26
  self.filters = self._parse_filters(filters or "")
27
27
  self.include = []
28
28
  try:
29
- self.include = [{item.split(':')[0]: item.split(':')[1]} for item in include.split(",")]
29
+ self.include = [{item.split(":")[0]: item.split(":")[1]} for item in include.split(",")]
30
30
  except:
31
31
  pass
32
32
  self.include_ids: List[int] = ([int(x) for x in include.split(",") if x.strip().isdigit()] if include else [])[
@@ -1,16 +1,19 @@
1
1
  from typing import Any
2
+
2
3
  from pydantic import BaseModel, ConfigDict, model_serializer
3
4
 
4
5
  from nlbone.core.domain.base import BaseId
5
6
 
6
7
  EXCLUDE_NONE = "exclude_none"
7
8
 
9
+
8
10
  def convert_decimal_to_string(v: Any) -> Any:
9
11
  """Converts a value to string if it's a large integer (over the safe limit)."""
10
12
  if isinstance(v, int) and v > 9007199254740991:
11
13
  return str(v)
12
14
  return v
13
15
 
16
+
14
17
  class BaseResponseModel(BaseModel):
15
18
  model_config = ConfigDict(
16
19
  from_attributes=True,
@@ -1,29 +1,32 @@
1
1
  import asyncio
2
+
2
3
  import typer
3
4
 
4
- from nlbone.adapters.ticketing.client import TicketingClient, CreateTicketIn
5
+ from nlbone.adapters.ticketing.client import CreateTicketIn, TicketingClient
5
6
 
6
7
  app = typer.Typer(add_completion=False)
7
8
 
9
+
8
10
  @app.command("create")
9
11
  def send_sample():
10
12
  payload = CreateTicketIn(
11
- assignee_id= "153",
12
- category_id= 2,
13
- channel= "site_chat",
14
- direction= "incoming",
15
- entity_id= "153",
16
- entity_type= "user",
17
- message= "سلام خوبی",
18
- priority= "medium",
19
- product_id= 0,
20
- status= "open",
21
- title= "پشتیبانی فنی (ثبت نام و لاگین)",
22
- user_id= 153,
13
+ assignee_id="153",
14
+ category_id=2,
15
+ channel="site_chat",
16
+ direction="incoming",
17
+ entity_id="153",
18
+ entity_type="user",
19
+ message="سلام خوبی",
20
+ priority="medium",
21
+ product_id=0,
22
+ status="open",
23
+ title="پشتیبانی فنی (ثبت نام و لاگین)",
24
+ user_id=153,
23
25
  )
24
26
  client = TicketingClient()
25
27
  asyncio.run(client.create_ticket(payload, created_by_id=995836))
26
28
  print("Ticket message published (or logged if Noop).")
27
29
 
30
+
28
31
  if __name__ == "__main__":
29
32
  app()
nlbone/utils/context.py CHANGED
@@ -6,6 +6,7 @@ request_id_ctx: ContextVar[str | None] = ContextVar("request_id", default=None)
6
6
  user_id_ctx: ContextVar[str | None] = ContextVar("user_id", default=None)
7
7
  ip_ctx: ContextVar[str | None] = ContextVar("ip", default=None)
8
8
  user_agent_ctx: ContextVar[str | None] = ContextVar("user_agent", default=None)
9
+ _current_locale: ContextVar[str] = ContextVar("current_locale", default="en")
9
10
 
10
11
 
11
12
  def bind_context(
@@ -15,6 +16,7 @@ def bind_context(
15
16
  user_id: str | None = None,
16
17
  ip: str | None = None,
17
18
  user_agent: str | None = None,
19
+ locale: str | None = None,
18
20
  ) -> dict[ContextVar, Any]:
19
21
  tokens: dict[ContextVar, Any] = {}
20
22
  if request is not None:
@@ -27,6 +29,8 @@ def bind_context(
27
29
  tokens[ip_ctx] = ip_ctx.set(ip)
28
30
  if user_agent is not None:
29
31
  tokens[user_agent_ctx] = user_agent_ctx.set(user_agent)
32
+ if locale is not None:
33
+ tokens[_current_locale] = _current_locale.set(locale)
30
34
  return tokens
31
35
 
32
36
 
@@ -50,3 +54,7 @@ def current_context_dict() -> dict[str, Any]:
50
54
  "ip": ip_ctx.get(),
51
55
  "user_agent": user_agent_ctx.get(),
52
56
  }
57
+
58
+
59
+ def get_locale():
60
+ return _current_locale.get()
@@ -0,0 +1,26 @@
1
+ from typing import List, Any
2
+ from sqlalchemy import Row
3
+
4
+
5
+ def flatten_sqlalchemy_result(items: List[Any]) -> List[Any]:
6
+ """
7
+ Converts a list of SQLAlchemy Rows (tuples) into a flat list of entities
8
+ with extra fields injected as attributes.
9
+ """
10
+ if not items:
11
+ return []
12
+
13
+ if not isinstance(items[0], Row):
14
+ return items
15
+
16
+ normalized = []
17
+ for row in items:
18
+ entity = row[0]
19
+
20
+ for key, value in row._mapping.items():
21
+ if not isinstance(value, type(entity)):
22
+ setattr(entity, key, value)
23
+
24
+ normalized.append(entity)
25
+
26
+ return normalized
nlbone/utils/http.py CHANGED
@@ -12,7 +12,7 @@ def auth_headers(token: str | None) -> dict[str, str]:
12
12
 
13
13
 
14
14
  def build_list_query(
15
- limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
15
+ limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
16
16
  ) -> dict[str, Any]:
17
17
  q: dict[str, Any] = {"limit": limit, "offset": offset}
18
18
  if filters:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.7.22
3
+ Version: 0.7.24
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