tie-storage 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.
- tie_storage-0.0.1/PKG-INFO +39 -0
- tie_storage-0.0.1/README.md +3 -0
- tie_storage-0.0.1/VERSION +1 -0
- tie_storage-0.0.1/setup.cfg +4 -0
- tie_storage-0.0.1/setup.py +47 -0
- tie_storage-0.0.1/tie_storage/__init__.py +9 -0
- tie_storage-0.0.1/tie_storage/abstract.py +129 -0
- tie_storage-0.0.1/tie_storage/configuration.py +20 -0
- tie_storage-0.0.1/tie_storage/datamodel.py +7 -0
- tie_storage-0.0.1/tie_storage/helper.py +14 -0
- tie_storage-0.0.1/tie_storage/local.py +30 -0
- tie_storage-0.0.1/tie_storage/s3.py +101 -0
- tie_storage-0.0.1/tie_storage.egg-info/PKG-INFO +39 -0
- tie_storage-0.0.1/tie_storage.egg-info/SOURCES.txt +16 -0
- tie_storage-0.0.1/tie_storage.egg-info/dependency_links.txt +1 -0
- tie_storage-0.0.1/tie_storage.egg-info/entry_points.txt +2 -0
- tie_storage-0.0.1/tie_storage.egg-info/requires.txt +7 -0
- tie_storage-0.0.1/tie_storage.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tie-storage
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: TIE storage client library
|
|
5
|
+
Author: ISPRAS TIE NLP team
|
|
6
|
+
Author-email: modis@ispras.ru
|
|
7
|
+
Maintainer: Vladimir Mayorov
|
|
8
|
+
Maintainer-email: vmayorov@ispras.ru
|
|
9
|
+
License: Apache Software License
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: talisman-interfaces~=0.11.0
|
|
19
|
+
Requires-Dist: fsspec
|
|
20
|
+
Provides-Extra: s3
|
|
21
|
+
Requires-Dist: boto3~=1.35.0; extra == "s3"
|
|
22
|
+
Requires-Dist: botocore~=1.35.0; extra == "s3"
|
|
23
|
+
Requires-Dist: s3fs; extra == "s3"
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: license
|
|
30
|
+
Dynamic: maintainer
|
|
31
|
+
Dynamic: maintainer-email
|
|
32
|
+
Dynamic: provides-extra
|
|
33
|
+
Dynamic: requires-dist
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
Dynamic: summary
|
|
36
|
+
|
|
37
|
+
# tie-storage
|
|
38
|
+
|
|
39
|
+
TIE storage client library providing a unified interface for local and S3-compatible storage.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as f:
|
|
4
|
+
long_description = f.read()
|
|
5
|
+
|
|
6
|
+
with open("VERSION", "r", encoding="utf-8") as f:
|
|
7
|
+
version = f.read()
|
|
8
|
+
|
|
9
|
+
setup(
|
|
10
|
+
name='tie-storage',
|
|
11
|
+
version=version,
|
|
12
|
+
description='TIE storage client library',
|
|
13
|
+
long_description=long_description,
|
|
14
|
+
long_description_content_type="text/markdown",
|
|
15
|
+
author='ISPRAS TIE NLP team',
|
|
16
|
+
author_email='modis@ispras.ru',
|
|
17
|
+
maintainer='Vladimir Mayorov',
|
|
18
|
+
maintainer_email='vmayorov@ispras.ru',
|
|
19
|
+
packages=find_packages(include=['tie_storage', 'tie_storage.*']),
|
|
20
|
+
install_requires=[
|
|
21
|
+
'talisman-interfaces~=0.11.0',
|
|
22
|
+
'fsspec'
|
|
23
|
+
],
|
|
24
|
+
extras_require={
|
|
25
|
+
's3': [
|
|
26
|
+
'boto3~=1.35.0',
|
|
27
|
+
'botocore~=1.35.0',
|
|
28
|
+
's3fs'
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
entry_points={
|
|
32
|
+
'talisman.plugins': [
|
|
33
|
+
'storage = tie_storage'
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
data_files=[('', ['VERSION'])],
|
|
37
|
+
python_requires='>=3.10',
|
|
38
|
+
license='Apache Software License',
|
|
39
|
+
classifiers=[
|
|
40
|
+
'Development Status :: 3 - Alpha',
|
|
41
|
+
'Programming Language :: Python :: 3',
|
|
42
|
+
'Programming Language :: Python :: 3.10',
|
|
43
|
+
'Intended Audience :: Developers',
|
|
44
|
+
'Operating System :: OS Independent',
|
|
45
|
+
'License :: OSI Approved :: Apache Software License'
|
|
46
|
+
]
|
|
47
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import BinaryIO
|
|
6
|
+
|
|
7
|
+
from fsspec import AbstractFileSystem
|
|
8
|
+
|
|
9
|
+
from tie_storage.datamodel import ObjectInfo
|
|
10
|
+
from tp_interfaces.abstract import AbstractConfigConstructableModel
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AbstractStorageClient(AbstractConfigConstructableModel):
|
|
16
|
+
|
|
17
|
+
def __init__(self, fs: AbstractFileSystem):
|
|
18
|
+
self._fs = fs
|
|
19
|
+
|
|
20
|
+
def _makedirs(self, path: str):
|
|
21
|
+
path = Path(path)
|
|
22
|
+
self._fs.makedirs(str(path.parent), exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def _preprocess_storage_path(self, path: str, source: bool = False) -> str:
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
def _postprocess_storage_path(self, path: str, source: bool = False) -> str:
|
|
28
|
+
return path
|
|
29
|
+
|
|
30
|
+
def upload_file(self, source: str, dest: str) -> str:
|
|
31
|
+
"""Upload a local file to storage.
|
|
32
|
+
|
|
33
|
+
@param source: local file path
|
|
34
|
+
@param dest: storage destination path
|
|
35
|
+
@return: the storage file path
|
|
36
|
+
"""
|
|
37
|
+
dest = self._preprocess_storage_path(dest)
|
|
38
|
+
self._makedirs(dest)
|
|
39
|
+
self._fs.put(source, dest)
|
|
40
|
+
|
|
41
|
+
logger.info(f"Uploaded {source} -> {dest}")
|
|
42
|
+
return self._postprocess_storage_path(dest)
|
|
43
|
+
|
|
44
|
+
def upload_fileobj(self, fileobj: BinaryIO, dest: str) -> str:
|
|
45
|
+
"""Upload a file-like object to storage.
|
|
46
|
+
|
|
47
|
+
@param fileobj: file-like object to upload
|
|
48
|
+
@param dest: storage destination path
|
|
49
|
+
@return: the storage file path
|
|
50
|
+
"""
|
|
51
|
+
dest = self._preprocess_storage_path(dest)
|
|
52
|
+
self._makedirs(dest)
|
|
53
|
+
with self._fs.open(dest, "wb") as f:
|
|
54
|
+
f.write(fileobj.read())
|
|
55
|
+
|
|
56
|
+
logger.info(f"Uploaded fileobj -> {dest}")
|
|
57
|
+
return self._postprocess_storage_path(dest)
|
|
58
|
+
|
|
59
|
+
def upload_directory(self, source_dir: str, dest: str, recursive: bool = False) -> str:
|
|
60
|
+
"""Upload files from a local directory to storage.
|
|
61
|
+
|
|
62
|
+
@param source_dir: local directory path
|
|
63
|
+
@param dest: storage destination path
|
|
64
|
+
@param recursive: if True, upload subdirectories recursively
|
|
65
|
+
@return: the storage path to uploaded directory
|
|
66
|
+
"""
|
|
67
|
+
preprocessed_dest = self._preprocess_storage_path(dest)
|
|
68
|
+
|
|
69
|
+
for entry in os.listdir(source_dir):
|
|
70
|
+
entry_path = os.path.join(source_dir, entry)
|
|
71
|
+
if os.path.isfile(entry_path):
|
|
72
|
+
self.upload_file(entry_path, f"{dest}/{entry}")
|
|
73
|
+
elif recursive and os.path.isdir(entry_path):
|
|
74
|
+
self.upload_directory(entry_path, f"{dest}/{entry}", recursive=True)
|
|
75
|
+
|
|
76
|
+
logger.info(f"Uploaded directory {source_dir} -> {dest}")
|
|
77
|
+
return self._postprocess_storage_path(preprocessed_dest)
|
|
78
|
+
|
|
79
|
+
def download_file(self, source: str, dest: str) -> str:
|
|
80
|
+
"""Download a file from storage to local filesystem.
|
|
81
|
+
|
|
82
|
+
@param source: storage source path
|
|
83
|
+
@param dest: local destination file path
|
|
84
|
+
@return: the local absolute file path
|
|
85
|
+
"""
|
|
86
|
+
dest_abspath = Path(dest).resolve()
|
|
87
|
+
os.makedirs(dest_abspath.parent, exist_ok=True)
|
|
88
|
+
self._fs.get(self._preprocess_storage_path(source, source=True), str(dest_abspath))
|
|
89
|
+
logger.info(f"Downloaded {source} -> {dest_abspath}")
|
|
90
|
+
return str(dest_abspath)
|
|
91
|
+
|
|
92
|
+
def download_fileobj(self, source: str) -> BytesIO:
|
|
93
|
+
"""Download a file from storage as a BytesIO object.
|
|
94
|
+
|
|
95
|
+
@param source: storage source path
|
|
96
|
+
@return: BytesIO with file contents
|
|
97
|
+
"""
|
|
98
|
+
data = BytesIO(self._fs.cat(self._preprocess_storage_path(source, source=True)))
|
|
99
|
+
logger.info(f"Downloaded {source} as fileobj")
|
|
100
|
+
return data
|
|
101
|
+
|
|
102
|
+
def download_to_dir(self, source: str, dest: str) -> str:
|
|
103
|
+
"""Download a file from storage into a directory, preserving the filename.
|
|
104
|
+
|
|
105
|
+
@param source: storage source path
|
|
106
|
+
@param dest: local directory path to save the file in
|
|
107
|
+
@return: the local absolute file path
|
|
108
|
+
"""
|
|
109
|
+
filename = os.path.basename(source)
|
|
110
|
+
dest = os.path.join(dest, filename)
|
|
111
|
+
return self.download_file(self._preprocess_storage_path(source, source=True), dest)
|
|
112
|
+
|
|
113
|
+
def list_objects(self, path: str) -> list[ObjectInfo]:
|
|
114
|
+
"""List objects at the given path.
|
|
115
|
+
|
|
116
|
+
@param path: storage path to list
|
|
117
|
+
@return: list of ObjectInfo
|
|
118
|
+
"""
|
|
119
|
+
entries = self._fs.ls(self._preprocess_storage_path(path), detail=True)
|
|
120
|
+
return [ObjectInfo(path=entry["name"], size=entry.get("size")) for entry in entries]
|
|
121
|
+
|
|
122
|
+
def remove(self, path: str) -> None:
|
|
123
|
+
"""Remove a file or object from storage."""
|
|
124
|
+
self._fs.rm(self._preprocess_storage_path(path))
|
|
125
|
+
logger.info(f"Removed {path}")
|
|
126
|
+
|
|
127
|
+
def exists(self, path: str) -> bool:
|
|
128
|
+
"""Check if a path exists in storage."""
|
|
129
|
+
return self._fs.exists(self._preprocess_storage_path(path))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from tp_interfaces.abstract import ModelTypeFactory
|
|
4
|
+
from .abstract import AbstractStorageClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_local_client() -> Type[AbstractStorageClient]:
|
|
8
|
+
from .local import LocalStorageClient
|
|
9
|
+
return LocalStorageClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_s3_client() -> Type[AbstractStorageClient]:
|
|
13
|
+
from .s3 import S3StorageClient
|
|
14
|
+
return S3StorageClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
STORAGE_CLIENTS = ModelTypeFactory({
|
|
18
|
+
"local": _get_local_client,
|
|
19
|
+
"s3": _get_s3_client
|
|
20
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from talisman_tools.plugin import StorageClientPlugins
|
|
2
|
+
from tie_storage import AbstractStorageClient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def storage_client_from_config(config: dict) -> AbstractStorageClient:
|
|
6
|
+
storage_plugin = config["plugin"]
|
|
7
|
+
storage_client = config["name"]
|
|
8
|
+
storage_client_factory = StorageClientPlugins.plugins[storage_plugin].get(storage_client)
|
|
9
|
+
|
|
10
|
+
if storage_client_factory is None:
|
|
11
|
+
raise ValueError(f"Unknown storage client: `{storage_client}`")
|
|
12
|
+
|
|
13
|
+
storage_client_config = config["config"]
|
|
14
|
+
return storage_client_factory().from_config(storage_client_config)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from fsspec.implementations.local import LocalFileSystem
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from tie_storage.abstract import AbstractStorageClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalStorageClient(AbstractStorageClient):
|
|
10
|
+
"""Local filesystem storage client. Paths are resolved relative to base_path."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, base_path: str):
|
|
13
|
+
self._base_path = os.path.abspath(base_path)
|
|
14
|
+
os.makedirs(self._base_path, exist_ok=True)
|
|
15
|
+
super().__init__(LocalFileSystem(auto_mkdir=True))
|
|
16
|
+
|
|
17
|
+
def _preprocess_storage_path(self, path: str, source: bool = False) -> str:
|
|
18
|
+
if source:
|
|
19
|
+
return path
|
|
20
|
+
return os.path.join(self._base_path, path)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_config(cls, config: dict) -> Self:
|
|
24
|
+
if "base_path" in config:
|
|
25
|
+
return cls(base_path=config["base_path"])
|
|
26
|
+
raise ValueError("Incorrect config")
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def base_path(self) -> str:
|
|
30
|
+
return self._base_path
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from os.path import splitext
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from s3fs import S3FileSystem
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from tie_storage.abstract import AbstractStorageClient
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class S3Envs(str, Enum):
|
|
16
|
+
S3_URL = ("S3_URL", "http://localhost:9000")
|
|
17
|
+
S3_ACCESS_KEY = ("S3_ACCESS_KEY", "minio")
|
|
18
|
+
S3_SECRET_KEY = ("S3_SECRET_KEY", "minio123")
|
|
19
|
+
S3_VERIFY_TLS = ("S3_VERIFY_TLS", "false")
|
|
20
|
+
S3_BUCKET = ("S3_BUCKET", None)
|
|
21
|
+
|
|
22
|
+
def __new__(cls, name: str, default_value: str):
|
|
23
|
+
obj = str.__new__(cls, name)
|
|
24
|
+
obj._value_ = name
|
|
25
|
+
obj._default_value_ = default_value
|
|
26
|
+
return obj
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def env(self) -> str:
|
|
30
|
+
return os.getenv(self._value_, self._default_value_)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
S3_PREFIX = "s3://"
|
|
34
|
+
MAX_FILENAME_LENGTH = 255 # POSIX name limitation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class S3StorageClient(AbstractStorageClient):
|
|
38
|
+
"""S3-compatible storage client."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, bucket: str | None = None):
|
|
41
|
+
fs = S3FileSystem(
|
|
42
|
+
key=S3Envs.S3_ACCESS_KEY.env,
|
|
43
|
+
secret=S3Envs.S3_SECRET_KEY.env,
|
|
44
|
+
client_kwargs={
|
|
45
|
+
'endpoint_url': S3Envs.S3_URL.env,
|
|
46
|
+
'verify': S3Envs.S3_VERIFY_TLS.env.lower() == "true"
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
super().__init__(fs)
|
|
50
|
+
|
|
51
|
+
env_bucket = S3Envs.S3_BUCKET.env
|
|
52
|
+
self._bucket = env_bucket if env_bucket else bucket
|
|
53
|
+
if self._bucket is None:
|
|
54
|
+
raise ValueError("`S3_BUCKET` is not set")
|
|
55
|
+
|
|
56
|
+
if not self._fs.exists(self._bucket):
|
|
57
|
+
logger.info(f"Creating bucket `{self._bucket}`")
|
|
58
|
+
self._fs.mkdir(self._bucket)
|
|
59
|
+
logger.info(f"S3StorageClient initialized. endpoint: `{S3Envs.S3_URL.env}`, bucket: `{self._bucket}`")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def bucket(self) -> str:
|
|
63
|
+
return self._bucket
|
|
64
|
+
|
|
65
|
+
def _preprocess_storage_path(self, path: str, source: bool = False) -> str:
|
|
66
|
+
if path.startswith(S3_PREFIX):
|
|
67
|
+
path = path[len(S3_PREFIX):]
|
|
68
|
+
|
|
69
|
+
if source:
|
|
70
|
+
return path
|
|
71
|
+
|
|
72
|
+
if not path.startswith(f"{self._bucket}/") and path != self._bucket:
|
|
73
|
+
path = f"{self._bucket}/{path}" if path else self._bucket
|
|
74
|
+
|
|
75
|
+
path = Path(path)
|
|
76
|
+
file_name = _check_filename_length(path.name)
|
|
77
|
+
file_dir = str(path.parent)
|
|
78
|
+
return f"{file_dir}/{file_name}" if file_dir != "." else file_name
|
|
79
|
+
|
|
80
|
+
def _postprocess_storage_path(self, path: str, source: bool = False) -> str:
|
|
81
|
+
return f"{S3_PREFIX}{path}"
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_config(cls, config: dict) -> Self:
|
|
85
|
+
return cls(bucket=config.get("bucket"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _splitext_multi(path: str) -> tuple[str, str]:
|
|
89
|
+
"""splitext that handles extensions with multiple dots (e.g. '.tar.gz')."""
|
|
90
|
+
parts = path.split(".")
|
|
91
|
+
if len(parts) > 2:
|
|
92
|
+
return ".".join(parts[:-2]), "." + ".".join(parts[-2:])
|
|
93
|
+
return splitext(path)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _check_filename_length(filename: str) -> str:
|
|
97
|
+
"""Truncate filename if it exceeds POSIX name length limit."""
|
|
98
|
+
if len(filename) > MAX_FILENAME_LENGTH:
|
|
99
|
+
name, ext = _splitext_multi(filename)
|
|
100
|
+
filename = name[:MAX_FILENAME_LENGTH - len(ext)] + ext
|
|
101
|
+
return filename
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tie-storage
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: TIE storage client library
|
|
5
|
+
Author: ISPRAS TIE NLP team
|
|
6
|
+
Author-email: modis@ispras.ru
|
|
7
|
+
Maintainer: Vladimir Mayorov
|
|
8
|
+
Maintainer-email: vmayorov@ispras.ru
|
|
9
|
+
License: Apache Software License
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: talisman-interfaces~=0.11.0
|
|
19
|
+
Requires-Dist: fsspec
|
|
20
|
+
Provides-Extra: s3
|
|
21
|
+
Requires-Dist: boto3~=1.35.0; extra == "s3"
|
|
22
|
+
Requires-Dist: botocore~=1.35.0; extra == "s3"
|
|
23
|
+
Requires-Dist: s3fs; extra == "s3"
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: license
|
|
30
|
+
Dynamic: maintainer
|
|
31
|
+
Dynamic: maintainer-email
|
|
32
|
+
Dynamic: provides-extra
|
|
33
|
+
Dynamic: requires-dist
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
Dynamic: summary
|
|
36
|
+
|
|
37
|
+
# tie-storage
|
|
38
|
+
|
|
39
|
+
TIE storage client library providing a unified interface for local and S3-compatible storage.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
VERSION
|
|
3
|
+
setup.py
|
|
4
|
+
tie_storage/__init__.py
|
|
5
|
+
tie_storage/abstract.py
|
|
6
|
+
tie_storage/configuration.py
|
|
7
|
+
tie_storage/datamodel.py
|
|
8
|
+
tie_storage/helper.py
|
|
9
|
+
tie_storage/local.py
|
|
10
|
+
tie_storage/s3.py
|
|
11
|
+
tie_storage.egg-info/PKG-INFO
|
|
12
|
+
tie_storage.egg-info/SOURCES.txt
|
|
13
|
+
tie_storage.egg-info/dependency_links.txt
|
|
14
|
+
tie_storage.egg-info/entry_points.txt
|
|
15
|
+
tie_storage.egg-info/requires.txt
|
|
16
|
+
tie_storage.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tie_storage
|