Skip to content

Commit 0752f6f

Browse files
authored
feat(v2): add support for spec version 0.3 (#261)
* feat: add support for spec version 0.3 Signed-off-by: Tudor Plugaru <plugaru.tudor@protonmail.com> * chore: make ruff fail on unused imports Signed-off-by: Tudor Plugaru <plugaru.tudor@protonmail.com> * chore: Move spec version constants to spec module Moves SPECVERSION_V1_0 and SPECVERSION_V0_3 from the core __init__.py to a new cloudevents.core.spec module. Signed-off-by: Tudor Plugaru <plugaru.tudor@protonmail.com> --------- Signed-off-by: Tudor Plugaru <plugaru.tudor@protonmail.com>
1 parent 91b092b commit 0752f6f

File tree

19 files changed

+1869
-54
lines changed

19 files changed

+1869
-54
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ will) break with every update.
1010
This SDK current supports the following versions of CloudEvents:
1111

1212
- v1.0
13+
- v0.3
1314

1415
## Python SDK
1516

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ exclude = [
114114
[tool.ruff.lint]
115115
ignore = ["E731"]
116116
extend-ignore = ["E203"]
117-
select = ["I"]
117+
select = [
118+
"I", # isort - import sorting
119+
"F401", # unused imports
120+
]
118121

119122

120123
[tool.pytest.ini_options]

src/cloudevents/core/bindings/amqp.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
from dateutil.parser import isoparse
2020

2121
from cloudevents.core.base import BaseCloudEvent, EventFactory
22+
from cloudevents.core.bindings.common import get_event_factory_for_version
2223
from cloudevents.core.formats.base import Format
2324
from cloudevents.core.formats.json import JSONFormat
25+
from cloudevents.core.spec import SPECVERSION_V1_0
2426
from cloudevents.core.v1.event import CloudEvent
2527

2628
# AMQP CloudEvents spec allows both cloudEvents_ and cloudEvents: prefixes
@@ -151,11 +153,14 @@ def to_binary(event: BaseCloudEvent, event_format: Format) -> AMQPMessage:
151153
def from_binary(
152154
message: AMQPMessage,
153155
event_format: Format,
154-
event_factory: EventFactory,
156+
event_factory: EventFactory | None = None,
155157
) -> BaseCloudEvent:
156158
"""
157159
Parse an AMQP binary content mode message to a CloudEvent.
158160
161+
Auto-detects the CloudEvents version from the application properties
162+
and uses the appropriate event factory if not explicitly provided.
163+
159164
Extracts CloudEvent attributes from AMQP application properties with either
160165
'cloudEvents_' or 'cloudEvents:' prefix (per AMQP CloudEvents spec), and treats
161166
the AMQP 'content-type' property as the 'datacontenttype' attribute. The
@@ -202,6 +207,11 @@ def from_binary(
202207
if CONTENT_TYPE_PROPERTY in message.properties:
203208
attributes["datacontenttype"] = message.properties[CONTENT_TYPE_PROPERTY]
204209

210+
# Auto-detect version if factory not provided
211+
if event_factory is None:
212+
specversion = attributes.get("specversion", SPECVERSION_V1_0)
213+
event_factory = get_event_factory_for_version(specversion)
214+
205215
datacontenttype = attributes.get("datacontenttype")
206216
data = event_format.read_data(message.application_data, datacontenttype)
207217

@@ -250,7 +260,7 @@ def to_structured(event: BaseCloudEvent, event_format: Format) -> AMQPMessage:
250260
def from_structured(
251261
message: AMQPMessage,
252262
event_format: Format,
253-
event_factory: EventFactory,
263+
event_factory: EventFactory | None = None,
254264
) -> BaseCloudEvent:
255265
"""
256266
Parse an AMQP structured content mode message to a CloudEvent.
@@ -259,33 +269,44 @@ def from_structured(
259269
specified format. Any cloudEvents_-prefixed application properties are ignored
260270
as the application-data contains all event metadata.
261271
272+
If event_factory is not provided, version detection is delegated to the format
273+
implementation, which will auto-detect based on the 'specversion' field.
274+
262275
Example:
263276
>>> from cloudevents.core.v1.event import CloudEvent
264277
>>> from cloudevents.core.formats.json import JSONFormat
265278
>>>
279+
>>> # Explicit factory
266280
>>> message = AMQPMessage(
267281
... properties={"content-type": "application/cloudevents+json"},
268282
... application_properties={},
269283
... application_data=b'{"type": "com.example.test", "source": "/test", ...}'
270284
... )
271285
>>> event = from_structured(message, JSONFormat(), CloudEvent)
286+
>>>
287+
>>> # Auto-detect version
288+
>>> event = from_structured(message, JSONFormat())
272289
273290
:param message: AMQPMessage to parse
274291
:param event_format: Format implementation for deserialization
275-
:param event_factory: Factory function to create CloudEvent instances
292+
:param event_factory: Factory function to create CloudEvent instances.
293+
If None, the format will auto-detect the version.
276294
:return: CloudEvent instance
277295
"""
296+
# Delegate version detection to format layer
278297
return event_format.read(event_factory, message.application_data)
279298

280299

281300
def from_amqp(
282301
message: AMQPMessage,
283302
event_format: Format,
284-
event_factory: EventFactory,
303+
event_factory: EventFactory | None = None,
285304
) -> BaseCloudEvent:
286305
"""
287306
Parse an AMQP message to a CloudEvent with automatic mode detection.
288307
308+
Auto-detects CloudEvents version and uses appropriate event factory if not provided.
309+
289310
Automatically detects whether the message uses binary or structured content mode:
290311
- If content-type starts with "application/cloudevents" → structured mode
291312
- Otherwise → binary mode
@@ -315,7 +336,7 @@ def from_amqp(
315336
316337
:param message: AMQPMessage to parse
317338
:param event_format: Format implementation for deserialization
318-
:param event_factory: Factory function to create CloudEvent instances
339+
:param event_factory: Factory function to create CloudEvent instances (auto-detected if None)
319340
:return: CloudEvent instance
320341
"""
321342
content_type = message.properties.get(CONTENT_TYPE_PROPERTY, "")

src/cloudevents/core/bindings/common.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525

2626
from dateutil.parser import isoparse
2727

28+
from cloudevents.core.base import EventFactory
29+
from cloudevents.core.spec import SPECVERSION_V0_3
30+
from cloudevents.core.v03.event import CloudEvent as CloudEventV03
31+
from cloudevents.core.v1.event import CloudEvent
32+
2833
TIME_ATTR: Final[str] = "time"
2934
CONTENT_TYPE_HEADER: Final[str] = "content-type"
3035
DATACONTENTTYPE_ATTR: Final[str] = "datacontenttype"
@@ -66,3 +71,19 @@ def decode_header_value(attr_name: str, value: str) -> Any:
6671
return isoparse(decoded)
6772

6873
return decoded
74+
75+
76+
def get_event_factory_for_version(specversion: str) -> EventFactory:
77+
"""
78+
Get the appropriate event factory based on the CloudEvents specification version.
79+
80+
This function returns the CloudEvent class implementation for the specified
81+
version. Used by protocol bindings for automatic version detection.
82+
83+
:param specversion: The CloudEvents specification version (e.g., "0.3" or "1.0")
84+
:return: EventFactory for the specified version (defaults to v1.0 for unknown versions)
85+
"""
86+
if specversion == SPECVERSION_V0_3:
87+
return CloudEventV03
88+
# Default to v1.0 for unknown versions
89+
return CloudEvent

src/cloudevents/core/bindings/http.py

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
DATACONTENTTYPE_ATTR,
2222
decode_header_value,
2323
encode_header_value,
24+
get_event_factory_for_version,
2425
)
2526
from cloudevents.core.formats.base import Format
2627
from cloudevents.core.formats.json import JSONFormat
27-
from cloudevents.core.v1.event import CloudEvent
28+
from cloudevents.core.spec import SPECVERSION_V1_0
2829

2930
CE_PREFIX: Final[str] = "ce-"
3031

@@ -94,11 +95,14 @@ def to_binary(event: BaseCloudEvent, event_format: Format) -> HTTPMessage:
9495
def from_binary(
9596
message: HTTPMessage,
9697
event_format: Format,
97-
event_factory: EventFactory,
98+
event_factory: EventFactory | None = None,
9899
) -> BaseCloudEvent:
99100
"""
100101
Parse an HTTP binary content mode message to a CloudEvent.
101102
103+
Auto-detects the CloudEvents version from the 'ce-specversion' header
104+
and uses the appropriate event factory if not explicitly provided.
105+
102106
Extracts CloudEvent attributes from ce-prefixed HTTP headers and treats the
103107
'Content-Type' header as the 'datacontenttype' attribute. The HTTP body is
104108
parsed as event data according to the content type.
@@ -116,7 +120,7 @@ def from_binary(
116120
117121
:param message: HTTPMessage to parse
118122
:param event_format: Format implementation for data deserialization
119-
:param event_factory: Factory function to create CloudEvent instances
123+
:param event_factory: Factory function to create CloudEvent instances (auto-detected if None)
120124
:return: CloudEvent instance
121125
"""
122126
attributes: dict[str, Any] = {}
@@ -130,6 +134,11 @@ def from_binary(
130134
elif normalized_name == CONTENT_TYPE_HEADER:
131135
attributes[DATACONTENTTYPE_ATTR] = header_value
132136

137+
# Auto-detect version if factory not provided
138+
if event_factory is None:
139+
specversion = attributes.get("specversion", SPECVERSION_V1_0)
140+
event_factory = get_event_factory_for_version(specversion)
141+
133142
datacontenttype = attributes.get(DATACONTENTTYPE_ATTR)
134143
data = event_format.read_data(message.body, datacontenttype)
135144

@@ -172,40 +181,51 @@ def to_structured(event: BaseCloudEvent, event_format: Format) -> HTTPMessage:
172181
def from_structured(
173182
message: HTTPMessage,
174183
event_format: Format,
175-
event_factory: EventFactory,
184+
event_factory: EventFactory | None = None,
176185
) -> BaseCloudEvent:
177186
"""
178187
Parse an HTTP structured content mode message to a CloudEvent.
179188
180189
Deserializes the CloudEvent from the HTTP body using the specified format.
181190
Any ce-prefixed headers are ignored as the body contains all event metadata.
182191
192+
If event_factory is not provided, version detection is delegated to the format
193+
implementation, which will auto-detect based on the 'specversion' field.
194+
183195
Example:
184196
>>> from cloudevents.core.v1.event import CloudEvent
185197
>>> from cloudevents.core.formats.json import JSONFormat
186198
>>>
199+
>>> # Explicit factory (recommended for performance)
187200
>>> message = HTTPMessage(
188201
... headers={"content-type": "application/cloudevents+json"},
189202
... body=b'{"type": "com.example.test", "source": "/test", ...}'
190203
... )
191204
>>> event = from_structured(message, JSONFormat(), CloudEvent)
205+
>>>
206+
>>> # Auto-detect version (convenient)
207+
>>> event = from_structured(message, JSONFormat())
192208
193209
:param message: HTTPMessage to parse
194210
:param event_format: Format implementation for deserialization
195-
:param event_factory: Factory function to create CloudEvent instances
211+
:param event_factory: Factory function to create CloudEvent instances.
212+
If None, the format will auto-detect the version.
196213
:return: CloudEvent instance
197214
"""
215+
# Delegate version detection to format layer
198216
return event_format.read(event_factory, message.body)
199217

200218

201219
def from_http(
202220
message: HTTPMessage,
203221
event_format: Format,
204-
event_factory: EventFactory,
222+
event_factory: EventFactory | None = None,
205223
) -> BaseCloudEvent:
206224
"""
207225
Parse an HTTP message to a CloudEvent with automatic mode detection.
208226
227+
Auto-detects CloudEvents version and uses appropriate event factory if not provided.
228+
209229
Automatically detects whether the message uses binary or structured content mode:
210230
- If any ce- prefixed headers are present → binary mode
211231
- Otherwise → structured mode
@@ -233,7 +253,7 @@ def from_http(
233253
234254
:param message: HTTPMessage to parse
235255
:param event_format: Format implementation for deserialization
236-
:param event_factory: Factory function to create CloudEvent instances
256+
:param event_factory: Factory function to create CloudEvent instances (auto-detected if None)
237257
:return: CloudEvent instance
238258
"""
239259
if any(key.lower().startswith(CE_PREFIX) for key in message.headers.keys()):
@@ -271,21 +291,23 @@ def to_binary_event(
271291
def from_binary_event(
272292
message: HTTPMessage,
273293
event_format: Format | None = None,
274-
) -> CloudEvent:
294+
) -> BaseCloudEvent:
275295
"""
276-
Convenience wrapper for from_binary with JSON format and CloudEvent as defaults.
296+
Convenience wrapper for from_binary with JSON format and auto-detection.
297+
298+
Auto-detects CloudEvents version (v0.3 or v1.0) from headers.
277299
278300
Example:
279301
>>> from cloudevents.core.bindings import http
280302
>>> event = http.from_binary_event(message)
281303
282304
:param message: HTTPMessage to parse
283305
:param event_format: Format implementation (defaults to JSONFormat)
284-
:return: CloudEvent instance
306+
:return: CloudEvent instance (v0.3 or v1.0 based on specversion)
285307
"""
286308
if event_format is None:
287309
event_format = JSONFormat()
288-
return from_binary(message, event_format, CloudEvent)
310+
return from_binary(message, event_format, None)
289311

290312

291313
def to_structured_event(
@@ -317,39 +339,41 @@ def to_structured_event(
317339
def from_structured_event(
318340
message: HTTPMessage,
319341
event_format: Format | None = None,
320-
) -> CloudEvent:
342+
) -> BaseCloudEvent:
321343
"""
322-
Convenience wrapper for from_structured with JSON format and CloudEvent as defaults.
344+
Convenience wrapper for from_structured with JSON format and auto-detection.
345+
346+
Auto-detects CloudEvents version (v0.3 or v1.0) from body.
323347
324348
Example:
325349
>>> from cloudevents.core.bindings import http
326350
>>> event = http.from_structured_event(message)
327351
328352
:param message: HTTPMessage to parse
329353
:param event_format: Format implementation (defaults to JSONFormat)
330-
:return: CloudEvent instance
354+
:return: CloudEvent instance (v0.3 or v1.0 based on specversion)
331355
"""
332356
if event_format is None:
333357
event_format = JSONFormat()
334-
return from_structured(message, event_format, CloudEvent)
358+
return from_structured(message, event_format, None)
335359

336360

337361
def from_http_event(
338362
message: HTTPMessage,
339363
event_format: Format | None = None,
340-
) -> CloudEvent:
364+
) -> BaseCloudEvent:
341365
"""
342-
Convenience wrapper for from_http with JSON format and CloudEvent as defaults.
343-
Auto-detects binary or structured mode.
366+
Convenience wrapper for from_http with JSON format and auto-detection.
367+
Auto-detects binary or structured mode, and CloudEvents version.
344368
345369
Example:
346370
>>> from cloudevents.core.bindings import http
347371
>>> event = http.from_http_event(message)
348372
349373
:param message: HTTPMessage to parse
350374
:param event_format: Format implementation (defaults to JSONFormat)
351-
:return: CloudEvent instance
375+
:return: CloudEvent instance (v0.3 or v1.0 based on specversion)
352376
"""
353377
if event_format is None:
354378
event_format = JSONFormat()
355-
return from_http(message, event_format, CloudEvent)
379+
return from_http(message, event_format, None)

0 commit comments

Comments
 (0)