nlbone 0.4.0__py3-none-any.whl → 0.4.2__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/__init__.py +1 -1
- nlbone/adapters/auth/keycloak.py +6 -1
- nlbone/adapters/db/__init__.py +4 -3
- nlbone/adapters/db/postgres/__init__.py +4 -0
- nlbone/adapters/db/postgres/audit.py +148 -0
- nlbone/adapters/db/{sqlalchemy → postgres}/base.py +0 -2
- nlbone/adapters/db/{sqlalchemy → postgres}/engine.py +4 -3
- nlbone/adapters/db/{sqlalchemy → postgres}/query_builder.py +27 -11
- nlbone/adapters/db/{sqlalchemy → postgres}/repository.py +4 -2
- nlbone/adapters/db/{sqlalchemy → postgres}/schema.py +7 -7
- nlbone/adapters/db/{sqlalchemy → postgres}/uow.py +3 -2
- nlbone/adapters/db/redis/client.py +22 -0
- nlbone/adapters/http_clients/uploadchi.py +27 -11
- nlbone/adapters/http_clients/uploadchi_async.py +29 -7
- nlbone/adapters/messaging/event_bus.py +3 -0
- nlbone/adapters/percolation/__init__.py +1 -0
- nlbone/adapters/percolation/connection.py +12 -0
- nlbone/config/logging.py +76 -117
- nlbone/config/settings.py +20 -24
- nlbone/container.py +6 -5
- nlbone/core/application/base_worker.py +36 -0
- nlbone/core/application/events.py +5 -1
- nlbone/core/application/use_case.py +3 -1
- nlbone/core/domain/base.py +4 -0
- nlbone/core/domain/models.py +38 -0
- nlbone/core/ports/__init__.py +3 -3
- nlbone/core/ports/auth.py +1 -0
- nlbone/core/ports/event_bus.py +3 -0
- nlbone/core/ports/files.py +26 -5
- nlbone/core/ports/repo.py +5 -2
- nlbone/core/ports/uow.py +3 -0
- nlbone/interfaces/api/dependencies/__init__.py +11 -3
- nlbone/interfaces/api/dependencies/async_auth.py +61 -0
- nlbone/interfaces/api/dependencies/auth.py +5 -3
- nlbone/interfaces/api/dependencies/db.py +5 -3
- nlbone/interfaces/api/dependencies/uow.py +3 -2
- nlbone/interfaces/api/exception_handlers.py +17 -15
- nlbone/interfaces/api/exceptions.py +1 -2
- nlbone/interfaces/api/middleware/__init__.py +2 -2
- nlbone/interfaces/api/middleware/access_log.py +12 -8
- nlbone/interfaces/api/middleware/add_request_context.py +55 -52
- nlbone/interfaces/api/middleware/authentication.py +4 -1
- nlbone/interfaces/api/pagination/__init__.py +4 -5
- nlbone/interfaces/api/pagination/offset_base.py +0 -2
- nlbone/interfaces/cli/init_db.py +24 -13
- nlbone/interfaces/cli/main.py +29 -0
- nlbone/utils/context.py +14 -4
- nlbone/utils/redactor.py +32 -0
- nlbone/utils/time.py +41 -2
- {nlbone-0.4.0.dist-info → nlbone-0.4.2.dist-info}/METADATA +5 -9
- nlbone-0.4.2.dist-info/RECORD +78 -0
- nlbone-0.4.2.dist-info/entry_points.txt +2 -0
- nlbone/adapters/db/postgres.py +0 -0
- nlbone/adapters/db/sqlalchemy/__init__.py +0 -4
- nlbone-0.4.0.dist-info/RECORD +0 -71
- /nlbone/adapters/db/{memory.py → redis/__init__.py} +0 -0
- {nlbone-0.4.0.dist-info → nlbone-0.4.2.dist-info}/WHEEL +0 -0
- {nlbone-0.4.0.dist-info → nlbone-0.4.2.dist-info}/licenses/LICENSE +0 -0
nlbone/adapters/auth/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from .keycloak import KeycloakAuthService
|
|
1
|
+
from .keycloak import KeycloakAuthService
|
nlbone/adapters/auth/keycloak.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from keycloak import KeycloakOpenID
|
|
2
2
|
from keycloak.exceptions import KeycloakAuthenticationError
|
|
3
|
+
|
|
4
|
+
from nlbone.config.settings import Settings, get_settings, is_production_env
|
|
3
5
|
from nlbone.core.ports.auth import AuthService
|
|
4
|
-
from nlbone.config.settings import Settings, get_settings
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class KeycloakAuthService(AuthService):
|
|
@@ -13,8 +14,12 @@ class KeycloakAuthService(AuthService):
|
|
|
13
14
|
realm_name=s.KEYCLOAK_REALM_NAME,
|
|
14
15
|
client_secret_key=s.KEYCLOAK_CLIENT_SECRET.get_secret_value().strip(),
|
|
15
16
|
)
|
|
17
|
+
self.bypass = not is_production_env()
|
|
16
18
|
|
|
17
19
|
def has_access(self, token, permissions):
|
|
20
|
+
if self.bypass:
|
|
21
|
+
return True
|
|
22
|
+
|
|
18
23
|
try:
|
|
19
24
|
result = self.keycloak_openid.has_uma_access(token, permissions=permissions)
|
|
20
25
|
return result.is_authorized
|
nlbone/adapters/db/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
3
|
-
from .
|
|
1
|
+
from .postgres import apply_pagination, get_paginated_response
|
|
2
|
+
from .postgres.base import Base
|
|
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
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
from .engine import async_ping, async_session, init_async_engine, init_sync_engine, sync_ping, sync_session
|
|
2
|
+
from .query_builder import apply_pagination, get_paginated_response
|
|
3
|
+
from .repository import AsyncSqlAlchemyRepository, SqlAlchemyRepository
|
|
4
|
+
from .uow import AsyncSqlAlchemyUnitOfWork, SqlAlchemyUnitOfWork
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
from sqlalchemy import event, inspect as sa_inspect
|
|
5
|
+
from sqlalchemy.orm import Session as SASession
|
|
6
|
+
from enum import Enum as _Enum
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
from nlbone.core.domain.models import AuditLog
|
|
10
|
+
from nlbone.utils.context import current_context_dict
|
|
11
|
+
|
|
12
|
+
DEFAULT_EXCLUDE = {"updated_at", "created_at"}
|
|
13
|
+
DEFAULT_ENABLED = False
|
|
14
|
+
DEFAULT_OPS = {"INSERT", "UPDATE", "DELETE"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_ops_for(obj) -> set[str]:
|
|
18
|
+
ops = getattr(obj, "__audit_ops__", None)
|
|
19
|
+
if ops is None:
|
|
20
|
+
return set(DEFAULT_OPS)
|
|
21
|
+
return {str(op).upper() for op in ops}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_audit_disabled(obj) -> bool:
|
|
25
|
+
if getattr(obj, "__audit_disable__", False):
|
|
26
|
+
return True
|
|
27
|
+
if hasattr(obj, "__audit_enable__") and not getattr(obj, "__audit_enable__"):
|
|
28
|
+
return True
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_op_enabled(obj, op: str) -> bool:
|
|
33
|
+
if _is_audit_disabled(obj):
|
|
34
|
+
return False
|
|
35
|
+
return op.upper() in _get_ops_for(obj)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ser(val):
|
|
39
|
+
if isinstance(val, (date, datetime)):
|
|
40
|
+
return val.isoformat()
|
|
41
|
+
# UUID
|
|
42
|
+
if isinstance(val, uuid.UUID):
|
|
43
|
+
return str(val)
|
|
44
|
+
# Enum
|
|
45
|
+
if isinstance(val, _Enum):
|
|
46
|
+
return val.value
|
|
47
|
+
if isinstance(val, Decimal):
|
|
48
|
+
return str(val)
|
|
49
|
+
if isinstance(val, set):
|
|
50
|
+
return list(val)
|
|
51
|
+
return val
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _entity_name(obj: Any) -> str:
|
|
55
|
+
return getattr(getattr(obj, "__table__", None), "name", None) or getattr(obj, "__tablename__",
|
|
56
|
+
None) or obj.__class__.__name__
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _entity_id(obj: Any) -> str:
|
|
60
|
+
insp = sa_inspect(obj)
|
|
61
|
+
if insp.identity and len(insp.identity) == 1:
|
|
62
|
+
return _ser(insp.identity[0])
|
|
63
|
+
for pk in insp.mapper.primary_key:
|
|
64
|
+
v = getattr(obj, pk.key)
|
|
65
|
+
if v is not None:
|
|
66
|
+
return _ser(v)
|
|
67
|
+
return _ser(getattr(obj, "id", "?"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _changes_for_update(obj: any) -> dict[str, dict[str, any]]:
|
|
71
|
+
changes = {}
|
|
72
|
+
insp = sa_inspect(obj)
|
|
73
|
+
exclude = set(getattr(obj, "__audit_exclude__", set())) | DEFAULT_EXCLUDE
|
|
74
|
+
|
|
75
|
+
for col in insp.mapper.column_attrs:
|
|
76
|
+
key = col.key
|
|
77
|
+
if key in exclude:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
state = insp.attrs[key]
|
|
82
|
+
except KeyError:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
hist = state.history # History object
|
|
86
|
+
if hist.has_changes():
|
|
87
|
+
old = hist.deleted[0] if hist.deleted else None
|
|
88
|
+
new = hist.added[0] if hist.added else None
|
|
89
|
+
if old != new:
|
|
90
|
+
changes[key] = {"old": _ser(old), "new": _ser(new)}
|
|
91
|
+
return changes
|
|
92
|
+
@event.listens_for(SASession, "before_flush")
|
|
93
|
+
def before_flush(session: SASession, flush_context, instances):
|
|
94
|
+
entries = session.info.setdefault("_audit_entries", [])
|
|
95
|
+
|
|
96
|
+
# INSERT
|
|
97
|
+
for obj in session.new:
|
|
98
|
+
if isinstance(obj, AuditLog) or not _is_op_enabled(obj, "INSERT"):
|
|
99
|
+
continue
|
|
100
|
+
insp = sa_inspect(obj)
|
|
101
|
+
exclude = set(getattr(obj, "__audit_exclude__", set())) | DEFAULT_EXCLUDE
|
|
102
|
+
row = {}
|
|
103
|
+
for col_attr in insp.mapper.column_attrs:
|
|
104
|
+
key = col_attr.key
|
|
105
|
+
if key in exclude:
|
|
106
|
+
continue
|
|
107
|
+
row[key] = _ser(getattr(obj, key, None))
|
|
108
|
+
entries.append({
|
|
109
|
+
"obj": obj,
|
|
110
|
+
"op": "INSERT",
|
|
111
|
+
"changes": {k: {"old": None, "new": v} for k, v in row.items()}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
# UPDATE
|
|
115
|
+
for obj in session.dirty:
|
|
116
|
+
if isinstance(obj, AuditLog) or not _is_op_enabled(obj, "UPDATE"):
|
|
117
|
+
continue
|
|
118
|
+
if session.is_modified(obj, include_collections=False):
|
|
119
|
+
ch = _changes_for_update(obj)
|
|
120
|
+
if ch:
|
|
121
|
+
entries.append({"obj": obj, "op": "UPDATE", "changes": ch})
|
|
122
|
+
|
|
123
|
+
# DELETE
|
|
124
|
+
for obj in session.deleted:
|
|
125
|
+
if isinstance(obj, AuditLog) or not _is_op_enabled(obj, "DELETE"):
|
|
126
|
+
continue
|
|
127
|
+
entries.append({"obj": obj, "op": "DELETE", "changes": None})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@event.listens_for(SASession, "after_flush_postexec")
|
|
131
|
+
def after_flush_postexec(session: SASession, flush_context):
|
|
132
|
+
entries = session.info.pop("_audit_entries", [])
|
|
133
|
+
if not entries:
|
|
134
|
+
return
|
|
135
|
+
ctx = current_context_dict()
|
|
136
|
+
for e in entries:
|
|
137
|
+
obj = e["obj"]
|
|
138
|
+
al = AuditLog(
|
|
139
|
+
entity=_entity_name(obj),
|
|
140
|
+
entity_id=str(_entity_id(obj)),
|
|
141
|
+
operation=e["op"],
|
|
142
|
+
changes=e.get("changes"),
|
|
143
|
+
actor_id=ctx.get("user_id"),
|
|
144
|
+
request_id=ctx.get("request_id"),
|
|
145
|
+
ip=ctx.get("ip"),
|
|
146
|
+
user_agent=ctx.get("user_agent"),
|
|
147
|
+
)
|
|
148
|
+
session.add(al)
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager, contextmanager
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Any, AsyncGenerator, Generator, Optional
|
|
3
3
|
|
|
4
4
|
from sqlalchemy import create_engine, text
|
|
5
5
|
from sqlalchemy.engine import Engine
|
|
6
|
-
from sqlalchemy.orm import Session, sessionmaker
|
|
7
|
-
|
|
8
6
|
from sqlalchemy.ext.asyncio import (
|
|
9
7
|
AsyncEngine,
|
|
10
8
|
AsyncSession,
|
|
11
9
|
async_sessionmaker,
|
|
12
10
|
create_async_engine,
|
|
13
11
|
)
|
|
12
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
14
13
|
|
|
15
14
|
from nlbone.config.settings import get_settings
|
|
16
15
|
|
|
@@ -117,12 +116,14 @@ def sync_ping() -> None:
|
|
|
117
116
|
with eng.connect() as conn:
|
|
118
117
|
conn.execute(text("SELECT 1"))
|
|
119
118
|
|
|
119
|
+
|
|
120
120
|
def get_async_session_factory() -> async_sessionmaker[AsyncSession]:
|
|
121
121
|
if _async_session_factory is None:
|
|
122
122
|
init_async_engine()
|
|
123
123
|
assert _async_session_factory is not None
|
|
124
124
|
return _async_session_factory
|
|
125
125
|
|
|
126
|
+
|
|
126
127
|
def get_sync_session_factory() -> sessionmaker[Session]:
|
|
127
128
|
if _sync_session_factory is None:
|
|
128
129
|
init_sync_engine()
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Any, Callable, Optional, Sequence, Type, Union
|
|
2
2
|
|
|
3
3
|
from sqlalchemy import asc, desc, or_
|
|
4
|
+
from sqlalchemy.dialects.postgresql import ENUM as PGEnum
|
|
5
|
+
from sqlalchemy.orm import Query, Session
|
|
4
6
|
from sqlalchemy.orm.interfaces import LoaderOption
|
|
5
7
|
from sqlalchemy.sql.sqltypes import (
|
|
6
|
-
|
|
8
|
+
BigInteger,
|
|
9
|
+
Boolean,
|
|
10
|
+
Float,
|
|
11
|
+
Integer,
|
|
12
|
+
Numeric,
|
|
13
|
+
SmallInteger,
|
|
14
|
+
String,
|
|
15
|
+
Text,
|
|
16
|
+
)
|
|
17
|
+
from sqlalchemy.sql.sqltypes import (
|
|
18
|
+
Enum as SAEnum,
|
|
7
19
|
)
|
|
8
|
-
from sqlalchemy.orm import Session, Query
|
|
9
|
-
from sqlalchemy.dialects.postgresql import ENUM as PGEnum
|
|
10
20
|
|
|
11
21
|
from nlbone.interfaces.api.exceptions import UnprocessableEntityException
|
|
12
22
|
from nlbone.interfaces.api.pagination import PaginateRequest, PaginateResponse
|
|
@@ -122,12 +132,16 @@ def _apply_filters(pagination, entity, query):
|
|
|
122
132
|
return float(v)
|
|
123
133
|
# Booleans
|
|
124
134
|
if isinstance(coltype, Boolean):
|
|
125
|
-
if isinstance(v, bool):
|
|
126
|
-
|
|
135
|
+
if isinstance(v, bool):
|
|
136
|
+
return v
|
|
137
|
+
if isinstance(v, (int, float)):
|
|
138
|
+
return bool(v)
|
|
127
139
|
if isinstance(v, str):
|
|
128
140
|
vl = v.strip().lower()
|
|
129
|
-
if vl in {"true", "1", "yes", "y", "t"}:
|
|
130
|
-
|
|
141
|
+
if vl in {"true", "1", "yes", "y", "t"}:
|
|
142
|
+
return True
|
|
143
|
+
if vl in {"false", "0", "no", "n", "f"}:
|
|
144
|
+
return False
|
|
131
145
|
return None
|
|
132
146
|
# fallback
|
|
133
147
|
return v
|
|
@@ -215,7 +229,8 @@ def _serialize_item(item: Any, output_cls: OutputType) -> Any:
|
|
|
215
229
|
try:
|
|
216
230
|
obj = output_cls(item) # type: ignore[call-arg]
|
|
217
231
|
try:
|
|
218
|
-
from dataclasses import
|
|
232
|
+
from dataclasses import asdict, is_dataclass
|
|
233
|
+
|
|
219
234
|
if is_dataclass(obj):
|
|
220
235
|
return asdict(obj)
|
|
221
236
|
except Exception:
|
|
@@ -252,5 +267,6 @@ def get_paginated_response(
|
|
|
252
267
|
data = [output_cls.model_validate(r, from_attributes=True).model_dump() for r in rows]
|
|
253
268
|
else:
|
|
254
269
|
data = rows
|
|
255
|
-
return PaginateResponse(
|
|
256
|
-
|
|
270
|
+
return PaginateResponse(
|
|
271
|
+
total_count=total_count, data=data, limit=pagination.limit, offset=pagination.offset
|
|
272
|
+
).to_dict()
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import Generic, Iterable, List, Optional, Type, TypeVar
|
|
3
4
|
|
|
4
5
|
from sqlalchemy import select
|
|
5
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
7
|
from sqlalchemy.orm import Session
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
from nlbone.core.ports.repo import AsyncRepository, Repository
|
|
8
10
|
|
|
9
11
|
T = TypeVar("T")
|
|
10
12
|
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
|
|
2
1
|
import importlib
|
|
3
2
|
from typing import Sequence
|
|
4
3
|
|
|
5
|
-
from nlbone.adapters.db.
|
|
6
|
-
from nlbone.adapters.db.
|
|
4
|
+
from nlbone.adapters.db.postgres.base import Base
|
|
5
|
+
from nlbone.adapters.db.postgres.engine import init_async_engine, init_sync_engine
|
|
7
6
|
|
|
7
|
+
DEFAULT_MODEL_MODULES: Sequence[str] = ()
|
|
8
8
|
|
|
9
|
-
DEFAULT_MODEL_MODULES: Sequence[str] = (
|
|
10
|
-
)
|
|
11
9
|
|
|
12
10
|
def import_model_modules(modules: Sequence[str] | None = None) -> None:
|
|
13
|
-
for m in
|
|
11
|
+
for m in modules or DEFAULT_MODEL_MODULES:
|
|
14
12
|
importlib.import_module(m)
|
|
15
13
|
|
|
14
|
+
|
|
16
15
|
# --------- Async (SQLAlchemy 2.x) ----------
|
|
17
16
|
async def init_db_async(model_modules: Sequence[str] | None = None) -> None:
|
|
18
17
|
"""Create tables using AsyncEngine (dev/test). Prefer Alembic in prod."""
|
|
@@ -21,9 +20,10 @@ async def init_db_async(model_modules: Sequence[str] | None = None) -> None:
|
|
|
21
20
|
async with engine.begin() as conn:
|
|
22
21
|
await conn.run_sync(Base.metadata.create_all)
|
|
23
22
|
|
|
23
|
+
|
|
24
24
|
# --------- Sync ----------
|
|
25
25
|
def init_db_sync(model_modules: Sequence[str] | None = None) -> None:
|
|
26
26
|
"""Create tables using Sync Engine (dev/test). Prefer Alembic in prod."""
|
|
27
27
|
import_model_modules(model_modules)
|
|
28
28
|
engine = init_sync_engine()
|
|
29
|
-
Base.metadata.create_all(bind=engine)
|
|
29
|
+
Base.metadata.create_all(bind=engine)
|
|
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from sqlalchemy.ext.asyncio import
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
6
6
|
from sqlalchemy.orm import Session, sessionmaker
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
from nlbone.core.ports.uow import AsyncUnitOfWork as AsyncUnitOfWorkPort
|
|
9
|
+
from nlbone.core.ports.uow import UnitOfWork
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class SqlAlchemyUnitOfWork(UnitOfWork):
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import redis
|
|
2
|
+
|
|
3
|
+
from nlbone.config.settings import get_settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RedisClient:
|
|
7
|
+
_client: redis.Redis | None = None
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def get_client(cls) -> redis.Redis:
|
|
11
|
+
if cls._client is None:
|
|
12
|
+
cls._client = redis.from_url(
|
|
13
|
+
get_settings().REDIS_URL,
|
|
14
|
+
decode_responses=True
|
|
15
|
+
)
|
|
16
|
+
return cls._client
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def close(cls):
|
|
20
|
+
if cls._client is not None:
|
|
21
|
+
cls._client.close()
|
|
22
|
+
cls._client = None
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import json
|
|
3
4
|
from typing import Any, Optional
|
|
4
5
|
from urllib.parse import urlparse, urlunparse
|
|
@@ -6,8 +7,8 @@ from urllib.parse import urlparse, urlunparse
|
|
|
6
7
|
import httpx
|
|
7
8
|
import requests
|
|
8
9
|
|
|
9
|
-
from nlbone.core.ports.files import FileServicePort
|
|
10
10
|
from nlbone.config.settings import get_settings
|
|
11
|
+
from nlbone.core.ports.files import FileServicePort
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class UploadchiError(RuntimeError):
|
|
@@ -28,8 +29,9 @@ def _auth_headers(token: str | None) -> dict[str, str]:
|
|
|
28
29
|
return {"Authorization": f"Bearer {token}"} if token else {}
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def _build_list_query(
|
|
32
|
-
|
|
32
|
+
def _build_list_query(
|
|
33
|
+
limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
|
|
34
|
+
) -> dict[str, Any]:
|
|
33
35
|
q: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
34
36
|
if filters:
|
|
35
37
|
q["filters"] = json.dumps(filters)
|
|
@@ -55,8 +57,12 @@ def _normalize_https_base(url: str) -> str:
|
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
class UploadchiClient(FileServicePort):
|
|
58
|
-
def __init__(
|
|
59
|
-
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
base_url: Optional[str] = None,
|
|
63
|
+
timeout_seconds: Optional[float] = None,
|
|
64
|
+
client: httpx.Client | None = None,
|
|
65
|
+
) -> None:
|
|
60
66
|
s = get_settings()
|
|
61
67
|
self._base_url = _normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
|
|
62
68
|
self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
|
|
@@ -65,8 +71,9 @@ class UploadchiClient(FileServicePort):
|
|
|
65
71
|
def close(self) -> None:
|
|
66
72
|
self._client.close()
|
|
67
73
|
|
|
68
|
-
def upload_file(
|
|
69
|
-
|
|
74
|
+
def upload_file(
|
|
75
|
+
self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
|
|
76
|
+
) -> dict:
|
|
70
77
|
tok = _resolve_token(token)
|
|
71
78
|
files = {"file": (filename, file_bytes)}
|
|
72
79
|
data = (params or {}).copy()
|
|
@@ -77,13 +84,22 @@ class UploadchiClient(FileServicePort):
|
|
|
77
84
|
|
|
78
85
|
def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
|
|
79
86
|
tok = _resolve_token(token)
|
|
80
|
-
r = self._client.post(
|
|
81
|
-
|
|
87
|
+
r = self._client.post(
|
|
88
|
+
f"{self._base_url}/{file_id}/commit",
|
|
89
|
+
headers=_auth_headers(tok),
|
|
90
|
+
params={"client_id": client_id} if client_id else None,
|
|
91
|
+
)
|
|
82
92
|
if r.status_code not in (204, 200):
|
|
83
93
|
raise UploadchiError(r.status_code, r.text)
|
|
84
94
|
|
|
85
|
-
def list_files(
|
|
86
|
-
|
|
95
|
+
def list_files(
|
|
96
|
+
self,
|
|
97
|
+
limit: int = 10,
|
|
98
|
+
offset: int = 0,
|
|
99
|
+
filters: dict[str, Any] | None = None,
|
|
100
|
+
sort: list[tuple[str, str]] | None = None,
|
|
101
|
+
token: str | None = None,
|
|
102
|
+
) -> dict:
|
|
87
103
|
tok = _resolve_token(token)
|
|
88
104
|
q = _build_list_query(limit, offset, filters, sort)
|
|
89
105
|
r = self._client.get(self._base_url, params=q, headers=_auth_headers(tok))
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator, Optional
|
|
4
|
+
|
|
3
5
|
import httpx
|
|
4
6
|
|
|
5
|
-
from nlbone.core.ports.files import AsyncFileServicePort
|
|
6
7
|
from nlbone.config.settings import get_settings
|
|
8
|
+
from nlbone.core.ports.files import AsyncFileServicePort
|
|
9
|
+
|
|
7
10
|
from .uploadchi import UploadchiError, _auth_headers, _build_list_query, _filename_from_cd, _resolve_token
|
|
8
11
|
|
|
12
|
+
|
|
9
13
|
class UploadchiAsyncClient(AsyncFileServicePort):
|
|
10
|
-
def __init__(
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
base_url: Optional[str] = None,
|
|
17
|
+
timeout_seconds: Optional[float] = None,
|
|
18
|
+
client: httpx.AsyncClient | None = None,
|
|
19
|
+
) -> None:
|
|
11
20
|
s = get_settings()
|
|
12
21
|
self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
|
|
13
22
|
self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
|
|
14
|
-
self._client = client or httpx.AsyncClient(
|
|
23
|
+
self._client = client or httpx.AsyncClient(
|
|
24
|
+
base_url=self._base_url, timeout=self._timeout, follow_redirects=True
|
|
25
|
+
)
|
|
15
26
|
|
|
16
27
|
async def aclose(self) -> None:
|
|
17
28
|
await self._client.aclose()
|
|
18
29
|
|
|
19
|
-
async def upload_file(
|
|
30
|
+
async def upload_file(
|
|
31
|
+
self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
|
|
32
|
+
) -> dict:
|
|
20
33
|
tok = _resolve_token(token)
|
|
21
34
|
files = {"file": (filename, file_bytes)}
|
|
22
35
|
data = (params or {}).copy()
|
|
@@ -27,11 +40,20 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
27
40
|
|
|
28
41
|
async def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
|
|
29
42
|
tok = _resolve_token(token)
|
|
30
|
-
r = await self._client.post(
|
|
43
|
+
r = await self._client.post(
|
|
44
|
+
f"/{file_id}/commit", headers=_auth_headers(tok), params={"client_id": client_id} if client_id else None
|
|
45
|
+
)
|
|
31
46
|
if r.status_code not in (204, 200):
|
|
32
47
|
raise UploadchiError(r.status_code, await r.aread())
|
|
33
48
|
|
|
34
|
-
async def list_files(
|
|
49
|
+
async def list_files(
|
|
50
|
+
self,
|
|
51
|
+
limit: int = 10,
|
|
52
|
+
offset: int = 0,
|
|
53
|
+
filters: dict[str, Any] | None = None,
|
|
54
|
+
sort: list[tuple[str, str]] | None = None,
|
|
55
|
+
token: str | None = None,
|
|
56
|
+
) -> dict:
|
|
35
57
|
tok = _resolve_token(token)
|
|
36
58
|
q = _build_list_query(limit, offset, filters, sort)
|
|
37
59
|
r = await self._client.get("", params=q, headers=_auth_headers(tok))
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from collections import defaultdict
|
|
3
4
|
from typing import Callable, Dict, Iterable, List
|
|
5
|
+
|
|
4
6
|
from nlbone.core.domain.base import DomainEvent
|
|
5
7
|
from nlbone.core.ports.event_bus import EventBusPort
|
|
6
8
|
|
|
9
|
+
|
|
7
10
|
class InMemoryEventBus(EventBusPort):
|
|
8
11
|
def __init__(self) -> None:
|
|
9
12
|
self._handlers: Dict[str, List[Callable[[DomainEvent], None]]] = defaultdict(list)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .connection import get_es_client
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from elasticsearch import Elasticsearch
|
|
2
|
+
|
|
3
|
+
from nlbone.config.settings import get_settings
|
|
4
|
+
|
|
5
|
+
setting = get_settings()
|
|
6
|
+
|
|
7
|
+
def get_es_client():
|
|
8
|
+
es = Elasticsearch(
|
|
9
|
+
setting.ELASTIC_PERCOLATE_URL,
|
|
10
|
+
basic_auth=(setting.ELASTIC_PERCOLATE_USER, setting.ELASTIC_PERCOLATE_PASS.get_secret_value().strip())
|
|
11
|
+
)
|
|
12
|
+
return es
|