Skip to content

Commit 9bbc57a

Browse files
committed
Improved docs. Add section Upload File into **Tutorial / Using files in models**
1 parent 67c2836 commit 9bbc57a

File tree

10 files changed

+190
-11
lines changed

10 files changed

+190
-11
lines changed

docs/tutorial/using-files-in-models.md

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,104 @@ uploaded file is a valid image.
5252
title = Column(String(100), unique=True)
5353
cover = Column(ImageField(thumbnail_size=(128, 128)))
5454
```
55-
## Uploaded Files Information
55+
## Upload File
56+
57+
Let's say you defined your model like this
58+
```python
59+
class Attachment(Base):
60+
__tablename__ = "attachment"
61+
62+
id = Column(Integer, autoincrement=True, primary_key=True)
63+
name = Column(String(50), unique=True)
64+
content = Column(FileField)
65+
```
66+
and configure your storage like this
67+
```python
68+
container = LocalStorageDriver("/tmp/storage").get_container("attachment")
69+
StorageManager.add_storage("default", container)
70+
```
71+
72+
### Save file object
73+
5674
Whenever a supported object is assigned to a [FileField][sqlalchemy_file.types.FileField] or [ImageField][sqlalchemy_file.types.ImageField]
5775
it will be converted to a [File][sqlalchemy_file.file.File] object.
76+
```python
77+
with Session(engine) as session:
78+
session.add(Attachment(name="attachment1", content=open("./example.txt", "rb")))
79+
session.add(Attachment(name="attachment2", content=b"Hello world"))
80+
session.add(Attachment(name="attachment3", content="Hello world"))
81+
file = File(content="Hello World", filename="hello.txt", content_type="text/plain")
82+
session.add(Attachment(name="attachment4", content=file))
83+
session.commit()
84+
```
85+
The file itself will be uploaded to your configured storage, and only the [File][sqlalchemy_file.file.File]
86+
information will be stored on the database as JSON.
5887

59-
This is the same object you will get back when reloading the models from database and apart from the file itself which is accessible
60-
through the `.file` property, it provides additional attributes described into the [File][sqlalchemy_file.file.File] documentation itself.
88+
### Retrieve file object
89+
90+
This is the same [File][sqlalchemy_file.file.File] object you will get back when reloading the models from database and the file itself is accessible
91+
through the `.file` property.
92+
93+
!!! note
94+
Check the [File][sqlalchemy_file.file.File] documentation for all default attributes save into the database.
95+
96+
```python
97+
with Session(engine) as session:
98+
attachment = session.execute(
99+
select(Attachment).where(Attachment.name == "attachment3")
100+
).scalar_one()
101+
assert attachment.content.saved # saved is True for saved file
102+
assert attachment.content.file.read() == b"Hello world" # access file content
103+
assert attachment.content["filename"] is not None # `unnamed` when no filename are provided
104+
assert attachment.content["file_id"] is not None # uuid v4
105+
assert attachment.content["upload_storage"] == "default"
106+
assert attachment.content["content_type"] is not None
107+
assert attachment.content["uploaded_at"] is not None
108+
```
109+
110+
### Save additional information
111+
112+
It's important to note that [File][sqlalchemy_file.file.File] object inherit from python `dict`.
113+
Therefore, you can add additional information to your file object like a dict object. Just make sure to not use
114+
the default attributes used by [File][sqlalchemy_file.file.File] object internally.
115+
116+
!!! Example
117+
```python
118+
content = File(open("./example.txt", "rb"),custom_key1="custom_value1", custom_key2="custom_value2")
119+
content["custom_key3"] = "custom_value3"
120+
attachment = Attachment(name="Dummy", content=content)
121+
122+
session.add(attachment)
123+
session.commit()
124+
session.refresh(attachment)
125+
126+
assert attachment.custom_key1 == "custom_value1"
127+
assert attachment.custom_key2 == "custom_value2"
128+
assert attachment["custom_key3"] == "custom_value3"
129+
```
130+
131+
!!! important
132+
[File][sqlalchemy_file.file.File] provides also attribute style access.
133+
You can access your keys as attributes.
134+
135+
### Metadata
136+
137+
*SQLAlchemy-file* store the uploaded file with some metadata. Only `filename` and `content_type` are sent by default,
138+
. You can complete with `metadata` key inside your [File][sqlalchemy_file.file.File] object.
139+
140+
!!! Example
141+
```py hl_lines="2"
142+
with Session(engine) as session:
143+
content = File(DummyFile(), metadata={"key1": "val1", "key2": "val2"})
144+
attachment = Attachment(name="Additional metadata", content=content)
145+
session.add(attachment)
146+
session.commit()
147+
attachment = session.execute(
148+
select(Attachment).where(Attachment.name == "Additional metadata")
149+
).scalar_one()
150+
assert attachment.content.file.object.meta_data["key1"] == "val1"
151+
assert attachment.content.file.object.meta_data["key2"] == "val2"
152+
```
61153

62154
## Uploading on a Specific Storage
63155

@@ -119,7 +211,7 @@ Validators can add additional properties to the file object. For example
119211
the file object.
120212

121213
**SQLAlchemy-file** has built-in validators to get started, but you can create your own validator
122-
by extending [ValidationError][sqlalchemy_file.exceptions.ValidationError] base class.
214+
by extending [Validator][sqlalchemy_file.validators.Validator] base class.
123215

124216
Built-in validators:
125217

docs_src/__init__.py

Whitespace-only changes.

docs_src/tutorial/__init__.py

Whitespace-only changes.

docs_src/tutorial/quick-start/__init__.py

Whitespace-only changes.

docs_src/tutorial/storage-manager/__init__.py

Whitespace-only changes.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import os
2+
3+
from libcloud.storage.drivers.local import LocalStorageDriver
4+
from sqlalchemy import Column, Integer, String, create_engine, select
5+
from sqlalchemy.ext.declarative import declarative_base
6+
from sqlalchemy.orm import Session
7+
from sqlalchemy_file import File, FileField
8+
from sqlalchemy_file.storage import StorageManager
9+
10+
Base = declarative_base()
11+
12+
13+
# Define your model
14+
class Attachment(Base):
15+
__tablename__ = "attachment"
16+
17+
id = Column(Integer, autoincrement=True, primary_key=True)
18+
name = Column(String(50), unique=True)
19+
content = Column(FileField)
20+
21+
22+
# Configure Storage
23+
os.makedirs("/tmp/storage/attachment", 0o777, exist_ok=True)
24+
container = LocalStorageDriver("/tmp/storage").get_container("attachment")
25+
StorageManager.add_storage("default", container)
26+
27+
# Save your model
28+
engine = create_engine(
29+
"sqlite:///example.db", connect_args={"check_same_thread": False}
30+
)
31+
Base.metadata.create_all(engine)
32+
33+
with Session(engine) as session:
34+
session.add(Attachment(name="attachment1", content=open("./example.txt", "rb")))
35+
session.add(Attachment(name="attachment2", content=b"Hello world"))
36+
session.add(Attachment(name="attachment3", content="Hello world"))
37+
file = File(content="Hello World", filename="hello.txt", content_type="text/plain")
38+
session.add(Attachment(name="attachment4", content=file))
39+
session.commit()
40+
41+
attachment = session.execute(
42+
select(Attachment).where(Attachment.name == "attachment3")
43+
).scalar_one()
44+
assert attachment.content.saved # saved is True for saved file
45+
assert attachment.content.file.read() == b"Hello world" # access file content
46+
assert (
47+
attachment.content["filename"] is not None
48+
) # `unnamed` when no filename are provided
49+
assert attachment.content["file_id"] is not None # uuid v4
50+
assert attachment.content["upload_storage"] == "default"
51+
assert attachment.content["content_type"] is not None
52+
assert attachment.content["uploaded_at"] is not None

docs_src/tutorial/using-files-in-models/__init__.py

Whitespace-only changes.

sqlalchemy_file/file.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ def __init__(
4343
content: Any,
4444
filename: Optional[str] = None,
4545
content_type: Optional[str] = None,
46+
**kwargs: Dict[str, Any],
4647
) -> None:
47-
super().__init__()
48+
super().__init__(**kwargs)
4849
if isinstance(content, dict):
4950
object.__setattr__(self, "original_content", None)
5051
object.__setattr__(self, "saved", True)
@@ -83,10 +84,12 @@ def apply_processors(
8384

8485
def save_to_storage(self, upload_storage: Optional[str] = None) -> None:
8586
"""Save current file into provided `upload_storage`"""
87+
metadata = self.get("metadata", {})
88+
metadata.update({"filename": self.filename, "content_type": self.content_type})
8689
stored_file = self.store_content(
8790
self.original_content,
8891
upload_storage,
89-
metadata={"filename": self.filename, "content_type": self.content_type},
92+
metadata=metadata,
9093
)
9194
self["file_id"] = stored_file.name
9295
self["upload_storage"] = upload_storage

sqlalchemy_file/processors.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,19 @@ def process(self, file: "File", upload_storage: Optional[str] = None) -> None:
114114
f"image/{self.thumbnail_format}".lower(),
115115
)
116116
ext = mimetypes.guess_extension(content_type)
117-
stored_file = file.store_content(
118-
output,
119-
upload_storage,
120-
metadata={
117+
metadata = file.get("metadata", {})
118+
metadata.update(
119+
{
121120
"filename": file["filename"] + f".thumbnail{width}x{height}{ext}",
122121
"content_type": content_type,
123122
"width": width,
124123
"height": height,
125-
},
124+
}
125+
)
126+
stored_file = file.store_content(
127+
output,
128+
upload_storage,
129+
metadata=metadata,
126130
)
127131
file.update(
128132
{

tests/test_result_value.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,34 @@ def test_single_column_is_dictlike(self) -> None:
6868
assert attachment.content.dummy_attr == "Dummy data"
6969
assert "del_attr" not in attachment.content
7070

71+
def test_file_custom_attributes(self) -> None:
72+
with Session(engine) as session:
73+
content = File(
74+
DummyFile(), custom_key1="custom_value1", custom_key2="custom_value2"
75+
)
76+
attachment = Attachment(name="Custom attributes", content=content)
77+
session.add(attachment)
78+
session.commit()
79+
attachment = session.execute(
80+
select(Attachment).where(Attachment.name == "Custom attributes")
81+
).scalar_one()
82+
assert attachment.content["custom_key1"] == "custom_value1"
83+
assert attachment.content["custom_key2"] == "custom_value2"
84+
assert attachment.content.custom_key1 == "custom_value1"
85+
assert attachment.content.custom_key2 == "custom_value2"
86+
87+
def test_file_additional_metadata(self) -> None:
88+
with Session(engine) as session:
89+
content = File(DummyFile(), metadata={"key1": "val1", "key2": "val2"})
90+
attachment = Attachment(name="Additional metadata", content=content)
91+
session.add(attachment)
92+
session.commit()
93+
attachment = session.execute(
94+
select(Attachment).where(Attachment.name == "Additional metadata")
95+
).scalar_one()
96+
assert attachment.content.file.object.meta_data["key1"] == "val1"
97+
assert attachment.content.file.object.meta_data["key2"] == "val2"
98+
7199
def test_multiple_column_is_list_of_dictlike(self) -> None:
72100
with Session(engine) as session:
73101
attachment = Attachment(

0 commit comments

Comments
 (0)