microsoft-agents-storage-blob 0.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of microsoft-agents-storage-blob might be problematic. Click here for more details.
- microsoft_agents_storage_blob-0.0.0/PKG-INFO +14 -0
- microsoft_agents_storage_blob-0.0.0/microsoft/agents/storage/blob/__init__.py +4 -0
- microsoft_agents_storage_blob-0.0.0/microsoft/agents/storage/blob/blob_storage.py +100 -0
- microsoft_agents_storage_blob-0.0.0/microsoft/agents/storage/blob/blob_storage_config.py +28 -0
- microsoft_agents_storage_blob-0.0.0/microsoft_agents_storage_blob.egg-info/PKG-INFO +14 -0
- microsoft_agents_storage_blob-0.0.0/microsoft_agents_storage_blob.egg-info/SOURCES.txt +11 -0
- microsoft_agents_storage_blob-0.0.0/microsoft_agents_storage_blob.egg-info/dependency_links.txt +1 -0
- microsoft_agents_storage_blob-0.0.0/microsoft_agents_storage_blob.egg-info/requires.txt +3 -0
- microsoft_agents_storage_blob-0.0.0/microsoft_agents_storage_blob.egg-info/top_level.txt +1 -0
- microsoft_agents_storage_blob-0.0.0/pyproject.toml +18 -0
- microsoft_agents_storage_blob-0.0.0/setup.cfg +4 -0
- microsoft_agents_storage_blob-0.0.0/setup.py +13 -0
- microsoft_agents_storage_blob-0.0.0/tests/test_blob_storage.py +196 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microsoft-agents-storage-blob
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A blob storage library for Microsoft Agents
|
|
5
|
+
Author: Microsoft Corporation
|
|
6
|
+
Project-URL: Homepage, https://github.com/microsoft/Agents
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: microsoft-agents-hosting-core==0.0.0
|
|
12
|
+
Requires-Dist: azure-core
|
|
13
|
+
Requires-Dist: azure-storage-blob
|
|
14
|
+
Dynamic: requires-dist
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import TypeVar, Union
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
from azure.storage.blob.aio import (
|
|
6
|
+
ContainerClient,
|
|
7
|
+
BlobServiceClient,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from microsoft.agents.hosting.core.storage import StoreItem
|
|
11
|
+
from microsoft.agents.hosting.core.storage.storage import AsyncStorageBase
|
|
12
|
+
from microsoft.agents.hosting.core.storage._type_aliases import JSON
|
|
13
|
+
from microsoft.agents.hosting.core.storage.error_handling import (
|
|
14
|
+
ignore_error,
|
|
15
|
+
is_status_code_error,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .blob_storage_config import BlobStorageConfig
|
|
19
|
+
|
|
20
|
+
StoreItemT = TypeVar("StoreItemT", bound=StoreItem)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BlobStorage(AsyncStorageBase):
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: BlobStorageConfig):
|
|
26
|
+
|
|
27
|
+
if not config.container_name:
|
|
28
|
+
raise ValueError("BlobStorage: Container name is required.")
|
|
29
|
+
|
|
30
|
+
self.config = config
|
|
31
|
+
|
|
32
|
+
blob_service_client: BlobServiceClient = self._create_client()
|
|
33
|
+
self._container_client: ContainerClient = (
|
|
34
|
+
blob_service_client.get_container_client(config.container_name)
|
|
35
|
+
)
|
|
36
|
+
self._initialized: bool = False
|
|
37
|
+
|
|
38
|
+
def _create_client(self) -> BlobServiceClient:
|
|
39
|
+
if self.config.url: # connect with URL and credentials
|
|
40
|
+
if not self.config.credential:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"BlobStorage: Credential is required when using a custom service URL."
|
|
43
|
+
)
|
|
44
|
+
return BlobServiceClient(
|
|
45
|
+
account_url=self.config.url, credential=self.config.credential
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
else: # connect with connection string
|
|
49
|
+
return BlobServiceClient.from_connection_string(
|
|
50
|
+
self.config.connection_string
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def initialize(self) -> None:
|
|
54
|
+
"""Initializes the storage container"""
|
|
55
|
+
if not self._initialized:
|
|
56
|
+
# This should only happen once - assuming this is a singleton.
|
|
57
|
+
await ignore_error(
|
|
58
|
+
self._container_client.create_container(), is_status_code_error(409)
|
|
59
|
+
)
|
|
60
|
+
self._initialized = True
|
|
61
|
+
|
|
62
|
+
async def _read_item(
|
|
63
|
+
self, key: str, *, target_cls: StoreItemT = None, **kwargs
|
|
64
|
+
) -> tuple[Union[str, None], Union[StoreItemT, None]]:
|
|
65
|
+
item = await ignore_error(
|
|
66
|
+
self._container_client.download_blob(blob=key),
|
|
67
|
+
is_status_code_error(404),
|
|
68
|
+
)
|
|
69
|
+
if not item:
|
|
70
|
+
return None, None
|
|
71
|
+
|
|
72
|
+
item_rep: str = await item.readall()
|
|
73
|
+
item_JSON: JSON = json.loads(item_rep)
|
|
74
|
+
try:
|
|
75
|
+
return key, target_cls.from_json_to_store_item(item_JSON)
|
|
76
|
+
except AttributeError as error:
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"BlobStorage.read_item(): could not deserialize blob item into {target_cls} class. Error: {error}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def _write_item(self, key: str, item: StoreItem) -> None:
|
|
82
|
+
item_JSON: JSON = item.store_item_to_json()
|
|
83
|
+
if item_JSON is None:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"BlobStorage.write(): StoreItem serialization cannot return None"
|
|
86
|
+
)
|
|
87
|
+
item_rep_bytes = json.dumps(item_JSON).encode("utf-8")
|
|
88
|
+
|
|
89
|
+
# getting the length is important for performance with large blobs
|
|
90
|
+
await self._container_client.upload_blob(
|
|
91
|
+
name=key,
|
|
92
|
+
data=BytesIO(item_rep_bytes),
|
|
93
|
+
overwrite=True,
|
|
94
|
+
length=len(item_rep_bytes),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def _delete_item(self, key: str) -> None:
|
|
98
|
+
await ignore_error(
|
|
99
|
+
self._container_client.delete_blob(blob=key), is_status_code_error(404)
|
|
100
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from azure.core.credentials import TokenCredential
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BlobStorageConfig:
|
|
7
|
+
"""Configuration settings for BlobStorage."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
container_name: str,
|
|
12
|
+
connection_string: str = "",
|
|
13
|
+
url: str = "",
|
|
14
|
+
credential: Union[TokenCredential, None] = None,
|
|
15
|
+
):
|
|
16
|
+
"""Configuration settings for BlobStorage.
|
|
17
|
+
|
|
18
|
+
container_name: The name of the blob container.
|
|
19
|
+
connection_string: The connection string to the storage account.
|
|
20
|
+
url: The URL of the blob service. If provided, credential must also be provided.
|
|
21
|
+
credential: The TokenCredential to use for authentication when using a custom URL.
|
|
22
|
+
|
|
23
|
+
credential-based authentication is prioritized over connection string authentication.
|
|
24
|
+
"""
|
|
25
|
+
self.container_name: str = container_name
|
|
26
|
+
self.connection_string: str = connection_string
|
|
27
|
+
self.url: str = url
|
|
28
|
+
self.credential: Union[TokenCredential, None] = credential
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microsoft-agents-storage-blob
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A blob storage library for Microsoft Agents
|
|
5
|
+
Author: Microsoft Corporation
|
|
6
|
+
Project-URL: Homepage, https://github.com/microsoft/Agents
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: microsoft-agents-hosting-core==0.0.0
|
|
12
|
+
Requires-Dist: azure-core
|
|
13
|
+
Requires-Dist: azure-storage-blob
|
|
14
|
+
Dynamic: requires-dist
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
setup.py
|
|
3
|
+
microsoft/agents/storage/blob/__init__.py
|
|
4
|
+
microsoft/agents/storage/blob/blob_storage.py
|
|
5
|
+
microsoft/agents/storage/blob/blob_storage_config.py
|
|
6
|
+
microsoft_agents_storage_blob.egg-info/PKG-INFO
|
|
7
|
+
microsoft_agents_storage_blob.egg-info/SOURCES.txt
|
|
8
|
+
microsoft_agents_storage_blob.egg-info/dependency_links.txt
|
|
9
|
+
microsoft_agents_storage_blob.egg-info/requires.txt
|
|
10
|
+
microsoft_agents_storage_blob.egg-info/top_level.txt
|
|
11
|
+
tests/test_blob_storage.py
|
microsoft_agents_storage_blob-0.0.0/microsoft_agents_storage_blob.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
microsoft
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "microsoft-agents-storage-blob"
|
|
7
|
+
dynamic = ["version", "dependencies"]
|
|
8
|
+
description = "A blob storage library for Microsoft Agents"
|
|
9
|
+
authors = [{name = "Microsoft Corporation"}]
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
"Homepage" = "https://github.com/microsoft/Agents"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from os import environ
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
package_version = environ.get("PackageVersion", "0.0.0")
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
version=package_version,
|
|
8
|
+
install_requires=[
|
|
9
|
+
f"microsoft-agents-hosting-core=={package_version}",
|
|
10
|
+
"azure-core",
|
|
11
|
+
"azure-storage-blob",
|
|
12
|
+
],
|
|
13
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import gc
|
|
3
|
+
import os
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import pytest_asyncio
|
|
8
|
+
|
|
9
|
+
from microsoft.agents.storage.blob import BlobStorage, BlobStorageConfig
|
|
10
|
+
from azure.storage.blob.aio import BlobServiceClient
|
|
11
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
12
|
+
|
|
13
|
+
from microsoft.agents.hosting.core.storage.storage_test_utils import (
|
|
14
|
+
CRUDStorageTests,
|
|
15
|
+
StorageBaseline,
|
|
16
|
+
MockStoreItem,
|
|
17
|
+
MockStoreItemB,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
EMULATOR_RUNNING = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def blob_storage_instance(existing=False):
|
|
24
|
+
|
|
25
|
+
# Default Azure Storage Emulator connection string
|
|
26
|
+
connection_string = (
|
|
27
|
+
"AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq"
|
|
28
|
+
+ "2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint="
|
|
29
|
+
+ "http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;"
|
|
30
|
+
+ "TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
|
|
34
|
+
|
|
35
|
+
container_name = "asdkunittest"
|
|
36
|
+
|
|
37
|
+
if not existing:
|
|
38
|
+
|
|
39
|
+
# reset state of test container
|
|
40
|
+
try:
|
|
41
|
+
container_client = blob_service_client.get_container_client(container_name)
|
|
42
|
+
await container_client.delete_container()
|
|
43
|
+
except ResourceNotFoundError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
container_client = await blob_service_client.create_container(container_name)
|
|
47
|
+
else:
|
|
48
|
+
container_client = blob_service_client.get_container_client(container_name)
|
|
49
|
+
|
|
50
|
+
blob_storage_config = BlobStorageConfig(
|
|
51
|
+
container_name=container_name,
|
|
52
|
+
connection_string=connection_string,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
storage = BlobStorage(blob_storage_config)
|
|
56
|
+
return storage, container_client
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest_asyncio.fixture
|
|
60
|
+
async def blob_storage():
|
|
61
|
+
|
|
62
|
+
# setup
|
|
63
|
+
storage, container_client = await blob_storage_instance()
|
|
64
|
+
|
|
65
|
+
yield storage
|
|
66
|
+
|
|
67
|
+
# teardown
|
|
68
|
+
await container_client.delete_container()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
|
|
72
|
+
class TestBlobStorage(CRUDStorageTests):
|
|
73
|
+
|
|
74
|
+
async def storage(self, initial_data=None, existing=False):
|
|
75
|
+
if not initial_data:
|
|
76
|
+
initial_data = {}
|
|
77
|
+
storage, container_client = await blob_storage_instance(existing=existing)
|
|
78
|
+
|
|
79
|
+
for key, value in initial_data.items():
|
|
80
|
+
value_rep = json.dumps(value.store_item_to_json())
|
|
81
|
+
await container_client.upload_blob(name=key, data=value_rep, overwrite=True)
|
|
82
|
+
|
|
83
|
+
return storage
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_initialize(self, blob_storage):
|
|
87
|
+
await blob_storage.initialize()
|
|
88
|
+
await blob_storage.initialize()
|
|
89
|
+
await blob_storage.write(
|
|
90
|
+
{"key": MockStoreItem({"id": "item", "value": "data"})}
|
|
91
|
+
)
|
|
92
|
+
await blob_storage.initialize()
|
|
93
|
+
assert (await blob_storage.read(["key"], target_cls=MockStoreItem)) == {
|
|
94
|
+
"key": MockStoreItem({"id": "item", "value": "data"})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_external_change_is_visible(self):
|
|
99
|
+
blob_storage, container_client = await blob_storage_instance()
|
|
100
|
+
assert (await blob_storage.read(["key"], target_cls=MockStoreItem)) == {}
|
|
101
|
+
assert (await blob_storage.read(["key2"], target_cls=MockStoreItem)) == {}
|
|
102
|
+
await container_client.upload_blob(
|
|
103
|
+
name="key", data=json.dumps({"id": "item", "value": "data"}), overwrite=True
|
|
104
|
+
)
|
|
105
|
+
await container_client.upload_blob(
|
|
106
|
+
name="key2",
|
|
107
|
+
data=json.dumps({"id": "another_item", "value": "new_val"}),
|
|
108
|
+
overwrite=True,
|
|
109
|
+
)
|
|
110
|
+
assert (await blob_storage.read(["key"], target_cls=MockStoreItem))[
|
|
111
|
+
"key"
|
|
112
|
+
] == MockStoreItem({"id": "item", "value": "data"})
|
|
113
|
+
assert (await blob_storage.read(["key2"], target_cls=MockStoreItem))[
|
|
114
|
+
"key2"
|
|
115
|
+
] == MockStoreItem({"id": "another_item", "value": "new_val"})
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_blob_storage_flow_existing_container_and_persistence(self):
|
|
119
|
+
|
|
120
|
+
connection_string = (
|
|
121
|
+
"AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq"
|
|
122
|
+
+ "2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint="
|
|
123
|
+
+ "http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;"
|
|
124
|
+
+ "TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
|
|
125
|
+
)
|
|
126
|
+
blob_service_client = BlobServiceClient.from_connection_string(
|
|
127
|
+
connection_string
|
|
128
|
+
)
|
|
129
|
+
container_name = "asdkunittestpopulated"
|
|
130
|
+
container_client = blob_service_client.get_container_client(container_name)
|
|
131
|
+
|
|
132
|
+
# reset state of test container
|
|
133
|
+
try:
|
|
134
|
+
await container_client.delete_container()
|
|
135
|
+
except ResourceNotFoundError:
|
|
136
|
+
pass
|
|
137
|
+
await container_client.create_container()
|
|
138
|
+
|
|
139
|
+
initial_data = {
|
|
140
|
+
"item1": MockStoreItem({"id": "item1", "value": "data1"}),
|
|
141
|
+
"__some_key": MockStoreItem({"id": "item2", "value": "data2"}),
|
|
142
|
+
"!another_key": MockStoreItem({"id": "item3", "value": "data3"}),
|
|
143
|
+
"1230": MockStoreItemB({"id": "item8", "value": "data"}, False),
|
|
144
|
+
"key-with-dash": MockStoreItem({"id": "item4", "value": "data"}),
|
|
145
|
+
"key.with.dot": MockStoreItem({"id": "item5", "value": "data"}),
|
|
146
|
+
"key/with/slash": MockStoreItem({"id": "item6", "value": "data"}),
|
|
147
|
+
"another key": MockStoreItemB({"id": "item7", "value": "data"}, True),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
baseline_storage = StorageBaseline(initial_data)
|
|
151
|
+
|
|
152
|
+
for key, value in initial_data.items():
|
|
153
|
+
value_rep = json.dumps(value.store_item_to_json()).encode("utf-8")
|
|
154
|
+
await container_client.upload_blob(
|
|
155
|
+
name=key, data=BytesIO(value_rep), overwrite=True
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
blob_storage_config = BlobStorageConfig(
|
|
159
|
+
container_name=container_name, connection_string=connection_string
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
storage = BlobStorage(blob_storage_config)
|
|
163
|
+
|
|
164
|
+
assert await baseline_storage.equals(storage)
|
|
165
|
+
assert (
|
|
166
|
+
await storage.read(["1230", "another key"], target_cls=MockStoreItemB)
|
|
167
|
+
) == baseline_storage.read(["1230", "another key"])
|
|
168
|
+
|
|
169
|
+
changes = {
|
|
170
|
+
"item1": MockStoreItem({"id": "item1", "value": "data1_changed"}),
|
|
171
|
+
"__some_key": MockStoreItem({"id": "item2", "value": "data2_changed"}),
|
|
172
|
+
"new_item": MockStoreItem({"id": "new_item", "value": "new_data"}),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
baseline_storage.write(changes)
|
|
176
|
+
await storage.write(changes)
|
|
177
|
+
|
|
178
|
+
baseline_storage.delete(["!another_key", "item1"])
|
|
179
|
+
await storage.delete(["!another_key", "item1"])
|
|
180
|
+
assert await baseline_storage.equals(storage)
|
|
181
|
+
|
|
182
|
+
del storage
|
|
183
|
+
gc.collect()
|
|
184
|
+
|
|
185
|
+
blob_client = container_client.get_blob_client("item1")
|
|
186
|
+
with pytest.raises(ResourceNotFoundError):
|
|
187
|
+
await (await blob_client.download_blob()).readall()
|
|
188
|
+
|
|
189
|
+
blob_client = container_client.get_blob_client("1230")
|
|
190
|
+
item = await (await blob_client.download_blob()).readall()
|
|
191
|
+
assert (
|
|
192
|
+
MockStoreItemB.from_json_to_store_item(json.loads(item))
|
|
193
|
+
== initial_data["1230"]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
await container_client.delete_container()
|