file-keeper 0.0.1__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.
- file_keeper-0.0.1/PKG-INFO +46 -0
- file_keeper-0.0.1/README.md +3 -0
- file_keeper-0.0.1/pyproject.toml +208 -0
- file_keeper-0.0.1/setup.cfg +4 -0
- file_keeper-0.0.1/setup.py +3 -0
- file_keeper-0.0.1/src/file_keeper/__init__.py +48 -0
- file_keeper-0.0.1/src/file_keeper/core/__init__.py +0 -0
- file_keeper-0.0.1/src/file_keeper/core/data.py +107 -0
- file_keeper-0.0.1/src/file_keeper/core/exceptions.py +275 -0
- file_keeper-0.0.1/src/file_keeper/core/registry.py +71 -0
- file_keeper-0.0.1/src/file_keeper/core/storage.py +632 -0
- file_keeper-0.0.1/src/file_keeper/core/types.py +30 -0
- file_keeper-0.0.1/src/file_keeper/core/upload.py +146 -0
- file_keeper-0.0.1/src/file_keeper/core/utils.py +262 -0
- file_keeper-0.0.1/src/file_keeper/default/__init__.py +133 -0
- file_keeper-0.0.1/src/file_keeper/default/adapters/__init__.py +5 -0
- file_keeper-0.0.1/src/file_keeper/default/adapters/fs.py +490 -0
- file_keeper-0.0.1/src/file_keeper/ext/__init__.py +30 -0
- file_keeper-0.0.1/src/file_keeper/ext/spec.py +27 -0
- file_keeper-0.0.1/src/file_keeper.egg-info/PKG-INFO +46 -0
- file_keeper-0.0.1/src/file_keeper.egg-info/SOURCES.txt +23 -0
- file_keeper-0.0.1/src/file_keeper.egg-info/dependency_links.txt +1 -0
- file_keeper-0.0.1/src/file_keeper.egg-info/entry_points.txt +2 -0
- file_keeper-0.0.1/src/file_keeper.egg-info/requires.txt +38 -0
- file_keeper-0.0.1/src/file_keeper.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: file-keeper
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Author-email: DataShades <datashades@linkdigital.com.au>, Sergey Motornyuk <sergey.motornyuk@linkdigital.com.au>
|
|
5
|
+
Maintainer-email: DataShades <datashades@linkdigital.com.au>
|
|
6
|
+
License: AGPL
|
|
7
|
+
Project-URL: Homepage, https://github.com/DataShades/file-keeper
|
|
8
|
+
Keywords: file-management,cloud,filesystem,DAL
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: typing_extensions
|
|
13
|
+
Requires-Dist: pluggy
|
|
14
|
+
Requires-Dist: pytz
|
|
15
|
+
Requires-Dist: python-magic
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-faker; extra == "test"
|
|
19
|
+
Requires-Dist: responses; extra == "test"
|
|
20
|
+
Provides-Extra: docs
|
|
21
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
22
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
23
|
+
Requires-Dist: pymdown-extensions; extra == "docs"
|
|
24
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-faker; extra == "dev"
|
|
28
|
+
Requires-Dist: responses; extra == "dev"
|
|
29
|
+
Requires-Dist: mkdocs; extra == "dev"
|
|
30
|
+
Requires-Dist: mkdocs-material; extra == "dev"
|
|
31
|
+
Requires-Dist: pymdown-extensions; extra == "dev"
|
|
32
|
+
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
33
|
+
Provides-Extra: s3
|
|
34
|
+
Requires-Dist: boto3; extra == "s3"
|
|
35
|
+
Requires-Dist: boto3-stubs[essential]; extra == "s3"
|
|
36
|
+
Provides-Extra: gcs
|
|
37
|
+
Requires-Dist: google-cloud-storage; extra == "gcs"
|
|
38
|
+
Provides-Extra: opendal
|
|
39
|
+
Requires-Dist: opendal; extra == "opendal"
|
|
40
|
+
Provides-Extra: libcloud
|
|
41
|
+
Requires-Dist: apache-libcloud; extra == "libcloud"
|
|
42
|
+
Requires-Dist: cryptography; extra == "libcloud"
|
|
43
|
+
|
|
44
|
+
# file-keeper
|
|
45
|
+
|
|
46
|
+
Abstraction level for object storages.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "setuptools",]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools.dynamic]
|
|
6
|
+
version = {attr = "file_keeper.__version__"}
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "file-keeper"
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
description = ""
|
|
12
|
+
classifiers = [
|
|
13
|
+
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
|
14
|
+
]
|
|
15
|
+
keywords = [ "file-management", "cloud", "filesystem", "DAL" ]
|
|
16
|
+
requires-python = ">= 3.8"
|
|
17
|
+
dependencies = [ "typing_extensions", "pluggy", "pytz", "python-magic" ]
|
|
18
|
+
authors = [
|
|
19
|
+
{name = "DataShades", email = "datashades@linkdigital.com.au"},
|
|
20
|
+
{name = "Sergey Motornyuk", email = "sergey.motornyuk@linkdigital.com.au"},
|
|
21
|
+
]
|
|
22
|
+
maintainers = [
|
|
23
|
+
{name = "DataShades", email = "datashades@linkdigital.com.au"},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.readme]
|
|
27
|
+
file = "README.md"
|
|
28
|
+
content-type = "text/markdown"
|
|
29
|
+
|
|
30
|
+
[project.license]
|
|
31
|
+
text = "AGPL"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/DataShades/file-keeper"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
test = [ "pytest-cov", "pytest-faker", "responses" ]
|
|
38
|
+
docs = [ "mkdocs", "mkdocs-material", "pymdown-extensions", "mkdocstrings[python]",]
|
|
39
|
+
dev = [ "pytest-cov", "pytest-faker", "responses", "mkdocs", "mkdocs-material", "pymdown-extensions", "mkdocstrings[python]",]
|
|
40
|
+
|
|
41
|
+
s3 = [ "boto3", "boto3-stubs[essential]"]
|
|
42
|
+
gcs = [ "google-cloud-storage",]
|
|
43
|
+
opendal = [ "opendal",]
|
|
44
|
+
libcloud = [ "apache-libcloud", "cryptography",]
|
|
45
|
+
|
|
46
|
+
[project.entry-points."file_keeper_ext"]
|
|
47
|
+
default = "file_keeper.default"
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
target-version = "py38"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = [
|
|
54
|
+
"ANN0", # type annotations for function arguments
|
|
55
|
+
"B", # likely bugs and design problems
|
|
56
|
+
"BLE", # do not catch blind exception
|
|
57
|
+
"C4", # better list/set/dict comprehensions
|
|
58
|
+
"C90", # check McCabe complexity
|
|
59
|
+
"DTZ", # enforce timezone in date objects
|
|
60
|
+
"E", # pycodestyle error
|
|
61
|
+
"W", # pycodestyle warning
|
|
62
|
+
"F", # pyflakes
|
|
63
|
+
"FA", # verify annotations from future
|
|
64
|
+
"G", # format strings for logging statements
|
|
65
|
+
"N", # naming conventions
|
|
66
|
+
"I", # isort
|
|
67
|
+
"ICN", # import conventions
|
|
68
|
+
# "D1", # require doc
|
|
69
|
+
"D2", # doc formatting
|
|
70
|
+
"D4", # doc convention
|
|
71
|
+
"PL", # pylint
|
|
72
|
+
"PERF", # performance anti-patterns
|
|
73
|
+
"PT", # pytest style
|
|
74
|
+
"PIE", # misc lints
|
|
75
|
+
"RET", # improvements for return statements
|
|
76
|
+
"RSE", # improvements for rise statements
|
|
77
|
+
"S", # security testing
|
|
78
|
+
"SIM", # simplify code
|
|
79
|
+
"T10", # debugging statements
|
|
80
|
+
"T20", # print statements
|
|
81
|
+
"TID", # tidier imports
|
|
82
|
+
"TRY", # better exceptions
|
|
83
|
+
"UP", # upgrade syntax for newer versions of the language
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
ignore = [
|
|
87
|
+
"RET503", # don't enforce return-None
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
"tests/*" = ["S", "PL", "ANN", "D"]
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint.pydocstyle]
|
|
94
|
+
convention = "google"
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint.isort]
|
|
97
|
+
section-order = [
|
|
98
|
+
"future",
|
|
99
|
+
"standard-library",
|
|
100
|
+
"first-party",
|
|
101
|
+
"third-party",
|
|
102
|
+
"self",
|
|
103
|
+
"local-folder",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[tool.ruff.lint.isort.sections]
|
|
107
|
+
self = ["file_keeper"]
|
|
108
|
+
|
|
109
|
+
[tool.pytest.ini_options]
|
|
110
|
+
addopts = "-p no:ckan"
|
|
111
|
+
markers = [
|
|
112
|
+
"fk_storage_option: modify storage configuration",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
[tool.git-changelog]
|
|
117
|
+
output = "CHANGELOG.md"
|
|
118
|
+
convention = "conventional"
|
|
119
|
+
parse-trailers = true
|
|
120
|
+
|
|
121
|
+
[tool.pyright]
|
|
122
|
+
pythonVersion = "3.8"
|
|
123
|
+
include = ["."]
|
|
124
|
+
exclude = [
|
|
125
|
+
# "**/test*",
|
|
126
|
+
]
|
|
127
|
+
strict = []
|
|
128
|
+
|
|
129
|
+
strictParameterNoneValue = true
|
|
130
|
+
# Check the meaning of rules here
|
|
131
|
+
# https://github.com/microsoft/pyright/blob/main/docs/configuration.md
|
|
132
|
+
reportFunctionMemberAccess = true # non-standard member accesses for functions
|
|
133
|
+
reportMissingImports = true
|
|
134
|
+
reportMissingModuleSource = true
|
|
135
|
+
reportMissingTypeStubs = false
|
|
136
|
+
reportImportCycles = false
|
|
137
|
+
reportUnusedImport = true
|
|
138
|
+
reportUnusedClass = true
|
|
139
|
+
reportUnusedFunction = true
|
|
140
|
+
reportUnusedVariable = true
|
|
141
|
+
reportDuplicateImport = true
|
|
142
|
+
reportOptionalSubscript = true
|
|
143
|
+
reportOptionalMemberAccess = true
|
|
144
|
+
reportOptionalCall = true
|
|
145
|
+
reportOptionalIterable = true
|
|
146
|
+
reportOptionalContextManager = true
|
|
147
|
+
reportOptionalOperand = true
|
|
148
|
+
reportTypedDictNotRequiredAccess = true
|
|
149
|
+
reportConstantRedefinition = true
|
|
150
|
+
reportIncompatibleMethodOverride = true
|
|
151
|
+
reportIncompatibleVariableOverride = true
|
|
152
|
+
reportOverlappingOverload = true
|
|
153
|
+
reportUntypedFunctionDecorator = false
|
|
154
|
+
reportUnknownParameterType = true
|
|
155
|
+
reportUnknownArgumentType = false
|
|
156
|
+
reportUnknownLambdaType = false
|
|
157
|
+
reportUnknownMemberType = false
|
|
158
|
+
reportMissingTypeArgument = true
|
|
159
|
+
reportInvalidTypeVarUse = true
|
|
160
|
+
reportCallInDefaultInitializer = true
|
|
161
|
+
reportUnknownVariableType = true
|
|
162
|
+
reportUntypedBaseClass = true
|
|
163
|
+
reportUnnecessaryIsInstance = true
|
|
164
|
+
reportUnnecessaryCast = true
|
|
165
|
+
reportUnnecessaryComparison = true
|
|
166
|
+
reportAssertAlwaysTrue = true
|
|
167
|
+
reportSelfClsParameterName = true
|
|
168
|
+
reportUnusedCallResult = false
|
|
169
|
+
useLibraryCodeForTypes = true
|
|
170
|
+
reportGeneralTypeIssues = true
|
|
171
|
+
reportPropertyTypeMismatch = true
|
|
172
|
+
reportWildcardImportFromLibrary = true
|
|
173
|
+
reportUntypedClassDecorator = false
|
|
174
|
+
reportUntypedNamedTuple = true
|
|
175
|
+
reportPrivateUsage = true
|
|
176
|
+
reportPrivateImportUsage = true
|
|
177
|
+
reportInconsistentConstructor = true
|
|
178
|
+
reportMissingSuperCall = false
|
|
179
|
+
reportUninitializedInstanceVariable = true
|
|
180
|
+
reportInvalidStringEscapeSequence = true
|
|
181
|
+
reportMissingParameterType = true
|
|
182
|
+
reportImplicitStringConcatenation = false
|
|
183
|
+
reportUndefinedVariable = true
|
|
184
|
+
reportUnboundVariable = true
|
|
185
|
+
reportInvalidStubStatement = true
|
|
186
|
+
reportIncompleteStub = true
|
|
187
|
+
reportUnsupportedDunderAll = true
|
|
188
|
+
reportUnusedCoroutine = true
|
|
189
|
+
reportUnnecessaryTypeIgnoreComment = true
|
|
190
|
+
reportMatchNotExhaustive = true
|
|
191
|
+
|
|
192
|
+
[tool.coverage.run]
|
|
193
|
+
branch = true
|
|
194
|
+
omit = ["tests/*"]
|
|
195
|
+
|
|
196
|
+
[tool.tox]
|
|
197
|
+
requires = ["tox>=4"]
|
|
198
|
+
env_list = ["3.13", "3.12", "3.11", "3.10", "3.9", "3.8"]
|
|
199
|
+
|
|
200
|
+
[tool.tox.env_run_base]
|
|
201
|
+
|
|
202
|
+
description = "run unit tests"
|
|
203
|
+
deps = [
|
|
204
|
+
"pytest>=8",
|
|
205
|
+
"pytest-sugar",
|
|
206
|
+
]
|
|
207
|
+
commands = [["pytest", { replace = "posargs", default = ["tests"], extend = true }]]
|
|
208
|
+
extras = ["test"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
2
|
+
|
|
3
|
+
from .core import exceptions as exc
|
|
4
|
+
from .core.data import BaseData, FileData, MultipartData
|
|
5
|
+
from .core.registry import Registry
|
|
6
|
+
from .core.storage import (
|
|
7
|
+
Manager,
|
|
8
|
+
Reader,
|
|
9
|
+
Settings,
|
|
10
|
+
Storage,
|
|
11
|
+
Uploader,
|
|
12
|
+
adapters,
|
|
13
|
+
make_storage,
|
|
14
|
+
)
|
|
15
|
+
from .core.upload import Upload, make_upload
|
|
16
|
+
from .core.utils import (
|
|
17
|
+
Capability,
|
|
18
|
+
HashingReader,
|
|
19
|
+
IterableBytesReader,
|
|
20
|
+
humanize_filesize,
|
|
21
|
+
is_supported_type,
|
|
22
|
+
parse_filesize,
|
|
23
|
+
)
|
|
24
|
+
from .ext import hookimpl # must be the last line to avoid circular imports
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"adapters",
|
|
28
|
+
"FileData",
|
|
29
|
+
"BaseData",
|
|
30
|
+
"MultipartData",
|
|
31
|
+
"is_supported_type",
|
|
32
|
+
"Registry",
|
|
33
|
+
"Capability",
|
|
34
|
+
"parse_filesize",
|
|
35
|
+
"humanize_filesize",
|
|
36
|
+
"IterableBytesReader",
|
|
37
|
+
"HashingReader",
|
|
38
|
+
"make_upload",
|
|
39
|
+
"Upload",
|
|
40
|
+
"exc",
|
|
41
|
+
"make_storage",
|
|
42
|
+
"Storage",
|
|
43
|
+
"Reader",
|
|
44
|
+
"Uploader",
|
|
45
|
+
"Manager",
|
|
46
|
+
"Settings",
|
|
47
|
+
"hookimpl",
|
|
48
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Base abstract functionality of the extentsion.
|
|
2
|
+
|
|
3
|
+
All classes required for specific storage implementations are defined
|
|
4
|
+
here. Some utilities, like `make_storage` are also added to this module instead
|
|
5
|
+
of `utils` to avoid import cycles.
|
|
6
|
+
|
|
7
|
+
This module relies only on types, exceptions and utils to prevent import
|
|
8
|
+
cycles.
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import copy
|
|
15
|
+
import dataclasses
|
|
16
|
+
from typing import Any, Generic
|
|
17
|
+
|
|
18
|
+
from typing_extensions import TypeVar
|
|
19
|
+
|
|
20
|
+
from . import types
|
|
21
|
+
|
|
22
|
+
TData = TypeVar("TData", bound=types.PData, default=Any)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass
|
|
26
|
+
class BaseData(Generic[TData]):
|
|
27
|
+
location: str
|
|
28
|
+
size: int = 0
|
|
29
|
+
content_type: str = ""
|
|
30
|
+
hash: str = ""
|
|
31
|
+
storage_data: dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
_plain_keys = ["location", "size", "content_type", "hash"]
|
|
34
|
+
_complex_keys = ["storage_data"]
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, record: dict[str, Any]):
|
|
38
|
+
return cls(
|
|
39
|
+
*[record[key] for key in cls._plain_keys],
|
|
40
|
+
*[copy.deepcopy(record[key]) for key in cls._complex_keys],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_object(cls, obj: TData):
|
|
45
|
+
return cls(
|
|
46
|
+
*[getattr(obj, key) for key in cls._plain_keys],
|
|
47
|
+
*[copy.deepcopy(getattr(obj, key)) for key in cls._complex_keys],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def into_object(self, obj: TData):
|
|
51
|
+
for key in self._plain_keys:
|
|
52
|
+
setattr(obj, key, getattr(self, key))
|
|
53
|
+
|
|
54
|
+
for key in self._complex_keys:
|
|
55
|
+
setattr(obj, key, copy.deepcopy(getattr(self, key)))
|
|
56
|
+
|
|
57
|
+
return obj
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclasses.dataclass
|
|
61
|
+
class FileData(BaseData[TData]):
|
|
62
|
+
"""Information required by storage to operate the file.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
location: filepath, filename or any other type of unique identifier
|
|
66
|
+
size: size of the file in bytes
|
|
67
|
+
content_type: MIMEtype of the file
|
|
68
|
+
hash: checksum of the file
|
|
69
|
+
storage_data: additional details set by storage adapter
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
```
|
|
73
|
+
FileData(
|
|
74
|
+
"local/path.txt",
|
|
75
|
+
123,
|
|
76
|
+
"text/plain",
|
|
77
|
+
md5_of_content,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
content_type: str = "application/octet-stream"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclasses.dataclass
|
|
86
|
+
class MultipartData(BaseData[TData]):
|
|
87
|
+
"""Information required by storage to operate the incomplete upload.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
location: filepath, filename or any other type of unique identifier
|
|
91
|
+
size: expected size of the file in bytes
|
|
92
|
+
content_type: expected MIMEtype of the file
|
|
93
|
+
hash: expected checksum of the file
|
|
94
|
+
storage_data: additional details set by storage adapter
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
```
|
|
98
|
+
MultipartData(
|
|
99
|
+
"local/path.txt",
|
|
100
|
+
expected_size,
|
|
101
|
+
expected_content_type,
|
|
102
|
+
expected_hash,
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
location: str = ""
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Exception definitions for the extension.
|
|
2
|
+
|
|
3
|
+
Hierarchy:
|
|
4
|
+
|
|
5
|
+
* Exception
|
|
6
|
+
* FilesError
|
|
7
|
+
* QueueError
|
|
8
|
+
* OutOfQueueError
|
|
9
|
+
* StorageError
|
|
10
|
+
* UnknownAdapterError
|
|
11
|
+
* UnknownStorageError
|
|
12
|
+
* UnsupportedOperationError
|
|
13
|
+
* PermissionError
|
|
14
|
+
* LocationError
|
|
15
|
+
* MissingFileError
|
|
16
|
+
* ExistingFileError
|
|
17
|
+
* ExtrasError
|
|
18
|
+
* MissingExtrasError
|
|
19
|
+
* InvalidStorageConfigurationError
|
|
20
|
+
* MissingStorageConfigurationError
|
|
21
|
+
* UploadError
|
|
22
|
+
* WrongUploadTypeError
|
|
23
|
+
* LocationStrategyError
|
|
24
|
+
* ContentError
|
|
25
|
+
* LargeUploadError
|
|
26
|
+
* UploadOutOfBoundError
|
|
27
|
+
* UploadMismatchError
|
|
28
|
+
* UploadTypeMismatchError
|
|
29
|
+
* UploadHashMismatchError
|
|
30
|
+
* UploadSizeMismatchError
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
Storage = Any
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FilesError(Exception):
|
|
42
|
+
"""Base error for catch-all scenario."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class QueueError(FilesError):
|
|
46
|
+
"""Error related to task queue."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class OutOfQueueError(QueueError):
|
|
50
|
+
"""Attempt to add task without initializing task queue context."""
|
|
51
|
+
|
|
52
|
+
def __str__(self):
|
|
53
|
+
return "Task queue accessed outside of queue context"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StorageError(FilesError):
|
|
57
|
+
"""Error related to storage."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UnknownStorageError(StorageError):
|
|
61
|
+
"""Storage with the given name is not configured."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, storage: str):
|
|
64
|
+
self.storage = storage
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return f"Storage {self.storage} is not configured"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class UnknownAdapterError(StorageError):
|
|
71
|
+
"""Specified storage adapter is not registered."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, adapter: str):
|
|
74
|
+
self.adapter = adapter
|
|
75
|
+
|
|
76
|
+
def __str__(self):
|
|
77
|
+
return f"Storage adapter {self.adapter} is not registered"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class UnsupportedOperationError(StorageError):
|
|
81
|
+
"""Requested operation is not supported by storage."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, operation: str, storage: Storage):
|
|
84
|
+
self.operation = operation
|
|
85
|
+
self.storage = storage
|
|
86
|
+
|
|
87
|
+
def __str__(self):
|
|
88
|
+
return f"Operation {self.operation} is not supported by {self.storage} storage"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class InvalidStorageConfigurationError(StorageError):
|
|
92
|
+
"""Storage cannot be initialized with given configuration."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, adapter_or_storage: type | str, problem: str):
|
|
95
|
+
self.adapter_or_storage = adapter_or_storage
|
|
96
|
+
self.problem = problem
|
|
97
|
+
|
|
98
|
+
def __str__(self):
|
|
99
|
+
if isinstance(self.adapter_or_storage, str):
|
|
100
|
+
what = f"storage adapter {self.adapter_or_storage}"
|
|
101
|
+
else:
|
|
102
|
+
what = f"storage adapter {self.adapter_or_storage.__name__}"
|
|
103
|
+
|
|
104
|
+
return f"Cannot initialize {what}" + f" due to following error: {self.problem}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PermissionError(StorageError):
|
|
108
|
+
"""Storage client does not have required permissions."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, storage: Storage, operation: str, problem: str):
|
|
111
|
+
self.storage = storage
|
|
112
|
+
self.operation = operation
|
|
113
|
+
self.problem = problem
|
|
114
|
+
|
|
115
|
+
def __str__(self):
|
|
116
|
+
msg = "Storage {} is not allowed to perform {} operation: {}"
|
|
117
|
+
return msg.format(
|
|
118
|
+
self.storage,
|
|
119
|
+
self.operation,
|
|
120
|
+
self.problem,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class MissingStorageConfigurationError(InvalidStorageConfigurationError):
|
|
125
|
+
"""Storage cannot be initialized due to missing option."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, adapter_or_storage: type | str, option: str):
|
|
128
|
+
super().__init__(
|
|
129
|
+
adapter_or_storage,
|
|
130
|
+
f"{option} option is required",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class LocationError(StorageError):
|
|
135
|
+
"""Storage cannot use given location."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, storage: Storage, location: str):
|
|
138
|
+
self.storage = storage
|
|
139
|
+
self.location = location
|
|
140
|
+
|
|
141
|
+
def __str__(self):
|
|
142
|
+
return f"{self.storage} cannot use location {self.location}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class MissingFileError(LocationError):
|
|
146
|
+
"""File does not exist."""
|
|
147
|
+
|
|
148
|
+
def __str__(self):
|
|
149
|
+
return f"File {self.location} does not exist inside storage {self.storage}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ExistingFileError(LocationError):
|
|
153
|
+
"""File already exists."""
|
|
154
|
+
|
|
155
|
+
def __str__(self):
|
|
156
|
+
return f"File {self.location} already exists inside storage {self.storage}"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class UploadError(StorageError):
|
|
160
|
+
"""Error related to file upload process."""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class LargeUploadError(UploadError):
|
|
164
|
+
"""Storage cannot be initialized due to missing option."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, actual_size: int, max_size: int):
|
|
167
|
+
self.actual_size = actual_size
|
|
168
|
+
self.max_size = max_size
|
|
169
|
+
|
|
170
|
+
def __str__(self):
|
|
171
|
+
return (
|
|
172
|
+
f"Upload size {self.actual_size} surpasses"
|
|
173
|
+
+ f" max allowed size {self.max_size}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class UploadOutOfBoundError(LargeUploadError):
|
|
178
|
+
"""Multipart upload exceeds expected size."""
|
|
179
|
+
|
|
180
|
+
def __str__(self):
|
|
181
|
+
return (
|
|
182
|
+
f"Upload size {self.actual_size} exceeds"
|
|
183
|
+
+ f" expected size {self.max_size}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class UploadMismatchError(UploadError):
|
|
188
|
+
"""Expected value of file attribute doesn't match the actual value."""
|
|
189
|
+
|
|
190
|
+
value_formatter = str
|
|
191
|
+
|
|
192
|
+
def __init__(self, attribute: str, actual: Any, expected: Any):
|
|
193
|
+
self.attribute = attribute
|
|
194
|
+
self.actual = actual
|
|
195
|
+
self.expected = expected
|
|
196
|
+
|
|
197
|
+
def __str__(self):
|
|
198
|
+
actual = self.value_formatter(self.actual)
|
|
199
|
+
expected = self.value_formatter(self.expected)
|
|
200
|
+
return (
|
|
201
|
+
f"Actual value of {self.attribute}({actual}) does not"
|
|
202
|
+
+ f" match expected value({expected})"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class UploadTypeMismatchError(UploadMismatchError):
|
|
207
|
+
"""Expected value of content type doesn't match the actual value."""
|
|
208
|
+
|
|
209
|
+
def __init__(self, actual: Any, expected: Any):
|
|
210
|
+
super().__init__("content type", actual, expected)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class UploadHashMismatchError(UploadMismatchError):
|
|
214
|
+
"""Expected value of hash match the actual value."""
|
|
215
|
+
|
|
216
|
+
def __init__(self, actual: Any, expected: Any):
|
|
217
|
+
super().__init__("content hash", actual, expected)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class UploadSizeMismatchError(UploadMismatchError):
|
|
221
|
+
"""Expected value of upload size doesn't match the actual value."""
|
|
222
|
+
|
|
223
|
+
def __init__(self, actual: Any, expected: Any):
|
|
224
|
+
super().__init__("upload size", actual, expected)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class WrongUploadTypeError(UploadError):
|
|
228
|
+
"""Storage does not support given MIMEType."""
|
|
229
|
+
|
|
230
|
+
def __init__(self, content_type: str):
|
|
231
|
+
self.content_type = content_type
|
|
232
|
+
|
|
233
|
+
def __str__(self):
|
|
234
|
+
return f"Type {self.content_type} is not supported by storage"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class LocationStrategyError(UploadError):
|
|
238
|
+
"""Undefined name strategy."""
|
|
239
|
+
|
|
240
|
+
def __init__(self, strategy: str):
|
|
241
|
+
self.strategy = strategy
|
|
242
|
+
|
|
243
|
+
def __str__(self):
|
|
244
|
+
return f"Unknown name strategy {self.strategy}"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ExtrasError(StorageError):
|
|
248
|
+
"""Wrong extras passed during upload."""
|
|
249
|
+
|
|
250
|
+
def __init__(self, extras: Any):
|
|
251
|
+
self.extras = extras
|
|
252
|
+
|
|
253
|
+
def __str__(self):
|
|
254
|
+
return f"Wrong extras: {self.extras}"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class MissingExtrasError(ExtrasError):
|
|
258
|
+
"""Wrong extras passed to storage method."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, key: Any):
|
|
261
|
+
self.key = key
|
|
262
|
+
|
|
263
|
+
def __str__(self):
|
|
264
|
+
return f"Key {self.key} is missing from extras"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ContentError(UploadError):
|
|
268
|
+
"""Storage cannot accept uploaded content."""
|
|
269
|
+
|
|
270
|
+
def __init__(self, storage: Storage, msg: str):
|
|
271
|
+
self.storage = storage
|
|
272
|
+
self.msg = msg
|
|
273
|
+
|
|
274
|
+
def __str__(self):
|
|
275
|
+
return f"{self.storage} rejected upload: {self.msg}"
|