diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 90e3b12f5520..7a2d0c81144d 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -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) diff --git a/sdk/core/azure-core/azure/core/_utils.py b/sdk/core/azure-core/azure/core/_utils.py new file mode 100644 index 000000000000..9178d4e5c7f1 --- /dev/null +++ b/sdk/core/azure-core/azure/core/_utils.py @@ -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 "".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 diff --git a/sdk/core/azure-core/azure/core/_version.py b/sdk/core/azure-core/azure/core/_version.py index 14d127f747d9..7643b787eff9 100644 --- a/sdk/core/azure-core/azure/core/_version.py +++ b/sdk/core/azure-core/azure/core/_version.py @@ -9,4 +9,4 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "1.11.1" +VERSION = "1.12.0" diff --git a/sdk/core/azure-core/azure/core/messaging.py b/sdk/core/azure-core/azure/core/messaging.py new file mode 100644 index 000000000000..3a4e2295903a --- /dev/null +++ b/sdk/core/azure-core/azure/core/messaging.py @@ -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 + ) diff --git a/sdk/core/azure-core/azure/core/pipeline/policies/_utils.py b/sdk/core/azure-core/azure/core/pipeline/policies/_utils.py index 173f19869804..76ee690c1d8f 100644 --- a/sdk/core/azure-core/azure/core/pipeline/policies/_utils.py +++ b/sdk/core/azure-core/azure/core/pipeline/policies/_utils.py @@ -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 "".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.""" diff --git a/sdk/core/azure-core/azure/core/serialization.py b/sdk/core/azure-core/azure/core/serialization.py new file mode 100644 index 000000000000..c3422efa0c27 --- /dev/null +++ b/sdk/core/azure-core/azure/core/serialization.py @@ -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. +""" diff --git a/sdk/core/azure-core/doc/azure.core.rst b/sdk/core/azure-core/doc/azure.core.rst index 26b0607f3ca4..36716ad49c62 100644 --- a/sdk/core/azure-core/doc/azure.core.rst +++ b/sdk/core/azure-core/doc/azure.core.rst @@ -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 ----------------- @@ -57,3 +65,10 @@ azure.core.settings :undoc-members: :inherited-members: +azure.core.serialization +------------------- + +.. automodule:: azure.core.serialization + :members: + :undoc-members: + :inherited-members: diff --git a/sdk/core/azure-core/tests/test_messaging_cloud_event.py b/sdk/core/azure-core/tests/test_messaging_cloud_event.py new file mode 100644 index 000000000000..c0a88b845488 --- /dev/null +++ b/sdk/core/azure-core/tests/test_messaging_cloud_event.py @@ -0,0 +1,282 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import logging +import sys +import os +import pytest +import json +import datetime + +from azure.core.messaging import CloudEvent +from azure.core.serialization import NULL + +# Cloud Event tests +def test_cloud_event_constructor(): + event = CloudEvent( + source='Azure.Core.Sample', + type='SampleType', + data='cloudevent' + ) + + assert event.specversion == '1.0' + assert event.time.__class__ == datetime.datetime + assert event.id is not None + assert event.source == 'Azure.Core.Sample' + assert event.data == 'cloudevent' + +def test_cloud_event_constructor_unexpected_keyword(): + with pytest.raises(ValueError) as e: + event = CloudEvent( + source='Azure.Core.Sample', + type='SampleType', + data='cloudevent', + unexpected_keyword="not allowed", + another_bad_kwarg="not allowed either" + ) + assert "unexpected_keyword" in e + assert "another_bad_kwarg" in e + +def test_cloud_event_constructor_blank_data(): + event = CloudEvent( + source='Azure.Core.Sample', + type='SampleType', + data='' + ) + + assert event.specversion == '1.0' + assert event.time.__class__ == datetime.datetime + assert event.id is not None + assert event.source == 'Azure.Core.Sample' + assert event.data == '' + +def test_cloud_event_constructor_NULL_data(): + event = CloudEvent( + source='Azure.Core.Sample', + type='SampleType', + data=NULL + ) + + assert event.data == NULL + assert event.data is NULL + +def test_cloud_event_constructor_none_data(): + event = CloudEvent( + source='Azure.Core.Sample', + type='SampleType', + data=None + ) + + assert event.data == None + +def test_cloud_event_constructor_missing_data(): + event = CloudEvent( + source='Azure.Core.Sample', + type='SampleType', + ) + + assert event.data == None + assert event.datacontenttype == None + assert event.dataschema == None + assert event.subject == None + +def test_cloud_storage_dict(): + cloud_storage_dict = { + "id":"a0517898-9fa4-4e70-b4a3-afda1dd68672", + "source":"/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.Storage/storageAccounts/{storage-account}", + "data":{ + "api":"PutBlockList", + "client_request_id":"6d79dbfb-0e37-4fc4-981f-442c9ca65760", + "request_id":"831e1650-001e-001b-66ab-eeb76e000000", + "e_tag":"0x8D4BCC2E4835CD0", + "content_type":"application/octet-stream", + "content_length":524288, + "blob_type":"BlockBlob", + "url":"https://oc2d2817345i60006.blob.core.windows.net/oc2d2817345i200097container/oc2d2817345i20002296blob", + "sequencer":"00000000000004420000000000028963", + "storage_diagnostics":{"batchId":"b68529f3-68cd-4744-baa4-3c0498ec19f0"} + }, + "type":"Microsoft.Storage.BlobCreated", + "time":"2021-02-18T20:18:10.53986Z", + "specversion":"1.0" + } + + event = CloudEvent.from_dict(cloud_storage_dict) + assert event.data == { + "api":"PutBlockList", + "client_request_id":"6d79dbfb-0e37-4fc4-981f-442c9ca65760", + "request_id":"831e1650-001e-001b-66ab-eeb76e000000", + "e_tag":"0x8D4BCC2E4835CD0", + "content_type":"application/octet-stream", + "content_length":524288, + "blob_type":"BlockBlob", + "url":"https://oc2d2817345i60006.blob.core.windows.net/oc2d2817345i200097container/oc2d2817345i20002296blob", + "sequencer":"00000000000004420000000000028963", + "storage_diagnostics":{"batchId":"b68529f3-68cd-4744-baa4-3c0498ec19f0"} + } + assert event.specversion == "1.0" + assert event.time.__class__ == datetime.datetime + assert event.time.month == 2 + assert event.time.day == 18 + assert event.time.hour == 20 + assert event.__class__ == CloudEvent + assert "id" in cloud_storage_dict + assert "data" in cloud_storage_dict + + +def test_cloud_custom_dict_with_extensions(): + cloud_custom_dict_with_extensions = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "data":{"team": "event grid squad"}, + "type":"Azure.Sdk.Sample", + "time":"2021-02-18T20:18:10.53986+00:00", + "specversion":"1.0", + "ext1": "example", + "ext2": "example2" + } + event = CloudEvent.from_dict(cloud_custom_dict_with_extensions) + assert event.data == {"team": "event grid squad"} + assert event.__class__ == CloudEvent + assert event.time.month == 2 + assert event.time.day == 18 + assert event.time.hour == 20 + assert event.extensions == {"ext1": "example", "ext2": "example2"} + +def test_cloud_custom_dict_blank_data(): + cloud_custom_dict_with_extensions = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "data":'', + "type":"Azure.Sdk.Sample", + "time":"2021-02-18T20:18:10+00:00", + "specversion":"1.0", + } + event = CloudEvent.from_dict(cloud_custom_dict_with_extensions) + assert event.data == '' + assert event.__class__ == CloudEvent + +def test_cloud_custom_dict_no_data(): + cloud_custom_dict_with_missing_data = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "type":"Azure.Sdk.Sample", + "time":"2021-02-18T20:18:10+00:00", + "specversion":"1.0", + } + event = CloudEvent.from_dict(cloud_custom_dict_with_missing_data) + assert event.__class__ == CloudEvent + assert event.data is None + +def test_cloud_custom_dict_null_data(): + cloud_custom_dict_with_none_data = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "type":"Azure.Sdk.Sample", + "data":None, + "dataschema":None, + "time":"2021-02-18T20:18:10+00:00", + "specversion":"1.0", + } + event = CloudEvent.from_dict(cloud_custom_dict_with_none_data) + assert event.__class__ == CloudEvent + assert event.data == NULL + assert event.dataschema is NULL + +def test_cloud_custom_dict_valid_optional_attrs(): + cloud_custom_dict_with_none_data = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "type":"Azure.Sdk.Sample", + "data":None, + "dataschema":"exists", + "time":"2021-02-18T20:18:10+00:00", + "specversion":"1.0", + } + event = CloudEvent.from_dict(cloud_custom_dict_with_none_data) + assert event.__class__ == CloudEvent + assert event.data is NULL + assert event.dataschema == "exists" + +def test_cloud_custom_dict_both_data_and_base64(): + cloud_custom_dict_with_data_and_base64 = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "data":"abc", + "data_base64":"Y2Wa==", + "type":"Azure.Sdk.Sample", + "time":"2021-02-18T20:18:10+00:00", + "specversion":"1.0", + } + with pytest.raises(ValueError): + event = CloudEvent.from_dict(cloud_custom_dict_with_data_and_base64) + +def test_cloud_custom_dict_base64(): + cloud_custom_dict_base64 = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "data_base64":'Y2xvdWRldmVudA==', + "type":"Azure.Sdk.Sample", + "time":"2021-02-23T17:11:13.308772-08:00", + "specversion":"1.0" + } + event = CloudEvent.from_dict(cloud_custom_dict_base64) + assert event.data == b'cloudevent' + assert event.specversion == "1.0" + assert event.time.hour == 17 + assert event.time.minute == 11 + assert event.time.day == 23 + assert event.time.tzinfo is not None + assert event.__class__ == CloudEvent + +def test_data_and_base64_both_exist_raises(): + with pytest.raises(ValueError): + CloudEvent.from_dict( + {"source":'sample', + "type":'type', + "data":'data', + "data_base64":'Y2kQ==' + } + ) + +def test_cloud_event_repr(): + event = CloudEvent( + source='Azure.Core.Sample', + type='SampleType', + data='cloudevent' + ) + + assert repr(event).startswith("CloudEvent(source=Azure.Core.Sample, type=SampleType, specversion=1.0,") + +def test_extensions_upper_case_value_error(): + with pytest.raises(ValueError): + event = CloudEvent( + source='sample', + type='type', + data='data', + extensions={"lowercase123": "accepted", "NOTlower123": "not allowed"} + ) + +def test_extensions_not_alphanumeric_value_error(): + with pytest.raises(ValueError): + event = CloudEvent( + source='sample', + type='type', + data='data', + extensions={"lowercase123": "accepted", "not@lph@nu^^3ic": "not allowed"} + ) + +def test_cloud_from_dict_with_invalid_extensions(): + cloud_custom_dict_with_extensions = { + "id":"de0fd76c-4ef4-4dfb-ab3a-8f24a307e033", + "source":"https://egtest.dev/cloudcustomevent", + "data":{"team": "event grid squad"}, + "type":"Azure.Sdk.Sample", + "time":"2020-08-07T02:06:08.11969Z", + "specversion":"1.0", + "ext1": "example", + "BADext2": "example2" + } + with pytest.raises(ValueError): + event = CloudEvent.from_dict(cloud_custom_dict_with_extensions) diff --git a/sdk/core/azure-core/tests/test_serialization.py b/sdk/core/azure-core/tests/test_serialization.py new file mode 100644 index 000000000000..7ac58850cd91 --- /dev/null +++ b/sdk/core/azure-core/tests/test_serialization.py @@ -0,0 +1,11 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from azure.core.serialization import NULL + +def test_NULL_is_falsy(): + assert NULL is not False + assert bool(NULL) is False + assert NULL is NULL \ No newline at end of file diff --git a/sdk/eventgrid/azure-eventgrid/CHANGELOG.md b/sdk/eventgrid/azure-eventgrid/CHANGELOG.md index 7826d7f3b0a6..3db74e36cd88 100644 --- a/sdk/eventgrid/azure-eventgrid/CHANGELOG.md +++ b/sdk/eventgrid/azure-eventgrid/CHANGELOG.md @@ -2,6 +2,8 @@ ## 2.0.0b6 (Unreleased) + **Breaking Changes** + - `~azure.eventgrid.CloudEvent` is now removed in favor of `~azure.core.messaging.CloudEvent`. ## 2.0.0b5 (2021-02-10) diff --git a/sdk/eventgrid/azure-eventgrid/README.md b/sdk/eventgrid/azure-eventgrid/README.md index 9629b210e18b..813cf6208a5e 100644 --- a/sdk/eventgrid/azure-eventgrid/README.md +++ b/sdk/eventgrid/azure-eventgrid/README.md @@ -145,7 +145,8 @@ This example publishes a Cloud event. ```Python import os from azure.core.credentials import AzureKeyCredential -from azure.eventgrid import EventGridPublisherClient, CloudEvent +from azure.core.messaging import CloudEvent +from azure.eventgrid import EventGridPublisherClient key = os.environ["CLOUD_ACCESS_KEY"] endpoint = os.environ["CLOUD_TOPIC_HOSTNAME"] @@ -166,7 +167,7 @@ client.send(event) This example consumes a message received from storage queue and deserializes it to a CloudEvent object. ```Python -from azure.eventgrid import CloudEvent +from azure.core.messaging import CloudEvent from azure.storage.queue import QueueServiceClient, BinaryBase64DecodePolicy import os import json @@ -244,7 +245,8 @@ Once the `tracer` and `exporter` are set, please follow the example below to sta ```python import os -from azure.eventgrid import EventGridPublisherClient, CloudEvent +from azure.eventgrid import EventGridPublisherClient +from azure.core.messaging import CloudEvent from azure.core.credentials import AzureKeyCredential hostname = os.environ['CLOUD_TOPIC_HOSTNAME'] diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/__init__.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/__init__.py index bb031e5ec320..1dc3655a13bb 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/__init__.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/__init__.py @@ -7,12 +7,11 @@ from ._publisher_client import EventGridPublisherClient from ._event_mappings import SystemEventNames from ._helpers import generate_sas -from ._models import CloudEvent, EventGridEvent +from ._models import EventGridEvent from ._version import VERSION __all__ = [ "EventGridPublisherClient", - "CloudEvent", "EventGridEvent", "generate_sas", "SystemEventNames", diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_helpers.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_helpers.py index 2780ceb569bb..a1a82d5bfac9 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_helpers.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_helpers.py @@ -18,6 +18,10 @@ from ._signature_credential_policy import EventGridSasCredentialPolicy from . import _constants as constants +from ._generated.models import ( + CloudEvent as InternalCloudEvent, +) + if TYPE_CHECKING: from datetime import datetime @@ -134,3 +138,25 @@ def _eventgrid_data_typecheck(event): "Data in EventGridEvent cannot be bytes. Please refer to" "https://docs.microsoft.com/en-us/azure/event-grid/event-schema" ) + +def _cloud_event_to_generated(cloud_event, **kwargs): + if isinstance(cloud_event.data, six.binary_type): + data_base64 = cloud_event.data + data = None + else: + data = cloud_event.data + data_base64 = None + return InternalCloudEvent( + id=cloud_event.id, + source=cloud_event.source, + type=cloud_event.type, + specversion=cloud_event.specversion, + data=data, + data_base64=data_base64, + time=cloud_event.time, + dataschema=cloud_event.dataschema, + datacontenttype=cloud_event.datacontenttype, + subject=cloud_event.subject, + additional_properties=cloud_event.extensions, + **kwargs + ) diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_models.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_models.py index 5154ed979fa4..61b74a4d6345 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_models.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_models.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- # pylint:disable=protected-access -from typing import Union, Any, Dict +from typing import Any import datetime as dt import uuid import json @@ -11,152 +11,10 @@ from msrest.serialization import UTC from ._generated.models import ( EventGridEvent as InternalEventGridEvent, - CloudEvent as InternalCloudEvent, ) -class EventMixin(object): - """ - Mixin for the event models comprising of some helper methods. - """ - - @staticmethod - def _from_json(event, encode): - """ - Load the event into the json - :param dict eventgrid_event: The event to be deserialized. - :type eventgrid_event: Union[str, dict, bytes] - :param str encode: The encoding to be used. Defaults to 'utf-8' - """ - if isinstance(event, six.binary_type): - event = json.loads(event.decode(encode)) - elif isinstance(event, six.string_types): - event = json.loads(event) - return event - - -class CloudEvent(EventMixin): # pylint:disable=too-many-instance-attributes - """Properties of an event published to an Event Grid topic using the CloudEvent 1.0 Schema. - - All required parameters must be populated in order to send to Azure. - If data is of binary type, data_base64 can be used alternatively. Note that data and data_base64 - cannot be present at the same time. - - :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. Only one of the `data` or `data_base64` - argument must be present. If data is of bytes type, it will be sent as data_base64 in the outgoing request. - :type data: object - :keyword time: Optional. The time (in UTC) the event was generated, in RFC3339 format. - :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 data_base64: Optional. Event data specific to the event type if the data is of bytes type. - Only data of bytes type is accepted by `data-base64` and only one of the `data` or `data_base64` argument - must be present. - :type data_base64: bytes - :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 data_base64: Event data specific to the event type if the data is of bytes type. - :vartype data_base64: bytes - :ivar type: Type of event related to the originating occurrence. - :vartype type: str - :ivar time: The time (in UTC) the event was generated, in RFC3339 format. - :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: Optional[str] - """ - - def __init__(self, source, type, **kwargs): # pylint: disable=redefined-builtin - # type: (str, str, Any) -> None - self.source = source - self.type = type - self.specversion = kwargs.pop("specversion", "1.0") - self.id = kwargs.pop("id", str(uuid.uuid4())) - self.time = kwargs.pop("time", dt.datetime.now(UTC()).isoformat()) - self.data = kwargs.pop("data", None) - self.datacontenttype = kwargs.pop("datacontenttype", None) - self.dataschema = kwargs.pop("dataschema", None) - self.subject = kwargs.pop("subject", None) - self.data_base64 = kwargs.pop("data_base64", None) - self.extensions = {} - self.extensions.update(dict(kwargs.pop("extensions", {}))) - if self.data is not None and self.data_base64 is not None: - raise ValueError( - "data and data_base64 cannot be provided at the same time.\ - Use data_base64 only if you are sending bytes, and use data otherwise." - ) - - @classmethod - def _from_generated(cls, cloud_event, **kwargs): - # type: (Union[str, Dict, bytes], Any) -> CloudEvent - generated = InternalCloudEvent.deserialize(cloud_event) - if generated.additional_properties: - extensions = dict(generated.additional_properties) - kwargs.setdefault("extensions", extensions) - return cls( - id=generated.id, - source=generated.source, - type=generated.type, - specversion=generated.specversion, - data=generated.data or generated.data_base64, - time=generated.time, - dataschema=generated.dataschema, - datacontenttype=generated.datacontenttype, - subject=generated.subject, - **kwargs - ) - - def _to_generated(self, **kwargs): - if isinstance(self.data, six.binary_type): - data_base64 = self.data - data = None - else: - data = self.data - data_base64 = None - return InternalCloudEvent( - id=self.id, - source=self.source, - type=self.type, - specversion=self.specversion, - data=data, - data_base64=self.data_base64 or data_base64, - time=self.time, - dataschema=self.dataschema, - datacontenttype=self.datacontenttype, - subject=self.subject, - additional_properties=self.extensions, - **kwargs - ) - - -class EventGridEvent(InternalEventGridEvent, EventMixin): +class EventGridEvent(InternalEventGridEvent): """Properties of an event published to an Event Grid topic using the EventGrid Schema. Variables are only populated by the server, and will be ignored when sending a request. diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_publisher_client.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_publisher_client.py index 20b8b04df3b0..482699f9068a 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_publisher_client.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_publisher_client.py @@ -21,14 +21,16 @@ HttpLoggingPolicy, UserAgentPolicy, ) +from azure.core.messaging import CloudEvent -from ._models import CloudEvent, EventGridEvent +from ._models import EventGridEvent from ._helpers import ( _get_endpoint_only_fqdn, _get_authentication_policy, _is_cloud_event, _is_eventgrid_event, _eventgrid_data_typecheck, + _cloud_event_to_generated, ) from ._generated._event_grid_publisher_client import ( EventGridPublisherClient as EventGridPublisherClientImpl, @@ -179,7 +181,7 @@ def send(self, events, **kwargs): if isinstance(events[0], CloudEvent) or _is_cloud_event(events[0]): try: events = [ - cast(CloudEvent, e)._to_generated(**kwargs) for e in events # pylint: disable=protected-access + _cloud_event_to_generated(e, **kwargs) for e in events # pylint: disable=protected-access ] except AttributeError: pass # means it's a dictionary diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/aio/_publisher_client_async.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/aio/_publisher_client_async.py index 5029e9a5f963..2ae3d973bf7b 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/aio/_publisher_client_async.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/aio/_publisher_client_async.py @@ -9,6 +9,7 @@ from typing import Any, Union, List, Dict, cast from azure.core.credentials import AzureKeyCredential, AzureSasCredential from azure.core.tracing.decorator_async import distributed_trace_async +from azure.core.messaging import CloudEvent from azure.core.pipeline.policies import ( RequestIdPolicy, HeadersPolicy, @@ -23,13 +24,14 @@ UserAgentPolicy, ) from .._policies import CloudEventDistributedTracingPolicy -from .._models import CloudEvent, EventGridEvent +from .._models import EventGridEvent from .._helpers import ( _get_endpoint_only_fqdn, _get_authentication_policy, _is_cloud_event, _is_eventgrid_event, _eventgrid_data_typecheck, + _cloud_event_to_generated, ) from .._generated.aio import EventGridPublisherClient as EventGridPublisherClientAsync from .._version import VERSION @@ -172,7 +174,7 @@ async def send(self, events: SendType, **kwargs: Any) -> None: if isinstance(events[0], CloudEvent) or _is_cloud_event(events[0]): try: events = [ - cast(CloudEvent, e)._to_generated(**kwargs) for e in events # pylint: disable=protected-access + _cloud_event_to_generated(e, **kwargs) for e in events # pylint: disable=protected-access ] except AttributeError: pass # means it's a dictionary diff --git a/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_cloud_event_using_dict_async.py b/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_cloud_event_using_dict_async.py index db958983fb53..63f36c5ecb80 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_cloud_event_using_dict_async.py +++ b/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_cloud_event_using_dict_async.py @@ -17,7 +17,7 @@ """ import os import asyncio -from azure.eventgrid import CloudEvent +from azure.core.messaging import CloudEvent from azure.eventgrid.aio import EventGridPublisherClient from azure.core.credentials import AzureKeyCredential diff --git a/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_events_using_cloud_events_1.0_schema_async.py b/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_events_using_cloud_events_1.0_schema_async.py index 24a881e51eb5..f67bf1a19df5 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_events_using_cloud_events_1.0_schema_async.py +++ b/sdk/eventgrid/azure-eventgrid/samples/async_samples/sample_publish_events_using_cloud_events_1.0_schema_async.py @@ -17,7 +17,7 @@ # [START publish_cloud_event_to_topic_async] import os import asyncio -from azure.eventgrid import CloudEvent +from azure.core.messaging import CloudEvent from azure.eventgrid.aio import EventGridPublisherClient from azure.core.credentials import AzureKeyCredential diff --git a/sdk/eventgrid/azure-eventgrid/samples/consume_samples/consume_cloud_events_from_storage_queue.py b/sdk/eventgrid/azure-eventgrid/samples/consume_samples/consume_cloud_events_from_storage_queue.py index 919183405d4d..511188b66df5 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/consume_samples/consume_cloud_events_from_storage_queue.py +++ b/sdk/eventgrid/azure-eventgrid/samples/consume_samples/consume_cloud_events_from_storage_queue.py @@ -14,7 +14,7 @@ 3) STORAGE_QUEUE_NAME: The name of the storage queue. """ -from azure.eventgrid import CloudEvent +from azure.core.messaging import CloudEvent from azure.storage.queue import QueueServiceClient, BinaryBase64DecodePolicy import os import json diff --git a/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_custom_topic_sample.py b/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_custom_topic_sample.py index 108904daa05c..fbd7aa72a024 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_custom_topic_sample.py +++ b/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_custom_topic_sample.py @@ -20,7 +20,8 @@ import time from azure.core.credentials import AzureKeyCredential -from azure.eventgrid import EventGridPublisherClient, CloudEvent +from azure.core.messaging import CloudEvent +from azure.eventgrid import EventGridPublisherClient key = os.environ.get("CLOUD_ACCESS_KEY") endpoint = os.environ["CLOUD_TOPIC_HOSTNAME"] diff --git a/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_domain_topic_sample.py b/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_domain_topic_sample.py index 9f71e67c752f..3cc781ac04be 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_domain_topic_sample.py +++ b/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_cloud_events_to_domain_topic_sample.py @@ -22,7 +22,8 @@ import time from azure.core.credentials import AzureKeyCredential -from azure.eventgrid import EventGridPublisherClient, CloudEvent +from azure.core.messaging import CloudEvent +from azure.eventgrid import EventGridPublisherClient domain_key = os.environ["DOMAIN_ACCESS_KEY"] domain_endpoint = os.environ["DOMAIN_TOPIC_HOSTNAME"] diff --git a/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_with_shared_access_signature_sample.py b/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_with_shared_access_signature_sample.py index 5782a2c65168..4d147a1aa75b 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_with_shared_access_signature_sample.py +++ b/sdk/eventgrid/azure-eventgrid/samples/publish_samples/publish_with_shared_access_signature_sample.py @@ -21,7 +21,8 @@ from datetime import datetime, timedelta from azure.core.credentials import AzureSasCredential -from azure.eventgrid import EventGridPublisherClient, CloudEvent, generate_sas +from azure.core.messaging import CloudEvent +from azure.eventgrid import EventGridPublisherClient, generate_sas key = os.environ["CLOUD_ACCESS_KEY"] endpoint = os.environ["CLOUD_TOPIC_HOSTNAME"] diff --git a/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_consume_custom_payload.py b/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_consume_custom_payload.py index 70ae25278024..b9559e94ffd7 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_consume_custom_payload.py +++ b/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_consume_custom_payload.py @@ -12,7 +12,7 @@ python sample_consume_custom_payload.py """ -from azure.eventgrid import CloudEvent +from azure.core.messaging import CloudEvent import json # all types of CloudEvents below produce same DeserializedEvent diff --git a/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_publish_events_using_cloud_events_1.0_schema.py b/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_publish_events_using_cloud_events_1.0_schema.py index c2147ce66238..f27cf822e6a5 100644 --- a/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_publish_events_using_cloud_events_1.0_schema.py +++ b/sdk/eventgrid/azure-eventgrid/samples/sync_samples/sample_publish_events_using_cloud_events_1.0_schema.py @@ -16,8 +16,9 @@ """ # [START publish_cloud_event_to_topic] import os -from azure.eventgrid import EventGridPublisherClient, CloudEvent +from azure.eventgrid import EventGridPublisherClient from azure.core.credentials import AzureKeyCredential +from azure.core.messaging import CloudEvent topic_key = os.environ["CLOUD_ACCESS_KEY"] endpoint = os.environ["CLOUD_TOPIC_HOSTNAME"] diff --git a/sdk/eventgrid/azure-eventgrid/setup.py b/sdk/eventgrid/azure-eventgrid/setup.py index 205d3ceec27c..36e7f2dee297 100644 --- a/sdk/eventgrid/azure-eventgrid/setup.py +++ b/sdk/eventgrid/azure-eventgrid/setup.py @@ -82,7 +82,7 @@ ]), install_requires=[ 'msrest>=0.6.19', - 'azure-core<2.0.0,>=1.10.0', + 'azure-core<2.0.0,>=1.12.0', ], extras_require={ ":python_version<'3.0'": ['azure-nspkg'], diff --git a/sdk/eventgrid/azure-eventgrid/tests/test_cloud_event_tracing.py b/sdk/eventgrid/azure-eventgrid/tests/test_cloud_event_tracing.py index 9032189d0658..feed7d9865d8 100644 --- a/sdk/eventgrid/azure-eventgrid/tests/test_cloud_event_tracing.py +++ b/sdk/eventgrid/azure-eventgrid/tests/test_cloud_event_tracing.py @@ -12,7 +12,7 @@ PipelineContext ) from azure.core.pipeline.transport import HttpRequest -from azure.eventgrid import CloudEvent +from azure.core.messaging import CloudEvent from azure.eventgrid._policies import CloudEventDistributedTracingPolicy from _mocks import ( cloud_storage_dict diff --git a/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client.py b/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client.py index 5792a07dec03..637c847249fa 100644 --- a/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client.py +++ b/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client.py @@ -18,7 +18,10 @@ from azure_devtools.scenario_tests import ReplayableTest from azure.core.credentials import AzureKeyCredential, AzureSasCredential -from azure.eventgrid import EventGridPublisherClient, CloudEvent, EventGridEvent, generate_sas +from azure.core.messaging import CloudEvent +from azure.core.serialization import NULL +from azure.eventgrid import EventGridPublisherClient, EventGridEvent, generate_sas +from azure.eventgrid._helpers import _cloud_event_to_generated from eventgrid_preparer import ( CachedEventGridTopicPreparer @@ -132,32 +135,27 @@ def test_send_cloud_event_data_dict(self, resource_group, eventgrid_topic, event ) client.send(cloud_event) + @pytest.mark.skip("https://github.com/Azure/azure-sdk-for-python/issues/16993") @CachedResourceGroupPreparer(name_prefix='eventgridtest') @CachedEventGridTopicPreparer(name_prefix='cloudeventgridtest') - def test_send_cloud_event_data_base64_using_data(self, resource_group, eventgrid_topic, eventgrid_topic_primary_key, eventgrid_topic_endpoint): + def test_send_cloud_event_data_NULL(self, resource_group, eventgrid_topic, eventgrid_topic_primary_key, eventgrid_topic_endpoint): akc_credential = AzureKeyCredential(eventgrid_topic_primary_key) client = EventGridPublisherClient(eventgrid_topic_endpoint, akc_credential) cloud_event = CloudEvent( source = "http://samplesource.dev", - data = b'cloudevent', + data = NULL, type="Sample.Cloud.Event" ) - - def callback(request): - req = json.loads(request.http_request.body) - assert req[0].get("data_base64") is not None - assert req[0].get("data") is None - - client.send(cloud_event, raw_response_hook=callback) + client.send(cloud_event) @CachedResourceGroupPreparer(name_prefix='eventgridtest') @CachedEventGridTopicPreparer(name_prefix='cloudeventgridtest') - def test_send_cloud_event_bytes_using_data_base64(self, resource_group, eventgrid_topic, eventgrid_topic_primary_key, eventgrid_topic_endpoint): + def test_send_cloud_event_data_base64_using_data(self, resource_group, eventgrid_topic, eventgrid_topic_primary_key, eventgrid_topic_endpoint): akc_credential = AzureKeyCredential(eventgrid_topic_primary_key) client = EventGridPublisherClient(eventgrid_topic_endpoint, akc_credential) cloud_event = CloudEvent( source = "http://samplesource.dev", - data_base64 = b'cloudevent', + data = b'cloudevent', type="Sample.Cloud.Event" ) @@ -168,9 +166,8 @@ def callback(request): client.send(cloud_event, raw_response_hook=callback) - def test_send_cloud_event_fails_on_providing_data_and_b64(self): - with pytest.raises(ValueError, match="data and data_base64 cannot be provided at the same time*"): + with pytest.raises(ValueError, match="Unexpected keyword arguments data_base64.*"): cloud_event = CloudEvent( source = "http://samplesource.dev", data_base64 = b'cloudevent', @@ -241,7 +238,7 @@ def test_send_cloud_event_data_with_extensions(self, resource_group, eventgrid_t } ) client.send([cloud_event]) - internal = cloud_event._to_generated().serialize() + internal = _cloud_event_to_generated(cloud_event).serialize() assert 'reason_code' in internal assert 'extension' in internal assert internal['reason_code'] == 204 diff --git a/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client_async.py b/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client_async.py index 040866a36ac2..6c2c53db9531 100644 --- a/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client_async.py +++ b/sdk/eventgrid/azure-eventgrid/tests/test_eg_publisher_client_async.py @@ -17,7 +17,9 @@ from azure_devtools.scenario_tests import ReplayableTest from azure.core.credentials import AzureKeyCredential, AzureSasCredential -from azure.eventgrid import CloudEvent, EventGridEvent, generate_sas +from azure.core.messaging import CloudEvent +from azure.core.serialization import NULL +from azure.eventgrid import EventGridEvent, generate_sas from azure.eventgrid.aio import EventGridPublisherClient from eventgrid_preparer import ( @@ -174,15 +176,15 @@ async def test_send_cloud_event_data_with_extensions(self, resource_group, event data = "cloudevent", type="Sample.Cloud.Event", extensions={ - 'reason_code':204, + 'reasonCode':204, 'extension':'hello' } ) await client.send([cloud_event]) internal = cloud_event._to_generated().serialize() - assert 'reason_code' in internal + assert 'reasonCode' in internal assert 'extension' in internal - assert internal['reason_code'] == 204 + assert internal['reasonCode'] == 204 @CachedResourceGroupPreparer(name_prefix='eventgridtest') @@ -213,6 +215,20 @@ async def test_send_cloud_event_data_none(self, resource_group, eventgrid_topic, ) await client.send(cloud_event) + @pytest.mark.skip("https://github.com/Azure/azure-sdk-for-python/issues/16993") + @CachedResourceGroupPreparer(name_prefix='eventgridtest') + @CachedEventGridTopicPreparer(name_prefix='cloudeventgridtest') + @pytest.mark.asyncio + async def test_send_cloud_event_data_NULL(self, resource_group, eventgrid_topic, eventgrid_topic_primary_key, eventgrid_topic_endpoint): + akc_credential = AzureKeyCredential(eventgrid_topic_primary_key) + client = EventGridPublisherClient(eventgrid_topic_endpoint, akc_credential) + cloud_event = CloudEvent( + source = "http://samplesource.dev", + data = NULL, + type="Sample.Cloud.Event" + ) + await client.send(cloud_event) + @CachedResourceGroupPreparer(name_prefix='eventgridtest') @CachedEventGridTopicPreparer(name_prefix='eventgridtest') @pytest.mark.asyncio diff --git a/sdk/eventgrid/azure-eventgrid/tests/test_serialization.py b/sdk/eventgrid/azure-eventgrid/tests/test_serialization.py index 5bed9c1c9212..cb455fc2c0a8 100644 --- a/sdk/eventgrid/azure-eventgrid/tests/test_serialization.py +++ b/sdk/eventgrid/azure-eventgrid/tests/test_serialization.py @@ -14,9 +14,10 @@ from devtools_testutils import AzureMgmtTestCase from msrest.serialization import UTC -from azure.eventgrid import CloudEvent, EventGridEvent +from azure.core.messaging import CloudEvent from azure.eventgrid._generated import models as internal_models -from azure.eventgrid import SystemEventNames +from azure.eventgrid._helpers import _cloud_event_to_generated +from azure.eventgrid import SystemEventNames, EventGridEvent from _mocks import ( cloud_storage_dict, cloud_storage_string, @@ -39,16 +40,14 @@ def test_cloud_event_serialization_extension_bytes(self, **kwargs): source="http://samplesource.dev", data=data, type="Sample.Cloud.Event", - foo="bar", extensions={'e1':1, 'e2':2} ) cloud_event.subject = "subject" # to test explicit setting of prop encoded = base64.b64encode(data).decode('utf-8') - internal = cloud_event._to_generated() + internal = _cloud_event_to_generated(cloud_event) assert internal.additional_properties is not None - assert 'foo' not in internal.additional_properties assert 'e1' in internal.additional_properties json = internal.serialize() @@ -72,15 +71,13 @@ def test_cloud_event_serialization_extension_string(self, **kwargs): source="http://samplesource.dev", data=data, type="Sample.Cloud.Event", - foo="bar", extensions={'e1':1, 'e2':2} ) cloud_event.subject = "subject" # to test explicit setting of prop - internal = cloud_event._to_generated() + internal = _cloud_event_to_generated(cloud_event) assert internal.additional_properties is not None - assert 'foo' not in internal.additional_properties assert 'e1' in internal.additional_properties json = internal.serialize()