repost-client 0.1.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.
- repost_client-0.1.0/.gitignore +4 -0
- repost_client-0.1.0/PKG-INFO +6 -0
- repost_client-0.1.0/fixture/repost_sdk/__init__.py +20 -0
- repost_client-0.1.0/fixture/repost_sdk/client.py +54 -0
- repost_client-0.1.0/fixture/repost_sdk/descriptors.py +55 -0
- repost_client-0.1.0/fixture/repost_sdk/models.py +52 -0
- repost_client-0.1.0/fixture/repost_sdk/schema.repost +52 -0
- repost_client-0.1.0/pyproject.toml +23 -0
- repost_client-0.1.0/src/repost_client/__init__.py +57 -0
- repost_client-0.1.0/src/repost_client/_cuid2.py +25 -0
- repost_client-0.1.0/src/repost_client/_generators.py +59 -0
- repost_client-0.1.0/src/repost_client/_sentinel.py +35 -0
- repost_client-0.1.0/src/repost_client/_types.py +117 -0
- repost_client-0.1.0/src/repost_client/errors.py +12 -0
- repost_client-0.1.0/src/repost_client/py.typed +0 -0
- repost_client-0.1.0/src/repost_client/serialize.py +293 -0
- repost_client-0.1.0/src/repost_client/transport.py +270 -0
- repost_client-0.1.0/src/repost_client/version.py +43 -0
- repost_client-0.1.0/src/repost_client/webhooks.py +128 -0
- repost_client-0.1.0/tests/test_conformance.py +247 -0
- repost_client-0.1.0/tests/test_cuid2.py +30 -0
- repost_client-0.1.0/tests/test_generated_fixture.py +50 -0
- repost_client-0.1.0/tests/test_live_publish.py +74 -0
- repost_client-0.1.0/tests/test_serialize.py +161 -0
- repost_client-0.1.0/tests/test_transport.py +220 -0
- repost_client-0.1.0/tests/test_version.py +27 -0
- repost_client-0.1.0/tests/test_webhooks.py +211 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Generated by Repost from your `.repost` schema — do not edit.
|
|
2
|
+
# Regenerate with `repost generate`.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from .client import RepostClient
|
|
7
|
+
from .models import (
|
|
8
|
+
Author,
|
|
9
|
+
Book,
|
|
10
|
+
Currency,
|
|
11
|
+
DeletedBook,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Author",
|
|
16
|
+
"Book",
|
|
17
|
+
"Currency",
|
|
18
|
+
"DeletedBook",
|
|
19
|
+
"RepostClient",
|
|
20
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Generated by Repost from your `.repost` schema — do not edit.
|
|
2
|
+
# Regenerate with `repost generate`.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from repost_client import (
|
|
7
|
+
ClientOptions,
|
|
8
|
+
SendResult,
|
|
9
|
+
assert_descriptor_version,
|
|
10
|
+
send,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .descriptors import SCHEMA
|
|
14
|
+
from .models import Book, DeletedBook
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BookWebhooks:
|
|
18
|
+
"""Sends the "book" group's events."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, options: ClientOptions) -> None:
|
|
21
|
+
self._options = options
|
|
22
|
+
|
|
23
|
+
def created(self, *, customer_id: str, data: Book, idempotency_key: str | None = None) -> SendResult:
|
|
24
|
+
"""A book was created"""
|
|
25
|
+
return send(self._options, SCHEMA, "book", "created", customer_id=customer_id, data=data, idempotency_key=idempotency_key)
|
|
26
|
+
|
|
27
|
+
def updated(self, *, customer_id: str, data: Book, idempotency_key: str | None = None) -> SendResult:
|
|
28
|
+
"""Sends a "book.updated" event."""
|
|
29
|
+
return send(self._options, SCHEMA, "book", "updated", customer_id=customer_id, data=data, idempotency_key=idempotency_key)
|
|
30
|
+
|
|
31
|
+
def deleted(self, *, customer_id: str, data: DeletedBook, idempotency_key: str | None = None) -> SendResult:
|
|
32
|
+
"""Sends a "book.deleted" event."""
|
|
33
|
+
return send(self._options, SCHEMA, "book", "deleted", customer_id=customer_id, data=data, idempotency_key=idempotency_key)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Webhooks:
|
|
37
|
+
"""The catalog method tree: one attribute per catalog with at least
|
|
38
|
+
one bound member."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, options: ClientOptions) -> None:
|
|
41
|
+
self.book = BookWebhooks(options)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RepostClient:
|
|
45
|
+
"""The Repost client generated from your schema. The typed send
|
|
46
|
+
methods live under ``.webhooks``."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, options: ClientOptions | None = None) -> None:
|
|
49
|
+
# The descriptor-format handshake with the runtime package runs
|
|
50
|
+
# here, so a version mismatch fails at construction — never
|
|
51
|
+
# silently at send time.
|
|
52
|
+
assert_descriptor_version(2)
|
|
53
|
+
self._options = options if options is not None else ClientOptions()
|
|
54
|
+
self.webhooks = Webhooks(self._options)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Generated by Repost from your `.repost` schema — do not edit.
|
|
2
|
+
# Regenerate with `repost generate`.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from repost_client import (
|
|
7
|
+
EventDescriptor,
|
|
8
|
+
FieldDescriptor,
|
|
9
|
+
ModelDescriptor,
|
|
10
|
+
SchemaDescriptor,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# SCHEMA is the complete descriptor set handed to the runtime on every
|
|
14
|
+
# send: the per-model serialization descriptors (field order, `@map` wire
|
|
15
|
+
# names, `@default` injection specs), the catalog method tree, and the
|
|
16
|
+
# descriptor-format version this package was emitted for (verified by the
|
|
17
|
+
# `RepostClient` construction-time handshake).
|
|
18
|
+
SCHEMA = SchemaDescriptor(
|
|
19
|
+
descriptor_format_version=2,
|
|
20
|
+
enums={
|
|
21
|
+
"Currency": {
|
|
22
|
+
"USD": "USD",
|
|
23
|
+
"EUR": "EUR",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
models={
|
|
27
|
+
"Author": ModelDescriptor(
|
|
28
|
+
fields=(
|
|
29
|
+
FieldDescriptor(schema_name="name", wire_name="name", scalar_kind="string", required_in_input=True, nullable_in_input=False, list=False, default=None),
|
|
30
|
+
FieldDescriptor(schema_name="email", wire_name="email", scalar_kind="string", required_in_input=False, nullable_in_input=True, list=False, default=None),
|
|
31
|
+
)
|
|
32
|
+
),
|
|
33
|
+
"Book": ModelDescriptor(
|
|
34
|
+
fields=(
|
|
35
|
+
FieldDescriptor(schema_name="title", wire_name="title", scalar_kind="string", required_in_input=True, nullable_in_input=False, list=False, default=None),
|
|
36
|
+
FieldDescriptor(schema_name="authors", wire_name="authors", scalar_kind="model", descriptor_id="Author", required_in_input=True, nullable_in_input=False, list=True, default=None),
|
|
37
|
+
FieldDescriptor(schema_name="price", wire_name="price", scalar_kind="float64", required_in_input=True, nullable_in_input=False, list=False, default=None),
|
|
38
|
+
FieldDescriptor(schema_name="currency", wire_name="currency", scalar_kind="enum", descriptor_id="Currency", required_in_input=True, nullable_in_input=False, list=False, default=None),
|
|
39
|
+
FieldDescriptor(schema_name="subtitle", wire_name="subtitle", scalar_kind="string", required_in_input=False, nullable_in_input=True, list=False, default=None),
|
|
40
|
+
)
|
|
41
|
+
),
|
|
42
|
+
"DeletedBook": ModelDescriptor(
|
|
43
|
+
fields=(
|
|
44
|
+
FieldDescriptor(schema_name="id", wire_name="id", scalar_kind="string", required_in_input=True, nullable_in_input=False, list=False, default=None),
|
|
45
|
+
)
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
webhooks={
|
|
49
|
+
"book": {
|
|
50
|
+
"created": EventDescriptor(type="book.created", model="Book"),
|
|
51
|
+
"updated": EventDescriptor(type="book.updated", model="Book"),
|
|
52
|
+
"deleted": EventDescriptor(type="book.deleted", model="DeletedBook"),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Generated by Repost from your `.repost` schema — do not edit.
|
|
2
|
+
# Regenerate with `repost generate`.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import enum
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from repost_client import UNSET, UnsetType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Currency(str, enum.Enum):
|
|
14
|
+
USD = "USD"
|
|
15
|
+
EUR = "EUR"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(kw_only=True)
|
|
19
|
+
class Author:
|
|
20
|
+
__repost_fields__: ClassVar[dict[str, str]] = {
|
|
21
|
+
"name": "name",
|
|
22
|
+
"email": "email",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
email: str | None | UnsetType = UNSET
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(kw_only=True)
|
|
30
|
+
class Book:
|
|
31
|
+
__repost_fields__: ClassVar[dict[str, str]] = {
|
|
32
|
+
"title": "title",
|
|
33
|
+
"authors": "authors",
|
|
34
|
+
"price": "price",
|
|
35
|
+
"currency": "currency",
|
|
36
|
+
"subtitle": "subtitle",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
title: str
|
|
40
|
+
authors: list[Author]
|
|
41
|
+
price: float
|
|
42
|
+
currency: Currency
|
|
43
|
+
subtitle: str | None | UnsetType = UNSET
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(kw_only=True)
|
|
47
|
+
class DeletedBook:
|
|
48
|
+
__repost_fields__: ClassVar[dict[str, str]] = {
|
|
49
|
+
"id": "id",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
id: str
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// --- canonical.repost
|
|
2
|
+
generator sdk {
|
|
3
|
+
language = "python"
|
|
4
|
+
output = "../generated/python"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
enum Currency {
|
|
8
|
+
USD
|
|
9
|
+
EUR
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Book {
|
|
13
|
+
/// A book was created
|
|
14
|
+
created
|
|
15
|
+
updated
|
|
16
|
+
deleted
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
model Author {
|
|
20
|
+
name String
|
|
21
|
+
email String?
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
model Book {
|
|
25
|
+
title String
|
|
26
|
+
authors Author[]
|
|
27
|
+
price Float
|
|
28
|
+
currency Currency
|
|
29
|
+
subtitle String?
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
model DeletedBook {
|
|
33
|
+
id String
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
event BookCreated {
|
|
37
|
+
type @type(Book.created)
|
|
38
|
+
data Book
|
|
39
|
+
timestamp DateTime
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
event BookUpdated {
|
|
43
|
+
type @type(Book.updated)
|
|
44
|
+
data Book
|
|
45
|
+
timestamp DateTime
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
event BookDeleted {
|
|
49
|
+
type @type(Book.deleted)
|
|
50
|
+
data DeletedBook
|
|
51
|
+
timestamp DateTime
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "repost-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Repost webhook client runtime — typed senders generated from your .repost schema"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["httpx>=0.27"]
|
|
11
|
+
|
|
12
|
+
[tool.hatch.build.targets.wheel]
|
|
13
|
+
packages = ["src/repost_client"]
|
|
14
|
+
|
|
15
|
+
[tool.pyright]
|
|
16
|
+
include = ["src", "tests", "fixture"]
|
|
17
|
+
extraPaths = ["fixture"]
|
|
18
|
+
typeCheckingMode = "strict"
|
|
19
|
+
venvPath = "."
|
|
20
|
+
venv = ".venv"
|
|
21
|
+
|
|
22
|
+
[tool.pytest.ini_options]
|
|
23
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Repost webhook client runtime — typed senders generated from your
|
|
2
|
+
``.repost`` schema.
|
|
3
|
+
|
|
4
|
+
Generated clients are data + types only; every behavior (serialization,
|
|
5
|
+
envelopes, retries, idempotency) lives here, pinned by
|
|
6
|
+
``sdk/conformance/CONTRACT.md`` and the ``sdk/conformance`` vectors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ._sentinel import UNSET, UnsetType
|
|
12
|
+
from ._types import (
|
|
13
|
+
DefaultSpec,
|
|
14
|
+
EventDescriptor,
|
|
15
|
+
FieldDescriptor,
|
|
16
|
+
Generators,
|
|
17
|
+
ModelDescriptor,
|
|
18
|
+
SchemaDescriptor,
|
|
19
|
+
SendResult,
|
|
20
|
+
)
|
|
21
|
+
from .errors import DescriptorVersionError
|
|
22
|
+
from .serialize import serialize_model
|
|
23
|
+
from .transport import (
|
|
24
|
+
Envelope,
|
|
25
|
+
HTTPTransportOptions,
|
|
26
|
+
PublishError,
|
|
27
|
+
SendRequest,
|
|
28
|
+
Transport,
|
|
29
|
+
http_transport,
|
|
30
|
+
)
|
|
31
|
+
from .version import DESCRIPTOR_FORMAT_VERSION, assert_descriptor_version
|
|
32
|
+
from .webhooks import ClientOptions, StubTransport, send
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"DESCRIPTOR_FORMAT_VERSION",
|
|
36
|
+
"UNSET",
|
|
37
|
+
"ClientOptions",
|
|
38
|
+
"DefaultSpec",
|
|
39
|
+
"DescriptorVersionError",
|
|
40
|
+
"Envelope",
|
|
41
|
+
"EventDescriptor",
|
|
42
|
+
"FieldDescriptor",
|
|
43
|
+
"Generators",
|
|
44
|
+
"HTTPTransportOptions",
|
|
45
|
+
"ModelDescriptor",
|
|
46
|
+
"PublishError",
|
|
47
|
+
"SchemaDescriptor",
|
|
48
|
+
"SendRequest",
|
|
49
|
+
"SendResult",
|
|
50
|
+
"StubTransport",
|
|
51
|
+
"Transport",
|
|
52
|
+
"UnsetType",
|
|
53
|
+
"assert_descriptor_version",
|
|
54
|
+
"http_transport",
|
|
55
|
+
"send",
|
|
56
|
+
"serialize_model",
|
|
57
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Vendored cuid2-shaped id generator.
|
|
2
|
+
|
|
3
|
+
Generates ids matching the documented cuid2 *shape* — 24 characters, a
|
|
4
|
+
lowercase letter first, the rest lowercase base36 — from :mod:`secrets`
|
|
5
|
+
entropy. Shape only, not the reference algorithm's session counters and
|
|
6
|
+
fingerprints; vendored so the runtime carries zero id-generation
|
|
7
|
+
dependencies (SDK distribution decision §3).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import secrets
|
|
13
|
+
|
|
14
|
+
LENGTH = 24
|
|
15
|
+
"""The number of characters in a generated id."""
|
|
16
|
+
|
|
17
|
+
_LETTERS = "abcdefghijklmnopqrstuvwxyz"
|
|
18
|
+
_BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cuid() -> str:
|
|
22
|
+
"""Return a cuid2-shaped id: 24 chars, lowercase letter first, base36."""
|
|
23
|
+
first = secrets.choice(_LETTERS)
|
|
24
|
+
rest = "".join(secrets.choice(_BASE36) for _ in range(LENGTH - 1))
|
|
25
|
+
return first + rest
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Default clock and id generators, plus the resolution helper that merges
|
|
2
|
+
a user :class:`~repost_client.Generators` over them."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import uuid as _uuid
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from . import _cuid2
|
|
12
|
+
from ._types import Generators
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_timestamp(value: datetime) -> str:
|
|
16
|
+
"""Format a datetime as the contract's timestamp canon.
|
|
17
|
+
|
|
18
|
+
``YYYY-MM-DDTHH:MM:SS.mmmZ`` — millisecond precision, ``Z`` suffix, in
|
|
19
|
+
UTC (the JS ``toISOString()`` shape). Aware datetimes are converted to
|
|
20
|
+
UTC; naive datetimes are assumed UTC.
|
|
21
|
+
"""
|
|
22
|
+
if value.tzinfo is None:
|
|
23
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
24
|
+
value = value.astimezone(timezone.utc)
|
|
25
|
+
return value.strftime("%Y-%m-%dT%H:%M:%S.") + f"{value.microsecond // 1000:03d}Z"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_now() -> str:
|
|
29
|
+
"""The default clock: the current UTC time in the timestamp canon."""
|
|
30
|
+
return format_timestamp(datetime.now(timezone.utc))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def default_uuid() -> str:
|
|
34
|
+
"""The default UUID source: a random UUIDv4."""
|
|
35
|
+
return str(_uuid.uuid4())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_cuid() -> str:
|
|
39
|
+
"""The default cuid source: the vendored cuid2-shape generator."""
|
|
40
|
+
return _cuid2.cuid()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class ResolvedGenerators:
|
|
45
|
+
"""A :class:`~repost_client.Generators` with every seam filled in."""
|
|
46
|
+
|
|
47
|
+
now: Callable[[], str]
|
|
48
|
+
uuid: Callable[[], str]
|
|
49
|
+
cuid: Callable[[], str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_generators(generators: Generators | None) -> ResolvedGenerators:
|
|
53
|
+
"""Merge a user ``Generators`` over the defaults (``None`` = default)."""
|
|
54
|
+
g = generators or Generators()
|
|
55
|
+
return ResolvedGenerators(
|
|
56
|
+
now=g.now or default_now,
|
|
57
|
+
uuid=g.uuid or default_uuid,
|
|
58
|
+
cuid=g.cuid or default_cuid,
|
|
59
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The ``UNSET`` sentinel: Python's tri-state for optional fields.
|
|
2
|
+
|
|
3
|
+
An attribute equal to :data:`UNSET` is *absent* (omitted from the wire, or
|
|
4
|
+
replaced by the field's ``@default``); ``None`` is an explicit JSON ``null``
|
|
5
|
+
(contract §2.3 — explicit null is not absence).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnsetType:
|
|
14
|
+
"""The type of the :data:`UNSET` sentinel (a falsy singleton).
|
|
15
|
+
|
|
16
|
+
Generated dataclasses annotate optional fields as
|
|
17
|
+
``T | None | UnsetType`` with ``UNSET`` as the default.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_instance: UnsetType | None = None
|
|
21
|
+
|
|
22
|
+
def __new__(cls) -> UnsetType:
|
|
23
|
+
if cls._instance is None:
|
|
24
|
+
cls._instance = super().__new__(cls)
|
|
25
|
+
return cls._instance
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return "UNSET"
|
|
29
|
+
|
|
30
|
+
def __bool__(self) -> bool:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
UNSET: Final[UnsetType] = UnsetType()
|
|
35
|
+
"""Marks an attribute as *not provided* — omit it (or inject its default)."""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Descriptor and result types shared across the runtime (contract §1).
|
|
2
|
+
|
|
3
|
+
Generated clients embed these descriptors as plain data; the runtime reads
|
|
4
|
+
them to serialize, envelope, and send. All descriptor dataclasses are frozen
|
|
5
|
+
— they are immutable schema facts, not builders.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _empty_enum_descriptors() -> dict[str, dict[str, str]]:
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class DefaultSpec:
|
|
21
|
+
"""An ``@default`` injection spec, applied when a field is absent.
|
|
22
|
+
|
|
23
|
+
``kind`` is one of ``"literal"`` (inject ``value`` verbatim), ``"now"``
|
|
24
|
+
(ISO-8601 timestamp from the clock), ``"uuid"``, or ``"cuid"`` (from the
|
|
25
|
+
id generators).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
kind: str
|
|
29
|
+
value: object = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class FieldDescriptor:
|
|
34
|
+
"""One field's serialization descriptor. Field order is wire key order.
|
|
35
|
+
|
|
36
|
+
``name`` is the DSL identifier (the SDK-input property); ``wire`` is the
|
|
37
|
+
``@map``'d wire name when it differs; ``list`` marks list arity (elements
|
|
38
|
+
serialized one by one); ``model`` names a nested payload model to recurse
|
|
39
|
+
into; ``enum`` maps member name → wire value; ``default`` is the
|
|
40
|
+
``@default`` injection when the field is absent from the input.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
schema_name: str | None = None
|
|
44
|
+
wire_name: str | None = None
|
|
45
|
+
scalar_kind: Literal["string", "boolean", "int64", "float64", "datetime", "json", "enum", "model"] | str | None = None
|
|
46
|
+
descriptor_id: str | None = None
|
|
47
|
+
required_in_input: bool = False
|
|
48
|
+
nullable_in_input: bool = True
|
|
49
|
+
list: bool = False
|
|
50
|
+
default: DefaultSpec | None = None
|
|
51
|
+
|
|
52
|
+
# Immutable descriptor-format-1 compatibility fields. Generated v2 code
|
|
53
|
+
# never sets them; the frozen v1 vector runner still does.
|
|
54
|
+
name: str | None = None
|
|
55
|
+
wire: str | None = None
|
|
56
|
+
model: str | None = None
|
|
57
|
+
enum: dict[str, str] | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class ModelDescriptor:
|
|
62
|
+
"""A payload model's serialization descriptor: its fields, in
|
|
63
|
+
declaration order."""
|
|
64
|
+
|
|
65
|
+
fields: tuple[FieldDescriptor, ...]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class EventDescriptor:
|
|
70
|
+
"""One catalog member's binding: wire event ``type`` (e.g.
|
|
71
|
+
``"book.created"``) plus the payload ``model``'s key in the model map."""
|
|
72
|
+
|
|
73
|
+
type: str
|
|
74
|
+
model: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class SchemaDescriptor:
|
|
79
|
+
"""Everything the runtime needs from a generated client.
|
|
80
|
+
|
|
81
|
+
``descriptor_format_version`` is the version the client was emitted for
|
|
82
|
+
— generated code asserts it against the runtime at construction via
|
|
83
|
+
:func:`repost_client.assert_descriptor_version`. ``webhooks`` is
|
|
84
|
+
``camelCase(Catalog) → member → event binding``.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
descriptor_format_version: int
|
|
88
|
+
models: dict[str, ModelDescriptor]
|
|
89
|
+
webhooks: dict[str, dict[str, EventDescriptor]]
|
|
90
|
+
enums: dict[str, dict[str, str]] = field(default_factory=_empty_enum_descriptors)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class SendResult:
|
|
95
|
+
"""The result of a webhook send, as returned by Repost's publish API
|
|
96
|
+
(202): the assigned message id (``msg_…``), the wire event type, the
|
|
97
|
+
receiving tenant, and the envelope's ISO-8601 timestamp."""
|
|
98
|
+
|
|
99
|
+
id: str
|
|
100
|
+
type: str
|
|
101
|
+
customer_id: str
|
|
102
|
+
timestamp: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class Generators:
|
|
107
|
+
"""Injectable clock and id sources (the deterministic-test seam).
|
|
108
|
+
|
|
109
|
+
Used by the envelope timestamp and ``@default(now()/uuid()/cuid())``
|
|
110
|
+
injection. ``None`` fields fall back to the real implementations —
|
|
111
|
+
override them for deterministic tests (the conformance suite's
|
|
112
|
+
requirement: every runtime exposes these seams).
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
now: Callable[[], str] | None = None
|
|
116
|
+
uuid: Callable[[], str] | None = None
|
|
117
|
+
cuid: Callable[[], str] | None = None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Runtime error types (the transport's ``PublishError`` lives in
|
|
2
|
+
:mod:`repost_client.transport`)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DescriptorVersionError(Exception):
|
|
8
|
+
"""A generated client's descriptor format does not match this runtime.
|
|
9
|
+
|
|
10
|
+
Raised by :func:`repost_client.assert_descriptor_version` at client
|
|
11
|
+
construction — never silently at send time.
|
|
12
|
+
"""
|
|
File without changes
|