diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c8dffd6..f225bc687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Removed `azure-core` and `azure-storage-blob` dependencies. +- `GriptapeCloudFileManagerDriver` no longer requires `drivers-file-manager-griptape-cloud` extra. + ## \[0.34.0\] - 2024-10-29 ### Added diff --git a/MIGRATION.md b/MIGRATION.md index d96d5ca0e..5e2d51f9d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -39,7 +39,6 @@ Defaults.drivers_config = AnthropicDriversConfig( Many callables have been renamed for consistency. Update your code to use the new names using the [CHANGELOG.md](https://github.com/griptape-ai/griptape/pull/1275/files#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed) as the source of truth. - ### Removed `CompletionChunkEvent` `CompletionChunkEvent` has been removed. There is now `BaseChunkEvent` with children `TextChunkEvent` and `ActionChunkEvent`. `BaseChunkEvent` can replace `completion_chunk_event.token` by doing `str(base_chunk_event)`. @@ -146,7 +145,6 @@ event_listener_driver.flush_events() The `observable` decorator has been moved to `griptape.common.decorators`. Update your imports accordingly. - #### Before ```python @@ -183,7 +181,6 @@ driver = HuggingFacePipelinePromptDriver( `execute` has been renamed to `run` in several places. Update your code accordingly. - #### Before ```python @@ -298,7 +295,6 @@ pip install griptape[drivers-prompt-huggingface-hub] pip install torch ``` - ### `CsvLoader`, `DataframeLoader`, and `SqlLoader` return types `CsvLoader`, `DataframeLoader`, and `SqlLoader` now return a `list[TextArtifact]` instead of `list[CsvRowArtifact]`. diff --git a/docs/griptape-framework/drivers/file-manager-drivers.md b/docs/griptape-framework/drivers/file-manager-drivers.md index 37012c29f..adb77ed57 100644 --- a/docs/griptape-framework/drivers/file-manager-drivers.md +++ b/docs/griptape-framework/drivers/file-manager-drivers.md @@ -19,9 +19,6 @@ Or use them independently as shown below for each driver: ### Griptape Cloud -!!! info - This driver requires the `drivers-file-manager-griptape-cloud` [extra](../index.md#extras). - The [GriptapeCloudFileManagerDriver](../../reference/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.md) allows you to load and save files sourced from Griptape Cloud Asset and Bucket resources. ```python diff --git a/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py b/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py index 5138a1fe4..7d917c124 100644 --- a/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py +++ b/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py @@ -2,20 +2,16 @@ import logging import os -from typing import TYPE_CHECKING, Optional +from typing import Optional from urllib.parse import urljoin import requests from attrs import Attribute, Factory, define, field from griptape.drivers import BaseFileManagerDriver -from griptape.utils import import_optional_dependency logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from azure.storage.blob import BlobClient - @define class GriptapeCloudFileManagerDriver(BaseFileManagerDriver): @@ -79,7 +75,6 @@ def try_list_files(self, path: str, postfix: str = "") -> list[str]: data = {"prefix": full_key} if postfix: data["postfix"] = postfix - # TODO: GTC SDK: Pagination list_assets_response = self._call_api( method="list", path=f"/buckets/{self.bucket_id}/assets", json=data, raise_for_status=False ).json() @@ -93,17 +88,15 @@ def try_load_file(self, path: str) -> bytes: raise IsADirectoryError try: - blob_client = self._get_blob_client(full_key=full_key) + sas_url, headers = self._get_asset_url(full_key) + response = requests.get(sas_url, headers=headers) + response.raise_for_status() + return response.content except requests.exceptions.HTTPError as e: if e.response.status_code == 404: raise FileNotFoundError from e raise e - try: - return blob_client.download_blob().readall() - except import_optional_dependency("azure.core.exceptions").ResourceNotFoundError as e: - raise FileNotFoundError from e - def try_save_file(self, path: str, value: bytes) -> str: full_key = self._to_full_key(path) @@ -114,23 +107,25 @@ def try_save_file(self, path: str, value: bytes) -> str: self._call_api(method="get", path=f"/buckets/{self.bucket_id}/assets/{full_key}", raise_for_status=True) except requests.exceptions.HTTPError as e: if e.response.status_code == 404: - logger.info("Asset '%s' not found, attempting to create", full_key) - data = {"name": full_key} - self._call_api(method="put", path=f"/buckets/{self.bucket_id}/assets", json=data, raise_for_status=True) + self._call_api( + method="put", + path=f"/buckets/{self.bucket_id}/assets", + json={"name": full_key}, + raise_for_status=True, + ) else: raise e + sas_url, headers = self._get_asset_url(full_key) + response = requests.put(sas_url, data=value, headers=headers) + response.raise_for_status() - blob_client = self._get_blob_client(full_key=full_key) - - blob_client.upload_blob(data=value, overwrite=True) return f"buckets/{self.bucket_id}/assets/{full_key}" - def _get_blob_client(self, full_key: str) -> BlobClient: + def _get_asset_url(self, full_key: str) -> tuple[str, dict]: url_response = self._call_api( method="post", path=f"/buckets/{self.bucket_id}/asset-urls/{full_key}", raise_for_status=True ).json() - sas_url = url_response["url"] - return import_optional_dependency("azure.storage.blob").BlobClient.from_blob_url(blob_url=sas_url) + return url_response["url"], url_response.get("headers", {}) def _get_url(self, path: str) -> str: path = path.lstrip("/") diff --git a/poetry.lock b/poetry.lock index b3d49dc6f..b08aaa05a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -265,45 +265,6 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] -[[package]] -name = "azure-core" -version = "1.31.0" -description = "Microsoft Azure Core Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure_core-1.31.0-py3-none-any.whl", hash = "sha256:22954de3777e0250029360ef31d80448ef1be13b80a459bff80ba7073379e2cd"}, - {file = "azure_core-1.31.0.tar.gz", hash = "sha256:656a0dd61e1869b1506b7c6a3b31d62f15984b1a573d6326f6aa2f3e4123284b"}, -] - -[package.dependencies] -requests = ">=2.21.0" -six = ">=1.11.0" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["aiohttp (>=3.0)"] - -[[package]] -name = "azure-storage-blob" -version = "12.23.1" -description = "Microsoft Azure Blob Storage Client Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure_storage_blob-12.23.1-py3-none-any.whl", hash = "sha256:1c2238aa841d1545f42714a5017c010366137a44a0605da2d45f770174bfc6b4"}, - {file = "azure_storage_blob-12.23.1.tar.gz", hash = "sha256:a587e54d4e39d2a27bd75109db164ffa2058fe194061e5446c5a89bca918272f"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -cryptography = ">=2.1.4" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["azure-core[aio] (>=1.30.0)"] - [[package]] name = "babel" version = "2.16.0" @@ -2339,17 +2300,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isodate" -version = "0.7.2" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, - {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, -] - [[package]] name = "jaraco-classes" version = "3.4.0" @@ -7192,7 +7142,7 @@ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linke test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] -all = ["anthropic", "astrapy", "azure-core", "azure-storage-blob", "beautifulsoup4", "boto3", "cohere", "diffusers", "duckduckgo-search", "elevenlabs", "exa-py", "google-generativeai", "mail-parser", "markdownify", "marqo", "ollama", "opensearch-py", "opentelemetry-api", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation", "opentelemetry-instrumentation-threading", "opentelemetry-sdk", "pandas", "pgvector", "pillow", "pinecone-client", "playwright", "psycopg2-binary", "pusher", "pymongo", "pypdf", "qdrant-client", "redis", "snowflake-sqlalchemy", "sqlalchemy", "tavily-python", "trafilatura", "transformers", "voyageai"] +all = ["anthropic", "astrapy", "beautifulsoup4", "boto3", "cohere", "diffusers", "duckduckgo-search", "elevenlabs", "exa-py", "google-generativeai", "mail-parser", "markdownify", "marqo", "ollama", "opensearch-py", "opentelemetry-api", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation", "opentelemetry-instrumentation-threading", "opentelemetry-sdk", "pandas", "pgvector", "pillow", "pinecone-client", "playwright", "psycopg2-binary", "pusher", "pymongo", "pypdf", "qdrant-client", "redis", "snowflake-sqlalchemy", "sqlalchemy", "tavily-python", "trafilatura", "transformers", "voyageai"] drivers-embedding-amazon-bedrock = ["boto3"] drivers-embedding-amazon-sagemaker = ["boto3"] drivers-embedding-cohere = ["cohere"] @@ -7204,7 +7154,6 @@ drivers-event-listener-amazon-iot = ["boto3"] drivers-event-listener-amazon-sqs = ["boto3"] drivers-event-listener-pusher = ["pusher"] drivers-file-manager-amazon-s3 = ["boto3"] -drivers-file-manager-griptape-cloud = ["azure-core", "azure-storage-blob"] drivers-image-generation-huggingface = ["diffusers", "pillow"] drivers-memory-conversation-amazon-dynamodb = ["boto3"] drivers-memory-conversation-redis = ["redis"] @@ -7246,4 +7195,4 @@ loaders-sql = ["sqlalchemy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "816a925736967c12b42ffddce1e48909348d11e7d341127d5e9e1224b44ba00e" +content-hash = "436fc99379ee14642f24a3e43f2ae2c99396839fd1ea986d6dc1a29a891e6865" diff --git a/pyproject.toml b/pyproject.toml index 7b7b5ce85..81da42fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,8 +64,6 @@ opentelemetry-exporter-otlp-proto-http = {version = "^1.25.0", optional = true} diffusers = {version = "^0.31.0", optional = true} tavily-python = {version = "^0.5.0", optional = true} exa-py = {version = "^1.1.4", optional = true} -azure-core = "^1.31.0" -azure-storage-blob = "^12.23.1" # loaders pandas = {version = "^1.3", optional = true} @@ -149,7 +147,6 @@ drivers-observability-datadog = [ drivers-image-generation-huggingface = ["diffusers", "pillow"] drivers-file-manager-amazon-s3 = ["boto3"] -drivers-file-manager-griptape-cloud = ["azure-core", "azure-storage-blob"] loaders-pdf = ["pypdf"] loaders-image = ["pillow"] diff --git a/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py b/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py index 0ce837dc1..4e9f9389a 100644 --- a/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py +++ b/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py @@ -2,7 +2,6 @@ import pytest import requests -from azure.core.exceptions import ResourceNotFoundError class TestGriptapeCloudFileManagerDriver: @@ -98,19 +97,18 @@ def test_try_list_files_not_directory(self, mocker, driver): driver.try_list_files("foo") def test_try_load_file(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"url": "https://foo.bar"} - mocker.patch("requests.request", return_value=mock_response) + mock_url_response = mocker.Mock() + mock_url_response.status_code = 200 + mock_url_response.json.return_value = {"url": "https://foo.bar"} + mocker.patch("requests.request", return_value=mock_url_response) - mock_bytes = b"bytes" - mock_blob_client = mocker.Mock() - mock_blob_client.download_blob.return_value.readall.return_value = mock_bytes - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) + mock_file_response = mocker.Mock() + mock_file_response.status_code = 200 + mock_file_response.content = b"bytes" + mocker.patch("requests.get", return_value=mock_file_response) response = driver.try_load_file("foo") - - assert response == mock_bytes + assert response == b"bytes" def test_try_load_file_directory(self, mocker, driver): mock_response = mocker.Mock() @@ -121,42 +119,29 @@ def test_try_load_file_directory(self, mocker, driver): with pytest.raises(IsADirectoryError): driver.try_load_file("foo/") - def test_try_load_file_sas_404(self, mocker, driver): + def test_try_load_file_asset_url_404(self, mocker, driver): mocker.patch("requests.request", side_effect=requests.exceptions.HTTPError(response=mock.Mock(status_code=404))) with pytest.raises(FileNotFoundError): driver.try_load_file("foo") - def test_try_load_file_sas_500(self, mocker, driver): + def test_try_load_file_asset_url_500(self, mocker, driver): mocker.patch("requests.request", side_effect=requests.exceptions.HTTPError(response=mock.Mock(status_code=500))) with pytest.raises(requests.exceptions.HTTPError): driver.try_load_file("foo") - def test_try_load_file_blob_404(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"url": "https://foo.bar"} - mocker.patch("requests.request", return_value=mock_response) - - mock_blob_client = mocker.Mock() - mock_blob_client.download_blob.side_effect = ResourceNotFoundError() - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) - - with pytest.raises(FileNotFoundError): - driver.try_load_file("foo") - - def test_try_save_files(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"url": "https://foo.bar"} - mocker.patch("requests.request", return_value=mock_response) + def test_try_save_file(self, mocker, driver): + mock_url_response = mocker.Mock() + mock_url_response.status_code = 200 + mock_url_response.json.return_value = {"url": "https://foo.bar"} + mocker.patch("requests.request", return_value=mock_url_response) - mock_blob_client = mocker.Mock() - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) + mock_put_response = mocker.Mock() + mock_put_response.status_code = 200 + mocker.patch("requests.put", return_value=mock_put_response) response = driver.try_save_file("foo", b"value") - assert response == "buckets/1/assets/foo" def test_try_save_file_directory(self, mocker, driver): @@ -168,24 +153,17 @@ def test_try_save_file_directory(self, mocker, driver): with pytest.raises(IsADirectoryError): driver.try_save_file("foo/", b"value") - def test_try_save_file_sas_404(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.json.return_value = {"url": "https://foo.bar"} - mock_response.raise_for_status.side_effect = [ - requests.exceptions.HTTPError(response=mock.Mock(status_code=404)), - None, - None, - ] - mocker.patch("requests.request", return_value=mock_response) - - mock_blob_client = mocker.Mock() - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) - - response = driver.try_save_file("foo", b"value") + def test_try_save_file_asset_url_404(self, mocker, driver): + mock_create_response = mocker.Mock() + mock_create_response.raise_for_status.side_effect = requests.exceptions.HTTPError( + response=mock.Mock(status_code=404) + ) + mocker.patch("requests.request", return_value=mock_create_response) - assert response == "buckets/1/assets/foo" + with pytest.raises(requests.exceptions.HTTPError): + driver.try_save_file("foo", b"value") - def test_try_save_file_sas_500(self, mocker, driver): + def test_try_save_file_asset_url_500(self, mocker, driver): mocker.patch("requests.request", side_effect=requests.exceptions.HTTPError(response=mock.Mock(status_code=500))) with pytest.raises(requests.exceptions.HTTPError):