Skip to content
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

fix: allow blob source to be string and BlobSource values #255

Closed
wants to merge 7 commits into from
Closed
6 changes: 5 additions & 1 deletion azure/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ._queue import QueueMessage
from ._servicebus import ServiceBusMessage
from ._sql import SqlRow, SqlRowList
from ._mysql import MySqlRow, MySqlRowList

# Import binding implementations to register them
from . import blob # NoQA
Expand All @@ -37,6 +38,7 @@
from . import durable_functions # NoQA
from . import sql # NoQA
from . import warmup # NoQA
from . import mysql # NoQA


__all__ = (
Expand Down Expand Up @@ -67,6 +69,8 @@
'SqlRowList',
'TimerRequest',
'WarmUpContext',
'MySqlRow',
'MySqlRowList',

# Middlewares
'WsgiMiddleware',
Expand Down Expand Up @@ -98,4 +102,4 @@
'BlobSource'
)

__version__ = '1.21.0'
__version__ = '1.22.0b2'
71 changes: 71 additions & 0 deletions azure/functions/_mysql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import abc
import collections
import json


class BaseMySqlRow(abc.ABC):

@classmethod
@abc.abstractmethod
def from_json(cls, json_data: str) -> 'BaseMySqlRow':
raise NotImplementedError

@classmethod
@abc.abstractmethod
def from_dict(cls, dct: dict) -> 'BaseMySqlRow':
raise NotImplementedError

@abc.abstractmethod
def __getitem__(self, key):
raise NotImplementedError

@abc.abstractmethod
def __setitem__(self, key, value):
raise NotImplementedError

@abc.abstractmethod
def to_json(self) -> str:
raise NotImplementedError


class BaseMySqlRowList(abc.ABC):
pass


class MySqlRow(BaseMySqlRow, collections.UserDict):
"""A MySql Row.

MySqlRow objects are ''UserDict'' subclasses and behave like dicts.
"""

@classmethod
def from_json(cls, json_data: str) -> 'BaseMySqlRow':
"""Create a MySqlRow from a JSON string."""
return cls.from_dict(json.loads(json_data))

@classmethod
def from_dict(cls, dct: dict) -> 'BaseMySqlRow':
"""Create a MySqlRow from a dict object"""
return cls({k: v for k, v in dct.items()})

def to_json(self) -> str:
"""Return the JSON representation of the MySqlRow"""
return json.dumps(dict(self))

def __getitem__(self, key):
return collections.UserDict.__getitem__(self, key)

def __setitem__(self, key, value):
return collections.UserDict.__setitem__(self, key, value)

def __repr__(self) -> str:
return (
f'<MySqlRow at 0x{id(self):0x}>'
)


class MySqlRowList(BaseMySqlRowList, collections.UserList):
"A ''UserList'' subclass containing a list of :class:'~MySqlRow' objects"
pass
5 changes: 4 additions & 1 deletion azure/functions/decorators/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ def __init__(self,
**kwargs):
self.path = path
self.connection = connection
self.source = source.value if source else None
if type(source) is BlobSource:
self.source = source.value if source else None
else:
self.source = source
super().__init__(name=name, data_type=data_type)

@staticmethod
Expand Down
26 changes: 22 additions & 4 deletions azure/functions/decorators/function_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def __str__(self):
return self.get_function_json()

def __call__(self, *args, **kwargs):
"""This would allow the Function object to be directly callable and runnable
directly using the interpreter locally.
"""This would allow the Function object to be directly callable
and runnable directly using the interpreter locally.

Example:
@app.route(route="http_trigger")
Expand Down Expand Up @@ -332,8 +332,8 @@ def decorator():
return wrap

def _get_durable_blueprint(self):
"""Attempt to import the Durable Functions SDK from which DF decorators are
implemented.
"""Attempt to import the Durable Functions SDK from which DF
decorators are implemented.
"""

try:
Expand Down Expand Up @@ -3266,6 +3266,8 @@ def assistant_query_input(self,
arg_name: str,
id: str,
timestamp_utc: str,
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
collection_name: Optional[str] = "ChatState", # noqa: E501
data_type: Optional[
Union[DataType, str]] = None,
**kwargs) \
Expand All @@ -3278,6 +3280,11 @@ def assistant_query_input(self,
:param timestamp_utc: the timestamp of the earliest message in the chat
history to fetch. The timestamp should be in ISO 8601 format - for
example, 2023-08-01T00:00:00Z.
:param chat_storage_connection_setting: The configuration section name
for the table settings for assistant chat storage. The default value is
"AzureWebJobsStorage".
:param collection_name: The table collection name for assistant chat
storage. The default value is "ChatState".
:param id: The ID of the Assistant to query.
:param data_type: Defines how Functions runtime should treat the
parameter value
Expand All @@ -3295,6 +3302,8 @@ def decorator():
name=arg_name,
id=id,
timestamp_utc=timestamp_utc,
chat_storage_connection_setting=chat_storage_connection_setting, # noqa: E501
collection_name=collection_name,
data_type=parse_singular_param_to_enum(data_type,
DataType),
**kwargs))
Expand All @@ -3308,6 +3317,8 @@ def assistant_post_input(self, arg_name: str,
id: str,
user_message: str,
model: Optional[str] = None,
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
collection_name: Optional[str] = "ChatState", # noqa: E501
data_type: Optional[
Union[DataType, str]] = None,
**kwargs) \
Expand All @@ -3321,6 +3332,11 @@ def assistant_post_input(self, arg_name: str,
:param user_message: The user message that user has entered for
assistant to respond to.
:param model: The OpenAI chat model to use.
:param chat_storage_connection_setting: The configuration section name
for the table settings for assistant chat storage. The default value is
"AzureWebJobsStorage".
:param collection_name: The table collection name for assistant chat
storage. The default value is "ChatState".
:param data_type: Defines how Functions runtime should treat the
parameter value
:param kwargs: Keyword arguments for specifying additional binding
Expand All @@ -3338,6 +3354,8 @@ def decorator():
id=id,
user_message=user_message,
model=model,
chat_storage_connection_setting=chat_storage_connection_setting, # noqa: E501
collection_name=collection_name,
data_type=parse_singular_param_to_enum(data_type,
DataType),
**kwargs))
Expand Down
8 changes: 8 additions & 0 deletions azure/functions/decorators/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,14 @@ def __init__(self,
name: str,
id: str,
timestamp_utc: str,
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
collection_name: Optional[str] = "ChatState",
data_type: Optional[DataType] = None,
**kwargs):
self.id = id
self.timestamp_utc = timestamp_utc
self.chat_storage_connection_setting = chat_storage_connection_setting
self.collection_name = collection_name
super().__init__(name=name, data_type=data_type)


Expand Down Expand Up @@ -165,12 +169,16 @@ def __init__(self, name: str,
id: str,
user_message: str,
model: Optional[str] = None,
chat_storage_connection_setting: Optional[str] = "AzureWebJobsStorage", # noqa: E501
collection_name: Optional[str] = "ChatState",
data_type: Optional[DataType] = None,
**kwargs):
self.name = name
self.id = id
self.user_message = user_message
self.model = model
self.chat_storage_connection_setting = chat_storage_connection_setting
self.collection_name = collection_name
super().__init__(name=name, data_type=data_type)


Expand Down
78 changes: 78 additions & 0 deletions azure/functions/mysql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import collections.abc
import json
import typing

from azure.functions import _mysql as mysql

from . import meta


class MySqlConverter(meta.InConverter, meta.OutConverter,
binding='mysql'):

@classmethod
def check_input_type_annotation(cls, pytype: type) -> bool:
return issubclass(pytype, mysql.BaseMySqlRowList)

@classmethod
def check_output_type_annotation(cls, pytype: type) -> bool:
return issubclass(pytype, (mysql.BaseMySqlRowList, mysql.BaseMySqlRow))

@classmethod
def decode(cls,
data: meta.Datum,
*,
trigger_metadata) -> typing.Optional[mysql.MySqlRowList]:
if data is None or data.type is None:
return None

data_type = data.type

if data_type in ['string', 'json']:
body = data.value

elif data_type == 'bytes':
body = data.value.decode('utf-8')

else:
raise NotImplementedError(
f'Unsupported payload type: {data_type}')

rows = json.loads(body)
if not isinstance(rows, list):
rows = [rows]

return mysql.MySqlRowList(
(None if row is None else mysql.MySqlRow.from_dict(row))
for row in rows)

@classmethod
def encode(cls, obj: typing.Any, *,
expected_type: typing.Optional[type]) -> meta.Datum:
if isinstance(obj, mysql.MySqlRow):
data = mysql.MySqlRowList([obj])

elif isinstance(obj, mysql.MySqlRowList):
data = obj

elif isinstance(obj, collections.abc.Iterable):
data = mysql.MySqlRowList()

for row in obj:
if not isinstance(row, mysql.MySqlRow):
raise NotImplementedError(
f'Unsupported list type: {type(obj)}, \
lists must contain MySqlRow objects')
else:
data.append(row)

else:
raise NotImplementedError(f'Unsupported type: {type(obj)}')

return meta.Datum(
type='json',
value=json.dumps([dict(d) for d in data])
)
8 changes: 8 additions & 0 deletions tests/decorators/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def test_text_completion_input_valid_creation(self):
def test_assistant_query_input_valid_creation(self):
input = AssistantQueryInput(name="test",
timestamp_utc="timestamp_utc",
chat_storage_connection_setting="AzureWebJobsStorage", # noqa: E501
collection_name="ChatState",
data_type=DataType.UNDEFINED,
id="test_id",
type="assistantQueryInput",
Expand All @@ -66,6 +68,8 @@ def test_assistant_query_input_valid_creation(self):
self.assertEqual(input.get_dict_repr(),
{"name": "test",
"timestampUtc": "timestamp_utc",
"chatStorageConnectionSetting": "AzureWebJobsStorage", # noqa: E501
"collectionName": "ChatState",
"dataType": DataType.UNDEFINED,
"direction": BindingDirection.IN,
"type": "assistantQuery",
Expand Down Expand Up @@ -111,6 +115,8 @@ def test_assistant_post_input_valid_creation(self):
input = AssistantPostInput(name="test",
id="test_id",
model="test_model",
chat_storage_connection_setting="AzureWebJobsStorage", # noqa: E501
collection_name="ChatState",
user_message="test_message",
data_type=DataType.UNDEFINED,
dummy_field="dummy")
Expand All @@ -120,6 +126,8 @@ def test_assistant_post_input_valid_creation(self):
{"name": "test",
"id": "test_id",
"model": "test_model",
"chatStorageConnectionSetting": "AzureWebJobsStorage", # noqa: E501
"collectionName": "ChatState",
"userMessage": "test_message",
"dataType": DataType.UNDEFINED,
"direction": BindingDirection.IN,
Expand Down
Loading
Loading