nlbone 0.3.3__tar.gz → 0.4.0__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 (78) hide show
  1. {nlbone-0.3.3 → nlbone-0.4.0}/PKG-INFO +1 -1
  2. {nlbone-0.3.3 → nlbone-0.4.0}/pyproject.toml +1 -1
  3. nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy/__init__.py +4 -0
  4. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/db/sqlalchemy/engine.py +11 -0
  5. nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy/repository.py +52 -0
  6. nlbone-0.4.0/src/nlbone/adapters/db/sqlalchemy/uow.py +70 -0
  7. nlbone-0.4.0/src/nlbone/adapters/messaging/__init__.py +1 -0
  8. nlbone-0.4.0/src/nlbone/adapters/messaging/event_bus.py +20 -0
  9. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/config/settings.py +7 -0
  10. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/container.py +15 -0
  11. nlbone-0.4.0/src/nlbone/core/application/events.py +16 -0
  12. nlbone-0.4.0/src/nlbone/core/application/use_case.py +10 -0
  13. nlbone-0.4.0/src/nlbone/core/domain/base.py +47 -0
  14. nlbone-0.4.0/src/nlbone/core/ports/__init__.py +5 -0
  15. nlbone-0.4.0/src/nlbone/core/ports/event_bus.py +7 -0
  16. nlbone-0.4.0/src/nlbone/core/ports/repo.py +16 -0
  17. nlbone-0.4.0/src/nlbone/core/ports/uow.py +16 -0
  18. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/dependencies/__init__.py +2 -1
  19. nlbone-0.4.0/src/nlbone/interfaces/api/dependencies/uow.py +31 -0
  20. nlbone-0.3.3/src/nlbone/adapters/db/sqlalchemy/__init__.py +0 -1
  21. nlbone-0.3.3/src/nlbone/core/application/use_cases/register_user.py +0 -0
  22. nlbone-0.3.3/src/nlbone/core/ports/__init__.py +0 -1
  23. nlbone-0.3.3/src/nlbone/core/ports/repo.py +0 -0
  24. nlbone-0.3.3/src/nlbone/interfaces/jobs/__init__.py +0 -0
  25. nlbone-0.3.3/src/nlbone/utils/__init__.py +0 -0
  26. {nlbone-0.3.3 → nlbone-0.4.0}/.gitignore +0 -0
  27. {nlbone-0.3.3 → nlbone-0.4.0}/LICENSE +0 -0
  28. {nlbone-0.3.3 → nlbone-0.4.0}/README.md +0 -0
  29. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/__init__.py +0 -0
  30. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/__init__.py +0 -0
  31. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/auth/__init__.py +0 -0
  32. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/auth/keycloak.py +0 -0
  33. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/db/__init__.py +0 -0
  34. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/db/memory.py +0 -0
  35. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/db/postgres.py +0 -0
  36. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/db/sqlalchemy/base.py +0 -0
  37. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/db/sqlalchemy/query_builder.py +0 -0
  38. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/db/sqlalchemy/schema.py +0 -0
  39. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/http_clients/__init__.py +0 -0
  40. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/http_clients/email_gateway.py +0 -0
  41. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/http_clients/uploadchi.py +0 -0
  42. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/http_clients/uploadchi_async.py +0 -0
  43. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/adapters/messaging/redis.py +0 -0
  44. {nlbone-0.3.3/src/nlbone/adapters/messaging → nlbone-0.4.0/src/nlbone/config}/__init__.py +0 -0
  45. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/config/logging.py +0 -0
  46. {nlbone-0.3.3/src/nlbone/config → nlbone-0.4.0/src/nlbone/core}/__init__.py +0 -0
  47. {nlbone-0.3.3/src/nlbone/core → nlbone-0.4.0/src/nlbone/core/application}/__init__.py +0 -0
  48. {nlbone-0.3.3/src/nlbone/core/application → nlbone-0.4.0/src/nlbone/core/application/services}/__init__.py +0 -0
  49. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/core/application/services.py +0 -0
  50. {nlbone-0.3.3/src/nlbone/core/application/services → nlbone-0.4.0/src/nlbone/core/domain}/__init__.py +0 -0
  51. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/core/domain/events.py +0 -0
  52. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/core/domain/models.py +0 -0
  53. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/core/ports/auth.py +0 -0
  54. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/core/ports/files.py +0 -0
  55. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/core/ports/messaging.py +0 -0
  56. {nlbone-0.3.3/src/nlbone/core/application/use_cases → nlbone-0.4.0/src/nlbone/interfaces}/__init__.py +0 -0
  57. {nlbone-0.3.3/src/nlbone/core/domain → nlbone-0.4.0/src/nlbone/interfaces/api}/__init__.py +0 -0
  58. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/dependencies/auth.py +0 -0
  59. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
  60. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
  61. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/exceptions.py +0 -0
  62. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
  63. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
  64. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
  65. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
  66. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
  67. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
  68. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/routers.py +0 -0
  69. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/api/schemas.py +0 -0
  70. {nlbone-0.3.3/src/nlbone/interfaces → nlbone-0.4.0/src/nlbone/interfaces/cli}/__init__.py +0 -0
  71. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/cli/init_db.py +0 -0
  72. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/cli/main.py +0 -0
  73. {nlbone-0.3.3/src/nlbone/interfaces/api → nlbone-0.4.0/src/nlbone/interfaces/jobs}/__init__.py +0 -0
  74. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
  75. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/types.py +0 -0
  76. {nlbone-0.3.3/src/nlbone/interfaces/cli → nlbone-0.4.0/src/nlbone/utils}/__init__.py +0 -0
  77. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/utils/context.py +0 -0
  78. {nlbone-0.3.3 → nlbone-0.4.0}/src/nlbone/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.3.3
3
+ Version: 0.4.0
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.3.3"
7
+ version = "0.4.0"
8
8
  description = "Backbone package for interfaces and infrastructure in Python projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,4 @@
1
+ from .query_builder import get_paginated_response, apply_pagination
2
+ from .engine import init_sync_engine, init_async_engine, sync_ping, sync_session, async_ping, async_session
3
+ from .repository import SqlAlchemyRepository, AsyncSqlAlchemyRepository
4
+ from .uow import SqlAlchemyUnitOfWork, AsyncSqlAlchemyUnitOfWork
@@ -117,3 +117,14 @@ def sync_ping() -> None:
117
117
  with eng.connect() as conn:
118
118
  conn.execute(text("SELECT 1"))
119
119
 
120
+ def get_async_session_factory() -> async_sessionmaker[AsyncSession]:
121
+ if _async_session_factory is None:
122
+ init_async_engine()
123
+ assert _async_session_factory is not None
124
+ return _async_session_factory
125
+
126
+ def get_sync_session_factory() -> sessionmaker[Session]:
127
+ if _sync_session_factory is None:
128
+ init_sync_engine()
129
+ assert _sync_session_factory is not None
130
+ return _sync_session_factory
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+ from typing import Generic, Iterable, Optional, Type, TypeVar, List
3
+
4
+ from sqlalchemy import select
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from sqlalchemy.orm import Session
7
+ from nlbone.core.ports.repo import Repository, AsyncRepository
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class SqlAlchemyRepository(Repository[T], Generic[T]):
13
+ def __init__(self, session: Session, model: Type[T]) -> None:
14
+ self.session = session
15
+ self.model = model
16
+
17
+ def get(self, id) -> Optional[T]:
18
+ return self.session.get(self.model, id)
19
+
20
+ def add(self, obj: T) -> None:
21
+ self.session.add(obj)
22
+
23
+ def remove(self, obj: T) -> None:
24
+ self.session.delete(obj)
25
+
26
+ def list(self, *, limit: int | None = None, offset: int = 0) -> Iterable[T]:
27
+ q = self.session.query(self.model).offset(offset)
28
+ if limit is not None:
29
+ q = q.limit(limit)
30
+ return q.all()
31
+
32
+
33
+ class AsyncSqlAlchemyRepository(AsyncRepository, Generic[T]):
34
+ def __init__(self, session: AsyncSession, model: Type[T]) -> None:
35
+ self.session = session
36
+ self.model = model
37
+
38
+ async def get(self, id) -> Optional[T]:
39
+ return await self.session.get(self.model, id)
40
+
41
+ def add(self, obj: T) -> None:
42
+ self.session.add(obj)
43
+
44
+ async def remove(self, obj: T) -> None:
45
+ await self.session.delete(obj)
46
+
47
+ async def list(self, *, limit: int | None = None, offset: int = 0) -> List[T]:
48
+ stmt = select(self.model).offset(offset)
49
+ if limit is not None:
50
+ stmt = stmt.limit(limit)
51
+ res = await self.session.execute(stmt)
52
+ return list(res.scalars().all())
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
6
+ from sqlalchemy.orm import Session, sessionmaker
7
+ from nlbone.core.ports.uow import UnitOfWork
8
+ from nlbone.core.ports.uow import AsyncUnitOfWork as AsyncUnitOfWorkPort
9
+
10
+
11
+ class SqlAlchemyUnitOfWork(UnitOfWork):
12
+ """sync UoW for SQLAlchemy."""
13
+
14
+ def __init__(self, session_factory: sessionmaker) -> None:
15
+ self._session_factory = session_factory
16
+ self.session: Session | None = None
17
+
18
+ def __enter__(self) -> "SqlAlchemyUnitOfWork":
19
+ self.session = self._session_factory()
20
+ return self
21
+
22
+ def __exit__(self, exc_type, exc, tb) -> None:
23
+ try:
24
+ if exc_type is None:
25
+ self.commit()
26
+ else:
27
+ self.rollback()
28
+ finally:
29
+ if self.session is not None:
30
+ self.session.close()
31
+ self.session = None
32
+
33
+ def commit(self) -> None:
34
+ if self.session:
35
+ self.session.commit()
36
+
37
+ def rollback(self) -> None:
38
+ if self.session:
39
+ self.session.rollback()
40
+
41
+
42
+ class AsyncSqlAlchemyUnitOfWork(AsyncUnitOfWorkPort):
43
+ """Transactional boundary for async SQLAlchemy."""
44
+
45
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
46
+ self._sf = session_factory
47
+ self.session: Optional[AsyncSession] = None
48
+
49
+ async def __aenter__(self) -> "AsyncSqlAlchemyUnitOfWork":
50
+ self.session = self._sf()
51
+ return self
52
+
53
+ async def __aexit__(self, exc_type, exc, tb) -> None:
54
+ try:
55
+ if exc_type is None:
56
+ await self.commit()
57
+ else:
58
+ await self.rollback()
59
+ finally:
60
+ if self.session is not None:
61
+ await self.session.close()
62
+ self.session = None
63
+
64
+ async def commit(self) -> None:
65
+ if self.session:
66
+ await self.session.commit()
67
+
68
+ async def rollback(self) -> None:
69
+ if self.session:
70
+ await self.session.rollback()
@@ -0,0 +1 @@
1
+ from .event_bus import InMemoryEventBus
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+ from collections import defaultdict
3
+ from typing import Callable, Dict, Iterable, List
4
+ from nlbone.core.domain.base import DomainEvent
5
+ from nlbone.core.ports.event_bus import EventBusPort
6
+
7
+ class InMemoryEventBus(EventBusPort):
8
+ def __init__(self) -> None:
9
+ self._handlers: Dict[str, List[Callable[[DomainEvent], None]]] = defaultdict(list)
10
+
11
+ def publish(self, events: Iterable[DomainEvent]) -> None:
12
+ for evt in events:
13
+ for h in self._handlers.get(evt.name, []):
14
+ try:
15
+ h(evt)
16
+ except Exception:
17
+ pass
18
+
19
+ def subscribe(self, event_name: str, handler: Callable[[DomainEvent], None]) -> None:
20
+ self._handlers[event_name].append(handler)
@@ -66,11 +66,18 @@ class Settings(BaseSettings):
66
66
  POSTGRES_DB_DSN: str = Field(default="postgresql+asyncpg://user:pass@localhost:5432/nlbone",
67
67
  validation_alias=AliasChoices("NLBONE_POSTGRES_DB_DSN",
68
68
  "POSTGRES_DB_DSN", "DATABASE_URL", "DB_DSN"))
69
+ DB_ECHO: bool = Field(default=False)
70
+ DB_POOL_SIZE: int = Field(default=5)
71
+ DB_MAX_OVERFLOW: int = Field(default=10)
69
72
 
70
73
  # ---------------------------
71
74
  # Messaging / Cache
72
75
  # ---------------------------
73
76
  REDIS_URL: str = Field(default="redis://localhost:6379/0")
77
+ # --- Event bus / Outbox ---
78
+ EVENT_BUS_BACKEND: Literal["inmemory"] = Field(default="inmemory")
79
+ OUTBOX_ENABLED: bool = Field(default=False)
80
+ OUTBOX_POLL_INTERVAL_MS: int = Field(default=500)
74
81
 
75
82
  # ---------------------------
76
83
  # UPLOADCHI
@@ -3,15 +3,30 @@ from typing import Any, Mapping, Optional
3
3
 
4
4
  from dependency_injector import containers, providers
5
5
 
6
+ from nlbone.adapters.db.sqlalchemy import SqlAlchemyUnitOfWork, AsyncUnitOfWork
7
+ from nlbone.adapters.db.sqlalchemy.engine import get_sync_session_factory, get_async_session_factory
6
8
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
7
9
  from nlbone.adapters.http_clients.uploadchi_async import UploadchiAsyncClient
8
10
  from nlbone.adapters.auth.keycloak import KeycloakAuthService
11
+ from nlbone.adapters.messaging import InMemoryEventBus
12
+ from nlbone.core.ports import EventBusPort
9
13
  from nlbone.core.ports.files import FileServicePort, AsyncFileServicePort
10
14
 
11
15
 
12
16
  class Container(containers.DeclarativeContainer):
13
17
  config = providers.Configuration(strict=False)
14
18
 
19
+ sync_session_factory = providers.Singleton(get_sync_session_factory)
20
+ async_session_factory = providers.Singleton(get_async_session_factory)
21
+
22
+ # --- UoW ---
23
+ uow = providers.Factory(SqlAlchemyUnitOfWork, session_factory=sync_session_factory)
24
+ async_uow = providers.Factory(AsyncUnitOfWork, session_factory=async_session_factory)
25
+
26
+ # --- Event bus ---
27
+ event_bus: providers.Singleton[EventBusPort] = providers.Singleton(InMemoryEventBus)
28
+
29
+ # --- Services ---
15
30
  auth: providers.Singleton[KeycloakAuthService] = providers.Singleton(KeycloakAuthService, settings=config)
16
31
  file_service: providers.Singleton[FileServicePort] = providers.Singleton(UploadchiClient)
17
32
  afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(UploadchiAsyncClient)
@@ -0,0 +1,16 @@
1
+ from typing import Iterable, Sequence
2
+ from nlbone.core.domain.base import AggregateRoot, DomainEvent
3
+ from nlbone.core.ports.event_bus import EventBusPort
4
+
5
+ def collect_events(*aggregates: Iterable[AggregateRoot]) -> list[DomainEvent]:
6
+ events: list[DomainEvent] = []
7
+ for agg in aggregates:
8
+ if isinstance(agg, AggregateRoot):
9
+ events.extend(agg.pull_events())
10
+ else:
11
+ for a in agg:
12
+ events.extend(a.pull_events())
13
+ return events
14
+
15
+ def publish_events(bus: EventBusPort, events: Sequence[DomainEvent]) -> None:
16
+ if events: bus.publish(events)
@@ -0,0 +1,10 @@
1
+ from typing import Protocol, TypeVar
2
+
3
+
4
+ InT = TypeVar("InT"); OutT = TypeVar("OutT")
5
+
6
+ class UseCase(Protocol):
7
+ def __call__(self, command: InT) -> OutT: ...
8
+
9
+ class AsyncUseCase(Protocol):
10
+ async def __call__(self, command: InT) -> OutT: ...
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Generic, List, TypeVar
5
+
6
+ TId = TypeVar("TId")
7
+
8
+
9
+ class DomainError(Exception):
10
+ """Base domain exception."""
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class DomainEvent:
15
+ """Immutable domain event."""
16
+ occurred_at: datetime = datetime.now(timezone.utc)
17
+
18
+ @property
19
+ def name(self):
20
+ return self.__class__.__name__
21
+
22
+
23
+ class ValueObject:
24
+ """Base for value objects (immutable in practice)."""
25
+ def __eq__(self, other: Any) -> bool:
26
+ return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
27
+
28
+ def __hash__(self) -> int: # allow in sets/dicts
29
+ return hash(tuple(sorted(self.__dict__.items())))
30
+
31
+
32
+ class Entity(Generic[TId]):
33
+ id: TId
34
+
35
+
36
+ class AggregateRoot(Entity[TId]):
37
+ """Aggregate root with domain event collection."""
38
+ def __init__(self, *args, **kwargs) -> None:
39
+ self._domain_events: List[DomainEvent] = []
40
+
41
+ def _raise(self, event: DomainEvent) -> None:
42
+ self._domain_events.append(event)
43
+
44
+ def pull_events(self) -> List[DomainEvent]:
45
+ events = list(self._domain_events)
46
+ self._domain_events.clear()
47
+ return events
@@ -0,0 +1,5 @@
1
+ from .auth import AuthService
2
+ from .repo import Repository, AsyncRepository
3
+ from .files import FileServicePort, AsyncFileServicePort
4
+ from .uow import UnitOfWork, AsyncUnitOfWork
5
+ from .event_bus import EventBusPort
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+ from typing import Callable, Iterable, Protocol
3
+ from nlbone.core.domain.base import DomainEvent
4
+
5
+ class EventBusPort(Protocol):
6
+ def publish(self, events: Iterable[DomainEvent]) -> None: ...
7
+ def subscribe(self, event_name: str, handler: Callable[[DomainEvent], None]) -> None: ...
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+ from typing import Iterable, Optional, Protocol, TypeVar, List
3
+
4
+ T = TypeVar("T")
5
+
6
+ class Repository(Protocol[T]): # ← نه Protocol, Generic[T]
7
+ def get(self, id) -> Optional[T]: ...
8
+ def add(self, obj: T) -> None: ...
9
+ def remove(self, obj: T) -> None: ...
10
+ def list(self, *, limit: int | None = None, offset: int = 0) -> Iterable[T]: ...
11
+
12
+ class AsyncRepository(Protocol[T]):
13
+ async def get(self, id) -> Optional[T]: ...
14
+ def add(self, obj: T) -> None: ...
15
+ async def remove(self, obj: T) -> None: ...
16
+ async def list(self, *, limit: int | None = None, offset: int = 0) -> List[T]: ...
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+ from typing import Protocol, runtime_checkable
3
+
4
+ @runtime_checkable
5
+ class UnitOfWork(Protocol):
6
+ def __enter__(self) -> "UnitOfWork": ...
7
+ def __exit__(self, exc_type, exc, tb) -> None: ...
8
+ def commit(self) -> None: ...
9
+ def rollback(self) -> None: ...
10
+
11
+ @runtime_checkable
12
+ class AsyncUnitOfWork(Protocol):
13
+ async def __aenter__(self) -> "AsyncUnitOfWork": ...
14
+ async def __aexit__(self, exc_type, exc, tb) -> None: ...
15
+ async def commit(self) -> None: ...
16
+ async def rollback(self) -> None: ...
@@ -1,2 +1,3 @@
1
1
  from .db import get_session, get_async_session
2
- from .auth import has_access, client_has_access, current_client_id, current_user_id, current_request, user_authenticated
2
+ from .auth import has_access, client_has_access, current_client_id, current_user_id, current_request, user_authenticated
3
+ from .uow import get_uow, get_async_uow
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+ from collections.abc import Iterator
3
+ from typing import AsyncIterator
4
+
5
+ from fastapi import Request
6
+
7
+ from nlbone.adapters.db.sqlalchemy import AsyncSqlAlchemyUnitOfWork
8
+ from nlbone.core.ports.uow import UnitOfWork, AsyncUnitOfWork
9
+
10
+
11
+ def get_uow(request: Request) -> Iterator[UnitOfWork]:
12
+ """
13
+ Uses DI container mounted at app.state.container to create a UoW per request.
14
+ Assumes container.uow is a provider returning SqlAlchemyUnitOfWork(session_factory).
15
+ """
16
+ container = getattr(request.app.state, "container", None)
17
+ if container is None or not hasattr(container, "uow"):
18
+ raise RuntimeError("Container with 'uow' provider not configured on app.state.container")
19
+
20
+ uow = container.uow()
21
+ with uow as _uow:
22
+ yield _uow
23
+
24
+
25
+ async def get_async_uow(request: Request) -> AsyncIterator[AsyncUnitOfWork]:
26
+ container = getattr(request.app.state, "container", None)
27
+ if container is None or not hasattr(container, "async_uow"):
28
+ raise RuntimeError("Container.async_uow provider not configured")
29
+ uow: AsyncSqlAlchemyUnitOfWork = container.async_uow()
30
+ async with uow as _uow:
31
+ yield _uow
@@ -1 +0,0 @@
1
- from .query_builder import get_paginated_response, apply_pagination
@@ -1 +0,0 @@
1
- from .auth import AuthService
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes