From 1a1c965cd74b20bce73a85bf6ed6ca756fc257d3 Mon Sep 17 00:00:00 2001 From: stabldev <114811070+stabldev@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:37:31 +0530 Subject: [PATCH 1/3] chore: remove async prefix from class names --- README.md | 6 +++--- pyproject.toml | 4 ++-- src/async_storages/__init__.py | 5 +++++ src/{cloud_storage => async_storages}/base.py | 8 ++++---- .../integrations/__init__.py | 0 .../integrations/sqlalchemy.py | 12 ++++++------ src/{cloud_storage => async_storages}/py.typed | 0 src/{cloud_storage => async_storages}/s3.py | 6 +++--- src/{cloud_storage => async_storages}/utils.py | 0 src/cloud_storage/__init__.py | 5 ----- tests/test_integrations/conftest.py | 6 +++--- tests/test_integrations/test_sqlalchemy.py | 10 +++++----- tests/test_s3_storage.py | 10 +++++----- uv.lock | 2 +- 14 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 src/async_storages/__init__.py rename src/{cloud_storage => async_storages}/base.py (86%) rename src/{cloud_storage => async_storages}/integrations/__init__.py (100%) rename src/{cloud_storage => async_storages}/integrations/sqlalchemy.py (68%) rename src/{cloud_storage => async_storages}/py.typed (100%) rename src/{cloud_storage => async_storages}/s3.py (96%) rename src/{cloud_storage => async_storages}/utils.py (100%) delete mode 100644 src/cloud_storage/__init__.py diff --git a/README.md b/README.md index a50fba1..8a05f11 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ uv add "fastapi-cloud-storage[s3]" 1. **Define your async S3 storage** ```py -from cloud_storage import AsyncS3Storage +from async_storages import AsyncS3Storage storage = AsyncS3Storage( bucket_name="your-bucket", @@ -32,7 +32,7 @@ storage = AsyncS3Storage( ```py from sqlalchemy import Column, Integer -from cloud_storage.integrations.sqlalchemy import AsyncFileType +from async_storages.integrations.sqlalchemy import AsyncFileType from sqlalchemy.orm import declarative_base Base = declarative_base() @@ -79,7 +79,7 @@ await doc.file.delete() # delete current file ```py from fastapi import FastAPI, UploadFile -from cloud_storage import AsyncS3Storage +from async_storages import AsyncS3Storage app = FastAPI(...) storage = AsyncS3Storage(...) diff --git a/pyproject.toml b/pyproject.toml index 2bd8670..e19698c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "fastapi-cloud-storage" +name = "fastapi-async-storages" version = "0.1.0" description = "A powerful, extensible, and async-ready cloud object storage backend for FastAPI." readme = "README.md" @@ -23,7 +23,7 @@ dev = [ [tool.uv.build-backend] module-root = "src" -module-name = "cloud_storage" +module-name = "async_storages" [tool.pyright] reportMissingTypeStubs = "none" diff --git a/src/async_storages/__init__.py b/src/async_storages/__init__.py new file mode 100644 index 0000000..7f86dff --- /dev/null +++ b/src/async_storages/__init__.py @@ -0,0 +1,5 @@ +from .base import StorageFile +from .s3 import S3Storage + +__version__ = "0.1.0" +__all__ = ["StorageFile", "S3Storage"] diff --git a/src/cloud_storage/base.py b/src/async_storages/base.py similarity index 86% rename from src/cloud_storage/base.py rename to src/async_storages/base.py index 2ef4d4e..3229783 100644 --- a/src/cloud_storage/base.py +++ b/src/async_storages/base.py @@ -2,7 +2,7 @@ from typing import BinaryIO -class AsyncBaseStorage: +class BaseStorage: def get_name(self, name: str) -> str: raise NotImplementedError() @@ -19,10 +19,10 @@ async def delete(self, name: str) -> None: raise NotImplementedError() -class AsyncStorageFile: - def __init__(self, name: str, storage: AsyncBaseStorage): +class StorageFile: + def __init__(self, name: str, storage: BaseStorage): self._name: str = name - self._storage: AsyncBaseStorage = storage + self._storage: BaseStorage = storage @property def name(self) -> str: diff --git a/src/cloud_storage/integrations/__init__.py b/src/async_storages/integrations/__init__.py similarity index 100% rename from src/cloud_storage/integrations/__init__.py rename to src/async_storages/integrations/__init__.py diff --git a/src/cloud_storage/integrations/sqlalchemy.py b/src/async_storages/integrations/sqlalchemy.py similarity index 68% rename from src/cloud_storage/integrations/sqlalchemy.py rename to src/async_storages/integrations/sqlalchemy.py index e29d362..67f6160 100644 --- a/src/cloud_storage/integrations/sqlalchemy.py +++ b/src/async_storages/integrations/sqlalchemy.py @@ -2,16 +2,16 @@ from sqlalchemy.engine.interfaces import Dialect from sqlalchemy.types import TypeDecorator, TypeEngine, Unicode -from cloud_storage.base import AsyncBaseStorage, AsyncStorageFile +from async_storages.base import BaseStorage, StorageFile -class AsyncFileType(TypeDecorator[Any]): +class FileType(TypeDecorator[Any]): impl: TypeEngine[Any] | type[TypeEngine[Any]] = Unicode cache_ok: bool | None = True - def __init__(self, storage: AsyncBaseStorage, *args: Any, **kwargs: Any): + def __init__(self, storage: BaseStorage, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.storage: AsyncBaseStorage = storage + self.storage: BaseStorage = storage @override def process_bind_param(self, value: Any, dialect: Dialect) -> str: @@ -28,7 +28,7 @@ def process_bind_param(self, value: Any, dialect: Dialect) -> str: @override def process_result_value( self, value: Any | None, dialect: Dialect - ) -> AsyncStorageFile | None: + ) -> StorageFile | None: if value is None: return None - return AsyncStorageFile(name=value, storage=self.storage) + return StorageFile(name=value, storage=self.storage) diff --git a/src/cloud_storage/py.typed b/src/async_storages/py.typed similarity index 100% rename from src/cloud_storage/py.typed rename to src/async_storages/py.typed diff --git a/src/cloud_storage/s3.py b/src/async_storages/s3.py similarity index 96% rename from src/cloud_storage/s3.py rename to src/async_storages/s3.py index 3038ce7..8925991 100644 --- a/src/cloud_storage/s3.py +++ b/src/async_storages/s3.py @@ -3,8 +3,8 @@ from pathlib import Path from typing import Any, BinaryIO, override -from cloud_storage.base import AsyncBaseStorage -from cloud_storage.utils import secure_filename +from async_storages.base import BaseStorage +from async_storages.utils import secure_filename try: import aioboto3 @@ -15,7 +15,7 @@ ) -class AsyncS3Storage(AsyncBaseStorage): +class S3Storage(BaseStorage): def __init__( self, bucket_name: str, diff --git a/src/cloud_storage/utils.py b/src/async_storages/utils.py similarity index 100% rename from src/cloud_storage/utils.py rename to src/async_storages/utils.py diff --git a/src/cloud_storage/__init__.py b/src/cloud_storage/__init__.py deleted file mode 100644 index 93ddba0..0000000 --- a/src/cloud_storage/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .base import AsyncStorageFile -from .s3 import AsyncS3Storage - -__version__ = "0.1.0" -__all__ = ["AsyncStorageFile", "AsyncS3Storage"] diff --git a/tests/test_integrations/conftest.py b/tests/test_integrations/conftest.py index 7b9e8f0..19384fc 100644 --- a/tests/test_integrations/conftest.py +++ b/tests/test_integrations/conftest.py @@ -1,14 +1,14 @@ from typing import Any import pytest -from cloud_storage import AsyncS3Storage +from async_storages import S3Storage @pytest.fixture -async def s3_test_storage(s3_test_env: Any) -> AsyncS3Storage: +async def s3_test_storage(s3_test_env: Any) -> S3Storage: bucket_name, endpoint_without_scheme = s3_test_env - return AsyncS3Storage( + return S3Storage( bucket_name=bucket_name, endpoint_url=endpoint_without_scheme, aws_access_key_id="fake-access-key", diff --git a/tests/test_integrations/test_sqlalchemy.py b/tests/test_integrations/test_sqlalchemy.py index 2ffcb9b..dc8afc8 100644 --- a/tests/test_integrations/test_sqlalchemy.py +++ b/tests/test_integrations/test_sqlalchemy.py @@ -7,8 +7,8 @@ from sqlalchemy.ext.asyncio.session import async_sessionmaker from sqlalchemy.orm import declarative_base -from cloud_storage import AsyncStorageFile -from cloud_storage.integrations.sqlalchemy import AsyncFileType +from async_storages import StorageFile +from async_storages.integrations.sqlalchemy import FileType Base = declarative_base() @@ -16,7 +16,7 @@ class Document(Base): __tablename__: str = "documents" id: Column[int] = Column(Integer, primary_key=True) - file: Column[str] = Column(AsyncFileType(storage=None)) # pyright: ignore[reportArgumentType] + file: Column[str] = Column(FileType(storage=None)) # pyright: ignore[reportArgumentType] @pytest.mark.asyncio @@ -55,7 +55,7 @@ async def test_sqlalchemy_with_s3(s3_test_storage: Any): doc = await session.get(Document, doc_id) # check instance type - assert isinstance(doc.file, AsyncStorageFile) + assert isinstance(doc.file, StorageFile) assert doc.file.name == f"{file_name}" # methods should work @@ -112,7 +112,7 @@ async def test_sqlalchemy_filetype_none_and_plain_string_with_s3(s3_test_storage assert doc_none.file is None # check instance type - assert isinstance(doc_plain.file, AsyncStorageFile) + assert isinstance(doc_plain.file, StorageFile) assert doc_plain.file.name == "plain/path/file.txt" # methods should work diff --git a/tests/test_s3_storage.py b/tests/test_s3_storage.py index ab3122c..e7c0841 100644 --- a/tests/test_s3_storage.py +++ b/tests/test_s3_storage.py @@ -2,13 +2,13 @@ from typing import Any import pytest -from cloud_storage import AsyncS3Storage +from async_storages import S3Storage @pytest.mark.asyncio async def test_s3_storage_methods(s3_test_env: Any): bucket_name, endpoint_without_scheme = s3_test_env - storage = AsyncS3Storage( + storage = S3Storage( bucket_name=bucket_name, endpoint_url=endpoint_without_scheme, aws_access_key_id="fake-access-key", @@ -44,7 +44,7 @@ async def test_s3_storage_methods(s3_test_env: Any): async def test_s3_storage_querystring_auth(s3_test_env: Any): bucket_name, endpoint_without_scheme = s3_test_env - storage = AsyncS3Storage( + storage = S3Storage( bucket_name=bucket_name, endpoint_url=endpoint_without_scheme, aws_access_key_id="fake-access-key", @@ -65,7 +65,7 @@ async def test_s3_storage_querystring_auth(s3_test_env: Any): async def test_s3_storage_custom_domain(s3_test_env: Any): bucket_name, endpoint_without_scheme = s3_test_env - storage = AsyncS3Storage( + storage = S3Storage( bucket_name=bucket_name, endpoint_url=endpoint_without_scheme, aws_access_key_id="fake-access-key", @@ -83,7 +83,7 @@ async def test_s3_storage_custom_domain(s3_test_env: Any): @pytest.mark.asyncio async def test_get_secure_key_normalization(): - storage = AsyncS3Storage( + storage = S3Storage( bucket_name="fake-bucket", endpoint_url="fake-endpoint-url", aws_access_key_id="fake-access-key", diff --git a/uv.lock b/uv.lock index 92c8d35..cc9836d 100644 --- a/uv.lock +++ b/uv.lock @@ -512,7 +512,7 @@ wheels = [ ] [[package]] -name = "fastapi-cloud-storage" +name = "fastapi-async-storages" version = "0.1.0" source = { editable = "." } From 7bf4a76fadbcc89ccd4a212ec875620a8f9ad819 Mon Sep 17 00:00:00 2001 From: stabldev <114811070+stabldev@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:38:55 +0530 Subject: [PATCH 2/3] chore: update in readme --- README.md | 18 +++++++++--------- tests/test_integrations/test_sqlalchemy.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8a05f11..a54131f 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ uv add "fastapi-cloud-storage[s3]" 1. **Define your async S3 storage** ```py -from async_storages import AsyncS3Storage +from async_storages import S3Storage -storage = AsyncS3Storage( +storage = S3Storage( bucket_name="your-bucket", endpoint_url="s3.your-cloud.com", aws_access_key_id="KEY", @@ -28,11 +28,11 @@ storage = AsyncS3Storage( ``` 2. **Define your SQLAlchemy/SQLModel model**\ - Use the provided `AsyncFileType` as the column type: + Use the provided `FileType` as the column type: ```py from sqlalchemy import Column, Integer -from async_storages.integrations.sqlalchemy import AsyncFileType +from async_storages.integrations.sqlalchemy import FileType from sqlalchemy.orm import declarative_base Base = declarative_base() @@ -40,7 +40,7 @@ Base = declarative_base() class Document(Base): __tablename__ = "documents" id = Column(Integer, primary_key=True) - file = Column(AsyncFileType(storage=storage)) + file = Column(FileType(storage=storage)) ``` 3. **Upload files asynchronously before DB commit**\ @@ -57,14 +57,14 @@ await session.commit() # fetch from DB doc = await session.get(Document, doc.id) -assert isinstance(doc.file, AsyncStorageFile) +assert isinstance(doc.file, StorageFile) url = await doc.file.get_url() await doc.file.delete() ``` 4. **Access files asynchronously**\ - When fetching from `DB`, file attribute is an `AsyncStorageFile` with async methods: + When fetching from `DB`, file attribute is an `StorageFile` with async methods: ```py doc = await session.get(Document, some_id) @@ -79,10 +79,10 @@ await doc.file.delete() # delete current file ```py from fastapi import FastAPI, UploadFile -from async_storages import AsyncS3Storage +from async_storages import S3Storage app = FastAPI(...) -storage = AsyncS3Storage(...) +storage = S3Storage(...) @app.post("/upload") async def upload_file(file: UploadFile): diff --git a/tests/test_integrations/test_sqlalchemy.py b/tests/test_integrations/test_sqlalchemy.py index dc8afc8..0d5b932 100644 --- a/tests/test_integrations/test_sqlalchemy.py +++ b/tests/test_integrations/test_sqlalchemy.py @@ -108,7 +108,7 @@ async def test_sqlalchemy_filetype_none_and_plain_string_with_s3(s3_test_storage doc_none = await session.get(Document, id_none) doc_plain = await session.get(Document, id_plain) - # None should stay None (no AsyncStorageFile instance) + # None should stay None (no StorageFile instance) assert doc_none.file is None # check instance type From c3f69fb58788ec10eb56d0902ec70eb963b0aeb1 Mon Sep 17 00:00:00 2001 From: stabldev <114811070+stabldev@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:41:03 +0530 Subject: [PATCH 3/3] chore: update readme --- README.md | 8 ++++---- src/async_storages/s3.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a54131f..ac57186 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# fastapi-cloud-storage +# fastapi-async-storages A powerful, extensible, and async-ready cloud object storage backend for FastAPI. > Drop-in, plug-and-play cloud storage for your FastAPI apps; with full async support.\ -Inspired by [fastapi-storages](https://github.com/aminalaee/fastapi-storages), built on modern async patterns using [aioboto3](https://github.com/terricain/aioboto3). +> Inspired by [fastapi-storages](https://github.com/aminalaee/fastapi-storages), built on modern async patterns using [aioboto3](https://github.com/terricain/aioboto3). ## Installation ```bash -uv add "fastapi-cloud-storage[s3]" +uv add "fastapi-async-storages[s3]" ``` ## Quick Start @@ -92,4 +92,4 @@ async def upload_file(file: UploadFile): ## License -[MIT](LICENSE) © 2025 ^_^ [`@stabldev`](https://github.com/stabldev) +[MIT](LICENSE) © 2025 ^\_^ [`@stabldev`](https://github.com/stabldev) diff --git a/src/async_storages/s3.py b/src/async_storages/s3.py index 8925991..89175ce 100644 --- a/src/async_storages/s3.py +++ b/src/async_storages/s3.py @@ -11,7 +11,7 @@ from botocore.exceptions import ClientError except ImportError: raise ImportError( - "'aioboto3' is not installed. Install with 'fastapi-cloud-storage[s3]'." + "'aioboto3' is not installed. Install with 'fastapi-async-storages[s3]'." )