typvend 0.1.0__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,15 @@
1
+ name: Lint
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ prek:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v8.1.0
14
+ - run: uv sync --group dev
15
+ - run: uvx prek run -a
@@ -0,0 +1,48 @@
1
+ name: publish-pypi
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: publish-pypi-${{ github.ref }}
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ - uses: astral-sh/setup-uv@v8.1.0
22
+ with:
23
+ python-version: "3.14"
24
+ - name: Build distributions
25
+ run: uv build
26
+ - name: Upload distributions
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: python-package-distributions
30
+ path: dist/
31
+
32
+ publish:
33
+ if: startsWith(github.ref, 'refs/tags/v')
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: pypi
38
+ url: https://pypi.org/p/sylphy
39
+ permissions:
40
+ id-token: write
41
+ steps:
42
+ - name: Download distributions
43
+ uses: actions/download-artifact@v8
44
+ with:
45
+ name: python-package-distributions
46
+ path: dist/
47
+ - name: Publish package distributions to PyPI
48
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,21 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v8.1.0
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: uv sync --group dev
21
+ - run: uv run pytest
@@ -0,0 +1,19 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Local test outputs
13
+ test_*/
14
+ .pytest_cache/
15
+
16
+ # Editor / IA agents
17
+ .idea/
18
+ .claude/
19
+ .antigravitycli/
@@ -0,0 +1 @@
1
+ 3.13
typvend-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Diego Alvarez S.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
typvend-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: typvend
3
+ Version: 0.1.0
4
+ Summary: Typst Package Vendoring CLI
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 Diego Alvarez S.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ License-File: LICENSE
27
+ Requires-Python: >=3.11
28
+ Requires-Dist: niquests>=3.18.0
29
+ Requires-Dist: platformdirs>=4.9
30
+ Description-Content-Type: text/markdown
31
+
32
+ # typvend — Typst Package Vendoring CLI
33
+
34
+ `typvend` is a lightweight Python CLI utility designed to vendor official Typst packages locally for offline development, sandboxed builds, or containerized production CI/CD workflows.
35
+
36
+ ## Why?
37
+
38
+ Typst downloads packages on the fly at compile time with no official way to pre-download them.
39
+ This can be problematic for offline compilation and read-only production environments (like containers).
40
+ The solution is to either run the compilation once to fetch the packages or download them manually.
41
+
42
+ `typvend` simplifies this, downloading packages to the default Typst cache path or any directory you choose (then point Typst to it via `--package-cache-path`), in two ways:
43
+ - **Explicit:** `add <pkg>[@<version>]` — download specific packages by name, with a version or `@latest`.
44
+ - **Scan:** recursively find all `@preview/<pkg>:<version>` imports in `.typ` files and vendor them in one go.
45
+
46
+ ## Usage
47
+
48
+ ```bash
49
+ # Install and run instantly using uvx / pipx
50
+ uvx typvend --help
51
+ ```
52
+
53
+ Global options:
54
+ - `-o`, `--output DIR` — Custom directory to extract packages (defaults to native OS Typst search path).
55
+ - `--namespace NS` — Custom namespace (defaults to `preview`).
56
+ - `-f`, `--force` — Re-download package even if it already exists.
57
+ - `-v`, `--verbose` — Enable verbose output logs.
58
+
59
+ ### 1. Adding Packages Explicitly
60
+
61
+ ```bash
62
+ # Download latest version of fontawesome
63
+ uvx typvend add fontawesome
64
+
65
+ # Download specific versions
66
+ uvx typvend add fontawesome@0.5.0 cetz
67
+ ```
68
+
69
+ ### 2. Scanning Project Directories
70
+
71
+ Recursively searches a file or directory for package imports and vendors all discovered packages in one command:
72
+
73
+ ```bash
74
+ # Scan a templates directory and output packages to typst cache folder
75
+ uvx typvend scan ./templates
76
+
77
+ # Scan and output to a custom directory (e.g. for Docker cache stages)
78
+ uvx typvend scan ./templates --output /typst-packages
79
+ ```
@@ -0,0 +1,48 @@
1
+ # typvend — Typst Package Vendoring CLI
2
+
3
+ `typvend` is a lightweight Python CLI utility designed to vendor official Typst packages locally for offline development, sandboxed builds, or containerized production CI/CD workflows.
4
+
5
+ ## Why?
6
+
7
+ Typst downloads packages on the fly at compile time with no official way to pre-download them.
8
+ This can be problematic for offline compilation and read-only production environments (like containers).
9
+ The solution is to either run the compilation once to fetch the packages or download them manually.
10
+
11
+ `typvend` simplifies this, downloading packages to the default Typst cache path or any directory you choose (then point Typst to it via `--package-cache-path`), in two ways:
12
+ - **Explicit:** `add <pkg>[@<version>]` — download specific packages by name, with a version or `@latest`.
13
+ - **Scan:** recursively find all `@preview/<pkg>:<version>` imports in `.typ` files and vendor them in one go.
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ # Install and run instantly using uvx / pipx
19
+ uvx typvend --help
20
+ ```
21
+
22
+ Global options:
23
+ - `-o`, `--output DIR` — Custom directory to extract packages (defaults to native OS Typst search path).
24
+ - `--namespace NS` — Custom namespace (defaults to `preview`).
25
+ - `-f`, `--force` — Re-download package even if it already exists.
26
+ - `-v`, `--verbose` — Enable verbose output logs.
27
+
28
+ ### 1. Adding Packages Explicitly
29
+
30
+ ```bash
31
+ # Download latest version of fontawesome
32
+ uvx typvend add fontawesome
33
+
34
+ # Download specific versions
35
+ uvx typvend add fontawesome@0.5.0 cetz
36
+ ```
37
+
38
+ ### 2. Scanning Project Directories
39
+
40
+ Recursively searches a file or directory for package imports and vendors all discovered packages in one command:
41
+
42
+ ```bash
43
+ # Scan a templates directory and output packages to typst cache folder
44
+ uvx typvend scan ./templates
45
+
46
+ # Scan and output to a custom directory (e.g. for Docker cache stages)
47
+ uvx typvend scan ./templates --output /typst-packages
48
+ ```
typvend-0.1.0/justfile ADDED
@@ -0,0 +1,20 @@
1
+ # Development commands for typvend
2
+
3
+ # Run all formatting and linting checks
4
+ check: format lint typecheck test
5
+
6
+ # Format code using ruff
7
+ format:
8
+ uv run ruff format .
9
+
10
+ # Lint code using ruff
11
+ lint:
12
+ uv run ruff check .
13
+
14
+ # Run type checking using pyrefly
15
+ typecheck:
16
+ uv run pyrefly check
17
+
18
+ # Run unit tests using pytest
19
+ test:
20
+ uv run pytest
@@ -0,0 +1,42 @@
1
+ #:schema https://www.schemastore.org/prek.json
2
+
3
+ exclude = "^.idea/|^uv\\.lock$"
4
+
5
+ [[repos]]
6
+ repo = "https://github.com/pre-commit/pre-commit-hooks"
7
+ rev = "v6.0.0"
8
+ hooks = [
9
+ { id = "check-yaml" },
10
+ { id = "check-toml" },
11
+ { id = "end-of-file-fixer" },
12
+ { id = "trailing-whitespace" }
13
+ ]
14
+
15
+ [[repos]]
16
+ repo = "local"
17
+ hooks = [
18
+ {
19
+ id = "ruff-format",
20
+ name = "ruff-format",
21
+ entry = "uv run ruff format",
22
+ language = "system",
23
+ pass_filenames = false,
24
+ always_run = true
25
+ },
26
+ {
27
+ id = "ruff-check",
28
+ name = "ruff-check",
29
+ entry = "uv run ruff check --fix",
30
+ language = "system",
31
+ pass_filenames = false,
32
+ always_run = true
33
+ },
34
+ {
35
+ id = "pyrefly",
36
+ name = "pyrefly",
37
+ entry = "uv run pyrefly check",
38
+ language = "system",
39
+ pass_filenames = false,
40
+ always_run = true
41
+ },
42
+ ]
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "typvend"
3
+ version = "0.1.0"
4
+ description = "Typst Package Vendoring CLI"
5
+ readme = "README.md"
6
+ license = { file = "LICENSE" }
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "platformdirs>=4.9",
10
+ "niquests>=3.18.0",
11
+ ]
12
+
13
+ [project.scripts]
14
+ typvend = "typvend.cli:main"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=8.0",
23
+ "ruff>=0.4",
24
+ "pyrefly>=1.0",
25
+ ]
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/typvend"]
29
+
30
+ [tool.ruff]
31
+ target-version = "py311"
32
+ line-length = 100
33
+
34
+ [tool.ruff.lint]
35
+ select = ["ALL"]
36
+ ignore = [
37
+ "COM812", # Conflicts with formatter
38
+ "ISC001", # Conflicts with formatter
39
+ "S202", # tarfile.extractall is safe as we use filter/validation
40
+ "TRY301", # Abstract raise is too pedantic
41
+ "TRY400", # We explicitly log or swallow tracebacks based on verbose flag
42
+ "TRY401", # Redundant exception object is preferred for clarity
43
+ "BLE001", # Graceful top-level exception catching is standard in CLIs
44
+ ]
45
+
46
+ [tool.ruff.lint.pydocstyle]
47
+ convention = "google"
48
+
49
+ [tool.ruff.lint.per-file-ignores]
50
+ "tests/**/*.py" = [
51
+ "S101", # asserts are standard in pytest
52
+ ]
53
+
54
+ [tool.pyrefly]
55
+ project-includes = [
56
+ "**/*.py*",
57
+ ]
@@ -0,0 +1,7 @@
1
+ """typvend - Typst Package Vendoring CLI.
2
+
3
+ A robust Python utility to scan Typst files and vendor required preview
4
+ packages locally for offline usage.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for executing the package with python -m typvend."""
2
+
3
+ from typvend.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,202 @@
1
+ """Command-line interface for the typvend tool.
2
+
3
+ This module sets up the argument parser, handles the subcommands 'add' and 'scan',
4
+ and configures logging.
5
+ """
6
+
7
+ import argparse
8
+ import logging
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import niquests
14
+ import platformdirs
15
+
16
+ from typvend.downloader import download_package
17
+ from typvend.index import resolve_latest_version
18
+ from typvend.scanner import scan_path
19
+
20
+ logger = logging.getLogger("typvend")
21
+
22
+
23
+ def get_default_output() -> Path:
24
+ """Returns the platform-specific default Typst package directory.
25
+
26
+ Returns:
27
+ A Path object pointing to the system package directory.
28
+ """
29
+ return platformdirs.user_data_path("typst") / "packages"
30
+
31
+
32
+ def parse_package_arg(pkg: str) -> tuple[str, str]:
33
+ """Parses a package argument in format name[@version].
34
+
35
+ Args:
36
+ pkg: A string of the form "name" or "name@version".
37
+
38
+ Returns:
39
+ A tuple (name, version) where version is "latest" if not specified.
40
+
41
+ Raises:
42
+ ValueError: If the package name is empty or contains invalid characters.
43
+ """
44
+ if "@" in pkg:
45
+ parts = pkg.split("@", 1)
46
+ name = parts[0]
47
+ version = parts[1] or "latest"
48
+ else:
49
+ name = pkg
50
+ version = "latest"
51
+
52
+ if not name or not re.fullmatch(r"[a-zA-Z0-9_-]+", name):
53
+ msg = f"Invalid package name: '{name}'. Only alphanumeric, hyphens, underscores allowed."
54
+ raise ValueError(msg)
55
+
56
+ return name, version
57
+
58
+
59
+ def handle_add(args: argparse.Namespace) -> int:
60
+ """Handles the 'add' subcommand.
61
+
62
+ Args:
63
+ args: Parsed command-line arguments.
64
+
65
+ Returns:
66
+ 0 if all packages were successfully vendored, 1 otherwise.
67
+ """
68
+ namespace: str = args.namespace
69
+ output_dir = Path(args.output) if args.output else get_default_output()
70
+ force: bool = args.force
71
+
72
+ failed = False
73
+
74
+ # Type refinement
75
+ packages: list[str] = args.packages
76
+
77
+ for pkg_arg in packages:
78
+ name, version = parse_package_arg(pkg_arg)
79
+ try:
80
+ if version == "latest":
81
+ logger.info("Resolving latest version for %s...", name)
82
+ version = resolve_latest_version(name, namespace)
83
+ logger.info("Latest version resolved to %s", version)
84
+
85
+ download_package(
86
+ name=name,
87
+ version=version,
88
+ output_dir=output_dir,
89
+ namespace=namespace,
90
+ force=force,
91
+ )
92
+ except (ValueError, TypeError, niquests.RequestException, OSError):
93
+ failed = True
94
+ logger.error("Error vendoring package '%s'", pkg_arg, exc_info=args.verbose)
95
+
96
+ return 1 if failed else 0
97
+
98
+
99
+ def handle_scan(args: argparse.Namespace) -> int:
100
+ """Handles the 'scan' subcommand.
101
+
102
+ Args:
103
+ args: Parsed command-line arguments.
104
+
105
+ Returns:
106
+ 0 if all discovered packages were successfully vendored, 1 otherwise.
107
+ """
108
+ namespace: str = args.namespace
109
+ output_dir = Path(args.output) if args.output else get_default_output()
110
+ force: bool = args.force
111
+ scan_target = Path(args.path)
112
+
113
+ if not scan_target.exists():
114
+ logger.error("Scan path does not exist: %s", scan_target)
115
+ return 1
116
+
117
+ logger.info("Scanning %s for package imports...", scan_target)
118
+ packages = scan_path(scan_target, namespace)
119
+ logger.info("Discovered %d package(s): %s", len(packages), packages)
120
+
121
+ if not packages:
122
+ logger.info("No packages found to vendor.")
123
+ return 0
124
+
125
+ failed = False
126
+ for name, version in sorted(packages):
127
+ try:
128
+ download_package(
129
+ name=name,
130
+ version=version,
131
+ output_dir=output_dir,
132
+ namespace=namespace,
133
+ force=force,
134
+ )
135
+ except (ValueError, TypeError, niquests.RequestException, OSError):
136
+ failed = True
137
+ logger.error("Error vendoring package '%s:%s'", name, version, exc_info=args.verbose)
138
+
139
+ return 1 if failed else 0
140
+
141
+
142
+ def main() -> None:
143
+ """Main entry point for the CLI."""
144
+ parent_parser = argparse.ArgumentParser(add_help=False)
145
+ parent_parser.add_argument(
146
+ "-o", "--output", help="Custom output directory for vendored packages"
147
+ )
148
+ parent_parser.add_argument(
149
+ "--namespace",
150
+ default="preview",
151
+ help="Package namespace (default: preview)",
152
+ )
153
+ parent_parser.add_argument(
154
+ "-f",
155
+ "--force",
156
+ action="store_true",
157
+ help="Re-download package even if destination already exists",
158
+ )
159
+ parent_parser.add_argument(
160
+ "-v",
161
+ "--verbose",
162
+ action="store_true",
163
+ help="Enable verbose output logging",
164
+ )
165
+
166
+ parser = argparse.ArgumentParser(description="typvend — Typst Package Vendoring CLI")
167
+ subparsers = parser.add_subparsers(dest="command", required=True)
168
+
169
+ add_parser = subparsers.add_parser(
170
+ "add", parents=[parent_parser], help="Add explicit package(s) by name"
171
+ )
172
+ add_parser.add_argument(
173
+ "packages",
174
+ nargs="+",
175
+ help="Package name(s) optionally with version (e.g. fontawesome or fontawesome@0.6.0)",
176
+ )
177
+
178
+ scan_parser = subparsers.add_parser(
179
+ "scan",
180
+ parents=[parent_parser],
181
+ help="Scan files/directories and vendor all discovered package imports",
182
+ )
183
+ scan_parser.add_argument("path", help="Path to file or directory to scan for imports")
184
+
185
+ args = parser.parse_args()
186
+
187
+ # Configure logging
188
+ log_level = logging.INFO if args.verbose else logging.WARNING
189
+ logging.basicConfig(
190
+ level=log_level,
191
+ format="%(levelname)s: %(message)s",
192
+ )
193
+ logger.setLevel(log_level)
194
+
195
+ if args.command == "add":
196
+ code = handle_add(args)
197
+ elif args.command == "scan":
198
+ code = handle_scan(args)
199
+ else:
200
+ code = 1
201
+
202
+ sys.exit(code)
@@ -0,0 +1,88 @@
1
+ """Module for downloading and extracting Typst packages.
2
+
3
+ This module provides functions to fetch a package tarball from the Typst
4
+ repository, verify its paths to prevent directory traversal, and extract
5
+ its contents to the local vendor directory.
6
+ """
7
+
8
+ import io
9
+ import logging
10
+ import sys
11
+ import tarfile
12
+ from pathlib import Path
13
+
14
+ import niquests
15
+
16
+ from typvend import __version__
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def download_package(
22
+ name: str,
23
+ version: str,
24
+ output_dir: Path,
25
+ namespace: str = "preview",
26
+ *,
27
+ force: bool = False,
28
+ ) -> bool:
29
+ """Downloads and extracts a Typst package to the local directory.
30
+
31
+ Args:
32
+ name: The name of the package.
33
+ version: The package version (e.g. "0.6.0").
34
+ output_dir: The base vendor output directory.
35
+ namespace: The namespace of the package (e.g. "preview").
36
+ force: If True, overwrite the package even if it already exists.
37
+
38
+ Returns:
39
+ True if the package was downloaded and extracted, False if it was
40
+ skipped because it already exists and force was False.
41
+
42
+ Raises:
43
+ ValueError: If a directory traversal is detected in the tarball.
44
+ niquests.RequestException: If the package download fails.
45
+ """
46
+ dest_dir = output_dir / namespace / name / version
47
+ if dest_dir.exists() and not force:
48
+ logger.info(
49
+ "Skipping package %s:%s (already exists at %s)",
50
+ name,
51
+ version,
52
+ dest_dir,
53
+ )
54
+ return False
55
+
56
+ url = f"https://packages.typst.org/{namespace}/{name}-{version}.tar.gz"
57
+ logger.info("Downloading %s...", url)
58
+ headers = {"User-Agent": f"typvend/{__version__}"}
59
+
60
+ try:
61
+ response = niquests.get(url, headers=headers, timeout=30)
62
+ response.raise_for_status()
63
+ content = response.content
64
+ if not content:
65
+ msg = f"Failed to download package: no content received from {url}"
66
+ raise ValueError(msg)
67
+ except niquests.RequestException as e:
68
+ logger.error("Failed to download package %s:%s: %s", name, version, e)
69
+ raise
70
+
71
+ logger.info("Extracting %s to %s...", name, dest_dir)
72
+ dest_dir.mkdir(parents=True, exist_ok=True)
73
+ dest_abs = dest_dir.resolve()
74
+
75
+ tar_data = io.BytesIO(content)
76
+ with tarfile.open(fileobj=tar_data, mode="r:gz") as tar:
77
+ if sys.version_info >= (3, 12):
78
+ tar.extractall(path=dest_dir, filter="data")
79
+ else:
80
+ for member in tar.getmembers():
81
+ member_path = (dest_dir / member.name).resolve()
82
+ if dest_abs not in member_path.parents and member_path != dest_abs:
83
+ msg = f"Attempted directory traversal in tarball: {member.name}"
84
+ raise ValueError(msg)
85
+ tar.extractall(path=dest_dir)
86
+
87
+ logger.info("Successfully vendored %s:%s", name, version)
88
+ return True