diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst new file mode 100644 index 0000000..7a84689 --- /dev/null +++ b/docs/source/api_reference.rst @@ -0,0 +1,8 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 2 + + async_storages + async_storages.integrations diff --git a/docs/source/async_storages.integrations.rst b/docs/source/async_storages.integrations.rst new file mode 100644 index 0000000..35bd434 --- /dev/null +++ b/docs/source/async_storages.integrations.rst @@ -0,0 +1,10 @@ +async_storages.integrations +========================= + +.. autoclass:: async_storages.integrations.sqlalchemy.FileType + :exclude-members: cache_ok, impl, process_bind_param, process_result_value + :show-inheritance: + +.. autoclass:: async_storages.integrations.sqlalchemy.ImageType + :exclude-members: process_result_value + :show-inheritance: diff --git a/docs/source/async_storages.rst b/docs/source/async_storages.rst new file mode 100644 index 0000000..6975e75 --- /dev/null +++ b/docs/source/async_storages.rst @@ -0,0 +1,6 @@ +async_storages +============= + +.. automodule:: async_storages + :members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..5e4eb50 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,30 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "fastapi-async-storages" +copyright = "2025, stabldev" +author = "stabldev" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + # "qiskit_sphinx_theme", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", +] + +templates_path = ["_templates"] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "furo" +html_static_path = ["_static"] diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 0000000..d712b59 --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1,53 @@ +Contributing +============ + +Thank you for your interest in contributing to ``fastapi-async-storages``! +We welcome contributions of all kinds; whether code, documentation, bug reports, or ideas. + +Contribution Guide +------------------ + +How to Contribute +~~~~~~~~~~~~~~~~~ +1. **Check the roadmap and open issues** + + See what’s planned or in progress to avoid duplication and align your efforts. + +2. **Fork the repository and create a feature branch** + + Create your own copy of the project to work on. + Use a descriptive name for your branch, e.g., ``feature/async-gcs-backend``. + +3. **Write clear, concise code and documentation** + + Follow existing style and patterns. Include tests for new features or bug fixes. + +4. **Open a pull request (PR)** + + Provide a detailed description of your changes, related issues, and any testing you performed. + +5. **Respond to review feedback** + + We may ask for clarifications, improvements, or changes before merging. + +Coding Guidelines +~~~~~~~~~~~~~~~~~ +- Write asynchronous code following Python’s async/await conventions. +- Ensure compatibility with FastAPI and `SQLAlchemy `_/`SQLModel `_ async patterns. +- Add type hints for functions and methods. +- Document public APIs and complex internal logic clearly. +- Adhere to `PEP 8 style `_ guidelines for Python code. + +Reporting Issues +~~~~~~~~~~~~~~~~~ +- Use the issue tracker to report bugs, suggest enhancements, or ask questions. +- Provide clear reproduction steps, error messages, and environment details. + +Community Conduct +~~~~~~~~~~~~~~~~~ +- Be respectful and considerate to all contributors. +- Follow inclusive language practices. +- Contributions must comply with the project’s license. + +Thank you for helping make ``fastapi-async-storages`` better for everyone! +We look forward to your contributions and collaboration. diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 0000000..bf0f2de --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,39 @@ +FAQ +=== + +This section answers common questions about ``fastapi-async-storages``, explaining its purpose, +differences from similar libraries, usage considerations, and extensibility options. + +What is `fastapi-async-storages `_? +--------------------------------------------------------------------------------------- +`fastapi-async-storages `_ is the asynchronous counterpart to `fastapi-storages `_. +It provides non-blocking storage backends for FastAPI applications using async libraries such as `aioboto3 `_ for S3. + +How is it different from `fastapi-storages `_? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +While `fastapi-storages `_ is sync-based and built around traditional blocking I/O, +`fastapi-async-storages `_ is fully asynchronous. +This allows your FastAPI app to handle more concurrent requests efficiently, especially during file upload or download operations. + +Why upload files before commit? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`SQLAlchemy `_’s ORM is primarily synchronous, even when used with `AsyncSession `_. +The model layer and column bindings (including file fields) are not async-aware. +Therefore, storage operations like uploading or deleting files must happen **before** the database commit. + +Does `SQLModel `_ integration work the same way? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Yes. The `SQLModel `_ integration layer internally uses `SQLAlchemy `_’s async engine and sessions. +You can define :class:`~async_storages.integrations.sqlalchemy.FileType` columns exactly as you would in SQLAlchemy models, following the same upload-before-commit pattern. + +Can I use it in synchronous contexts? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You can, but it’s not recommended. The library is designed around `asyncio `_ to take advantage of FastAPI’s asynchronous execution. +If your app runs entirely synchronously, stick with `fastapi-storages `_ for simplicity. + +Can I extend it with other storage backends? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Yes. The library’s base :class:`~async_storages.base.BaseStorage` class is extensible. +You can implement new async backends (like local filesystem, Google Cloud Storage, or Azure Blob) by subclassing it and implementing the async methods ``upload``, ``get_url``, and ``delete``. + +`fastapi-async-storages `_ aims to stay compatible with existing `fastapi-storages `_ APIs, so extension patterns remain familiar. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..8acf037 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,86 @@ +fastapi-async-storages +====================== + +A powerful, extensible, and async-ready cloud object storage backend for FastAPI. + +.. epigraph:: + Drop-in, plug-and-play cloud storage for your FastAPI apps; with full async support. + Inspired by `fastapi-storages `_, + built on modern async patterns using `aioboto3 `_. + +Features +-------- + +* Fully asynchronous storage interface designed for FastAPI applications +* Async S3 backend powered by `aioboto3 `_ +* `SQLAlchemy `_ and `SQLModel `_ integration +* Typed and extensible design +* Supports FastAPI dependency injection + +Table of contents +----------------- + +.. toctree:: + :caption: USER GUIDE + :maxdepth: 3 + + installation + usage + api_reference + +.. toctree:: + :maxdepth: 1 + + faq + +.. toctree:: + :caption: DEVELOPMENT + :maxdepth: 1 + + roadmap + contributing + +Example: FastAPI +---------------- + +.. code-block:: python + + from fastapi import FastAPI, UploadFile + from sqlalchemy import Column, Integer + from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession + from sqlalchemy.orm import sessionmaker, declarative_base + from async_storages import S3Storage + from async_storages.integrations.sqlalchemy import FileType + + Base = declarative_base() + + app = FastAPI() + storage = S3Storage(...) + engine = create_async_engine("sqlite+aiosqlite:///test.db", echo=True) + + # create AsyncSession factory + AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + class Example(Base): + __tablename__ = "example" + + id = Column(Integer, primary_key=True) + file = Column(FileType(storage=storage)) + + # create tables inside an async context + @app.on_event("startup") + async def startup(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + @app.post("/upload/") + async def create_upload_file(file: UploadFile): + file_name = f"uploads/{file.filename}" + # upload before commit due to the sqlalchemy binding being sync + await storage.upload(file.file, file_name) + + example = Example(file=file) + async with AsyncSessionLocal() as session: + session.add(example) + await session.commit() + return {"filename": example.file.name} diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 0000000..20b0f0a --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,49 @@ +Installation +============ + +This document explains how to install the ``fastapi-async-storages`` package. +Note that you can use any package manager you prefer, such as `uv`, `pip`, or others. + +Prerequisites +------------- +- Python 3.12 or higher +- A package manager like `uv` installed (see https://astral.sh/uv/ for more info) + +Installation with uv +-------------------- + +You can install the package using `uv`: + +.. code-block:: bash + + uv add fastapi-async-storages + # for s3 support: + uv add fastapi-async-storages[s3] + +.. warning:: + + If you need **image support** (using :class:`~async_storages.integrations.sqlalchemy.ImageType` or :class:`~async_storages.StorageImage`), + make sure :code:`Pillow` library is installed. + + .. code-block:: bash + + uv add Pillow + # or uv pip install Pillow + +Or, to install from source: + +.. code-block:: bash + + git clone https://github.com/stabldev/fastapi-async-storages.git + cd fastapi-async-storages + uv sync --all-extras + +Verify installation +------------------- + +You can verify the package installation by importing it: + +.. code-block:: python + + import async_storages + print(async_storages.__version__) diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst new file mode 100644 index 0000000..d4e352b --- /dev/null +++ b/docs/source/roadmap.rst @@ -0,0 +1,36 @@ +Roadmap +======= + +This list shows the current and planned features of ``fastapi-async-storages``; +checked items are implemented, unchecked are upcoming. + +Completed & Planned Features +------------------------------- + +Storage Backends +~~~~~~~~~~~~~~~~ +- [x] Async S3 backend powered by `aioboto3 `_ +- [x] Compatibility with `MinIO `_ and other S3-compatible services +- [ ] Async Local Filesystem backend using `aiofiles `_ +- [ ] Async Google Cloud Storage (GCS) backend using `google-cloud-storage `_ +- [ ] Async Azure Blob Storage integration with `azure-sdk-for-python `_ + +Framework Integrations +~~~~~~~~~~~~~~~~~~~~~~ +- [x] `SQLAlchemy ORM `_ async integration +- [ ] `Tortoise ORM `_ async integration +- [ ] `Peewee ORM `_ async integration + +Features & Enhancements +~~~~~~~~~~~~~~~~~~~~~~~ +- [x] Presigned URL generation for uploads and downloads +- [ ] Async streaming support for large files +- [ ] Bulk async upload and delete operations +- [ ] Progress tracking with hooks or callbacks +- [ ] Automatic cleanup utilities for orphaned/expired files + +DX & Documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- [x] Core documentation and usage examples +- [x] Testing utilities and mocks for async storage testing +- [ ] Expanded real-world usage and best practices diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 0000000..e55beb3 --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,132 @@ +Usage +===== + +This section provides practical examples and guidance on using ``fastapi-async-storages`` +to handle asynchronous file storage in your FastAPI applications. + +Working with storages +--------------------- + +Often in projects, you want to get input file in the API and store it somewhere. +The `fastapi-async-storages` simplifies the process to store and retrieve the files in a re-usable manner. + +Available storage backends: + +* :class:`~async_storages.S3Storage`: To store file objects in Amazon S3 or any s3-compatible object storage. + +S3Storage +~~~~~~~~~ + +The :class:`~async_storages.S3Storage` class provides an asynchronous interface for interacting +with S3-compatible object storages such as AWS S3 or MinIO. + +Now let's see a minimal example of using :class:`~async_storages.S3Storage` in action: + +.. code-block:: python + + from io import BytesIO + from async_storages import S3Storage + + storage = S3Storage( + bucket_name="my-bucket", + endpoint_url="localhost:9000", # no protocol (http/https) + aws_access_key_id="fake-access-key", + aws_secret_access_key="fake-secret-key", + use_ssl=False, + ) + + async def main(): + file_name = "uploads/example.txt" + file_obj = BytesIO(b"hello world") + + # upload a file + await storage.upload(file_obj, file_name) + # get file URL + url = await storage.get_url(file_name) + print(url) + # get file size + size = await storage.get_size(file_name) + # delete file + await storage.delete(file_name) + +.. warning:: + + You should never hard-code credentials like `aws_access_key_id` and `aws_secret_access_key` in the code. + Instead, you can read values from environment variables or read from your app settings. + +Working with ORM extensions +--------------------------- + +The example you saw was useful, but **fastapi-async-storages** has ORM integrations +which makes storing and serving the files easier. + +Support ORM include: + +* `SQLAlchemy `_ +* `SQLModel `_ + +SQLAlchemy +~~~~~~~~~~ + +You can use custom :code:`SQLAlchemy` types from :code:`fastapi-async-storages` for this. + +Supported types include: + +* :class:`~async_storages.integrations.sqlalchemy.FileType`: Type that returns a :class:`~async_storages.StorageFile` instance for file fields. +* :class:`~async_storages.integrations.sqlalchemy.ImageType`: Type that returns a :class:`~async_storages.StorageImage` instance for image fields. + + +Let's see an example: + +.. code-block:: python + + from sqlalchemy import Column, Integer + from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession + from sqlalchemy.ext.asyncio.session import async_sessionmaker + from sqlalchemy.orm import declarative_base + + from async_storages import S3Storage + from async_storages.integrations.sqlalchemy import FileType, ImageType + + Base = declarative_base() + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) + + storage = S3Storage( + bucket_name="my-bucket", + endpoint_url="localhost:9000", + aws_access_key_id="fake-access-key", + aws_secret_access_key="fake-secret-key", + use_ssl=False, + ) + + class Document(Base): + __tablename__ = "documents" + + id = Column(Integer, primary_key=True) + file = Column(FileType(storage=storage)) + image = Column(ImageType(storage=storage)) + + async def main(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + # create an in-memory image + img_buf = BytesIO() + Image.new("RGB", (32, 16), color=(255, 0, 0)).save(img_buf, format="PNG") + img_buf.seek(0) + + # upload and link file and image + img_name await storage.upload(img_buf, "uploads/test-image.png") + file_name = await storage.upload(BytesIO(b"hello world"), "uploads/test.txt") + + async with async_session() as session: + doc = Document(file=file_name, image=img_name) + session.add(doc) + await session.commit() + + doc = await session.get(Document, doc.id) + url = await doc.file.get_url() + print(url) + width, height = await doc.image.get_dimensions() + print(f"Dimensions: {width}x{height}") diff --git a/pyproject.toml b/pyproject.toml index b78fb40..9c82f4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,10 @@ dev = [ "pytest-asyncio>=1.2.0", "sqlalchemy>=2.0.44", ] +docs = [ + "furo>=2025.9.25", + "sphinx>=8.2.3", +] [tool.uv.build-backend] module-root = "src" diff --git a/src/async_storages/base.py b/src/async_storages/base.py index c05d259..959bdff 100644 --- a/src/async_storages/base.py +++ b/src/async_storages/base.py @@ -7,85 +7,28 @@ class BaseStorage(ABC): - """ - Abstract base class defining the interface for storage backends. - - Subclasses must implement all methods to provide storage operations - such as file upload, deletion, and retrieval of metadata or URLs. - """ - @abstractmethod def get_name(self, name: str) -> str: - """ - Get the name of the file. - - :param name: Original file name or identifier. - :type name: str - :return: The name of the file in storage. - :rtype: str - """ pass @abstractmethod async def get_size(self, name: str) -> int: - """ - Get the size of the file in bytes. - - :param name: Original file name or identifier. - :type name: str - :return: The file size in bytes. - :rtype: int - """ pass @abstractmethod async def get_url(self, name: str) -> str: - """ - Get a URL or path to access the file. - - :param name: Original file name or identifier. - :type name: str - :return: A URL or file path string. - :rtype: str - """ pass @abstractmethod async def open(self, name: str) -> BytesIO: - """ - Open an object and return it as an in-memory binary stream. - - :param name: Original file name or identifier. - :type name: str - :return: A BytesIO object containing the file's contents. - :rtype: BytesIO - """ pass @abstractmethod async def upload(self, file: BinaryIO, name: str) -> str: - """ - Upload a file to the storage backend. - - :param file: A binary file-like object to upload. - :type file: BinaryIO - :param name: Original file name or identifier. - :type name: str - :return: The name or path of the uploaded file. - :rtype: str - """ pass @abstractmethod async def delete(self, name: str) -> None: - """ - Delete the file from the storage backend. - - :param name: Original file name or identifier. - :type name: str - :return: None - :rtype: None - """ pass diff --git a/src/async_storages/integrations/sqlalchemy.py b/src/async_storages/integrations/sqlalchemy.py index a0a36a1..8ecd6be 100644 --- a/src/async_storages/integrations/sqlalchemy.py +++ b/src/async_storages/integrations/sqlalchemy.py @@ -29,20 +29,6 @@ def __init__(self, storage: BaseStorage, *args: Any, **kwargs: Any): @override def process_bind_param(self, value: Any, dialect: Dialect) -> str: - """ - Process the Python value before storing it in the database. - - Converts :class:`~async_storages.StorageFile` objects - (or similar objects with a ``name`` attribute) into their - string name representation for persistence. - - :param value: The Python value being bound to a database column. - :type value: Any - :param dialect: The SQLAlchemy database dialect in use. - :type dialect: Dialect - :return: The serialized string representation of the file name. - :rtype: str - """ if value is None: return value if isinstance(value, str): @@ -57,20 +43,6 @@ def process_bind_param(self, value: Any, dialect: Dialect) -> str: def process_result_value( self, value: Any | None, dialect: Dialect ) -> StorageFile | None: - """ - Process the database value after fetching from the database. - - Converts a stored file name string into a - :class:`~async_storages.StorageFile` instance - associated with the configured storage backend. - - :param value: The raw value retrieved from the database. - :type value: Any or None - :param dialect: The SQLAlchemy database dialect in use. - :type dialect: Dialect - :return: A :class:`~async_storages.StorageFile` instance, or ``None``. - :rtype: StorageFile or None - """ if value is None: return None return StorageFile(name=value, storage=self.storage) @@ -98,20 +70,6 @@ class ImageType(FileType): def process_result_value( self, value: Any | None, dialect: Dialect ) -> StorageFile | None: - """ - Process the database value after fetching from the database. - - Converts a stored image file name string into a - :class:`~async_storages.StorageImage` instance associated with - the configured storage backend. - - :param value: The raw value retrieved from the database. - :type value: Any or None - :param dialect: The SQLAlchemy database dialect in use. - :type dialect: Dialect - :return: A :class:`~async_storages.StorageImage` instance, or ``None``. - :rtype: StorageImage or None - """ if value is None: return None return StorageImage(name=value, storage=self.storage) diff --git a/uv.lock b/uv.lock index f4e8061..8b97c76 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,18 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + [[package]] name = "aioboto3" version = "15.4.0" @@ -175,6 +187,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, ] +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -230,6 +251,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/69/b417833a8926fa5491e5346d7c233bf7d8a9b12ba1f4ef41ccea2494000c/aws_xray_sdk-2.14.0-py2.py3-none-any.whl", hash = "sha256:cfbe6feea3d26613a2a869d14c9246a844285c97087ad8f296f901633554ad94", size = 101922, upload-time = "2024-06-04T22:12:25.729Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -511,6 +554,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + [[package]] name = "fastapi-async-storages" version = "0.1.0" @@ -530,6 +582,10 @@ dev = [ { name = "pytest-asyncio" }, { name = "sqlalchemy" }, ] +docs = [ + { name = "furo" }, + { name = "sphinx" }, +] [package.metadata] requires-dist = [{ name = "aioboto3", marker = "extra == 's3'", specifier = ">=15.4.0" }] @@ -544,6 +600,10 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "sqlalchemy", specifier = ">=2.0.44" }, ] +docs = [ + { name = "furo", specifier = ">=2025.9.25" }, + { name = "sphinx", specifier = ">=8.2.3" }, +] [[package]] name = "flask" @@ -664,6 +724,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "furo" +version = "2025.9.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/29/ff3b83a1ffce74676043ab3e7540d398e0b1ce7660917a00d7c4958b93da/furo-2025.9.25.tar.gz", hash = "sha256:3eac05582768fdbbc2bdfa1cdbcdd5d33cfc8b4bd2051729ff4e026a1d7e0a98", size = 1662007, upload-time = "2025-09-25T21:37:19.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/69/964b55f389c289e16ba2a5dfe587c3c462aac09e24123f09ddf703889584/furo-2025.9.25-py3-none-any.whl", hash = "sha256:2937f68e823b8e37b410c972c371bc2b1d88026709534927158e0cb3fac95afe", size = 340409, upload-time = "2025-09-25T21:37:17.244Z" }, +] + [[package]] name = "graphql-core" version = "3.2.6" @@ -715,6 +791,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -1682,6 +1767,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, ] +[[package]] +name = "roman-numerals-py" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, +] + [[package]] name = "rpds-py" version = "0.28.0" @@ -1793,6 +1887,118 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals-py" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.44"