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.
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ dist/
4
+ .pytest_cache/
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: repost-client
3
+ Version: 0.1.0
4
+ Summary: Repost webhook client runtime — typed senders generated from your .repost schema
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27
@@ -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