repak 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.
repak/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """repak: package a local directory as a synthetic PyPI wheel for transport."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
repak/archive.py ADDED
@@ -0,0 +1,64 @@
1
+ """Build a deterministic ``tar.gz`` of a directory's contents + checksum."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gzip
6
+ import hashlib
7
+ import io
8
+ import os
9
+ import tarfile
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import List
13
+
14
+ VCS_DIRS = {".git", ".hg", ".svn", ".bzr"}
15
+
16
+ # Fixed timestamp so identical trees produce identical archives (helps make
17
+ # uploads/tests reproducible). 2020-01-01 UTC.
18
+ _FIXED_MTIME = 1577836800
19
+
20
+
21
+ @dataclass
22
+ class Archive:
23
+ data: bytes
24
+ sha256: str
25
+ size: int # compressed size in bytes
26
+
27
+
28
+ def _iter_files(root: Path) -> List[Path]:
29
+ collected: List[Path] = []
30
+ for dirpath, dirnames, filenames in os.walk(root):
31
+ # Prune VCS directories in-place so os.walk does not descend.
32
+ dirnames[:] = sorted(d for d in dirnames if d not in VCS_DIRS)
33
+ for name in sorted(filenames):
34
+ collected.append(Path(dirpath) / name)
35
+ return collected
36
+
37
+
38
+ def build_archive(source_dir: str | os.PathLike) -> Archive:
39
+ """Tar (gzip) the *contents* of ``source_dir`` at the archive root.
40
+
41
+ VCS directories are excluded. The archive and its SHA-256 are
42
+ deterministic for a given set of file contents and relative paths.
43
+ """
44
+ root = Path(source_dir).resolve()
45
+ if not root.is_dir():
46
+ raise NotADirectoryError(f"{root} is not a directory")
47
+
48
+ raw = io.BytesIO()
49
+ with tarfile.open(fileobj=raw, mode="w") as tar:
50
+ for path in _iter_files(root):
51
+ rel = path.relative_to(root).as_posix()
52
+ info = tar.gettarinfo(str(path), arcname=rel)
53
+ info.mtime = _FIXED_MTIME
54
+ info.uid = info.gid = 0
55
+ info.uname = info.gname = ""
56
+ if info.isreg():
57
+ with open(path, "rb") as fh:
58
+ tar.addfile(info, fh)
59
+ else:
60
+ tar.addfile(info)
61
+
62
+ compressed = gzip.compress(raw.getvalue(), compresslevel=9, mtime=0)
63
+ digest = hashlib.sha256(compressed).hexdigest()
64
+ return Archive(data=compressed, sha256=digest, size=len(compressed))
repak/cli.py ADDED
@@ -0,0 +1,143 @@
1
+ """repak command-line entry point (upload side)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ from . import naming, pypi, versioning
12
+ from .archive import build_archive
13
+ from .wheel import WheelTooLarge, build_wheel
14
+
15
+ TOKEN_ENV_VARS = ("PYPI_TOKEN", "TWINE_PASSWORD")
16
+
17
+
18
+ def _get_token() -> str:
19
+ for var in TOKEN_ENV_VARS:
20
+ val = os.environ.get(var)
21
+ if val:
22
+ return val
23
+ raise SystemExit(
24
+ "ERROR: no PyPI token found. Set the PYPI_TOKEN environment variable "
25
+ "to a PyPI API token (passed via the environment, never as a flag, "
26
+ "so it stays out of shell history and process listings)."
27
+ )
28
+
29
+
30
+ def _confirm(prompt: str, assume_yes: bool) -> bool:
31
+ if assume_yes:
32
+ return True
33
+ try:
34
+ answer = input(f"{prompt} [y/N] ").strip().lower()
35
+ except EOFError:
36
+ return False
37
+ return answer in ("y", "yes")
38
+
39
+
40
+ def _parse_args(argv):
41
+ parser = argparse.ArgumentParser(
42
+ prog="repak",
43
+ description="Package a local directory as a synthetic PyPI wheel and "
44
+ "upload it to public PyPI for transport into an isolated mirror.",
45
+ )
46
+ parser.add_argument(
47
+ "--path",
48
+ default=".",
49
+ help="Directory to package (default: current directory).",
50
+ )
51
+ parser.add_argument(
52
+ "--yes",
53
+ "-y",
54
+ action="store_true",
55
+ help="Skip confirmation prompts (for non-interactive use).",
56
+ )
57
+ return parser.parse_args(argv)
58
+
59
+
60
+ def main(argv=None) -> int:
61
+ args = _parse_args(argv)
62
+
63
+ source = Path(args.path).resolve()
64
+ if not source.is_dir():
65
+ sys.stderr.write(f"ERROR: {source} is not a directory.\n")
66
+ return 1
67
+
68
+ try:
69
+ pypi_name = naming.pypi_name(source.name)
70
+ except naming.NameError_ as exc:
71
+ sys.stderr.write(f"ERROR: {exc}\n")
72
+ return 1
73
+
74
+ token = _get_token()
75
+
76
+ info = pypi.query(pypi_name)
77
+ if info.exists and not info.is_repak:
78
+ sys.stderr.write(
79
+ f"ERROR: '{pypi_name}' already exists on PyPI but was not created "
80
+ "by repak (no repak marker). Refusing to collide with an "
81
+ "unrelated package. Rename the source folder and retry.\n"
82
+ )
83
+ return 1
84
+
85
+ try:
86
+ version = versioning.next_version(info.versions if info.exists else None)
87
+ except versioning.VersionExhausted as exc:
88
+ sys.stderr.write(f"ERROR: {exc}\n")
89
+ return 1
90
+
91
+ if not info.exists:
92
+ print(f"Creating new package {pypi_name} at version {version}")
93
+ else:
94
+ latest = info.versions[-1] if info.versions else "?"
95
+ print(
96
+ f"Warning: {pypi_name} already exists at version {latest}\n"
97
+ f"This upload will create version {version}."
98
+ )
99
+ if not _confirm("Proceed?", args.yes):
100
+ print("Aborted.")
101
+ return 1
102
+
103
+ archive = build_archive(source)
104
+ print(
105
+ f"Archived contents of {source} "
106
+ f"({archive.size / (1024 * 1024):.2f} MiB compressed, "
107
+ f"sha256 {archive.sha256[:16]}...)"
108
+ )
109
+
110
+ with tempfile.TemporaryDirectory() as tmp:
111
+ try:
112
+ wheel = build_wheel(source.name, version, archive, tmp)
113
+ except WheelTooLarge as exc:
114
+ sys.stderr.write(f"ERROR: {exc}\n")
115
+ return 1
116
+
117
+ print(
118
+ f"Built wheel {wheel.filename} "
119
+ f"({wheel.size / (1024 * 1024):.2f} MiB)"
120
+ )
121
+ if not _confirm(f"Upload {wheel.filename} to public PyPI?", args.yes):
122
+ print("Aborted.")
123
+ return 1
124
+
125
+ try:
126
+ pypi.upload(wheel.path, token)
127
+ except pypi.UploadError as exc:
128
+ sys.stderr.write(f"ERROR: {exc}\n")
129
+ return 1
130
+
131
+ script = naming.console_script(source.name)
132
+ print(
133
+ f"\nUploaded {pypi_name} {version} to public PyPI.\n"
134
+ "Note: propagation to the internal mirror is NOT immediate; there is "
135
+ "an unknown sync lag before it becomes installable in the isolated "
136
+ "environment.\n"
137
+ f"Once available: pip install {pypi_name} && {script} /target/folder"
138
+ )
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ raise SystemExit(main())
repak/naming.py ADDED
@@ -0,0 +1,43 @@
1
+ """Folder name -> PyPI package name / Python module name normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ REPAK_PREFIX = "repak"
8
+
9
+
10
+ class NameError_(ValueError):
11
+ """Raised when a folder name cannot be normalized to a valid name."""
12
+
13
+
14
+ def normalize(folder_name: str) -> str:
15
+ """Normalize a folder basename to a PEP 503-style component.
16
+
17
+ Lowercase, runs of non-alphanumeric characters collapse to a single
18
+ hyphen, leading/trailing hyphens stripped.
19
+ """
20
+ lowered = folder_name.strip().lower()
21
+ collapsed = re.sub(r"[^a-z0-9]+", "-", lowered)
22
+ stripped = collapsed.strip("-")
23
+ if not stripped:
24
+ raise NameError_(
25
+ f"folder name {folder_name!r} does not contain any characters "
26
+ "usable in a PyPI package name"
27
+ )
28
+ return stripped
29
+
30
+
31
+ def pypi_name(folder_name: str) -> str:
32
+ """Return the public PyPI distribution name: ``repak-{normalized}``."""
33
+ return f"{REPAK_PREFIX}-{normalize(folder_name)}"
34
+
35
+
36
+ def module_name(folder_name: str) -> str:
37
+ """Return the import package name: ``repak_{normalized_with_underscores}``."""
38
+ return f"{REPAK_PREFIX}_{normalize(folder_name).replace('-', '_')}"
39
+
40
+
41
+ def console_script(folder_name: str) -> str:
42
+ """Return the destination-side console command: ``unpak-{normalized}``."""
43
+ return f"unpak-{normalize(folder_name)}"
repak/pypi.py ADDED
@@ -0,0 +1,75 @@
1
+ """PyPI interaction: version query, repak-marker detection, twine upload."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import urllib.error
9
+ import urllib.request
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ from .wheel import MARKER
15
+
16
+ PYPI_JSON_URL = "https://pypi.org/pypi/{name}/json"
17
+
18
+
19
+ @dataclass
20
+ class PackageInfo:
21
+ exists: bool
22
+ versions: List[str]
23
+ is_repak: bool # True only if an existing package carries the repak marker
24
+
25
+
26
+ class UploadError(RuntimeError):
27
+ pass
28
+
29
+
30
+ def query(pypi_name: str, *, timeout: float = 15.0) -> PackageInfo:
31
+ """Look up ``pypi_name`` on public PyPI.
32
+
33
+ A 404 means the name is free. If it exists, the repak marker keyword
34
+ distinguishes a package repak itself created from an unrelated public
35
+ package that merely shares the name.
36
+ """
37
+ url = PYPI_JSON_URL.format(name=pypi_name)
38
+ try:
39
+ with urllib.request.urlopen(url, timeout=timeout) as resp:
40
+ payload = json.loads(resp.read().decode("utf-8"))
41
+ except urllib.error.HTTPError as exc:
42
+ if exc.code == 404:
43
+ return PackageInfo(exists=False, versions=[], is_repak=False)
44
+ raise
45
+
46
+ versions = sorted((payload.get("releases") or {}).keys())
47
+ info = payload.get("info") or {}
48
+ keywords = (info.get("keywords") or "").lower()
49
+ is_repak = MARKER in keywords
50
+ return PackageInfo(exists=True, versions=versions, is_repak=is_repak)
51
+
52
+
53
+ def upload(wheel_path: str | Path, token: str, *, repository_url: Optional[str] = None) -> None:
54
+ """Upload a wheel with ``twine`` using token auth.
55
+
56
+ The token is passed via the environment (``TWINE_PASSWORD`` with
57
+ ``TWINE_USERNAME=__token__``) so it never appears in argv or shell
58
+ history.
59
+ """
60
+ env = {
61
+ "TWINE_USERNAME": "__token__",
62
+ "TWINE_PASSWORD": token,
63
+ }
64
+ import os
65
+
66
+ full_env = {**os.environ, **env}
67
+ cmd = [sys.executable, "-m", "twine", "upload", str(wheel_path)]
68
+ if repository_url:
69
+ cmd[4:4] = ["--repository-url", repository_url]
70
+ result = subprocess.run(cmd, env=full_env, capture_output=True, text=True)
71
+ if result.returncode != 0:
72
+ raise UploadError(
73
+ "twine upload failed:\n"
74
+ f"{result.stdout}\n{result.stderr}".strip()
75
+ )
@@ -0,0 +1,102 @@
1
+ """Self-contained extractor embedded into every repak-generated wheel.
2
+
3
+ This module ships *inside* the synthetic wheel as ``<pkg>/_unpak.py`` and is
4
+ wired to the ``unpak-{name}`` console script. It must depend only on the
5
+ Python standard library: repak is not installed on the destination side.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import io
13
+ import os
14
+ import sys
15
+ import tarfile
16
+ from importlib import resources
17
+ from pathlib import Path
18
+
19
+ PAYLOAD = "payload.tar.gz"
20
+ CHECKSUM = "payload.sha256"
21
+
22
+
23
+ def _read_resource(name: str) -> bytes:
24
+ return resources.files(__package__).joinpath(name).read_bytes()
25
+
26
+
27
+ def _safe_members(tar: tarfile.TarFile, dest: Path):
28
+ """Yield only members that extract safely under ``dest``.
29
+
30
+ Rejects absolute paths and ``..`` traversal; restricts to regular files
31
+ and directories (mirrors the intent of tarfile's ``data`` filter while
32
+ remaining compatible with Python 3.9).
33
+ """
34
+ dest = dest.resolve()
35
+ for member in tar.getmembers():
36
+ name = member.name
37
+ if name.startswith("/") or os.path.isabs(name):
38
+ raise ValueError(f"unsafe absolute path in archive: {name!r}")
39
+ target = (dest / name).resolve()
40
+ if target != dest and dest not in target.parents:
41
+ raise ValueError(f"unsafe path traversal in archive: {name!r}")
42
+ if member.isdir() or member.isreg():
43
+ yield member
44
+ else:
45
+ raise ValueError(
46
+ f"archive contains unsupported entry {name!r} "
47
+ f"(type {member.type!r})"
48
+ )
49
+
50
+
51
+ def _extract(data: bytes, dest: Path) -> None:
52
+ dest.mkdir(parents=True, exist_ok=True)
53
+ with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar:
54
+ members = list(_safe_members(tar, dest))
55
+ for member in members:
56
+ out = dest / member.name
57
+ if member.isdir():
58
+ out.mkdir(parents=True, exist_ok=True)
59
+ continue
60
+ out.parent.mkdir(parents=True, exist_ok=True)
61
+ src = tar.extractfile(member)
62
+ if src is None:
63
+ continue
64
+ with open(out, "wb") as fh:
65
+ fh.write(src.read())
66
+ os.chmod(out, member.mode & 0o777)
67
+
68
+
69
+ def main(argv=None) -> int:
70
+ parser = argparse.ArgumentParser(
71
+ prog=f"unpak ({__package__})",
72
+ description="Verify and extract a repak-transported directory.",
73
+ )
74
+ parser.add_argument(
75
+ "target",
76
+ help="Destination folder; created if missing. Existing unrelated "
77
+ "files are left untouched (overwrite/merge).",
78
+ )
79
+ args = parser.parse_args(argv)
80
+
81
+ data = _read_resource(PAYLOAD)
82
+ expected = _read_resource(CHECKSUM).decode("ascii").strip()
83
+ actual = hashlib.sha256(data).hexdigest()
84
+ if actual != expected:
85
+ sys.stderr.write(
86
+ "ERROR: checksum verification failed; the payload is corrupt "
87
+ "and nothing was written.\n"
88
+ f" expected: {expected}\n"
89
+ f" actual: {actual}\n"
90
+ )
91
+ return 1
92
+
93
+ dest = Path(args.target)
94
+ _extract(data, dest)
95
+ sys.stdout.write(
96
+ f"Verified SHA-256 and extracted contents to {dest.resolve()}\n"
97
+ )
98
+ return 0
99
+
100
+
101
+ if __name__ == "__main__":
102
+ raise SystemExit(main())
repak/versioning.py ADDED
@@ -0,0 +1,47 @@
1
+ """Version scheme: 0.N where N is an integer 1..99.
2
+
3
+ Sequence: 0.1, 0.2, ... 0.9, 0.10, 0.11, ... 0.99 (99 uploads per package).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Iterable, Optional
10
+
11
+ _VERSION_RE = re.compile(r"^0\.(\d+)$")
12
+
13
+ MIN_MINOR = 1
14
+ MAX_MINOR = 99
15
+
16
+
17
+ class VersionExhausted(RuntimeError):
18
+ """Raised when the 0.99 ceiling has been reached for a package."""
19
+
20
+
21
+ def next_version(existing: Optional[Iterable[str]]) -> str:
22
+ """Return the next ``0.N`` version string.
23
+
24
+ ``None`` or an empty iterable means the package does not exist yet, so
25
+ versioning starts at ``0.1``. Otherwise the next version is one greater
26
+ than the highest ``0.N`` already published; non-conforming version
27
+ strings are ignored.
28
+ """
29
+ if not existing:
30
+ return f"0.{MIN_MINOR}"
31
+
32
+ minors = [
33
+ int(m.group(1))
34
+ for m in (_VERSION_RE.match(v.strip()) for v in existing)
35
+ if m is not None
36
+ ]
37
+ if not minors:
38
+ return f"0.{MIN_MINOR}"
39
+
40
+ current = max(minors)
41
+ nxt = current + 1
42
+ if nxt > MAX_MINOR:
43
+ raise VersionExhausted(
44
+ f"package has reached the maximum version 0.{MAX_MINOR}; "
45
+ "no further uploads are possible under this package name"
46
+ )
47
+ return f"0.{nxt}"
repak/wheel.py ADDED
@@ -0,0 +1,108 @@
1
+ """Hand-built synthetic wheel: a transport container, not a real package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import zipfile
8
+ from dataclasses import dataclass
9
+ from importlib import resources
10
+ from pathlib import Path
11
+ from typing import Dict
12
+
13
+ from . import naming
14
+ from .archive import Archive
15
+ from .unpak_template import CHECKSUM, PAYLOAD
16
+
17
+ MARKER = "repak-transport-container"
18
+
19
+ # PyPI's default per-file upload limit.
20
+ MAX_WHEEL_BYTES = 100 * 1024 * 1024
21
+
22
+
23
+ class WheelTooLarge(RuntimeError):
24
+ def __init__(self, size: int):
25
+ self.size = size
26
+ super().__init__(
27
+ f"generated wheel is {size / (1024 * 1024):.2f} MiB, which "
28
+ f"exceeds PyPI's {MAX_WHEEL_BYTES // (1024 * 1024)} MiB per-file "
29
+ "limit. Reduce the directory size and try again."
30
+ )
31
+
32
+
33
+ @dataclass
34
+ class BuiltWheel:
35
+ path: Path
36
+ size: int
37
+ filename: str
38
+
39
+
40
+ def _record_hash(data: bytes) -> str:
41
+ digest = hashlib.sha256(data).digest()
42
+ b64 = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
43
+ return f"sha256={b64}"
44
+
45
+
46
+ def _unpak_source() -> str:
47
+ return resources.files("repak").joinpath("unpak_template.py").read_text(
48
+ encoding="utf-8"
49
+ )
50
+
51
+
52
+ def build_wheel(
53
+ folder_name: str,
54
+ version: str,
55
+ archive: Archive,
56
+ out_dir: str | Path,
57
+ ) -> BuiltWheel:
58
+ """Write ``repak_{name}-{version}-py3-none-any.whl`` into ``out_dir``."""
59
+ pkg = naming.module_name(folder_name)
60
+ dist_name = naming.pypi_name(folder_name)
61
+ script = naming.console_script(folder_name)
62
+ dist_info = f"{pkg}-{version}.dist-info"
63
+
64
+ metadata = (
65
+ "Metadata-Version: 2.1\n"
66
+ f"Name: {dist_name}\n"
67
+ f"Version: {version}\n"
68
+ "Summary: Directory payload transported by repak.\n"
69
+ f"Keywords: {MARKER}\n"
70
+ )
71
+ wheel_meta = (
72
+ "Wheel-Version: 1.0\n"
73
+ "Generator: repak\n"
74
+ "Root-Is-Purelib: true\n"
75
+ "Tag: py3-none-any\n"
76
+ )
77
+ entry_points = f"[console_scripts]\n{script} = {pkg}._unpak:main\n"
78
+
79
+ files: Dict[str, bytes] = {
80
+ f"{pkg}/__init__.py": b"",
81
+ f"{pkg}/_unpak.py": _unpak_source().encode("utf-8"),
82
+ f"{pkg}/{PAYLOAD}": archive.data,
83
+ f"{pkg}/{CHECKSUM}": archive.sha256.encode("ascii") + b"\n",
84
+ f"{dist_info}/METADATA": metadata.encode("utf-8"),
85
+ f"{dist_info}/WHEEL": wheel_meta.encode("utf-8"),
86
+ f"{dist_info}/entry_points.txt": entry_points.encode("utf-8"),
87
+ }
88
+
89
+ record_lines = [
90
+ f"{name},{_record_hash(data)},{len(data)}"
91
+ for name, data in files.items()
92
+ ]
93
+ record_path = f"{dist_info}/RECORD"
94
+ record_lines.append(f"{record_path},,")
95
+ files[record_path] = ("\n".join(record_lines) + "\n").encode("utf-8")
96
+
97
+ filename = f"{pkg}-{version}-py3-none-any.whl"
98
+ out_path = Path(out_dir) / filename
99
+ with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zf:
100
+ for name, data in files.items():
101
+ zf.writestr(name, data)
102
+
103
+ size = out_path.stat().st_size
104
+ if size > MAX_WHEEL_BYTES:
105
+ out_path.unlink(missing_ok=True)
106
+ raise WheelTooLarge(size)
107
+
108
+ return BuiltWheel(path=out_path, size=size, filename=filename)
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: repak
3
+ Version: 0.1.0
4
+ Summary: Transfer a local directory across a network boundary via a synthetic PyPI wheel
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: twine>=4.0
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=7.0; extra == "dev"
@@ -0,0 +1,13 @@
1
+ repak/__init__.py,sha256=QMI24uAFXyzQ-GOGdXllmLjcMDcDR-6SCJ0QR-G464Q,130
2
+ repak/archive.py,sha256=3xjdlMqNKI4knYV9dAYoI-QqzhPfQvEWLw1NEX-Qw1I,2054
3
+ repak/cli.py,sha256=GErQjRB6gBTymh4-uAZCTylaRSrdKy9Bb3EYy1F1MSk,4221
4
+ repak/naming.py,sha256=5vO369XEuQBbYTDZOqmsPWMEbiX9Wo-_EhEDOAdynSE,1356
5
+ repak/pypi.py,sha256=nKr4vn4Qjwizl159dw3pa7vQDvJVqoYVYPrD1iNMgdo,2328
6
+ repak/unpak_template.py,sha256=AkBMA5uMXtsdiuTGur3mSzvUWX0qcNLCFNgS3Q004Bo,3299
7
+ repak/versioning.py,sha256=1IAOuswvr-h7qiMsjJfmVsqbr1KkBIDsJ6So_zdjTgs,1284
8
+ repak/wheel.py,sha256=uLstphenb8NOnYokHilv4AOQSQjUTJ3G3grhaMgMhnQ,3206
9
+ repak-0.1.0.dist-info/METADATA,sha256=LnVCCUfxyOANOGMHF9e8vsMa4DQBdDjbf4qohlcX5kQ,250
10
+ repak-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ repak-0.1.0.dist-info/entry_points.txt,sha256=lX2p3GeQHHPMu5s5G6Z5Dv9PzGPv9GFAngXlcVOM6ho,41
12
+ repak-0.1.0.dist-info/top_level.txt,sha256=jDYOl_fY_uwpgjJPzWs4GdZ4G34NItkq9fXYMMtVPK8,6
13
+ repak-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ repak = repak.cli:main
@@ -0,0 +1 @@
1
+ repak