Skip to content

Commit f21fe2e

Browse files
committed
Added File and Image Doc; code refactoring and File made attributeDict
1 parent 05b4138 commit f21fe2e

File tree

15 files changed

+363
-106
lines changed

15 files changed

+363
-106
lines changed

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ such as **model**, **session**, and **engine**, ensuring an efficient and cohere
2121

2222
Notably, EllarSQL refrains from altering the fundamental workings or usage of SQLAlchemy.
2323
This documentation is focused on the meticulous setup of EllarSQL. For an in-depth exploration of SQLAlchemy,
24-
we recommend referring to the comprehensive [SQLAlchemy documentation](https://docs.sqlalchemy.org/).
24+
we recommend referring to the comprehensive [SQLAlchemy documentation](https://docs.sqlalchemy.org/){target="_blank"}.
2525

2626
## **Feature Highlights**
2727
EllarSQL comes packed with a set of awesome features designed:

docs/models/file-fields.md

Lines changed: 245 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,250 @@
11
# **File & Image Column Types**
22

3+
EllarSQL provides **File** and **Image** column type descriptors to attach files to your models. It integrates seamlessly with the [Sqlalchemy-file](https://jowilf.github.io/sqlalchemy-file/tutorial/using-files-in-models/){target="_blank"} and [EllarStorage](https://github.com/python-ellar/ellar-storage){target="_blank"} packages.
4+
35
## **FileField Column**
6+
`FileField` can handle any type of file, making it a versatile option for file storage in your database models.
7+
8+
```python
9+
from ellar_sql import model
10+
11+
class Attachment(model.Model):
12+
__tablename__ = "attachments"
13+
14+
id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
15+
name: model.Mapped[str] = model.mapped_column(model.String(50), unique=True)
16+
content: model.Mapped[model.typeDecorator.File] = model.mapped_column(model.typeDecorator.FileField)
17+
```
18+
419
## **ImageField Column**
5-
### **Uploading File**
6-
#### Save file object
7-
#### Retrieve file object
8-
#### Extra and Headers
9-
#### Metadata
10-
## **Validators**
11-
## **Processors**
20+
`ImageField` builds on **FileField**, adding validation to ensure the uploaded file is a valid image. This guarantees that only image files are stored.
21+
22+
```python
23+
from ellar_sql import model
24+
25+
class Book(model.Model):
26+
__tablename__ = "books"
27+
28+
id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
29+
title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)
30+
cover: model.Mapped[model.typeDecorator.File] = model.mapped_column(
31+
model.typeDecorator.ImageField(
32+
thumbnail_size=(128, 128),
33+
)
34+
)
35+
```
36+
37+
By setting `thumbnail_size`, an additional thumbnail image is created and saved alongside the original `cover` image. You can access the thumbnail via `book.cover.thumbnail`.
38+
39+
**Note**: `ImageField` requires the [`Pillow`](https://pypi.org/project/pillow/) package:
40+
```shell
41+
pip install pillow
42+
```
43+
44+
### **Uploading Files**
45+
To handle where files are saved, EllarSQL's File and Image Fields require EllarStorage's `StorageModule` setup. For more details, refer to the [`StorageModule` setup](https://github.com/python-ellar/ellar-storage?tab=readme-ov-file#storagemodulesetup){target="_blank"}.
46+
47+
### **Saving Files**
48+
EllarSQL supports `Starlette.datastructures.UploadFile` for Image and File Fields, simplifying file saving directly from requests.
49+
50+
For example:
51+
52+
```python
53+
import ellar.common as ecm
54+
from ellar_sql import model
55+
from ..models import Book
56+
from .schema import BookSchema
57+
58+
@ecm.Controller
59+
class BooksController(ecm.ControllerBase):
60+
@ecm.post("/", response={201: BookSchema})
61+
def create_book(
62+
self,
63+
title: ecm.Body[str],
64+
cover: ecm.File[ecm.UploadFile],
65+
session: ecm.Inject[model.Session],
66+
):
67+
book = Book(title=title, cover=cover)
68+
session.add(book)
69+
session.commit()
70+
session.refresh(book)
71+
return book
72+
```
73+
74+
#### Retrieving File Object
75+
The object retrieved from an Image or File Field is an instance of [`ellar_sql.model.typeDecorator.File`](https://github.com/python-ellar/ellar-sql/blob/master/ellar_sql/model/typeDecorator/file/file.py).
76+
77+
```python
78+
@ecm.get("/{book_id:int}", response={200: BookSchema})
79+
def get_book_by_id(
80+
self,
81+
book_id: int,
82+
session: ecm.Inject[model.Session],
83+
):
84+
book = session.execute(
85+
model.select(Book).where(Book.id == book_id)
86+
).scalar_one()
87+
88+
assert book.cover.saved # saved is True for a saved file
89+
assert book.cover.file.read() is not None # access file content
90+
91+
assert book.cover.filename is not None # `unnamed` when no filename is provided
92+
assert book.cover.file_id is not None # UUID v4
93+
94+
assert book.cover.upload_storage == "default"
95+
assert book.cover.content_type is not None
96+
97+
assert book.cover.uploaded_at is not None
98+
assert len(book.cover.files) == 2 # original image and generated thumbnail image
99+
100+
return book
101+
```
102+
103+
#### Adding More Information to a Saved File Object
104+
The File object behaves like a Python dictionary, allowing you to add custom metadata. Be careful not to overwrite default attributes used by the File object internally.
105+
106+
```python
107+
from ellar_sql.model.typeDecorator import File
108+
from ..models import Book
109+
110+
content = File(open("./example.png", "rb"), custom_key1="custom_value1", custom_key2="custom_value2")
111+
content["custom_key3"] = "custom_value3"
112+
book = Book(title="Dummy", cover=content)
113+
114+
session.add(book)
115+
session.commit()
116+
session.refresh(book)
117+
118+
assert book.cover.custom_key1 == "custom_value1"
119+
assert book.cover.custom_key2 == "custom_value2"
120+
assert book.cover["custom_key3"] == "custom_value3"
121+
```
122+
123+
## **Extra and Headers**
124+
`Apache-libcloud` allows you to store each object with additional attributes or headers.
125+
126+
You can add extras and headers in two ways:
127+
128+
### Inline Field Declaration
129+
You can specify these extras and headers directly in the field declaration:
130+
131+
```python
132+
from ellar_sql import model
133+
134+
class Attachment(model.Model):
135+
__tablename__ = "attachments"
136+
137+
id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
138+
name: model.Mapped[str] = model.mapped_column(model.String(50), unique=True)
139+
content: model.Mapped[model.typeDecorator.File] = model.mapped_column(model.typeDecorator.FileField(
140+
extra={
141+
"acl": "private",
142+
"dummy_key": "dummy_value",
143+
"meta_data": {"key1": "value1", "key2": "value2"},
144+
},
145+
headers={
146+
"Access-Control-Allow-Origin": "http://test.com",
147+
"Custom-Key": "xxxxxxx",
148+
},
149+
))
150+
```
151+
152+
### In File Object
153+
Alternatively, you can set extras and headers in the File object itself:
154+
155+
```python
156+
from ellar_sql.model.typeDecorator import File
157+
158+
attachment = Attachment(
159+
name="Public document",
160+
content=File(DummyFile(), extra={"acl": "public-read"}),
161+
)
162+
session.add(attachment)
163+
session.commit()
164+
session.refresh(attachment)
165+
166+
assert attachment.content.file.object.extra["acl"] == "public-read"
167+
```
168+
169+
## **Uploading to a Specific Storage**
170+
By default, files are uploaded to the `default` storage specified in `StorageModule`.
171+
You can change this by specifying a different `upload_storage` in the field declaration:
172+
173+
```python
174+
from ellar_sql import model
175+
176+
class Book(model.Model):
177+
__tablename__ = "books"
178+
179+
id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
180+
title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)
181+
cover: model.Mapped[model.typeDecorator.File] = model.mapped_column(
182+
model.typeDecorator.ImageField(
183+
thumbnail_size=(128, 128), upload_storage="bookstore"
184+
)
185+
)
186+
```
187+
Setting `upload_storage="bookstore"` ensures
188+
that the book cover is uploaded to the `bookstore` container defined in `StorageModule`.
189+
12190
## **Multiple Files**
191+
A File or Image Field column can be configured to hold multiple files by setting `multiple=True`.
192+
193+
For example:
194+
195+
```python
196+
import typing as t
197+
from ellar_sql import model
198+
199+
class Article(model.Model):
200+
__tablename__ = "articles"
201+
202+
id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
203+
title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)
204+
documents: model.Mapped[t.List[model.typeDecorator.File]] = model.mapped_column(
205+
model.typeDecorator.FileField(multiple=True, upload_storage="documents")
206+
)
207+
```
208+
The `Article` model's `documents` column will store a list of files,
209+
applying validators and processors to each file individually.
210+
The returned model is a list of File objects.
211+
212+
#### Saving Multiple File Fields
213+
Saving multiple files is as simple as passing a list of file contents to the file field column. For example:
214+
215+
```python
216+
import typing as t
217+
import ellar.common as ecm
218+
from ellar_sql import model
219+
from ..models import Article
220+
from .schema import ArticleSchema
221+
222+
@ecm.Controller
223+
class ArticlesController(ecm.ControllerBase):
224+
@ecm.post("/", response={201: ArticleSchema})
225+
def create_article(
226+
self,
227+
title: ecm.Body[str],
228+
documents: ecm.File[t.List[ecm.UploadFile]],
229+
session: ecm.Inject[model.Session],
230+
):
231+
article = Article(
232+
title=title, documents=[
233+
model.typeDecorator.File(
234+
content="Hello World",
235+
filename="hello.txt",
236+
content_type="text/plain",
237+
)
238+
] + documents
239+
)
240+
session.add(article)
241+
session.commit()
242+
session.refresh(article)
243+
return article
244+
```
245+
246+
## **See Also**
247+
- [Validators](https://jowilf.github.io/sqlalchemy-file/tutorial/using-files-in-models/#validators)
248+
- [Processors](https://jowilf.github.io/sqlalchemy-file/tutorial/using-files-in-models/#processors)
249+
250+
For a more comprehensive hands-on experience, check out the [file-field-example](https://github.com/python-ellar/ellar-sql/tree/main/samples/file-field-example) project.

ellar_sql/model/typeDecorator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
"GenericIP",
88
"FileField",
99
"ImageField",
10+
"File",
1011
]

ellar_sql/model/typeDecorator/exceptions.py

Lines changed: 0 additions & 31 deletions
This file was deleted.

ellar_sql/model/typeDecorator/file/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .exceptions import FileExceptionHandler
12
from .file import File
23
from .file_tracker import ModifiedFileFieldSessionTracker
34
from .processors import Processor, ThumbnailGenerator
@@ -14,6 +15,7 @@
1415
"ImageField",
1516
"Processor",
1617
"ThumbnailGenerator",
18+
"FileExceptionHandler",
1719
]
1820

1921

ellar_sql/model/typeDecorator/file/exceptions.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,22 @@
33
from sqlalchemy_file.exceptions import DimensionValidationError # noqa
44
from sqlalchemy_file.exceptions import AspectRatioValidationError # noqa
55
from sqlalchemy_file.exceptions import SizeValidationError # noqa
6-
from sqlalchemy_file.exceptions import ValidationError # noqa
6+
from sqlalchemy_file.exceptions import ValidationError
7+
8+
from ellar.common import IExecutionContext
9+
from ellar.common.exceptions import CallableExceptionHandler
10+
11+
12+
def _exception_handlers(ctx: IExecutionContext, exc: ValidationError):
13+
app_config = ctx.get_app().config
14+
return app_config.DEFAULT_JSON_CLASS(
15+
{"message": exc.msg, "key": exc.key},
16+
status_code=400,
17+
)
18+
19+
20+
# Register to application config.EXCEPTION_HANDLERS to add exception handler for sqlalchemy-file
21+
FileExceptionHandler = CallableExceptionHandler(
22+
exc_class_or_status_code=ValidationError,
23+
callable_exception_handler=_exception_handlers,
24+
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy