-
Notifications
You must be signed in to change notification settings - Fork 3.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
Merged
Merged
Add cloud event to core #16800
Changes from all commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
abe2389
Add cloud event to core
rakshith91 9ed935d
extensions
rakshith91 d11e02f
raise on both
rakshith91 ec3474c
minor
rakshith91 e658ce7
more changes
rakshith91 50de6e6
Update sdk/core/azure-core/azure/core/messaging.py
9f3624b
comments
rakshith91 04acd47
changes
rakshith91 f18f35d
test fix
rakshith91 9400e0e
test
rakshith91 70e08c0
comments
rakshith91 b69cd73
lint
rakshith91 89c80b5
mypy
rakshith91 10d81c3
type hint
rakshith91 a121adc
Apply suggestions from code review
428e35c
serialize date
rakshith91 c88c1e9
fix
rakshith91 f638961
fix
rakshith91 13335c1
fix
rakshith91 2dec996
Docstring
lmazuel c3368d5
change util
rakshith91 f461890
lint
rakshith91 248db3d
apply black
rakshith91 32b2532
utilize tz utc
rakshith91 2441222
comments
rakshith91 666bbd7
raise on unexpected kwargs
rakshith91 45d2a90
doc
rakshith91 8c1f9fc
lint
rakshith91 8dcf54c
more lint
rakshith91 f0d718f
attrs are optional
rakshith91 950ffed
add sentinel
rakshith91 c15a73a
falsy object
rakshith91 a756fe3
few more asserts
rakshith91 6b4a31f
lint
rakshith91 9371f77
pyt2 compat
rakshith91 78696ef
tests
rakshith91 79e74d4
comments
rakshith91 a9c05f4
update toc tree
rakshith91 57b1bd0
doc
rakshith91 e8b53b0
doc
rakshith91 3a16ca9
doc
rakshith91 37e887c
unconditional
rakshith91 8e196ef
test fix
rakshith91 b4c726e
mypy
rakshith91 3f49138
wrong import
rakshith91 22db8b8
type annotations
rakshith91 6e9ce14
data
rakshith91 43a79c1
coment
rakshith91 ba654a0
assets
rakshith91 4f9d80e
lint
rakshith91 0a3aa87
unnecessary none
rakshith91 5051034
format
rakshith91 c7afd6e
cast to str
rakshith91 9613fd4
remove cast
rakshith91 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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): | ||
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 | ||
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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() | ||
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. | ||
| """ | ||
This file contains hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.