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.
- varco_core-0.0.1/PKG-INFO +91 -0
- varco_core-0.0.1/README.md +72 -0
- varco_core-0.0.1/pyproject.toml +30 -0
- varco_core-0.0.1/varco_core/__init__.py +263 -0
- varco_core-0.0.1/varco_core/assembler.py +219 -0
- varco_core-0.0.1/varco_core/auth/__init__.py +23 -0
- varco_core-0.0.1/varco_core/auth/authorizer.py +114 -0
- varco_core-0.0.1/varco_core/auth/base.py +524 -0
- varco_core-0.0.1/varco_core/auth/helpers.py +434 -0
- varco_core-0.0.1/varco_core/authority/__init__.py +105 -0
- varco_core-0.0.1/varco_core/authority/config.py +224 -0
- varco_core-0.0.1/varco_core/authority/exceptions.py +109 -0
- varco_core-0.0.1/varco_core/authority/jwt_authority.py +419 -0
- varco_core-0.0.1/varco_core/authority/multi_key_authority.py +323 -0
- varco_core-0.0.1/varco_core/authority/registry.py +753 -0
- varco_core-0.0.1/varco_core/authority/sources/__init__.py +43 -0
- varco_core-0.0.1/varco_core/authority/sources/authority.py +221 -0
- varco_core-0.0.1/varco_core/authority/sources/factory.py +187 -0
- varco_core-0.0.1/varco_core/authority/sources/jwks_url.py +244 -0
- varco_core-0.0.1/varco_core/authority/sources/oidc.py +232 -0
- varco_core-0.0.1/varco_core/authority/sources/pem_file.py +186 -0
- varco_core-0.0.1/varco_core/authority/sources/pem_folder.py +277 -0
- varco_core-0.0.1/varco_core/authority/sources/protocol.py +120 -0
- varco_core-0.0.1/varco_core/dto/__init__.py +38 -0
- varco_core-0.0.1/varco_core/dto/base.py +161 -0
- varco_core-0.0.1/varco_core/dto/factory.py +389 -0
- varco_core-0.0.1/varco_core/dto/pagination.py +361 -0
- varco_core-0.0.1/varco_core/exception/__init__.py +66 -0
- varco_core-0.0.1/varco_core/exception/codes.py +202 -0
- varco_core-0.0.1/varco_core/exception/http.py +305 -0
- varco_core-0.0.1/varco_core/exception/query.py +112 -0
- varco_core-0.0.1/varco_core/exception/repository.py +131 -0
- varco_core-0.0.1/varco_core/exception/service.py +219 -0
- varco_core-0.0.1/varco_core/jwk/__init__.py +29 -0
- varco_core-0.0.1/varco_core/jwk/builder.py +526 -0
- varco_core-0.0.1/varco_core/jwk/model.py +584 -0
- varco_core-0.0.1/varco_core/jwt/__init__.py +36 -0
- varco_core-0.0.1/varco_core/jwt/builder.py +352 -0
- varco_core-0.0.1/varco_core/jwt/model.py +268 -0
- varco_core-0.0.1/varco_core/jwt/parser.py +271 -0
- varco_core-0.0.1/varco_core/jwt/util.py +297 -0
- varco_core-0.0.1/varco_core/mapper.py +295 -0
- varco_core-0.0.1/varco_core/meta.py +727 -0
- varco_core-0.0.1/varco_core/migrator.py +144 -0
- varco_core-0.0.1/varco_core/model.py +608 -0
- varco_core-0.0.1/varco_core/providers.py +185 -0
- varco_core-0.0.1/varco_core/query/__init__.py +34 -0
- varco_core-0.0.1/varco_core/query/applicator/__init__.py +22 -0
- varco_core-0.0.1/varco_core/query/applicator/applicator.py +121 -0
- varco_core-0.0.1/varco_core/query/applicator/sqlalchemy.py +228 -0
- varco_core-0.0.1/varco_core/query/builder.py +283 -0
- varco_core-0.0.1/varco_core/query/grammar.lark +26 -0
- varco_core-0.0.1/varco_core/query/params.py +81 -0
- varco_core-0.0.1/varco_core/query/parser.py +129 -0
- varco_core-0.0.1/varco_core/query/transformer.py +186 -0
- varco_core-0.0.1/varco_core/query/type.py +227 -0
- varco_core-0.0.1/varco_core/query/visitor/__init__.py +30 -0
- varco_core-0.0.1/varco_core/query/visitor/ast_visitor.py +210 -0
- varco_core-0.0.1/varco_core/query/visitor/query_optimizer.py +117 -0
- varco_core-0.0.1/varco_core/query/visitor/sqlalchemy.py +202 -0
- varco_core-0.0.1/varco_core/query/visitor/type_coercion.py +417 -0
- varco_core-0.0.1/varco_core/query/visitor/walking.py +153 -0
- varco_core-0.0.1/varco_core/registry.py +188 -0
- varco_core-0.0.1/varco_core/repository.py +224 -0
- varco_core-0.0.1/varco_core/service/__init__.py +22 -0
- varco_core-0.0.1/varco_core/service/base.py +943 -0
- varco_core-0.0.1/varco_core/service/soft_delete.py +314 -0
- varco_core-0.0.1/varco_core/service/tenant.py +568 -0
- varco_core-0.0.1/varco_core/service/types.py +287 -0
- varco_core-0.0.1/varco_core/tracing.py +247 -0
- 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
|
+
"""
|