pygraph-mcp 0.2.2__tar.gz → 0.2.4__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 (75) hide show
  1. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/PKG-INFO +1 -1
  2. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/pyproject.toml +1 -1
  3. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/flask.py +60 -1
  4. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/implements.py +2 -0
  5. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/types.py +2 -0
  6. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_extractors.py +70 -0
  7. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/uv.lock +1 -1
  8. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/.github/workflows/publish.yml +0 -0
  9. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/.gitignore +0 -0
  10. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/AGENTS.md +0 -0
  11. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/README.md +0 -0
  12. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/TODOS.md +0 -0
  13. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/opencode.json +0 -0
  14. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/__init__.py +0 -0
  15. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/__main__.py +0 -0
  16. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/builder.py +0 -0
  17. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/cli.py +0 -0
  18. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/__init__.py +0 -0
  19. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/boundaries.py +0 -0
  20. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/callees.py +0 -0
  21. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/callers.py +0 -0
  22. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/changes.py +0 -0
  23. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/complexity.py +0 -0
  24. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/context.py +0 -0
  25. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/coupling.py +0 -0
  26. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/deps.py +0 -0
  27. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/focus.py +0 -0
  28. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/graph_report.py +0 -0
  29. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/hotspot.py +0 -0
  30. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/impact.py +0 -0
  31. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/imports_cmd.py +0 -0
  32. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/node.py +0 -0
  33. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/opencode_plugin.py +0 -0
  34. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/orphans.py +0 -0
  35. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/path_cmd.py +0 -0
  36. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/plan.py +0 -0
  37. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/public.py +0 -0
  38. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/query_cmd.py +0 -0
  39. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/review.py +0 -0
  40. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/source.py +0 -0
  41. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/stale.py +0 -0
  42. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/commands/trace.py +0 -0
  43. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/config.py +0 -0
  44. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/__init__.py +0 -0
  45. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/calls.py +0 -0
  46. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/decorators.py +0 -0
  47. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/env.py +0 -0
  48. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/errors.py +0 -0
  49. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/http_calls.py +0 -0
  50. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/imports.py +0 -0
  51. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/symbols.py +0 -0
  52. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/extractors/tests.py +0 -0
  53. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/__init__.py +0 -0
  54. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/boundaries.py +0 -0
  55. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/cache.py +0 -0
  56. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/graph/serialize.py +0 -0
  57. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/query.py +0 -0
  58. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/scanner/__init__.py +0 -0
  59. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/scanner/gitignore.py +0 -0
  60. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/scanner/walker.py +0 -0
  61. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/src/pygraph/server.py +0 -0
  62. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/__init__.py +0 -0
  63. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/conftest.py +0 -0
  64. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/fixtures/__init__.py +0 -0
  65. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_boundaries.py +0 -0
  66. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_changes_stale.py +0 -0
  67. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_commands.py +0 -0
  68. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_graph_report.py +0 -0
  69. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_graph_types.py +0 -0
  70. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_incremental_build.py +0 -0
  71. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_mcp_server.py +0 -0
  72. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_opencode_plugin.py +0 -0
  73. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_plan_review.py +0 -0
  74. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_plugins.py +0 -0
  75. {pygraph_mcp-0.2.2 → pygraph_mcp-0.2.4}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygraph-mcp
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: AST-based Python/Flask codebase indexer for AI agents
5
5
  Project-URL: Homepage, https://github.com/shvmgyl15/pygraph
6
6
  Project-URL: Repository, https://github.com/shvmgyl15/pygraph
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pygraph-mcp"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "AST-based Python/Flask codebase indexer for AI agents"
5
5
  requires-python = ">=3.11"
6
6
  authors = [{name = "Shivam Goel", email = "sg15rokz@gmail.com"}]
@@ -375,6 +375,65 @@ def _extract_add_url_rule_calls(tree: ast.Module, file_path: str) -> list[HTTPRo
375
375
  return routes
376
376
 
377
377
 
378
+ def _extract_flask_restful_routes(tree: ast.Module, file_path: str) -> list[HTTPRoute]:
379
+ routes: list[HTTPRoute] = []
380
+
381
+ class_defs: dict[str, ast.ClassDef] = {}
382
+ for node in ast.iter_child_nodes(tree):
383
+ if isinstance(node, ast.ClassDef):
384
+ class_defs[node.name] = node
385
+
386
+ for node in ast.walk(tree):
387
+ if not isinstance(node, ast.Call):
388
+ continue
389
+ func = node.func
390
+ if not isinstance(func, ast.Attribute) or func.attr != "add_resource":
391
+ continue
392
+ if len(node.args) < 2:
393
+ continue
394
+
395
+ cls_node = node.args[0]
396
+ cls_name = ast.unparse(cls_node) if isinstance(cls_node, ast.Name) else ""
397
+ if not cls_name:
398
+ continue
399
+
400
+ path_args: list[str] = []
401
+ for arg in node.args[1:]:
402
+ p = _string_from_node(arg)
403
+ if p:
404
+ path_args.append(p)
405
+
406
+ if not path_args:
407
+ continue
408
+
409
+ http_methods: list[str] = []
410
+ cls_def = class_defs.get(cls_name)
411
+ if cls_def:
412
+ for item in cls_def.body:
413
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
414
+ method_upper = {
415
+ "get": "GET", "post": "POST", "put": "PUT",
416
+ "delete": "DELETE", "patch": "PATCH",
417
+ "head": "HEAD", "options": "OPTIONS",
418
+ }.get(item.name.lower())
419
+ if method_upper:
420
+ http_methods.append(method_upper)
421
+ if not http_methods:
422
+ http_methods = ["GET"]
423
+
424
+ for path in path_args:
425
+ for method in http_methods:
426
+ routes.append(HTTPRoute(
427
+ method=method,
428
+ path=path,
429
+ handler=f"{file_path}::{cls_name}",
430
+ file=file_path,
431
+ line=node.lineno,
432
+ ))
433
+
434
+ return routes
435
+
436
+
378
437
  def extract_flask(source: str, file_path: str) -> dict[str, Any]:
379
438
  try:
380
439
  tree = ast.parse(source)
@@ -390,7 +449,7 @@ def extract_flask(source: str, file_path: str) -> dict[str, Any]:
390
449
  }
391
450
 
392
451
  return {
393
- "routes": _extract_routes(tree, file_path) + _extract_add_url_rule_calls(tree, file_path),
452
+ "routes": _extract_routes(tree, file_path) + _extract_add_url_rule_calls(tree, file_path) + _extract_flask_restful_routes(tree, file_path),
394
453
  "blueprints": _detect_blueprint_assignments(tree),
395
454
  "blueprint_registrations": _extract_blueprint_registrations(tree, file_path),
396
455
  "template_refs": _extract_template_refs(tree, file_path),
@@ -55,6 +55,8 @@ def extract_implements(source: str, file_path: str) -> list[ImplementsEdge]:
55
55
  ImplementsEdge(
56
56
  interface=full_base,
57
57
  concrete=concrete_name,
58
+ file=file_path,
59
+ line=node.lineno,
58
60
  )
59
61
  )
60
62
 
@@ -155,6 +155,8 @@ class TestEdge:
155
155
  class ImplementsEdge:
156
156
  interface: str = ""
157
157
  concrete: str = ""
158
+ file: str = ""
159
+ line: int = 0
158
160
 
159
161
 
160
162
  @dataclass
@@ -355,6 +355,76 @@ migrate.init_app(app)
355
355
  assert result["blueprints"] == []
356
356
  assert result["template_refs"] == []
357
357
 
358
+ def test_flask_restful_add_resource_single_path(self) -> None:
359
+ src = """
360
+ from flask_restful import Resource, Api
361
+
362
+ api = Api(app)
363
+
364
+ class UserResource(Resource):
365
+ def get(self, id):
366
+ pass
367
+ def post(self):
368
+ pass
369
+
370
+ api.add_resource(UserResource, '/users/<id>')
371
+ """
372
+ from pygraph.extractors.flask import extract_flask
373
+
374
+ result = extract_flask(src, "app.py")
375
+ routes = [r for r in result["routes"] if r.method == "WRAPPER" or r.method in ("GET", "POST", "PUT", "DELETE")]
376
+ matching = [r for r in routes if r.path == "/users/<id>"]
377
+ assert len(matching) == 2
378
+ methods = {r.method for r in matching}
379
+ assert methods == {"GET", "POST"}
380
+ assert all(r.handler == "app.py::UserResource" for r in matching)
381
+
382
+ def test_flask_restful_add_resource_multi_path(self) -> None:
383
+ src = """
384
+ from flask_restful import Resource, Api
385
+
386
+ api = Api(app)
387
+
388
+ class ItemResource(Resource):
389
+ def get(self):
390
+ pass
391
+ def delete(self):
392
+ pass
393
+
394
+ api.add_resource(ItemResource, '/items', '/items/<id>')
395
+ """
396
+ from pygraph.extractors.flask import extract_flask
397
+
398
+ result = extract_flask(src, "app.py")
399
+ matching = [r for r in result["routes"] if "ItemResource" in r.handler]
400
+ assert len(matching) == 4 # 2 paths × 2 methods
401
+ paths = {r.path for r in matching}
402
+ assert paths == {"/items", "/items/<id>"}
403
+ methods = {r.method for r in matching}
404
+ assert methods == {"GET", "DELETE"}
405
+
406
+ def test_flask_restful_no_methods_no_crash(self) -> None:
407
+ src = """
408
+ api = Api(app)
409
+ api.add_resource(PlainClass, '/plain')
410
+ """
411
+ from pygraph.extractors.flask import extract_flask
412
+
413
+ result = extract_flask(src, "app.py")
414
+ matching = [r for r in result["routes"] if r.path == "/plain"]
415
+ assert len(matching) == 1
416
+ assert matching[0].method == "GET"
417
+
418
+ def test_flask_restful_not_add_resource_not_detected(self) -> None:
419
+ src = """
420
+ api.add_url_rule('/regular', view_func=handler)
421
+ """
422
+ from pygraph.extractors.flask import extract_flask
423
+
424
+ result = extract_flask(src, "app.py")
425
+ assert len(result["routes"]) == 1
426
+ assert result["routes"][0].path == "/regular"
427
+
358
428
 
359
429
  class TestComplexityExtraction:
360
430
  def test_base_complexity_one(self) -> None:
@@ -804,7 +804,7 @@ wheels = [
804
804
 
805
805
  [[package]]
806
806
  name = "pygraph-mcp"
807
- version = "0.2.2"
807
+ version = "0.2.4"
808
808
  source = { editable = "." }
809
809
  dependencies = [
810
810
  { name = "astroid" },
File without changes
File without changes
File without changes
File without changes
File without changes