Skip to content

Commit 4ba0d0f

Browse files
authored
chore: add migration.md file (#263)
Signed-off-by: Tudor Plugaru <plugaru.tudor@protonmail.com>
1 parent 0752f6f commit 4ba0d0f

1 file changed

Lines changed: 389 additions & 0 deletions

File tree

MIGRATION.md

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
# Migrating from CloudEvents SDK v1 to v2
2+
3+
This guide covers the breaking changes and new patterns introduced in v2 of the CloudEvents Python SDK.
4+
5+
## Requirements
6+
7+
| | v1 | v2 |
8+
|---|---|---|
9+
| Python | 3.7+ | **3.10+** |
10+
| Dependencies | varies (optional `pydantic` extra) | `python-dateutil>=2.8.2` only |
11+
12+
## Architectural Changes
13+
14+
v2 is a ground-up rewrite with four fundamental shifts:
15+
16+
1. **Protocol-based design** -- `BaseCloudEvent` is a `Protocol`, not a base class. Events expose explicit getter methods instead of dict-like access.
17+
2. **Explicit serialization** -- Implicit JSON handling with marshaller callbacks is replaced by a `Format` protocol. `JSONFormat` is the built-in implementation; you can write your own.
18+
3. **No auto-generated attributes** -- v1 auto-generated `id` (UUID), `time` (UTC now), and `specversion` ("1.0") if omitted. v2 requires all required attributes to be provided explicitly.
19+
4. **Strict validation** -- Events are validated at construction time. Extension attribute names must be 1-20 lowercase alphanumeric characters. `time` must be a timezone-aware `datetime`.
20+
21+
## Creating Events
22+
23+
**v1:**
24+
25+
```python
26+
from cloudevents.http import CloudEvent
27+
28+
# id, specversion, and time are auto-generated
29+
event = CloudEvent(
30+
{"type": "com.example.test", "source": "/myapp"},
31+
data={"message": "Hello"},
32+
)
33+
```
34+
35+
**v2:**
36+
37+
```python
38+
import uuid
39+
from cloudevents.core.v1.event import CloudEvent
40+
41+
# All required attributes must be explicit
42+
event = CloudEvent(
43+
attributes={
44+
"type": "com.example.test",
45+
"source": "/myapp",
46+
"id": str(uuid.uuid4()),
47+
"specversion": "1.0",
48+
# "time" is optional and NOT auto-generated
49+
},
50+
data={"message": "Hello"},
51+
)
52+
```
53+
54+
## Accessing Event Attributes
55+
56+
v1 events were dict-like. v2 events use explicit getter methods and are immutable after construction.
57+
58+
**v1:**
59+
60+
```python
61+
# Dict-like access
62+
source = event["source"]
63+
event["subject"] = "my-subject"
64+
del event["subject"]
65+
66+
# Iteration
67+
for attr_name in event:
68+
print(attr_name, event[attr_name])
69+
70+
# Membership test
71+
if "subject" in event:
72+
pass
73+
```
74+
75+
**v2:**
76+
77+
```python
78+
# Explicit getters for required attributes
79+
source = event.get_source()
80+
event_type = event.get_type()
81+
event_id = event.get_id()
82+
specversion = event.get_specversion()
83+
84+
# Explicit getters for optional attributes (return None if absent)
85+
subject = event.get_subject()
86+
time = event.get_time()
87+
datacontenttype = event.get_datacontenttype()
88+
dataschema = event.get_dataschema()
89+
90+
# Extension attributes
91+
custom_value = event.get_extension("myextension")
92+
93+
# All attributes as a dict
94+
attrs = event.get_attributes()
95+
96+
# Data
97+
data = event.get_data()
98+
```
99+
100+
## HTTP Binding
101+
102+
### Serializing Events
103+
104+
**v1:**
105+
106+
```python
107+
from cloudevents.conversion import to_binary, to_structured
108+
109+
# Returns a (headers, body) tuple
110+
headers, body = to_binary(event)
111+
headers, body = to_structured(event)
112+
```
113+
114+
**v2:**
115+
116+
```python
117+
from cloudevents.core.bindings.http import to_binary_event, to_structured_event
118+
119+
# Returns an HTTPMessage dataclass with .headers and .body
120+
message = to_binary_event(event)
121+
message = to_structured_event(event)
122+
123+
# Use in HTTP requests
124+
requests.post(url, headers=message.headers, data=message.body)
125+
```
126+
127+
If you need to pass a custom `Format`, use the lower-level functions:
128+
129+
```python
130+
from cloudevents.core.bindings.http import to_binary, to_structured
131+
from cloudevents.core.formats.json import JSONFormat
132+
133+
message = to_binary(event, event_format=JSONFormat())
134+
message = to_structured(event, event_format=JSONFormat())
135+
```
136+
137+
### Deserializing Events
138+
139+
**v1:**
140+
141+
```python
142+
from cloudevents.http import from_http
143+
144+
# Auto-detects binary vs structured from headers
145+
event = from_http(headers, body)
146+
```
147+
148+
**v2:**
149+
150+
```python
151+
from cloudevents.core.bindings.http import from_http_event, HTTPMessage
152+
153+
# Wrap raw headers/body into an HTTPMessage first
154+
message = HTTPMessage(headers=headers, body=body)
155+
156+
# Auto-detects binary vs structured and spec version (v1.0 / v0.3)
157+
event = from_http_event(message)
158+
```
159+
160+
Or explicitly choose the content mode:
161+
162+
```python
163+
from cloudevents.core.bindings.http import from_binary_event, from_structured_event
164+
165+
event = from_binary_event(message)
166+
event = from_structured_event(message)
167+
```
168+
169+
## Kafka Binding
170+
171+
### Serializing
172+
173+
**v1:**
174+
175+
```python
176+
from cloudevents.kafka import to_binary, KafkaMessage
177+
178+
kafka_msg = to_binary(event)
179+
# kafka_msg is a NamedTuple: .headers, .key, .value
180+
```
181+
182+
**v2:**
183+
184+
```python
185+
from cloudevents.core.bindings.kafka import to_binary_event, KafkaMessage
186+
187+
kafka_msg = to_binary_event(event)
188+
# kafka_msg is a frozen dataclass: .headers, .key, .value
189+
190+
# Custom key mapping
191+
kafka_msg = to_binary_event(
192+
event,
193+
key_mapper=lambda e: e.get_extension("partitionkey"),
194+
)
195+
```
196+
197+
### Deserializing
198+
199+
**v1:**
200+
201+
```python
202+
from cloudevents.kafka import from_binary, KafkaMessage
203+
204+
msg = KafkaMessage(headers=headers, key=key, value=value)
205+
event = from_binary(msg)
206+
```
207+
208+
**v2:**
209+
210+
```python
211+
from cloudevents.core.bindings.kafka import from_kafka_event, KafkaMessage
212+
213+
msg = KafkaMessage(headers=headers, key=key, value=value)
214+
215+
# Auto-detects binary vs structured and spec version
216+
event = from_kafka_event(msg)
217+
```
218+
219+
## AMQP Binding (New in v2)
220+
221+
v2 adds native AMQP 1.0 protocol binding support.
222+
223+
```python
224+
from cloudevents.core.v1.event import CloudEvent
225+
from cloudevents.core.bindings.amqp import (
226+
AMQPMessage,
227+
to_binary_event,
228+
from_amqp_event,
229+
)
230+
231+
# Serialize: attributes go to application_properties with cloudEvents_ prefix
232+
amqp_msg = to_binary_event(event)
233+
# amqp_msg.properties - AMQP message properties (e.g. content-type)
234+
# amqp_msg.application_properties - CloudEvent attributes
235+
# amqp_msg.application_data - event data as bytes
236+
237+
# Deserialize: auto-detects binary vs structured
238+
event = from_amqp_event(amqp_msg)
239+
```
240+
241+
## Custom Serialization Formats
242+
243+
**v1** used marshaller/unmarshaller callbacks:
244+
245+
```python
246+
# v1: pass callbacks directly
247+
headers, body = to_binary(event, data_marshaller=yaml.dump)
248+
event = from_http(headers, body, data_unmarshaller=yaml.safe_load)
249+
```
250+
251+
**v2** uses the `Format` protocol. Implement it to support non-JSON formats:
252+
253+
```python
254+
from cloudevents.core.formats.base import Format
255+
from cloudevents.core.base import BaseCloudEvent, EventFactory
256+
257+
class YAMLFormat:
258+
"""Example custom format -- implement the Format protocol."""
259+
260+
def read(
261+
self,
262+
event_factory: EventFactory | None,
263+
data: str | bytes,
264+
) -> BaseCloudEvent:
265+
... # Parse YAML into attributes dict, call event_factory(attributes, data)
266+
267+
def write(self, event: BaseCloudEvent) -> bytes:
268+
... # Serialize entire event to YAML bytes
269+
270+
def write_data(
271+
self,
272+
data: dict | str | bytes | None,
273+
datacontenttype: str | None,
274+
) -> bytes:
275+
... # Serialize just the data payload
276+
277+
def read_data(
278+
self,
279+
body: bytes,
280+
datacontenttype: str | None,
281+
) -> dict | str | bytes | None:
282+
... # Deserialize just the data payload
283+
284+
def get_content_type(self) -> str:
285+
return "application/cloudevents+yaml"
286+
```
287+
288+
Then use it with any binding:
289+
290+
```python
291+
from cloudevents.core.bindings.http import to_binary
292+
293+
message = to_binary(event, event_format=YAMLFormat())
294+
```
295+
296+
## Error Handling
297+
298+
v2 replaces v1's exception hierarchy with more granular, typed exceptions.
299+
300+
**v1:**
301+
302+
```python
303+
from cloudevents.exceptions import (
304+
GenericException,
305+
MissingRequiredFields,
306+
InvalidRequiredFields,
307+
DataMarshallerError,
308+
DataUnmarshallerError,
309+
)
310+
```
311+
312+
**v2:**
313+
314+
```python
315+
from cloudevents.core.exceptions import (
316+
BaseCloudEventException, # Base for all CloudEvent errors
317+
CloudEventValidationError, # Aggregated validation errors (raised on construction)
318+
MissingRequiredAttributeError, # Missing required attribute (also a ValueError)
319+
InvalidAttributeTypeError, # Wrong attribute type (also a TypeError)
320+
InvalidAttributeValueError, # Invalid attribute value (also a ValueError)
321+
CustomExtensionAttributeError, # Invalid extension name (also a ValueError)
322+
)
323+
```
324+
325+
`CloudEventValidationError` contains all validation failures at once:
326+
327+
```python
328+
try:
329+
event = CloudEvent(attributes={"source": "/test"}) # missing type, id, specversion
330+
except CloudEventValidationError as e:
331+
# e.errors is a dict[str, list[BaseCloudEventException]]
332+
for attr_name, errors in e.errors.items():
333+
print(f"{attr_name}: {errors}")
334+
```
335+
336+
## Removed Features
337+
338+
| Feature | v1 | v2 Alternative |
339+
|---|---|---|
340+
| Pydantic integration | `from cloudevents.pydantic import CloudEvent` | Removed -- use the core `CloudEvent` directly |
341+
| Dict-like event access | `event["source"]`, `event["x"] = y` | `event.get_source()`, `event.get_extension("x")` |
342+
| Auto-generated `id` | Automatic UUID4 | Provide explicitly |
343+
| Auto-generated `time` | Automatic UTC timestamp | Provide explicitly or omit |
344+
| Auto-generated `specversion` | Defaults to `"1.0"` | Provide explicitly |
345+
| `from_dict()` | `from cloudevents.http import from_dict` | Construct `CloudEvent(attributes=d)` directly |
346+
| `to_dict()` | `from cloudevents.conversion import to_dict` | `event.get_attributes()` + `event.get_data()` |
347+
| `from_json()` | `from cloudevents.http import from_json` | `JSONFormat().read(None, json_bytes)` |
348+
| `to_json()` | `from cloudevents.conversion import to_json` | `JSONFormat().write(event)` |
349+
| Custom marshallers | `data_marshaller=fn` / `data_unmarshaller=fn` | Implement the `Format` protocol |
350+
| `is_binary()` / `is_structured()` | `from cloudevents.http import is_binary` | Mode is handled internally by `from_http_event()` |
351+
| Deprecated helpers | `to_binary_http()`, `to_structured_http()` | `to_binary_event()`, `to_structured_event()` |
352+
353+
## Quick Reference: Import Mapping
354+
355+
| v1 Import | v2 Import |
356+
|---|---|
357+
| `cloudevents.http.CloudEvent` | `cloudevents.core.v1.event.CloudEvent` |
358+
| `cloudevents.http.from_http` | `cloudevents.core.bindings.http.from_http_event` |
359+
| `cloudevents.http.from_json` | `cloudevents.core.formats.json.JSONFormat().read` |
360+
| `cloudevents.http.from_dict` | `cloudevents.core.v1.event.CloudEvent(attributes=...)` |
361+
| `cloudevents.conversion.to_binary` | `cloudevents.core.bindings.http.to_binary_event` |
362+
| `cloudevents.conversion.to_structured` | `cloudevents.core.bindings.http.to_structured_event` |
363+
| `cloudevents.conversion.to_json` | `cloudevents.core.formats.json.JSONFormat().write` |
364+
| `cloudevents.conversion.to_dict` | `event.get_attributes()` |
365+
| `cloudevents.kafka.KafkaMessage` | `cloudevents.core.bindings.kafka.KafkaMessage` |
366+
| `cloudevents.kafka.to_binary` | `cloudevents.core.bindings.kafka.to_binary_event` |
367+
| `cloudevents.kafka.from_binary` | `cloudevents.core.bindings.kafka.from_binary_event` |
368+
| `cloudevents.pydantic.CloudEvent` | Removed |
369+
| `cloudevents.abstract.AnyCloudEvent` | `cloudevents.core.base.BaseCloudEvent` |
370+
371+
## CloudEvents Spec v0.3
372+
373+
Both v1 and v2 support CloudEvents spec v0.3. In v2, use the dedicated class:
374+
375+
```python
376+
from cloudevents.core.v03.event import CloudEvent
377+
378+
event = CloudEvent(
379+
attributes={
380+
"type": "com.example.test",
381+
"source": "/myapp",
382+
"id": "123",
383+
"specversion": "0.3",
384+
"schemaurl": "https://example.com/schema", # v0.3-specific (renamed to dataschema in v1.0)
385+
},
386+
)
387+
```
388+
389+
Binding functions auto-detect the spec version when deserializing, so no special handling is needed on the receiving side.

0 commit comments

Comments
 (0)