vocker 0.1.0__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.
- vocker/__init__.py +0 -0
- vocker/__main__.py +3 -0
- vocker/cli.py +384 -0
- vocker/dedup.py +1676 -0
- vocker/dedup_models.py +174 -0
- vocker/image.py +870 -0
- vocker/integer_to_path.py +51 -0
- vocker/multihash.py +302 -0
- vocker/py.typed +0 -0
- vocker/repo/__init__.py +0 -0
- vocker/repo/compression.py +239 -0
- vocker/repo/io.py +711 -0
- vocker/system.py +681 -0
- vocker/util.py +120 -0
- vocker/util_models.py +13 -0
- vocker-0.1.0.dist-info/METADATA +56 -0
- vocker-0.1.0.dist-info/RECORD +19 -0
- vocker-0.1.0.dist-info/WHEEL +5 -0
- vocker-0.1.0.dist-info/top_level.txt +1 -0
vocker/util.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import concurrent.futures as _cf
|
|
5
|
+
import contextlib
|
|
6
|
+
import platform
|
|
7
|
+
import urllib.parse as _up
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PurePathBase = object
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@contextlib.contextmanager
|
|
14
|
+
def pprofile(options=None):
|
|
15
|
+
if options is None:
|
|
16
|
+
options = os.environ.get("PPROFILE")
|
|
17
|
+
|
|
18
|
+
if not options:
|
|
19
|
+
yield None
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
d = dict(_up.parse_qsl(options.replace(",", "&"), keep_blank_values=True))
|
|
23
|
+
use_threads = int(d.get("thread", "1"))
|
|
24
|
+
prefix = d.get("prefix", "")
|
|
25
|
+
if (stat := d.get("stat")) is not None:
|
|
26
|
+
stat = float(stat)
|
|
27
|
+
if (annotate := d.get("annotate")) is not None and not annotate:
|
|
28
|
+
annotate = prefix + "annotate.txt"
|
|
29
|
+
if (cg := d.get("cachegrind")) is not None and not cg:
|
|
30
|
+
cg = prefix + "cachegrind.out.0"
|
|
31
|
+
|
|
32
|
+
import pprofile
|
|
33
|
+
|
|
34
|
+
if stat is None:
|
|
35
|
+
prof = pprofile.Profile()
|
|
36
|
+
else:
|
|
37
|
+
prof = pprofile.StatisticalThread()
|
|
38
|
+
|
|
39
|
+
if stat is not None:
|
|
40
|
+
prof = pprofile.StatisticalProfile()
|
|
41
|
+
runner = pprofile.StatisticalThread(profiler=prof, period=stat, single=not use_threads)
|
|
42
|
+
else:
|
|
43
|
+
klass = pprofile.ThreadProfile if use_threads else pprofile.Profile
|
|
44
|
+
prof = runner = klass()
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
with runner:
|
|
48
|
+
yield runner
|
|
49
|
+
finally:
|
|
50
|
+
with open(cg, "wt", encoding="utf-8") as file:
|
|
51
|
+
prof.callgrind(file)
|
|
52
|
+
with open(annotate, "wt", encoding="utf-8") as file:
|
|
53
|
+
prof.annotate(file)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def supports_executable() -> bool:
|
|
57
|
+
return platform.system() != "Windows"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def assert_(x, message=None):
|
|
61
|
+
if not x:
|
|
62
|
+
if message:
|
|
63
|
+
raise AssertionError(message)
|
|
64
|
+
else:
|
|
65
|
+
raise AssertionError
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pathwalk(p: Path, **kw):
|
|
69
|
+
for root, dirs, files in os.walk(str(p), **kw):
|
|
70
|
+
yield Path(root), dirs, files
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def random_names(prefix: str, suffix: str):
|
|
74
|
+
for n in (3, 4, 8, 16, 16):
|
|
75
|
+
yield "".join((prefix, os.urandom(n).hex(), suffix))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_file_random(parent: Path, prefix: str, suffix: str):
|
|
79
|
+
for name in random_names(prefix, suffix):
|
|
80
|
+
try:
|
|
81
|
+
return (path := parent / name).open("x+b")
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
if not Path(path).parent.is_dir():
|
|
84
|
+
raise
|
|
85
|
+
exc_ = exc
|
|
86
|
+
|
|
87
|
+
raise exc_
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_path_prefix_to_pathlib_type = {
|
|
91
|
+
"windows": "PureWindowsPath",
|
|
92
|
+
"posix": "PurePosixPath",
|
|
93
|
+
"": "PurePath",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_pure_path(path: str) -> PurePathBase:
|
|
98
|
+
before, sep, after = path.partition(":")
|
|
99
|
+
if (clsname := _path_prefix_to_pathlib_type.get(before)) is None or not sep:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f'pure path must start with "windows:" or "posix:" or ":" prefix, got {path!r}'
|
|
102
|
+
)
|
|
103
|
+
return getattr(pathlib, clsname)(after)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def raise_as_completed(*args, **kwargs):
|
|
107
|
+
"""Go through each future as completed, re-raising any exception that occurs."""
|
|
108
|
+
for future in _cf.as_completed(*args, **kwargs):
|
|
109
|
+
future.result() # this will re-raise an exception if one was raised by _process_file
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@contextlib.contextmanager
|
|
113
|
+
def cancel_futures_on_error(exe: _cf.Executor):
|
|
114
|
+
ok = False
|
|
115
|
+
try:
|
|
116
|
+
yield
|
|
117
|
+
ok = True
|
|
118
|
+
finally:
|
|
119
|
+
if not ok:
|
|
120
|
+
exe.shutdown(cancel_futures=True)
|
vocker/util_models.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from datetime import timezone
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def now() -> int:
|
|
6
|
+
return int(datetime.datetime.now(timezone.utc).timestamp())
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
rel_kw_basic = dict(repr=False, lazy="raise")
|
|
10
|
+
|
|
11
|
+
# Coupled with ForeignKey(..., ondelete="CASCADE"), this implements automatic deletion of child
|
|
12
|
+
# records when the parent is deleted.
|
|
13
|
+
rel_kw_cascade = rel_kw_basic | dict(cascade="all, delete", passive_deletes=True)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vocker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Docker-like manager for virtualenvs
|
|
5
|
+
Author-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
|
|
6
|
+
License: General Public License v3
|
|
7
|
+
Project-URL: Homepage, https://hydra.ecd.space/deaduard/vocker/
|
|
8
|
+
Project-URL: Changelog, https://hydra.ecd.space/deaduard/vocker/file?name=CHANGELOG.md&ci=trunk
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: atomicwrites
|
|
11
|
+
Requires-Dist: attrs
|
|
12
|
+
Requires-Dist: boltons
|
|
13
|
+
Requires-Dist: cached_property
|
|
14
|
+
Requires-Dist: filelock
|
|
15
|
+
Requires-Dist: immutabledict
|
|
16
|
+
Requires-Dist: marshmallow
|
|
17
|
+
Requires-Dist: platformdirs
|
|
18
|
+
Requires-Dist: sansio_tools>=1.0.0
|
|
19
|
+
Requires-Dist: sqlalchemy_boltons>=2.4.0
|
|
20
|
+
Requires-Dist: SQLAlchemy
|
|
21
|
+
Requires-Dist: strictyaml
|
|
22
|
+
Requires-Dist: structlog
|
|
23
|
+
Requires-Dist: cbor2
|
|
24
|
+
Provides-Extra: zstandard
|
|
25
|
+
Requires-Dist: pyzstd; extra == "zstandard"
|
|
26
|
+
Provides-Extra: tests
|
|
27
|
+
Requires-Dist: pytest; extra == "tests"
|
|
28
|
+
|
|
29
|
+
# vocker
|
|
30
|
+
|
|
31
|
+
Manager for complete Python environments written with security in mind. Mostly for Windows.
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
OK so here's a typical experience. You're working on different Python projects which require incompatible versions of dependencies. For example, one of them needs `libfoo==1.0.0` and the other needs `libfoo>3.0.0`. There's just no way to satisfy both. Python people encourage you to create different virtualenvs ("venvs") for different purposes. Sometimes a user reports a bug that they experience with some very specific version of a dependency, so you need to create yet another venv just to investigate that.
|
|
36
|
+
|
|
37
|
+
Here's a problem: every venv you install takes up a few hundred megabytes of disk space, and a lot of it is for completely redundant files. You were conned into buying an overpriced non-modular computer, so now your tiny non-upgradeable SSD space is now filled with many copies of the same files. You regret your life choices. Wouldn't it be nice if the duplicate files across different venvs didn't take up any additional space?
|
|
38
|
+
|
|
39
|
+
Users often report bugs against very specific versions of your software, and the café you work at has pretty slow WiFi. Installing hundreds of megabytes of the same packages over and over quickly grows tiresome. Wouldn't it be nice if you could just copy an existing venv and just tweak it a bit, for example replace the few packages that are actually different?
|
|
40
|
+
|
|
41
|
+
Finally, some of your nontechnical users refuse to compile and install their own software, but they do want to sometimes have multiple versions installed for testing purposes. However, they also bought non-upgradeable hardware so they don't want multiple copies of the same files that are identical across different versions of the software. Wouldn't it be nice if installing a new venv somehow recycled the existing files from the currently-installed venvs?
|
|
42
|
+
|
|
43
|
+
Some of your users are paranoid about security. Wouldn't it be nice if the software integrity of the venv-based software package were guaranteed through hashing and Merkle trees?
|
|
44
|
+
|
|
45
|
+
That's why.
|
|
46
|
+
|
|
47
|
+
## Goals
|
|
48
|
+
|
|
49
|
+
- Developers can easily create images, and then distribute them to users who use them to run applications. The users don't necessarily use vocker directly to create containers, they may use some extra layer on top of it (like an installer that provides a GUI and maybe digital signature verification).
|
|
50
|
+
- Developers can easily create images from existing images by tweaking whatever needs to be different. For example, installing new software or modifying files.
|
|
51
|
+
- Image creation should be reproducible. That is, creating a Python environment and then turning it into an image should give you exactly the same image if you do that a second time. The resulting image hash should be identical.
|
|
52
|
+
- Developers can easily audit existing images by just rebuilding them from scratch and checking whether the final result is the same.
|
|
53
|
+
|
|
54
|
+
## Non-goals
|
|
55
|
+
|
|
56
|
+
- Digital signature verification.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
vocker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
vocker/__main__.py,sha256=jNkuxmxpoWPeKXHVf-TyEECSY62QZQHoR2F_Bp6zsNM,35
|
|
3
|
+
vocker/cli.py,sha256=MQfac_AL5febfwDcgNOjuQA9rsq6kyGXVn2rp-6toP8,13248
|
|
4
|
+
vocker/dedup.py,sha256=XgWJMZ14mP4BgNkH3gVOqwKX6KScRrGuRPs4Q2vysH8,63923
|
|
5
|
+
vocker/dedup_models.py,sha256=R0lTKazCQnYNaIdlTfeCLldR4e66CxQ8jgfIjxfjmrA,5612
|
|
6
|
+
vocker/image.py,sha256=lewNLLiXnd_N1CSs4gnYFEj-d5RkIBiPQiN8hNL2fIs,28181
|
|
7
|
+
vocker/integer_to_path.py,sha256=5ghlupk9VLzXLtcfwnVEVFxtBxyT8A_ooV8-2EAnoFw,1433
|
|
8
|
+
vocker/multihash.py,sha256=-VhksUBam6N01fICtTg_TJrJcEIHJrYVKzkD1B_bdfI,8760
|
|
9
|
+
vocker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
vocker/system.py,sha256=kw0-0vxE5jjHjnE1kzhDZB_YdzA5kBDLeU0TdudnJgg,24416
|
|
11
|
+
vocker/util.py,sha256=bQcMzscMPaiBC4PGV9_clTOLDTJHuqa6l6o2thQJeU8,3223
|
|
12
|
+
vocker/util_models.py,sha256=2bN5eousF92oH7BAv1ZFoyh6iqNAnJ_niiclp2_RaHI,395
|
|
13
|
+
vocker/repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
vocker/repo/compression.py,sha256=l2g1e6SaugpqORbg3zwRM1zwlEXedbYOihm5nDpCejU,6442
|
|
15
|
+
vocker/repo/io.py,sha256=7oUDd2vag300hRJe72VcfGVAtWDIxtMd-5pY6I_z_Fs,25250
|
|
16
|
+
vocker-0.1.0.dist-info/METADATA,sha256=P9TNrszsFyA2Q3HItjFoMeKdeCALldVmbu4VCxwv_KE,3879
|
|
17
|
+
vocker-0.1.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
|
18
|
+
vocker-0.1.0.dist-info/top_level.txt,sha256=5x7g7T2L44UKODxVZ4vmWjxDnnruxaZ5yloYi0wLoUg,7
|
|
19
|
+
vocker-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vocker
|