diff --git a/README.md b/README.md index 783e409..1278782 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,128 @@ def upload_file(self, file: UploadFile, storage_service: Inject[StorageService]) See [Sample Project](https://github.com/python-ellar/ellar-storage/tree/master/samples) +## Some Quick Cloud Setup + +### Google Cloud Storage + +- For a service Account + + ```python + from ellar.common import Module + from ellar.core import ModuleBase + from ellar_storage import StorageModule, get_driver, Provider + + @Module(modules=[ + StorageModule.setup( + files={ + # For a service Account + "driver": get_driver(Provider.GOOGLE_STORAGE), + "options": { + "key": "client_email", + "secret": "private_key", + "...": "..." + }, + }, + ) + ]) + class ApplicationModule(ModuleBase): + pass + ``` +- Installed Application + ```python + from ellar.common import Module + from ellar.core import ModuleBase + from ellar_storage import StorageModule, get_driver, Provider + + @Module(modules=[ + StorageModule.setup( + files={ + # For a service Account + "driver": get_driver(Provider.GOOGLE_STORAGE), + "options": { + "key": "client_id", + "secret": "client_secret", + "...": "..." + }, + }, + ) + ]) + class ApplicationModule(ModuleBase): + pass + ``` + +- GCE instance + ```python + from ellar.common import Module + from ellar.core import ModuleBase + from ellar_storage import StorageModule, get_driver, Provider + + @Module(modules=[ + StorageModule.setup( + files={ + # For a service Account + "driver": get_driver(Provider.GOOGLE_STORAGE), + "options": { + "key": "GOOG0123456789ABCXYZ", + "secret": "key_secret", + "...": "..." + }, + }, + ) + ]) + class ApplicationModule(ModuleBase): + pass + ``` +See [GCS](https://libcloud.readthedocs.io/en/stable/storage/drivers/google_storage.html?highlight=Google%20Cloud%20Storage#api-docs) + +### AWS S3 + +```python +from ellar.common import Module +from ellar.core import ModuleBase +from ellar_storage import StorageModule, get_driver, Provider + +@Module(modules=[ + StorageModule.setup( + files={ + "driver": get_driver(Provider.S3), + "options": { + "key": "api key", + "secret": "api secret key" + }, + }, + ) +]) +class ApplicationModule(ModuleBase): + pass +``` + +#### Specifying canned ACL when uploading an object +If you want to specify custom ACL when uploading an object, +you can do so by passing `extra` argument with the acl attribute to the `save` or `save_content` methods. + +Valid values for this attribute are: + +- private (default) +- public-read +- public-read-write +- authenticated-read +- bucket-owner-read +- bucket-owner-full-control + +For example +```python +from ellar.common import UploadFile, Inject, post +from ellar_storage import StorageService + +@post('/upload') +def upload_file(self, file: UploadFile, storage_service: Inject[StorageService]): + extra = {"content_type": "application/octet-stream", "acl": "public-read"} + stored_file = storage_service.save(file=file, extra=extra) + + return {"message": f"{stored_file.filename} saved"} +``` + ## API Reference ### StorageService diff --git a/ellar_storage/__init__.py b/ellar_storage/__init__.py index ed71ec7..8eb1caf 100644 --- a/ellar_storage/__init__.py +++ b/ellar_storage/__init__.py @@ -1,6 +1,6 @@ """Storage Module for Ellar""" -__version__ = "0.1.1" +__version__ = "0.1.2" from .module import StorageModule from .providers import Provider, get_driver diff --git a/ellar_storage/module.py b/ellar_storage/module.py index becae70..185d591 100644 --- a/ellar_storage/module.py +++ b/ellar_storage/module.py @@ -16,7 +16,7 @@ class _ContainerOptions(t.TypedDict): class _StorageSetupKey(t.TypedDict): driver: t.Type[StorageDriver] - options: _ContainerOptions + options: t.Union[_ContainerOptions, t.Dict[str, t.Any]] @Module() diff --git a/ellar_storage/schemas.py b/ellar_storage/schemas.py index 0168b71..be1a789 100644 --- a/ellar_storage/schemas.py +++ b/ellar_storage/schemas.py @@ -1,6 +1,6 @@ import typing as t -from ellar.pydantic import model_validator +from ellar.pydantic import field_validator, model_validator from pydantic import BaseModel from ellar_storage.storage import StorageDriver @@ -10,6 +10,13 @@ class _StorageSetupItem(BaseModel): driver: t.Type[StorageDriver] options: t.Dict[str, t.Any] = {} + @field_validator("options", mode="before") + def pre_options_validate(cls, value: t.Dict) -> t.Any: + if "key" not in value: + raise ValueError("Driver Options must have a `key` option ") + + return value + class StorageSetup(BaseModel): # default storage name that must exist in `storages` diff --git a/ellar_storage/services.py b/ellar_storage/services.py index deaa9b7..fa650eb 100644 --- a/ellar_storage/services.py +++ b/ellar_storage/services.py @@ -62,7 +62,15 @@ def save( self, file: UploadFile, upload_storage: t.Optional[str] = None, + extra: t.Optional[t.Dict[str, t.Any]] = None, ) -> StoredFile: + extra_ = extra or {} + extra_.setdefault( + "meta_data", + {"content_type": file.content_type, "filename": file.filename}, + ) + extra_.setdefault("content_type", file.content_type) + return self.save_content( name=file.filename or str(uuid.uuid4())[10], content=file.file, diff --git a/tests/test_schema.py b/tests/test_schema.py index a736c52..9c856bb 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -68,3 +68,15 @@ def test_storage_fails_for_invalid_default_storage(): }, default="files", ) + + +def test_storage_fails_for_invalid_driver_option(): + with pytest.raises(ValueError, match="Driver Options must have a `key` option"): + StorageSetup( + storages={ + "cloud": { + "driver": get_driver(Provider.GOOGLE_STORAGE), + "options": {"secret": "key_secret"}, + } + }, + )