commiter-cli 0.3.0__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.
Files changed (96) hide show
  1. commiter/__init__.py +3 -0
  2. commiter/adapters/__init__.py +0 -0
  3. commiter/adapters/base.py +96 -0
  4. commiter/adapters/django_rest.py +247 -0
  5. commiter/adapters/express.py +204 -0
  6. commiter/adapters/fastapi.py +170 -0
  7. commiter/adapters/flask.py +169 -0
  8. commiter/adapters/nextjs.py +180 -0
  9. commiter/adapters/prisma.py +76 -0
  10. commiter/adapters/raw_sql.py +191 -0
  11. commiter/adapters/react.py +129 -0
  12. commiter/adapters/sqlalchemy.py +99 -0
  13. commiter/adapters/supabase.py +68 -0
  14. commiter/auth.py +130 -0
  15. commiter/cli.py +667 -0
  16. commiter/correlator.py +208 -0
  17. commiter/extractors/__init__.py +0 -0
  18. commiter/extractors/api_calls.py +91 -0
  19. commiter/extractors/api_endpoints.py +354 -0
  20. commiter/extractors/backend_files.py +33 -0
  21. commiter/extractors/base.py +40 -0
  22. commiter/extractors/db_operations.py +69 -0
  23. commiter/extractors/dependencies.py +219 -0
  24. commiter/generic_resolver.py +204 -0
  25. commiter/handler_index.py +97 -0
  26. commiter/lib.py +63 -0
  27. commiter/middleware_index.py +350 -0
  28. commiter/models.py +117 -0
  29. commiter/parser.py +1283 -0
  30. commiter/prefix_index.py +211 -0
  31. commiter/report/__init__.py +0 -0
  32. commiter/report/ai.py +120 -0
  33. commiter/report/api_guide.py +217 -0
  34. commiter/report/architecture.py +930 -0
  35. commiter/report/console.py +254 -0
  36. commiter/report/json_output.py +122 -0
  37. commiter/report/markdown.py +163 -0
  38. commiter/scanner.py +383 -0
  39. commiter/type_index.py +304 -0
  40. commiter/uploader.py +46 -0
  41. commiter/utils/__init__.py +0 -0
  42. commiter/utils/env_reader.py +78 -0
  43. commiter/utils/file_classifier.py +187 -0
  44. commiter/utils/path_helpers.py +73 -0
  45. commiter/utils/tsconfig_resolver.py +281 -0
  46. commiter/wrapper_index.py +288 -0
  47. commiter_cli-0.3.0.dist-info/METADATA +14 -0
  48. commiter_cli-0.3.0.dist-info/RECORD +96 -0
  49. commiter_cli-0.3.0.dist-info/WHEEL +5 -0
  50. commiter_cli-0.3.0.dist-info/entry_points.txt +2 -0
  51. commiter_cli-0.3.0.dist-info/top_level.txt +2 -0
  52. tests/__init__.py +0 -0
  53. tests/fixtures/arch_backend/app.py +22 -0
  54. tests/fixtures/arch_backend/middleware/__init__.py +0 -0
  55. tests/fixtures/arch_backend/middleware/rate_limit.py +4 -0
  56. tests/fixtures/arch_backend/routes/__init__.py +0 -0
  57. tests/fixtures/arch_backend/routes/analytics.py +20 -0
  58. tests/fixtures/arch_backend/routes/auth.py +29 -0
  59. tests/fixtures/arch_backend/routes/projects.py +60 -0
  60. tests/fixtures/arch_backend/routes/users.py +55 -0
  61. tests/fixtures/arch_monorepo/apps/api/app.py +30 -0
  62. tests/fixtures/arch_monorepo/apps/api/middleware/__init__.py +0 -0
  63. tests/fixtures/arch_monorepo/apps/api/middleware/auth.py +17 -0
  64. tests/fixtures/arch_monorepo/apps/api/middleware/rate_limit.py +10 -0
  65. tests/fixtures/arch_monorepo/apps/api/routes/__init__.py +0 -0
  66. tests/fixtures/arch_monorepo/apps/api/routes/auth.py +46 -0
  67. tests/fixtures/arch_monorepo/apps/api/routes/invites.py +30 -0
  68. tests/fixtures/arch_monorepo/apps/api/routes/notifications.py +25 -0
  69. tests/fixtures/arch_monorepo/apps/api/routes/projects.py +80 -0
  70. tests/fixtures/arch_monorepo/apps/api/routes/tasks.py +91 -0
  71. tests/fixtures/arch_monorepo/apps/api/routes/users.py +48 -0
  72. tests/fixtures/arch_monorepo/apps/api/services/__init__.py +0 -0
  73. tests/fixtures/arch_monorepo/apps/api/services/email.py +11 -0
  74. tests/fixtures/backend_b/app.py +17 -0
  75. tests/fixtures/fastapi_app/app.py +48 -0
  76. tests/fixtures/fastapi_crossfile/routes.py +18 -0
  77. tests/fixtures/fastapi_crossfile/schemas.py +21 -0
  78. tests/fixtures/flask_app/app.py +33 -0
  79. tests/fixtures/flask_blueprint/app.py +7 -0
  80. tests/fixtures/flask_blueprint/routes/items.py +13 -0
  81. tests/fixtures/flask_blueprint/routes/users.py +20 -0
  82. tests/fixtures/middleware_test_flask/routes/public.py +8 -0
  83. tests/fixtures/middleware_test_flask/routes/users.py +26 -0
  84. tests/fixtures/python_deep_imports/app/__init__.py +0 -0
  85. tests/fixtures/python_deep_imports/app/api/__init__.py +0 -0
  86. tests/fixtures/python_deep_imports/app/api/health.py +11 -0
  87. tests/fixtures/python_deep_imports/app/api/v1/__init__.py +0 -0
  88. tests/fixtures/python_deep_imports/app/api/v1/items.py +18 -0
  89. tests/fixtures/python_deep_imports/app/api/v1/users.py +27 -0
  90. tests/fixtures/python_deep_imports/app/schemas/__init__.py +0 -0
  91. tests/fixtures/python_deep_imports/app/schemas/item.py +13 -0
  92. tests/fixtures/python_deep_imports/app/schemas/user.py +15 -0
  93. tests/fixtures/python_deep_imports/app/shared/__init__.py +0 -0
  94. tests/fixtures/python_deep_imports/app/shared/models.py +7 -0
  95. tests/fixtures/raw_sql_test/app.py +54 -0
  96. tests/test_architecture.py +757 -0
@@ -0,0 +1,350 @@
1
+ """Cross-file middleware index: maps middleware to the routes they cover.
2
+
3
+ Detects middleware registrations across files and resolves which routes
4
+ are covered by which middleware based on scope, path prefix, and line ordering.
5
+
6
+ Patterns detected:
7
+ Express: app.use(authMiddleware)
8
+ app.use("/api", authMiddleware)
9
+ router.use(authMiddleware)
10
+ Flask: @app.before_request / @bp.before_request
11
+ app.before_request(func)
12
+ FastAPI: app.add_middleware(AuthMiddleware)
13
+ @app.middleware("http")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import re
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING
23
+
24
+ if TYPE_CHECKING:
25
+ from tree_sitter import Tree
26
+
27
+
28
+ # Patterns that indicate a variable is a router, not middleware
29
+ _ROUTER_PATTERNS = re.compile(
30
+ r'.*[Rr]outer$|.*[Rr]outes$|.*_bp$|.*_blueprint$|.*[Hh]andler$|.*[Cc]ontroller$'
31
+ )
32
+
33
+
34
+ def _is_router_name(name: str) -> bool:
35
+ """Check if a variable name looks like a router rather than middleware."""
36
+ return bool(_ROUTER_PATTERNS.match(name))
37
+
38
+
39
+ @dataclass
40
+ class MiddlewareBinding:
41
+ """A middleware registration found in source code."""
42
+ name: str # middleware function/class name
43
+ scope: str # "global", "path", "router"
44
+ path_prefix: str # "/api" for path-scoped, "" for global/router
45
+ router_var: str # "app", "router", "bp" — which object it's mounted on
46
+ file_path: str
47
+ line: int
48
+
49
+
50
+ class MiddlewareIndex:
51
+ """Index of middleware registrations across all files.
52
+
53
+ Built during Pass 1, queried during Pass 2 to attach middleware to routes.
54
+ """
55
+
56
+ def __init__(self) -> None:
57
+ # file_path -> list of bindings found in that file
58
+ self._bindings_by_file: dict[str, list[MiddlewareBinding]] = {}
59
+ # router_var -> list of bindings (for cross-file: find middleware on a router)
60
+ self._bindings_by_router: dict[str, list[MiddlewareBinding]] = {}
61
+ # For cross-file: imported router name -> source file
62
+ self._router_imports: dict[str, dict[str, str]] = {}
63
+ # Track where routers are mounted: file -> {router_var: mount_line}
64
+ self._router_mount_lines: dict[str, dict[str, int]] = {}
65
+
66
+ def index_file(self, file_path: str, tree: "Tree", source: bytes, language: str) -> None:
67
+ """Scan a file for middleware registrations."""
68
+ abs_path = os.path.abspath(file_path)
69
+ text = source.decode("utf-8", errors="replace")
70
+
71
+ bindings: list[MiddlewareBinding] = []
72
+
73
+ if language == "python":
74
+ bindings.extend(self._find_python_middleware(abs_path, text))
75
+ elif language in ("javascript", "typescript", "tsx"):
76
+ bindings.extend(self._find_js_middleware(abs_path, text))
77
+
78
+ if bindings:
79
+ self._bindings_by_file[abs_path] = bindings
80
+ for b in bindings:
81
+ self._bindings_by_router.setdefault(b.router_var, []).append(b)
82
+
83
+ # Track imports for cross-file resolution
84
+ self._track_imports(abs_path, text, language)
85
+
86
+ def get_middleware_for_route(
87
+ self,
88
+ file_path: str,
89
+ route_pattern: str,
90
+ router_var: str,
91
+ route_line: int,
92
+ ) -> list[str]:
93
+ """Get middleware names that cover a specific route.
94
+
95
+ Args:
96
+ file_path: File containing the route.
97
+ route_pattern: The route's URL pattern (e.g. "/api/users").
98
+ router_var: The variable the route is defined on (e.g. "router", "bp").
99
+ route_line: Line number of the route definition.
100
+
101
+ Returns:
102
+ List of middleware names covering this route.
103
+ """
104
+ abs_path = os.path.abspath(file_path)
105
+ middleware: list[str] = []
106
+ seen: set[str] = set()
107
+
108
+ def _add(name: str) -> None:
109
+ if name not in seen:
110
+ seen.add(name)
111
+ middleware.append(name)
112
+
113
+ # 1. Same-file middleware on the same router, registered before this line
114
+ for binding in self._bindings_by_file.get(abs_path, []):
115
+ if binding.router_var == router_var and binding.line < route_line:
116
+ if binding.scope == "router":
117
+ _add(binding.name)
118
+ elif binding.scope == "path" and route_pattern.startswith(binding.path_prefix):
119
+ _add(binding.name)
120
+ elif binding.scope == "global":
121
+ _add(binding.name)
122
+
123
+ # 2. Same-file app-level middleware (covers all routes regardless of router_var)
124
+ for binding in self._bindings_by_file.get(abs_path, []):
125
+ if binding.scope == "global" and binding.router_var != router_var:
126
+ if binding.line < route_line:
127
+ _add(binding.name)
128
+
129
+ # 3. Cross-file: find middleware registered on 'app' in other files
130
+ # Use router mount lines to check ordering (middleware must be before the router mount)
131
+ for other_file, bindings in self._bindings_by_file.items():
132
+ if other_file == abs_path:
133
+ continue
134
+
135
+ # Find the mount line for the router that serves our routes
136
+ mount_lines = self._router_mount_lines.get(other_file, {})
137
+ # The router from our file could be imported under various names
138
+ router_mount_line = None
139
+ for var_name, mount_line in mount_lines.items():
140
+ # Check if this import points to our file
141
+ imports = self._router_imports.get(other_file, {})
142
+ source = imports.get(var_name, "")
143
+ file_stem = Path(abs_path).stem
144
+ if file_stem in source or router_var in source:
145
+ router_mount_line = mount_line
146
+ break
147
+
148
+ for binding in bindings:
149
+ # Global app-level middleware — must be before router mount (if we know the mount line)
150
+ if binding.scope == "global" and binding.router_var in ("app", "server"):
151
+ if router_mount_line is None or binding.line < router_mount_line:
152
+ _add(binding.name)
153
+ # Path-scoped middleware — must match path AND be before router mount
154
+ elif binding.scope == "path" and route_pattern.startswith(binding.path_prefix):
155
+ if router_mount_line is None or binding.line < router_mount_line:
156
+ _add(binding.name)
157
+
158
+ # 4. Cross-file: middleware on the router variable that imports this file's router
159
+ self._resolve_cross_file_router_middleware(abs_path, router_var, middleware, seen)
160
+
161
+ return middleware
162
+
163
+ def _find_python_middleware(self, abs_path: str, text: str) -> list[MiddlewareBinding]:
164
+ """Find Flask/FastAPI middleware patterns in Python code."""
165
+ bindings = []
166
+
167
+ # @app.before_request or @bp.before_request
168
+ for match in re.finditer(r'@(\w+)\.before_request\b', text):
169
+ var_name = match.group(1)
170
+ line = text[:match.start()].count("\n") + 1
171
+ # Find the function name on the next line
172
+ func_match = re.search(r'def\s+(\w+)', text[match.end():match.end() + 200])
173
+ func_name = func_match.group(1) if func_match else "before_request"
174
+ bindings.append(MiddlewareBinding(
175
+ name=func_name, scope="router", path_prefix="",
176
+ router_var=var_name, file_path=abs_path, line=line,
177
+ ))
178
+
179
+ # app.before_request(func) — non-decorator form
180
+ for match in re.finditer(r'(\w+)\.before_request\s*\(\s*(\w+)\s*\)', text):
181
+ var_name = match.group(1)
182
+ func_name = match.group(2)
183
+ line = text[:match.start()].count("\n") + 1
184
+ bindings.append(MiddlewareBinding(
185
+ name=func_name, scope="router", path_prefix="",
186
+ router_var=var_name, file_path=abs_path, line=line,
187
+ ))
188
+
189
+ # app.add_middleware(AuthMiddleware) — FastAPI
190
+ for match in re.finditer(r'(\w+)\.add_middleware\s*\(\s*(\w+)', text):
191
+ var_name = match.group(1)
192
+ class_name = match.group(2)
193
+ line = text[:match.start()].count("\n") + 1
194
+ bindings.append(MiddlewareBinding(
195
+ name=class_name, scope="global", path_prefix="",
196
+ router_var=var_name, file_path=abs_path, line=line,
197
+ ))
198
+
199
+ # @app.middleware("http") — FastAPI decorator
200
+ for match in re.finditer(r'@(\w+)\.middleware\s*\(\s*["\']http["\']\s*\)', text):
201
+ var_name = match.group(1)
202
+ line = text[:match.start()].count("\n") + 1
203
+ func_match = re.search(r'(?:async\s+)?def\s+(\w+)', text[match.end():match.end() + 200])
204
+ func_name = func_match.group(1) if func_match else "http_middleware"
205
+ bindings.append(MiddlewareBinding(
206
+ name=func_name, scope="global", path_prefix="",
207
+ router_var=var_name, file_path=abs_path, line=line,
208
+ ))
209
+
210
+ return bindings
211
+
212
+ def _find_js_middleware(self, abs_path: str, text: str) -> list[MiddlewareBinding]:
213
+ """Find Express middleware patterns in JS/TS code."""
214
+ bindings = []
215
+
216
+ # app.use(middleware), app.use("/path", middleware), router.use(middleware)
217
+ # Also: app.all("*", middleware) — another pattern for global middleware
218
+ # Need to distinguish from app.use("/path", router) which is a route mount
219
+ for match in re.finditer(
220
+ r'(\w+)\.(?:use|all)\s*\(([^)]+)\)',
221
+ text,
222
+ ):
223
+ obj_name = match.group(1)
224
+ args_text = match.group(2).strip()
225
+ line = text[:match.start()].count("\n") + 1
226
+
227
+ # Parse arguments
228
+ parts = [p.strip() for p in self._split_args(args_text)]
229
+ if not parts:
230
+ continue
231
+
232
+ path_prefix = ""
233
+ middleware_names = []
234
+
235
+ for part in parts:
236
+ # String argument = path prefix
237
+ if (part.startswith('"') or part.startswith("'") or part.startswith("`")):
238
+ path_prefix = part.strip("\"'`")
239
+ # Identifier = middleware or router
240
+ elif re.match(r'^[a-zA-Z_]\w*$', part):
241
+ middleware_names.append(part)
242
+ # Function call like cors() or helmet()
243
+ elif re.match(r'^[a-zA-Z_]\w*\s*\(', part):
244
+ func_name = re.match(r'^(\w+)', part).group(1)
245
+ middleware_names.append(f"{func_name}()")
246
+
247
+ # Heuristic: if there's only one identifier and it looks like a router
248
+ # (ends with Router, _router, etc.), record mount line and skip middleware
249
+ if len(middleware_names) == 1 and path_prefix:
250
+ name = middleware_names[0]
251
+ if _is_router_name(name):
252
+ self._router_mount_lines.setdefault(abs_path, {})[name] = line
253
+ continue # This is a route mount, handled by PrefixIndex
254
+
255
+ # If there are multiple identifiers with a path, the last might be a router
256
+ # and the others are middleware. Record mount line for router-like ones.
257
+ if path_prefix and len(middleware_names) > 1:
258
+ last = middleware_names[-1]
259
+ if _is_router_name(last):
260
+ self._router_mount_lines.setdefault(abs_path, {})[last] = line
261
+ middleware_names = middleware_names[:-1] # remove router, keep middleware
262
+
263
+ for mw_name in middleware_names:
264
+ scope = "path" if path_prefix else "router"
265
+ # app.use with no path = global; app.all("*", ...) is also global
266
+ if obj_name in ("app", "server") and (not path_prefix or path_prefix == "*"):
267
+ scope = "global"
268
+ path_prefix = ""
269
+ bindings.append(MiddlewareBinding(
270
+ name=mw_name, scope=scope, path_prefix=path_prefix,
271
+ router_var=obj_name, file_path=abs_path, line=line,
272
+ ))
273
+
274
+ return bindings
275
+
276
+ def _split_args(self, args_text: str) -> list[str]:
277
+ """Split function arguments, respecting nested parens and quotes."""
278
+ parts = []
279
+ depth = 0
280
+ current = ""
281
+ in_string = None
282
+
283
+ for ch in args_text:
284
+ if in_string:
285
+ current += ch
286
+ if ch == in_string:
287
+ in_string = None
288
+ continue
289
+
290
+ if ch in ("'", '"', '`'):
291
+ in_string = ch
292
+ current += ch
293
+ elif ch == '(' :
294
+ depth += 1
295
+ current += ch
296
+ elif ch == ')':
297
+ depth -= 1
298
+ current += ch
299
+ elif ch == ',' and depth == 0:
300
+ if current.strip():
301
+ parts.append(current.strip())
302
+ current = ""
303
+ else:
304
+ current += ch
305
+
306
+ if current.strip():
307
+ parts.append(current.strip())
308
+ return parts
309
+
310
+ def _track_imports(self, abs_path: str, text: str, language: str) -> None:
311
+ """Track router imports for cross-file middleware resolution."""
312
+ if language in ("javascript", "typescript", "tsx"):
313
+ for match in re.finditer(r'import\s+(\w+)\s+from\s+["\']([^"\']+)["\']', text):
314
+ self._router_imports.setdefault(abs_path, {})[match.group(1)] = match.group(2)
315
+ for match in re.finditer(r'(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*["\']([^"\']+)["\']\s*\)', text):
316
+ self._router_imports.setdefault(abs_path, {})[match.group(1)] = match.group(2)
317
+ elif language == "python":
318
+ for match in re.finditer(r'from\s+(\S+)\s+import\s+(.+)', text):
319
+ module = match.group(1)
320
+ names = [n.strip().split(" as ")[-1].strip() for n in match.group(2).split(",")]
321
+ for name in names:
322
+ if name:
323
+ self._router_imports.setdefault(abs_path, {})[name] = module
324
+
325
+ def _resolve_cross_file_router_middleware(
326
+ self, route_file: str, router_var: str, middleware: list[str], seen: set[str]
327
+ ) -> None:
328
+ """Check if other files mount this router with middleware."""
329
+ route_file_stem = Path(route_file).stem
330
+
331
+ for other_file, imports in self._router_imports.items():
332
+ if other_file == route_file:
333
+ continue
334
+ for imported_name, source_path in imports.items():
335
+ if route_file_stem in source_path or router_var in source_path:
336
+ # Find where this router is mounted
337
+ mount_line = self._router_mount_lines.get(other_file, {}).get(imported_name)
338
+
339
+ for binding in self._bindings_by_file.get(other_file, []):
340
+ if binding.router_var == imported_name or binding.scope == "global":
341
+ # Respect line ordering: middleware must be before router mount
342
+ if mount_line is not None and binding.line >= mount_line:
343
+ continue
344
+ if binding.name not in seen:
345
+ seen.add(binding.name)
346
+ middleware.append(binding.name)
347
+
348
+ @property
349
+ def binding_count(self) -> int:
350
+ return sum(len(b) for b in self._bindings_by_file.values())
commiter/models.py ADDED
@@ -0,0 +1,117 @@
1
+ """Data models for all extracted documentation artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+
8
+
9
+ class FileRole(Enum):
10
+ BACKEND = "backend"
11
+ FRONTEND = "frontend"
12
+ CONFIG = "config"
13
+ TEST = "test"
14
+ MIGRATION = "migration"
15
+ SHARED = "shared"
16
+ UNKNOWN = "unknown"
17
+
18
+
19
+ class ParamSource(Enum):
20
+ PATH = "path"
21
+ QUERY = "query"
22
+ HEADER = "header"
23
+ BODY = "body"
24
+
25
+
26
+ @dataclass
27
+ class Param:
28
+ name: str
29
+ source: ParamSource
30
+ type_hint: str | None = None
31
+ required: bool = True
32
+
33
+
34
+ @dataclass
35
+ class APIEndpoint:
36
+ repo: str
37
+ file_path: str
38
+ line: int
39
+ http_method: str
40
+ route_pattern: str
41
+ handler_name: str
42
+ framework: str
43
+ parameters: list[Param] = field(default_factory=list)
44
+ request_body_fields: list[str] = field(default_factory=list)
45
+ response_fields: list[str] = field(default_factory=list)
46
+ auth_decorators: list[str] = field(default_factory=list)
47
+ db_tables: list[str] = field(default_factory=list)
48
+ request_body_type: str | None = None
49
+ response_type: str | None = None
50
+ middleware: list[str] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class APICall:
55
+ repo: str
56
+ file_path: str
57
+ line: int
58
+ component_or_page: str
59
+ http_method: str
60
+ url_pattern: str
61
+ client_library: str
62
+ body_fields: list[str] = field(default_factory=list)
63
+ traced_from: str | None = None
64
+ response_type: str | None = None
65
+ body_type: str | None = None
66
+
67
+
68
+ @dataclass
69
+ class Dependency:
70
+ repo: str
71
+ name: str
72
+ version_constraint: str
73
+ source_file: str
74
+ dev_only: bool = False
75
+
76
+
77
+ @dataclass
78
+ class FileClassification:
79
+ file_path: str
80
+ role: FileRole
81
+ language: str
82
+ framework_hints: list[str] = field(default_factory=list)
83
+
84
+
85
+ @dataclass
86
+ class DBOperation:
87
+ repo: str
88
+ file_path: str
89
+ line: int
90
+ operation_type: str # select, insert, update, delete
91
+ table_name: str
92
+ orm_library: str
93
+ filters: list[str] = field(default_factory=list)
94
+
95
+
96
+ @dataclass
97
+ class ServiceRelationship:
98
+ source_repo: str
99
+ target_repo: str
100
+ connection_type: str # api_call, shared_db, shared_package
101
+ source_file: str
102
+ target_endpoint: str
103
+ confidence: float = 0.0
104
+
105
+
106
+ @dataclass
107
+ class RepoDocumentation:
108
+ repo_name: str
109
+ repo_path: str
110
+ languages: list[str] = field(default_factory=list)
111
+ frameworks: list[str] = field(default_factory=list)
112
+ endpoints: list[APIEndpoint] = field(default_factory=list)
113
+ api_calls: list[APICall] = field(default_factory=list)
114
+ dependencies: list[Dependency] = field(default_factory=list)
115
+ file_classifications: list[FileClassification] = field(default_factory=list)
116
+ db_operations: list[DBOperation] = field(default_factory=list)
117
+ service_relationships: list[ServiceRelationship] = field(default_factory=list)