Skip to content

Commit 8fbbaf1

Browse files
authored
Merge branch 'dev' into hallvictoria/df-unit-testing
2 parents 1426076 + 7683200 commit 8fbbaf1

File tree

11 files changed

+495
-12
lines changed

11 files changed

+495
-12
lines changed

.github/workflows/label.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: "Pull Request Labeler"
22
on:
3-
pull_request:
3+
pull_request_target:
44
paths:
55
- '**/__init__.py'
66
jobs:

azure/functions/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ._queue import QueueMessage
2424
from ._servicebus import ServiceBusMessage
2525
from ._sql import SqlRow, SqlRowList
26+
from ._mysql import MySqlRow, MySqlRowList
2627

2728
# Import binding implementations to register them
2829
from . import blob # NoQA
@@ -37,6 +38,7 @@
3738
from . import durable_functions # NoQA
3839
from . import sql # NoQA
3940
from . import warmup # NoQA
41+
from . import mysql # NoQA
4042

4143

4244
__all__ = (
@@ -67,6 +69,8 @@
6769
'SqlRowList',
6870
'TimerRequest',
6971
'WarmUpContext',
72+
'MySqlRow',
73+
'MySqlRowList',
7074

7175
# Middlewares
7276
'WsgiMiddleware',
@@ -98,4 +102,4 @@
98102
'BlobSource'
99103
)
100104

101-
__version__ = '1.21.0b3'
105+
__version__ = '1.22.0b3'

azure/functions/_mysql.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import abc
4+
import collections
5+
import json
6+
7+
8+
class BaseMySqlRow(abc.ABC):
9+
10+
@classmethod
11+
@abc.abstractmethod
12+
def from_json(cls, json_data: str) -> 'BaseMySqlRow':
13+
raise NotImplementedError
14+
15+
@classmethod
16+
@abc.abstractmethod
17+
def from_dict(cls, dct: dict) -> 'BaseMySqlRow':
18+
raise NotImplementedError
19+
20+
@abc.abstractmethod
21+
def __getitem__(self, key):
22+
raise NotImplementedError
23+
24+
@abc.abstractmethod
25+
def __setitem__(self, key, value):
26+
raise NotImplementedError
27+
28+
@abc.abstractmethod
29+
def to_json(self) -> str:
30+
raise NotImplementedError
31+
32+
33+
class BaseMySqlRowList(abc.ABC):
34+
pass
35+
36+
37+
class MySqlRow(BaseMySqlRow, collections.UserDict):
38+
"""A MySql Row.
39+
40+
MySqlRow objects are ''UserDict'' subclasses and behave like dicts.
41+
"""
42+
43+
@classmethod
44+
def from_json(cls, json_data: str) -> 'BaseMySqlRow':
45+
"""Create a MySqlRow from a JSON string."""
46+
return cls.from_dict(json.loads(json_data))
47+
48+
@classmethod
49+
def from_dict(cls, dct: dict) -> 'BaseMySqlRow':
50+
"""Create a MySqlRow from a dict object"""
51+
return cls({k: v for k, v in dct.items()})
52+
53+
def to_json(self) -> str:
54+
"""Return the JSON representation of the MySqlRow"""
55+
return json.dumps(dict(self))
56+
57+
def __getitem__(self, key):
58+
return collections.UserDict.__getitem__(self, key)
59+
60+
def __setitem__(self, key, value):
61+
return collections.UserDict.__setitem__(self, key, value)
62+
63+
def __repr__(self) -> str:
64+
return (
65+
f'<MySqlRow at 0x{id(self):0x}>'
66+
)
67+
68+
69+
class MySqlRowList(BaseMySqlRowList, collections.UserList):
70+
"A ''UserList'' subclass containing a list of :class:'~MySqlRow' objects"
71+
pass

azure/functions/decorators/blob.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ def __init__(self,
1717
**kwargs):
1818
self.path = path
1919
self.connection = connection
20-
self.source = source
20+
if isinstance(source, BlobSource):
21+
self.source = source.value
22+
else:
23+
self.source = source # type: ignore
2124
super().__init__(name=name, data_type=data_type)
2225

2326
@staticmethod

azure/functions/decorators/function_app.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ def __str__(self):
8080
return self.get_function_json()
8181

8282
def __call__(self, *args, **kwargs):
83-
"""This would allow the Function object to be directly callable and runnable
84-
directly using the interpreter locally.
83+
"""This would allow the Function object to be directly callable
84+
and runnable directly using the interpreter locally.
8585
8686
Example:
8787
@app.route(route="http_trigger")
@@ -342,8 +342,8 @@ def decorator():
342342
return wrap
343343

344344
def _get_durable_blueprint(self):
345-
"""Attempt to import the Durable Functions SDK from which DF decorators are
346-
implemented.
345+
"""Attempt to import the Durable Functions SDK from which DF
346+
decorators are implemented.
347347
"""
348348

349349
try:
@@ -3276,6 +3276,8 @@ def assistant_query_input(self,
32763276
arg_name: str,
32773277
id: str,
32783278
timestamp_utc: str,
3279+
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
3280+
collection_name: Optional[str] = "ChatState", # noqa: E501
32793281
data_type: Optional[
32803282
Union[DataType, str]] = None,
32813283
**kwargs) \
@@ -3288,6 +3290,11 @@ def assistant_query_input(self,
32883290
:param timestamp_utc: the timestamp of the earliest message in the chat
32893291
history to fetch. The timestamp should be in ISO 8601 format - for
32903292
example, 2023-08-01T00:00:00Z.
3293+
:param chat_storage_connection_setting: The configuration section name
3294+
for the table settings for assistant chat storage. The default value is
3295+
"AzureWebJobsStorage".
3296+
:param collection_name: The table collection name for assistant chat
3297+
storage. The default value is "ChatState".
32913298
:param id: The ID of the Assistant to query.
32923299
:param data_type: Defines how Functions runtime should treat the
32933300
parameter value
@@ -3305,6 +3312,8 @@ def decorator():
33053312
name=arg_name,
33063313
id=id,
33073314
timestamp_utc=timestamp_utc,
3315+
chat_storage_connection_setting=chat_storage_connection_setting, # noqa: E501
3316+
collection_name=collection_name,
33083317
data_type=parse_singular_param_to_enum(data_type,
33093318
DataType),
33103319
**kwargs))
@@ -3318,6 +3327,8 @@ def assistant_post_input(self, arg_name: str,
33183327
id: str,
33193328
user_message: str,
33203329
model: Optional[str] = None,
3330+
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
3331+
collection_name: Optional[str] = "ChatState", # noqa: E501
33213332
data_type: Optional[
33223333
Union[DataType, str]] = None,
33233334
**kwargs) \
@@ -3331,6 +3342,11 @@ def assistant_post_input(self, arg_name: str,
33313342
:param user_message: The user message that user has entered for
33323343
assistant to respond to.
33333344
:param model: The OpenAI chat model to use.
3345+
:param chat_storage_connection_setting: The configuration section name
3346+
for the table settings for assistant chat storage. The default value is
3347+
"AzureWebJobsStorage".
3348+
:param collection_name: The table collection name for assistant chat
3349+
storage. The default value is "ChatState".
33343350
:param data_type: Defines how Functions runtime should treat the
33353351
parameter value
33363352
:param kwargs: Keyword arguments for specifying additional binding
@@ -3348,6 +3364,8 @@ def decorator():
33483364
id=id,
33493365
user_message=user_message,
33503366
model=model,
3367+
chat_storage_connection_setting=chat_storage_connection_setting, # noqa: E501
3368+
collection_name=collection_name,
33513369
data_type=parse_singular_param_to_enum(data_type,
33523370
DataType),
33533371
**kwargs))

azure/functions/decorators/openai.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ def __init__(self,
7777
name: str,
7878
id: str,
7979
timestamp_utc: str,
80+
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
81+
collection_name: Optional[str] = "ChatState",
8082
data_type: Optional[DataType] = None,
8183
**kwargs):
8284
self.id = id
8385
self.timestamp_utc = timestamp_utc
86+
self.chat_storage_connection_setting = chat_storage_connection_setting
87+
self.collection_name = collection_name
8488
super().__init__(name=name, data_type=data_type)
8589

8690

@@ -165,12 +169,16 @@ def __init__(self, name: str,
165169
id: str,
166170
user_message: str,
167171
model: Optional[str] = None,
172+
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
173+
collection_name: Optional[str] = "ChatState",
168174
data_type: Optional[DataType] = None,
169175
**kwargs):
170176
self.name = name
171177
self.id = id
172178
self.user_message = user_message
173179
self.model = model
180+
self.chat_storage_connection_setting = chat_storage_connection_setting
181+
self.collection_name = collection_name
174182
super().__init__(name=name, data_type=data_type)
175183

176184

azure/functions/mysql.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import collections.abc
5+
import json
6+
import typing
7+
8+
from azure.functions import _mysql as mysql
9+
10+
from . import meta
11+
12+
13+
class MySqlConverter(meta.InConverter, meta.OutConverter,
14+
binding='mysql'):
15+
16+
@classmethod
17+
def check_input_type_annotation(cls, pytype: type) -> bool:
18+
return issubclass(pytype, mysql.BaseMySqlRowList)
19+
20+
@classmethod
21+
def check_output_type_annotation(cls, pytype: type) -> bool:
22+
return issubclass(pytype, (mysql.BaseMySqlRowList, mysql.BaseMySqlRow))
23+
24+
@classmethod
25+
def decode(cls,
26+
data: meta.Datum,
27+
*,
28+
trigger_metadata) -> typing.Optional[mysql.MySqlRowList]:
29+
if data is None or data.type is None:
30+
return None
31+
32+
data_type = data.type
33+
34+
if data_type in ['string', 'json']:
35+
body = data.value
36+
37+
elif data_type == 'bytes':
38+
body = data.value.decode('utf-8')
39+
40+
else:
41+
raise NotImplementedError(
42+
f'Unsupported payload type: {data_type}')
43+
44+
rows = json.loads(body)
45+
if not isinstance(rows, list):
46+
rows = [rows]
47+
48+
return mysql.MySqlRowList(
49+
(None if row is None else mysql.MySqlRow.from_dict(row))
50+
for row in rows)
51+
52+
@classmethod
53+
def encode(cls, obj: typing.Any, *,
54+
expected_type: typing.Optional[type]) -> meta.Datum:
55+
if isinstance(obj, mysql.MySqlRow):
56+
data = mysql.MySqlRowList([obj])
57+
58+
elif isinstance(obj, mysql.MySqlRowList):
59+
data = obj
60+
61+
elif isinstance(obj, collections.abc.Iterable):
62+
data = mysql.MySqlRowList()
63+
64+
for row in obj:
65+
if not isinstance(row, mysql.MySqlRow):
66+
raise NotImplementedError(
67+
f'Unsupported list type: {type(obj)}, \
68+
lists must contain MySqlRow objects')
69+
else:
70+
data.append(row)
71+
72+
else:
73+
raise NotImplementedError(f'Unsupported type: {type(obj)}')
74+
75+
return meta.Datum(
76+
type='json',
77+
value=json.dumps([dict(d) for d in data])
78+
)

tests/decorators/test_blob.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ def test_blob_trigger_creation_with_default_specified_source(self):
4242
"name": "req",
4343
"dataType": DataType.UNDEFINED,
4444
"path": "dummy_path",
45-
'source': BlobSource.LOGS_AND_CONTAINER_SCAN,
45+
'source': 'LogsAndContainerScan',
4646
"connection": "dummy_connection"
4747
})
4848

4949
def test_blob_trigger_creation_with_source_as_string(self):
5050
trigger = BlobTrigger(name="req",
5151
path="dummy_path",
5252
connection="dummy_connection",
53-
source=BlobSource.EVENT_GRID,
53+
source="EventGrid",
5454
data_type=DataType.UNDEFINED,
5555
dummy_field="dummy")
5656

@@ -62,7 +62,7 @@ def test_blob_trigger_creation_with_source_as_string(self):
6262
"name": "req",
6363
"dataType": DataType.UNDEFINED,
6464
"path": "dummy_path",
65-
'source': BlobSource.EVENT_GRID,
65+
'source': 'EventGrid',
6666
"connection": "dummy_connection"
6767
})
6868

@@ -82,7 +82,7 @@ def test_blob_trigger_creation_with_source_as_enum(self):
8282
"name": "req",
8383
"dataType": DataType.UNDEFINED,
8484
"path": "dummy_path",
85-
'source': BlobSource.EVENT_GRID,
85+
'source': 'EventGrid',
8686
"connection": "dummy_connection"
8787
})
8888

tests/decorators/test_decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1628,7 +1628,7 @@ def test_blob_input_binding():
16281628
"type": BLOB_TRIGGER,
16291629
"name": "req",
16301630
"path": "dummy_path",
1631-
"source": BlobSource.EVENT_GRID,
1631+
"source": 'EventGrid',
16321632
"connection": "dummy_conn"
16331633
})
16341634

tests/decorators/test_openai.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ def test_text_completion_input_valid_creation(self):
5757
def test_assistant_query_input_valid_creation(self):
5858
input = AssistantQueryInput(name="test",
5959
timestamp_utc="timestamp_utc",
60+
chat_storage_connection_setting="AzureWebJobsStorage", # noqa: E501
61+
collection_name="ChatState",
6062
data_type=DataType.UNDEFINED,
6163
id="test_id",
6264
type="assistantQueryInput",
@@ -66,6 +68,8 @@ def test_assistant_query_input_valid_creation(self):
6668
self.assertEqual(input.get_dict_repr(),
6769
{"name": "test",
6870
"timestampUtc": "timestamp_utc",
71+
"chatStorageConnectionSetting": "AzureWebJobsStorage", # noqa: E501
72+
"collectionName": "ChatState",
6973
"dataType": DataType.UNDEFINED,
7074
"direction": BindingDirection.IN,
7175
"type": "assistantQuery",
@@ -111,6 +115,8 @@ def test_assistant_post_input_valid_creation(self):
111115
input = AssistantPostInput(name="test",
112116
id="test_id",
113117
model="test_model",
118+
chat_storage_connection_setting="AzureWebJobsStorage", # noqa: E501
119+
collection_name="ChatState",
114120
user_message="test_message",
115121
data_type=DataType.UNDEFINED,
116122
dummy_field="dummy")
@@ -120,6 +126,8 @@ def test_assistant_post_input_valid_creation(self):
120126
{"name": "test",
121127
"id": "test_id",
122128
"model": "test_model",
129+
"chatStorageConnectionSetting": "AzureWebJobsStorage", # noqa: E501
130+
"collectionName": "ChatState",
123131
"userMessage": "test_message",
124132
"dataType": DataType.UNDEFINED,
125133
"direction": BindingDirection.IN,

0 commit comments

Comments
 (0)