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.
- nlbone/adapters/auth/auth_service.py +10 -16
- nlbone/adapters/db/postgres/query_builder.py +183 -78
- nlbone/adapters/db/postgres/types.py +3 -1
- nlbone/adapters/http_clients/pricing/pricing_service.py +1 -1
- nlbone/adapters/i18n/__init__.py +0 -0
- nlbone/adapters/i18n/engine.py +28 -0
- nlbone/adapters/i18n/loaders.py +58 -0
- nlbone/adapters/i18n/locales/fa.json +12 -0
- nlbone/adapters/messaging/rabbitmq.py +2 -6
- nlbone/adapters/outbox/outbox_consumer.py +21 -21
- nlbone/adapters/outbox/outbox_repo.py +6 -5
- nlbone/adapters/snowflake.py +9 -6
- nlbone/adapters/ticketing/client.py +10 -5
- nlbone/config/settings.py +4 -4
- nlbone/container.py +5 -2
- nlbone/core/domain/base.py +3 -7
- nlbone/core/domain/models.py +2 -1
- nlbone/core/ports/outbox.py +33 -33
- nlbone/core/ports/translation.py +8 -0
- nlbone/interfaces/api/dependencies/async_auth.py +2 -1
- nlbone/interfaces/api/dependencies/auth.py +3 -1
- nlbone/interfaces/api/dependencies/client_credential.py +3 -2
- nlbone/interfaces/api/middleware/add_request_context.py +2 -1
- nlbone/interfaces/api/middleware/authentication.py +7 -4
- nlbone/interfaces/api/pagination/offset_base.py +1 -1
- nlbone/interfaces/api/schema/base_response_model.py +3 -0
- nlbone/interfaces/cli/ticket.py +16 -13
- nlbone/utils/context.py +8 -0
- nlbone/utils/flatten_sqlalchemy_result.py +26 -0
- nlbone/utils/http.py +1 -1
- {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/METADATA +1 -1
- {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/RECORD +35 -29
- {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/WHEEL +0 -0
- {nlbone-0.7.22.dist-info → nlbone-0.7.24.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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(
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
nlbone/adapters/snowflake.py
CHANGED
|
@@ -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
|
|
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
|
|
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 = (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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(
|
|
32
|
-
|
|
33
|
-
|
|
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({
|
|
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
|
|
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
|
|
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:
|
|
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()
|
nlbone/core/domain/base.py
CHANGED
|
@@ -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,
|
|
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}>"
|
nlbone/core/domain/models.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
import uuid
|
|
3
|
-
from datetime import datetime,
|
|
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__)
|
nlbone/core/ports/outbox.py
CHANGED
|
@@ -8,29 +8,29 @@ from nlbone.core.domain.models import Outbox, OutboxStatus
|
|
|
8
8
|
|
|
9
9
|
class OutboxRepository(Protocol):
|
|
10
10
|
def enqueue(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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: ...
|
|
@@ -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 !=
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
nlbone/interfaces/cli/ticket.py
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
|
|
2
3
|
import typer
|
|
3
4
|
|
|
4
|
-
from nlbone.adapters.ticketing.client import
|
|
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=
|
|
12
|
-
category_id=
|
|
13
|
-
channel=
|
|
14
|
-
direction=
|
|
15
|
-
entity_id=
|
|
16
|
-
entity_type=
|
|
17
|
-
message=
|
|
18
|
-
priority=
|
|
19
|
-
product_id=
|
|
20
|
-
status=
|
|
21
|
-
title=
|
|
22
|
-
user_id=
|
|
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
|
-
|
|
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:
|