""" python3 -m docgen extract python --root SRC [-o ir.json] [--exclude NAME ...] python3 -m docgen extract code --root SRC [-o ir.json] [--ext .cs ...] python3 -m docgen extract db --schema schema.json [-o ir.json] python3 -m docgen extract openapi --spec spec.yaml [-o ir.json] python3 -m docgen extract usage --har session.har [-o ir.json] Source -> IR, one reader per source type. Writes to stdout without `-o`, so it composes with `view` and `emit`. """ import argparse import json from collections import Counter from pathlib import Path from .common import PROG, Abort, dump_ir def _parser(reader: str, source_flag: str, help_: str) -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog=f"{PROG} extract {reader}") p.add_argument(source_flag, "-s", required=True, type=Path, help=help_) p.add_argument("--output", "-o", type=Path, help="Where to write. Default stdout.") return p def _counts(g) -> str: c = Counter(n.kind for n in g.nodes) return " · ".join(f"{v} {k}" for k, v in sorted(c.items(), key=lambda kv: -kv[1])) def python(argv) -> int: p = _parser("python", "--root", "Tree to read.") p.add_argument("--exclude", action="append", default=[], help="Directory name to skip.") args = p.parse_args(argv) from ..extractors.python import extract try: g = extract(args.root, exclude=tuple(args.exclude)) except (NotADirectoryError, OSError) as e: raise Abort(str(e)) from None unparsed = sum(1 for n in g.nodes if n.attrs.get("error")) dump_ir(g, args.output, f"{len(g.nodes)} nodes, {len(g.edges)} edges -> {args.output}" + (f" ({unparsed} file(s) unparsed)" if unparsed else "")) return 0 def code(argv) -> int: p = _parser("code", "--root", "Tree to read.") p.add_argument("--ext", action="append", default=[], help="Limit to these extensions. Default: every registered one.") p.add_argument("--exclude", action="append", default=[], help="Directory name to skip.") args = p.parse_args(argv) from ..extractors.code import MissingParser, extract try: g = extract(args.root, suffixes=args.ext or None, exclude=tuple(args.exclude)) except (MissingParser, NotADirectoryError, OSError) as e: raise Abort(str(e)) from None dump_ir(g, args.output, f"{len(g.nodes)} nodes -> {args.output} {_counts(g)}") return 0 def db(argv) -> int: args = _parser("db", "--schema", "A graphgen-compatible schema.json, as modelgen emits.").parse_args(argv) from ..extractors.db import extract try: g = extract(args.schema) except (OSError, json.JSONDecodeError, KeyError) as e: raise Abort(f"could not read {args.schema}: {e}") from None dump_ir(g, args.output, f"{len(g.nodes)} nodes, {len(g.edges)} edges -> {args.output}") return 0 def openapi(argv) -> int: args = _parser("openapi", "--spec", "An OpenAPI document.").parse_args(argv) from ..extractors.openapi import extract try: g = extract(args.spec) except (OSError, ImportError, ValueError) as e: raise Abort(str(e)) from None eps = sum(1 for n in g.nodes if n.kind == "endpoint") dump_ir(g, args.output, f"{len(g.nodes)} nodes ({eps} endpoints), {len(g.edges)} edges -> {args.output}") return 0 def usage(argv) -> int: args = _parser("usage", "--har", "A HAR recording, as devtools/mitmproxy/Charles export.").parse_args(argv) from ..extractors.usage import extract try: g = extract(args.har) except (OSError, json.JSONDecodeError, KeyError) as e: raise Abort(f"could not read {args.har}: {e}") from None eps = sum(1 for n in g.nodes if n.kind == "endpoint") ops = sum(1 for n in g.nodes if n.kind == "operation") dump_ir(g, args.output, f"{len(g.nodes)} nodes ({eps} endpoints, {ops} graphql), " f"{len(g.edges)} sequence edges -> {args.output}") return 0 READERS = {"python": python, "code": code, "db": db, "openapi": openapi, "usage": usage} def main(argv) -> int: if not argv or argv[0] in ("-h", "--help"): print(__doc__.strip("\n")) return 0 if argv else 2 if argv[0] not in READERS: raise Abort(f"no reader {argv[0]!r} — have: {', '.join(READERS)}") return READERS[argv[0]](argv[1:])