-
Notifications
You must be signed in to change notification settings - Fork 3k
Add cloud event to core #16800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add cloud event to core #16800
Changes from 53 commits
abe2389
9ed935d
d11e02f
ec3474c
e658ce7
50de6e6
9f3624b
04acd47
f18f35d
9400e0e
70e08c0
b69cd73
89c80b5
10d81c3
a121adc
428e35c
c88c1e9
f638961
13335c1
2dec996
c3368d5
f461890
248db3d
32b2532
2441222
666bbd7
45d2a90
8c1f9fc
8dcf54c
f0d718f
950ffed
c15a73a
a756fe3
6b4a31f
9371f77
78696ef
79e74d4
a9c05f4
57b1bd0
e8b53b0
3a16ca9
37e887c
8e196ef
b4c726e
3f49138
22db8b8
6e9ce14
43a79c1
ba654a0
4f9d80e
0a3aa87
5051034
c7afd6e
9613fd4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# -------------------------------------------------------------------------- | ||
import datetime | ||
|
||
|
||
class _FixedOffset(datetime.tzinfo): | ||
"""Fixed offset in minutes east from UTC. | ||
|
||
Copy/pasted from Python doc | ||
|
||
:param int offset: offset in minutes | ||
""" | ||
|
||
def __init__(self, offset): | ||
self.__offset = datetime.timedelta(minutes=offset) | ||
|
||
def utcoffset(self, dt): | ||
return self.__offset | ||
|
||
def tzname(self, dt): | ||
return str(self.__offset.total_seconds() / 3600) | ||
|
||
def __repr__(self): | ||
return "<FixedOffset {}>".format(self.tzname(None)) | ||
|
||
def dst(self, dt): | ||
return datetime.timedelta(0) | ||
|
||
|
||
try: | ||
from datetime import timezone | ||
|
||
TZ_UTC = timezone.utc # type: ignore | ||
except ImportError: | ||
TZ_UTC = _FixedOffset(0) # type: ignore | ||
|
||
|
||
def _convert_to_isoformat(date_time): | ||
lmazuel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Deserialize a date in RFC 3339 format to datetime object. | ||
Check https://tools.ietf.org/html/rfc3339#section-5.8 for examples. | ||
""" | ||
if not date_time: | ||
return None | ||
if date_time[-1] == "Z": | ||
delta = 0 | ||
timestamp = date_time[:-1] | ||
else: | ||
timestamp = date_time[:-6] | ||
sign, offset = date_time[-6], date_time[-5:] | ||
delta = int(sign + offset[:1]) * 60 + int(sign + offset[-2:]) | ||
|
||
if delta == 0: | ||
tzinfo = TZ_UTC | ||
else: | ||
try: | ||
tzinfo = datetime.timezone(datetime.timedelta(minutes=delta)) | ||
except AttributeError: | ||
tzinfo = _FixedOffset(delta) | ||
|
||
try: | ||
deserialized = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f") | ||
except ValueError: | ||
deserialized = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S") | ||
|
||
deserialized = deserialized.replace(tzinfo=tzinfo) | ||
return deserialized |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# -------------------------------------------------------------------------- | ||
import uuid | ||
from base64 import b64decode | ||
from datetime import datetime | ||
from azure.core._utils import _convert_to_isoformat, TZ_UTC | ||
from azure.core.serialization import NULL | ||
|
||
try: | ||
from typing import TYPE_CHECKING, cast, Union | ||
except ImportError: | ||
TYPE_CHECKING = False | ||
|
||
if TYPE_CHECKING: | ||
from typing import Any, Optional, Dict | ||
|
||
|
||
__all__ = ["CloudEvent"] | ||
|
||
|
||
class CloudEvent(object): # pylint:disable=too-many-instance-attributes | ||
"""Properties of the CloudEvent 1.0 Schema. | ||
All required parameters must be populated in order to send to Azure. | ||
|
||
:param source: Required. Identifies the context in which an event happened. The combination of id and source must | ||
be unique for each distinct event. If publishing to a domain topic, source must be the domain name. | ||
:type source: str | ||
:param type: Required. Type of event related to the originating occurrence. | ||
:type type: str | ||
:keyword data: Optional. Event data specific to the event type. | ||
:type data: object | ||
:keyword time: Optional. The time (in UTC) the event was generated. | ||
:type time: ~datetime.datetime | ||
:keyword dataschema: Optional. Identifies the schema that data adheres to. | ||
:type dataschema: str | ||
:keyword datacontenttype: Optional. Content type of data value. | ||
:type datacontenttype: str | ||
:keyword subject: Optional. This describes the subject of the event in the context of the event producer | ||
(identified by source). | ||
:type subject: str | ||
:keyword specversion: Optional. The version of the CloudEvent spec. Defaults to "1.0" | ||
:type specversion: str | ||
:keyword id: Optional. An identifier for the event. The combination of id and source must be | ||
unique for each distinct event. If not provided, a random UUID will be generated and used. | ||
:type id: Optional[str] | ||
:keyword extensions: Optional. A CloudEvent MAY include any number of additional context attributes | ||
with distinct names represented as key - value pairs. Each extension must be alphanumeric, lower cased | ||
and must not exceed the length of 20 characters. | ||
:type extensions: Optional[Dict] | ||
:ivar source: Identifies the context in which an event happened. The combination of id and source must | ||
be unique for each distinct event. If publishing to a domain topic, source must be the domain name. | ||
:vartype source: str | ||
:ivar data: Event data specific to the event type. | ||
:vartype data: object | ||
:ivar type: Type of event related to the originating occurrence. | ||
:vartype type: str | ||
:ivar time: The time (in UTC) the event was generated. | ||
:vartype time: ~datetime.datetime | ||
:ivar dataschema: Identifies the schema that data adheres to. | ||
:vartype dataschema: str | ||
:ivar datacontenttype: Content type of data value. | ||
:vartype datacontenttype: str | ||
:ivar subject: This describes the subject of the event in the context of the event producer | ||
(identified by source). | ||
:vartype subject: str | ||
:ivar specversion: Optional. The version of the CloudEvent spec. Defaults to "1.0" | ||
:vartype specversion: str | ||
:ivar id: An identifier for the event. The combination of id and source must be | ||
unique for each distinct event. If not provided, a random UUID will be generated and used. | ||
:vartype id: str | ||
:ivar extensions: A CloudEvent MAY include any number of additional context attributes | ||
with distinct names represented as key - value pairs. Each extension must be alphanumeric, lower cased | ||
and must not exceed the length of 20 characters. | ||
:vartype extensions: Dict | ||
""" | ||
|
||
def __init__(self, source, type, **kwargs): # pylint: disable=redefined-builtin | ||
# type: (str, str, **Any) -> None | ||
self.source = source # type: str | ||
self.type = type # type: str | ||
self.specversion = kwargs.pop("specversion", "1.0") # type: Optional[str] | ||
self.id = kwargs.pop("id", str(uuid.uuid4())) # type: Optional[str] | ||
self.time = kwargs.pop("time", datetime.now(TZ_UTC)) # type: Optional[datetime] | ||
|
||
self.datacontenttype = kwargs.pop("datacontenttype", None) # type: Optional[str] | ||
self.dataschema = kwargs.pop("dataschema", None) # type: Optional[str] | ||
self.subject = kwargs.pop("subject", None) # type: Optional[str] | ||
self.data = kwargs.pop("data", None) # type: Optional[object] | ||
|
||
try: | ||
self.extensions = kwargs.pop("extensions") # type: Optional[Dict] | ||
for key in self.extensions.keys(): # type:ignore # extensions won't be None here | ||
if not key.islower() or not key.isalnum(): | ||
raise ValueError( | ||
"Extension attributes should be lower cased and alphanumeric." | ||
) | ||
except KeyError: | ||
self.extensions = None | ||
|
||
if kwargs: | ||
remaining = ", ".join(kwargs.keys()) | ||
raise ValueError( | ||
"Unexpected keyword arguments {}. Any extension attributes must be passed explicitly using extensions." | ||
.format(remaining) | ||
) | ||
|
||
def __repr__(self): | ||
return "CloudEvent(source={}, type={}, specversion={}, id={}, time={})".format( | ||
self.source, self.type, self.specversion, self.id, self.time | ||
)[:1024] | ||
|
||
@classmethod | ||
def from_dict(cls, event): | ||
# type: (Dict) -> CloudEvent | ||
""" | ||
Returns the deserialized CloudEvent object when a dict is provided. | ||
:param event: The dict representation of the event which needs to be deserialized. | ||
:type event: dict | ||
:rtype: CloudEvent | ||
""" | ||
kwargs = {} # type: Dict[Any, Any] | ||
reserved_attr = [ | ||
"data", | ||
"data_base64", | ||
"id", | ||
"source", | ||
"type", | ||
"specversion", | ||
"time", | ||
"dataschema", | ||
"datacontenttype", | ||
"subject", | ||
] | ||
|
||
if "data" in event and "data_base64" in event: | ||
raise ValueError( | ||
"Invalid input. Only one of data and data_base64 must be present." | ||
) | ||
|
||
if "data" in event: | ||
data = event.get("data") | ||
kwargs["data"] = data if data is not None else NULL | ||
elif "data_base64" in event: | ||
kwargs["data"] = b64decode( | ||
cast(Union[str, bytes], event.get("data_base64")) | ||
) | ||
|
||
for item in ["datacontenttype", "dataschema", "subject"]: | ||
if item in event: | ||
val = event.get(item) | ||
kwargs[item] = val if val is not None else NULL | ||
|
||
extensions = {k: v for k, v in event.items() if k not in reserved_attr} | ||
if extensions: | ||
kwargs["extensions"] = extensions | ||
|
||
return cls( | ||
id=event.get("id"), | ||
source=cast(str, event.get("source")), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. left it to event["source"] |
||
type=cast(str, event.get("type")), | ||
specversion=event.get("specversion"), | ||
time=_convert_to_isoformat(event.get("time")), | ||
**kwargs | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# -------------------------------------------------------------------------- | ||
|
||
__all__ = ["NULL"] | ||
|
||
class _Null(object): | ||
"""To create a Falsy object | ||
""" | ||
def __bool__(self): | ||
return False | ||
|
||
__nonzero__ = __bool__ # Python2 compatibility | ||
|
||
|
||
NULL = _Null() | ||
johanste marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
A falsy sentinel object which is supposed to be used to specify attributes | ||
with no data. This gets serialized to `null` on the wire. | ||
""" |
Uh oh!
There was an error while loading. Please reload this page.