-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add cloud event to core * extensions * raise on both * minor * more changes * Update sdk/core/azure-core/azure/core/messaging.py * comments * changes * test fix * test * comments * lint * mypy * type hint * Apply suggestions from code review * serialize date * fix * fix * fix * Docstring Co-authored-by: Rakshith Bhyravabhotla <[email protected]> * change util * lint * apply black * utilize tz utc * comments * raise on unexpected kwargs * doc * lint * more lint * attrs are optional * add sentinel * falsy object * few more asserts * lint * pyt2 compat * tests * comments * update toc tree * doc * doc * doc * unconditional * test fix * mypy * wrong import * type annotations * data * coment * assets * lint * unnecessary none * format * cast to str * remove cast Co-authored-by: Laurent Mazuel <[email protected]>
- Loading branch information
Showing
9 changed files
with
576 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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): | ||
"""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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=event["source"], | ||
type=event["type"], | ||
specversion=event.get("specversion"), | ||
time=_convert_to_isoformat(event.get("time")), | ||
**kwargs | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
""" | ||
A falsy sentinel object which is supposed to be used to specify attributes | ||
with no data. This gets serialized to `null` on the wire. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.