sqlalchemy-file 0.5.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,9 @@
1
1
  *.pyc
2
2
  env*
3
3
  .mypy_cache
4
+ .ty
5
+ .zensical
6
+ .ruff_cache
4
7
  .vscode
5
8
  .idea
6
9
  poetry.lock
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.5
2
2
  Name: sqlalchemy-file
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: SQLAlchemy-file is a SQLAlchemy extension for attaching files to SQLAlchemy model and uploading them to various storage.
5
5
  Project-URL: Homepage, https://jowilf.github.io/sqlalchemy-file
6
6
  Project-URL: Documentation, https://jowilf.github.io/sqlalchemy-file
@@ -17,37 +17,24 @@ Classifier: Intended Audience :: Developers
17
17
  Classifier: License :: OSI Approved :: MIT License
18
18
  Classifier: Operating System :: OS Independent
19
19
  Classifier: Programming Language :: Python
20
- Classifier: Programming Language :: Python :: 3.7
21
- Classifier: Programming Language :: Python :: 3.8
22
- Classifier: Programming Language :: Python :: 3.9
23
- Classifier: Programming Language :: Python :: 3.10
24
20
  Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
25
25
  Classifier: Topic :: Database
26
26
  Classifier: Topic :: Database :: Database Engines/Servers
27
27
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
- Requires-Python: >=3.7
29
- Requires-Dist: apache-libcloud<3.8,>=3.6
30
- Requires-Dist: sqlalchemy<2.1,>=1.4
31
- Provides-Extra: dev
32
- Requires-Dist: pre-commit<4.0.0,>=2.20.0; extra == 'dev'
33
- Requires-Dist: uvicorn<0.22.0,>=0.20.0; extra == 'dev'
34
- Provides-Extra: doc
35
- Requires-Dist: mkdocs-material<10.0.0,>=9.0.0; extra == 'doc'
36
- Requires-Dist: mkdocstrings[python]<0.23.0,>=0.19.0; extra == 'doc'
37
- Provides-Extra: test
38
- Requires-Dist: black==23.7.0; extra == 'test'
39
- Requires-Dist: coverage<7.3.0,>=7.0.0; extra == 'test'
40
- Requires-Dist: fastapi<0.96,>=0.92; extra == 'test'
41
- Requires-Dist: fasteners==0.18; extra == 'test'
42
- Requires-Dist: flask-sqlalchemy<3.1,>=3.0; extra == 'test'
43
- Requires-Dist: flask<2.3,>=2.2; extra == 'test'
44
- Requires-Dist: mypy==1.4.1; extra == 'test'
45
- Requires-Dist: pillow<10.1.0,>=9.4.0; extra == 'test'
46
- Requires-Dist: psycopg2-binary<3.0.0,>=2.9.5; extra == 'test'
47
- Requires-Dist: pymysql[rsa]<1.1.0,>=1.0.2; extra == 'test'
48
- Requires-Dist: pytest<7.5.0,>=7.2.0; extra == 'test'
49
- Requires-Dist: python-multipart==0.0.6; extra == 'test'
50
- Requires-Dist: ruff==0.0.278; extra == 'test'
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: apache-libcloud>=3.6
30
+ Requires-Dist: sqlalchemy>=2.0
31
+ Provides-Extra: all
32
+ Requires-Dist: fasteners>=0.15; extra == 'all'
33
+ Requires-Dist: pillow>=9.4; extra == 'all'
34
+ Provides-Extra: local
35
+ Requires-Dist: fasteners>=0.15; extra == 'local'
36
+ Provides-Extra: pillow
37
+ Requires-Dist: pillow>=9.4; extra == 'pillow'
51
38
  Description-Content-Type: text/markdown
52
39
 
53
40
  # sqlalchemy-file
@@ -105,9 +92,7 @@ The key features are:
105
92
 
106
93
  ## Requirements
107
94
 
108
- A recent and currently supported version of Python (right
109
- now, <a href="https://www.python.org/downloads/" class="external-link" target="_blank">Python supports versions 3.7 and
110
- above</a>).
95
+ Python 3.11 or above.
111
96
 
112
97
  As **SQLAlchemy-file** is based on **Apache Libcloud** and **SQLAlchemy**, it requires them. They will be automatically
113
98
  installed when you install SQLAlchemy-file.
@@ -120,12 +105,33 @@ installed when you install SQLAlchemy-file.
120
105
  $ pip install sqlalchemy-file
121
106
  ```
122
107
 
123
- ### Poetry
108
+ ### uv
124
109
 
125
110
  ```shell
126
- $ poetry add sqlalchemy-file
111
+ $ uv add sqlalchemy-file
127
112
  ```
128
113
 
114
+ ### Optional dependencies (extras)
115
+
116
+ Some features need third-party packages that are **not** installed by default:
117
+
118
+ | Extra | Installs | Required for |
119
+ |--------------------------------|---------------------------------------------|---------------------------------------------------------------------|
120
+ | `sqlalchemy-file[pillow]` | `Pillow` | `ImageField`, `ImageValidator`, `ThumbnailGenerator` |
121
+ | `sqlalchemy-file[local]` | `fasteners` | `LocalStorageDriver` backend (required by libcloud's local storage) |
122
+ | `sqlalchemy-file[all]` | `Pillow` + `fasteners` | Everything above |
123
+
124
+ ```shell
125
+ $ pip install "sqlalchemy-file[local,pillow]"
126
+ ```
127
+
128
+ ```shell
129
+ $ uv add "sqlalchemy-file[all]"
130
+ ```
131
+
132
+ Without the matching extra you get an explicit `ImportError` telling you which extra to install
133
+ (e.g. use `ImageField` without `Pillow`, or the local storage driver without `fasteners`).
134
+
129
135
  ## Example
130
136
 
131
137
  Attaching files to models is as simple as declaring a field on the model itself
@@ -164,13 +170,25 @@ engine = create_engine(
164
170
  Base.metadata.create_all(engine)
165
171
 
166
172
  with Session(engine) as session, open("./example.txt", "rb") as local_file:
173
+ # from an opened local file
167
174
  session.add(Attachment(name="attachment1", content=local_file))
175
+
176
+ # from bytes
168
177
  session.add(Attachment(name="attachment2", content=b"Hello world"))
178
+
179
+ # from string
169
180
  session.add(Attachment(name="attachment3", content="Hello world"))
181
+
182
+ # from a File object with custom filename and content_type
170
183
  file = File(content="Hello World", filename="hello.txt", content_type="text/plain")
171
184
  session.add(Attachment(name="attachment4", content=file))
172
- session.commit()
173
185
 
186
+ # from a File object specifying a content path
187
+ session.add(
188
+ Attachment(name="attachment5", content=File(content_path="./example.txt"))
189
+ )
190
+
191
+ session.commit()
174
192
  ```
175
193
 
176
194
  ## Related projects and inspirations
@@ -53,9 +53,7 @@ The key features are:
53
53
 
54
54
  ## Requirements
55
55
 
56
- A recent and currently supported version of Python (right
57
- now, <a href="https://www.python.org/downloads/" class="external-link" target="_blank">Python supports versions 3.7 and
58
- above</a>).
56
+ Python 3.11 or above.
59
57
 
60
58
  As **SQLAlchemy-file** is based on **Apache Libcloud** and **SQLAlchemy**, it requires them. They will be automatically
61
59
  installed when you install SQLAlchemy-file.
@@ -68,12 +66,33 @@ installed when you install SQLAlchemy-file.
68
66
  $ pip install sqlalchemy-file
69
67
  ```
70
68
 
71
- ### Poetry
69
+ ### uv
72
70
 
73
71
  ```shell
74
- $ poetry add sqlalchemy-file
72
+ $ uv add sqlalchemy-file
75
73
  ```
76
74
 
75
+ ### Optional dependencies (extras)
76
+
77
+ Some features need third-party packages that are **not** installed by default:
78
+
79
+ | Extra | Installs | Required for |
80
+ |--------------------------------|---------------------------------------------|---------------------------------------------------------------------|
81
+ | `sqlalchemy-file[pillow]` | `Pillow` | `ImageField`, `ImageValidator`, `ThumbnailGenerator` |
82
+ | `sqlalchemy-file[local]` | `fasteners` | `LocalStorageDriver` backend (required by libcloud's local storage) |
83
+ | `sqlalchemy-file[all]` | `Pillow` + `fasteners` | Everything above |
84
+
85
+ ```shell
86
+ $ pip install "sqlalchemy-file[local,pillow]"
87
+ ```
88
+
89
+ ```shell
90
+ $ uv add "sqlalchemy-file[all]"
91
+ ```
92
+
93
+ Without the matching extra you get an explicit `ImportError` telling you which extra to install
94
+ (e.g. use `ImageField` without `Pillow`, or the local storage driver without `fasteners`).
95
+
77
96
  ## Example
78
97
 
79
98
  Attaching files to models is as simple as declaring a field on the model itself
@@ -112,13 +131,25 @@ engine = create_engine(
112
131
  Base.metadata.create_all(engine)
113
132
 
114
133
  with Session(engine) as session, open("./example.txt", "rb") as local_file:
134
+ # from an opened local file
115
135
  session.add(Attachment(name="attachment1", content=local_file))
136
+
137
+ # from bytes
116
138
  session.add(Attachment(name="attachment2", content=b"Hello world"))
139
+
140
+ # from string
117
141
  session.add(Attachment(name="attachment3", content="Hello world"))
142
+
143
+ # from a File object with custom filename and content_type
118
144
  file = File(content="Hello World", filename="hello.txt", content_type="text/plain")
119
145
  session.add(Attachment(name="attachment4", content=file))
120
- session.commit()
121
146
 
147
+ # from a File object specifying a content path
148
+ session.add(
149
+ Attachment(name="attachment5", content=File(content_path="./example.txt"))
150
+ )
151
+
152
+ session.commit()
122
153
  ```
123
154
 
124
155
  ## Related projects and inspirations
@@ -0,0 +1,210 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sqlalchemy-file"
7
+ description = "SQLAlchemy-file is a SQLAlchemy extension for attaching files to SQLAlchemy model and uploading them to various storage."
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ requires-python = ">=3.11"
11
+ keywords = ["sqlalchemy", "sqlmodel", "file-upload", "apache-libcloud"]
12
+ authors = [
13
+ { name = "Jocelin Hounon", email = "hounonj@gmail.com" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Web Environment",
18
+ "Framework :: AsyncIO",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Programming Language :: Python :: 3.15",
28
+ "Topic :: Database",
29
+ "Topic :: Database :: Database Engines/Servers",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ ]
32
+ dependencies = [
33
+ "SQLAlchemy >=2.0",
34
+ "apache-libcloud >=3.6",
35
+ ]
36
+ dynamic = ["version"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://jowilf.github.io/sqlalchemy-file"
40
+ Documentation = "https://jowilf.github.io/sqlalchemy-file"
41
+ Repository = "https://github.com/jowilf/sqlalchemy-file"
42
+ Changelog = "https://jowilf.github.io/sqlalchemy-file/changelog/"
43
+
44
+ [project.optional-dependencies]
45
+ # Optional feature dependencies. Install with e.g.
46
+ # `pip install sqlalchemy-file[local,pillow]`.
47
+ pillow = [
48
+ # Required by ImageField, ImageValidator and ThumbnailGenerator.
49
+ "Pillow >=9.4",
50
+ ]
51
+ local = [
52
+ # Required by libcloud's LocalStorageDriver backend.
53
+ "fasteners >=0.15",
54
+ ]
55
+ all = [
56
+ "Pillow >=9.4",
57
+ "fasteners >=0.15",
58
+ ]
59
+ # Dev tooling in PEP 735 groups, managed by uv. Tasks in [tool.poe.tasks];
60
+ # run with `uv run poe <task>`.
61
+ [dependency-groups]
62
+ dev = [
63
+ "poethepoet >=0.30.0, <0.49.0",
64
+ "pre-commit >=2.20.0",
65
+ "uvicorn >=0.20.0",
66
+ ]
67
+ lint = [
68
+ "ruff ==0.17.1",
69
+ "ty ==0.0.89",
70
+ ]
71
+ test = [
72
+ "pytest >=8",
73
+ "testcontainers[mysql,postgres,minio] >=4.6",
74
+ "fasteners >=0.15",
75
+ "PyMySQL >=1.0.2",
76
+ "psycopg2-binary >=2.9.5, <3.0.0",
77
+ "Pillow >=9.4",
78
+ "python-multipart >=0.0.6",
79
+ "sqlmodel >=0.0.11",
80
+ "fastapi >=0.92",
81
+ "Flask >=2.2",
82
+ "Flask-SQLAlchemy >=3.0",
83
+ { include-group = "lint" },
84
+ ]
85
+ cov = [
86
+ "coverage[toml] >=7.4.0",
87
+ ]
88
+ docs = [
89
+ "mkdocstrings[python] >=0.27.0",
90
+ "zensical ==0.0.71",
91
+ ]
92
+ # Mutually exclusive SQLA test environments, see [tool.uv] conflicts.
93
+ sqla20 = [
94
+ "SQLAlchemy >=2.0, <2.1",
95
+ ]
96
+ sqla21 = [
97
+ "SQLAlchemy >=2.1",
98
+ ]
99
+
100
+ [tool.uv]
101
+ conflicts = [
102
+ [
103
+ { group = "sqla20" },
104
+ { group = "sqla21" },
105
+ ],
106
+ ]
107
+ # sqla21 default so plain `uv sync` tests the 2.1 series. sqla20 is opt-in.
108
+ # conflicting groups require `uv run --no-default-groups --group sqla20 ...`.
109
+ default-groups = ["dev", "lint", "test", "cov", "docs", "sqla21"]
110
+
111
+ [tool.poe.tasks]
112
+ # Code quality
113
+ format = ["_ruff-fix", "_ruff-format"]
114
+ lint = ["_ty", "_ruff-check", "_ruff-format-check"]
115
+ _ty = "ty check sqlalchemy_file"
116
+ _ruff-check = "ruff check sqlalchemy_file tests"
117
+ _ruff-fix = "ruff check sqlalchemy_file tests --fix"
118
+ _ruff-format = "ruff format sqlalchemy_file tests docs_src examples"
119
+ _ruff-format-check = "ruff format sqlalchemy_file tests docs_src examples --check"
120
+
121
+ # Tests (sqla tasks re-sync their pinned group)
122
+ test = "coverage run -m pytest tests"
123
+ test-sqla20 = "uv run --no-default-groups --group sqla20 --group test --group cov coverage run -m pytest tests"
124
+ test-sqla21 = "uv run --group sqla21 coverage run -m pytest tests"
125
+ test-matrix = ["test-sqla20", "test-sqla21"]
126
+
127
+ # Coverage
128
+ cov-report = ["_cov-combine", "_cov-report", "_cov-xml"]
129
+ _cov-combine = "coverage combine"
130
+ _cov-report = "coverage report --show-missing"
131
+ _cov-xml = "coverage xml"
132
+
133
+ # Docs
134
+ docs-serve = "zensical serve -a localhost:8080"
135
+ docs-build = "zensical build"
136
+
137
+ # Clean
138
+ clean = "rm -rf .coverage .coverage.* coverage.xml .pytest_cache .ruff_cache .mypy_cache site .zensical"
139
+
140
+ [tool.hatch.version]
141
+ path = "sqlalchemy_file/__init__.py"
142
+
143
+ [tool.pytest.ini_options]
144
+ filterwarnings = [
145
+ "ignore:datetime.datetime.utcnow\\(\\) is deprecated:DeprecationWarning",
146
+ ]
147
+
148
+ [tool.coverage.report]
149
+ fail_under = 99
150
+ show_missing = true
151
+ exclude_lines = [
152
+ "pragma: no cover",
153
+ "if TYPE_CHECKING:",
154
+ "except ImportError"
155
+ ]
156
+
157
+ [tool.coverage.run]
158
+ parallel = true
159
+ command_line = "-m pytest"
160
+ source = ["sqlalchemy_file", "tests"]
161
+
162
+ [tool.ruff]
163
+ target-version = "py311"
164
+
165
+ [tool.ruff.lint]
166
+ select = [
167
+ "B", # flake8-bugbear
168
+ "C4", # flake8-comprehensions
169
+ "C90", # mccabe
170
+ "D", # pydocstyle
171
+ "E", # pycodestyle errors
172
+ "ERA", # eradicate
173
+ "F", # pyflakes
174
+ "I", # isort
175
+ "N", # pep8-naming
176
+ "PIE", # flake8-pie,
177
+ "PLC", # pylint - convention
178
+ "PLE", # pylint - error
179
+ "PLW", # pylint - warning
180
+ "Q", # flake8-quotes
181
+ "RET", # flake8-return,
182
+ "RUF", # Ruff-specific rules
183
+ "SIM", # flake8-simplify
184
+ "UP", # pyupgrade
185
+ "W", # pycodestyle warnings
186
+ ]
187
+ ignore = ["E501", "B904", "B008", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D205"]
188
+
189
+ [tool.ruff.lint.pydocstyle]
190
+ convention = "google"
191
+
192
+ [tool.ruff.lint.isort]
193
+ known-third-party = ["sqlalchemy_file"]
194
+
195
+ [tool.ruff.lint.per-file-ignores]
196
+ "__init__.py" = ["F401"]
197
+ "docs_src/**" = ["N999"]
198
+ # Test fixtures intentionally keep temp files open for the test body.
199
+ "tests/**" = ["SIM115"]
200
+
201
+ [tool.ty.src]
202
+ include = ["sqlalchemy_file"]
203
+
204
+ [tool.ty.rules]
205
+ unused-ignore-comment = "error"
206
+ unused-type-ignore-comment = "error"
207
+
208
+ [tool.hatch.build.targets.wheel]
209
+ [tool.hatch.build.targets.sdist]
210
+ include = ["sqlalchemy_file"]
@@ -0,0 +1,5 @@
1
+ __version__ = "0.7.0"
2
+
3
+ from .file import File as File
4
+ from .types import FileField as FileField
5
+ from .types import ImageField as ImageField
@@ -1,8 +1,7 @@
1
- import typing
2
1
  from typing import Any
3
2
 
4
3
 
5
- class BaseFile(typing.Dict[str, Any]):
4
+ class BaseFile(dict[str, Any]):
6
5
  """Base class for file object.
7
6
 
8
7
  It keeps information on a content related to a specific storage.
@@ -37,7 +36,7 @@ class BaseFile(typing.Dict[str, Any]):
37
36
  raise AttributeError(name)
38
37
 
39
38
  def __delitem__(self, key: str) -> None:
40
- if object.__getattribute__(self, "_frozen"):
39
+ if getattr(self, "_frozen", False):
41
40
  raise TypeError("Already saved files are immutable")
42
41
  dict.__delitem__(self, key)
43
42
 
@@ -1,7 +1,9 @@
1
+ import copy
2
+ import os.path
1
3
  import uuid
2
4
  import warnings
3
- from datetime import datetime
4
- from typing import Any, Dict, List, Optional
5
+ from datetime import UTC, datetime
6
+ from typing import Any
5
7
 
6
8
  from sqlalchemy_file.base import BaseFile
7
9
  from sqlalchemy_file.helpers import (
@@ -39,11 +41,14 @@ class File(BaseFile):
39
41
 
40
42
  def __init__(
41
43
  self,
42
- content: Any,
43
- filename: Optional[str] = None,
44
- content_type: Optional[str] = None,
45
- **kwargs: Dict[str, Any],
44
+ content: Any = None,
45
+ filename: str | None = None,
46
+ content_type: str | None = None,
47
+ content_path: str | None = None,
48
+ **kwargs: Any,
46
49
  ) -> None:
50
+ if content is None and content_path is None:
51
+ raise ValueError("Either content or content_path must be specified")
47
52
  super().__init__(**kwargs)
48
53
  if isinstance(content, dict):
49
54
  object.__setattr__(self, "original_content", None)
@@ -51,8 +56,15 @@ class File(BaseFile):
51
56
  self.update(content)
52
57
  self._freeze()
53
58
  else:
54
- self.original_content = get_content_from_file_obj(content)
55
- filename = filename or get_filename_from_fileob(content)
59
+ self.content_path = content_path
60
+ if content_path is not None:
61
+ self.original_content = None
62
+ filename = filename or os.path.basename(content_path)
63
+ size = os.path.getsize(content_path)
64
+ else:
65
+ self.original_content = get_content_from_file_obj(content)
66
+ filename = filename or get_filename_from_fileob(content)
67
+ size = get_content_size_from_fileobj(self.original_content)
56
68
  content_type = content_type or get_content_type_from_fileobj(
57
69
  content, filename
58
70
  )
@@ -60,30 +72,32 @@ class File(BaseFile):
60
72
  {
61
73
  "filename": filename,
62
74
  "content_type": content_type,
63
- "size": get_content_size_from_fileobj(self.original_content),
75
+ "size": size,
64
76
  "files": [],
65
77
  }
66
78
  )
67
79
  self._thaw()
68
80
 
69
- def apply_validators(self, validators: List[Validator], key: str = "") -> None:
81
+ def apply_validators(self, validators: list[Validator], key: str = "") -> None:
70
82
  """Apply validators to current file."""
71
83
  for validator in validators:
72
84
  validator.process(self, key)
73
85
 
74
86
  def apply_processors(
75
87
  self,
76
- processors: List[Processor],
77
- upload_storage: Optional[str] = None,
88
+ processors: list[Processor],
89
+ upload_storage: str | None = None,
78
90
  ) -> None:
79
91
  """Apply processors to current file."""
80
92
  for processor in processors:
81
93
  processor.process(self, upload_storage)
82
94
  self._freeze()
83
95
 
84
- def save_to_storage(self, upload_storage: Optional[str] = None) -> None:
96
+ def save_to_storage(self, upload_storage: str | None = None) -> None:
85
97
  """Save current file into provided `upload_storage`."""
86
- extra = self.get("extra", {})
98
+ # Deep copy `extra`: column-level config must not leak between files.
99
+ extra = copy.deepcopy(self.get("extra", None) or {})
100
+
87
101
  extra.update({"content_type": self.content_type})
88
102
 
89
103
  metadata = self.get("metadata", None)
@@ -91,7 +105,7 @@ class File(BaseFile):
91
105
  warnings.warn(
92
106
  'metadata attribute is deprecated. Use extra={"meta_data": ...} instead',
93
107
  DeprecationWarning,
94
- stacklevel=1,
108
+ stacklevel=2,
95
109
  )
96
110
  extra.update({"meta_data": metadata})
97
111
 
@@ -101,15 +115,17 @@ class File(BaseFile):
101
115
  extra["meta_data"].update(
102
116
  {"filename": self.filename, "content_type": self.content_type}
103
117
  )
118
+ self["extra"] = extra
104
119
  stored_file = self.store_content(
105
120
  self.original_content,
106
121
  upload_storage,
107
122
  extra=extra,
108
123
  headers=self.get("headers", None),
124
+ content_path=self.content_path,
109
125
  )
110
126
  self["file_id"] = stored_file.name
111
127
  self["upload_storage"] = upload_storage
112
- self["uploaded_at"] = datetime.utcnow().isoformat()
128
+ self["uploaded_at"] = datetime.now(UTC).isoformat()
113
129
  self["path"] = f"{upload_storage}/{stored_file.name}"
114
130
  self["url"] = stored_file.get_cdn_url()
115
131
  self["saved"] = True
@@ -117,17 +133,19 @@ class File(BaseFile):
117
133
  def store_content(
118
134
  self,
119
135
  content: Any,
120
- upload_storage: Optional[str] = None,
121
- name: Optional[str] = None,
122
- metadata: Optional[Dict[str, Any]] = None,
123
- extra: Optional[Dict[str, Any]] = None,
124
- headers: Optional[Dict[str, str]] = None,
136
+ upload_storage: str | None = None,
137
+ name: str | None = None,
138
+ metadata: dict[str, Any] | None = None,
139
+ extra: dict[str, Any] | None = None,
140
+ headers: dict[str, str] | None = None,
141
+ content_path: str | None = None,
125
142
  ) -> StoredFile:
126
143
  """Store content into provided `upload_storage`
127
144
  with additional `metadata`. Can be used by processors
128
145
  to store additional files.
129
146
  """
130
147
  name = name or str(uuid.uuid4())
148
+ upload_storage = upload_storage or StorageManager.get_default()
131
149
  stored_file = StorageManager.save_file(
132
150
  name=name,
133
151
  content=content,
@@ -135,11 +153,12 @@ class File(BaseFile):
135
153
  metadata=metadata,
136
154
  extra=extra,
137
155
  headers=headers,
156
+ content_path=content_path,
138
157
  )
139
158
  self["files"].append(f"{upload_storage}/{name}")
140
159
  return stored_file
141
160
 
142
- def encode(self) -> Dict[str, Any]:
161
+ def encode(self) -> dict[str, Any]:
143
162
  return {k: v for k, v in self.items() if k not in ["original_content"]}
144
163
 
145
164
  @classmethod