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.
@@ -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,3 @@
1
+ # file-keeper
2
+
3
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()
@@ -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}"