cmap-core 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,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ *.pyc
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.5
2
+ Name: cmap-core
3
+ Version: 0.1.0
4
+ Summary: Code Map: inspect Python code structure and relationships from the terminal.
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: rich<15,>=13.9
7
+ Requires-Dist: typer<1,>=0.16
@@ -0,0 +1,22 @@
1
+ # CMap
2
+
3
+ CMap (Code Map) is a terminal-first Python code analysis tool. It scans Python source with the standard-library AST and exposes symbols, calls, imports, call traces, dependency information, and circular dependency detection.
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ uv sync
9
+ uv run pytest
10
+ uv run cmap-core --help
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ cmap-core scan .
17
+ cmap-core trace . main
18
+ cmap-core callers . parse
19
+ cmap-core callees . main
20
+ cmap-core deps .
21
+ cmap-core circular .
22
+ ```
@@ -0,0 +1,11 @@
1
+ def load() -> str:
2
+ return "data"
3
+
4
+ def parse(value: str) -> dict[str, str]:
5
+ return {"value": value}
6
+
7
+ def main() -> dict[str, str]:
8
+ return parse(load())
9
+
10
+ if __name__ == "__main__":
11
+ print(main())
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "cmap-core"
3
+ version = "0.1.0"
4
+ description = "Code Map: inspect Python code structure and relationships from the terminal."
5
+ requires-python = ">=3.12"
6
+ dependencies = ["typer>=0.16,<1", "rich>=13.9,<15"]
7
+
8
+ [project.scripts]
9
+ cmap-core = "cmap_core.main:app"
10
+
11
+ [dependency-groups]
12
+ dev = ["pytest>=8,<9"]
13
+
14
+ [tool.pytest.ini_options]
15
+ testpaths = ["tests"]
16
+ addopts = "-q"
17
+
18
+ [tool.ruff]
19
+ target-version = "py312"
20
+ line-length = 88
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/cmap_core"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .main import app
2
+
3
+ app()
@@ -0,0 +1,68 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from .cmap_models import Call, CodeMap, Import, Symbol
4
+
5
+ class Analyzer(ast.NodeVisitor):
6
+ def __init__(self, path: Path) -> None:
7
+ self.path = path
8
+ self.result = CodeMap()
9
+ self.scope: list[str] = []
10
+
11
+ @property
12
+ def current(self) -> str:
13
+ return ".".join(self.scope) or "<module>"
14
+
15
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
16
+ self._visit_function(node)
17
+
18
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
19
+ self._visit_function(node)
20
+
21
+ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
22
+ parent = self.current if self.scope else None
23
+ symbol_id = f"{self.path}:{'.'.join((*self.scope, node.name))}"
24
+ self.result.symbols.append(Symbol(symbol_id, node.name, "function", str(self.path), node.lineno, parent))
25
+ self.scope.append(node.name)
26
+ self.generic_visit(node)
27
+ self.scope.pop()
28
+
29
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
30
+ parent = self.current if self.scope else None
31
+ symbol_id = f"{self.path}:{'.'.join((*self.scope, node.name))}"
32
+ self.result.symbols.append(Symbol(symbol_id, node.name, "class", str(self.path), node.lineno, parent))
33
+ self.scope.append(node.name)
34
+ self.generic_visit(node)
35
+ self.scope.pop()
36
+
37
+ def visit_Call(self, node: ast.Call) -> None:
38
+ if isinstance(node.func, ast.Name):
39
+ callee = node.func.id
40
+ elif isinstance(node.func, ast.Attribute):
41
+ callee = ast.unparse(node.func)
42
+ else:
43
+ callee = ast.unparse(node.func)
44
+ self.result.calls.append(Call(self.current, callee, str(self.path), node.lineno))
45
+ self.generic_visit(node)
46
+
47
+ def visit_Import(self, node: ast.Import) -> None:
48
+ for alias in node.names:
49
+ self.result.imports.append(Import(alias.name, str(self.path), node.lineno))
50
+
51
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
52
+ module = "." * node.level + (node.module or "")
53
+ self.result.imports.append(Import(module, str(self.path), node.lineno))
54
+
55
+ def analyze(path: Path) -> CodeMap:
56
+ from .cmap_ast import parse
57
+ analyzer = Analyzer(path)
58
+ analyzer.visit(parse(path))
59
+ return analyzer.result
60
+
61
+ def analyze_paths(paths: list[Path]) -> CodeMap:
62
+ result = CodeMap()
63
+ for path in paths:
64
+ current = analyze(path)
65
+ result.symbols.extend(current.symbols)
66
+ result.calls.extend(current.calls)
67
+ result.imports.extend(current.imports)
68
+ return result
@@ -0,0 +1,5 @@
1
+ import ast
2
+ from pathlib import Path
3
+
4
+ def parse(path: Path) -> ast.Module:
5
+ return ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
@@ -0,0 +1,67 @@
1
+ from collections import defaultdict
2
+ from pathlib import Path
3
+ from .cmap_models import CodeMap
4
+
5
+ def callers(graph: CodeMap, target: str) -> list[str]:
6
+ return sorted({call.caller for call in graph.calls if call.callee == target})
7
+
8
+ def callees(graph: CodeMap, target: str) -> list[str]:
9
+ return sorted({call.callee for call in graph.calls if call.caller == target})
10
+
11
+ def dependencies(graph: CodeMap) -> dict[str, set[str]]:
12
+ result: dict[str, set[str]] = defaultdict(set)
13
+ for item in graph.imports:
14
+ result[item.file].add(item.module)
15
+ return dict(result)
16
+
17
+ def _module_name(path: str, root: Path) -> str:
18
+ relative = Path(path).relative_to(root)
19
+ parts = list(relative.with_suffix("").parts)
20
+ if parts[-1] == "__init__":
21
+ parts.pop()
22
+ return ".".join(parts)
23
+
24
+ def circular_dependencies(graph: CodeMap, root: Path) -> list[list[str]]:
25
+ files = {Path(symbol.file) for symbol in graph.symbols}
26
+ modules = {_module_name(str(path), root): path for path in files}
27
+ edges: dict[Path, set[Path]] = defaultdict(set)
28
+ for item in graph.imports:
29
+ source = Path(item.file)
30
+ for module, target in modules.items():
31
+ if item.module == module or item.module.startswith(f"{module}."):
32
+ edges[source].add(target)
33
+ visited: set[Path] = set()
34
+ stack: list[Path] = []
35
+ found: list[list[str]] = []
36
+
37
+ def visit(node: Path) -> None:
38
+ if node in stack:
39
+ cycle = stack[stack.index(node):] + [node]
40
+ names = [str(item) for item in cycle]
41
+ if names not in found:
42
+ found.append(names)
43
+ return
44
+ if node in visited:
45
+ return
46
+ visited.add(node)
47
+ stack.append(node)
48
+ for target in edges.get(node, ()):
49
+ visit(target)
50
+ stack.pop()
51
+
52
+ for node in files:
53
+ visit(node)
54
+ return found
55
+
56
+ def trace(graph: CodeMap, target: str) -> list[tuple[str, str]]:
57
+ result: list[tuple[str, str]] = []
58
+ queue = [target]
59
+ seen = {target}
60
+ while queue:
61
+ current = queue.pop(0)
62
+ for callee in callees(graph, current):
63
+ result.append((current, callee))
64
+ if callee not in seen:
65
+ seen.add(callee)
66
+ queue.append(callee)
67
+ return result
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ @dataclass(frozen=True, slots=True)
4
+ class Symbol:
5
+ id: str
6
+ name: str
7
+ kind: str
8
+ file: str
9
+ line: int
10
+ parent: str | None = None
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class Call:
14
+ caller: str
15
+ callee: str
16
+ file: str
17
+ line: int
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class Import:
21
+ module: str
22
+ file: str
23
+ line: int
24
+
25
+ @dataclass(slots=True)
26
+ class CodeMap:
27
+ symbols: list[Symbol] = field(default_factory=list)
28
+ calls: list[Call] = field(default_factory=list)
29
+ imports: list[Import] = field(default_factory=list)
@@ -0,0 +1,14 @@
1
+ import json
2
+ from dataclasses import asdict
3
+ from .cmap_models import CodeMap
4
+
5
+ def text(graph: CodeMap) -> str:
6
+ lines = [f"{symbol.kind} {symbol.name} {symbol.file}:{symbol.line}" for symbol in graph.symbols]
7
+ if graph.calls:
8
+ lines += ["", "Calls", *[f" {call.caller} -> {call.callee}" for call in graph.calls]]
9
+ if graph.imports:
10
+ lines += ["", "Imports", *[f" {item.file} -> {item.module}" for item in graph.imports]]
11
+ return "\n".join(lines)
12
+
13
+ def json_output(graph: CodeMap) -> str:
14
+ return json.dumps({"symbols": [asdict(item) for item in graph.symbols], "calls": [asdict(item) for item in graph.calls], "imports": [asdict(item) for item in graph.imports]}, indent=2)
@@ -0,0 +1,6 @@
1
+ from pathlib import Path
2
+
3
+ def python_files(root: Path) -> list[Path]:
4
+ if root.is_file():
5
+ return [root] if root.suffix == ".py" else []
6
+ return sorted(path for path in root.rglob("*.py") if ".venv" not in path.parts and "__pycache__" not in path.parts)
@@ -0,0 +1,66 @@
1
+ from pathlib import Path
2
+ from typing import Annotated
3
+ import json
4
+ import typer
5
+ from rich.console import Console
6
+ from . import __version__
7
+ from .cmap_analyzer import analyze_paths
8
+ from .cmap_graph import callers, callees, circular_dependencies, dependencies, trace
9
+ from .cmap_render import json_output, text
10
+ from .cmap_scan import python_files
11
+
12
+ app = typer.Typer(help="CMap — Code Map for Python.", no_args_is_help=True)
13
+ console = Console()
14
+
15
+ @app.command()
16
+ def version() -> None:
17
+ """Show the CMap version."""
18
+ typer.echo(__version__)
19
+
20
+ @app.command()
21
+ def scan(path: Annotated[Path, typer.Argument(exists=True, readable=True)], json_mode: Annotated[bool, typer.Option("--json")] = False) -> None:
22
+ """Scan Python files and show their structure."""
23
+ graph = analyze_paths(python_files(path))
24
+ typer.echo(json_output(graph) if json_mode else text(graph))
25
+
26
+ @app.command("trace")
27
+ def trace_code(path: Annotated[Path, typer.Argument(exists=True, readable=True)], target: Annotated[str, typer.Argument()], json_mode: Annotated[bool, typer.Option("--json")] = False) -> None:
28
+ """Show the reachable calls from a function."""
29
+ graph = analyze_paths(python_files(path))
30
+ result = trace(graph, target)
31
+ typer.echo(json.dumps(result) if json_mode else "\n".join(f"{a} -> {b}" for a, b in result))
32
+
33
+ @app.command("callers")
34
+ def callers_code(path: Annotated[Path, typer.Argument(exists=True, readable=True)], target: Annotated[str, typer.Argument()]) -> None:
35
+ """Show callers of a function."""
36
+ graph = analyze_paths(python_files(path))
37
+ typer.echo("\n".join(callers(graph, target)))
38
+
39
+ @app.command("callees")
40
+ def callees_code(path: Annotated[Path, typer.Argument(exists=True, readable=True)], target: Annotated[str, typer.Argument()]) -> None:
41
+ """Show functions called by a function."""
42
+ graph = analyze_paths(python_files(path))
43
+ typer.echo("\n".join(callees(graph, target)))
44
+
45
+ @app.command()
46
+ def deps(path: Annotated[Path, typer.Argument(exists=True, readable=True)]) -> None:
47
+ """Show Python import dependencies."""
48
+ graph = analyze_paths(python_files(path))
49
+ for source, modules in dependencies(graph).items():
50
+ typer.echo(source)
51
+ for module in sorted(modules):
52
+ typer.echo(f" -> {module}")
53
+
54
+ @app.command()
55
+ def circular(path: Annotated[Path, typer.Argument(exists=True, readable=True)]) -> None:
56
+ """Find circular local import dependencies."""
57
+ graph = analyze_paths(python_files(path))
58
+ cycles = circular_dependencies(graph)
59
+ if not cycles:
60
+ typer.echo("No circular dependencies found.")
61
+ return
62
+ for cycle in cycles:
63
+ typer.echo(" -> ".join(cycle))
64
+
65
+ if __name__ == "__main__":
66
+ app()
@@ -0,0 +1,10 @@
1
+ from pathlib import Path
2
+ from cmap_core.cmap_analyzer import analyze
3
+
4
+ def test_analyze_collects_symbols_calls_and_imports(tmp_path: Path) -> None:
5
+ path = tmp_path / "example.py"
6
+ path.write_text("import os\n\ndef main():\n print(os.getcwd())\n", encoding="utf-8")
7
+ graph = analyze(path)
8
+ assert any(symbol.name == "main" for symbol in graph.symbols)
9
+ assert any(call.callee == "print" for call in graph.calls)
10
+ assert graph.imports[0].module == "os"
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+ from cmap_core.cmap_ast import parse
3
+
4
+ def test_parse(tmp_path: Path) -> None:
5
+ path = tmp_path / "example.py"
6
+ path.write_text("def main():\n print('ok')\n", encoding="utf-8")
7
+ tree = parse(path)
8
+ assert tree.body[0].name == "main"
@@ -0,0 +1,7 @@
1
+ from cmap_core.cmap_graph import CodeMap, Call, callers, callees, trace
2
+
3
+ def test_graph_queries() -> None:
4
+ graph = CodeMap(calls=[Call("main", "load", "x.py", 1), Call("load", "parse", "x.py", 2)])
5
+ assert callers(graph, "load") == ["main"]
6
+ assert callees(graph, "main") == ["load"]
7
+ assert trace(graph, "main") == [("main", "load"), ("load", "parse")]
@@ -0,0 +1,154 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "annotated-doc"
7
+ version = "0.0.5"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "cmap-core"
16
+ version = "0.1.0"
17
+ source = { editable = "." }
18
+ dependencies = [
19
+ { name = "rich" },
20
+ { name = "typer" },
21
+ ]
22
+
23
+ [package.dev-dependencies]
24
+ dev = [
25
+ { name = "pytest" },
26
+ ]
27
+
28
+ [package.metadata]
29
+ requires-dist = [
30
+ { name = "rich", specifier = ">=13.9,<15" },
31
+ { name = "typer", specifier = ">=0.16,<1" },
32
+ ]
33
+
34
+ [package.metadata.requires-dev]
35
+ dev = [{ name = "pytest", specifier = ">=8,<9" }]
36
+
37
+ [[package]]
38
+ name = "colorama"
39
+ version = "0.4.6"
40
+ source = { registry = "https://pypi.org/simple" }
41
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
42
+ wheels = [
43
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
44
+ ]
45
+
46
+ [[package]]
47
+ name = "iniconfig"
48
+ version = "2.3.0"
49
+ source = { registry = "https://pypi.org/simple" }
50
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
51
+ wheels = [
52
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
53
+ ]
54
+
55
+ [[package]]
56
+ name = "markdown-it-py"
57
+ version = "4.2.0"
58
+ source = { registry = "https://pypi.org/simple" }
59
+ dependencies = [
60
+ { name = "mdurl" },
61
+ ]
62
+ sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
63
+ wheels = [
64
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
65
+ ]
66
+
67
+ [[package]]
68
+ name = "mdurl"
69
+ version = "0.1.2"
70
+ source = { registry = "https://pypi.org/simple" }
71
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
72
+ wheels = [
73
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
74
+ ]
75
+
76
+ [[package]]
77
+ name = "packaging"
78
+ version = "26.3"
79
+ source = { registry = "https://pypi.org/simple" }
80
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
81
+ wheels = [
82
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
83
+ ]
84
+
85
+ [[package]]
86
+ name = "pluggy"
87
+ version = "1.6.0"
88
+ source = { registry = "https://pypi.org/simple" }
89
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
90
+ wheels = [
91
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
92
+ ]
93
+
94
+ [[package]]
95
+ name = "pygments"
96
+ version = "2.21.0"
97
+ source = { registry = "https://pypi.org/simple" }
98
+ sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
99
+ wheels = [
100
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
101
+ ]
102
+
103
+ [[package]]
104
+ name = "pytest"
105
+ version = "8.4.2"
106
+ source = { registry = "https://pypi.org/simple" }
107
+ dependencies = [
108
+ { name = "colorama", marker = "sys_platform == 'win32'" },
109
+ { name = "iniconfig" },
110
+ { name = "packaging" },
111
+ { name = "pluggy" },
112
+ { name = "pygments" },
113
+ ]
114
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
115
+ wheels = [
116
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
117
+ ]
118
+
119
+ [[package]]
120
+ name = "rich"
121
+ version = "14.3.4"
122
+ source = { registry = "https://pypi.org/simple" }
123
+ dependencies = [
124
+ { name = "markdown-it-py" },
125
+ { name = "pygments" },
126
+ ]
127
+ sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" }
128
+ wheels = [
129
+ { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" },
130
+ ]
131
+
132
+ [[package]]
133
+ name = "shellingham"
134
+ version = "1.5.4"
135
+ source = { registry = "https://pypi.org/simple" }
136
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
137
+ wheels = [
138
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
139
+ ]
140
+
141
+ [[package]]
142
+ name = "typer"
143
+ version = "0.27.2"
144
+ source = { registry = "https://pypi.org/simple" }
145
+ dependencies = [
146
+ { name = "annotated-doc" },
147
+ { name = "colorama", marker = "sys_platform == 'win32'" },
148
+ { name = "rich" },
149
+ { name = "shellingham" },
150
+ ]
151
+ sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
152
+ wheels = [
153
+ { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
154
+ ]