Skip to content

Commit 303ff1f

Browse files
Rakshith Bhyravabhotlalmazuel
Rakshith Bhyravabhotla
andauthored
Add cloud event to core (#16800)
* 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]>
1 parent 9dc741a commit 303ff1f

File tree

9 files changed

+576
-25
lines changed

9 files changed

+576
-25
lines changed

sdk/core/azure-core/CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# Release History
22

3-
## 1.11.1 (Unreleased)
3+
## 1.12.0 (Unreleased)
44

5+
### Features
6+
7+
- Added `azure.core.messaging.CloudEvent` model that follows the cloud event spec.
8+
- Added `azure.core.serialization.NULL` sentinel value
59

610
## 1.11.0 (2021-02-08)
711

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for
5+
# license information.
6+
# --------------------------------------------------------------------------
7+
import datetime
8+
9+
10+
class _FixedOffset(datetime.tzinfo):
11+
"""Fixed offset in minutes east from UTC.
12+
13+
Copy/pasted from Python doc
14+
15+
:param int offset: offset in minutes
16+
"""
17+
18+
def __init__(self, offset):
19+
self.__offset = datetime.timedelta(minutes=offset)
20+
21+
def utcoffset(self, dt):
22+
return self.__offset
23+
24+
def tzname(self, dt):
25+
return str(self.__offset.total_seconds() / 3600)
26+
27+
def __repr__(self):
28+
return "<FixedOffset {}>".format(self.tzname(None))
29+
30+
def dst(self, dt):
31+
return datetime.timedelta(0)
32+
33+
34+
try:
35+
from datetime import timezone
36+
37+
TZ_UTC = timezone.utc # type: ignore
38+
except ImportError:
39+
TZ_UTC = _FixedOffset(0) # type: ignore
40+
41+
42+
def _convert_to_isoformat(date_time):
43+
"""Deserialize a date in RFC 3339 format to datetime object.
44+
Check https://tools.ietf.org/html/rfc3339#section-5.8 for examples.
45+
"""
46+
if not date_time:
47+
return None
48+
if date_time[-1] == "Z":
49+
delta = 0
50+
timestamp = date_time[:-1]
51+
else:
52+
timestamp = date_time[:-6]
53+
sign, offset = date_time[-6], date_time[-5:]
54+
delta = int(sign + offset[:1]) * 60 + int(sign + offset[-2:])
55+
56+
if delta == 0:
57+
tzinfo = TZ_UTC
58+
else:
59+
try:
60+
tzinfo = datetime.timezone(datetime.timedelta(minutes=delta))
61+
except AttributeError:
62+
tzinfo = _FixedOffset(delta)
63+
64+
try:
65+
deserialized = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
66+
except ValueError:
67+
deserialized = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S")
68+
69+
deserialized = deserialized.replace(tzinfo=tzinfo)
70+
return deserialized

sdk/core/azure-core/azure/core/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
# regenerated.
1010
# --------------------------------------------------------------------------
1111

12-
VERSION = "1.11.1"
12+
VERSION = "1.12.0"
+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for
5+
# license information.
6+
# --------------------------------------------------------------------------
7+
import uuid
8+
from base64 import b64decode
9+
from datetime import datetime
10+
from azure.core._utils import _convert_to_isoformat, TZ_UTC
11+
from azure.core.serialization import NULL
12+
13+
try:
14+
from typing import TYPE_CHECKING, cast, Union
15+
except ImportError:
16+
TYPE_CHECKING = False
17+
18+
if TYPE_CHECKING:
19+
from typing import Any, Optional, Dict
20+
21+
22+
__all__ = ["CloudEvent"]
23+
24+
25+
class CloudEvent(object): # pylint:disable=too-many-instance-attributes
26+
"""Properties of the CloudEvent 1.0 Schema.
27+
All required parameters must be populated in order to send to Azure.
28+
29+
:param source: Required. Identifies the context in which an event happened. The combination of id and source must
30+
be unique for each distinct event. If publishing to a domain topic, source must be the domain name.
31+
:type source: str
32+
:param type: Required. Type of event related to the originating occurrence.
33+
:type type: str
34+
:keyword data: Optional. Event data specific to the event type.
35+
:type data: object
36+
:keyword time: Optional. The time (in UTC) the event was generated.
37+
:type time: ~datetime.datetime
38+
:keyword dataschema: Optional. Identifies the schema that data adheres to.
39+
:type dataschema: str
40+
:keyword datacontenttype: Optional. Content type of data value.
41+
:type datacontenttype: str
42+
:keyword subject: Optional. This describes the subject of the event in the context of the event producer
43+
(identified by source).
44+
:type subject: str
45+
:keyword specversion: Optional. The version of the CloudEvent spec. Defaults to "1.0"
46+
:type specversion: str
47+
:keyword id: Optional. An identifier for the event. The combination of id and source must be
48+
unique for each distinct event. If not provided, a random UUID will be generated and used.
49+
:type id: Optional[str]
50+
:keyword extensions: Optional. A CloudEvent MAY include any number of additional context attributes
51+
with distinct names represented as key - value pairs. Each extension must be alphanumeric, lower cased
52+
and must not exceed the length of 20 characters.
53+
:type extensions: Optional[Dict]
54+
:ivar source: Identifies the context in which an event happened. The combination of id and source must
55+
be unique for each distinct event. If publishing to a domain topic, source must be the domain name.
56+
:vartype source: str
57+
:ivar data: Event data specific to the event type.
58+
:vartype data: object
59+
:ivar type: Type of event related to the originating occurrence.
60+
:vartype type: str
61+
:ivar time: The time (in UTC) the event was generated.
62+
:vartype time: ~datetime.datetime
63+
:ivar dataschema: Identifies the schema that data adheres to.
64+
:vartype dataschema: str
65+
:ivar datacontenttype: Content type of data value.
66+
:vartype datacontenttype: str
67+
:ivar subject: This describes the subject of the event in the context of the event producer
68+
(identified by source).
69+
:vartype subject: str
70+
:ivar specversion: Optional. The version of the CloudEvent spec. Defaults to "1.0"
71+
:vartype specversion: str
72+
:ivar id: An identifier for the event. The combination of id and source must be
73+
unique for each distinct event. If not provided, a random UUID will be generated and used.
74+
:vartype id: str
75+
:ivar extensions: A CloudEvent MAY include any number of additional context attributes
76+
with distinct names represented as key - value pairs. Each extension must be alphanumeric, lower cased
77+
and must not exceed the length of 20 characters.
78+
:vartype extensions: Dict
79+
"""
80+
81+
def __init__(self, source, type, **kwargs): # pylint: disable=redefined-builtin
82+
# type: (str, str, **Any) -> None
83+
self.source = source # type: str
84+
self.type = type # type: str
85+
self.specversion = kwargs.pop("specversion", "1.0") # type: Optional[str]
86+
self.id = kwargs.pop("id", str(uuid.uuid4())) # type: Optional[str]
87+
self.time = kwargs.pop("time", datetime.now(TZ_UTC)) # type: Optional[datetime]
88+
89+
self.datacontenttype = kwargs.pop("datacontenttype", None) # type: Optional[str]
90+
self.dataschema = kwargs.pop("dataschema", None) # type: Optional[str]
91+
self.subject = kwargs.pop("subject", None) # type: Optional[str]
92+
self.data = kwargs.pop("data", None) # type: Optional[object]
93+
94+
try:
95+
self.extensions = kwargs.pop("extensions") # type: Optional[Dict]
96+
for key in self.extensions.keys(): # type:ignore # extensions won't be None here
97+
if not key.islower() or not key.isalnum():
98+
raise ValueError(
99+
"Extension attributes should be lower cased and alphanumeric."
100+
)
101+
except KeyError:
102+
self.extensions = None
103+
104+
if kwargs:
105+
remaining = ", ".join(kwargs.keys())
106+
raise ValueError(
107+
"Unexpected keyword arguments {}. Any extension attributes must be passed explicitly using extensions."
108+
.format(remaining)
109+
)
110+
111+
def __repr__(self):
112+
return "CloudEvent(source={}, type={}, specversion={}, id={}, time={})".format(
113+
self.source, self.type, self.specversion, self.id, self.time
114+
)[:1024]
115+
116+
@classmethod
117+
def from_dict(cls, event):
118+
# type: (Dict) -> CloudEvent
119+
"""
120+
Returns the deserialized CloudEvent object when a dict is provided.
121+
:param event: The dict representation of the event which needs to be deserialized.
122+
:type event: dict
123+
:rtype: CloudEvent
124+
"""
125+
kwargs = {} # type: Dict[Any, Any]
126+
reserved_attr = [
127+
"data",
128+
"data_base64",
129+
"id",
130+
"source",
131+
"type",
132+
"specversion",
133+
"time",
134+
"dataschema",
135+
"datacontenttype",
136+
"subject",
137+
]
138+
139+
if "data" in event and "data_base64" in event:
140+
raise ValueError(
141+
"Invalid input. Only one of data and data_base64 must be present."
142+
)
143+
144+
if "data" in event:
145+
data = event.get("data")
146+
kwargs["data"] = data if data is not None else NULL
147+
elif "data_base64" in event:
148+
kwargs["data"] = b64decode(
149+
cast(Union[str, bytes], event.get("data_base64"))
150+
)
151+
152+
for item in ["datacontenttype", "dataschema", "subject"]:
153+
if item in event:
154+
val = event.get(item)
155+
kwargs[item] = val if val is not None else NULL
156+
157+
extensions = {k: v for k, v in event.items() if k not in reserved_attr}
158+
if extensions:
159+
kwargs["extensions"] = extensions
160+
161+
return cls(
162+
id=event.get("id"),
163+
source=event["source"],
164+
type=event["type"],
165+
specversion=event.get("specversion"),
166+
time=_convert_to_isoformat(event.get("time")),
167+
**kwargs
168+
)

sdk/core/azure-core/azure/core/pipeline/policies/_utils.py

+1-23
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,7 @@
2626
import datetime
2727
import email.utils
2828
from requests.structures import CaseInsensitiveDict
29-
30-
class _FixedOffset(datetime.tzinfo):
31-
"""Fixed offset in minutes east from UTC.
32-
33-
Copy/pasted from Python doc
34-
35-
:param int offset: offset in minutes
36-
"""
37-
38-
def __init__(self, offset):
39-
self.__offset = datetime.timedelta(minutes=offset)
40-
41-
def utcoffset(self, dt):
42-
return self.__offset
43-
44-
def tzname(self, dt):
45-
return str(self.__offset.total_seconds()/3600)
46-
47-
def __repr__(self):
48-
return "<FixedOffset {}>".format(self.tzname(None))
49-
50-
def dst(self, dt):
51-
return datetime.timedelta(0)
29+
from ..._utils import _FixedOffset
5230

5331
def _parse_http_date(text):
5432
"""Parse a HTTP date format into datetime."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for
5+
# license information.
6+
# --------------------------------------------------------------------------
7+
8+
__all__ = ["NULL"]
9+
10+
class _Null(object):
11+
"""To create a Falsy object
12+
"""
13+
def __bool__(self):
14+
return False
15+
16+
__nonzero__ = __bool__ # Python2 compatibility
17+
18+
19+
NULL = _Null()
20+
"""
21+
A falsy sentinel object which is supposed to be used to specify attributes
22+
with no data. This gets serialized to `null` on the wire.
23+
"""

sdk/core/azure-core/doc/azure.core.rst

+15
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ azure.core.exceptions
4141
:members:
4242
:undoc-members:
4343

44+
azure.core.messaging
45+
-------------------
46+
47+
.. automodule:: azure.core.messaging
48+
:members:
49+
:undoc-members:
50+
:inherited-members:
51+
4452
azure.core.paging
4553
-----------------
4654

@@ -57,3 +65,10 @@ azure.core.settings
5765
:undoc-members:
5866
:inherited-members:
5967

68+
azure.core.serialization
69+
-------------------
70+
71+
.. automodule:: azure.core.serialization
72+
:members:
73+
:undoc-members:
74+
:inherited-members:

0 commit comments

Comments
 (0)