tgzr.snap 0.0.1__py3-none-any.whl
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.
- tgzr/snap/__init__.py +1 -0
- tgzr/snap/_version.py +34 -0
- tgzr/snap/base_repository.py +115 -0
- tgzr/snap/plugin.py +36 -0
- tgzr/snap/plugins/__init__.py +19 -0
- tgzr/snap/plugins/dvc.py +363 -0
- tgzr/snap/plugins/folder.py +287 -0
- tgzr_snap-0.0.1.dist-info/METADATA +26 -0
- tgzr_snap-0.0.1.dist-info/RECORD +12 -0
- tgzr_snap-0.0.1.dist-info/WHEEL +4 -0
- tgzr_snap-0.0.1.dist-info/entry_points.txt +3 -0
- tgzr_snap-0.0.1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import json
|
|
5
|
+
import hashlib
|
|
6
|
+
import shutil
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
import dataclasses
|
|
10
|
+
import tarfile
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
from ..base_repository import SnapRepository, RepositoryConfig, Remote, VersionInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FolderRepositoryConfig(RepositoryConfig):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FolderSnap(SnapRepository[FolderRepositoryConfig]):
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def create_repository(
|
|
24
|
+
cls, path: str | Path, config: RepositoryConfig, force: bool = False
|
|
25
|
+
):
|
|
26
|
+
path = Path(path)
|
|
27
|
+
path.mkdir(parents=True, exist_ok=force)
|
|
28
|
+
dot_snap = path / ".snap"
|
|
29
|
+
with dot_snap.open("w") as fp:
|
|
30
|
+
json.dump(dataclasses.asdict(config), fp)
|
|
31
|
+
|
|
32
|
+
for remote in config.remotes:
|
|
33
|
+
if not Path(remote.url).is_dir():
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"The remote {remote.name} url is invalid (not a directory): {remote.url}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _make_tarfile(self, dir_path: str | Path) -> str:
|
|
39
|
+
dir_path = Path(dir_path)
|
|
40
|
+
temp_fd, temp_path = tempfile.mkstemp(suffix=".tgz")
|
|
41
|
+
|
|
42
|
+
def filter(o: tarfile.TarInfo):
|
|
43
|
+
print(f"Adding {o.name} to archive...")
|
|
44
|
+
return o
|
|
45
|
+
|
|
46
|
+
with tarfile.open(temp_path, "w") as tar:
|
|
47
|
+
tar.add(dir_path, arcname=dir_path.name, filter=filter)
|
|
48
|
+
return temp_path
|
|
49
|
+
|
|
50
|
+
def _untar(self, tar_path: str | Path, destination: str | Path):
|
|
51
|
+
with tarfile.open(tar_path) as f:
|
|
52
|
+
f.extractall(path=destination)
|
|
53
|
+
|
|
54
|
+
def _get_hash(self, path: str | Path):
|
|
55
|
+
hasher = hashlib.md5()
|
|
56
|
+
with open(path, "rb") as f:
|
|
57
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
58
|
+
hasher.update(chunk)
|
|
59
|
+
return hasher.hexdigest()
|
|
60
|
+
|
|
61
|
+
#
|
|
62
|
+
#
|
|
63
|
+
#
|
|
64
|
+
|
|
65
|
+
def default_remote_name(self) -> str:
|
|
66
|
+
return self.config.default_remote
|
|
67
|
+
|
|
68
|
+
def remotes(self) -> list[Remote]:
|
|
69
|
+
return self.config.remotes.copy()
|
|
70
|
+
|
|
71
|
+
def get_remote(self, remote_name: str | None) -> Remote:
|
|
72
|
+
remote_name = remote_name or self.config.default_remote
|
|
73
|
+
for remote in self.config.remotes:
|
|
74
|
+
if remote.name == remote_name:
|
|
75
|
+
return remote
|
|
76
|
+
raise KeyError(
|
|
77
|
+
f"Unknown remote {remote_name!r} (got {[r.name for r in self.config.remotes]})."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
#
|
|
82
|
+
#
|
|
83
|
+
|
|
84
|
+
def push(self, repo_path: str, remote_name: str | None = None) -> VersionInfo:
|
|
85
|
+
path = self.path / repo_path
|
|
86
|
+
is_dir = path.is_dir()
|
|
87
|
+
data_name = "data"
|
|
88
|
+
if path.is_dir():
|
|
89
|
+
tar_path = self._make_tarfile(path)
|
|
90
|
+
path = Path(tar_path)
|
|
91
|
+
|
|
92
|
+
file_hash = self._get_hash(path)
|
|
93
|
+
if is_dir:
|
|
94
|
+
file_hash += ".dir"
|
|
95
|
+
data_name = "data.tar"
|
|
96
|
+
remote = self.get_remote(remote_name)
|
|
97
|
+
remote_dir = Path(remote.url) / file_hash
|
|
98
|
+
remote_dir.mkdir(exist_ok=True)
|
|
99
|
+
|
|
100
|
+
shutil.copy2(path, remote_dir / data_name)
|
|
101
|
+
|
|
102
|
+
pstat = path.stat()
|
|
103
|
+
vi = VersionInfo(
|
|
104
|
+
name=file_hash,
|
|
105
|
+
isdir=is_dir,
|
|
106
|
+
size=pstat.st_size,
|
|
107
|
+
nfiles=None,
|
|
108
|
+
isexec=bool(pstat.st_mode & stat.S_IXUSR),
|
|
109
|
+
version_id=None,
|
|
110
|
+
md5=None,
|
|
111
|
+
mtime=pstat.st_mtime,
|
|
112
|
+
remote_name=remote.name,
|
|
113
|
+
)
|
|
114
|
+
return vi
|
|
115
|
+
|
|
116
|
+
def get_versions(self, repo_path: str):
|
|
117
|
+
path = self.path / repo_path
|
|
118
|
+
raise NotImplementedError()
|
|
119
|
+
|
|
120
|
+
def status(self, repo_path: str):
|
|
121
|
+
path = self.path / repo_path
|
|
122
|
+
raise NotImplementedError()
|
|
123
|
+
|
|
124
|
+
def pull(self, repo_path: str | Path, version: str, remote_name: str | None = None):
|
|
125
|
+
path = self.path / repo_path
|
|
126
|
+
data_name = "data"
|
|
127
|
+
remote = self.get_remote(remote_name)
|
|
128
|
+
|
|
129
|
+
is_dir = version.endswith(".dir")
|
|
130
|
+
if is_dir:
|
|
131
|
+
data_name += ".tar"
|
|
132
|
+
remote_path = Path(remote.url) / version / data_name
|
|
133
|
+
if not remote_path.exists():
|
|
134
|
+
raise Exception(f"Version {version} not found ({remote_path})")
|
|
135
|
+
|
|
136
|
+
if is_dir:
|
|
137
|
+
self._untar(remote_path, path)
|
|
138
|
+
else:
|
|
139
|
+
shutil.copy2(remote_path, path)
|
|
140
|
+
|
|
141
|
+
def unprotect(self, local_path: str):
|
|
142
|
+
# Simply ensure the user has write permissions
|
|
143
|
+
current_mode = os.stat(local_path).st_mode
|
|
144
|
+
os.chmod(local_path, current_mode | stat.S_IWRITE)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
#
|
|
148
|
+
#
|
|
149
|
+
#
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_config() -> FolderRepositoryConfig:
|
|
153
|
+
config = FolderRepositoryConfig(
|
|
154
|
+
remotes=[
|
|
155
|
+
Remote("blessed", "/tmp/snap_tests_folder/PROD"),
|
|
156
|
+
Remote("review", "/tmp/snap_tests_folder/REVIEW"),
|
|
157
|
+
],
|
|
158
|
+
default_remote="review",
|
|
159
|
+
)
|
|
160
|
+
return config
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def create_repo():
|
|
164
|
+
config = test_config()
|
|
165
|
+
path = Path("/tmp/snap_tests_folder/WORK")
|
|
166
|
+
force = True
|
|
167
|
+
for remote in config.remotes:
|
|
168
|
+
Path(remote.url).mkdir(exist_ok=True, parents=False)
|
|
169
|
+
FolderSnap.create_repository(path, config, force=force)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_push():
|
|
173
|
+
import time
|
|
174
|
+
|
|
175
|
+
config = test_config()
|
|
176
|
+
path = Path("/tmp/snap_tests_folder/WORK")
|
|
177
|
+
snap = FolderSnap(path, config)
|
|
178
|
+
artifact = "bob.txt"
|
|
179
|
+
|
|
180
|
+
snap.unprotect(artifact)
|
|
181
|
+
|
|
182
|
+
with open(snap.path / artifact, "w") as fp:
|
|
183
|
+
fp.write(f"Change... {time.time()}")
|
|
184
|
+
|
|
185
|
+
version = snap.push(artifact)
|
|
186
|
+
|
|
187
|
+
print(f" version='{version.name}'")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_pull():
|
|
191
|
+
|
|
192
|
+
config = test_config()
|
|
193
|
+
path = Path("/tmp/snap_tests_folder/WORK")
|
|
194
|
+
snap = FolderSnap(path, config)
|
|
195
|
+
artifact = "bob.txt"
|
|
196
|
+
|
|
197
|
+
version = "bba2820fbf33bc4aef1444eca8c745da"
|
|
198
|
+
version = "cc2fcf6e6f788f6a9c66955d8407b196"
|
|
199
|
+
|
|
200
|
+
snap.pull(artifact, version)
|
|
201
|
+
with open(path / artifact, "r") as fp:
|
|
202
|
+
print("CONTENT is:", fp.read())
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_push_iso():
|
|
206
|
+
import rich
|
|
207
|
+
|
|
208
|
+
config = test_config()
|
|
209
|
+
path = Path("/tmp/snap_tests_folder/WORK")
|
|
210
|
+
snap = FolderSnap(path, config)
|
|
211
|
+
artifact = "big.iso"
|
|
212
|
+
|
|
213
|
+
Mo = 360000
|
|
214
|
+
with open(path / artifact, "w") as fp:
|
|
215
|
+
fp.write(str(os.urandom(500 * Mo)))
|
|
216
|
+
|
|
217
|
+
version_info = snap.push(artifact)
|
|
218
|
+
|
|
219
|
+
rich.print(f"{version_info}")
|
|
220
|
+
print(f" version='{version_info.name}'")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_pull_iso():
|
|
224
|
+
import time
|
|
225
|
+
|
|
226
|
+
config = test_config()
|
|
227
|
+
path = Path("/tmp/snap_tests_folder/WORK")
|
|
228
|
+
snap = FolderSnap(path, config)
|
|
229
|
+
artifact = "big.iso"
|
|
230
|
+
|
|
231
|
+
version = "ecdf67fa6c06ae448ad0d5b6a76350e4" # 9.9M
|
|
232
|
+
# version = "051223e2f81bb5242b9423ad4e6331a0" # 99M
|
|
233
|
+
# version = "e973302bab90201b4c865a16dbfb35b8" # 493M
|
|
234
|
+
|
|
235
|
+
t = time.time()
|
|
236
|
+
snap.pull(artifact, version)
|
|
237
|
+
print(time.time() - t)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_push_dir():
|
|
241
|
+
import random, rich
|
|
242
|
+
|
|
243
|
+
config = test_config()
|
|
244
|
+
path = Path("/tmp/snap_tests_folder/WORK")
|
|
245
|
+
snap = FolderSnap(path, config)
|
|
246
|
+
artifact = "data_folder"
|
|
247
|
+
|
|
248
|
+
(path / artifact).mkdir(exist_ok=True)
|
|
249
|
+
|
|
250
|
+
for i in range(random.randrange(5, 10)):
|
|
251
|
+
Mo = 360000
|
|
252
|
+
size = random.randrange(1, 20)
|
|
253
|
+
with open(path / artifact / f"file_{i:03}.data", "w") as fp:
|
|
254
|
+
fp.write(str(os.urandom(size * Mo)))
|
|
255
|
+
|
|
256
|
+
version_info = snap.push(artifact)
|
|
257
|
+
|
|
258
|
+
rich.print(f"{version_info}")
|
|
259
|
+
print(f" version='{version_info.name}'")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_pull_dir():
|
|
263
|
+
|
|
264
|
+
import time
|
|
265
|
+
|
|
266
|
+
config = test_config()
|
|
267
|
+
path = Path("/tmp/snap_tests_folder/WORK")
|
|
268
|
+
snap = FolderSnap(path, config)
|
|
269
|
+
artifact = "data_folder"
|
|
270
|
+
|
|
271
|
+
version = "476d2b77e9f9356f8d47c8711c41ebe8.dir"
|
|
272
|
+
version = "ff844fed890f1623075ddfcc7e31e4ad.dir"
|
|
273
|
+
# version = "1ead82a778cfae4b1f1a517f9e04589b.dir"
|
|
274
|
+
|
|
275
|
+
t = time.time()
|
|
276
|
+
snap.pull(artifact, version)
|
|
277
|
+
print(time.time() - t)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
# create_repo()
|
|
282
|
+
# test_push()
|
|
283
|
+
# test_pull()
|
|
284
|
+
# test_push_iso()
|
|
285
|
+
# test_pull_iso()
|
|
286
|
+
# test_push_dir()
|
|
287
|
+
test_pull_dir()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tgzr.snap
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Snap Normalizes Artifact Persistence - tgzr artifacts repository API
|
|
5
|
+
Project-URL: Documentation, https://github.com/open-tgzr/tgzr.snap#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/open-tgzr/tgzr.snap/issues
|
|
7
|
+
Project-URL: Source, https://github.com/open-tgzr/tgzr.snap
|
|
8
|
+
Author-email: Dee <dee.sometech@gmail.com>
|
|
9
|
+
License-Expression: GPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: dvc>=3.66.1
|
|
22
|
+
Requires-Dist: tgzr-package-management>=0.101
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# tgzr.snap
|
|
26
|
+
Snap Normalizes Artifact Persistence - tgzr artifacts repository API.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
tgzr/snap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
+
tgzr/snap/_version.py,sha256=qf6R-J7-UyuABBo8c0HgaquJ8bejVbf07HodXgwAwgQ,704
|
|
3
|
+
tgzr/snap/base_repository.py,sha256=cWQAASq0HTeXYuDfkp871X9FKWYqdEfyR409m8UB9Qc,2712
|
|
4
|
+
tgzr/snap/plugin.py,sha256=VbVLM5qgy0XAXW2VVRFjFqn0YDO7zptwzYsj5zs_KBc,963
|
|
5
|
+
tgzr/snap/plugins/__init__.py,sha256=rszdLaaCDzBat7AtPiOPNksxJ6WPRX33QTWXi3mtdew,407
|
|
6
|
+
tgzr/snap/plugins/dvc.py,sha256=jBBQxXOEi67HsmEWgh6G-pIQZTSWHtlDUFQOduGSWAk,10404
|
|
7
|
+
tgzr/snap/plugins/folder.py,sha256=tcf0Nku1SB2_t-yHDXtFq8-s21YLrl2mDSMS_ZoEaFU,7642
|
|
8
|
+
tgzr_snap-0.0.1.dist-info/METADATA,sha256=Ng0Pk2YBPiqJL5ZJ6fmZoFaBLBFiWKVrdBfNTpHb_Rk,1133
|
|
9
|
+
tgzr_snap-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
tgzr_snap-0.0.1.dist-info/entry_points.txt,sha256=KSrm3fgVbJ2kxCsTg6kxppSo3kOWcbS6iQXehpjK9VM,101
|
|
11
|
+
tgzr_snap-0.0.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
12
|
+
tgzr_snap-0.0.1.dist-info/RECORD,,
|