From 01fcbe08729b49c211f5fef710d2839e5b139aaf Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Fri, 5 Apr 2024 06:32:50 +0100 Subject: [PATCH 1/5] fixed failing test --- tests/conftest.py | 9 ++++++++ tests/test_service.py | 44 ++++++++++++++++++------------------- tests/test_service_async.py | 43 +++++++++++++++++++----------------- tests/utils.py | 21 +++++------------- 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..9e46059 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from .utils import clear + + +@pytest.fixture +def clear_dir(): + yield + clear("fixtures") diff --git a/tests/test_service.py b/tests/test_service.py index 73cef59..aa6a866 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -15,26 +15,28 @@ get_driver, ) -from .utils import DUMB_DIRS, TEST_FIXTURES_DIRS, clean_directory +from .utils import DUMB_DIRS, TEST_FIXTURES_DIRS module_config = { - "modules": [ - StorageModule.setup( - files={ - "driver": get_driver(Provider.LOCAL), - "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, - }, - images={ - "driver": get_driver(Provider.LOCAL), - "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, - }, - ) - ] + "modules": [StorageModule.register_setup()], + "config_module": { + "STORAGE_CONFIG": { + "storages": { + "files": { + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, + }, + "images": { + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, + }, + } + } + }, } -@clean_directory("fixtures") -def test_storage_get_operation(): +def test_storage_get_operation(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) @@ -56,8 +58,7 @@ def test_storage_get_operation(): assert from_images.read() == b"File saving worked in images" -@clean_directory("fixtures") -def test_storage_delete_operation(): +def test_storage_delete_operation(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) @@ -71,8 +72,7 @@ def test_storage_delete_operation(): assert storage_service.delete("images/get.txt") -@clean_directory("fixtures") -def test_storage_save_content_operation(): +def test_storage_save_content_operation(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) @@ -101,8 +101,7 @@ def test_storage_save_content_operation(): assert files == ["copied-test.txt", "get.txt.metadata.json", "get.txt"] -@clean_directory("fixtures") -def test_storage_get_container(): +def test_storage_get_container(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) @@ -123,8 +122,7 @@ def test_storage_get_container(): storage_service.get_container("images-invalid") -@clean_directory("fixtures") -def test_storage_stored_file(): +def test_storage_stored_file(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) diff --git a/tests/test_service_async.py b/tests/test_service_async.py index 133a499..1142b4b 100644 --- a/tests/test_service_async.py +++ b/tests/test_service_async.py @@ -1,5 +1,6 @@ import os.path +import pytest from ellar.common.datastructures import ContentFile from ellar.testing import Test @@ -11,26 +12,28 @@ get_driver, ) -from .utils import DUMB_DIRS, TEST_FIXTURES_DIRS, clean_directory +from .utils import DUMB_DIRS, TEST_FIXTURES_DIRS module_config = { - "modules": [ - StorageModule.setup( - files={ - "driver": get_driver(Provider.LOCAL), - "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, - }, - images={ - "driver": get_driver(Provider.LOCAL), - "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, - }, - ) - ] + "modules": [StorageModule.register_setup()], + "config_module": { + "STORAGE_CONFIG": { + "storages": { + "files": { + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, + }, + "images": { + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(DUMB_DIRS, "fixtures")}, + }, + } + } + }, } - -@clean_directory("fixtures") -async def test_storage_get_operation_async(anyio_backend): +@pytest.mark.asyncio +async def test_storage_get_operation_async(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) @@ -52,8 +55,8 @@ async def test_storage_get_operation_async(anyio_backend): assert from_images.read() == b"File saving worked in images" -@clean_directory("fixtures") -async def test_storage_delete_operation_async(anyio_backend): +@pytest.mark.asyncio +async def test_storage_delete_operation_async(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) @@ -67,8 +70,8 @@ async def test_storage_delete_operation_async(anyio_backend): assert await storage_service.delete_async("images/get.txt") -@clean_directory("fixtures") -async def test_storage_save_content_operation_async(anyio_backend): +@pytest.mark.asyncio +async def test_storage_save_content_operation_async(clear_dir): tm = Test.create_test_module(**module_config) storage_service: StorageService = tm.get(StorageService) diff --git a/tests/utils.py b/tests/utils.py index 08eb7c5..78adbc5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,3 @@ -import functools import os.path import shutil @@ -8,18 +7,8 @@ TEST_FIXTURES_DIRS = get_main_directory_by_stack("__main__/fixtures/", stack_level=1) -def clean_directory(directory): - def _decorator(f): - @functools.wraps(f) - def _wrapper(*args, **kwargs): - try: - f(*args, **kwargs) - finally: - try: - shutil.rmtree(os.path.join(DUMB_DIRS, directory)) - except OSError: - pass - - return _wrapper - - return _decorator +def clear(directory): + try: + shutil.rmtree(os.path.join(DUMB_DIRS, directory)) + except OSError: + pass From 3790bc994505421d7c398df8ae55a394c20774af Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 7 Apr 2024 15:52:01 +0100 Subject: [PATCH 2/5] Added some tests and sample project --- README.md | 253 +++++++++++++++++- ellar_storage/module.py | 4 +- pyproject.toml | 2 +- samples/README.md | 18 ++ samples/ellar_storage_tut/__init__.py | 0 samples/ellar_storage_tut/config.py | 93 +++++++ samples/ellar_storage_tut/controller.py | 53 ++++ samples/ellar_storage_tut/core/__init__.py | 0 samples/ellar_storage_tut/domain/__init__.py | 0 .../media/documents/docs.txt | 1 + .../media/documents/docs.txt.metadata.json | 1 + .../ellar_storage_tut/media/files/file.txt | 1 + .../media/files/file.txt.metadata.json | 1 + .../ellar_storage_tut/media/images/image.txt | 1 + .../media/images/image.txt.metadata.json | 1 + samples/ellar_storage_tut/root_module.py | 20 ++ samples/ellar_storage_tut/server.py | 59 ++++ samples/manage.py | 12 + samples/requirements.txt | 2 + samples/tests/conftest.py | 0 20 files changed, 518 insertions(+), 4 deletions(-) create mode 100644 samples/README.md create mode 100644 samples/ellar_storage_tut/__init__.py create mode 100644 samples/ellar_storage_tut/config.py create mode 100644 samples/ellar_storage_tut/controller.py create mode 100644 samples/ellar_storage_tut/core/__init__.py create mode 100644 samples/ellar_storage_tut/domain/__init__.py create mode 100644 samples/ellar_storage_tut/media/documents/docs.txt create mode 100644 samples/ellar_storage_tut/media/documents/docs.txt.metadata.json create mode 100644 samples/ellar_storage_tut/media/files/file.txt create mode 100644 samples/ellar_storage_tut/media/files/file.txt.metadata.json create mode 100644 samples/ellar_storage_tut/media/images/image.txt create mode 100644 samples/ellar_storage_tut/media/images/image.txt.metadata.json create mode 100644 samples/ellar_storage_tut/root_module.py create mode 100644 samples/ellar_storage_tut/server.py create mode 100644 samples/manage.py create mode 100644 samples/requirements.txt create mode 100644 samples/tests/conftest.py diff --git a/README.md b/README.md index a272cb0..6c47478 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,259 @@ [![PyPI version](https://img.shields.io/pypi/v/ellar-storage.svg)](https://pypi.python.org/pypi/ellar-storage) [![PyPI version](https://img.shields.io/pypi/pyversions/ellar-storage.svg)](https://pypi.python.org/pypi/ellar-storage) +## Introduction +EllarStorage Module adds support for cloud and local file storage +management using [apache `libcloud`](https://github.com/apache/libcloud) package to your Ellar application. +## Installation +```shell +$(venv) pip install ellar-storage +``` -## License +This library was inspired by [sqlalchemy-file](https://github.com/jowilf/sqlalchemy-file) + + +## **Usage** +Follow Ellar project scaffold here, then you configure your module. + +### StorageModule +Just like every other ellar `Module`s, `StorageModule` +can be configured directly in where its used or through application config. + +### **StorageModule.setup** +Quick example using `StorageModule.setup` method. + +Pattern of configuring Storages are in key-word patterns +where the `key` is `Folder name/Container` and value is `StorageDriver` init properties. +Example is shown below: + +```python +import os +from pathlib import Path +from ellar.common import Module +from ellar.core import ModuleBase +from ellar_storage import StorageModule, get_driver, Provider + +BASE_DIRS = Path(__file__).parent + +@Module(modules=[ + StorageModule.setup( + files={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + }, + images={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + }, + documents={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + }, + default="files" + ) +]) +class ApplicationModule(ModuleBase): + pass +``` + +In the above illustration, When application initialization is complete, +`files`, `images` and `documents` will be created in `os.path.join(BASE_DIRS, "media")`. +Each is configured to be managed by Local Storage Driver. +See other supported [storage drivers](https://libcloud.readthedocs.io/en/stable/storage/supported_providers.html#provider-matrix) + +Each storage required `key` and some other parameters for object instantiation, +so those should be provided in the `options` as a key-value pair. + +Also, `default` parameter defines container/folder of choice when saving/retrieving +a file if storage container was specified. +It is important to note that if `default` is not set, +it will default to the first storage container which in this can is `files`. + + +### **StorageModule.register_setup** +Alternatively, we can move the storage configuration to application Config and everything will still work fine. +For example: +```python +## project_name/root_module.py + +from ellar.common import Module +from ellar.core import ModuleBase +from ellar_storage import StorageModule + +@Module(modules=[StorageModule.register_setup()]) +class ApplicationModule(ModuleBase): + pass +``` + +Then in `config.py` add the following code: + +```python +import os +from pathlib import Path +from ellar.core.conf import ConfigDefaultTypesMixin +from ellar_storage import get_driver, Provider + +BASE_DIRS = Path(__file__).parent + + +class DevelopmentConfig(ConfigDefaultTypesMixin): + DEBUG = True + + STORAGE_CONFIG = dict( + storages=dict( + files={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + }, + images={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + }, + documents={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + } + ), + default="files" + ) +``` + +### StorageService + +At the end of `StorageModule` setup, `StorageService` is registered into an Ellar DI system. +A quick way to test this would be through application instance. +For example: +```python +## project_name/server.py + +import os +from ellar.app import AppFactory +from ellar.common import datastructures, constants +from ellar.core import LazyModuleImport as lazyLoad +from ellar_storage import StorageService + + +application = AppFactory.create_from_app_module( + lazyLoad("project_name.root_module:ApplicationModule"), + config_module=os.environ.get( + constants.ELLAR_CONFIG_MODULE, "carapp.config:DevelopmentConfig" + ), +) + +storage_service: StorageService = application.injector.get(StorageService) +# save a file in files folder +storage_service.save( + file=datastructures.ContentFile(b"We can now save files in files folder", name="file.txt"), upload_storage='files') +# save a file in images folder +storage_service.save( + file=datastructures.ContentFile(b"We can now save files in images folder", name="image.txt"), upload_storage='images') +# save a file in document folder +storage_service.save( + file=datastructures.ContentFile(b"We can now save files in documents folder", name="docs.txt"), upload_storage='documents') +``` +### StorageService in Route functions +You can inject `StorageService` into your controller or route functions. For example: + +In Controller: +```python +from ellar.common import ControllerBase, Controller +from ellar_storage import StorageService + +@Controller() +class FileManagerController(ControllerBase): + def __init__(self, storage_service: StorageService): + self._storage_service = storage_service +``` + +In Route Function: +```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]): + pass +``` + +Here is a quick example of a controller to manage files. This is just to illustrate how to use `StorageService`. + +```python +from ellar.common import ( + Controller, + ControllerBase, + File, + Form, + Inject, + Query, + UploadFile, + delete, + file, + get, + post, +) + +from ellar_storage import StorageService + + +@Controller('/upload') +class FileManagerController(ControllerBase): + def __init__(self, storage_service: StorageService): + self._storage_service = storage_service + + @post("/", response=str) + def upload_file( + self, + upload: File[UploadFile], + storage_service: Inject[StorageService], + upload_storage: Form[str] + ): + assert self._storage_service == storage_service + res = storage_service.save(file=upload, upload_storage=upload_storage) + return res.filename + + @get("/") + @file(media_type="application/octet-stream", streaming=True) + def download_file(self, path: Query[str]): + res = self._storage_service.get(path) + return {"media_type": res.content_type, "content": res.as_stream()} + + @get("/download_as_attachment") + @file(media_type="application/octet-stream") + def download_as_attachment(self, path: Query[str]): + res = self._storage_service.get(path) + return { + "path": res.get_cdn_url(), # since we are using a local storage, this will return a path to the file + "filename": res.filename, + 'media_type': res.content_type + } + + @delete("/", response=dict) + def delete_file(self, path: Query[str]): + self._storage_service.delete(path) + return "" +``` + +See [Sample Project]() + + +### StoredFile +`StoredFile` is file-like object returned from saving and retrieving saved files. +Its also extends some `libcloud` Object methods +and has reference to the `libcloud` Object retrieved from the `libcloud` storage container. + +Some important attributes: + +- **name**: File name +- **size**: File Size +- **filename**: File name +- **content_type**: File Content Type +- **object**: `libcloud` Object reference +- **read(self, n: int = -1, chunk_size: t.Optional[int] = None) -> bytes**: Reads file content +- **get_cdn_url(self) -> t.Optional[str]**: gets file cdn url +- **as_stream(self, chunk_size: t.Optional[int] = None) -> t.Iterator[bytes]**: create a file stream +- **delete(self) -> bool**: deletes file from container + +## License Ellar is [MIT licensed](LICENSE). diff --git a/ellar_storage/module.py b/ellar_storage/module.py index d5aa46b..de0923d 100644 --- a/ellar_storage/module.py +++ b/ellar_storage/module.py @@ -22,8 +22,8 @@ class _StorageSetupKey(t.TypedDict): @Module() class StorageModule(ModuleBase, IModuleSetup): @classmethod - def setup(cls, **kwargs: _StorageSetupKey) -> DynamicModule: - schema = StorageSetup(storages=kwargs) # type:ignore[arg-type] + def setup(cls, default: t.Optional[str]=None, **kwargs: _StorageSetupKey) -> DynamicModule: + schema = StorageSetup(storages=kwargs, default=default) # type:ignore[arg-type] return DynamicModule( cls, providers=[ diff --git a/pyproject.toml b/pyproject.toml index ed96ed2..1bcfa87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ classifiers = [ ] dependencies = [ - "ellar >= 0.7.2", + "ellar >= 0.7.3", "apache-libcloud >=3.6, <3.9", ] diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..2ff08bb --- /dev/null +++ b/samples/README.md @@ -0,0 +1,18 @@ +# ellar_storage_tut +A Quick illustration of how to use ellar-storage for any local/cloud file storage. + +## Requirements +Python >= 3.8 +ellar + +## Project setup +``` +pip install -r requirements.txt +``` + +### Development Server +``` +python manage.py runserver --reload +``` + +Visit [http://localhost:8000/docs](http://localhost:8000/docs) \ No newline at end of file diff --git a/samples/ellar_storage_tut/__init__.py b/samples/ellar_storage_tut/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/ellar_storage_tut/config.py b/samples/ellar_storage_tut/config.py new file mode 100644 index 0000000..8902a9d --- /dev/null +++ b/samples/ellar_storage_tut/config.py @@ -0,0 +1,93 @@ +""" +Application Configurations +Default Ellar Configurations are exposed here through `ConfigDefaultTypesMixin` +Make changes and define your own configurations specific to your application + +export ELLAR_CONFIG_MODULE=ellar_storage_tut.config:DevelopmentConfig +""" +import os +import typing as t +from pathlib import Path + +from ellar.common import IExceptionHandler, JSONResponse +from ellar.core import ConfigDefaultTypesMixin +from ellar.core.versioning import BaseAPIVersioning, DefaultAPIVersioning +from ellar.pydantic import ENCODERS_BY_TYPE as encoders_by_type +from starlette.middleware import Middleware + +from ellar_storage import Provider, get_driver + +BASE_DIRS = Path(__file__).parent + + +class BaseConfig(ConfigDefaultTypesMixin): + DEBUG: bool = False + + DEFAULT_JSON_CLASS: t.Type[JSONResponse] = JSONResponse + SECRET_KEY: str = "ellar_ugl4IS-ZjJxJ0dH_hfJb8ZCwK31595uS4aaBqXYO4Sg" + + # injector auto_bind = True allows you to resolve types that are not registered on the container + # For more info, read: https://injector.readthedocs.io/en/latest/index.html + INJECTOR_AUTO_BIND = False + + # jinja Environment options + # https://jinja.palletsprojects.com/en/3.0.x/api/#high-level-api + JINJA_TEMPLATES_OPTIONS: t.Dict[str, t.Any] = {} + + # Application route versioning scheme + VERSIONING_SCHEME: BaseAPIVersioning = DefaultAPIVersioning() + + # Enable or Disable Application Router route searching by appending backslash + REDIRECT_SLASHES: bool = False + + # Define references to static folders in python packages. + # eg STATIC_FOLDER_PACKAGES = [('boostrap4', 'statics')] + STATIC_FOLDER_PACKAGES: t.Optional[t.List[t.Union[str, t.Tuple[str, str]]]] = [] + + # Define references to static folders defined within the project + STATIC_DIRECTORIES: t.Optional[t.List[t.Union[str, t.Any]]] = [] + + # static route path + STATIC_MOUNT_PATH: str = "/static" + + CORS_ALLOW_ORIGINS: t.List[str] = ["*"] + CORS_ALLOW_METHODS: t.List[str] = ["*"] + CORS_ALLOW_HEADERS: t.List[str] = ["*"] + ALLOWED_HOSTS: t.List[str] = ["*"] + + # Application middlewares + MIDDLEWARE: t.Sequence[Middleware] = [] + + # A dictionary mapping either integer status codes, + # or exception class types onto callables which handle the exceptions. + # Exception handler callables should be of the form + # `handler(context:IExecutionContext, exc: Exception) -> response` + # and may be either standard functions, or async functions. + EXCEPTION_HANDLERS: t.List[IExceptionHandler] = [] + + # Object Serializer custom encoders + SERIALIZER_CUSTOM_ENCODER: t.Dict[ + t.Any, t.Callable[[t.Any], t.Any] + ] = encoders_by_type + + STORAGE_CONFIG = dict( + storages=dict( + files={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + }, + images={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + }, + documents={ + "driver": get_driver(Provider.LOCAL), + "options": {"key": os.path.join(BASE_DIRS, "media")}, + } + ), + default="files" + ) + + +class DevelopmentConfig(BaseConfig): + DEBUG: bool = True diff --git a/samples/ellar_storage_tut/controller.py b/samples/ellar_storage_tut/controller.py new file mode 100644 index 0000000..84f441b --- /dev/null +++ b/samples/ellar_storage_tut/controller.py @@ -0,0 +1,53 @@ +from ellar.common import ( + Controller, + ControllerBase, + File, + Form, + Inject, + Query, + UploadFile, + delete, + file, + get, + post, +) + +from ellar_storage import StorageService + + +@Controller('/upload') +class FileManagerController(ControllerBase): + def __init__(self, storage_service: StorageService): + self._storage_service = storage_service + + @post("/", response=str) + def upload_file( + self, + upload: File[UploadFile], + storage_service: Inject[StorageService], + upload_storage: Form[str] + ): + assert self._storage_service == storage_service + res = storage_service.save(file=upload, upload_storage=upload_storage) + return res.filename + + @get("/") + @file(media_type="application/octet-stream", streaming=True) + def download_file(self, path: Query[str]): + res = self._storage_service.get(path) + return {"media_type": res.content_type, "content": res.as_stream()} + + @get("/download_as_attachment") + @file(media_type="application/octet-stream") + def download_as_attachment(self, path: Query[str]): + res = self._storage_service.get(path) + return { + "path": res.get_cdn_url(), # since we are using a local storage, this will return a path to the file + "filename": res.filename, + 'media_type': res.content_type + } + + @delete("/", response=dict) + def delete_file(self, path: Query[str]): + self._storage_service.delete(path) + return "" diff --git a/samples/ellar_storage_tut/core/__init__.py b/samples/ellar_storage_tut/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/ellar_storage_tut/domain/__init__.py b/samples/ellar_storage_tut/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/ellar_storage_tut/media/documents/docs.txt b/samples/ellar_storage_tut/media/documents/docs.txt new file mode 100644 index 0000000..0e8b28f --- /dev/null +++ b/samples/ellar_storage_tut/media/documents/docs.txt @@ -0,0 +1 @@ +We can now save files in documents folder \ No newline at end of file diff --git a/samples/ellar_storage_tut/media/documents/docs.txt.metadata.json b/samples/ellar_storage_tut/media/documents/docs.txt.metadata.json new file mode 100644 index 0000000..db0dabe --- /dev/null +++ b/samples/ellar_storage_tut/media/documents/docs.txt.metadata.json @@ -0,0 +1 @@ +{"content_type": "text/plain", "filename": "docs.txt"} \ No newline at end of file diff --git a/samples/ellar_storage_tut/media/files/file.txt b/samples/ellar_storage_tut/media/files/file.txt new file mode 100644 index 0000000..d0e7fdd --- /dev/null +++ b/samples/ellar_storage_tut/media/files/file.txt @@ -0,0 +1 @@ +We can now save files in files folder \ No newline at end of file diff --git a/samples/ellar_storage_tut/media/files/file.txt.metadata.json b/samples/ellar_storage_tut/media/files/file.txt.metadata.json new file mode 100644 index 0000000..c0e8f5d --- /dev/null +++ b/samples/ellar_storage_tut/media/files/file.txt.metadata.json @@ -0,0 +1 @@ +{"content_type": "text/plain", "filename": "file.txt"} \ No newline at end of file diff --git a/samples/ellar_storage_tut/media/images/image.txt b/samples/ellar_storage_tut/media/images/image.txt new file mode 100644 index 0000000..d32b503 --- /dev/null +++ b/samples/ellar_storage_tut/media/images/image.txt @@ -0,0 +1 @@ +We can now save files in images folder \ No newline at end of file diff --git a/samples/ellar_storage_tut/media/images/image.txt.metadata.json b/samples/ellar_storage_tut/media/images/image.txt.metadata.json new file mode 100644 index 0000000..e878adf --- /dev/null +++ b/samples/ellar_storage_tut/media/images/image.txt.metadata.json @@ -0,0 +1 @@ +{"content_type": "text/plain", "filename": "image.txt"} \ No newline at end of file diff --git a/samples/ellar_storage_tut/root_module.py b/samples/ellar_storage_tut/root_module.py new file mode 100644 index 0000000..bcf8a01 --- /dev/null +++ b/samples/ellar_storage_tut/root_module.py @@ -0,0 +1,20 @@ +from ellar.common import ( + IExecutionContext, + JSONResponse, + Module, + Response, + exception_handler, +) +from ellar.core import ModuleBase +from ellar.samples.modules import HomeModule + +from ellar_storage import StorageModule + +from .controller import FileManagerController + + +@Module(modules=[HomeModule, StorageModule.register_setup()], controllers=[FileManagerController]) +class ApplicationModule(ModuleBase): + @exception_handler(404) + def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response: + return JSONResponse({"detail": "Resource not found."}, status_code=404) diff --git a/samples/ellar_storage_tut/server.py b/samples/ellar_storage_tut/server.py new file mode 100644 index 0000000..7f19eeb --- /dev/null +++ b/samples/ellar_storage_tut/server.py @@ -0,0 +1,59 @@ +import os + +from ellar.app import App, AppFactory +from ellar.common import datastructures +from ellar.common.constants import ELLAR_CONFIG_MODULE +from ellar.core import LazyModuleImport as lazyLoad +from ellar.openapi import OpenAPIDocumentBuilder, OpenAPIDocumentModule, SwaggerUI + +from ellar_storage import StorageService + + +def seed_files(application): + # Seed some files into the containers configured. + + storage_service: StorageService = application.injector.get(StorageService) + + # save a file in files folder + storage_service.save( + file=datastructures.ContentFile(b"We can now save files in files folder", name="file.txt"), + upload_storage='files') + # save a file in images folder + storage_service.save( + file=datastructures.ContentFile(b"We can now save files in images folder", name="image.txt"), + upload_storage='images') + # save a file in document folder + storage_service.save( + file=datastructures.ContentFile(b"We can now save files in documents folder", name="docs.txt"), + upload_storage='documents') + + +def bootstrap() -> App: + application = AppFactory.create_from_app_module( + lazyLoad("ellar_storage_tut.root_module:ApplicationModule"), + config_module=os.environ.get( + ELLAR_CONFIG_MODULE, "ellar_storage_tut.config:DevelopmentConfig" + ), + global_guards=[] + ) + + # uncomment this section if you want API documentation + + document_builder = OpenAPIDocumentBuilder() + document_builder.set_title('Ellar_storage_tut Title') \ + .set_version('1.0.2') \ + .set_contact(name='Author Name', url='https://www.author-name.com', email='authorname@gmail.com') \ + .set_license('MIT Licence', url='https://www.google.com') \ + .add_server('/', description='Development Server') + + document = document_builder.build_document(application) + OpenAPIDocumentModule.setup( + app=application, + document=document, + docs_ui=SwaggerUI(), + guards=[] + ) + + seed_files(application) + + return application diff --git a/samples/manage.py b/samples/manage.py new file mode 100644 index 0000000..5995e8a --- /dev/null +++ b/samples/manage.py @@ -0,0 +1,12 @@ +import os + +from ellar.common.constants import ELLAR_CONFIG_MODULE +from ellar_cli.main import create_ellar_cli + +if __name__ == '__main__': + os.environ.setdefault(ELLAR_CONFIG_MODULE, "ellar_storage_tut.config:DevelopmentConfig") + + # initialize Commandline program + cli = create_ellar_cli('ellar_storage_tut.server:bootstrap') + # start commandline execution + cli(prog_name="Ellar Web Framework CLI") diff --git a/samples/requirements.txt b/samples/requirements.txt new file mode 100644 index 0000000..ab675b1 --- /dev/null +++ b/samples/requirements.txt @@ -0,0 +1,2 @@ +ellar-storage +ellar-cli \ No newline at end of file diff --git a/samples/tests/conftest.py b/samples/tests/conftest.py new file mode 100644 index 0000000..e69de29 From 5033f13ae4ecd019f1e3d97579e713d9a448998c Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 7 Apr 2024 15:55:40 +0100 Subject: [PATCH 3/5] fixed missing httpx package --- requirements-tests.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-tests.txt b/requirements-tests.txt index b1856be..382ec0b 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -7,3 +7,4 @@ types-python-dateutil types-pytz mypy == 1.9.0 anyio[trio] >= 3.2.1 +httpx From 031ff302fc15bfd1c04a6ce2913c5a9cbd6d9dbd Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 7 Apr 2024 18:11:30 +0100 Subject: [PATCH 4/5] fixed fasteners dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1bcfa87..fde6b42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ classifiers = [ dependencies = [ "ellar >= 0.7.3", "apache-libcloud >=3.6, <3.9", + "fasteners ==0.19" ] [project.urls] From 1597acd9bacaefc2a37f567e3fc195253fc3877b Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 7 Apr 2024 18:14:44 +0100 Subject: [PATCH 5/5] fixed failing tests --- tests/test_service.py | 2 +- tests/test_service_async.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_service.py b/tests/test_service.py index aa6a866..ca8c4ac 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -98,7 +98,7 @@ def test_storage_save_content_operation(clear_dir): ) files = os.listdir(os.path.join(DUMB_DIRS, "fixtures", "files")) - assert files == ["copied-test.txt", "get.txt.metadata.json", "get.txt"] + assert set(files) == {"copied-test.txt", "get.txt.metadata.json", "get.txt"} def test_storage_get_container(clear_dir): diff --git a/tests/test_service_async.py b/tests/test_service_async.py index 1142b4b..89c92d1 100644 --- a/tests/test_service_async.py +++ b/tests/test_service_async.py @@ -97,4 +97,4 @@ async def test_storage_save_content_operation_async(clear_dir): ) files = os.listdir(os.path.join(DUMB_DIRS, "fixtures", "files")) - assert files == ["copied-test.txt", "get.txt.metadata.json", "get.txt"] + assert set(files) == {"copied-test.txt", "get.txt.metadata.json", "get.txt"}