Visitar URL original
feat: Image and FileField support with sqlachemy-file by eadwinCode · Pull Request #35 · python-ellar/ellar-sql · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added image and field sample projects
  • Loading branch information
eadwinCode committed Jun 10, 2024
commit 05b4138a70229d96a5cf1dafcdf1c2abda2f3128
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dependencies = [
"ellar-cli >= 0.3.7",
"sqlalchemy >= 2.0.23",
"alembic >= 1.10.0",
"ellar-storage >= 0.1.2",
"ellar-storage >= 0.1.4",
"sqlalchemy-file >= 0.6.0",
]

Expand Down
18 changes: 18 additions & 0 deletions samples/file-field-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Introduction
This project illustrates how to use ImageField and FileField with EllarStorage package

## Requirements
Python >= 3.8
ellar-cli
ellar-sql

## Project setup
```
pip install -r requirements.txt
```

### Development Server
```
python manage.py runserver --reload
```
visit API [docs](http://localhost:8000/docs)
Empty file.
70 changes: 70 additions & 0 deletions samples/file-field-example/file_field_example/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
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=file_field_example.config:DevelopmentConfig
"""

import typing as t

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


class BaseConfig(ConfigDefaultTypesMixin):
DEBUG: bool = False

DEFAULT_JSON_CLASS: t.Type[JSONResponse] = JSONResponse
SECRET_KEY: str = "ellar_4BmOAHaR4N6K81UX22hwJQqVh93XJyiUYKrhgzWMKsc"

# 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
)


class DevelopmentConfig(BaseConfig):
DEBUG: bool = True
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .articles import ArticlesController
from .attachments import AttachmentController
from .books import BooksController

__all__ = [
"BooksController",
"ArticlesController",
"AttachmentController",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import typing as t

import ellar.common as ecm

from ellar_sql import model, paginate

from ..models import Article
from .schema import ArticleSchema


@ecm.Controller
class ArticlesController(ecm.ControllerBase):
@ecm.post("/", response={201: ArticleSchema})
def create_article(
self,
title: ecm.Body[str],
documents: ecm.File[t.List[ecm.UploadFile]],
session: ecm.Inject[model.Session],
):
article = Article(title=title, documents=documents)
session.add(article)

session.commit()
session.refresh(article)

return article

@ecm.get("/")
@paginate(model=Article, item_schema=ArticleSchema)
def list_articles(self):
return {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ellar.common as ecm

from ellar_sql import model, paginate

from ..models import Attachment
from .schema import AttachmentSchema


@ecm.Controller
class AttachmentController(ecm.ControllerBase):
@ecm.post("/", response={201: AttachmentSchema})
def create_attachment(
self,
name: ecm.Body[str],
content: ecm.File[ecm.UploadFile],
session: ecm.Inject[model.Session],
):
attachment = Attachment(name=name, content=content)
session.add(attachment)

session.commit()
session.refresh(attachment)

return attachment

@ecm.get("/")
@paginate(model=Attachment, item_schema=AttachmentSchema)
def list_attachments(self):
return {}
29 changes: 29 additions & 0 deletions samples/file-field-example/file_field_example/controllers/books.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ellar.common as ecm

from ellar_sql import model, paginate

from ..models import Book
from .schema import BookSchema


@ecm.Controller
class BooksController(ecm.ControllerBase):
@ecm.post("/", response={201: BookSchema})
def create_book(
self,
title: ecm.Body[str],
cover: ecm.File[ecm.UploadFile],
session: ecm.Inject[model.Session],
):
book = Book(title=title, cover=cover)
session.add(book)

session.commit()
session.refresh(book)

return book

@ecm.get("/")
@paginate(model=Book, item_schema=BookSchema)
def list_book(self):
return {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import typing as t

import ellar.common as ecm
from ellar.app import current_injector
from ellar.core import Request
from ellar.pydantic import model_validator
from pydantic import HttpUrl

from ellar_sql.model.mixins import ModelDataExportMixin
from ellar_sql.model.typeDecorator.file import File


class BookSchema(ecm.Serializer):
id: int
title: str
cover: HttpUrl

thumbnail: HttpUrl

@model_validator(mode="before")
def cover_validate_before(cls, values) -> t.Any:
req: Request = (
current_injector.get(ecm.IExecutionContext)
.switch_to_http_connection()
.get_request()
)
values = values.dict() if isinstance(values, ModelDataExportMixin) else values

cover: File = values["cover"]

values["cover"] = str(
req.url_for("storage:download", path=cover["path"])
) # from ellar_storage.StorageController
values["thumbnail"] = str(
req.url_for("storage:download", path=cover["files"][1])
) # from ellar_storage.StorageController

return values


class ArticleSchema(ecm.Serializer):
id: int
title: str
documents: t.List[HttpUrl]

@model_validator(mode="before")
def model_validate_before(cls, values) -> t.Any:
req: Request = (
current_injector.get(ecm.IExecutionContext)
.switch_to_http_connection()
.get_request()
)
values = values.dict() if isinstance(values, ModelDataExportMixin) else values

values["documents"] = [
str(req.url_for("storage:download", path=item["path"]))
for item in values["documents"]
]

return values


class AttachmentSchema(ecm.Serializer):
id: int
name: str
content: HttpUrl

@model_validator(mode="before")
def model_validate_before(cls, values) -> t.Any:
req: Request = (
current_injector.get(ecm.IExecutionContext)
.switch_to_http_connection()
.get_request()
)
values = values.dict() if isinstance(values, ModelDataExportMixin) else values

values["content"] = str(
req.url_for("storage:download", path=values["content"]["path"])
)
return values
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .article import Article
from .attachment import Attachment
from .book import Book

__all__ = [
"Book",
"Article",
"Attachment",
]
12 changes: 12 additions & 0 deletions samples/file-field-example/file_field_example/models/article.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from ellar_sql import model


class Article(model.Model):
__tablename__ = "articles"

id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)

documents: model.Mapped[dict] = model.mapped_column(
model.typeDecorator.FileField(multiple=True, upload_storage="documents")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from ellar_sql import model


class Attachment(model.Model):
__tablename__ = "attachments"

id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
name: model.Mapped[str] = model.mapped_column(model.String(50), unique=True)
content: model.Mapped[dict] = model.mapped_column(model.typeDecorator.FileField)
14 changes: 14 additions & 0 deletions samples/file-field-example/file_field_example/models/book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from ellar_sql import model


class Book(model.Model):
__tablename__ = "books"

id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)

cover: model.Mapped[dict] = model.mapped_column(
model.typeDecorator.ImageField(
thumbnail_size=(128, 128), upload_storage="bookstore"
)
)
59 changes: 59 additions & 0 deletions samples/file-field-example/file_field_example/root_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os
from pathlib import Path

from ellar.app import App
from ellar.common import (
IApplicationStartup,
IExecutionContext,
JSONResponse,
Module,
Response,
exception_handler,
)
from ellar.core import ModuleBase
from ellar.samples.modules import HomeModule
from ellar_storage import Provider, StorageModule, get_driver

from ellar_sql import EllarSQLModule, EllarSQLService

from .controllers import ArticlesController, AttachmentController, BooksController

BASE_DIRS = Path(__file__).parent

modules = [
HomeModule,
StorageModule.setup(
files={
"driver": get_driver(Provider.LOCAL),
"options": {"key": os.path.join(BASE_DIRS, "media")},
},
bookstore={
"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",
),
EllarSQLModule.setup(
databases="sqlite:///project.db",
models=["file_field_example.models"],
migration_options={"directory": os.path.join(BASE_DIRS, "migrations")},
),
]


@Module(
modules=modules,
controllers=[BooksController, AttachmentController, ArticlesController],
)
class ApplicationModule(ModuleBase, IApplicationStartup):
async def on_startup(self, app: App) -> None:
db_service = app.injector.get(EllarSQLService)
db_service.create_all()

@exception_handler(404)
def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response:
return JSONResponse({"detail": "Resource not found."}, status_code=404)
Loading