diff --git a/CHANGELOG.md b/CHANGELOG.md index 3efe452..6bdd09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Version 0.1.2 - 2022-08-11 +## [0.1.3] - 2022-08-23 + +--- + +### Added +- Add `thumbnail_size` property to ImageField by @jowilf https://github.com/jowilf/sqlalchemy-file/pull/9 + +## [0.1.2] - 2022-08-11 --- diff --git a/README.md b/README.md index 6ec8db3..948852a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **SQLAlchemy-file** is a [SQLAlchemy](https://www.sqlalchemy.org/) extension for attaching files to SQLAlchemy model and -uploading them to various storage such as Amazon S3, Rackspace CloudFiles, Google Storage and others +uploading them to various storage such as Local Storage, Amazon S3, Rackspace CloudFiles, Google Storage and others using [Apache Libcloud](https://github.com/apache/libcloud).

@@ -123,7 +123,9 @@ with Session(engine) as session: ## Related projects and inspirations -* [Depot: ](https://github.com/amol-/depot) When I was looking for a library like this, depot was the -best I saw. But it offers less storage backend, doesn't support multiple files and doesn't work with -[SQLModel](https://github.com/tiangolo/sqlmodel). This project inspired **SQLAlchemy-file** extensively +* [filedepot: ](https://github.com/amol-/depot) When I was looking for a library like this, depot was the +best I saw. This project inspired **SQLAlchemy-file** extensively and some features are implemented the same. +* [sqlalchemy-media](https://github.com/pylover/sqlalchemy-media) Another attachment extension for SqlAlchemy +to manage assets which are associated with database models + diff --git a/docs/changelog.md b/docs/changelog.md index 3efe452..5040b16 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Version 0.1.2 - 2022-08-11 +## [0.1.3] - 2022-08-23 + +--- + +### Added +- Add `thumbnail_size` property to ImageField in [#9](https://github.com/jowilf/sqlalchemy-file/pull/9) + +## [0.1.2] - 2022-08-11 --- diff --git a/docs/index.md b/docs/index.md index 0137126..55d6fa9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ # Overview **SQLAlchemy-file** is a [SQLAlchemy](https://www.sqlalchemy.org/) extension for attaching files to SQLAlchemy model and -uploading them to various storage such as Amazon S3, Rackspace CloudFiles, Google Storage and others +uploading them to various storage such as Local Storage, Amazon S3, Rackspace CloudFiles, Google Storage and others using [Apache Libcloud](https://github.com/apache/libcloud).

@@ -122,7 +122,8 @@ with Session(engine) as session: ## Related projects and inspirations -* [Depot: ](https://github.com/amol-/depot) When I was looking for a library like this, depot was the -best I saw. But it offers less storage backend, doesn't support multiple files and doesn't work with -[SQLModel](https://github.com/tiangolo/sqlmodel). This project inspired **SQLAlchemy-file** extensively +* [filedepot: ](https://github.com/amol-/depot) When I was looking for a library like this, depot was the +best I saw. This project inspired **SQLAlchemy-file** extensively and some features are implemented the same. +* [sqlalchemy-media](https://github.com/pylover/sqlalchemy-media) Another attachment extension for SqlAlchemy +to manage assets which are associated with database models \ No newline at end of file diff --git a/docs/tutorial/using-files-in-models.md b/docs/tutorial/using-files-in-models.md index 9fd4495..ceb0018 100644 --- a/docs/tutorial/using-files-in-models.md +++ b/docs/tutorial/using-files-in-models.md @@ -29,16 +29,17 @@ You can use two Column type in your model. Inherits all attributes and methods from [FileField][sqlalchemy_file.types.FileField], but also validates that the uploaded file is a valid image. -!!! info +!!! note Using [ImageField][sqlalchemy_file.types.ImageField] is like using [FileField][sqlalchemy_file.types.FileField] - with [ImageValidator][sqlalchemy_file.validators.ImageValidator] + with [ImageValidator][sqlalchemy_file.validators.ImageValidator] and + [ThumbnailGenerator][sqlalchemy_file.processors.ThumbnailGenerator] + !!! example ```Python from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy_file import ImageField Base = declarative_base() @@ -49,7 +50,7 @@ uploaded file is a valid image. id = Column(Integer, autoincrement=True, primary_key=True) title = Column(String(100), unique=True) - cover = Column(ImageField) + cover = Column(ImageField(thumbnail_size=(128, 128))) ``` ## Uploaded Files Information Whenever a supported object is assigned to a [FileField][sqlalchemy_file.types.FileField] or [ImageField][sqlalchemy_file.types.ImageField] diff --git a/docs_src/tutorial/using-files-in-models/002_imagefield_example.py b/docs_src/tutorial/using-files-in-models/002_imagefield_example.py index 7fc82a9..14c3870 100644 --- a/docs_src/tutorial/using-files-in-models/002_imagefield_example.py +++ b/docs_src/tutorial/using-files-in-models/002_imagefield_example.py @@ -10,4 +10,4 @@ class Book(Base): id = Column(Integer, autoincrement=True, primary_key=True) title = Column(String(100), unique=True) - cover = Column(ImageField) + cover = Column(ImageField(thumbnail_size=(128, 128))) diff --git a/mkdocs.yml b/mkdocs.yml index a75b15e..404e41f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: SQLAlchemy File -site_description: SQLAlchemy-file is a SQLAlchemy extension for attaching files to SQLAlchemy model and uploading them to various storage such as Amazon S3, Rackspace CloudFiles, Google Storage and others using Apache Libcloud. +site_description: SQLAlchemy-file is a SQLAlchemy extension for attaching files to SQLAlchemy model and uploading them to various storage such as Local Storage Amazon S3, Rackspace CloudFiles, Google Storage and others using Apache Libcloud. site_url: https://jowilf.github.io/sqlalchemy-file repo_name: jowilf/sqlalchemy-file repo_url: https://github.com/jowilf/sqlalchemy-file @@ -64,8 +64,8 @@ plugins: rendering: show_root_heading: true show_source: false - watch: - - sqlalchemy_file +watch: + - sqlalchemy_file extra: diff --git a/poetry.lock b/poetry.lock index a5d1a00..0d939fe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -132,7 +132,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.3" +version = "6.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -165,7 +165,7 @@ test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", [[package]] name = "fastapi" -version = "0.79.0" +version = "0.79.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "dev" optional = false @@ -176,10 +176,10 @@ pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1. starlette = "0.19.1" [package.extras] -all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] -dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.0.0)"] -test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"] +test = ["types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "anyio[trio] (>=3.2.1,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "peewee (>=3.13.3,<4.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "email_validator (>=1.1.1,<2.0.0)", "httpx (>=0.14.0,<0.19.0)", "requests (>=2.24.0,<3.0.0)", "isort (>=5.0.6,<6.0.0)", "black (==22.3.0)", "flake8 (>=3.8.3,<4.0.0)", "mypy (==0.910)", "pytest-cov (>=2.12.0,<4.0.0)", "pytest (>=6.2.4,<7.0.0)"] +doc = ["pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.4.0,<2.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)"] +all = ["uvicorn[standard] (>=0.12.0,<0.18.0)", "email_validator (>=1.1.1,<2.0.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "jinja2 (>=2.11.2,<4.0.0)", "requests (>=2.24.0,<3.0.0)"] [[package]] name = "fasteners" @@ -189,45 +189,19 @@ category = "dev" optional = false python-versions = ">=3.6" -[[package]] -name = "filedepot" -version = "0.8.0" -description = "Toolkit for storing files and attachments in web applications" -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -testing = ["mock", "requests", "ming", "turbogears2", "boto3", "flaky", "boto", "coverage", "pillow", "webtest", "sqlalchemy"] - [[package]] name = "flake8" -version = "5.0.4" +version = "3.9.2" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = ">=3.6.1" - -[package.dependencies] -importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" - -[[package]] -name = "flake8-bugbear" -version = "22.7.1" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -category = "dev" -optional = false -python-versions = ">=3.6" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -attrs = ">=19.2.0" -flake8 = ">=3.0.0" - -[package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flask" @@ -320,19 +294,20 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.2.0" +version = "4.12.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -399,11 +374,11 @@ python-versions = ">=3.7" [[package]] name = "mccabe" -version = "0.7.0" +version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = "*" [[package]] name = "mergedeep" @@ -415,7 +390,7 @@ python-versions = ">=3.6" [[package]] name = "mkdocs" -version = "1.2.4" +version = "1.3.1" description = "Project documentation with Markdown." category = "dev" optional = false @@ -424,9 +399,9 @@ python-versions = ">=3.6" [package.dependencies] click = ">=3.3" ghp-import = ">=1.0" -importlib-metadata = ">=3.10" -Jinja2 = ">=2.10.1" -Markdown = ">=3.2.1" +importlib-metadata = ">=4.3" +Jinja2 = ">=2.10.2" +Markdown = ">=3.2.1,<3.4" mergedeep = ">=1.3.4" packaging = ">=20.5" PyYAML = ">=3.10" @@ -450,19 +425,19 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-material" -version = "8.2.7" -description = "A Material Design theme for MkDocs" +version = "8.4.1" +description = "Documentation that simply works" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -jinja2 = ">=2.11.1,<3.1" +jinja2 = ">=3.0.2" markdown = ">=3.2" -mkdocs = ">=1.2.3" -mkdocs-material-extensions = ">=1.0" -pygments = ">=2.10" -pymdown-extensions = ">=9.0" +mkdocs = ">=1.3.0" +mkdocs-material-extensions = ">=1.0.3" +pygments = ">=2.12" +pymdown-extensions = ">=9.4" [[package]] name = "mkdocs-material-extensions" @@ -609,11 +584,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycodestyle" -version = "2.9.1" +version = "2.7.0" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycparser" @@ -640,11 +615,11 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" -version = "2.5.0" +version = "2.3.1" description = "passive checker of Python programs" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" @@ -953,7 +928,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "c20e5505f3cc9980df1a2118290516d3eb014d0aa3fca60ef0d21ab3b802e0a9" +content-hash = "171394df930a221f0c24ca2d09aa22cc5857d5d996e8c0ee79f1a9d96e6cdcf6" [metadata.files] anyio = [ @@ -1069,11 +1044,7 @@ coverage = [] cryptography = [] fastapi = [] fasteners = [] -filedepot = [ - {file = "filedepot-0.8.0.tar.gz", hash = "sha256:25316ecd352e16524b4d321dbad08e9beb563d5de5447f3ed312daec5d011849"}, -] flake8 = [] -flake8-bugbear = [] flask = [] flask-sqlalchemy = [] ghp-import = [] @@ -1143,7 +1114,10 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -importlib-metadata = [] +importlib-metadata = [ + {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, + {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, +] iniconfig = [] isort = [] itsdangerous = [] diff --git a/pyproject.toml b/pyproject.toml index a66b61e..6f1ac2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sqlalchemy-file" -version = "0.1.2" +version = "0.1.3" description = "SQLAlchemy-file is a SQLAlchemy extension for attaching files to SQLAlchemy model and uploading them to various storage." authors = ["Jocelin Hounon "] license = "MIT" @@ -37,8 +37,7 @@ Pillow = "^9.2.0" fasteners = "^0.17.3" black = "^22.6.0" coverage = { extras = ["toml"], version = "^6.4.2" } -flake8 = "^5.0.4" -flake8-bugbear = "^22.7.1" +flake8 = "^3.9.2" mypy = "^0.971" isort = "^5.10.1" mkdocs-material = "^8.2.7" diff --git a/sqlalchemy_file/__init__.py b/sqlalchemy_file/__init__.py index 72d4672..b51995f 100644 --- a/sqlalchemy_file/__init__.py +++ b/sqlalchemy_file/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.0" +__version__ = "0.1.3" from .file import File as File from .types import FileField as FileField diff --git a/sqlalchemy_file/helpers.py b/sqlalchemy_file/helpers.py index d3ed2fe..1c0edb9 100644 --- a/sqlalchemy_file/helpers.py +++ b/sqlalchemy_file/helpers.py @@ -7,6 +7,7 @@ from typing import Any, Dict, Union INMEMORY_FILESIZE = 1024 * 1024 +LOCAL_STORAGE_DRIVER_NAME = "Local Storage" def get_metadata_file_obj(metadata: Dict[str, Any]) -> "SpooledTemporaryFile[bytes]": diff --git a/sqlalchemy_file/storage.py b/sqlalchemy_file/storage.py index a4fecac..74a987e 100644 --- a/sqlalchemy_file/storage.py +++ b/sqlalchemy_file/storage.py @@ -1,9 +1,8 @@ from typing import Any, Dict, Iterator, Optional from libcloud.storage.base import Container -from libcloud.storage.drivers.local import LocalStorageDriver from libcloud.storage.types import ObjectDoesNotExistError -from sqlalchemy_file.helpers import get_metadata_file_obj +from sqlalchemy_file.helpers import LOCAL_STORAGE_DRIVER_NAME, get_metadata_file_obj from sqlalchemy_file.stored_file import StoredFile @@ -75,7 +74,7 @@ def save_file( ) -> StoredFile: """Save file into provided `upload_storage`""" container = cls.get(upload_storage) - if isinstance(container.driver, LocalStorageDriver): + if container.driver.name == LOCAL_STORAGE_DRIVER_NAME: obj = container.upload_object_via_stream(iterator=content, object_name=name) if metadata is not None: """ @@ -115,7 +114,7 @@ def delete_file(cls, path: str) -> bool: """ upload_storage, file_id = path.split("/") obj = StorageManager.get(upload_storage).get_object(file_id) - if isinstance(obj.driver, LocalStorageDriver): + if obj.driver.name == LOCAL_STORAGE_DRIVER_NAME: """Try deleting associated metadata file""" try: obj.container.get_object(f"{obj.name}.metadata.json").delete() diff --git a/sqlalchemy_file/stored_file.py b/sqlalchemy_file/stored_file.py index 0649c25..b5baed4 100644 --- a/sqlalchemy_file/stored_file.py +++ b/sqlalchemy_file/stored_file.py @@ -3,13 +3,13 @@ from typing import Optional from libcloud.storage.base import Object -from libcloud.storage.drivers.local import LocalStorageDriver from libcloud.storage.types import ObjectDoesNotExistError +from sqlalchemy_file.helpers import LOCAL_STORAGE_DRIVER_NAME class StoredFile: def __init__(self, obj: Object) -> None: - if isinstance(obj.driver, LocalStorageDriver): + if obj.driver.name == LOCAL_STORAGE_DRIVER_NAME: """Retrieve metadata from associated metadata file""" try: metadata_obj = obj.container.get_object(f"{obj.name}.metadata.json") @@ -31,7 +31,7 @@ def get_cdn_url(self) -> Optional[str]: return None def read(self, n: int = -1) -> bytes: - if isinstance(self.object.driver, LocalStorageDriver): + if self.object.driver.name == LOCAL_STORAGE_DRIVER_NAME: return open(self.object.get_cdn_url(), "rb").read(n) _file = tempfile.NamedTemporaryFile() self.object.download(_file.name, overwrite_existing=True) diff --git a/sqlalchemy_file/types.py b/sqlalchemy_file/types.py index 3415140..ecde4a4 100644 --- a/sqlalchemy_file/types.py +++ b/sqlalchemy_file/types.py @@ -6,7 +6,7 @@ from sqlalchemy.orm.attributes import get_history from sqlalchemy_file.file import File from sqlalchemy_file.mutable_list import MutableList -from sqlalchemy_file.processors import Processor +from sqlalchemy_file.processors import Processor, ThumbnailGenerator from sqlalchemy_file.storage import StorageManager from sqlalchemy_file.validators import ImageValidator, Validator @@ -34,6 +34,7 @@ class FileField(types.TypeDecorator): # type: ignore """ impl = types.JSON + cache_ok = True def __init__( self, @@ -106,6 +107,7 @@ def __init__( self, *args: Tuple[Any], upload_storage: Optional[str] = None, + thumbnail_size: Optional[Tuple[int, int]] = None, image_validator: Optional[ImageValidator] = None, validators: Optional[List[Validator]] = None, processors: Optional[List[Processor]] = None, @@ -118,6 +120,9 @@ def __init__( upload_storage: storage to use image_validator: ImageField use default image validator, Use this property to customize it. + thumbnail_size: If set, a thumbnail will be generated + from original image using [ThumbnailGenerator] + [sqlalchemy_file.processors.ThumbnailGenerator] validators: List of additional validators to apply processors: List of validators to apply upload_type: File class to use, could be @@ -128,8 +133,11 @@ def __init__( validators = [] if image_validator is None: image_validator = ImageValidator() - assert isinstance(image_validator, ImageValidator) - validators.insert(0, image_validator) + if thumbnail_size is not None: + if processors is None: + processors = [] + processors.append(ThumbnailGenerator(thumbnail_size)) + validators.append(image_validator) super().__init__( *args, upload_storage=upload_storage, diff --git a/tests/test_processor.py b/tests/test_processor.py index 2d0d344..c27cff6 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -4,7 +4,6 @@ import pytest from sqlalchemy import Column, Integer, String, select from sqlalchemy.orm import Session, declarative_base -from sqlalchemy_file.processors import ThumbnailGenerator from sqlalchemy_file.storage import StorageManager from sqlalchemy_file.types import ImageField @@ -36,7 +35,9 @@ class Book(Base): id = Column(Integer, autoincrement=True, primary_key=True) title = Column(String(100), unique=True) - cover = Column(ImageField(processors=[ThumbnailGenerator()])) + cover = Column( + ImageField(thumbnail_size=(128, 128)) + ) # will add thumbnail generator def __repr__(self): return "" % (