lightweb-adapter-sdk 0.2.2__tar.gz → 0.2.3__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.
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/CHANGELOG.md +44 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/PKG-INFO +1 -1
- lightweb_adapter_sdk-0.2.3/src/lightweb_adapter_sdk/event_consumer/parser.py +119 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/testkit/fake_event_source.py +39 -24
- lightweb_adapter_sdk-0.2.2/src/lightweb_adapter_sdk/event_consumer/parser.py +0 -94
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/.github/workflows/ci.yml +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/.github/workflows/release.yml +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/.gitignore +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/LICENSE +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/README.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/conformance-audit.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/conformance.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/contract.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/event-consumer.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/examples/round-0-prompt.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/round-prompt-template.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/supervisor-side-edits.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/testkit.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/docs/wrap-procedures.md +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/pyproject.toml +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/__init__.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/adapter/__init__.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/__init__.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/fault_control.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_apply.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_capabilities.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_disable.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_event_consumer.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_health.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_identity_mapping.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_list_accounts.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/conformance/test_reconcile.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/event_consumer/__init__.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/event_consumer/dedup.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/event_consumer/jetstream.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/event_consumer/protocol.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/py.typed +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/reference/__init__.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/reference/event_consumer.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/reference/fake_server.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/reference/fault_control.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/testkit/__init__.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/testkit/fake_entitlements.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/testkit/structural.py +0 -0
- {lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/types/__init__.py +0 -0
|
@@ -8,6 +8,50 @@ Breaking changes are flagged with `**BREAKING:**` at the start of the entry, eve
|
|
|
8
8
|
|
|
9
9
|
The SDK is in 0.x until its contract has been validated by at least one production adapter. Per [`CONVENTIONS.md`](CONVENTIONS.md) §1.3, breaking changes may land in minor (0.X → 0.(X+1)) releases while in 0.x; pin against a specific minor version.
|
|
10
10
|
|
|
11
|
+
## [0.2.3] - 2026-05-24
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `event_consumer.parser.parse_envelope` now reads the platform's
|
|
16
|
+
native event-envelope field names — `event_id`, `event_type`,
|
|
17
|
+
`event_version`, `occurred_at`, `emitted_by` — per
|
|
18
|
+
`libs/events/schemas/entitlements/_envelope.json` and the entitlements
|
|
19
|
+
service's `outbox/envelope.py::build_envelope`. It previously read a
|
|
20
|
+
CloudEvents shape (`id`, `type`, `specversion`, `time`, `source`) that
|
|
21
|
+
the platform never emits, rejecting every real event at `Missing
|
|
22
|
+
required field: id`; an adapter consuming the live bus would have
|
|
23
|
+
nacked/dead-lettered all entitlements events. `member_id` is still
|
|
24
|
+
sourced from `data.member_id`; `subject` is tolerated and ignored (it
|
|
25
|
+
is schema-required and always present, but the consumer hook is
|
|
26
|
+
`data.member_id`, since `subject.id` varies by event type). The
|
|
27
|
+
`EventEnvelope` type is unchanged — this is a parser-internal fix,
|
|
28
|
+
non-breaking. The parser docstring, which had incorrectly described
|
|
29
|
+
the CloudEvents shape as matching the schema, is corrected to the
|
|
30
|
+
native shape. Surfaced by NextCloud D3 consumer-wiring integration
|
|
31
|
+
work (integration finding F8).
|
|
32
|
+
- `testkit.FakeEventSource.publish`/`publish_async` now serialize the
|
|
33
|
+
native envelope shape (matching `build_envelope`: `event_id`,
|
|
34
|
+
`event_type`, `event_version`, `occurred_at`, `emitted_by`, a member
|
|
35
|
+
`subject`, `data.member_id`, and `context` as an object). They
|
|
36
|
+
previously emitted the same CloudEvents shape the parser wrongly read
|
|
37
|
+
— so the fake producer and the parser were a matched pair that agreed
|
|
38
|
+
with each other and not with the platform, which is why no test caught
|
|
39
|
+
F8. Adapter authors testing consumers against `FakeEventSource` now
|
|
40
|
+
exercise the real on-wire shape. Behavior change to the testkit fake;
|
|
41
|
+
adapter consumer tests that inspected the published bytes for `id`/
|
|
42
|
+
`type`/etc. must read the native field names.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Raw-bytes parser coverage in `tests/unit/test_event_consumer_conformance.py`
|
|
47
|
+
(`TestParseEnvelope`): asserts `parse_envelope` reads the native
|
|
48
|
+
envelope shape and rejects the legacy CloudEvents shape (F8 regression
|
|
49
|
+
guard), plus a case confirming `member_id` is taken from
|
|
50
|
+
`data.member_id` rather than `subject.id`. The prior parser tests
|
|
51
|
+
asserted the CloudEvents shape — they validated the bug rather than
|
|
52
|
+
catching it. Test payloads are derived from the producer
|
|
53
|
+
(`build_envelope`) pending a captured real event.
|
|
54
|
+
|
|
11
55
|
## [0.2.2] - 2026-05-24
|
|
12
56
|
|
|
13
57
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lightweb-adapter-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Published Python SDK for the Lightweb adapter contract
|
|
5
5
|
Project-URL: Homepage, https://github.com/lightweb/adapter-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/lightweb/adapter-sdk
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Envelope parser: raw JetStream payload → EventEnvelope.
|
|
2
|
+
|
|
3
|
+
The platform publishes JetStream messages as JSON in the entitlements
|
|
4
|
+
event-envelope shape (per ``libs/events/schemas/entitlements/_envelope.json``,
|
|
5
|
+
Conventions §4.3, emitted by the entitlements service's
|
|
6
|
+
``outbox/envelope.py::build_envelope``):
|
|
7
|
+
|
|
8
|
+
.. code-block:: json
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
"event_id": "evt_<base58>",
|
|
12
|
+
"event_type": "entitlements.<entity>.<verb>",
|
|
13
|
+
"event_version": "1.0",
|
|
14
|
+
"occurred_at": "2026-05-17T12:00:00Z",
|
|
15
|
+
"emitted_by": "service:entitlements",
|
|
16
|
+
"subject": {"type": "member", "id": "mem_..."},
|
|
17
|
+
"data": {"member_id": "mem_...", ...},
|
|
18
|
+
"context": {...}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
This module parses that shape into the SDK's ``EventEnvelope``,
|
|
22
|
+
flattening ``data.member_id`` to the top level and omitting ``subject``
|
|
23
|
+
and ``data`` (beyond ``member_id``). ``subject`` is tolerated and
|
|
24
|
+
ignored — the platform always emits it (the schema requires it), but
|
|
25
|
+
the consumer's hook is ``data.member_id``, not ``subject.id`` (subject's
|
|
26
|
+
``id`` varies by event type: member_id for member events, subscription_id
|
|
27
|
+
for subscription events, etc.).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
|
|
35
|
+
from lightweb_adapter_sdk.types import EventEnvelope
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ParseError(ValueError):
|
|
39
|
+
"""The raw payload could not be parsed into an EventEnvelope."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_envelope(raw_payload: bytes) -> EventEnvelope:
|
|
43
|
+
"""Parse a raw JetStream message payload into an ``EventEnvelope``.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
raw_payload: The raw bytes from JetStream, expected to be
|
|
47
|
+
UTF-8 encoded JSON matching the entitlements event-envelope
|
|
48
|
+
schema (``_envelope.json``).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A fully-populated ``EventEnvelope``.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ParseError: If the payload is not valid JSON, is missing
|
|
55
|
+
required fields, or has an unexpected shape.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(raw_payload)
|
|
59
|
+
except (json.JSONDecodeError, TypeError) as exc:
|
|
60
|
+
raise ParseError(f"Payload is not valid JSON: {exc}") from exc
|
|
61
|
+
|
|
62
|
+
if not isinstance(data, dict):
|
|
63
|
+
raise ParseError(
|
|
64
|
+
f"Payload must be a JSON object, got {type(data).__name__}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Required top-level fields, per _envelope.json. ``subject`` and
|
|
68
|
+
# ``data`` are also schema-required but handled separately below
|
|
69
|
+
# (subject is tolerated-and-ignored; data is read for member_id).
|
|
70
|
+
for field in (
|
|
71
|
+
"event_id",
|
|
72
|
+
"event_type",
|
|
73
|
+
"event_version",
|
|
74
|
+
"occurred_at",
|
|
75
|
+
"emitted_by",
|
|
76
|
+
):
|
|
77
|
+
if field not in data:
|
|
78
|
+
raise ParseError(f"Missing required field: {field}")
|
|
79
|
+
|
|
80
|
+
# data.member_id is the source for EventEnvelope.member_id.
|
|
81
|
+
if "data" not in data or not isinstance(data["data"], dict):
|
|
82
|
+
raise ParseError("Missing or invalid 'data' field")
|
|
83
|
+
if "member_id" not in data["data"]:
|
|
84
|
+
raise ParseError("Missing 'data.member_id' field")
|
|
85
|
+
|
|
86
|
+
member_id = data["data"]["member_id"]
|
|
87
|
+
if not isinstance(member_id, str):
|
|
88
|
+
raise ParseError(
|
|
89
|
+
f"'data.member_id' must be a string, got {type(member_id).__name__}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# context is optional and preserved when present. The producer emits
|
|
93
|
+
# an empty object ``{}`` (not null) when there is no context; both an
|
|
94
|
+
# empty dict and an absent/None context are accepted here.
|
|
95
|
+
context = data.get("context")
|
|
96
|
+
if context is not None and not isinstance(context, dict):
|
|
97
|
+
raise ParseError("'context' must be an object when present")
|
|
98
|
+
|
|
99
|
+
# occurred_at — accept ISO-8601 strings (the producer emits with a
|
|
100
|
+
# trailing 'Z'; datetime.fromisoformat handles 'Z' on Python 3.11+).
|
|
101
|
+
occurred_at_str = data["occurred_at"]
|
|
102
|
+
try:
|
|
103
|
+
occurred_at = datetime.fromisoformat(occurred_at_str)
|
|
104
|
+
if occurred_at.tzinfo is None:
|
|
105
|
+
occurred_at = occurred_at.replace(tzinfo=timezone.utc)
|
|
106
|
+
except (ValueError, TypeError) as exc:
|
|
107
|
+
raise ParseError(
|
|
108
|
+
f"Invalid 'occurred_at' field: {occurred_at_str!r}: {exc}"
|
|
109
|
+
) from exc
|
|
110
|
+
|
|
111
|
+
return EventEnvelope(
|
|
112
|
+
event_id=data["event_id"],
|
|
113
|
+
event_type=data["event_type"],
|
|
114
|
+
event_version=data["event_version"],
|
|
115
|
+
occurred_at=occurred_at,
|
|
116
|
+
emitted_by=data["emitted_by"],
|
|
117
|
+
member_id=member_id,
|
|
118
|
+
context=context,
|
|
119
|
+
)
|
|
@@ -4,6 +4,17 @@ Provides ``FakeEventSource``, an in-process fake that publishes
|
|
|
4
4
|
``EventEnvelope`` instances as raw JetStream-style JSON bytes,
|
|
5
5
|
allowing tests to drive event consumer behavior without a real
|
|
6
6
|
NATS/JetStream connection.
|
|
7
|
+
|
|
8
|
+
The published bytes match the platform's native event-envelope shape
|
|
9
|
+
(per ``libs/events/schemas/entitlements/_envelope.json`` and the
|
|
10
|
+
entitlements service's ``build_envelope``): ``event_id``, ``event_type``,
|
|
11
|
+
``event_version``, ``occurred_at``, ``emitted_by``, ``subject``,
|
|
12
|
+
``data`` (carrying ``member_id``), and ``context``. This is the shape
|
|
13
|
+
``parse_envelope`` consumes, so a round-trip publish→handle_event
|
|
14
|
+
exercises the real parse path. (Prior to 0.2.3 this fake emitted a
|
|
15
|
+
CloudEvents shape — ``id``/``type``/``specversion``/``time``/``source``
|
|
16
|
+
— matching a parser that read the same wrong shape; neither matched the
|
|
17
|
+
platform. See CHANGELOG 0.2.3 / integration finding F8.)
|
|
7
18
|
"""
|
|
8
19
|
|
|
9
20
|
from __future__ import annotations
|
|
@@ -23,6 +34,10 @@ class FakeEventSource:
|
|
|
23
34
|
callbacks (consumers). Events are stored internally for inspection
|
|
24
35
|
and can be published synchronously or asynchronously.
|
|
25
36
|
|
|
37
|
+
The serialized bytes use the platform's native envelope shape so
|
|
38
|
+
that consumers exercising ``parse_envelope`` see exactly what the
|
|
39
|
+
entitlements service emits.
|
|
40
|
+
|
|
26
41
|
Args:
|
|
27
42
|
subject: The subject name this fake source publishes to
|
|
28
43
|
(e.g. ``"entitlements.member.>"``).
|
|
@@ -47,6 +62,28 @@ class FakeEventSource:
|
|
|
47
62
|
self._sequence += 1
|
|
48
63
|
return f"evt-fake-{self._sequence:04d}"
|
|
49
64
|
|
|
65
|
+
def _serialize(self, envelope: EventEnvelope) -> bytes:
|
|
66
|
+
"""Serialize an ``EventEnvelope`` to the platform's native shape.
|
|
67
|
+
|
|
68
|
+
Mirrors the entitlements service's ``build_envelope`` output:
|
|
69
|
+
native field names, a member ``subject`` derived from
|
|
70
|
+
``member_id`` (the consumer ignores ``subject`` but the real
|
|
71
|
+
envelope always carries it), ``member_id`` nested under
|
|
72
|
+
``data``, and ``context`` as an object (``{}`` when empty,
|
|
73
|
+
matching the producer, which never emits null).
|
|
74
|
+
"""
|
|
75
|
+
payload_dict: dict[str, Any] = {
|
|
76
|
+
"event_id": envelope.event_id,
|
|
77
|
+
"event_type": envelope.event_type,
|
|
78
|
+
"event_version": envelope.event_version,
|
|
79
|
+
"occurred_at": envelope.occurred_at.isoformat(),
|
|
80
|
+
"emitted_by": envelope.emitted_by,
|
|
81
|
+
"subject": {"type": "member", "id": envelope.member_id},
|
|
82
|
+
"data": {"member_id": envelope.member_id},
|
|
83
|
+
"context": envelope.context if envelope.context is not None else {},
|
|
84
|
+
}
|
|
85
|
+
return json.dumps(payload_dict).encode("utf-8")
|
|
86
|
+
|
|
50
87
|
def publish(self, envelope: EventEnvelope) -> bytes:
|
|
51
88
|
"""Publish an event envelope as raw JSON bytes.
|
|
52
89
|
|
|
@@ -58,18 +95,7 @@ class FakeEventSource:
|
|
|
58
95
|
Returns:
|
|
59
96
|
The raw JSON bytes that were published.
|
|
60
97
|
"""
|
|
61
|
-
|
|
62
|
-
"id": envelope.event_id,
|
|
63
|
-
"type": envelope.event_type,
|
|
64
|
-
"specversion": envelope.event_version,
|
|
65
|
-
"time": envelope.occurred_at.isoformat(),
|
|
66
|
-
"source": envelope.emitted_by,
|
|
67
|
-
"data": {"member_id": envelope.member_id},
|
|
68
|
-
}
|
|
69
|
-
if envelope.context is not None:
|
|
70
|
-
payload_dict["context"] = envelope.context
|
|
71
|
-
|
|
72
|
-
raw = json.dumps(payload_dict).encode("utf-8")
|
|
98
|
+
raw = self._serialize(envelope)
|
|
73
99
|
self._published.append((envelope, raw))
|
|
74
100
|
|
|
75
101
|
for consumer in self._consumers:
|
|
@@ -88,18 +114,7 @@ class FakeEventSource:
|
|
|
88
114
|
Returns:
|
|
89
115
|
The raw JSON bytes that were published.
|
|
90
116
|
"""
|
|
91
|
-
|
|
92
|
-
"id": envelope.event_id,
|
|
93
|
-
"type": envelope.event_type,
|
|
94
|
-
"specversion": envelope.event_version,
|
|
95
|
-
"time": envelope.occurred_at.isoformat(),
|
|
96
|
-
"source": envelope.emitted_by,
|
|
97
|
-
"data": {"member_id": envelope.member_id},
|
|
98
|
-
}
|
|
99
|
-
if envelope.context is not None:
|
|
100
|
-
payload_dict["context"] = envelope.context
|
|
101
|
-
|
|
102
|
-
raw = json.dumps(payload_dict).encode("utf-8")
|
|
117
|
+
raw = self._serialize(envelope)
|
|
103
118
|
self._published.append((envelope, raw))
|
|
104
119
|
|
|
105
120
|
for consumer in self._consumers:
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""Envelope parser: raw JetStream payload → EventEnvelope.
|
|
2
|
-
|
|
3
|
-
The platform publishes JetStream messages as JSON with the following
|
|
4
|
-
shape (per ``libs/events/schemas/entitlements/_envelope.json``):
|
|
5
|
-
|
|
6
|
-
.. code-block:: json
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
"id": "<event-id>",
|
|
10
|
-
"type": "entitlements.<entity>.<verb>",
|
|
11
|
-
"specversion": "1.0",
|
|
12
|
-
"time": "2026-05-17T12:00:00Z",
|
|
13
|
-
"source": "service:entitlements",
|
|
14
|
-
"subject": {"type": "member", "id": "mem_..."},
|
|
15
|
-
"data": {"member_id": "mem_...", ...},
|
|
16
|
-
"context": {"trace_context": "..."}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
This module parses that shape into the SDK's ``EventEnvelope``,
|
|
20
|
-
flattening ``data.member_id`` to the top level and omitting ``subject``
|
|
21
|
-
and ``data`` (beyond ``member_id``).
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import json
|
|
27
|
-
from datetime import datetime, timezone
|
|
28
|
-
|
|
29
|
-
from lightweb_adapter_sdk.types import EventEnvelope
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ParseError(ValueError):
|
|
33
|
-
"""The raw payload could not be parsed into an EventEnvelope."""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def parse_envelope(raw_payload: bytes) -> EventEnvelope:
|
|
37
|
-
"""Parse a raw JetStream message payload into an ``EventEnvelope``.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
raw_payload: The raw bytes from JetStream, expected to be
|
|
41
|
-
UTF-8 encoded JSON matching the platform envelope schema.
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
A fully-populated ``EventEnvelope``.
|
|
45
|
-
|
|
46
|
-
Raises:
|
|
47
|
-
ParseError: If the payload is not valid JSON, is missing
|
|
48
|
-
required fields, or has an unexpected shape.
|
|
49
|
-
"""
|
|
50
|
-
try:
|
|
51
|
-
data = json.loads(raw_payload)
|
|
52
|
-
except (json.JSONDecodeError, TypeError) as exc:
|
|
53
|
-
raise ParseError(f"Payload is not valid JSON: {exc}") from exc
|
|
54
|
-
|
|
55
|
-
# Required top-level fields
|
|
56
|
-
for field in ("id", "type", "specversion", "time", "source"):
|
|
57
|
-
if field not in data:
|
|
58
|
-
raise ParseError(f"Missing required field: {field}")
|
|
59
|
-
|
|
60
|
-
# data.member_id is the source for EventEnvelope.member_id
|
|
61
|
-
if "data" not in data or not isinstance(data["data"], dict):
|
|
62
|
-
raise ParseError("Missing or invalid 'data' field")
|
|
63
|
-
if "member_id" not in data["data"]:
|
|
64
|
-
raise ParseError("Missing 'data.member_id' field")
|
|
65
|
-
|
|
66
|
-
member_id = data["data"]["member_id"]
|
|
67
|
-
if not isinstance(member_id, str):
|
|
68
|
-
raise ParseError(
|
|
69
|
-
f"'data.member_id' must be a string, got {type(member_id).__name__}"
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# context is optional but preserved when present
|
|
73
|
-
context = data.get("context")
|
|
74
|
-
if context is not None and not isinstance(context, dict):
|
|
75
|
-
raise ParseError("'context' must be an object when present")
|
|
76
|
-
|
|
77
|
-
# Parse time — accept ISO-8601 strings
|
|
78
|
-
time_str = data["time"]
|
|
79
|
-
try:
|
|
80
|
-
occurred_at = datetime.fromisoformat(time_str)
|
|
81
|
-
if occurred_at.tzinfo is None:
|
|
82
|
-
occurred_at = occurred_at.replace(tzinfo=timezone.utc)
|
|
83
|
-
except (ValueError, TypeError) as exc:
|
|
84
|
-
raise ParseError(f"Invalid 'time' field: {time_str}: {exc}") from exc
|
|
85
|
-
|
|
86
|
-
return EventEnvelope(
|
|
87
|
-
event_id=data["id"],
|
|
88
|
-
event_type=data["type"],
|
|
89
|
-
event_version=data["specversion"],
|
|
90
|
-
occurred_at=occurred_at,
|
|
91
|
-
emitted_by=data["source"],
|
|
92
|
-
member_id=member_id,
|
|
93
|
-
context=context,
|
|
94
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lightweb_adapter_sdk-0.2.2 → lightweb_adapter_sdk-0.2.3}/src/lightweb_adapter_sdk/types/__init__.py
RENAMED
|
File without changes
|