ctxgraph-code 0.4.1__tar.gz → 0.5.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.
Files changed (38) hide show
  1. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/PKG-INFO +4 -1
  2. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/pyproject.toml +5 -1
  3. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/treesitter/analyzer.py +10 -2
  4. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/cli.py +49 -26
  5. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/config/settings.py +8 -0
  6. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/graph/builder.py +56 -11
  7. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code.egg-info/PKG-INFO +4 -1
  8. ctxgraph_code-0.5.0/src/ctxgraph_code.egg-info/requires.txt +9 -0
  9. ctxgraph_code-0.4.1/src/ctxgraph_code.egg-info/requires.txt +0 -5
  10. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/README.md +0 -0
  11. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/setup.cfg +0 -0
  12. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/__init__.py +0 -0
  13. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/__main__.py +0 -0
  14. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/__init__.py +0 -0
  15. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/python/__init__.py +0 -0
  16. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/python/importer.py +0 -0
  17. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/python/semantic.py +0 -0
  18. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/python/symbols.py +0 -0
  19. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/treesitter/__init__.py +0 -0
  20. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/analyzers/treesitter/languages.py +0 -0
  21. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/config/__init__.py +0 -0
  22. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/config/build_status.py +0 -0
  23. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/config/global_paths.py +0 -0
  24. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/config/hooks.py +0 -0
  25. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/config/init.py +0 -0
  26. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/exclude/__init__.py +0 -0
  27. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/exclude/patterns.py +0 -0
  28. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/graph/__init__.py +0 -0
  29. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/graph/models.py +0 -0
  30. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/graph/query.py +0 -0
  31. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/graph/storage.py +0 -0
  32. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/render.py +0 -0
  33. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/view/__init__.py +0 -0
  34. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code/view/visualizer.py +0 -0
  35. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code.egg-info/SOURCES.txt +0 -0
  36. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code.egg-info/dependency_links.txt +0 -0
  37. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code.egg-info/entry_points.txt +0 -0
  38. {ctxgraph_code-0.4.1 → ctxgraph_code-0.5.0}/src/ctxgraph_code.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxgraph-code
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions.
5
5
  Author: ctxgraph-code contributors
6
6
  License: MIT
@@ -18,6 +18,9 @@ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  Requires-Dist: typer>=0.9
20
20
  Requires-Dist: rich>=13.0
21
+ Provides-Extra: full
22
+ Requires-Dist: tree-sitter>=0.22; extra == "full"
23
+ Requires-Dist: tree-sitter-language-pack>=0.13; extra == "full"
21
24
  Provides-Extra: dev
22
25
  Requires-Dist: pytest>=7.0; extra == "dev"
23
26
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ctxgraph-code"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -30,6 +30,10 @@ dependencies = [
30
30
  ]
31
31
 
32
32
  [project.optional-dependencies]
33
+ full = [
34
+ "tree-sitter>=0.22",
35
+ "tree-sitter-language-pack>=0.13",
36
+ ]
33
37
  dev = [
34
38
  "pytest>=7.0",
35
39
  ]
@@ -38,8 +38,16 @@ class TSAnalyzer:
38
38
  if not self.can_handle():
39
39
  return TSAnalyzerResult()
40
40
 
41
- import tree_sitter as ts
42
- from tree_sitter_language_pack import get_language
41
+ try:
42
+ import tree_sitter as ts
43
+ from tree_sitter_language_pack import get_language
44
+ except ImportError:
45
+ import warnings
46
+ warnings.warn(
47
+ f"Missing tree-sitter dependency for {self.lang_name} files. "
48
+ "Install with: pip install 'ctxgraph-code[full]'"
49
+ )
50
+ raise
43
51
 
44
52
  lang = self._get_lang(self.lang_name)
45
53
  if not lang:
@@ -1,4 +1,4 @@
1
- from __future__ import annotations
1
+ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import time
@@ -245,12 +245,8 @@ def _write_slash_command(slash_path: Path, path: Path):
245
245
  content = SLASH_COMMAND_TEMPLATE.format(build_time=build_label, available=avail_str)
246
246
 
247
247
  slash_path.parent.mkdir(parents=True, exist_ok=True)
248
- if not slash_path.exists():
249
- slash_path.write_text(content, encoding="utf-8")
250
- console.print(f"[green][OK] Created [bold]{slash_path}[/bold][/green]")
251
- else:
252
- console.print(f"[yellow] Skipped (already exists): [bold]{slash_path}[/bold][/yellow]")
253
- console.print("[yellow] To overwrite, delete it and run again.[/yellow]")
248
+ slash_path.write_text(content, encoding="utf-8")
249
+ console.print(f"[green][OK] {'Updated' if slash_path.exists() else 'Created'} [bold]{slash_path}[/bold][/green]")
254
250
 
255
251
 
256
252
  # ── slash command template ───────────────────────────────────────────────────
@@ -311,8 +307,7 @@ def _print_build_hint(path: Path):
311
307
 
312
308
  def _build_single_graph(path, exts, user_patterns, db_path, label,
313
309
  incremental=False, verbose=False, no_summary=False):
314
- ctx_dir = path / ".ctxgraph"
315
- mark_build_started(ctx_dir, os.getpid())
310
+ mark_build_started(path, os.getpid())
316
311
  start = time.time()
317
312
  with console.status(f"Scanning {', '.join(exts)} files for '{label}'..."):
318
313
  stats = build_graph(
@@ -324,7 +319,7 @@ def _build_single_graph(path, exts, user_patterns, db_path, label,
324
319
  verbose=verbose,
325
320
  no_summary=no_summary,
326
321
  )
327
- mark_build_complete(ctx_dir, time.time() - start)
322
+ mark_build_complete(path, time.time() - start)
328
323
 
329
324
  table = Table(title=f"Graph Build: {label}")
330
325
  table.add_column("Metric", style="cyan")
@@ -341,8 +336,7 @@ def _build_single_graph(path, exts, user_patterns, db_path, label,
341
336
  def _build_dir_worker(path, exts, user_patterns, db_path, label,
342
337
  incremental=False, verbose=False, no_summary=False):
343
338
  """Silent build worker for parallel execution. No console output."""
344
- ctx_dir = path / ".ctxgraph"
345
- mark_build_started(ctx_dir, os.getpid())
339
+ mark_build_started(path, os.getpid())
346
340
  start = time.time()
347
341
  stats = build_graph(
348
342
  path,
@@ -353,7 +347,7 @@ def _build_dir_worker(path, exts, user_patterns, db_path, label,
353
347
  verbose=verbose,
354
348
  no_summary=no_summary,
355
349
  )
356
- mark_build_complete(ctx_dir, time.time() - start)
350
+ mark_build_complete(path, time.time() - start)
357
351
  return label, stats
358
352
 
359
353
 
@@ -363,9 +357,11 @@ def _build_dirs_parallel(path, exts, user_patterns, top_dirs, graphs_dir, jobs,
363
357
  from concurrent.futures import ThreadPoolExecutor, as_completed
364
358
 
365
359
  n_workers = max(1, jobs) if jobs > 0 else os.cpu_count() or 1
366
- console.print(f"[dim]Building {len(top_dirs)} graphs with {n_workers} workers...[/dim]")
360
+ total = len(top_dirs)
361
+ console.print(f"[bold]Building {total} graphs with {n_workers} workers...[/bold]")
367
362
 
368
- futures = []
363
+ futures = {}
364
+ _build_progress_start = time.time()
369
365
  with ThreadPoolExecutor(max_workers=n_workers) as pool:
370
366
  for d in top_dirs:
371
367
  db_path = graphs_dir / f"{d.name}.db"
@@ -373,15 +369,37 @@ def _build_dirs_parallel(path, exts, user_patterns, top_dirs, graphs_dir, jobs,
373
369
  _build_dir_worker, path, exts, user_patterns, db_path, d.name,
374
370
  incremental, verbose, no_summary,
375
371
  )
376
- futures.append(fut)
372
+ futures[fut] = d.name
377
373
 
378
374
  results = []
375
+ completed = 0
376
+ err_count = 0
377
+
379
378
  for f in as_completed(futures):
379
+ label = futures[f]
380
+ completed += 1
380
381
  try:
381
- label, stats = f.result()
382
- results.append((label, stats))
382
+ lbl, stats = f.result()
383
+ results.append((lbl, stats))
384
+ files = stats.get("files_analyzed", 0)
385
+ nodes = stats.get("total_nodes", 0)
386
+ edges = stats.get("total_edges", 0)
387
+ t = stats.get("elapsed_seconds", 0)
388
+ console.print(
389
+ f" [green]✔[/green] {label}/ "
390
+ f"({files} files, {nodes} nodes, {edges} edges, {t}s)"
391
+ )
383
392
  except Exception as e:
384
- results.append(("error", str(e)))
393
+ results.append((label, str(e)))
394
+ err_count += 1
395
+ console.print(f" [red]✘[/red] {label}/ ([red]{e}[/red])")
396
+
397
+ elapsed_total = time.time() - _build_progress_start
398
+ if err_count:
399
+ console.print(f"\n[yellow]Built {total - err_count}/{total} graphs"
400
+ f" ({err_count} failed) in {elapsed_total:.1f}s[/yellow]")
401
+ else:
402
+ console.print(f"\n[green]Built all {total} graphs in {elapsed_total:.1f}s[/green]")
385
403
 
386
404
  results.sort(key=lambda x: x[0] if isinstance(x[0], str) else "")
387
405
 
@@ -413,9 +431,8 @@ def _build_dirs_parallel(path, exts, user_patterns, top_dirs, graphs_dir, jobs,
413
431
 
414
432
  console.print(summary)
415
433
  console.print(
416
- f"\n[green]Built {len(top_dirs)} graphs: "
417
- f"{total_files} files, {total_nodes} nodes, {total_edges} edges "
418
- f"in {total_time}s[/green]"
434
+ f"[green]Total: {total_files} files, {total_nodes} nodes, {total_edges} edges "
435
+ f"in {elapsed_total:.1f}s[/green]"
419
436
  )
420
437
  return results
421
438
 
@@ -921,7 +938,7 @@ def probe(
921
938
  5, "--max", "-m", help="Maximum files to probe"
922
939
  ),
923
940
  context_lines: int = typer.Option(
924
- 3, "--context", "-c", help="Lines of context around matches"
941
+ 40, "--context", "-c", help="Number of lines to show per file"
925
942
  ),
926
943
  dir_name: Optional[str] = typer.Option(
927
944
  None, "--dir", "-d", help="Directory graph to query"
@@ -971,10 +988,16 @@ def probe(
971
988
 
972
989
  if code:
973
990
  lines = code.splitlines()
974
- snippet_lines = lines[:40]
991
+ n = context_lines if context_lines > 0 else len(lines)
992
+ snippet_lines = lines[:n]
975
993
  snippet = "\n".join(snippet_lines)
976
- extra = f"\n... ({len(lines) - 40} more lines)" if len(lines) > 40 else ""
977
- syntax = Syntax(snippet + extra, "python", theme="monokai", line_numbers=True)
994
+ extra = f"\n... ({len(lines) - n} more lines)" if len(lines) > n else ""
995
+ lang = "python"
996
+ if node.path:
997
+ ext = Path(node.path).suffix.lower()
998
+ lang_map = {".js": "javascript", ".ts": "typescript", ".tsx": "typescript", ".jsx": "javascript", ".go": "go", ".rs": "rust", ".c": "c", ".h": "c", ".cpp": "cpp", ".java": "java", ".rb": "ruby", ".kt": "kotlin", ".swift": "swift", ".cs": "csharp", ".scala": "scala", ".lua": "lua", ".zig": "zig", ".php": "php", ".sh": "bash", ".ps1": "powershell", ".json": "json", ".yaml": "yaml", ".yml": "yaml"}
999
+ lang = lang_map.get(ext, "python")
1000
+ syntax = Syntax(snippet + extra, lang, theme="monokai", line_numbers=True)
978
1001
  panel = Panel(syntax, title=header, border_style="dim")
979
1002
  console.print(panel)
980
1003
  else:
@@ -52,6 +52,14 @@ class Settings:
52
52
  def exclude_patterns(self) -> list[str]:
53
53
  return self._data["graph"].get("exclude", [])
54
54
 
55
+ @property
56
+ def follow_symlinks(self) -> bool:
57
+ return self._data["graph"].get("follow_symlinks", False)
58
+
59
+ @property
60
+ def max_file_size_mb(self) -> int:
61
+ return self._data["graph"].get("max_file_size_mb", 5)
62
+
55
63
  def to_dict(self) -> dict:
56
64
  return dict(self._data)
57
65
 
@@ -59,6 +59,11 @@ def build_graph(
59
59
  if jobs <= 0:
60
60
  jobs = (os.cpu_count() or 1)
61
61
 
62
+ from ctxgraph_code.config.settings import Settings
63
+ _settings = Settings(repo_path)
64
+ follow_symlinks = _settings.follow_symlinks
65
+ max_bytes = _settings.max_file_size_mb * 1024 * 1024
66
+
62
67
  @lru_cache(maxsize=None)
63
68
  def _should_exclude(path: Path) -> bool:
64
69
  return should_exclude(path, repo_path, exclude_patterns)
@@ -68,7 +73,7 @@ def build_graph(
68
73
  scan_files: list[Path] = []
69
74
  current_mtimes: dict[str, float] = {}
70
75
 
71
- for dirpath, dirnames, filenames in os.walk(repo_path):
76
+ for dirpath, dirnames, filenames in os.walk(repo_path, followlinks=follow_symlinks):
72
77
  dirnames[:] = [
73
78
  d for d in dirnames
74
79
  if not _should_exclude(repo_path / d)
@@ -140,7 +145,7 @@ def build_graph(
140
145
 
141
146
  with mp.Pool(jobs) as pool:
142
147
  results = [
143
- pool.apply_async(_worker_batch, (chunk, repo_path, path_index, no_summary))
148
+ pool.apply_async(_worker_batch, (chunk, repo_path, path_index, no_summary, max_bytes))
144
149
  for chunk in chunks
145
150
  ]
146
151
  for i, r in enumerate(results):
@@ -159,7 +164,7 @@ def build_graph(
159
164
  else:
160
165
  for i, file_path in enumerate(changed_files):
161
166
  try:
162
- nds, eds, fh = _process_file(file_path, repo_path, path_index, no_summary)
167
+ nds, eds, fh = _process_file(file_path, repo_path, path_index, no_summary, max_bytes)
163
168
  all_nodes.extend(nds)
164
169
  all_edges.extend(eds)
165
170
  file_hashes.update(fh)
@@ -213,14 +218,22 @@ def build_graph(
213
218
  # ── worker helpers ───────────────────────────────────────────────────────────
214
219
 
215
220
 
216
- def _quick_scan(source: str) -> bool:
217
- """Quick pre-check: does this file have imports, classes, functions,
218
- or decorators? Returns True if full AST parsing is needed."""
221
+ def _quick_scan(source: str, is_python: bool = True) -> bool:
222
+ """Quick pre-check: does this file have meaningful code?
223
+ Returns True if full parsing is needed."""
219
224
  for line in source.splitlines():
220
225
  line = line.strip()
221
- if not line or line.startswith("#"):
226
+ if not line or line.startswith(("#", "//", "/*", "*", "--", ";")) and is_python:
227
+ continue
228
+ if not line:
222
229
  continue
223
- if any(line.startswith(kw) for kw in ("import ", "from ", "class ", "def ", "@")):
230
+ if is_python:
231
+ if any(line.startswith(kw) for kw in ("import ", "from ", "class ", "def ", "@")):
232
+ return True
233
+ else:
234
+ if any(kw in line for kw in ("function", "class", "struct", "trait", "interface", "import ", "include ", "fn ", "def ", "pub ", "export ", "impl ")):
235
+ return True
236
+ if any(c in line for c in ("{", "(", "=", ";")):
224
237
  return True
225
238
  return False
226
239
 
@@ -234,9 +247,17 @@ def _process_file(
234
247
  root_path: Path,
235
248
  path_index: set[str],
236
249
  no_summary: bool = False,
250
+ max_bytes: int = 0,
237
251
  ) -> tuple[list[dict], list[dict], dict[str, str]]:
238
252
  rel = str(file_path.relative_to(root_path)).replace("\\", "/")
239
253
 
254
+ if max_bytes > 0:
255
+ try:
256
+ if file_path.stat().st_size > max_bytes:
257
+ return [], [], {}
258
+ except OSError:
259
+ pass
260
+
240
261
  try:
241
262
  source = file_path.read_text(encoding="utf-8", errors="replace")
242
263
  except OSError:
@@ -261,7 +282,7 @@ def _process_python(
261
282
  ) -> tuple[list[dict], list[dict], dict[str, str]]:
262
283
  fhash = fhash or {}
263
284
 
264
- if not _quick_scan(source):
285
+ if not _quick_scan(source, is_python=True):
265
286
  return [{
266
287
  "id": f"{root_path}:{rel}",
267
288
  "type": "file",
@@ -320,13 +341,36 @@ def _process_treesitter(
320
341
  fhash: dict[str, str] | None = None,
321
342
  ) -> tuple[list[dict], list[dict], dict[str, str]]:
322
343
  fhash = fhash or {}
344
+
345
+ if not _quick_scan(source, is_python=False):
346
+ return [{
347
+ "id": f"{root_path}:{rel}",
348
+ "type": "file",
349
+ "name": file_path.name,
350
+ "path": rel,
351
+ "parent_id": None,
352
+ "summary": None,
353
+ "importance": 0.5,
354
+ "size_bytes": len(source),
355
+ "lineno": 0,
356
+ }], [], fhash
357
+
323
358
  from ctxgraph_code.analyzers.treesitter import TSAnalyzer
324
359
 
325
360
  analyzer = TSAnalyzer(file_path, root_path)
326
361
  if not analyzer.can_handle():
327
362
  return [], [], fhash
328
363
 
329
- result = analyzer.analyze(source)
364
+ import warnings
365
+ try:
366
+ result = analyzer.analyze(source)
367
+ except ImportError:
368
+ warnings.warn(
369
+ f"tree-sitter not installed — skipping {rel}. "
370
+ "Install with: pip install 'ctxgraph-code[full]'"
371
+ )
372
+ return [], [], fhash
373
+
330
374
  return result.nodes, result.edges, fhash
331
375
 
332
376
 
@@ -335,6 +379,7 @@ def _worker_batch(
335
379
  root_path: Path,
336
380
  path_index: set[str],
337
381
  no_summary: bool = False,
382
+ max_bytes: int = 0,
338
383
  ) -> tuple[list[dict], list[dict], int, int, dict[str, str]]:
339
384
  nodes: list[dict] = []
340
385
  edges: list[dict] = []
@@ -343,7 +388,7 @@ def _worker_batch(
343
388
  err = 0
344
389
  for fp in file_paths:
345
390
  try:
346
- nds, eds, fh = _process_file(fp, root_path, path_index, no_summary)
391
+ nds, eds, fh = _process_file(fp, root_path, path_index, no_summary, max_bytes)
347
392
  nodes.extend(nds)
348
393
  edges.extend(eds)
349
394
  hashes.update(fh)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxgraph-code
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions.
5
5
  Author: ctxgraph-code contributors
6
6
  License: MIT
@@ -18,6 +18,9 @@ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  Requires-Dist: typer>=0.9
20
20
  Requires-Dist: rich>=13.0
21
+ Provides-Extra: full
22
+ Requires-Dist: tree-sitter>=0.22; extra == "full"
23
+ Requires-Dist: tree-sitter-language-pack>=0.13; extra == "full"
21
24
  Provides-Extra: dev
22
25
  Requires-Dist: pytest>=7.0; extra == "dev"
23
26
 
@@ -0,0 +1,9 @@
1
+ typer>=0.9
2
+ rich>=13.0
3
+
4
+ [dev]
5
+ pytest>=7.0
6
+
7
+ [full]
8
+ tree-sitter>=0.22
9
+ tree-sitter-language-pack>=0.13
@@ -1,5 +0,0 @@
1
- typer>=0.9
2
- rich>=13.0
3
-
4
- [dev]
5
- pytest>=7.0
File without changes
File without changes