Skip to content

Commit

Permalink
Add cloud event to core (#16800)
Browse files Browse the repository at this point in the history
* 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
Rakshith Bhyravabhotla and lmazuel authored Mar 3, 2021
1 parent 9dc741a commit 303ff1f
Show file tree
Hide file tree
Showing 9 changed files with 576 additions and 25 deletions.
6 changes: 5 additions & 1 deletion sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Release History

## 1.11.1 (Unreleased)
## 1.12.0 (Unreleased)

### Features

- Added `azure.core.messaging.CloudEvent` model that follows the cloud event spec.
- Added `azure.core.serialization.NULL` sentinel value

## 1.11.0 (2021-02-08)

Expand Down
70 changes: 70 additions & 0 deletions sdk/core/azure-core/azure/core/_utils.py
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
2 changes: 1 addition & 1 deletion sdk/core/azure-core/azure/core/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
# regenerated.
# --------------------------------------------------------------------------

VERSION = "1.11.1"
VERSION = "1.12.0"
168 changes: 168 additions & 0 deletions sdk/core/azure-core/azure/core/messaging.py
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
)
24 changes: 1 addition & 23 deletions sdk/core/azure-core/azure/core/pipeline/policies/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,7 @@
import datetime
import email.utils
from requests.structures import CaseInsensitiveDict

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)
from ..._utils import _FixedOffset

def _parse_http_date(text):
"""Parse a HTTP date format into datetime."""
Expand Down
23 changes: 23 additions & 0 deletions sdk/core/azure-core/azure/core/serialization.py
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.
"""
15 changes: 15 additions & 0 deletions sdk/core/azure-core/doc/azure.core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ azure.core.exceptions
:members:
:undoc-members:

azure.core.messaging
-------------------

.. automodule:: azure.core.messaging
:members:
:undoc-members:
:inherited-members:

azure.core.paging
-----------------

Expand All @@ -57,3 +65,10 @@ azure.core.settings
:undoc-members:
:inherited-members:

azure.core.serialization
-------------------

.. automodule:: azure.core.serialization
:members:
:undoc-members:
:inherited-members:
Loading

0 comments on commit 303ff1f

Please sign in to comment.