Skip to content
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

[EventGrid] Adapt to azure core's CloudEvent #17039

Closed
wants to merge 51 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5bd7aa2
Add cloud event to core
rakshith91 Feb 18, 2021
f4df9a8
extensions
rakshith91 Feb 18, 2021
5a3addd
raise on both
rakshith91 Feb 18, 2021
39da5de
minor
rakshith91 Feb 18, 2021
462762c
more changes
rakshith91 Feb 18, 2021
59ea3b3
Update sdk/core/azure-core/azure/core/messaging.py
Feb 18, 2021
9a911ad
comments
rakshith91 Feb 18, 2021
723bd77
changes
rakshith91 Feb 19, 2021
f3f16f8
test fix
rakshith91 Feb 19, 2021
c5c58e8
test
rakshith91 Feb 19, 2021
1bea681
comments
rakshith91 Feb 21, 2021
3c567c2
lint
rakshith91 Feb 21, 2021
e640917
mypy
rakshith91 Feb 21, 2021
191ad53
type hint
rakshith91 Feb 22, 2021
f83b91c
Apply suggestions from code review
Feb 22, 2021
027f57b
serialize date
rakshith91 Feb 24, 2021
40065ed
fix
rakshith91 Feb 24, 2021
e62acf6
fix
rakshith91 Feb 24, 2021
fb9ff6d
fix
rakshith91 Feb 24, 2021
d9b5d31
Docstring
lmazuel Feb 24, 2021
7e22c21
change util
rakshith91 Feb 24, 2021
6626696
lint
rakshith91 Feb 24, 2021
e5b19cb
apply black
rakshith91 Feb 24, 2021
dc99b5c
utilize tz utc
rakshith91 Feb 25, 2021
b8c61a8
comments
rakshith91 Feb 25, 2021
c9db015
raise on unexpected kwargs
rakshith91 Feb 25, 2021
317ccbb
doc
rakshith91 Feb 25, 2021
f0d7d34
lint
rakshith91 Feb 25, 2021
f21193b
more lint
rakshith91 Feb 25, 2021
6d32a12
attrs are optional
rakshith91 Feb 26, 2021
1223721
add sentinel
rakshith91 Feb 26, 2021
b9b1bb9
falsy object
rakshith91 Feb 28, 2021
d9b9bbb
few more asserts
rakshith91 Feb 28, 2021
45f1b8b
lint
rakshith91 Feb 28, 2021
0b3608c
pyt2 compat
rakshith91 Feb 28, 2021
9acc262
tests
rakshith91 Mar 1, 2021
c5a7ee0
comments
rakshith91 Mar 1, 2021
a92dcc9
update toc tree
rakshith91 Mar 1, 2021
6510b0b
doc
rakshith91 Mar 1, 2021
33a0087
doc
rakshith91 Mar 1, 2021
e372c6b
doc
rakshith91 Mar 1, 2021
f91b18c
unconditional
rakshith91 Mar 1, 2021
07ca924
test fix
rakshith91 Mar 1, 2021
bd72df4
mypy
rakshith91 Mar 1, 2021
2f5f7a5
wrong import
rakshith91 Mar 1, 2021
5bb4ac5
type annotations
rakshith91 Mar 2, 2021
ed2fe64
data
rakshith91 Mar 2, 2021
35c8467
coment
rakshith91 Mar 2, 2021
d7bde2f
assets
rakshith91 Mar 2, 2021
a3e7a0f
lint
rakshith91 Mar 2, 2021
f687fdb
Adapt to cloud event's azure core
rakshith91 Mar 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
172 changes: 172 additions & 0 deletions sdk/core/azure-core/azure/core/messaging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# 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", None),
source=event.get("source", None),
type=event.get("type", None),
specversion=event.get("specversion", None),
time=_convert_to_isoformat(event.get("time", None)),
**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