Skip to content

Commit

Permalink
Improved docs. Add section Upload File into **Tutorial / Using file…
Browse files Browse the repository at this point in the history
…s in models**
  • Loading branch information
jowilf committed Aug 30, 2022
1 parent 67c2836 commit 9bbc57a
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 11 deletions.
100 changes: 96 additions & 4 deletions docs/tutorial/using-files-in-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,104 @@ uploaded file is a valid image.
title = Column(String(100), unique=True)
cover = Column(ImageField(thumbnail_size=(128, 128)))
```
## Uploaded Files Information
## Upload File

Let's say you defined your model like this
```python
class Attachment(Base):
__tablename__ = "attachment"

id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(String(50), unique=True)
content = Column(FileField)
```
and configure your storage like this
```python
container = LocalStorageDriver("/tmp/storage").get_container("attachment")
StorageManager.add_storage("default", container)
```

### Save file object

Whenever a supported object is assigned to a [FileField][sqlalchemy_file.types.FileField] or [ImageField][sqlalchemy_file.types.ImageField]
it will be converted to a [File][sqlalchemy_file.file.File] object.
```python
with Session(engine) as session:
session.add(Attachment(name="attachment1", content=open("./example.txt", "rb")))
session.add(Attachment(name="attachment2", content=b"Hello world"))
session.add(Attachment(name="attachment3", content="Hello world"))
file = File(content="Hello World", filename="hello.txt", content_type="text/plain")
session.add(Attachment(name="attachment4", content=file))
session.commit()
```
The file itself will be uploaded to your configured storage, and only the [File][sqlalchemy_file.file.File]
information will be stored on the database as JSON.

This is the same object you will get back when reloading the models from database and apart from the file itself which is accessible
through the `.file` property, it provides additional attributes described into the [File][sqlalchemy_file.file.File] documentation itself.
### Retrieve file object

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
through the `.file` property.

!!! note
Check the [File][sqlalchemy_file.file.File] documentation for all default attributes save into the database.

```python
with Session(engine) as session:
attachment = session.execute(
select(Attachment).where(Attachment.name == "attachment3")
).scalar_one()
assert attachment.content.saved # saved is True for saved file
assert attachment.content.file.read() == b"Hello world" # access file content
assert attachment.content["filename"] is not None # `unnamed` when no filename are provided
assert attachment.content["file_id"] is not None # uuid v4
assert attachment.content["upload_storage"] == "default"
assert attachment.content["content_type"] is not None
assert attachment.content["uploaded_at"] is not None
```

### Save additional information

It's important to note that [File][sqlalchemy_file.file.File] object inherit from python `dict`.
Therefore, you can add additional information to your file object like a dict object. Just make sure to not use
the default attributes used by [File][sqlalchemy_file.file.File] object internally.

!!! Example
```python
content = File(open("./example.txt", "rb"),custom_key1="custom_value1", custom_key2="custom_value2")
content["custom_key3"] = "custom_value3"
attachment = Attachment(name="Dummy", content=content)

session.add(attachment)
session.commit()
session.refresh(attachment)

assert attachment.custom_key1 == "custom_value1"
assert attachment.custom_key2 == "custom_value2"
assert attachment["custom_key3"] == "custom_value3"
```

!!! important
[File][sqlalchemy_file.file.File] provides also attribute style access.
You can access your keys as attributes.

### Metadata

*SQLAlchemy-file* store the uploaded file with some metadata. Only `filename` and `content_type` are sent by default,
. You can complete with `metadata` key inside your [File][sqlalchemy_file.file.File] object.

!!! Example
```py hl_lines="2"
with Session(engine) as session:
content = File(DummyFile(), metadata={"key1": "val1", "key2": "val2"})
attachment = Attachment(name="Additional metadata", content=content)
session.add(attachment)
session.commit()
attachment = session.execute(
select(Attachment).where(Attachment.name == "Additional metadata")
).scalar_one()
assert attachment.content.file.object.meta_data["key1"] == "val1"
assert attachment.content.file.object.meta_data["key2"] == "val2"
```

## Uploading on a Specific Storage

Expand Down Expand Up @@ -119,7 +211,7 @@ Validators can add additional properties to the file object. For example
the file object.

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

Built-in validators:

Expand Down
Empty file added docs_src/__init__.py
Empty file.
Empty file added docs_src/tutorial/__init__.py
Empty file.
Empty file.
Empty file.
52 changes: 52 additions & 0 deletions docs_src/tutorial/using-files-in-models/008_file_information.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os

from libcloud.storage.drivers.local import LocalStorageDriver
from sqlalchemy import Column, Integer, String, create_engine, select
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
from sqlalchemy_file import File, FileField
from sqlalchemy_file.storage import StorageManager

Base = declarative_base()


# Define your model
class Attachment(Base):
__tablename__ = "attachment"

id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(String(50), unique=True)
content = Column(FileField)


# Configure Storage
os.makedirs("/tmp/storage/attachment", 0o777, exist_ok=True)
container = LocalStorageDriver("/tmp/storage").get_container("attachment")
StorageManager.add_storage("default", container)

# Save your model
engine = create_engine(
"sqlite:///example.db", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)

with Session(engine) as session:
session.add(Attachment(name="attachment1", content=open("./example.txt", "rb")))
session.add(Attachment(name="attachment2", content=b"Hello world"))
session.add(Attachment(name="attachment3", content="Hello world"))
file = File(content="Hello World", filename="hello.txt", content_type="text/plain")
session.add(Attachment(name="attachment4", content=file))
session.commit()

attachment = session.execute(
select(Attachment).where(Attachment.name == "attachment3")
).scalar_one()
assert attachment.content.saved # saved is True for saved file
assert attachment.content.file.read() == b"Hello world" # access file content
assert (
attachment.content["filename"] is not None
) # `unnamed` when no filename are provided
assert attachment.content["file_id"] is not None # uuid v4
assert attachment.content["upload_storage"] == "default"
assert attachment.content["content_type"] is not None
assert attachment.content["uploaded_at"] is not None
Empty file.
7 changes: 5 additions & 2 deletions sqlalchemy_file/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ def __init__(
content: Any,
filename: Optional[str] = None,
content_type: Optional[str] = None,
**kwargs: Dict[str, Any],
) -> None:
super().__init__()
super().__init__(**kwargs)
if isinstance(content, dict):
object.__setattr__(self, "original_content", None)
object.__setattr__(self, "saved", True)
Expand Down Expand Up @@ -83,10 +84,12 @@ def apply_processors(

def save_to_storage(self, upload_storage: Optional[str] = None) -> None:
"""Save current file into provided `upload_storage`"""
metadata = self.get("metadata", {})
metadata.update({"filename": self.filename, "content_type": self.content_type})
stored_file = self.store_content(
self.original_content,
upload_storage,
metadata={"filename": self.filename, "content_type": self.content_type},
metadata=metadata,
)
self["file_id"] = stored_file.name
self["upload_storage"] = upload_storage
Expand Down
14 changes: 9 additions & 5 deletions sqlalchemy_file/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,19 @@ def process(self, file: "File", upload_storage: Optional[str] = None) -> None:
f"image/{self.thumbnail_format}".lower(),
)
ext = mimetypes.guess_extension(content_type)
stored_file = file.store_content(
output,
upload_storage,
metadata={
metadata = file.get("metadata", {})
metadata.update(
{
"filename": file["filename"] + f".thumbnail{width}x{height}{ext}",
"content_type": content_type,
"width": width,
"height": height,
},
}
)
stored_file = file.store_content(
output,
upload_storage,
metadata=metadata,
)
file.update(
{
Expand Down
28 changes: 28 additions & 0 deletions tests/test_result_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ def test_single_column_is_dictlike(self) -> None:
assert attachment.content.dummy_attr == "Dummy data"
assert "del_attr" not in attachment.content

def test_file_custom_attributes(self) -> None:
with Session(engine) as session:
content = File(
DummyFile(), custom_key1="custom_value1", custom_key2="custom_value2"
)
attachment = Attachment(name="Custom attributes", content=content)
session.add(attachment)
session.commit()
attachment = session.execute(
select(Attachment).where(Attachment.name == "Custom attributes")
).scalar_one()
assert attachment.content["custom_key1"] == "custom_value1"
assert attachment.content["custom_key2"] == "custom_value2"
assert attachment.content.custom_key1 == "custom_value1"
assert attachment.content.custom_key2 == "custom_value2"

def test_file_additional_metadata(self) -> None:
with Session(engine) as session:
content = File(DummyFile(), metadata={"key1": "val1", "key2": "val2"})
attachment = Attachment(name="Additional metadata", content=content)
session.add(attachment)
session.commit()
attachment = session.execute(
select(Attachment).where(Attachment.name == "Additional metadata")
).scalar_one()
assert attachment.content.file.object.meta_data["key1"] == "val1"
assert attachment.content.file.object.meta_data["key2"] == "val2"

def test_multiple_column_is_list_of_dictlike(self) -> None:
with Session(engine) as session:
attachment = Attachment(
Expand Down

0 comments on commit 9bbc57a

Please sign in to comment.