varco-core 0.0.1__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 (71) hide show
  1. varco_core-0.0.1/PKG-INFO +91 -0
  2. varco_core-0.0.1/README.md +72 -0
  3. varco_core-0.0.1/pyproject.toml +30 -0
  4. varco_core-0.0.1/varco_core/__init__.py +263 -0
  5. varco_core-0.0.1/varco_core/assembler.py +219 -0
  6. varco_core-0.0.1/varco_core/auth/__init__.py +23 -0
  7. varco_core-0.0.1/varco_core/auth/authorizer.py +114 -0
  8. varco_core-0.0.1/varco_core/auth/base.py +524 -0
  9. varco_core-0.0.1/varco_core/auth/helpers.py +434 -0
  10. varco_core-0.0.1/varco_core/authority/__init__.py +105 -0
  11. varco_core-0.0.1/varco_core/authority/config.py +224 -0
  12. varco_core-0.0.1/varco_core/authority/exceptions.py +109 -0
  13. varco_core-0.0.1/varco_core/authority/jwt_authority.py +419 -0
  14. varco_core-0.0.1/varco_core/authority/multi_key_authority.py +323 -0
  15. varco_core-0.0.1/varco_core/authority/registry.py +753 -0
  16. varco_core-0.0.1/varco_core/authority/sources/__init__.py +43 -0
  17. varco_core-0.0.1/varco_core/authority/sources/authority.py +221 -0
  18. varco_core-0.0.1/varco_core/authority/sources/factory.py +187 -0
  19. varco_core-0.0.1/varco_core/authority/sources/jwks_url.py +244 -0
  20. varco_core-0.0.1/varco_core/authority/sources/oidc.py +232 -0
  21. varco_core-0.0.1/varco_core/authority/sources/pem_file.py +186 -0
  22. varco_core-0.0.1/varco_core/authority/sources/pem_folder.py +277 -0
  23. varco_core-0.0.1/varco_core/authority/sources/protocol.py +120 -0
  24. varco_core-0.0.1/varco_core/dto/__init__.py +38 -0
  25. varco_core-0.0.1/varco_core/dto/base.py +161 -0
  26. varco_core-0.0.1/varco_core/dto/factory.py +389 -0
  27. varco_core-0.0.1/varco_core/dto/pagination.py +361 -0
  28. varco_core-0.0.1/varco_core/exception/__init__.py +66 -0
  29. varco_core-0.0.1/varco_core/exception/codes.py +202 -0
  30. varco_core-0.0.1/varco_core/exception/http.py +305 -0
  31. varco_core-0.0.1/varco_core/exception/query.py +112 -0
  32. varco_core-0.0.1/varco_core/exception/repository.py +131 -0
  33. varco_core-0.0.1/varco_core/exception/service.py +219 -0
  34. varco_core-0.0.1/varco_core/jwk/__init__.py +29 -0
  35. varco_core-0.0.1/varco_core/jwk/builder.py +526 -0
  36. varco_core-0.0.1/varco_core/jwk/model.py +584 -0
  37. varco_core-0.0.1/varco_core/jwt/__init__.py +36 -0
  38. varco_core-0.0.1/varco_core/jwt/builder.py +352 -0
  39. varco_core-0.0.1/varco_core/jwt/model.py +268 -0
  40. varco_core-0.0.1/varco_core/jwt/parser.py +271 -0
  41. varco_core-0.0.1/varco_core/jwt/util.py +297 -0
  42. varco_core-0.0.1/varco_core/mapper.py +295 -0
  43. varco_core-0.0.1/varco_core/meta.py +727 -0
  44. varco_core-0.0.1/varco_core/migrator.py +144 -0
  45. varco_core-0.0.1/varco_core/model.py +608 -0
  46. varco_core-0.0.1/varco_core/providers.py +185 -0
  47. varco_core-0.0.1/varco_core/query/__init__.py +34 -0
  48. varco_core-0.0.1/varco_core/query/applicator/__init__.py +22 -0
  49. varco_core-0.0.1/varco_core/query/applicator/applicator.py +121 -0
  50. varco_core-0.0.1/varco_core/query/applicator/sqlalchemy.py +228 -0
  51. varco_core-0.0.1/varco_core/query/builder.py +283 -0
  52. varco_core-0.0.1/varco_core/query/grammar.lark +26 -0
  53. varco_core-0.0.1/varco_core/query/params.py +81 -0
  54. varco_core-0.0.1/varco_core/query/parser.py +129 -0
  55. varco_core-0.0.1/varco_core/query/transformer.py +186 -0
  56. varco_core-0.0.1/varco_core/query/type.py +227 -0
  57. varco_core-0.0.1/varco_core/query/visitor/__init__.py +30 -0
  58. varco_core-0.0.1/varco_core/query/visitor/ast_visitor.py +210 -0
  59. varco_core-0.0.1/varco_core/query/visitor/query_optimizer.py +117 -0
  60. varco_core-0.0.1/varco_core/query/visitor/sqlalchemy.py +202 -0
  61. varco_core-0.0.1/varco_core/query/visitor/type_coercion.py +417 -0
  62. varco_core-0.0.1/varco_core/query/visitor/walking.py +153 -0
  63. varco_core-0.0.1/varco_core/registry.py +188 -0
  64. varco_core-0.0.1/varco_core/repository.py +224 -0
  65. varco_core-0.0.1/varco_core/service/__init__.py +22 -0
  66. varco_core-0.0.1/varco_core/service/base.py +943 -0
  67. varco_core-0.0.1/varco_core/service/soft_delete.py +314 -0
  68. varco_core-0.0.1/varco_core/service/tenant.py +568 -0
  69. varco_core-0.0.1/varco_core/service/types.py +287 -0
  70. varco_core-0.0.1/varco_core/tracing.py +247 -0
  71. varco_core-0.0.1/varco_core/uow.py +59 -0
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: varco-core
3
+ Version: 0.0.1
4
+ Summary: Core domain abstractions for varco (models, mapper, meta, query)
5
+ Author: edoardo.scarpaci
6
+ Author-email: edoardo.scarpaci@gmail.com
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Dist: PyJWT (>=2.8,<3.0)
13
+ Requires-Dist: cryptography (>=41.0)
14
+ Requires-Dist: lark (>=1.1)
15
+ Requires-Dist: providify (>=0.1.4a3)
16
+ Requires-Dist: pydantic (>=2.12.5,<3.0.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # varco-core
20
+
21
+ Backend-agnostic domain model and service layer for **varco**.
22
+
23
+ Provides the pure-Python building blocks that all backend packages depend on — no ORM imports at the core layer.
24
+
25
+ ## What lives here
26
+
27
+ | Module | Purpose |
28
+ |---|---|
29
+ | `model.py` | `DomainModel`, `AuditedDomainModel`, `VersionedDomainModel`, `SoftDeleteMixin`, `TenantMixin` and derived classes |
30
+ | `meta.py` | `FieldHint`, `ForeignKey`, `PrimaryKey`, `PKStrategy`, constraints, `pk_field()` |
31
+ | `mapper.py` | `AbstractMapper` — bidirectional ORM ↔ domain translation |
32
+ | `repository.py` | `AsyncRepository` ABC — CRUD + `exists()` + `stream_by_query()` |
33
+ | `uow.py` | `AsyncUnitOfWork` ABC |
34
+ | `registry.py` | `DomainModelRegistry` + `@register` decorator |
35
+ | `providers.py` | `RepositoryProvider` ABC |
36
+ | `assembler.py` | `AbstractDTOAssembler[D, C, R, U]` |
37
+ | `service/base.py` | `AsyncService`, `IUoWProvider` |
38
+ | `service/tenant.py` | `TenantAwareService`, `TenantUoWProvider`, `tenant_context` |
39
+ | `service/soft_delete.py` | `SoftDeleteService` |
40
+ | `service/types.py` | `Assembler` alias, `ServiceProtocol` |
41
+ | `auth/` | `AbstractAuthorizer`, `Action`, `AuthContext`, `ResourceGrant` |
42
+ | `auth/helpers.py` | `GrantBasedAuthorizer`, `OwnershipAuthorizer`, `RoleBasedAuthorizer` |
43
+ | `exception/codes.py` | `FastrestErrorCodes` enum, `ErrorCode` |
44
+ | `exception/http.py` | `ErrorMessage`, `error_message_for`, `register_error_code` |
45
+ | `tracing.py` | `correlation_context`, `current_correlation_id`, `CorrelationIdFilter` |
46
+ | `query/` | `QueryBuilder`, `QueryParams`, `QueryParser`, AST visitors |
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install varco-core
52
+ ```
53
+
54
+ ## Quick start
55
+
56
+ ```python
57
+ from __future__ import annotations
58
+ from typing import Annotated
59
+ from varco_core import AuditedDomainModel
60
+ from varco_core.meta import FieldHint, PrimaryKey, PKStrategy, pk_field
61
+
62
+ class Post(AuditedDomainModel):
63
+ pk: Annotated[int, PrimaryKey(PKStrategy.INT_AUTO)] = pk_field()
64
+ title: Annotated[str, FieldHint(max_length=200)]
65
+ body: str
66
+ published: bool = False
67
+ ```
68
+
69
+ Install `varco-sa` or `varco-beanie` for a concrete backend, then wire a service:
70
+
71
+ ```python
72
+ from varco_core import AsyncService, IUoWProvider
73
+ from varco_core.assembler import AbstractDTOAssembler
74
+ from varco_core.auth import AbstractAuthorizer
75
+ from providify import Inject, Singleton
76
+
77
+ @Singleton
78
+ class PostService(AsyncService[Post, int, CreatePostDTO, PostReadDTO, UpdatePostDTO]):
79
+ def __init__(
80
+ self,
81
+ uow_provider: Inject[IUoWProvider],
82
+ authorizer: Inject[AbstractAuthorizer],
83
+ assembler: Inject[AbstractDTOAssembler[Post, CreatePostDTO, PostReadDTO, UpdatePostDTO]],
84
+ ) -> None:
85
+ super().__init__(uow_provider=uow_provider, authorizer=authorizer, assembler=assembler)
86
+
87
+ def _get_repo(self, uow): return uow.posts
88
+ ```
89
+
90
+ See the [root README](../README.md) for the full documentation.
91
+
@@ -0,0 +1,72 @@
1
+ # varco-core
2
+
3
+ Backend-agnostic domain model and service layer for **varco**.
4
+
5
+ Provides the pure-Python building blocks that all backend packages depend on — no ORM imports at the core layer.
6
+
7
+ ## What lives here
8
+
9
+ | Module | Purpose |
10
+ |---|---|
11
+ | `model.py` | `DomainModel`, `AuditedDomainModel`, `VersionedDomainModel`, `SoftDeleteMixin`, `TenantMixin` and derived classes |
12
+ | `meta.py` | `FieldHint`, `ForeignKey`, `PrimaryKey`, `PKStrategy`, constraints, `pk_field()` |
13
+ | `mapper.py` | `AbstractMapper` — bidirectional ORM ↔ domain translation |
14
+ | `repository.py` | `AsyncRepository` ABC — CRUD + `exists()` + `stream_by_query()` |
15
+ | `uow.py` | `AsyncUnitOfWork` ABC |
16
+ | `registry.py` | `DomainModelRegistry` + `@register` decorator |
17
+ | `providers.py` | `RepositoryProvider` ABC |
18
+ | `assembler.py` | `AbstractDTOAssembler[D, C, R, U]` |
19
+ | `service/base.py` | `AsyncService`, `IUoWProvider` |
20
+ | `service/tenant.py` | `TenantAwareService`, `TenantUoWProvider`, `tenant_context` |
21
+ | `service/soft_delete.py` | `SoftDeleteService` |
22
+ | `service/types.py` | `Assembler` alias, `ServiceProtocol` |
23
+ | `auth/` | `AbstractAuthorizer`, `Action`, `AuthContext`, `ResourceGrant` |
24
+ | `auth/helpers.py` | `GrantBasedAuthorizer`, `OwnershipAuthorizer`, `RoleBasedAuthorizer` |
25
+ | `exception/codes.py` | `FastrestErrorCodes` enum, `ErrorCode` |
26
+ | `exception/http.py` | `ErrorMessage`, `error_message_for`, `register_error_code` |
27
+ | `tracing.py` | `correlation_context`, `current_correlation_id`, `CorrelationIdFilter` |
28
+ | `query/` | `QueryBuilder`, `QueryParams`, `QueryParser`, AST visitors |
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install varco-core
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from __future__ import annotations
40
+ from typing import Annotated
41
+ from varco_core import AuditedDomainModel
42
+ from varco_core.meta import FieldHint, PrimaryKey, PKStrategy, pk_field
43
+
44
+ class Post(AuditedDomainModel):
45
+ pk: Annotated[int, PrimaryKey(PKStrategy.INT_AUTO)] = pk_field()
46
+ title: Annotated[str, FieldHint(max_length=200)]
47
+ body: str
48
+ published: bool = False
49
+ ```
50
+
51
+ Install `varco-sa` or `varco-beanie` for a concrete backend, then wire a service:
52
+
53
+ ```python
54
+ from varco_core import AsyncService, IUoWProvider
55
+ from varco_core.assembler import AbstractDTOAssembler
56
+ from varco_core.auth import AbstractAuthorizer
57
+ from providify import Inject, Singleton
58
+
59
+ @Singleton
60
+ class PostService(AsyncService[Post, int, CreatePostDTO, PostReadDTO, UpdatePostDTO]):
61
+ def __init__(
62
+ self,
63
+ uow_provider: Inject[IUoWProvider],
64
+ authorizer: Inject[AbstractAuthorizer],
65
+ assembler: Inject[AbstractDTOAssembler[Post, CreatePostDTO, PostReadDTO, UpdatePostDTO]],
66
+ ) -> None:
67
+ super().__init__(uow_provider=uow_provider, authorizer=authorizer, assembler=assembler)
68
+
69
+ def _get_repo(self, uow): return uow.posts
70
+ ```
71
+
72
+ See the [root README](../README.md) for the full documentation.
@@ -0,0 +1,30 @@
1
+ [tool.poetry]
2
+ name = "varco-core"
3
+ version = "0.0.1"
4
+ description = "Core domain abstractions for varco (models, mapper, meta, query)"
5
+ authors = ["edoardo.scarpaci <edoardo.scarpaci@gmail.com>"]
6
+ readme = "README.md"
7
+ packages = [{include = "varco_core"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = ">=3.12"
11
+ pydantic = ">=2.12.5,<3.0.0"
12
+ providify = ">=0.1.4a3"
13
+ # lark is used by QueryParser — runtime dependency, not dev-only
14
+ lark = ">=1.1"
15
+ # PyJWT is used by varco_core.jwt for encoding/decoding JWT tokens
16
+ PyJWT = ">=2.8,<3.0"
17
+ # cryptography is used by varco_core.jwk for RSA/EC key operations
18
+ cryptography = ">=41.0"
19
+
20
+ [tool.poetry.group.dev.dependencies]
21
+ pytest = ">=8.0"
22
+ pytest-asyncio = ">=0.24"
23
+
24
+ [build-system]
25
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
26
+ build-backend = "poetry.core.masonry.api"
27
+
28
+ [tool.pytest.ini_options]
29
+ asyncio_mode = "auto"
30
+ testpaths = ["tests"]
@@ -0,0 +1,263 @@
1
+ """
2
+ varco_core
3
+ =============
4
+ Backend-agnostic domain model and query layer.
5
+
6
+ All stable public symbols are importable directly from ``varco_core``::
7
+
8
+ # Domain
9
+ from varco_core import DomainModel, AuditedDomainModel, cast_raw, register
10
+ from varco_core import TenantDomainModel, TenantAuditedDomainModel
11
+ from varco_core import AbstractMapper, AsyncRepository, AsyncUnitOfWork
12
+ from varco_core import DomainModelRegistry, RepositoryProvider
13
+
14
+ # Metadata
15
+ from varco_core.meta import (
16
+ FieldHint, ForeignKey, PrimaryKey, PKStrategy,
17
+ UniqueConstraint, CheckConstraint, pk_field,
18
+ )
19
+
20
+ # Query system
21
+ from varco_core import QueryBuilder, QueryParams, QueryParser
22
+ from varco_core import SortField, SortOrder, Operation
23
+
24
+ # DTOs (API layer)
25
+ from varco_core import CreateDTO, ReadDTO, UpdateDTO, UpdateOperation
26
+
27
+ Sub-package layout
28
+ ------------------
29
+ varco_core/
30
+ ├── model.py — DomainModel, cast_raw
31
+ ├── meta.py — FieldHint, ForeignKey, PKStrategy, constraints
32
+ ├── mapper.py — AbstractMapper (bidirectional ORM ↔ domain)
33
+ ├── registry.py — DomainModelRegistry, @register decorator
34
+ ├── repository.py — AsyncRepository ABC (CRUD + query)
35
+ ├── providers.py — RepositoryProvider ABC + autodiscover
36
+ ├── uow.py — AsyncUnitOfWork ABC
37
+ ├── dto.py — CreateDTO, ReadDTO, UpdateDTO, UpdateOperation
38
+ └── query/
39
+ ├── type.py — AST node types
40
+ ├── builder.py — Fluent QueryBuilder
41
+ ├── params.py — QueryParams value object
42
+ ├── parser.py — QueryParser (string → AST via Lark)
43
+ ├── transformer.py— Lark transformer
44
+ ├── grammar.lark — Query grammar
45
+ ├── visitor/ — ASTVisitor, optimizer, type coercion, SA compiler
46
+ └── applicator/ — QueryApplicator ABC + SA implementation
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ # ── Domain layer ───────────────────────────────────────────────────────────────
52
+ from varco_core.exception.repository import StaleEntityError
53
+ from varco_core.mapper import AbstractMapper
54
+ from varco_core.migrator import DomainMigrator, MigrationError
55
+ from varco_core.model import (
56
+ AuditedDomainModel,
57
+ DomainModel,
58
+ SoftDeleteAuditedDomainModel,
59
+ SoftDeleteDomainModel,
60
+ SoftDeleteMixin,
61
+ TenantAuditedDomainModel,
62
+ TenantDomainModel,
63
+ TenantMixin,
64
+ TenantVersionedDomainModel,
65
+ VersionedDomainModel,
66
+ cast_raw,
67
+ )
68
+ from varco_core.providers import RepositoryProvider
69
+ from varco_core.registry import DomainModelRegistry, register
70
+ from varco_core.repository import AsyncRepository
71
+ from varco_core.uow import AsyncUnitOfWork
72
+
73
+ # ── DTO layer ──────────────────────────────────────────────────────────────────
74
+ from varco_core.dto import (
75
+ CreateDTO,
76
+ ReadDTO,
77
+ TCreateDTO,
78
+ TReadDTO,
79
+ TUpdateDTO,
80
+ UpdateDTO,
81
+ UpdateOperation,
82
+ )
83
+ from varco_core.dto.factory import DTOSet, generate_dtos
84
+ from varco_core.dto.pagination import (
85
+ PageCursor,
86
+ PagedReadDTO,
87
+ SortCursorField,
88
+ paged_response,
89
+ )
90
+
91
+ # ── Query system ───────────────────────────────────────────────────────────────
92
+ from varco_core.query.builder import QueryBuilder
93
+ from varco_core.query.params import QueryParams
94
+ from varco_core.query.parser import QueryParser
95
+ from varco_core.query.type import Operation, SortField, SortOrder
96
+
97
+ # ── Multi-tenancy ───────────────────────────────────────────────────────────────
98
+ from varco_core.service.tenant import (
99
+ TenantAwareService,
100
+ TenantUoWProvider,
101
+ current_tenant,
102
+ tenant_context,
103
+ )
104
+
105
+ # ── Soft delete ─────────────────────────────────────────────────────────────────
106
+ from varco_core.service.soft_delete import SoftDeleteService
107
+
108
+ # ── Service type aliases and protocols ──────────────────────────────────────────
109
+ from varco_core.service.types import Assembler, ServiceProtocol
110
+
111
+ # ── Tracing / correlation ID ────────────────────────────────────────────────────
112
+ from varco_core.tracing import (
113
+ CorrelationIdFilter,
114
+ correlation_context,
115
+ current_correlation_id,
116
+ generate_correlation_id,
117
+ )
118
+
119
+ # ── Auth helpers ─────────────────────────────────────────────────────────────────
120
+ from varco_core.auth.helpers import (
121
+ GrantBasedAuthorizer,
122
+ OwnershipAuthorizer,
123
+ RoleBasedAuthorizer,
124
+ )
125
+
126
+ # ── Error codes and HTTP error mapping ──────────────────────────────────────────
127
+ from varco_core.exception.codes import ErrorCode, FastrestErrorCodes
128
+ from varco_core.exception.http import (
129
+ AnyErrorCode,
130
+ ErrorMessage,
131
+ error_code_for,
132
+ error_message_for,
133
+ register_error_code,
134
+ )
135
+
136
+ # ── JWT layer ──────────────────────────────────────────────────────────────────
137
+ from varco_core.jwt import (
138
+ SYSTEM_ISSUER,
139
+ JsonWebToken,
140
+ JwtBuilder,
141
+ JwtParser,
142
+ JwtUtil,
143
+ )
144
+
145
+ # ── JWK layer ──────────────────────────────────────────────────────────────────
146
+ from varco_core.jwk import (
147
+ JsonWebKey,
148
+ JsonWebKeySet,
149
+ JwkBuilder,
150
+ )
151
+
152
+ # ── Authority layer ─────────────────────────────────────────────────────────────
153
+ from varco_core.authority import (
154
+ AuthorizationConfig,
155
+ AuthorityError,
156
+ AuthoritySource,
157
+ IssuerNotFoundError,
158
+ JwtAuthority,
159
+ KeyLoadError,
160
+ MultiKeyAuthority,
161
+ TrustedIssuerEntry,
162
+ TrustedIssuerRegistry,
163
+ UnknownKidError,
164
+ )
165
+
166
+ __all__ = [
167
+ # ── Domain base ────────────────────────────────────────────────────────────
168
+ "DomainModel",
169
+ "AuditedDomainModel",
170
+ "VersionedDomainModel",
171
+ "TenantMixin",
172
+ "TenantDomainModel",
173
+ "TenantAuditedDomainModel",
174
+ "TenantVersionedDomainModel",
175
+ "cast_raw",
176
+ # ── Soft delete domain mixins ───────────────────────────────────────────────
177
+ "SoftDeleteMixin",
178
+ "SoftDeleteDomainModel",
179
+ "SoftDeleteAuditedDomainModel",
180
+ # ── Migration ──────────────────────────────────────────────────────────────
181
+ "DomainMigrator",
182
+ "MigrationError",
183
+ "StaleEntityError",
184
+ # ── Abstraction layer ──────────────────────────────────────────────────────
185
+ "AbstractMapper",
186
+ "AsyncRepository",
187
+ "AsyncUnitOfWork",
188
+ # ── Registration ───────────────────────────────────────────────────────────
189
+ "DomainModelRegistry",
190
+ "register",
191
+ # ── Provider ABC ───────────────────────────────────────────────────────────
192
+ "RepositoryProvider",
193
+ # ── DTO layer ──────────────────────────────────────────────────────────────
194
+ "CreateDTO",
195
+ "ReadDTO",
196
+ "UpdateDTO",
197
+ "UpdateOperation",
198
+ "TCreateDTO",
199
+ "TReadDTO",
200
+ "TUpdateDTO",
201
+ "DTOSet",
202
+ "generate_dtos",
203
+ # ── Pagination ──────────────────────────────────────────────────────────────
204
+ "SortCursorField",
205
+ "PageCursor",
206
+ "PagedReadDTO",
207
+ "paged_response",
208
+ # ── Query system ───────────────────────────────────────────────────────────
209
+ "QueryBuilder",
210
+ "QueryParams",
211
+ "QueryParser",
212
+ "SortField",
213
+ "SortOrder",
214
+ "Operation",
215
+ # ── Multi-tenancy ───────────────────────────────────────────────────────────
216
+ "TenantAwareService",
217
+ "TenantUoWProvider",
218
+ "current_tenant",
219
+ "tenant_context",
220
+ # ── Soft delete service ─────────────────────────────────────────────────────
221
+ "SoftDeleteService",
222
+ # ── Service type aliases ────────────────────────────────────────────────────
223
+ "Assembler",
224
+ "ServiceProtocol",
225
+ # ── Tracing ─────────────────────────────────────────────────────────────────
226
+ "CorrelationIdFilter",
227
+ "correlation_context",
228
+ "current_correlation_id",
229
+ "generate_correlation_id",
230
+ # ── Auth helpers ─────────────────────────────────────────────────────────────
231
+ "GrantBasedAuthorizer",
232
+ "OwnershipAuthorizer",
233
+ "RoleBasedAuthorizer",
234
+ # ── Error codes and HTTP error mapping ───────────────────────────────────────
235
+ "AnyErrorCode",
236
+ "ErrorCode",
237
+ "FastrestErrorCodes",
238
+ "ErrorMessage",
239
+ "error_code_for",
240
+ "error_message_for",
241
+ "register_error_code",
242
+ # ── JWT layer ───────────────────────────────────────────────────────────────
243
+ "SYSTEM_ISSUER",
244
+ "JsonWebToken",
245
+ "JwtBuilder",
246
+ "JwtParser",
247
+ "JwtUtil",
248
+ # ── JWK layer ───────────────────────────────────────────────────────────────
249
+ "JsonWebKey",
250
+ "JsonWebKeySet",
251
+ "JwkBuilder",
252
+ # ── Authority layer ─────────────────────────────────────────────────────────
253
+ "JwtAuthority",
254
+ "MultiKeyAuthority",
255
+ "TrustedIssuerRegistry",
256
+ "TrustedIssuerEntry",
257
+ "AuthoritySource",
258
+ "AuthorizationConfig",
259
+ "AuthorityError",
260
+ "UnknownKidError",
261
+ "IssuerNotFoundError",
262
+ "KeyLoadError",
263
+ ]
@@ -0,0 +1,219 @@
1
+ """
2
+ varco_core.assembler
3
+ ========================
4
+ Abstract DTO assembler — translates between domain entities and DTOs.
5
+
6
+ The assembler is the **only** layer responsible for:
7
+
8
+ - Mapping a ``CreateDTO`` → fresh ``DomainModel`` (INSERT path).
9
+ - Mapping a persisted ``DomainModel`` → ``ReadDTO`` (response path).
10
+ - Applying an ``UpdateDTO`` to an existing entity → updated entity (UPDATE path).
11
+
12
+ DESIGN: one combined assembler over three separate classes
13
+ ✅ All three mappings concern the same (D, C, R, U) quartet — they are
14
+ inherently cohesive and always deployed together for a given entity.
15
+ ✅ One DI binding per entity type instead of three — simpler container.
16
+ ✅ Consistent with the existing ``AbstractMapper[D, O]`` pattern in
17
+ this codebase (one class handles both translation directions).
18
+ ❌ ``to_read_dto`` cannot be extracted for reuse in a read-only context
19
+ (e.g. a search service) without referencing the full assembler.
20
+ If that use case arises, extract a separate ``AbstractReadAssembler``
21
+ at that point — don't pre-optimise now.
22
+
23
+ DESIGN: assembler is a separate injectable class, not methods on the service
24
+ ✅ Service stays focused on orchestration and authorization — no field-
25
+ level mapping code inside the service.
26
+ ✅ Assembler can be injected via ``Inject[AbstractDTOAssembler]`` and
27
+ swapped in tests without touching the service.
28
+ ✅ Same assembler can be reused across multiple consumers (REST handler,
29
+ CLI, background job) without duplicating mapping logic.
30
+ ❌ One extra class per entity — justified by the clear SRP benefit.
31
+
32
+ Type parameters::
33
+
34
+ D — DomainModel subclass (e.g. ``Post``)
35
+ C — CreateDTO subclass (e.g. ``CreatePostDTO``)
36
+ R — ReadDTO subclass (e.g. ``PostReadDTO``)
37
+ U — UpdateDTO subclass (e.g. ``UpdatePostDTO``)
38
+
39
+ Usage::
40
+
41
+ class PostAssembler(AbstractDTOAssembler[Post, CreatePostDTO, PostReadDTO, UpdatePostDTO]):
42
+
43
+ def to_domain(self, dto: CreatePostDTO) -> Post:
44
+ return Post(title=dto.title, body=dto.body)
45
+
46
+ def to_read_dto(self, entity: Post) -> PostReadDTO:
47
+ return PostReadDTO(
48
+ id=str(entity.pk),
49
+ title=entity.title,
50
+ body=entity.body,
51
+ created_at=entity.created_at,
52
+ updated_at=entity.updated_at,
53
+ )
54
+
55
+ def apply_update(self, entity: Post, dto: UpdatePostDTO) -> Post:
56
+ from dataclasses import replace
57
+ return replace(
58
+ entity,
59
+ title=dto.title if dto.title is not None else entity.title,
60
+ body=dto.body if dto.body is not None else entity.body,
61
+ )
62
+
63
+ Thread safety: ✅ Implementations must be stateless — safe to share as singleton.
64
+ Async safety: ✅ All methods are synchronous — no I/O or awaiting.
65
+ """
66
+
67
+ from __future__ import annotations
68
+
69
+ from abc import ABC, abstractmethod
70
+ from typing import Generic, TypeVar
71
+
72
+ from varco_core.dto import CreateDTO, ReadDTO, UpdateDTO
73
+ from varco_core.model import DomainModel
74
+
75
+ D = TypeVar("D", bound=DomainModel)
76
+ C = TypeVar("C", bound=CreateDTO)
77
+ R = TypeVar("R", bound=ReadDTO)
78
+ U = TypeVar("U", bound=UpdateDTO)
79
+
80
+
81
+ class AbstractDTOAssembler(ABC, Generic[D, C, R, U]):
82
+ """
83
+ Abstract DTO assembler for a single entity/DTO quartet (D, C, R, U).
84
+
85
+ Concrete subclasses implement three translation methods that cover the
86
+ full CRUD lifecycle for one entity type:
87
+
88
+ - ``to_domain`` — ``CreateDTO`` → fresh ``DomainModel`` (INSERT)
89
+ - ``to_read_dto`` — persisted ``DomainModel`` → ``ReadDTO`` (response)
90
+ - ``apply_update`` — ``UpdateDTO`` applied to an existing entity (UPDATE)
91
+
92
+ Thread safety: ✅ Implementations must be stateless — safe to share as
93
+ a singleton across requests and coroutines.
94
+ Async safety: ✅ All methods are synchronous (pure transformations,
95
+ no I/O).
96
+
97
+ Edge cases:
98
+ - ``to_domain`` must return an *unpersisted* entity
99
+ (``entity._raw_orm is None``) — the repository INSERT path
100
+ detects ``_raw_orm is None`` to distinguish INSERT from UPDATE.
101
+ - ``apply_update`` must return a *new* entity — never mutate the
102
+ input. ``dataclasses.replace(entity, ...)`` copies ``_raw_orm``
103
+ automatically (it is a dataclass field) so the repository still
104
+ treats the result as an UPDATE.
105
+ - ``to_read_dto`` is only called on *persisted* entities — ``pk``,
106
+ ``created_at``, and ``updated_at`` are always populated.
107
+ """
108
+
109
+ @abstractmethod
110
+ def to_domain(self, dto: C) -> D:
111
+ """
112
+ Map a ``CreateDTO`` to a fresh, unpersisted domain entity.
113
+
114
+ The returned entity must have ``_raw_orm is None`` so that
115
+ ``repo.save()`` performs an INSERT rather than an UPDATE.
116
+
117
+ Args:
118
+ dto: Validated ``CreateDTO`` payload from the HTTP adapter.
119
+
120
+ Returns:
121
+ A new, unpersisted ``DomainModel`` instance ready for
122
+ ``repo.save()``.
123
+
124
+ Raises:
125
+ ServiceValidationError: Business rule violated by ``dto`` that
126
+ Pydantic could not catch (e.g. a value
127
+ that conflicts with a domain invariant).
128
+
129
+ Edge cases:
130
+ - Do NOT call ``repo.save()`` here — the service handles persistence.
131
+ - Set entity default values here (e.g. ``is_active=True``),
132
+ not in the DTO — keeps the DTO focused on the API contract.
133
+ - Auto-generated fields (``pk``, ``created_at``, ``updated_at``)
134
+ must NOT be set here — the mapper / repository manages them.
135
+
136
+ Example::
137
+
138
+ def to_domain(self, dto: CreatePostDTO) -> Post:
139
+ return Post(title=dto.title, body=dto.body, is_published=False)
140
+ """
141
+
142
+ @abstractmethod
143
+ def to_read_dto(self, entity: D) -> R:
144
+ """
145
+ Map a persisted domain entity to a ``ReadDTO``.
146
+
147
+ Called after every ``save()``, ``find_by_id()``, or
148
+ ``find_by_query()`` to produce the value returned to the caller.
149
+
150
+ Args:
151
+ entity: A persisted ``DomainModel`` (``entity.is_persisted()``
152
+ is ``True`` — ``pk``, ``created_at``, ``updated_at``
153
+ are all populated).
154
+
155
+ Returns:
156
+ A fully populated ``ReadDTO`` instance.
157
+
158
+ Edge cases:
159
+ - ``entity.pk`` may be any type (``UUID``, ``int``, ``str``).
160
+ Always cast to ``str`` for ``ReadDTO.id``.
161
+ - ``entity.created_at`` / ``entity.updated_at`` are ``None``
162
+ on entities that do not extend ``AuditedDomainModel`` — handle
163
+ gracefully if your ``ReadDTO`` declares these as required fields.
164
+
165
+ Example::
166
+
167
+ def to_read_dto(self, entity: Post) -> PostReadDTO:
168
+ return PostReadDTO(
169
+ id=str(entity.pk),
170
+ title=entity.title,
171
+ body=entity.body,
172
+ created_at=entity.created_at,
173
+ updated_at=entity.updated_at,
174
+ )
175
+ """
176
+
177
+ @abstractmethod
178
+ def apply_update(self, entity: D, dto: U) -> D:
179
+ """
180
+ Apply ``dto`` to ``entity`` and return the updated entity.
181
+
182
+ Must return a **new** entity — never mutate ``entity`` in place.
183
+ Use ``dataclasses.replace(entity, field=value)`` to produce a copy
184
+ that preserves ``_raw_orm`` (inherited automatically from ``entity``
185
+ because it is a dataclass field) so the repository performs an
186
+ UPDATE rather than an INSERT.
187
+
188
+ Args:
189
+ entity: The current, persisted state of the entity before the
190
+ update is applied.
191
+ dto: The ``UpdateDTO`` describing what fields to change.
192
+
193
+ Returns:
194
+ A new ``DomainModel`` instance with the update applied.
195
+ ``_raw_orm`` must be inherited from ``entity``.
196
+
197
+ Raises:
198
+ ServiceValidationError: Business rule violated by the combination
199
+ of the current entity state and the dto
200
+ values (e.g. invalid state transition).
201
+ ServiceConflictError: The requested change is not allowed in
202
+ the current entity state.
203
+
204
+ Edge cases:
205
+ - Fields set to ``None`` in ``dto`` conventionally mean
206
+ "no change" — check each field before replacing.
207
+ - Honour ``dto.op`` (REPLACE / EXTEND / REMOVE / MERGE) for
208
+ collection and dict fields where partial updates are supported.
209
+
210
+ Example::
211
+
212
+ def apply_update(self, entity: Post, dto: UpdatePostDTO) -> Post:
213
+ from dataclasses import replace
214
+ return replace(
215
+ entity,
216
+ title=dto.title if dto.title is not None else entity.title,
217
+ body=dto.body if dto.body is not None else entity.body,
218
+ )
219
+ """