Skip to content

Commit 0800b4d

Browse files
committed
Add support for EventGrid bindings
1 parent c31b006 commit 0800b4d

File tree

16 files changed

+335
-5
lines changed

16 files changed

+335
-5
lines changed

azure/functions/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ._abc import HttpRequest, TimerRequest, InputStream, Context, Out # NoQA
22
from ._abc import EventHubEvent # NoQA
3+
from ._abc import EventGridEvent # NoQA
34
from ._cosmosdb import Document, DocumentList # NoQA
45
from ._http import HttpResponse # NoQA
56
from ._queue import QueueMessage # NoQA

azure/functions/_abc.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,42 @@ def pop_receipt(self) -> typing.Optional[str]:
199199
pass
200200

201201

202+
class EventGridEvent(abc.ABC):
203+
@property
204+
@abc.abstractmethod
205+
def id(self) -> str:
206+
pass
207+
208+
@abc.abstractmethod
209+
def get_json(self) -> typing.Any:
210+
pass
211+
212+
@property
213+
@abc.abstractmethod
214+
def topic(self) -> str:
215+
pass
216+
217+
@property
218+
@abc.abstractmethod
219+
def subject(self) -> str:
220+
pass
221+
222+
@property
223+
@abc.abstractmethod
224+
def event_type(self) -> str:
225+
pass
226+
227+
@property
228+
@abc.abstractmethod
229+
def event_time(self) -> datetime.datetime:
230+
pass
231+
232+
@property
233+
@abc.abstractmethod
234+
def data_version(self) -> str:
235+
pass
236+
237+
202238
class Document(abc.ABC):
203239

204240
@classmethod

azure/functions_worker/bindings/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# to get them registered and available:
1010
from . import blob # NoQA
1111
from . import cosmosdb # NoQA
12+
from . import eventgrid # NoQA
1213
from . import eventhub # NoQA
1314
from . import http # NoQA
1415
from . import queue # NoQA
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import datetime
2+
import json
3+
import typing
4+
5+
from azure.functions import _abc as azf_abc
6+
7+
from . import meta
8+
from .. import protos
9+
10+
11+
class EventGridEvent(azf_abc.EventGridEvent):
12+
"""An EventGrid event message."""
13+
14+
def __init__(self, *,
15+
id: str,
16+
data: typing.Dict[str, object],
17+
topic: str,
18+
subject: str,
19+
event_type: str,
20+
event_time: datetime.datetime,
21+
data_version: str) -> None:
22+
self.__id = id
23+
self.__data = data
24+
self.__subject = subject
25+
self.__topic = topic
26+
self.__event_type = event_type
27+
self.__event_time = event_time
28+
self.__data_version = data_version
29+
30+
@property
31+
def id(self) -> str:
32+
return self.__id
33+
34+
def get_json(self) -> typing.Any:
35+
return self.__data
36+
37+
@property
38+
def topic(self) -> str:
39+
return self.__topic
40+
41+
@property
42+
def subject(self) -> str:
43+
return self.__subject
44+
45+
@property
46+
def event_type(self) -> str:
47+
return self.__event_type
48+
49+
@property
50+
def event_time(self) -> datetime.datetime:
51+
return self.__event_time
52+
53+
@property
54+
def data_version(self) -> str:
55+
return self.__data_version
56+
57+
def __repr__(self) -> str:
58+
return (
59+
f'<azure.EventGridEvent id={self.id} '
60+
f'topic={self.topic} '
61+
f'subject={self.subject} '
62+
f'at 0x{id(self):0x}>'
63+
)
64+
65+
66+
class EventGridEventInConverter(meta.InConverter,
67+
binding='eventGridTrigger', trigger=True):
68+
69+
@classmethod
70+
def check_input_type_annotation(cls, pytype: type) -> bool:
71+
return issubclass(pytype, azf_abc.EventGridEvent)
72+
73+
@classmethod
74+
def from_proto(cls, data: protos.TypedData, *,
75+
pytype: typing.Optional[type],
76+
trigger_metadata) -> typing.Any:
77+
data_type = data.WhichOneof('data')
78+
79+
if data_type == 'json':
80+
body = json.loads(data.json)
81+
else:
82+
raise NotImplementedError(
83+
f'unsupported event grid payload type: {data_type}')
84+
85+
if trigger_metadata is None:
86+
raise NotImplementedError(
87+
f'missing trigger metadata for event grid input')
88+
89+
return EventGridEvent(
90+
id=body.get('id'),
91+
topic=body.get('topic'),
92+
subject=body.get('subject'),
93+
event_type=body.get('eventType'),
94+
event_time=cls._parse_datetime(body.get('eventTime')),
95+
data=body.get('data'),
96+
data_version=body.get('dataVersion'),
97+
)

azure/functions_worker/bindings/meta.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,29 @@ def _parse_datetime_metadata(
143143
if datetime_str is None:
144144
return None
145145
else:
146-
# UTC ISO 8601 assumed
147-
dt = datetime.datetime.strptime(
148-
datetime_str, '%Y-%m-%dT%H:%M:%S+00:00')
149-
return dt.replace(tzinfo=datetime.timezone.utc)
146+
return cls._parse_datetime(datetime_str)
147+
148+
@classmethod
149+
def _parse_datetime(
150+
cls, datetime_str: str) -> datetime.datetime:
151+
# UTC ISO 8601 assumed
152+
formats = [
153+
'%Y-%m-%dT%H:%M:%S+00:00',
154+
'%Y-%m-%dT%H:%M:%S.%f+00:00',
155+
'%Y-%m-%dT%H:%M:%SZ',
156+
'%Y-%m-%dT%H:%M:%S.%fZ',
157+
]
158+
dt = None
159+
for fmt in formats:
160+
try:
161+
dt = datetime.datetime.strptime(datetime_str, fmt)
162+
except ValueError as e:
163+
last_error = e
164+
165+
if dt is None:
166+
raise last_error
167+
168+
return dt.replace(tzinfo=datetime.timezone.utc)
150169

151170

152171
class InConverter(_BaseConverter, binding=None):

azure/functions_worker/testutils.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ def start_webhost(*, script_dir=None, stdout=None):
574574
params={'code': 'testFunctionKey'})
575575
if 200 <= r.status_code < 300:
576576
# Give the host a bit more time to settle
577-
time.sleep(1)
577+
time.sleep(2)
578578
break
579579
except requests.exceptions.ConnectionError:
580580
pass
@@ -594,13 +594,26 @@ def _main():
594594

595595
args = parser.parse_args()
596596

597+
extensions = pathlib.Path(args.scriptroot) / 'bin'
598+
599+
if not extensions.exists():
600+
if extensions.is_symlink():
601+
extensions.unlink()
602+
603+
extensions.symlink_to(EXTENSIONS_PATH, target_is_directory=True)
604+
linked_extensions = True
605+
else:
606+
linked_extensions = False
607+
597608
host = popen_webhost(
598609
stdout=sys.stdout, stderr=sys.stderr,
599610
script_root=os.path.abspath(args.scriptroot))
600611
try:
601612
host.wait()
602613
finally:
603614
host.terminate()
615+
if linked_extensions:
616+
extensions.unlink()
604617

605618

606619
if __name__ == '__main__':

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
"id": "Microsoft.Azure.WebJobs.Extensions.EventHubs",
2929
"version": "3.0.0-beta5"
3030
},
31+
{
32+
"id": "Microsoft.Azure.WebJobs.Extensions.EventGrid",
33+
"version": "2.0.0-beta1"
34+
},
3135
]
3236

3337

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import json
2+
3+
import azure.functions as func
4+
5+
6+
def main(event: func.EventGridEvent) -> str:
7+
result = json.dumps({
8+
'id': event.id,
9+
'data': event.get_json(),
10+
'topic': event.topic,
11+
'subject': event.subject,
12+
'event_type': event.event_type,
13+
})
14+
15+
return result
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"scriptFile": "__init__.py",
3+
"disabled": false,
4+
5+
"bindings": [
6+
{
7+
"type": "eventGridTrigger",
8+
"direction": "in",
9+
"name": "event",
10+
},
11+
{
12+
"type": "blob",
13+
"direction": "out",
14+
"name": "$return",
15+
"connection": "AzureWebJobsStorage",
16+
"path": "python-worker-tests/test-eventgrid-triggered.txt"
17+
}
18+
]
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"scriptFile": "main.py",
3+
"disabled": false,
4+
"bindings": [
5+
{
6+
"type": "httpTrigger",
7+
"direction": "in",
8+
"name": "req"
9+
},
10+
{
11+
"type": "blob",
12+
"direction": "in",
13+
"name": "file",
14+
"connection": "AzureWebJobsStorage",
15+
"path": "python-worker-tests/test-eventgrid-triggered.txt"
16+
},
17+
{
18+
"type": "http",
19+
"direction": "out",
20+
"name": "$return",
21+
}
22+
]
23+
}

0 commit comments

Comments
 (0)