bubble-analysis 0.2.0__py3-none-any.whl → 0.3.4__py3-none-any.whl

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.
bubble/detectors.py CHANGED
@@ -13,6 +13,7 @@ from bubble.integrations.cli_scripts.detector import (
13
13
  from bubble.integrations.django.detector import (
14
14
  DjangoExceptionHandlerVisitor,
15
15
  DjangoViewVisitor,
16
+ detect_django_entrypoints,
16
17
  )
17
18
  from bubble.integrations.django.semantics import (
18
19
  EXCEPTION_RESPONSES as DJANGO_EXCEPTION_RESPONSES,
@@ -26,7 +27,9 @@ from bubble.integrations.fastapi.semantics import (
26
27
  )
27
28
  from bubble.integrations.flask.detector import (
28
29
  FlaskErrorHandlerVisitor,
30
+ FlaskRESTfulVisitor,
29
31
  FlaskRouteVisitor,
32
+ detect_flask_entrypoints,
30
33
  )
31
34
  from bubble.integrations.flask.semantics import (
32
35
  EXCEPTION_RESPONSES as FLASK_EXCEPTION_RESPONSES,
@@ -53,13 +56,13 @@ FRAMEWORK_EXCEPTION_RESPONSES: dict[str, dict[str, str]] = {
53
56
  def detect_entrypoints(source: str, file_path: str) -> list[Entrypoint]:
54
57
  """Detect entrypoints in a Python source file (HTTP routes and CLI scripts).
55
58
 
56
- Uses the generic detector with Flask, FastAPI, and Django configurations,
57
- plus CLI script detection.
59
+ Uses framework-specific detectors for Flask and Django (with HTTP method detection),
60
+ the generic detector for FastAPI, plus CLI script detection.
58
61
  """
59
62
  entrypoints: list[Entrypoint] = []
60
- entrypoints.extend(generic_detect_entrypoints(source, file_path, FLASK_CONFIG))
63
+ entrypoints.extend(detect_flask_entrypoints(source, file_path))
61
64
  entrypoints.extend(generic_detect_entrypoints(source, file_path, FASTAPI_CONFIG))
62
- entrypoints.extend(generic_detect_entrypoints(source, file_path, DJANGO_CONFIG))
65
+ entrypoints.extend(detect_django_entrypoints(source, file_path))
63
66
  entrypoints.extend(detect_cli_entrypoints(source, file_path))
64
67
  return entrypoints
65
68
 
bubble/extractor.py CHANGED
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
13
13
  pass
14
14
 
15
15
  from bubble.detectors import detect_entrypoints, detect_global_handlers
16
+ from bubble.integrations.flask import correlate_flask_restful_entrypoints
16
17
  from bubble.enums import ResolutionKind
17
18
  from bubble.loader import load_detectors
18
19
  from bubble.models import (
@@ -440,13 +441,19 @@ class CodeExtractor(cst.CSTVisitor):
440
441
  return True
441
442
 
442
443
  def _get_current_qualified_name(self) -> str:
443
- """Get the fully qualified name of the current context."""
444
- parts = [self.relative_path]
444
+ """Get the fully qualified name of the current context.
445
+
446
+ Returns format: file.py::Class.method (matches canonical format from CLAUDE.md)
447
+ """
445
448
  if self._class_stack:
446
- parts.append(".".join(self._class_stack))
447
- if self._function_stack:
448
- parts.append(self._function_stack[-1])
449
- return "::".join(parts) if len(parts) > 1 else parts[0]
449
+ if self._function_stack:
450
+ qualified = ".".join(self._class_stack + [self._function_stack[-1]])
451
+ else:
452
+ qualified = ".".join(self._class_stack)
453
+ return f"{self.relative_path}::{qualified}"
454
+ elif self._function_stack:
455
+ return f"{self.relative_path}::{self._function_stack[-1]}"
456
+ return self.relative_path
450
457
 
451
458
  def _extract_depends_calls(
452
459
  self,
@@ -615,13 +622,14 @@ def extract_from_file(file_path: Path, relative_path: str | None = None) -> File
615
622
  result.return_types = extractor.return_types
616
623
  result.detected_frameworks = extractor.detected_frameworks
617
624
 
625
+ detection_path = relative_path if relative_path else str(file_path)
618
626
  try:
619
- result.entrypoints = detect_entrypoints(source, str(file_path))
627
+ result.entrypoints = detect_entrypoints(source, detection_path)
620
628
  except Exception:
621
629
  pass
622
630
 
623
631
  try:
624
- result.global_handlers = detect_global_handlers(source, str(file_path))
632
+ result.global_handlers = detect_global_handlers(source, detection_path)
625
633
  except Exception:
626
634
  pass
627
635
 
@@ -664,6 +672,10 @@ def _inject_drf_dispatch_calls(model: ProgramModel) -> None:
664
672
  When a DRF view class is detected as an entrypoint, this creates CallSite
665
673
  entries from the view class to each HTTP method handler (get, post, etc.)
666
674
  that exists on the class.
675
+
676
+ Handles both:
677
+ - Class-level entrypoints: "UserAPIView" -> creates edges to get, post, etc.
678
+ - Method-level entrypoints: "UserAPIView.get" -> creates edge to that method
667
679
  """
668
680
  drf_view_entrypoints = [
669
681
  ep
@@ -671,18 +683,32 @@ def _inject_drf_dispatch_calls(model: ProgramModel) -> None:
671
683
  if ep.metadata.get("framework") == "django" and ep.metadata.get("view_type") == "class"
672
684
  ]
673
685
 
686
+ seen_edges: set[tuple[str, str]] = set()
687
+
674
688
  for entrypoint in drf_view_entrypoints:
675
- view_class = entrypoint.function
689
+ view_function = entrypoint.function
676
690
  view_file = entrypoint.file
677
691
  view_line = entrypoint.line
678
692
 
693
+ if "." in view_function:
694
+ view_class, method_name = view_function.rsplit(".", 1)
695
+ methods_to_inject = [method_name] if method_name in DRF_DISPATCH_METHODS else []
696
+ else:
697
+ view_class = view_function
698
+ methods_to_inject = list(DRF_DISPATCH_METHODS)
699
+
679
700
  for _func_key, func_def in model.functions.items():
680
701
  if not func_def.is_method:
681
702
  continue
682
703
  if func_def.class_name != view_class:
683
704
  continue
684
- if func_def.name not in DRF_DISPATCH_METHODS:
705
+ if func_def.name not in methods_to_inject:
706
+ continue
707
+
708
+ edge_key = (view_class, func_def.name)
709
+ if edge_key in seen_edges:
685
710
  continue
711
+ seen_edges.add(edge_key)
686
712
 
687
713
  relative_file = view_file
688
714
  if "/" in relative_file or "\\" in relative_file:
@@ -824,6 +850,7 @@ def extract_from_directory(
824
850
  if cache:
825
851
  cache.close()
826
852
 
853
+ model.entrypoints = correlate_flask_restful_entrypoints(model.entrypoints)
827
854
  _inject_drf_dispatch_calls(model)
828
855
 
829
856
  return model
bubble/formatters.py CHANGED
@@ -694,7 +694,13 @@ def catches(
694
694
 
695
695
  total = len(result.local_catches) + len(result.global_handlers)
696
696
  if total == 0:
697
- console.print(f"[yellow]No catch sites found for {result.exception_type}[/yellow]")
697
+ if result.raise_site_count == 0:
698
+ console.print(
699
+ f"[yellow]No catch sites found for {result.exception_type}[/yellow]\n"
700
+ f"[dim] (this exception is not raised anywhere in the codebase)[/dim]"
701
+ )
702
+ else:
703
+ console.print(f"[yellow]No catch sites found for {result.exception_type}[/yellow]")
698
704
  return
699
705
 
700
706
  subclass_note = " (and subclasses)" if result.include_subclasses else ""
@@ -16,6 +16,7 @@ from bubble.integrations.queries import (
16
16
  trace_routes_to_exception,
17
17
  )
18
18
  from bubble.models import ProgramModel
19
+ from bubble.stubs import load_stubs
19
20
 
20
21
  app = typer.Typer(
21
22
  name="cli",
@@ -57,7 +58,10 @@ def audit(
57
58
  directory = directory.resolve()
58
59
  model = _build_model(directory, use_cache=not no_cache)
59
60
  entrypoints = _get_cli_entrypoints(model)
60
- result = audit_integration(model, integration, entrypoints, [])
61
+ stub_library = load_stubs(directory)
62
+ result = audit_integration(
63
+ model, integration, entrypoints, [], stub_library=stub_library
64
+ )
61
65
  formatters.audit(result, OutputFormat(output_format), directory, console)
62
66
 
63
67
 
@@ -40,8 +40,28 @@ def _get_django_entrypoints_and_handlers(model: ProgramModel) -> tuple[list, lis
40
40
  return entrypoints, handlers
41
41
 
42
42
 
43
+ def _filter_entrypoints(
44
+ entrypoints: list, filter_arg: str | None, directory: Path
45
+ ) -> list:
46
+ """Filter entrypoints by file path or route path."""
47
+ if not filter_arg:
48
+ return entrypoints
49
+
50
+ if filter_arg.startswith("/"):
51
+ return [e for e in entrypoints if e.metadata.get("http_path") == filter_arg]
52
+
53
+ filter_path = Path(filter_arg)
54
+ if not filter_path.is_absolute():
55
+ filter_path = directory / filter_path
56
+
57
+ return [e for e in entrypoints if Path(directory / e.file).resolve() == filter_path.resolve()]
58
+
59
+
43
60
  @app.command()
44
61
  def audit(
62
+ filter_arg: Annotated[
63
+ str | None, typer.Argument(help="Filter by file path or route (e.g., /users)")
64
+ ] = None,
45
65
  directory: Annotated[
46
66
  Path, typer.Option("--directory", "-d", help="Directory to analyze")
47
67
  ] = Path("."),
@@ -50,22 +70,35 @@ def audit(
50
70
  ) -> None:
51
71
  """Check Django views for escaping exceptions.
52
72
 
53
- Scans every Django view (APIView, ViewSet, @api_view), reports which have uncaught exceptions.
73
+ Scans Django views (APIView, ViewSet, @api_view) and reports which have uncaught exceptions.
54
74
 
55
- Example:
56
- flow django audit
57
- flow django audit -d /path/to/project
75
+ Examples:
76
+ bubble django audit # All views
77
+ bubble django audit /users # Views matching /users
78
+ bubble django audit views/users.py # Views in specific file
58
79
  """
59
80
  directory = directory.resolve()
60
81
  config = load_config(directory)
61
82
  model = _build_model(directory, use_cache=not no_cache)
62
83
  entrypoints, handlers = _get_django_entrypoints_and_handlers(model)
84
+ entrypoints = _filter_entrypoints(entrypoints, filter_arg, directory)
85
+
86
+ if filter_arg and not entrypoints:
87
+ if filter_arg.startswith("/"):
88
+ console.print(f"[yellow]No Django views found matching {filter_arg}[/yellow]")
89
+ else:
90
+ console.print(f"[yellow]No Django views found in {filter_arg}[/yellow]")
91
+ return
92
+
63
93
  result = audit_integration(model, integration, entrypoints, handlers, config=config)
64
94
  formatters.audit(result, output_format, directory, console)
65
95
 
66
96
 
67
97
  @app.command(name="entrypoints")
68
98
  def list_views(
99
+ filter_arg: Annotated[
100
+ str | None, typer.Argument(help="Filter by file path or route (e.g., /users)")
101
+ ] = None,
69
102
  directory: Annotated[
70
103
  Path, typer.Option("--directory", "-d", help="Directory to analyze")
71
104
  ] = Path("."),
@@ -74,12 +107,15 @@ def list_views(
74
107
  ) -> None:
75
108
  """List Django views (APIView, ViewSet, @api_view).
76
109
 
77
- Example:
78
- flow django entrypoints
110
+ Examples:
111
+ bubble django entrypoints # All views
112
+ bubble django entrypoints /users # Views matching /users
113
+ bubble django entrypoints views/users.py # Views in specific file
79
114
  """
80
115
  directory = directory.resolve()
81
116
  model = _build_model(directory, use_cache=not no_cache)
82
117
  entrypoints, _ = _get_django_entrypoints_and_handlers(model)
118
+ entrypoints = _filter_entrypoints(entrypoints, filter_arg, directory)
83
119
  result = list_integration_entrypoints(integration, entrypoints)
84
120
  formatters.entrypoints(result, output_format, directory, console)
85
121
 
@@ -23,6 +23,17 @@ DRF_BASE_CLASSES = {
23
23
  "RetrieveUpdateDestroyAPIView",
24
24
  }
25
25
 
26
+ DRF_HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
27
+ DRF_ACTION_METHODS = {"list", "create", "retrieve", "update", "partial_update", "destroy"}
28
+ DRF_METHOD_TO_HTTP = {
29
+ "list": "GET",
30
+ "create": "POST",
31
+ "retrieve": "GET",
32
+ "update": "PUT",
33
+ "partial_update": "PATCH",
34
+ "destroy": "DELETE",
35
+ }
36
+
26
37
  DRF_GENERICS_QUALIFIERS = {
27
38
  "generics",
28
39
  "rest_framework.generics",
@@ -51,31 +62,65 @@ class DjangoViewVisitor(cst.CSTVisitor):
51
62
  self._in_class: str | None = None
52
63
  self._class_is_view = False
53
64
  self._class_line: int = 0
65
+ self._class_methods: dict[str, int] = {}
54
66
 
55
67
  def visit_ClassDef(self, node: cst.ClassDef) -> bool:
56
68
  self._in_class = node.name.value
57
69
  self._class_is_view = self._is_view_class(node)
70
+ self._class_methods = {}
58
71
  if self._class_is_view:
59
72
  pos = self.get_metadata(PositionProvider, node)
60
73
  self._class_line = pos.start.line
61
- self.entrypoints.append(
62
- Entrypoint(
63
- file=self.file_path,
64
- function=node.name.value,
65
- line=self._class_line,
66
- kind="http_route",
67
- metadata={
68
- "framework": "django",
69
- "view_type": "class",
70
- },
71
- )
72
- )
74
+ return True
75
+
76
+ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool:
77
+ if self._class_is_view and self._in_class:
78
+ method_name = node.name.value.lower()
79
+ if method_name in DRF_HTTP_METHODS or method_name in DRF_ACTION_METHODS:
80
+ pos = self.get_metadata(PositionProvider, node)
81
+ self._class_methods[method_name] = pos.start.line
73
82
  return True
74
83
 
75
84
  def leave_ClassDef(self, node: cst.ClassDef) -> None:
85
+ if self._class_is_view and self._in_class:
86
+ class_name = self._in_class
87
+ if self._class_methods:
88
+ for method_name, line in self._class_methods.items():
89
+ http_method = DRF_METHOD_TO_HTTP.get(method_name, method_name.upper())
90
+ self.entrypoints.append(
91
+ Entrypoint(
92
+ file=self.file_path,
93
+ function=f"{class_name}.{method_name}",
94
+ line=line,
95
+ kind="http_route",
96
+ metadata={
97
+ "framework": "django",
98
+ "view_type": "class",
99
+ "http_method": http_method,
100
+ "http_path": f"<drf:{class_name}>",
101
+ },
102
+ )
103
+ )
104
+ else:
105
+ self.entrypoints.append(
106
+ Entrypoint(
107
+ file=self.file_path,
108
+ function=class_name,
109
+ line=self._class_line,
110
+ kind="http_route",
111
+ metadata={
112
+ "framework": "django",
113
+ "view_type": "class",
114
+ "http_method": "ANY",
115
+ "http_path": f"<drf:{class_name}>",
116
+ },
117
+ )
118
+ )
119
+
76
120
  self._in_class = None
77
121
  self._class_is_view = False
78
122
  self._class_line = 0
123
+ self._class_methods = {}
79
124
 
80
125
  def _is_view_class(self, node: cst.ClassDef) -> bool:
81
126
  """Check if a class inherits from a Django/DRF view base class."""
@@ -16,6 +16,7 @@ from bubble.integrations.queries import (
16
16
  trace_routes_to_exception,
17
17
  )
18
18
  from bubble.models import ProgramModel
19
+ from bubble.stubs import load_stubs
19
20
 
20
21
  app = typer.Typer(
21
22
  name="fastapi",
@@ -40,8 +41,28 @@ def _get_fastapi_entrypoints_and_handlers(model: ProgramModel) -> tuple[list, li
40
41
  return entrypoints, handlers
41
42
 
42
43
 
44
+ def _filter_entrypoints(
45
+ entrypoints: list, filter_arg: str | None, directory: Path
46
+ ) -> list:
47
+ """Filter entrypoints by file path or route path."""
48
+ if not filter_arg:
49
+ return entrypoints
50
+
51
+ if filter_arg.startswith("/"):
52
+ return [e for e in entrypoints if e.metadata.get("http_path") == filter_arg]
53
+
54
+ filter_path = Path(filter_arg)
55
+ if not filter_path.is_absolute():
56
+ filter_path = directory / filter_path
57
+
58
+ return [e for e in entrypoints if Path(directory / e.file).resolve() == filter_path.resolve()]
59
+
60
+
43
61
  @app.command()
44
62
  def audit(
63
+ filter_arg: Annotated[
64
+ str | None, typer.Argument(help="Filter by file path or route (e.g., /users)")
65
+ ] = None,
45
66
  directory: Annotated[
46
67
  Path, typer.Option("--directory", "-d", help="Directory to analyze")
47
68
  ] = Path("."),
@@ -50,21 +71,37 @@ def audit(
50
71
  ) -> None:
51
72
  """Check FastAPI routes for escaping exceptions.
52
73
 
53
- Scans every FastAPI HTTP route, reports which have uncaught exceptions.
74
+ Scans FastAPI HTTP routes and reports which have uncaught exceptions.
54
75
 
55
- Example:
56
- flow fastapi audit
57
- flow fastapi audit -d /path/to/project
76
+ Examples:
77
+ bubble fastapi audit # All routes
78
+ bubble fastapi audit /users # Routes matching /users
79
+ bubble fastapi audit routers/users.py # Routes in specific file
58
80
  """
59
81
  directory = directory.resolve()
60
82
  model = _build_model(directory, use_cache=not no_cache)
61
83
  entrypoints, handlers = _get_fastapi_entrypoints_and_handlers(model)
62
- result = audit_integration(model, integration, entrypoints, handlers)
84
+ entrypoints = _filter_entrypoints(entrypoints, filter_arg, directory)
85
+
86
+ if filter_arg and not entrypoints:
87
+ if filter_arg.startswith("/"):
88
+ console.print(f"[yellow]No FastAPI routes found matching {filter_arg}[/yellow]")
89
+ else:
90
+ console.print(f"[yellow]No FastAPI routes found in {filter_arg}[/yellow]")
91
+ return
92
+
93
+ stub_library = load_stubs(directory)
94
+ result = audit_integration(
95
+ model, integration, entrypoints, handlers, stub_library=stub_library
96
+ )
63
97
  formatters.audit(result, OutputFormat(output_format), directory, console)
64
98
 
65
99
 
66
100
  @app.command(name="entrypoints")
67
101
  def list_routes(
102
+ filter_arg: Annotated[
103
+ str | None, typer.Argument(help="Filter by file path or route (e.g., /users)")
104
+ ] = None,
68
105
  directory: Annotated[
69
106
  Path, typer.Option("--directory", "-d", help="Directory to analyze")
70
107
  ] = Path("."),
@@ -73,12 +110,15 @@ def list_routes(
73
110
  ) -> None:
74
111
  """List FastAPI HTTP routes.
75
112
 
76
- Example:
77
- flow fastapi entrypoints
113
+ Examples:
114
+ bubble fastapi entrypoints # All routes
115
+ bubble fastapi entrypoints /users # Routes matching /users
116
+ bubble fastapi entrypoints routers/users.py # Routes in specific file
78
117
  """
79
118
  directory = directory.resolve()
80
119
  model = _build_model(directory, use_cache=not no_cache)
81
120
  entrypoints, _ = _get_fastapi_entrypoints_and_handlers(model)
121
+ entrypoints = _filter_entrypoints(entrypoints, filter_arg, directory)
82
122
  result = list_integration_entrypoints(integration, entrypoints)
83
123
  formatters.entrypoints(result, OutputFormat(output_format), directory, console)
84
124
 
@@ -5,7 +5,9 @@ import typer
5
5
  from bubble.integrations.base import Entrypoint, GlobalHandler
6
6
  from bubble.integrations.flask.detector import (
7
7
  FlaskErrorHandlerVisitor,
8
+ FlaskRESTfulVisitor,
8
9
  FlaskRouteVisitor,
10
+ correlate_flask_restful_entrypoints,
9
11
  detect_flask_entrypoints,
10
12
  detect_flask_global_handlers,
11
13
  )
@@ -50,8 +52,10 @@ class FlaskIntegration:
50
52
  __all__ = [
51
53
  "FlaskIntegration",
52
54
  "FlaskRouteVisitor",
55
+ "FlaskRESTfulVisitor",
53
56
  "FlaskErrorHandlerVisitor",
54
57
  "EXCEPTION_RESPONSES",
58
+ "correlate_flask_restful_entrypoints",
55
59
  "detect_flask_entrypoints",
56
60
  "detect_flask_global_handlers",
57
61
  ]
@@ -16,6 +16,7 @@ from bubble.integrations.queries import (
16
16
  trace_routes_to_exception,
17
17
  )
18
18
  from bubble.models import ProgramModel
19
+ from bubble.stubs import load_stubs
19
20
 
20
21
  app = typer.Typer(
21
22
  name="flask",
@@ -40,8 +41,28 @@ def _get_flask_entrypoints_and_handlers(model: ProgramModel) -> tuple[list, list
40
41
  return entrypoints, handlers
41
42
 
42
43
 
44
+ def _filter_entrypoints(
45
+ entrypoints: list, filter_arg: str | None, directory: Path
46
+ ) -> list:
47
+ """Filter entrypoints by file path or route path."""
48
+ if not filter_arg:
49
+ return entrypoints
50
+
51
+ if filter_arg.startswith("/"):
52
+ return [e for e in entrypoints if e.metadata.get("http_path") == filter_arg]
53
+
54
+ filter_path = Path(filter_arg)
55
+ if not filter_path.is_absolute():
56
+ filter_path = directory / filter_path
57
+
58
+ return [e for e in entrypoints if Path(directory / e.file).resolve() == filter_path.resolve()]
59
+
60
+
43
61
  @app.command()
44
62
  def audit(
63
+ filter_arg: Annotated[
64
+ str | None, typer.Argument(help="Filter by file path or route (e.g., /balance)")
65
+ ] = None,
45
66
  directory: Annotated[
46
67
  Path, typer.Option("--directory", "-d", help="Directory to analyze")
47
68
  ] = Path("."),
@@ -50,21 +71,37 @@ def audit(
50
71
  ) -> None:
51
72
  """Check Flask routes for escaping exceptions.
52
73
 
53
- Scans every Flask HTTP route, reports which have uncaught exceptions.
74
+ Scans Flask HTTP routes and reports which have uncaught exceptions.
54
75
 
55
- Example:
56
- flow flask audit
57
- flow flask audit -d /path/to/project
76
+ Examples:
77
+ bubble flask audit # All routes
78
+ bubble flask audit /balance # Routes matching /balance
79
+ bubble flask audit blueprints/api.py # Routes in specific file
58
80
  """
59
81
  directory = directory.resolve()
60
82
  model = _build_model(directory, use_cache=not no_cache)
61
83
  entrypoints, handlers = _get_flask_entrypoints_and_handlers(model)
62
- result = audit_integration(model, integration, entrypoints, handlers)
84
+ entrypoints = _filter_entrypoints(entrypoints, filter_arg, directory)
85
+
86
+ if filter_arg and not entrypoints:
87
+ if filter_arg.startswith("/"):
88
+ console.print(f"[yellow]No Flask routes found matching {filter_arg}[/yellow]")
89
+ else:
90
+ console.print(f"[yellow]No Flask routes found in {filter_arg}[/yellow]")
91
+ return
92
+
93
+ stub_library = load_stubs(directory)
94
+ result = audit_integration(
95
+ model, integration, entrypoints, handlers, stub_library=stub_library
96
+ )
63
97
  formatters.audit(result, OutputFormat(output_format), directory, console)
64
98
 
65
99
 
66
100
  @app.command(name="entrypoints")
67
101
  def list_routes(
102
+ filter_arg: Annotated[
103
+ str | None, typer.Argument(help="Filter by file path or route (e.g., /balance)")
104
+ ] = None,
68
105
  directory: Annotated[
69
106
  Path, typer.Option("--directory", "-d", help="Directory to analyze")
70
107
  ] = Path("."),
@@ -73,12 +110,15 @@ def list_routes(
73
110
  ) -> None:
74
111
  """List Flask HTTP routes.
75
112
 
76
- Example:
77
- flow flask entrypoints
113
+ Examples:
114
+ bubble flask entrypoints # All routes
115
+ bubble flask entrypoints /users # Routes matching /users
116
+ bubble flask entrypoints blueprints/api.py # Routes in specific file
78
117
  """
79
118
  directory = directory.resolve()
80
119
  model = _build_model(directory, use_cache=not no_cache)
81
120
  entrypoints, _ = _get_flask_entrypoints_and_handlers(model)
121
+ entrypoints = _filter_entrypoints(entrypoints, filter_arg, directory)
82
122
  result = list_integration_entrypoints(integration, entrypoints)
83
123
  formatters.entrypoints(result, OutputFormat(output_format), directory, console)
84
124