seampact-hooks 1.0.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 (39) hide show
  1. seampact_hooks-1.0.0/.gitignore +6 -0
  2. seampact_hooks-1.0.0/LICENSE +21 -0
  3. seampact_hooks-1.0.0/PKG-INFO +13 -0
  4. seampact_hooks-1.0.0/README.md +1 -0
  5. seampact_hooks-1.0.0/pyproject.toml +25 -0
  6. seampact_hooks-1.0.0/src/seampact/hooks/__init__.py +71 -0
  7. seampact_hooks-1.0.0/src/seampact/hooks/_redaction.py +39 -0
  8. seampact_hooks-1.0.0/src/seampact/hooks/authenticate_hook.py +11 -0
  9. seampact_hooks-1.0.0/src/seampact/hooks/authenticate_request.py +25 -0
  10. seampact_hooks-1.0.0/src/seampact/hooks/authenticate_result.py +18 -0
  11. seampact_hooks-1.0.0/src/seampact/hooks/decide_hook.py +11 -0
  12. seampact_hooks-1.0.0/src/seampact/hooks/decision.py +18 -0
  13. seampact_hooks-1.0.0/src/seampact/hooks/error_code_with_params.py +10 -0
  14. seampact_hooks-1.0.0/src/seampact/hooks/field.py +13 -0
  15. seampact_hooks-1.0.0/src/seampact/hooks/field_direction.py +8 -0
  16. seampact_hooks-1.0.0/src/seampact/hooks/idempotency_begin.py +28 -0
  17. seampact_hooks-1.0.0/src/seampact/hooks/idempotency_claim.py +13 -0
  18. seampact_hooks-1.0.0/src/seampact/hooks/idempotency_store.py +22 -0
  19. seampact_hooks-1.0.0/src/seampact/hooks/operation_ref.py +9 -0
  20. seampact_hooks-1.0.0/src/seampact/hooks/purpose.py +26 -0
  21. seampact_hooks-1.0.0/src/seampact/hooks/py.typed +0 -0
  22. seampact_hooks-1.0.0/src/seampact/hooks/request_view.py +19 -0
  23. seampact_hooks-1.0.0/src/seampact/hooks/requirement_kind.py +8 -0
  24. seampact_hooks-1.0.0/src/seampact/hooks/requirement_request.py +43 -0
  25. seampact_hooks-1.0.0/src/seampact/hooks/resolve_scope_hook.py +11 -0
  26. seampact_hooks-1.0.0/src/seampact/hooks/resolve_scope_request.py +25 -0
  27. seampact_hooks-1.0.0/src/seampact/hooks/resolved_scope.py +10 -0
  28. seampact_hooks-1.0.0/src/seampact/hooks/scope_id_source.py +18 -0
  29. seampact_hooks-1.0.0/src/seampact/hooks/scope_resolution.py +19 -0
  30. seampact_hooks-1.0.0/src/seampact/hooks/signature_encoding.py +8 -0
  31. seampact_hooks-1.0.0/src/seampact/hooks/signature_template.py +15 -0
  32. seampact_hooks-1.0.0/src/seampact/hooks/signed_application_request.py +19 -0
  33. seampact_hooks-1.0.0/src/seampact/hooks/signed_request.py +6 -0
  34. seampact_hooks-1.0.0/src/seampact/hooks/signed_template_request.py +21 -0
  35. seampact_hooks-1.0.0/src/seampact/hooks/stored_response.py +22 -0
  36. seampact_hooks-1.0.0/src/seampact/hooks/subject_ref.py +10 -0
  37. seampact_hooks-1.0.0/src/seampact/hooks/throttle_bucket.py +14 -0
  38. seampact_hooks-1.0.0/src/seampact/hooks/unexpected_error_listener.py +13 -0
  39. seampact_hooks-1.0.0/src/seampact/hooks/unexpected_failure.py +12 -0
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ .mypy_cache/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Julien Metral
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.5
2
+ Name: seampact-hooks
3
+ Version: 1.0.0
4
+ Summary: The seampact hook protocols and the values they exchange, for Python apps and generated code.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Typing :: Typed
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ The hook protocols, the values they exchange and `Field` for Python apps, checked against the shared hooks shape.
@@ -0,0 +1 @@
1
+ The hook protocols, the values they exchange and `Field` for Python apps, checked against the shared hooks shape.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling==1.33.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "seampact-hooks"
7
+ version = "1.0.0"
8
+ description = "The seampact hook protocols and the values they exchange, for Python apps and generated code."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ dependencies = []
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Typing :: Typed",
18
+ ]
19
+
20
+ # PEP 420: `seampact` is a namespace shared by every seampact distribution, so it has no `__init__.py`.
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/seampact"]
23
+
24
+ [tool.hatch.build.targets.sdist]
25
+ include = ["src/seampact", "LICENSE"]
@@ -0,0 +1,71 @@
1
+ """The hook protocols an app implements and the values they exchange, with the shape every hooks package shares.
2
+
3
+ A union's arms live in the union's module: `seampact.hooks.decision.Allowed`, `seampact.hooks.purpose.Field`.
4
+ """
5
+
6
+ from seampact.hooks.authenticate_hook import AuthenticateHook
7
+ from seampact.hooks.authenticate_request import AuthenticateRequest
8
+ from seampact.hooks.authenticate_result import AuthenticateResult
9
+ from seampact.hooks.decide_hook import DecideHook
10
+ from seampact.hooks.decision import Decision
11
+ from seampact.hooks.error_code_with_params import ErrorCodeWithParams
12
+ from seampact.hooks.field import Absent, Field
13
+ from seampact.hooks.field_direction import FieldDirection
14
+ from seampact.hooks.idempotency_begin import IdempotencyBegin
15
+ from seampact.hooks.idempotency_claim import IdempotencyClaim
16
+ from seampact.hooks.idempotency_store import IdempotencyStore
17
+ from seampact.hooks.operation_ref import OperationRef
18
+ from seampact.hooks.purpose import Purpose
19
+ from seampact.hooks.request_view import RequestView
20
+ from seampact.hooks.requirement_kind import RequirementKind
21
+ from seampact.hooks.requirement_request import RequirementRequest
22
+ from seampact.hooks.resolve_scope_hook import ResolveScopeHook
23
+ from seampact.hooks.resolve_scope_request import ResolveScopeRequest
24
+ from seampact.hooks.resolved_scope import ResolvedScope
25
+ from seampact.hooks.scope_id_source import ScopeIdSource
26
+ from seampact.hooks.scope_resolution import ScopeResolution
27
+ from seampact.hooks.signature_encoding import SignatureEncoding
28
+ from seampact.hooks.signature_template import SignatureTemplate
29
+ from seampact.hooks.signed_application_request import SignedApplicationRequest
30
+ from seampact.hooks.signed_request import SignedRequest
31
+ from seampact.hooks.signed_template_request import SignedTemplateRequest
32
+ from seampact.hooks.stored_response import StoredResponse
33
+ from seampact.hooks.subject_ref import SubjectRef
34
+ from seampact.hooks.throttle_bucket import ThrottleBucket
35
+ from seampact.hooks.unexpected_error_listener import UnexpectedErrorListener
36
+ from seampact.hooks.unexpected_failure import UnexpectedFailure
37
+
38
+ __all__ = [
39
+ "Absent",
40
+ "AuthenticateHook",
41
+ "AuthenticateRequest",
42
+ "AuthenticateResult",
43
+ "DecideHook",
44
+ "Decision",
45
+ "ErrorCodeWithParams",
46
+ "Field",
47
+ "FieldDirection",
48
+ "IdempotencyBegin",
49
+ "IdempotencyClaim",
50
+ "IdempotencyStore",
51
+ "OperationRef",
52
+ "Purpose",
53
+ "RequestView",
54
+ "RequirementKind",
55
+ "RequirementRequest",
56
+ "ResolveScopeHook",
57
+ "ResolveScopeRequest",
58
+ "ResolvedScope",
59
+ "ScopeIdSource",
60
+ "ScopeResolution",
61
+ "SignatureEncoding",
62
+ "SignatureTemplate",
63
+ "SignedApplicationRequest",
64
+ "SignedRequest",
65
+ "SignedTemplateRequest",
66
+ "StoredResponse",
67
+ "SubjectRef",
68
+ "ThrottleBucket",
69
+ "UnexpectedErrorListener",
70
+ "UnexpectedFailure",
71
+ ]
@@ -0,0 +1,39 @@
1
+ """The redacted rendering of a value that may carry a secret, so a logged or captured object never leaks it."""
2
+
3
+ import dataclasses
4
+ from collections.abc import Iterator
5
+ from typing import Any, ClassVar, Final
6
+
7
+ REDACTED: Final = "[redacted]"
8
+
9
+
10
+ class RedactedRepr:
11
+ """A base for the frozen dataclasses holding a credential or raw bytes: `repr`, `str`, `pprint` and `rich` print
12
+ `[redacted]` in place of each field named in `_REDACTED_FIELDS`. A subclass sets `repr=False` on its dataclass."""
13
+
14
+ __slots__ = ()
15
+
16
+ _REDACTED_FIELDS: ClassVar[frozenset[str]]
17
+ # Set by the subclass's dataclass decorator.
18
+ __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]]
19
+
20
+ def _rendered_fields(self) -> Iterator[tuple[str, object]]:
21
+ for field in dataclasses.fields(self):
22
+ value = getattr(self, field.name)
23
+
24
+ yield field.name, _Redacted() if field.name in self._REDACTED_FIELDS else value
25
+
26
+ def __repr__(self) -> str:
27
+ rendered = ", ".join(f"{name}={value!r}" for name, value in self._rendered_fields())
28
+
29
+ return f"{type(self).__qualname__}({rendered})"
30
+
31
+ def __rich_repr__(self) -> Iterator[tuple[str, object]]:
32
+ yield from self._rendered_fields()
33
+
34
+
35
+ class _Redacted:
36
+ __slots__ = ()
37
+
38
+ def __repr__(self) -> str:
39
+ return REDACTED
@@ -0,0 +1,11 @@
1
+ """The authenticate hook: a plain `def` or an `async def` method satisfies it."""
2
+
3
+ from collections.abc import Awaitable
4
+ from typing import Protocol
5
+
6
+ from seampact.hooks.authenticate_request import AuthenticateRequest
7
+ from seampact.hooks.authenticate_result import AuthenticateResult
8
+
9
+
10
+ class AuthenticateHook(Protocol):
11
+ def authenticate(self, request: AuthenticateRequest, /) -> AuthenticateResult | Awaitable[AuthenticateResult]: ...
@@ -0,0 +1,25 @@
1
+ """What the authenticate hook gets: the identity, its credentials and the request; credentials never show."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import ClassVar
6
+
7
+ from seampact.hooks._redaction import RedactedRepr
8
+ from seampact.hooks.operation_ref import OperationRef
9
+ from seampact.hooks.request_view import RequestView
10
+ from seampact.hooks.signed_request import SignedRequest
11
+
12
+
13
+ @dataclass(frozen=True, slots=True, kw_only=True, repr=False)
14
+ class AuthenticateRequest(RedactedRepr):
15
+ """`credentials` holds what the identity's schemes found in the request; `signature` is set for a signed one."""
16
+
17
+ _REDACTED_FIELDS: ClassVar[frozenset[str]] = frozenset({"credentials", "csrf_token"})
18
+
19
+ identity: str
20
+ credentials: object
21
+ csrf_token: str | None = None
22
+ signature: SignedRequest | None = None
23
+ request: RequestView
24
+ operation: OperationRef
25
+ extensions: Mapping[str, object]
@@ -0,0 +1,18 @@
1
+ """The authenticate hook's answer: the authenticated principal, or a denial with its declared error."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.error_code_with_params import ErrorCodeWithParams
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class Authenticated:
10
+ principal: object
11
+
12
+
13
+ @dataclass(frozen=True, slots=True, kw_only=True)
14
+ class Denied:
15
+ deny: ErrorCodeWithParams
16
+
17
+
18
+ type AuthenticateResult = Authenticated | Denied
@@ -0,0 +1,11 @@
1
+ """The decide hook, which answers each right and rule: a plain `def` or an `async def` method satisfies it."""
2
+
3
+ from collections.abc import Awaitable
4
+ from typing import Protocol
5
+
6
+ from seampact.hooks.decision import Decision
7
+ from seampact.hooks.requirement_request import RequirementRequest
8
+
9
+
10
+ class DecideHook(Protocol):
11
+ def decide(self, request: RequirementRequest, /) -> Decision | Awaitable[Decision]: ...
@@ -0,0 +1,18 @@
1
+ """The decide hook's answer: allowed, or a denial with its declared error."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.error_code_with_params import ErrorCodeWithParams
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class Allowed:
10
+ pass
11
+
12
+
13
+ @dataclass(frozen=True, slots=True, kw_only=True)
14
+ class Denied:
15
+ deny: ErrorCodeWithParams
16
+
17
+
18
+ type Decision = Allowed | Denied
@@ -0,0 +1,10 @@
1
+ """A declared error code and its parameters, the answer of a hook that denies."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True, slots=True, kw_only=True)
8
+ class ErrorCodeWithParams:
9
+ code: str
10
+ params: Mapping[str, object] | None = None
@@ -0,0 +1,13 @@
1
+ """`Field`: a value that may be absent, told apart from an explicit null (`None`) by the `Absent` sentinel."""
2
+
3
+ from enum import Enum
4
+ from typing import Literal
5
+
6
+
7
+ class Absent(Enum):
8
+ """The one value of an absent field."""
9
+
10
+ ABSENT = "absent"
11
+
12
+
13
+ type Field[T] = T | Literal[Absent.ABSENT]
@@ -0,0 +1,8 @@
1
+ """Whether a field right is checked on the way in or on the way out."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class FieldDirection(StrEnum):
7
+ INPUT = "input"
8
+ OUTPUT = "output"
@@ -0,0 +1,28 @@
1
+ """The idempotency store's answer to a claim: started, still running, a fingerprint mismatch, or a replay."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.stored_response import StoredResponse
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class Started:
10
+ pass
11
+
12
+
13
+ @dataclass(frozen=True, slots=True, kw_only=True)
14
+ class Running:
15
+ pass
16
+
17
+
18
+ @dataclass(frozen=True, slots=True, kw_only=True)
19
+ class Mismatch:
20
+ pass
21
+
22
+
23
+ @dataclass(frozen=True, slots=True, kw_only=True)
24
+ class Replay:
25
+ response: StoredResponse
26
+
27
+
28
+ type IdempotencyBegin = Started | Running | Mismatch | Replay
@@ -0,0 +1,13 @@
1
+ """The claim an idempotent request makes on its key: the key, the request's fingerprint and the key's lifetime."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.operation_ref import OperationRef
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class IdempotencyClaim:
10
+ key: str
11
+ fingerprint: str
12
+ lifetime_ms: int
13
+ operation: OperationRef
@@ -0,0 +1,22 @@
1
+ """The store of idempotent requests: plain `def` or `async def` methods satisfy it."""
2
+
3
+ from collections.abc import Awaitable
4
+ from typing import Protocol
5
+
6
+ from seampact.hooks.idempotency_begin import IdempotencyBegin
7
+ from seampact.hooks.idempotency_claim import IdempotencyClaim
8
+ from seampact.hooks.stored_response import StoredResponse
9
+
10
+
11
+ class IdempotencyStore(Protocol):
12
+ def begin(self, claim: IdempotencyClaim, /) -> IdempotencyBegin | Awaitable[IdempotencyBegin]:
13
+ """Claims the key: started for a new key, or running, mismatch or replay for a known one."""
14
+ ...
15
+
16
+ def complete(self, key: str, response: StoredResponse, /) -> Awaitable[None] | None:
17
+ """Keeps the response for the key's replays."""
18
+ ...
19
+
20
+ def abandon(self, key: str, /) -> Awaitable[None] | None:
21
+ """Releases the key of a request that failed, so a retry starts again."""
22
+ ...
@@ -0,0 +1,9 @@
1
+ """The operation a hook call is about: its api and its operation id."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True, kw_only=True)
7
+ class OperationRef:
8
+ api: str
9
+ id: str
@@ -0,0 +1,26 @@
1
+ """Why a requirement is checked: for the route, for one field, or for a sort key."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.field_direction import FieldDirection
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class Route:
10
+ pass
11
+
12
+
13
+ @dataclass(frozen=True, slots=True, kw_only=True)
14
+ class Field:
15
+ """`pointer` is the JSON pointer of the field in the body."""
16
+
17
+ pointer: str
18
+ direction: FieldDirection
19
+
20
+
21
+ @dataclass(frozen=True, slots=True, kw_only=True)
22
+ class SortKey:
23
+ key: str
24
+
25
+
26
+ type Purpose = Route | Field | SortKey
File without changes
@@ -0,0 +1,19 @@
1
+ """What a hook sees of the HTTP request; its headers never show in a repr."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import ClassVar
6
+
7
+ from seampact.hooks._redaction import RedactedRepr
8
+
9
+
10
+ @dataclass(frozen=True, slots=True, kw_only=True, repr=False)
11
+ class RequestView(RedactedRepr):
12
+ """`headers` holds every header by lower-case name, each with its values in order."""
13
+
14
+ _REDACTED_FIELDS: ClassVar[frozenset[str]] = frozenset({"headers"})
15
+
16
+ method: str
17
+ path: str
18
+ headers: Mapping[str, tuple[str, ...]]
19
+ client_address: str | None = None
@@ -0,0 +1,8 @@
1
+ """Whether a requirement is a right or a rule."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class RequirementKind(StrEnum):
7
+ RIGHT = "right"
8
+ RULE = "rule"
@@ -0,0 +1,43 @@
1
+ """What the decide hook gets: the requirement, the principal, the subject, the scopes and the request."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import ClassVar
6
+
7
+ from seampact.hooks._redaction import RedactedRepr
8
+ from seampact.hooks.operation_ref import OperationRef
9
+ from seampact.hooks.purpose import Purpose
10
+ from seampact.hooks.request_view import RequestView
11
+ from seampact.hooks.requirement_kind import RequirementKind
12
+ from seampact.hooks.resolved_scope import ResolvedScope
13
+ from seampact.hooks.subject_ref import SubjectRef
14
+ from seampact.hooks.throttle_bucket import ThrottleBucket
15
+
16
+
17
+ @dataclass(frozen=True, slots=True, kw_only=True, repr=False)
18
+ class RequirementRequest(RedactedRepr):
19
+ """`params` holds the use site's params as an instance of the right's or rule's generated params class, None when
20
+ it declares none; `scopes` are the resolved scopes, outermost first; `principal` is None for an anonymous request.
21
+ The rule proof is sensitive by construction, so it renders redacted."""
22
+
23
+ _REDACTED_FIELDS: ClassVar[frozenset[str]] = frozenset({"proof"})
24
+
25
+ @dataclass(frozen=True, slots=True, kw_only=True)
26
+ class Extensions:
27
+ """The extension values of the requirement and of the operation."""
28
+
29
+ requirement: Mapping[str, object]
30
+ operation: Mapping[str, object]
31
+
32
+ kind: RequirementKind
33
+ name: str
34
+ params: object | None = None
35
+ principal: object | None
36
+ subject: SubjectRef | None = None
37
+ scopes: tuple[ResolvedScope, ...]
38
+ proof: object | None = None
39
+ bucket: ThrottleBucket | None = None
40
+ purpose: Purpose
41
+ operation: OperationRef
42
+ request: RequestView
43
+ extensions: Extensions
@@ -0,0 +1,11 @@
1
+ """The resolve-scope hook: a plain `def` or an `async def` method satisfies it."""
2
+
3
+ from collections.abc import Awaitable
4
+ from typing import Protocol
5
+
6
+ from seampact.hooks.resolve_scope_request import ResolveScopeRequest
7
+ from seampact.hooks.scope_resolution import ScopeResolution
8
+
9
+
10
+ class ResolveScopeHook(Protocol):
11
+ def resolve_scope(self, request: ResolveScopeRequest, /) -> ScopeResolution | Awaitable[ScopeResolution]: ...
@@ -0,0 +1,25 @@
1
+ """What the resolve-scope hook gets: the scope, where its id comes from, the principal and the outer scopes."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+
6
+ from seampact.hooks.resolved_scope import ResolvedScope
7
+ from seampact.hooks.scope_id_source import ScopeIdSource
8
+
9
+
10
+ @dataclass(frozen=True, slots=True, kw_only=True)
11
+ class ResolveScopeRequest:
12
+ """`outer_scopes` are the scopes already resolved, outermost first."""
13
+
14
+ @dataclass(frozen=True, slots=True, kw_only=True)
15
+ class Extensions:
16
+ """The extension values of the scope and of the operation."""
17
+
18
+ scope: Mapping[str, object]
19
+ operation: Mapping[str, object]
20
+
21
+ scope: str
22
+ source: ScopeIdSource
23
+ principal: object | None
24
+ outer_scopes: tuple[ResolvedScope, ...]
25
+ extensions: Extensions
@@ -0,0 +1,10 @@
1
+ """A scope resolved for the request: its name, its id and, when loaded, its value."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True, kw_only=True)
7
+ class ResolvedScope:
8
+ scope: str
9
+ id: object
10
+ value: object | None = None
@@ -0,0 +1,18 @@
1
+ """Where a scope's id comes from: declared in the contract, or resolved by the application from the request."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.request_view import RequestView
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class Declared:
10
+ id: object
11
+
12
+
13
+ @dataclass(frozen=True, slots=True, kw_only=True)
14
+ class Custom:
15
+ request: RequestView
16
+
17
+
18
+ type ScopeIdSource = Declared | Custom
@@ -0,0 +1,19 @@
1
+ """The resolve-scope hook's answer: the resolved scope, or a denial with its declared error."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.error_code_with_params import ErrorCodeWithParams
6
+ from seampact.hooks.resolved_scope import ResolvedScope
7
+
8
+
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Resolved:
11
+ resolved: ResolvedScope
12
+
13
+
14
+ @dataclass(frozen=True, slots=True, kw_only=True)
15
+ class Denied:
16
+ deny: ErrorCodeWithParams
17
+
18
+
19
+ type ScopeResolution = Resolved | Denied
@@ -0,0 +1,8 @@
1
+ """How a signature scheme writes its MAC in the header."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class SignatureEncoding(StrEnum):
7
+ HEX = "hex"
8
+ BASE64 = "base64"
@@ -0,0 +1,15 @@
1
+ """A template signature scheme of the IR (ADR 0013): algorithm, signed payload, encoding and tolerance."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from seampact.hooks.signature_encoding import SignatureEncoding
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class SignatureTemplate:
10
+ """`payload` holds the `{ts}` and `{rawBody}` placeholders; `tolerance_ms` is set when it holds `{ts}`."""
11
+
12
+ algorithm: str
13
+ payload: str
14
+ encoding: SignatureEncoding
15
+ tolerance_ms: int | None = None
@@ -0,0 +1,19 @@
1
+ """A request signed by a scheme the application verifies itself; its body, signature and headers never show."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import ClassVar
6
+
7
+ from seampact.hooks._redaction import RedactedRepr
8
+
9
+
10
+ @dataclass(frozen=True, slots=True, kw_only=True, repr=False)
11
+ class SignedApplicationRequest(RedactedRepr):
12
+ """The decoded body bytes exactly as received, the signature header, and the headers the scheme declares."""
13
+
14
+ _REDACTED_FIELDS: ClassVar[frozenset[str]] = frozenset({"raw_body", "signature_header", "headers"})
15
+
16
+ raw_body: bytes
17
+ signature_header: str
18
+ headers: Mapping[str, str]
19
+ key_id: str | None = None
@@ -0,0 +1,6 @@
1
+ """A signed request: by a template scheme the verifier checks, or by one the application checks."""
2
+
3
+ from seampact.hooks.signed_application_request import SignedApplicationRequest
4
+ from seampact.hooks.signed_template_request import SignedTemplateRequest
5
+
6
+ type SignedRequest = SignedTemplateRequest | SignedApplicationRequest
@@ -0,0 +1,21 @@
1
+ """A request signed by a template scheme, for the verifier; its body and signatures never show in a repr."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from seampact.hooks._redaction import RedactedRepr
7
+ from seampact.hooks.signature_template import SignatureTemplate
8
+
9
+
10
+ @dataclass(frozen=True, slots=True, kw_only=True, repr=False)
11
+ class SignedTemplateRequest(RedactedRepr):
12
+ """The decoded body bytes exactly as received, the signed timestamp text when the format has one, and every
13
+ signature the header carries, still encoded."""
14
+
15
+ _REDACTED_FIELDS: ClassVar[frozenset[str]] = frozenset({"raw_body", "candidate_signatures"})
16
+
17
+ raw_body: bytes
18
+ timestamp: str | None = None
19
+ candidate_signatures: tuple[str, ...]
20
+ key_id: str
21
+ template: SignatureTemplate
@@ -0,0 +1,22 @@
1
+ """The response an idempotency store keeps for a replay; its body and header values never show in a repr."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from seampact.hooks._redaction import RedactedRepr
7
+
8
+
9
+ @dataclass(frozen=True, slots=True, kw_only=True, repr=False)
10
+ class StoredResponse(RedactedRepr):
11
+ _REDACTED_FIELDS: ClassVar[frozenset[str]] = frozenset({"body"})
12
+
13
+ @dataclass(frozen=True, slots=True, kw_only=True, repr=False)
14
+ class Header(RedactedRepr):
15
+ _REDACTED_FIELDS: ClassVar[frozenset[str]] = frozenset({"value"})
16
+
17
+ name: str
18
+ value: str
19
+
20
+ status: int
21
+ headers: tuple[Header, ...]
22
+ body: bytes
@@ -0,0 +1,10 @@
1
+ """The subject a requirement is checked against: its type, its id and, when loaded, its record."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True, kw_only=True)
7
+ class SubjectRef:
8
+ type: str
9
+ id: object
10
+ record: object | None = None
@@ -0,0 +1,14 @@
1
+ """The throttle bucket a requirement counts against."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True, kw_only=True)
7
+ class ThrottleBucket:
8
+ """`key` is a fixed-size digest of the key values; `values` are the values of the key sources in order."""
9
+
10
+ id: str
11
+ key: str
12
+ values: tuple[object, ...]
13
+ limit: int | None = None
14
+ window_ms: int | None = None
@@ -0,0 +1,13 @@
1
+ """The listener the boundary calls for each failure it answers with the api's 500: a plain `def` or an `async def`
2
+ satisfies it."""
3
+
4
+ from collections.abc import Awaitable
5
+ from typing import Protocol
6
+
7
+ from seampact.hooks.unexpected_failure import UnexpectedFailure
8
+
9
+
10
+ class UnexpectedErrorListener(Protocol):
11
+ def on_unexpected_error(self, failure: UnexpectedFailure, /) -> Awaitable[None] | None:
12
+ """Hears of the failure, to report it; the reply is the api's 500 whatever it does."""
13
+ ...
@@ -0,0 +1,12 @@
1
+ """One failure the boundary answered with the api's 500: the api, the operation when one matched, a reason that holds
2
+ no input value, and the exception when one was raised."""
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True, slots=True, kw_only=True)
8
+ class UnexpectedFailure:
9
+ api: str
10
+ operation: str | None
11
+ reason: str
12
+ cause: BaseException | None = None