workspace-graph 0.3.2__tar.gz → 0.3.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.
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/PKG-INFO +1 -1
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/pyproject.toml +1 -1
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/query.py +41 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/server.py +111 -25
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/uv.lock +1 -1
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.github/workflows/publish.yml +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.gitignore +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.pygraph/.build_cache.json +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/.pygraph/graph.json +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/AGENTS.md +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/README.md +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/TODOS.md +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/scripts/install.sh +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/scripts/uninstall.sh +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/__init__.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/__main__.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/builder.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/cli.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/__init__.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/build.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/callees.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/callers.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/clean.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/context.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/cross_service.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/impact.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/opencode_plugin.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/orphans.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/query_cmd.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/routes.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/status.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/commands/trace.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/config.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/cross_service.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/discover.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/graph/__init__.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/graph/serialize.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/graph/types.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/src/codegraph/plugin.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/__init__.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/conftest.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_builder.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_config.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_cross_service.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_discover.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_opencode_plugin.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_plugins.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_query.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_serialize.py +0 -0
- {workspace_graph-0.3.2 → workspace_graph-0.3.4}/tests/test_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workspace-graph
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Multi-language workspace code graph orchestrator
|
|
5
5
|
Project-URL: Homepage, https://github.com/shvmgyl15/codegraph
|
|
6
6
|
Project-URL: Repository, https://github.com/shvmgyl15/codegraph
|
|
@@ -511,6 +511,9 @@ class WorkspaceQuery:
|
|
|
511
511
|
if callee_receiver and caller_class and callee_receiver == caller_class:
|
|
512
512
|
self_ref_count += 1
|
|
513
513
|
continue
|
|
514
|
+
if cs is None and ce.get("callee_raw", "").startswith(("self.", "cls.", "self[")):
|
|
515
|
+
self_ref_count += 1
|
|
516
|
+
continue
|
|
514
517
|
if filter_builtins and callee_name in PYTHON_BUILTINS:
|
|
515
518
|
builtin_count += 1
|
|
516
519
|
continue
|
|
@@ -775,6 +778,44 @@ class WorkspaceQuery:
|
|
|
775
778
|
except OSError:
|
|
776
779
|
return None
|
|
777
780
|
|
|
781
|
+
def _extract_call_args(
|
|
782
|
+
self, file_path: str, line: int, callee_raw: str
|
|
783
|
+
) -> dict[str, Any]:
|
|
784
|
+
source = self._load_source(file_path)
|
|
785
|
+
if source is None:
|
|
786
|
+
return {"args": [], "kwargs": {}}
|
|
787
|
+
try:
|
|
788
|
+
import ast
|
|
789
|
+
tree = ast.parse(source)
|
|
790
|
+
except SyntaxError:
|
|
791
|
+
return {"args": [], "kwargs": {}}
|
|
792
|
+
for node in ast.walk(tree):
|
|
793
|
+
if not isinstance(node, ast.Call) or getattr(node, "lineno", None) != line:
|
|
794
|
+
continue
|
|
795
|
+
parsed = ast.unparse(node.func)
|
|
796
|
+
name_match = (
|
|
797
|
+
callee_raw.startswith(parsed) or parsed.endswith(callee_raw)
|
|
798
|
+
)
|
|
799
|
+
if not name_match:
|
|
800
|
+
continue
|
|
801
|
+
args: list[str] = []
|
|
802
|
+
for arg in node.args:
|
|
803
|
+
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
|
804
|
+
args.append(arg.value)
|
|
805
|
+
elif isinstance(arg, ast.Name):
|
|
806
|
+
args.append(arg.id)
|
|
807
|
+
|
|
808
|
+
kwargs: dict[str, str] = {}
|
|
809
|
+
for kw in node.keywords:
|
|
810
|
+
if kw.arg is None:
|
|
811
|
+
continue
|
|
812
|
+
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
813
|
+
kwargs[kw.arg] = kw.value.value
|
|
814
|
+
elif isinstance(kw.value, ast.Name):
|
|
815
|
+
kwargs[kw.arg] = kw.value.id
|
|
816
|
+
return {"args": args, "kwargs": kwargs}
|
|
817
|
+
return {"args": [], "kwargs": {}}
|
|
818
|
+
|
|
778
819
|
def get_trace(self, error_message: str) -> list[dict[str, Any]]:
|
|
779
820
|
matching: list[dict[str, Any]] = []
|
|
780
821
|
for err in self.graph.errors:
|
|
@@ -300,31 +300,82 @@ def routes(
|
|
|
300
300
|
|
|
301
301
|
if include_route_wrappers:
|
|
302
302
|
wrapper_names = list(q.list_classifications(kind="route_wrapper").get("symbols", {}).keys())
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
303
|
+
route_patterns = q._classification.get("route_patterns", {})
|
|
304
|
+
seen_calls: set[tuple[str, str, int]] = set()
|
|
305
|
+
|
|
306
|
+
for call in q.graph.calls:
|
|
307
|
+
craw = call.get("callee_raw", "")
|
|
308
|
+
matching_wrappers = [w for w in wrapper_names if w in craw]
|
|
309
|
+
if not matching_wrappers:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
cid = call.get("caller_symbol_id", "")
|
|
313
|
+
lineno = call.get("line", 0)
|
|
314
|
+
k = (cid, craw, lineno)
|
|
315
|
+
if k in seen_calls:
|
|
316
|
+
continue
|
|
317
|
+
seen_calls.add(k)
|
|
318
|
+
|
|
319
|
+
caller_sym = q._symbols_by_id.get(cid)
|
|
320
|
+
caller_name = caller_sym.get("name", "") if caller_sym else "<module>"
|
|
321
|
+
|
|
322
|
+
wrapper_name = matching_wrappers[0]
|
|
323
|
+
config = route_patterns.get(wrapper_name, {})
|
|
324
|
+
path_idx = config.get("path_arg_index", 0)
|
|
325
|
+
class_idx = config.get("class_arg_index")
|
|
326
|
+
|
|
327
|
+
if config:
|
|
328
|
+
raw_args = q._extract_call_args(
|
|
329
|
+
call.get("file", ""), lineno, craw,
|
|
330
|
+
)
|
|
331
|
+
path_args = raw_args.get("args", [])
|
|
332
|
+
paths = [path_args[path_idx]] if path_idx < len(path_args) else [f"[{craw}]"]
|
|
333
|
+
|
|
334
|
+
http_methods = ["GET"]
|
|
335
|
+
cls_name = ""
|
|
336
|
+
if class_idx is not None and class_idx < len(path_args):
|
|
337
|
+
cls_name = path_args[class_idx]
|
|
338
|
+
cls_methods = q._methods_by_class.get(cls_name, [])
|
|
339
|
+
if cls_methods:
|
|
340
|
+
http_methods = []
|
|
341
|
+
for m in cls_methods:
|
|
342
|
+
mname = m.get("name", "").lower()
|
|
343
|
+
method_map = {
|
|
344
|
+
"get": "GET", "post": "POST", "put": "PUT",
|
|
345
|
+
"delete": "DELETE", "patch": "PATCH",
|
|
346
|
+
"head": "HEAD", "options": "OPTIONS",
|
|
347
|
+
}
|
|
348
|
+
mapped = method_map.get(mname)
|
|
349
|
+
if mapped:
|
|
350
|
+
http_methods.append(mapped)
|
|
351
|
+
if not http_methods:
|
|
352
|
+
http_methods = ["GET"]
|
|
353
|
+
|
|
354
|
+
for path in paths:
|
|
355
|
+
for method in http_methods:
|
|
356
|
+
results.append({
|
|
357
|
+
"method": method,
|
|
358
|
+
"path": path,
|
|
359
|
+
"handler": cls_name if cls_name else caller_name,
|
|
360
|
+
"file": call.get("file", ""),
|
|
361
|
+
"line": lineno,
|
|
362
|
+
"entry_name": call.get("entry_name", ""),
|
|
363
|
+
"language": call.get("language", ""),
|
|
364
|
+
"type": call.get("type", ""),
|
|
365
|
+
"detected_by": "route_pattern",
|
|
366
|
+
})
|
|
367
|
+
else:
|
|
368
|
+
results.append({
|
|
369
|
+
"method": "WRAPPER",
|
|
370
|
+
"path": f"[{craw}]",
|
|
371
|
+
"handler": caller_name,
|
|
372
|
+
"file": call.get("file", ""),
|
|
373
|
+
"line": lineno,
|
|
374
|
+
"entry_name": call.get("entry_name", ""),
|
|
375
|
+
"language": call.get("language", ""),
|
|
376
|
+
"type": call.get("type", ""),
|
|
377
|
+
"detected_by": "route_classification",
|
|
378
|
+
})
|
|
328
379
|
|
|
329
380
|
items = [
|
|
330
381
|
{
|
|
@@ -579,6 +630,41 @@ def classify_discover(
|
|
|
579
630
|
return result
|
|
580
631
|
|
|
581
632
|
|
|
633
|
+
@server.tool()
|
|
634
|
+
def define_route_pattern(
|
|
635
|
+
name: str,
|
|
636
|
+
path_arg_index: int = 0,
|
|
637
|
+
class_arg_index: int | None = None,
|
|
638
|
+
root: str = ".",
|
|
639
|
+
) -> dict[str, Any]:
|
|
640
|
+
"""Define a custom route wrapper pattern for synthetic route detection.
|
|
641
|
+
name: function name (e.g. 'register_path', 'add_resource')
|
|
642
|
+
path_arg_index: which positional arg contains the route path (0-based)
|
|
643
|
+
class_arg_index: which positional arg contains the handler class (optional)
|
|
644
|
+
|
|
645
|
+
After defining a pattern, use classify_symbol(kind='route_wrapper') to
|
|
646
|
+
activate detection, then routes(include_route_wrappers=True) shows results.
|
|
647
|
+
Stored in .codegraph/classification.json — persists across sessions.
|
|
648
|
+
"""
|
|
649
|
+
_s = time.monotonic()
|
|
650
|
+
q = create_query(root)
|
|
651
|
+
data = q._classification
|
|
652
|
+
patterns = data.setdefault("route_patterns", {})
|
|
653
|
+
patterns[name] = {
|
|
654
|
+
"path_arg_index": path_arg_index,
|
|
655
|
+
"class_arg_index": class_arg_index,
|
|
656
|
+
}
|
|
657
|
+
from codegraph.query import _save_classifications
|
|
658
|
+
_save_classifications(root, data)
|
|
659
|
+
q._classification["route_patterns"] = patterns
|
|
660
|
+
return {
|
|
661
|
+
"defined": name,
|
|
662
|
+
"path_arg_index": path_arg_index,
|
|
663
|
+
"class_arg_index": class_arg_index,
|
|
664
|
+
"duration_ms": _duration(_s),
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
|
|
582
668
|
def run_server(root: str = ".") -> None:
|
|
583
669
|
with suppress(FileNotFoundError):
|
|
584
670
|
create_query(root)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|