peta 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.
peta/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """peta - Human-friendly Python package metadata viewer."""
peta/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Entry point for ``python -m peta``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from peta.cli.app import run
6
+
7
+ run()
peta/__metadata__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Metadata for the project."""
2
+
3
+ PROJECT_NAME: str = "peta"
4
+ """Name of the project."""
5
+
6
+ __all__ = ["PROJECT_NAME"]
peta/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
peta/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Command-line interface."""
peta/cli/app.py ADDED
@@ -0,0 +1,214 @@
1
+ """Typer application and command registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from importlib.metadata import Distribution, PackageNotFoundError
7
+ from typing import Annotated, cast
8
+
9
+ import typer
10
+
11
+ from peta.__metadata__ import PROJECT_NAME
12
+ from peta.cli.commands import (
13
+ compare as compare_mod,
14
+ deps as deps_mod,
15
+ files as files_mod,
16
+ info as info_mod,
17
+ versions as versions_mod,
18
+ )
19
+ from peta.cli.output.console import resolve_color
20
+ from peta.cli.state import CliState
21
+
22
+ __all__ = ["compare", "deps", "files", "info", "main", "run", "versions"]
23
+
24
+
25
+ _SUBCOMMANDS = {
26
+ "info",
27
+ "deps",
28
+ "files",
29
+ "versions",
30
+ "compare",
31
+ "--help",
32
+ "-h",
33
+ "--version",
34
+ "-V",
35
+ }
36
+
37
+ app = typer.Typer(
38
+ name="peta",
39
+ help="Human-friendly Python package metadata viewer.",
40
+ no_args_is_help=True,
41
+ context_settings={"help_option_names": ["-h", "--help"]},
42
+ )
43
+
44
+
45
+ def _version_callback(value: bool) -> None:
46
+ if not value:
47
+ return
48
+ try:
49
+ distribution = Distribution.from_name(PROJECT_NAME)
50
+ except PackageNotFoundError as exc:
51
+ typer.echo("Error: peta package metadata not found.", err=True)
52
+ raise typer.Exit(code=1) from exc
53
+ typer.echo(f"peta {distribution.version}")
54
+ raise typer.Exit
55
+
56
+
57
+ def _color_from_ctx(ctx: typer.Context) -> bool:
58
+ """Read the resolved color setting stashed on the root context.
59
+
60
+ Returns:
61
+ The resolved color flag, or ``False`` if unavailable (e.g. the root
62
+ callback did not run, as can happen when invoking a command object
63
+ directly in tests).
64
+ """
65
+ obj = cast("object", ctx.obj)
66
+ return obj.color if isinstance(obj, CliState) else False
67
+
68
+
69
+ @app.callback(invoke_without_command=True)
70
+ def main(
71
+ ctx: typer.Context,
72
+ _version: Annotated[
73
+ bool,
74
+ typer.Option(
75
+ "--version",
76
+ "-V",
77
+ callback=_version_callback,
78
+ is_eager=True,
79
+ help="Show version and exit.",
80
+ ),
81
+ ] = False,
82
+ no_color: Annotated[
83
+ bool, typer.Option("--no-color", help="Disable colored output.")
84
+ ] = False,
85
+ ) -> None:
86
+ """Human-friendly Python package metadata viewer."""
87
+ ctx.obj = CliState(color=resolve_color(no_color=no_color))
88
+
89
+
90
+ @app.command()
91
+ def info(
92
+ ctx: typer.Context,
93
+ package: Annotated[
94
+ str, typer.Argument(help="Package name (optionally name==version).")
95
+ ],
96
+ use_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
97
+ local: Annotated[
98
+ bool, typer.Option("--local", "-l", help="Force local lookup.")
99
+ ] = False,
100
+ remote: Annotated[
101
+ bool, typer.Option("--remote", "-r", help="Force PyPI lookup.")
102
+ ] = False,
103
+ no_osv: Annotated[
104
+ bool, typer.Option("--no-osv", help="Skip the OSV vulnerability lookup.")
105
+ ] = False,
106
+ no_stats: Annotated[
107
+ bool, typer.Option("--no-stats", help="Skip download/dependent count lookups.")
108
+ ] = False,
109
+ ) -> None:
110
+ """Show detailed package metadata."""
111
+ info_mod.info(
112
+ package,
113
+ use_json=use_json,
114
+ local=local,
115
+ remote=remote,
116
+ color=_color_from_ctx(ctx),
117
+ no_osv=no_osv,
118
+ no_stats=no_stats,
119
+ )
120
+
121
+
122
+ @app.command()
123
+ def compare(
124
+ ctx: typer.Context,
125
+ a: Annotated[str, typer.Argument(help="First package name.")],
126
+ b: Annotated[str, typer.Argument(help="Second package name.")],
127
+ use_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
128
+ local: Annotated[
129
+ bool, typer.Option("--local", "-l", help="Force local lookup.")
130
+ ] = False,
131
+ remote: Annotated[
132
+ bool, typer.Option("--remote", "-r", help="Force PyPI lookup.")
133
+ ] = False,
134
+ no_osv: Annotated[
135
+ bool, typer.Option("--no-osv", help="Skip the OSV vulnerability lookup.")
136
+ ] = False,
137
+ no_stats: Annotated[
138
+ bool, typer.Option("--no-stats", help="Skip download/dependent count lookups.")
139
+ ] = False,
140
+ ) -> None:
141
+ """Compare two packages' metadata side by side."""
142
+ compare_mod.compare(
143
+ a,
144
+ b,
145
+ use_json=use_json,
146
+ local=local,
147
+ remote=remote,
148
+ color=_color_from_ctx(ctx),
149
+ no_osv=no_osv,
150
+ no_stats=no_stats,
151
+ )
152
+
153
+
154
+ @app.command()
155
+ def deps(
156
+ ctx: typer.Context,
157
+ package: Annotated[str, typer.Argument(help="Package name.")],
158
+ use_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
159
+ local: Annotated[
160
+ bool, typer.Option("--local", "-l", help="Force local lookup.")
161
+ ] = False,
162
+ remote: Annotated[
163
+ bool, typer.Option("--remote", "-r", help="Force PyPI lookup.")
164
+ ] = False,
165
+ why: Annotated[
166
+ str | None, typer.Option("--why", help="Show why TARGET is a dependency.")
167
+ ] = None,
168
+ depth: Annotated[
169
+ int, typer.Option("--depth", min=1, help="Max recursion depth.")
170
+ ] = 10,
171
+ ) -> None:
172
+ """Show a package's recursive dependency tree."""
173
+ deps_mod.deps(
174
+ package,
175
+ use_json=use_json,
176
+ local=local,
177
+ remote=remote,
178
+ color=_color_from_ctx(ctx),
179
+ why=why,
180
+ depth=depth,
181
+ )
182
+
183
+
184
+ @app.command()
185
+ def files(
186
+ ctx: typer.Context,
187
+ package: Annotated[str, typer.Argument(help="Package name.")],
188
+ use_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
189
+ ) -> None:
190
+ """List files installed by a local package."""
191
+ files_mod.files(package, use_json=use_json, color=_color_from_ctx(ctx))
192
+
193
+
194
+ @app.command()
195
+ def versions(
196
+ ctx: typer.Context,
197
+ package: Annotated[str, typer.Argument(help="Package name.")],
198
+ use_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
199
+ limit: Annotated[
200
+ int, typer.Option("--limit", "-n", min=1, help="Max versions to show.")
201
+ ] = 20,
202
+ ) -> None:
203
+ """Show published versions of a package from PyPI."""
204
+ versions_mod.versions(
205
+ package, use_json=use_json, limit=limit, color=_color_from_ctx(ctx)
206
+ )
207
+
208
+
209
+ def run() -> None:
210
+ """Entry point; ``peta <package>`` is shorthand for ``peta info <package>``."""
211
+ args = sys.argv[1:]
212
+ if args and args[0] not in _SUBCOMMANDS and not args[0].startswith("-"):
213
+ sys.argv.insert(1, "info")
214
+ app()
@@ -0,0 +1 @@
1
+ """CLI command handlers."""
@@ -0,0 +1,70 @@
1
+ """The ``peta compare`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import typer
8
+
9
+ from peta.cli.output.json import format_compare as json_format
10
+ from peta.cli.output.tables import render_compare as rich_format
11
+ from peta.core.enrich import enrich
12
+ from peta.core.local import PackageNotFoundError as LocalNotFound
13
+ from peta.core.remote import NetworkError, PackageNotFoundError as RemoteNotFound
14
+ from peta.core.resolve import resolve_package
15
+
16
+ if TYPE_CHECKING:
17
+ from peta.core.models import PackageInfo
18
+
19
+ __all__ = ["compare"]
20
+
21
+
22
+ # Tuple constant (not an inline ``except (A, B)`` literal) so the ruff formatter
23
+ # cannot strip the parentheses into Python-2-only ``except A, B`` syntax.
24
+ _NOT_FOUND = (LocalNotFound, RemoteNotFound)
25
+
26
+
27
+ def _resolve_and_enrich(
28
+ package: str, *, local: bool, remote: bool, no_osv: bool, no_stats: bool
29
+ ) -> PackageInfo:
30
+ pkg = resolve_package(package, local=local, remote=remote)
31
+ return enrich(pkg, no_osv=no_osv, no_stats=no_stats)
32
+
33
+
34
+ def compare(
35
+ a: str,
36
+ b: str,
37
+ *,
38
+ use_json: bool = False,
39
+ local: bool = False,
40
+ remote: bool = False,
41
+ color: bool = False,
42
+ no_osv: bool = False,
43
+ no_stats: bool = False,
44
+ ) -> None:
45
+ """Compare two packages' metadata side by side.
46
+
47
+ Raises:
48
+ Exit: With code 1 if either package is not found, or code 2 on network
49
+ failure.
50
+ """
51
+ try:
52
+ a_pkg = _resolve_and_enrich(
53
+ a, local=local, remote=remote, no_osv=no_osv, no_stats=no_stats
54
+ )
55
+ b_pkg = _resolve_and_enrich(
56
+ b, local=local, remote=remote, no_osv=no_osv, no_stats=no_stats
57
+ )
58
+ except _NOT_FOUND as exc:
59
+ version = getattr(exc, "version", None)
60
+ target = f"{exc.name}=={version}" if version else exc.name
61
+ typer.echo(f"Package '{target}' not found.", err=True)
62
+ raise typer.Exit(code=1) from None
63
+ except NetworkError as exc:
64
+ typer.echo(str(exc), err=True)
65
+ raise typer.Exit(code=2) from None
66
+ typer.echo(
67
+ json_format(a_pkg, b_pkg)
68
+ if use_json
69
+ else rich_format(a_pkg, b_pkg, color=color)
70
+ )
@@ -0,0 +1,81 @@
1
+ """The ``peta deps`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from peta.cli.output.json import format_dep_tree, format_why
8
+ from peta.cli.output.tables import render_dep_tree, render_why
9
+ from peta.core.deptree import build_tree, find_why
10
+ from peta.core.local import PackageNotFoundError as LocalNotFound
11
+ from peta.core.remote import NetworkError, PackageNotFoundError as RemoteNotFound
12
+
13
+ __all__ = ["deps"]
14
+
15
+
16
+ # Tuple constant (not an inline ``except (A, B)`` literal) so the ruff formatter
17
+ # cannot strip the parentheses into Python-2-only ``except A, B`` syntax.
18
+ _NOT_FOUND = (LocalNotFound, RemoteNotFound)
19
+
20
+
21
+ def _print_why(
22
+ package: str,
23
+ target: str,
24
+ paths: list[list[str]],
25
+ *,
26
+ use_json: bool,
27
+ color: bool,
28
+ depth: int,
29
+ ) -> None:
30
+ if not paths:
31
+ msg = (
32
+ f"'{target}' was not found in the dependency tree of "
33
+ f"'{package}' (depth {depth})."
34
+ )
35
+ typer.echo(msg, err=True)
36
+ raise typer.Exit(code=1)
37
+ typer.echo(
38
+ format_why(target, paths)
39
+ if use_json
40
+ else render_why(target, paths, color=color)
41
+ )
42
+
43
+
44
+ def deps(
45
+ package: str,
46
+ *,
47
+ use_json: bool = False,
48
+ local: bool = False,
49
+ remote: bool = False,
50
+ color: bool = False,
51
+ why: str | None = None,
52
+ depth: int = 10,
53
+ ) -> None:
54
+ """Show a package's recursive dependency tree, or ``--why`` a target is pulled in.
55
+
56
+ Raises:
57
+ Exit: With code 1 if the package (or, with ``--why``, the target) is
58
+ not found, or code 2 on network failure.
59
+ """
60
+ try:
61
+ tree = build_tree(package, local=local, remote=remote, max_depth=depth)
62
+ except _NOT_FOUND:
63
+ typer.echo(f"Package '{package}' not found.", err=True)
64
+ raise typer.Exit(code=1) from None
65
+ except NetworkError as exc:
66
+ typer.echo(str(exc), err=True)
67
+ raise typer.Exit(code=2) from None
68
+
69
+ if why is not None:
70
+ _print_why(
71
+ package,
72
+ why,
73
+ find_why(tree, why),
74
+ use_json=use_json,
75
+ color=color,
76
+ depth=depth,
77
+ )
78
+ return
79
+ typer.echo(
80
+ format_dep_tree(tree) if use_json else render_dep_tree(tree, color=color)
81
+ )
@@ -0,0 +1,28 @@
1
+ """The ``peta files`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from peta.cli.output.json import format_files as json_format
8
+ from peta.cli.output.tables import render_files as rich_format
9
+ from peta.core.local import (
10
+ PackageNotFoundError as LocalNotFound,
11
+ get_package as local_get_package,
12
+ )
13
+
14
+ __all__ = ["files"]
15
+
16
+
17
+ def files(package: str, *, use_json: bool = False, color: bool = False) -> None:
18
+ """List files installed by a local package.
19
+
20
+ Raises:
21
+ Exit: With code 1 if the package is not installed locally.
22
+ """
23
+ try:
24
+ pkg = local_get_package(package)
25
+ except LocalNotFound:
26
+ typer.echo(f"Package '{package}' not found locally.", err=True)
27
+ raise typer.Exit(code=1) from None
28
+ typer.echo(json_format(pkg) if use_json else rich_format(pkg, color=color))
@@ -0,0 +1,59 @@
1
+ """The ``peta info`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import typer
8
+
9
+ from peta.cli.output.json import format_info as json_format
10
+ from peta.cli.output.tables import render_info as rich_format
11
+ from peta.core.enrich import enrich
12
+ from peta.core.local import PackageNotFoundError as LocalNotFound
13
+ from peta.core.remote import NetworkError, PackageNotFoundError as RemoteNotFound
14
+ from peta.core.resolve import resolve_package
15
+
16
+ if TYPE_CHECKING:
17
+ from peta.core.models import PackageInfo
18
+
19
+ __all__ = ["info"]
20
+
21
+
22
+ # Tuple constant (not an inline ``except (A, B)`` literal) so the ruff formatter
23
+ # cannot strip the parentheses into Python-2-only ``except A, B`` syntax.
24
+ _NOT_FOUND = (LocalNotFound, RemoteNotFound)
25
+
26
+
27
+ def _resolve_and_enrich(
28
+ package: str, *, local: bool, remote: bool, no_osv: bool, no_stats: bool
29
+ ) -> PackageInfo:
30
+ pkg = resolve_package(package, local=local, remote=remote)
31
+ return enrich(pkg, no_osv=no_osv, no_stats=no_stats)
32
+
33
+
34
+ def info(
35
+ package: str,
36
+ *,
37
+ use_json: bool = False,
38
+ local: bool = False,
39
+ remote: bool = False,
40
+ color: bool = False,
41
+ no_osv: bool = False,
42
+ no_stats: bool = False,
43
+ ) -> None:
44
+ """Show detailed package metadata.
45
+
46
+ Raises:
47
+ Exit: With code 1 if the package is not found, or code 2 on network failure.
48
+ """
49
+ try:
50
+ pkg = _resolve_and_enrich(
51
+ package, local=local, remote=remote, no_osv=no_osv, no_stats=no_stats
52
+ )
53
+ except _NOT_FOUND:
54
+ typer.echo(f"Package '{package}' not found.", err=True)
55
+ raise typer.Exit(code=1) from None
56
+ except NetworkError as exc:
57
+ typer.echo(str(exc), err=True)
58
+ raise typer.Exit(code=2) from None
59
+ typer.echo(json_format(pkg) if use_json else rich_format(pkg, color=color))
@@ -0,0 +1,161 @@
1
+ """The ``peta versions`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import operator
6
+ from typing import TYPE_CHECKING, cast
7
+
8
+ import httpx
9
+ import typer
10
+ from packaging.version import InvalidVersion, Version
11
+
12
+ from peta.cli.output.json import format_versions as json_format
13
+ from peta.cli.output.tables import render_versions as rich_format
14
+ from peta.core.remote import DEFAULT_TIMEOUT, PYPI_BASE_URL, NetworkError
15
+
16
+ if TYPE_CHECKING:
17
+ from peta.core.remote import PyPIReleaseFile
18
+
19
+ __all__ = ["get_versions", "versions"]
20
+
21
+
22
+ def _sorted_version_keys(releases: dict[str, list[PyPIReleaseFile]]) -> list[str]:
23
+ """Return release keys newest-first, tolerating non-PEP-440 keys.
24
+
25
+ ``packaging.version.Version`` raises ``InvalidVersion`` on legacy keys that
26
+ PyPI still serves, so parse defensively: PEP 440 versions sort newest-first,
27
+ and any unparsable keys are kept (sorted after) rather than letting one bad
28
+ key abort the whole listing with a traceback.
29
+
30
+ Returns:
31
+ Release keys, PEP 440 versions newest-first, then non-PEP-440 keys.
32
+ """
33
+ valid: list[tuple[Version, str]] = []
34
+ invalid: list[str] = []
35
+ for ver in releases:
36
+ try:
37
+ valid.append((Version(ver), ver))
38
+ except InvalidVersion:
39
+ invalid.append(ver)
40
+ valid.sort(key=operator.itemgetter(0), reverse=True)
41
+ return [ver for _, ver in valid] + sorted(invalid, reverse=True)
42
+
43
+
44
+ def _extract_releases(body: object) -> dict[str, list[PyPIReleaseFile]]:
45
+ """Validate and pull the ``releases`` mapping out of a decoded PyPI body.
46
+
47
+ Returns:
48
+ The release mapping, with any non-list release values dropped.
49
+
50
+ Raises:
51
+ NetworkError: If the body, or its ``releases`` field, is not a dict.
52
+ """
53
+ if not isinstance(body, dict):
54
+ msg = "malformed response from PyPI"
55
+ raise NetworkError(msg)
56
+ mapping = cast("dict[str, object]", body)
57
+ releases = mapping.get("releases", {})
58
+ if not isinstance(releases, dict):
59
+ msg = "malformed response from PyPI"
60
+ raise NetworkError(msg)
61
+ releases_map = cast("dict[str, object]", releases)
62
+ return {
63
+ ver: cleaned
64
+ for ver, raw in releases_map.items()
65
+ if (cleaned := _clean_files(raw)) is not None
66
+ }
67
+
68
+
69
+ def _clean_files(raw: object) -> list[PyPIReleaseFile] | None:
70
+ """Coerce one release's file list, dropping non-dict members.
71
+
72
+ Returns:
73
+ The list of file dicts, or ``None`` if ``raw`` is not a list at all.
74
+ """
75
+ if not isinstance(raw, list):
76
+ return None
77
+ items = cast("list[object]", raw)
78
+ return cast("list[PyPIReleaseFile]", [f for f in items if isinstance(f, dict)])
79
+
80
+
81
+ def _decode_body(response: httpx.Response) -> object:
82
+ """Decode a JSON response body, mapping a decode failure to NetworkError.
83
+
84
+ Returns:
85
+ The decoded body as an untyped object.
86
+
87
+ Raises:
88
+ NetworkError: If the body is not valid JSON.
89
+ """
90
+ try:
91
+ return cast("object", response.json())
92
+ except ValueError as exc: # includes json.JSONDecodeError
93
+ msg = "malformed response from PyPI"
94
+ raise NetworkError(msg) from exc
95
+
96
+
97
+ def get_versions(name: str) -> list[dict[str, str]]:
98
+ """Fetch all published versions for a package from PyPI.
99
+
100
+ Returns:
101
+ A list of ``{"version", "upload_time"}`` dicts, newest first; empty
102
+ if the package is not found (HTTP 404).
103
+
104
+ Raises:
105
+ NetworkError: If the request fails, returns a non-success status, or
106
+ the decoded body is malformed (not a dict, or ``releases`` is not
107
+ a dict).
108
+ """
109
+ url = f"{PYPI_BASE_URL}/{name}/json"
110
+ try:
111
+ response = httpx.get(url, timeout=DEFAULT_TIMEOUT)
112
+ except httpx.RequestError as exc:
113
+ raise NetworkError(str(exc)) from exc
114
+
115
+ if response.status_code == 404: # noqa: PLR2004
116
+ return []
117
+
118
+ try:
119
+ _ = response.raise_for_status()
120
+ except httpx.HTTPStatusError as exc:
121
+ msg = f"PyPI returned HTTP {exc.response.status_code}"
122
+ raise NetworkError(msg) from exc
123
+
124
+ # The PyPI JSON API is untyped and its shape is not guaranteed, so
125
+ # validate the decoded body before casting into our typed view.
126
+ releases = _extract_releases(_decode_body(response))
127
+ result: list[dict[str, str]] = []
128
+ for ver in _sorted_version_keys(releases):
129
+ files = releases[ver]
130
+ raw_time: object = files[0].get("upload_time", "") if files else ""
131
+ upload_time = raw_time[:10] if isinstance(raw_time, str) else ""
132
+ result.append({"version": ver, "upload_time": upload_time})
133
+ return result
134
+
135
+
136
+ # Patch target used by tests.
137
+ remote_get_versions = get_versions
138
+
139
+
140
+ def versions(
141
+ package: str, *, use_json: bool = False, limit: int = 20, color: bool = False
142
+ ) -> None:
143
+ """Show published versions of a package from PyPI.
144
+
145
+ Raises:
146
+ Exit: With code 2 on network failure, or code 1 if the package is absent.
147
+ """
148
+ try:
149
+ vers = remote_get_versions(package)
150
+ except NetworkError as exc:
151
+ typer.echo(str(exc), err=True)
152
+ raise typer.Exit(code=2) from None
153
+ if not vers:
154
+ typer.echo(f"Package '{package}' not found on PyPI.", err=True)
155
+ raise typer.Exit(code=1) from None
156
+ shown = vers[:limit]
157
+ typer.echo(
158
+ json_format(package, shown)
159
+ if use_json
160
+ else rich_format(package, shown, color=color)
161
+ )
@@ -0,0 +1 @@
1
+ """Output formatters."""