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.
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/.gitignore +3 -0
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/PKG-INFO +53 -35
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/README.md +37 -6
- sqlalchemy_file-0.7.0/pyproject.toml +210 -0
- sqlalchemy_file-0.7.0/sqlalchemy_file/__init__.py +5 -0
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/base.py +2 -3
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/file.py +41 -22
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/helpers.py +19 -16
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/mutable_list.py +18 -13
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/processors.py +46 -29
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/storage.py +41 -17
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/stored_file.py +14 -10
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/types.py +71 -65
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/validators.py +25 -13
- sqlalchemy_file-0.5.0/pyproject.toml +0 -177
- sqlalchemy_file-0.5.0/sqlalchemy_file/__init__.py +0 -4
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/LICENSE +0 -0
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/exceptions.py +0 -0
- {sqlalchemy_file-0.5.0 → sqlalchemy_file-0.7.0}/sqlalchemy_file/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlalchemy-file
|
|
3
|
-
Version: 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.
|
|
29
|
-
Requires-Dist: apache-libcloud
|
|
30
|
-
Requires-Dist: sqlalchemy
|
|
31
|
-
Provides-Extra:
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Provides-Extra:
|
|
35
|
-
Requires-Dist:
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
108
|
+
### uv
|
|
124
109
|
|
|
125
110
|
```shell
|
|
126
|
-
$
|
|
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
|
-
|
|
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
|
-
###
|
|
69
|
+
### uv
|
|
72
70
|
|
|
73
71
|
```shell
|
|
74
|
-
$
|
|
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"]
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import typing
|
|
2
1
|
from typing import Any
|
|
3
2
|
|
|
4
3
|
|
|
5
|
-
class BaseFile(
|
|
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
|
|
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
|
|
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:
|
|
44
|
-
content_type:
|
|
45
|
-
|
|
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.
|
|
55
|
-
|
|
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":
|
|
75
|
+
"size": size,
|
|
64
76
|
"files": [],
|
|
65
77
|
}
|
|
66
78
|
)
|
|
67
79
|
self._thaw()
|
|
68
80
|
|
|
69
|
-
def apply_validators(self, validators:
|
|
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:
|
|
77
|
-
upload_storage:
|
|
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:
|
|
96
|
+
def save_to_storage(self, upload_storage: str | None = None) -> None:
|
|
85
97
|
"""Save current file into provided `upload_storage`."""
|
|
86
|
-
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=
|
|
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.
|
|
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:
|
|
121
|
-
name:
|
|
122
|
-
metadata:
|
|
123
|
-
extra:
|
|
124
|
-
headers:
|
|
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) ->
|
|
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
|