Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from .store_item import StoreItem
from .storage import Storage, AsyncStorageBase
from .memory_storage import MemoryStorage
from .transcript_info import TranscriptInfo
from .transcript_logger import (

from .transcript import (
TranscriptInfo,
TranscriptLogger,
ConsoleTranscriptLogger,
TranscriptLoggerMiddleware,
TranscriptStore,
FileTranscriptLogger,
FileTranscriptStore,
PagedResult,
)
from .transcript_store import TranscriptStore
from .transcript_file_store import FileTranscriptStore

__all__ = [
"StoreItem",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import MutableMapping, Any

JSON = MutableMapping[str, Any]
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from collections.abc import Callable, Awaitable
from typing import TypeVar
from collections.abc import Callable, Awaitable

error_filter = TypeVar("error_filter", bound=Callable[[Exception], bool])
ErrorFilter = Callable[[Exception], bool]

T = TypeVar("T")

async def ignore_error(promise: Awaitable, ignore_error_filter: error_filter):

async def ignore_error(
promise: Awaitable[T], ignore_error_filter: ErrorFilter
) -> T | None:
"""
Ignores errors based on the provided filter function.

promise: the awaitable to execute
ignore_error_filter: a function that takes an Exception and returns True if the error should be
ignored, False otherwise.

Returns the result of the promise if successful, or None if the error is ignored.
Raises the error if it is not ignored.
:param promise: An awaitable that may raise an exception.
:param ignore_error_filter: A function that takes an Exception and returns True if the error should be ignored.
:return: The result of the promise if successful, or None if the error is ignored.
:raises Exception: Re-raises the exception if it is not ignored.
"""
try:
return await promise
Expand All @@ -26,16 +28,17 @@ async def ignore_error(promise: Awaitable, ignore_error_filter: error_filter):
raise err


def is_status_code_error(*ignored_codes: list[int]) -> error_filter:
def is_status_code_error(*ignored_codes: int) -> ErrorFilter:
"""
Creates an error filter function that ignores errors with specific status codes.

ignored_codes: a list of status codes to ignore
Returns a function that takes an Exception and returns True if the error's status code is in ignored_codes.
:param ignored_codes: A list of status codes to ignore.
:return: A function that takes an Exception and returns True if the error's status code is in the ignored list.
"""
Comment thread
rodrigobr-msft marked this conversation as resolved.

def func(err: Exception) -> bool:
if hasattr(err, "status_code") and err.status_code in ignored_codes:
status_code = getattr(err, "status_code", None)
if status_code is not None and status_code in ignored_codes:
return True
return False

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,47 @@


class MemoryStorage(Storage):
"""In-memory storage implementation for testing and development purposes."""

def __init__(self, state: dict[str, JSON] | None = None):
"""Initializes the MemoryStorage with an optional initial state.

:param state: An optional dictionary representing the initial state of the storage.
:raises ValueError: If state is not a dictionary or None.
"""
self._memory: dict[str, JSON] = state or {}
self._lock = Lock()

async def read(
self, keys: list[str], *, target_cls: StoreItemT = None, **kwargs
self, keys: list[str], *, target_cls: type[StoreItemT], **kwargs
) -> dict[str, StoreItemT]:
"""Reads items from the in-memory storage.

:param keys: A list of keys to read from the storage.
:param target_cls: The class type of the items to be read. Must be a subclass of StoreItem.
:return: A dictionary mapping keys to their corresponding StoreItem instances.
:raises ValueError: If keys are empty.
"""

if not keys:
raise ValueError("Storage.read(): Keys are required when reading.")
if not target_cls:
raise ValueError("Storage.read(): target_cls cannot be None.")

Comment thread
rodrigobr-msft marked this conversation as resolved.
result: dict[str, StoreItem] = {}
result: dict[str, StoreItemT] = {}
async with self._lock:
for key in keys:
if key == "":
raise ValueError("MemoryStorage.read(): key cannot be empty")
if key in self._memory:
if not target_cls:
result[key] = self._memory[key]
else:
try:
result[key] = target_cls.from_json_to_store_item(
self._memory[key]
)
except AttributeError as error:
raise TypeError(
f"MemoryStorage.read(): could not deserialize in-memory item into {target_cls} class. Error: {error}"
)
result[key] = target_cls.from_json_to_store_item(self._memory[key])

return result

async def write(self, changes: dict[str, StoreItem]):
"""Writes items to the in-memory storage.

:param changes: A dictionary mapping keys to StoreItem instances to be written to the storage.
:raises ValueError: If changes is None or any key is empty.
"""
if not changes:
raise ValueError("MemoryStorage.write(): changes cannot be None")

Expand All @@ -55,6 +63,12 @@ async def write(self, changes: dict[str, StoreItem]):
self._memory[key] = changes[key].store_item_to_json()

async def delete(self, keys: list[str]):
"""Deletes items from the in-memory storage.

:param keys: A list of keys to delete from the storage.
:raises ValueError: If keys is empty or any key is empty.
"""

if not keys:
raise ValueError("Storage.delete(): Keys are required when deleting.")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Protocol, TypeVar, Type
from abc import abstractmethod
from typing import TypeVar
from abc import ABC, abstractmethod
from asyncio import gather

from .store_item import StoreItem
Expand All @@ -11,26 +11,30 @@
StoreItemT = TypeVar("StoreItemT", bound=StoreItem)


class Storage(Protocol):
class Storage(ABC):
"""Abstract base class for storage implementations."""

@abstractmethod
async def read(
self, keys: list[str], *, target_cls: Type[StoreItemT] | None = None, **kwargs
self, keys: list[str], *, target_cls: type[StoreItemT], **kwargs
) -> dict[str, StoreItemT]:
"""Reads multiple items from storage.

keys: A list of keys to read.
target_cls: The class to deserialize the stored JSON into.
Returns a dictionary of key to StoreItem.

missing keys are omitted from the result.
:param keys: A list of keys to read.
:param target_cls: The class of the StoreItem to deserialize the data into.
:return: A dictionary of key to StoreItem.
"""
pass

async def write(self, changes: dict[str, StoreItemT]) -> None:
@abstractmethod
async def write(self, changes: dict[str, StoreItem]) -> None:
"""Writes multiple items to storage.

changes: A dictionary of key to StoreItem to write."""
:param changes: A dictionary of key to StoreItem to write.
"""
pass

@abstractmethod
async def delete(self, keys: list[str]) -> None:
"""Deletes multiple items from storage.

Expand All @@ -53,36 +57,53 @@ async def initialize(self) -> None:

@abstractmethod
async def _read_item(
self, key: str, *, target_cls: Type[StoreItemT] | None = None, **kwargs
self, key: str, *, target_cls: type[StoreItemT], **kwargs
) -> tuple[str | None, StoreItemT | None]:
"""Reads a single item from storage by key.

Returns a tuple of (key, StoreItem) if found, or (None, None) if not found.
:param key: The key to read.
:param target_cls: The class of the StoreItem to deserialize the data into.
:return: A tuple of key and StoreItem. If the item does not exist, returns (None, None).
"""
pass

async def read(
self, keys: list[str], *, target_cls: Type[StoreItemT] | None = None, **kwargs
self, keys: list[str], *, target_cls: type[StoreItemT], **kwargs
) -> dict[str, StoreItemT]:
"""
Reads multiple items from storage.

:param keys: A list of keys to read.
:param target_cls: The class of the StoreItem to deserialize the data into.
:return: A dictionary of key to StoreItem.
:raises ValueError: If keys is empty.
"""
if not keys:
raise ValueError("Storage.read(): Keys are required when reading.")
if not target_cls:
raise ValueError("Storage.read(): target_cls cannot be None.")

Comment thread
rodrigobr-msft marked this conversation as resolved.
with spans.StorageRead(len(keys)):
await self.initialize()

items: list[tuple[str | None, StoreItemT | None]] = await gather(
*[self._read_item(key, target_cls=target_cls, **kwargs) for key in keys]
)
return {key: value for key, value in items if key is not None}
return {
key: value
for key, value in items
if key is not None and value is not None
}

@abstractmethod
async def _write_item(self, key: str, value: StoreItemT) -> None:
async def _write_item(self, key: str, value: StoreItem) -> None:
"""Writes a single item to storage by key."""
pass

async def write(self, changes: dict[str, StoreItemT]) -> None:
async def write(self, changes: dict[str, StoreItem]) -> None:
"""Writes multiple items to storage.

:param changes: A dictionary of key to StoreItem to write.
:raises ValueError: If changes is empty.
"""
if not changes:
raise ValueError("Storage.write(): Changes are required when writing.")

Expand All @@ -95,10 +116,18 @@ async def write(self, changes: dict[str, StoreItemT]) -> None:

@abstractmethod
async def _delete_item(self, key: str) -> None:
"""Deletes a single item from storage by key."""
"""Deletes a single item from storage by key.

:param key: The key to delete.
"""
pass

async def delete(self, keys: list[str]) -> None:
"""Deletes multiple items from storage.

:param keys: A list of keys to delete.
:raises ValueError: If keys is empty.
"""
if not keys:
raise ValueError("Storage.delete(): Keys are required when deleting.")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from abc import ABC
from __future__ import annotations

from abc import ABC, abstractmethod

from ._type_aliases import JSON


class StoreItem(ABC):
"""Abstract base class for items stored in the storage system."""

@abstractmethod
def store_item_to_json(self) -> JSON:
"""Serializes the StoreItem to a JSON-compatible dictionary.

:return: A JSON-compatible dictionary representation of the StoreItem.
"""
pass

@staticmethod
def from_json_to_store_item(json_data: JSON) -> "StoreItem":
@abstractmethod
def from_json_to_store_item(json_data: JSON) -> StoreItem:
"""Deserializes a JSON-compatible dictionary to a StoreItem.

:param json_data: A JSON-compatible dictionary representation of the StoreItem.
:return: A StoreItem instance.
"""
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from .transcript_info import TranscriptInfo
from .transcript_logger import (
TranscriptLogger,
ConsoleTranscriptLogger,
TranscriptLoggerMiddleware,
FileTranscriptLogger,
PagedResult,
)
from .transcript_store import TranscriptStore
from .transcript_file_store import FileTranscriptStore

__all__ = [
"TranscriptInfo",
"TranscriptLogger",
"ConsoleTranscriptLogger",
"TranscriptLoggerMiddleware",
"TranscriptStore",
"FileTranscriptLogger",
"FileTranscriptStore",
"PagedResult",
]
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Awaitable, Callable, Optional
from dataclasses import dataclass

from microsoft_agents.activity import Activity, ChannelAccount
from microsoft_agents.activity import Activity, ChannelAccount, ResourceResponse
from microsoft_agents.activity.activity import ConversationReference
from microsoft_agents.activity.activity_types import ActivityTypes
from microsoft_agents.activity.conversation_reference import ActivityEventNames
Expand Down Expand Up @@ -139,8 +139,8 @@ async def on_turn(
async def send_activities_handler(
ctx: TurnContext,
activities: list[Activity],
next_send: Callable[[], Awaitable[None]],
):
next_send: Callable[[], Awaitable[list[ResourceResponse]]],
) -> list[ResourceResponse]:
# Run full pipeline
responses = await next_send()
for index, activity in enumerate(activities):
Expand Down
Loading
Loading