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.
@@ -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,3 @@
1
+ # tie-storage
2
+
3
+ TIE storage client library providing a unified interface for local and S3-compatible storage.
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ from .abstract import AbstractStorageClient
2
+ from .configuration import STORAGE_CLIENTS
3
+ from .datamodel import ObjectInfo
4
+
5
+ __all__ = [
6
+ "AbstractStorageClient",
7
+ "ObjectInfo",
8
+ "STORAGE_CLIENTS"
9
+ ]
@@ -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,7 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class ObjectInfo:
6
+ path: str
7
+ size: int | None = None
@@ -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,2 @@
1
+ [talisman.plugins]
2
+ storage = tie_storage
@@ -0,0 +1,7 @@
1
+ talisman-interfaces~=0.11.0
2
+ fsspec
3
+
4
+ [s3]
5
+ boto3~=1.35.0
6
+ botocore~=1.35.0
7
+ s3fs
@@ -0,0 +1 @@
1
+ tie_storage