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,288 @@
1
+ """Wrapper function index: traces API calls through helper/wrapper functions.
2
+
3
+ Builds an index of exported JS/TS functions that internally call fetch() or axios,
4
+ then resolves calls to those wrappers from component files back to the underlying URLs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING
13
+
14
+ from commiter.adapters.base import APICallMatch
15
+ from commiter.adapters.react import ReactAdapter
16
+ from commiter.parser import (
17
+ find_js_exported_functions,
18
+ find_js_re_exports,
19
+ build_constants_from_file,
20
+ get_source,
21
+ node_text,
22
+ JSReExport,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from tree_sitter import Tree
27
+ from commiter.utils.tsconfig_resolver import TSConfigResolver
28
+
29
+ # Directories likely to contain API wrapper functions
30
+ WRAPPER_DIR_HINTS = {"lib", "services", "utils", "api", "helpers", "client", "hooks"}
31
+
32
+ # File extensions to try when resolving import paths
33
+ RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js", "/index.tsx"]
34
+
35
+ _react_adapter = ReactAdapter()
36
+
37
+
38
+ @dataclass
39
+ class WrapperDefinition:
40
+ """An exported function that contains direct fetch/axios calls."""
41
+ function_name: str
42
+ file_path: str
43
+ line: int
44
+ api_calls: list[APICallMatch] = field(default_factory=list)
45
+
46
+
47
+ class WrapperIndex:
48
+ """Index of API wrapper functions within a single repository.
49
+
50
+ Usage:
51
+ idx = WrapperIndex()
52
+ # Pass 1: index all potential wrapper files
53
+ for path in files:
54
+ idx.index_file(path, tree, source)
55
+ # Pass 2: resolve calls from components
56
+ calls = idx.resolve("getUser", "../lib/api", "/project/components/Dash.tsx")
57
+ """
58
+
59
+ def __init__(self, alias_resolver: "TSConfigResolver | None" = None,
60
+ env_vars: dict[str, str] | None = None) -> None:
61
+ # func_name -> list of wrapper definitions (same name can exist in multiple files)
62
+ self._wrappers: dict[str, list[WrapperDefinition]] = {}
63
+ # normalized absolute path (no extension) -> actual absolute path
64
+ self._indexed_paths: dict[str, str] = {}
65
+ self._alias_resolver = alias_resolver
66
+ self._env_vars = env_vars or {}
67
+ # barrel file re-export cache: abs_path -> list[JSReExport]
68
+ self._re_exports: dict[str, list[JSReExport]] = {}
69
+ # unmatched exports for chained wrapper resolution
70
+ self._unmatched_exports: dict[str, list] = {}
71
+
72
+ def index_file(self, file_path: str, tree: Tree, source: bytes) -> None:
73
+ """Pass 1: scan a file for exported functions containing API calls.
74
+
75
+ Scans all JS/TS files — the actual filter is whether exported functions
76
+ contain fetch()/axios calls, not which directory the file is in.
77
+ Unmatched exports are stored for chain resolution in resolve_chains().
78
+ """
79
+ abs_path = os.path.abspath(file_path)
80
+ # Store path without extension for import resolution
81
+ stem = _strip_extension(abs_path)
82
+ self._indexed_paths[stem] = abs_path
83
+ # Also store index-stripped version: /lib/api/index.ts -> /lib/api
84
+ if Path(abs_path).stem == "index":
85
+ parent_stem = str(Path(abs_path).parent)
86
+ self._indexed_paths[parent_stem] = abs_path
87
+
88
+ # Cache re-exports for barrel file resolution
89
+ re_exports = find_js_re_exports(tree.root_node, source)
90
+ if re_exports:
91
+ self._re_exports[abs_path] = re_exports
92
+
93
+ # Build per-file constants for URL resolution in wrappers
94
+ constants = build_constants_from_file(tree.root_node, source, self._env_vars)
95
+
96
+ exports = find_js_exported_functions(tree.root_node, source, file_path=file_path)
97
+ for export in exports:
98
+ # Scan the exported function body for fetch/axios calls
99
+ fetch_calls = _react_adapter._find_fetch_calls(export.body_node, source, constants)
100
+ axios_calls = _react_adapter._find_axios_calls(export.body_node, source, constants)
101
+ all_calls = fetch_calls + axios_calls
102
+
103
+ if all_calls:
104
+ wrapper = WrapperDefinition(
105
+ function_name=export.name,
106
+ file_path=abs_path,
107
+ line=export.line,
108
+ api_calls=all_calls,
109
+ )
110
+ self._wrappers.setdefault(export.name, []).append(wrapper)
111
+ else:
112
+ # Store unmatched exports for chained wrapper resolution
113
+ body_text = node_text(export.body_node, source)
114
+ self._unmatched_exports.setdefault(abs_path, []).append(
115
+ (export, body_text, constants)
116
+ )
117
+
118
+ def resolve_chains(self) -> None:
119
+ """Resolve chained wrappers: functions that call other wrapper functions.
120
+
121
+ Called by the scanner after all files have been indexed.
122
+ Exported functions that didn't directly contain fetch/axios calls are
123
+ re-checked to see if they call any known wrapper function.
124
+ """
125
+ if not self._unmatched_exports:
126
+ return
127
+
128
+ # Build a set of all known wrapper function names
129
+ known_wrappers = set(self._wrappers.keys())
130
+ if not known_wrappers:
131
+ return
132
+
133
+ # Multiple passes to resolve chains of depth > 1
134
+ for _pass in range(3):
135
+ newly_resolved = 0
136
+ remaining: dict[str, list] = {}
137
+
138
+ for file_path, exports in self._unmatched_exports.items():
139
+ still_unmatched = []
140
+ for export, body_text, constants in exports:
141
+ # Check if the function body calls any known wrapper
142
+ found_calls = self._find_chained_calls(body_text, known_wrappers)
143
+ if found_calls:
144
+ wrapper = WrapperDefinition(
145
+ function_name=export.name,
146
+ file_path=file_path,
147
+ line=export.line,
148
+ api_calls=found_calls,
149
+ )
150
+ self._wrappers.setdefault(export.name, []).append(wrapper)
151
+ known_wrappers.add(export.name)
152
+ newly_resolved += 1
153
+ else:
154
+ still_unmatched.append((export, body_text, constants))
155
+
156
+ if still_unmatched:
157
+ remaining[file_path] = still_unmatched
158
+
159
+ self._unmatched_exports = remaining
160
+ if newly_resolved == 0:
161
+ break # no progress, stop
162
+
163
+ def _find_chained_calls(self, body_text: str, known_wrappers: set[str]) -> list:
164
+ """Check if a function body calls any known wrapper function.
165
+
166
+ Returns the underlying API calls from the wrapper being called.
167
+ """
168
+ all_calls = []
169
+ for wrapper_name in known_wrappers:
170
+ # Check if the wrapper name appears as a function call in the body
171
+ # Match: wrapperName( or obj.wrapperName(
172
+ if wrapper_name + "(" in body_text or "." + wrapper_name + "(" in body_text:
173
+ # Get the API calls from this wrapper
174
+ for wrapper_def in self._wrappers.get(wrapper_name, []):
175
+ all_calls.extend(wrapper_def.api_calls)
176
+ return all_calls
177
+
178
+ def resolve(self, func_name: str, import_path: str, caller_file: str) -> list[WrapperDefinition] | None:
179
+ """Pass 2: resolve a function call to its wrapper definitions.
180
+
181
+ Args:
182
+ func_name: The imported function name (e.g. "getUser").
183
+ import_path: The import module path (e.g. "../lib/api").
184
+ caller_file: Absolute path of the file that imports it.
185
+
186
+ Returns:
187
+ List of matching WrapperDefinitions, or None if not a known wrapper.
188
+ """
189
+ if func_name not in self._wrappers:
190
+ return None
191
+
192
+ resolved_path = self._resolve_import_path(import_path, caller_file)
193
+ if resolved_path is None:
194
+ return None
195
+
196
+ # Filter wrappers to those defined in the resolved file
197
+ matches = [
198
+ w for w in self._wrappers[func_name]
199
+ if w.file_path == resolved_path
200
+ ]
201
+ if matches:
202
+ return matches
203
+
204
+ # Barrel file fallback: check if resolved_path re-exports this name
205
+ actual_path = self._resolve_through_barrel(func_name, resolved_path)
206
+ if actual_path:
207
+ matches = [
208
+ w for w in self._wrappers[func_name]
209
+ if w.file_path == actual_path
210
+ ]
211
+ return matches if matches else None
212
+
213
+ return None
214
+
215
+ def _resolve_import_path(self, import_path: str, caller_file: str) -> str | None:
216
+ """Resolve an import path to an absolute file path."""
217
+ if not import_path.startswith("."):
218
+ # Non-relative import — try tsconfig alias resolution
219
+ if self._alias_resolver is None:
220
+ return None
221
+ resolved = self._alias_resolver.resolve(import_path, caller_file)
222
+ if resolved:
223
+ stem = _strip_extension(resolved)
224
+ if stem in self._indexed_paths:
225
+ return self._indexed_paths[stem]
226
+ return None
227
+
228
+ caller_dir = str(Path(os.path.abspath(caller_file)).parent)
229
+ base = os.path.normpath(os.path.join(caller_dir, import_path))
230
+
231
+ # Try exact match first
232
+ if base in self._indexed_paths:
233
+ return self._indexed_paths[base]
234
+
235
+ # Try with extensions
236
+ for ext in RESOLVE_EXTENSIONS:
237
+ candidate = base + ext
238
+ candidate_stem = _strip_extension(candidate)
239
+ if candidate_stem in self._indexed_paths:
240
+ return self._indexed_paths[candidate_stem]
241
+
242
+ return None
243
+
244
+ def _resolve_through_barrel(self, name: str, barrel_path: str) -> str | None:
245
+ """Follow re-exports in a barrel file to find the actual source file."""
246
+ re_exports = self._re_exports.get(barrel_path)
247
+ if not re_exports:
248
+ return None
249
+
250
+ barrel_dir = str(Path(barrel_path).parent)
251
+
252
+ for re_export in re_exports:
253
+ # Check if this re-export includes the name we're looking for
254
+ if name not in re_export.names and "*" not in re_export.names:
255
+ continue
256
+
257
+ # Resolve the re-export's module path relative to the barrel file
258
+ target_base = os.path.normpath(os.path.join(barrel_dir, re_export.module_path))
259
+ target_stem = _strip_extension(target_base)
260
+
261
+ if target_stem in self._indexed_paths:
262
+ return self._indexed_paths[target_stem]
263
+
264
+ # Try with extensions
265
+ for ext in RESOLVE_EXTENSIONS:
266
+ candidate_stem = _strip_extension(target_base + ext)
267
+ if candidate_stem in self._indexed_paths:
268
+ return self._indexed_paths[candidate_stem]
269
+
270
+ return None
271
+
272
+ def _is_wrapper_candidate(self, file_path: str) -> bool:
273
+ """Check if a file is likely to contain API wrapper functions."""
274
+ parts = Path(file_path).parts
275
+ return any(part.lower() in WRAPPER_DIR_HINTS for part in parts)
276
+
277
+ @property
278
+ def wrapper_count(self) -> int:
279
+ """Total number of indexed wrapper functions."""
280
+ return sum(len(defs) for defs in self._wrappers.values())
281
+
282
+
283
+ def _strip_extension(path: str) -> str:
284
+ """Strip JS/TS file extensions from a path."""
285
+ p = Path(path)
286
+ while p.suffix in (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"):
287
+ p = p.with_suffix("")
288
+ return str(p)
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: commiter-cli
3
+ Version: 0.3.0
4
+ Summary: Commiter CLI — scan repositories and enrich architecture snapshots on commiter.dev
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: tree-sitter>=0.21.0
7
+ Requires-Dist: tree-sitter-python>=0.21.0
8
+ Requires-Dist: tree-sitter-javascript>=0.21.0
9
+ Requires-Dist: tree-sitter-typescript>=0.21.0
10
+ Requires-Dist: click>=8.0
11
+ Requires-Dist: rich>=13.0
12
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.0; extra == "dev"
@@ -0,0 +1,96 @@
1
+ commiter/__init__.py,sha256=akqY0gZgdXcMvoBSK0Yfel8t5IvTr7gpAh94E6oq-Y4,101
2
+ commiter/auth.py,sha256=wLeVWYTPIcXIZplh5WT29oOU3LnrhvwMicyJmjoiM_s,4222
3
+ commiter/cli.py,sha256=FqjmqR8Kjwq2WZhIEo3Chq6Bx9dXZFBc_s-zOu81cY0,15173
4
+ commiter/correlator.py,sha256=PMDhIJTZX8I9pQ_aQugoMWIS3qwMdb_hKPS8MiOl1xk,7130
5
+ commiter/generic_resolver.py,sha256=qPXMC6TjpZo32RiiWwpctnN_vMu9POtpaIb06t2G4vs,6142
6
+ commiter/handler_index.py,sha256=SSMOKI4pqKmtTDIMeEwyGQso-J3QYXqSv1RtcHuGo-Y,3180
7
+ commiter/lib.py,sha256=Ce6LYgvQMeYub3WFOPiT1TKSAOU2gglLEhY32G_cAAs,2082
8
+ commiter/middleware_index.py,sha256=L8LnjgkcELVBtHlr-q6S0GIxaKyW5UGKcHtepZr2mng,15530
9
+ commiter/models.py,sha256=8BftI08-zMyOMxMyeJ_7S_4JZSyAOQWGCyNQjhN_6aY,2852
10
+ commiter/parser.py,sha256=6G7TH-0crotF8BQVkTgpQXPap10l_5jhsqHZBHPQIpg,45982
11
+ commiter/prefix_index.py,sha256=Z5FME0iYkEC6twlXnBwdsjPgSoUty3mG7taWCU22yqM,9173
12
+ commiter/scanner.py,sha256=69o1HJNJtP8I36juD4KdHU8sXfAPFO7m3TuIMnfvI2U,14900
13
+ commiter/type_index.py,sha256=jqqAvMZKgeo3jWAk_vMspi5GtX0IwK-u08LRF63XHS4,12458
14
+ commiter/uploader.py,sha256=6Wa1F00Tw9yzVx2VVAK4PZEh82zlPpq23xtmVnp83D8,1564
15
+ commiter/wrapper_index.py,sha256=B5JGCi0NRRzNnlEIUn_pdEHWhkKI3rSS7XY_xKGtKik,11636
16
+ commiter/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ commiter/adapters/base.py,sha256=YE11QC71pbj9JqnURgLRlP4mZL5g626hYU0bjQnBg6k,2925
18
+ commiter/adapters/django_rest.py,sha256=3IRKAA8oQW2mn-H1XqS_jzpJ7aJqvqF56gJl_BEeCmA,10023
19
+ commiter/adapters/express.py,sha256=tsZeQh_O-Ypo3dq07HsDDeOXdbRgNeePpB-FyC9jnbw,8384
20
+ commiter/adapters/fastapi.py,sha256=Xn7SzuNKBFtIIw8GblFL9giqfKF8o_rAjBC9yYOX93Y,6289
21
+ commiter/adapters/flask.py,sha256=PNKcHC0ZHYkmDXaotkHXziM9ix3U4BA1j_vWfHOvgBY,6089
22
+ commiter/adapters/nextjs.py,sha256=EfKhJQSIq-haAUm8uKY_GDL4gRtDBcz61zhcNcO-z4E,7504
23
+ commiter/adapters/prisma.py,sha256=1VV-_gsjMm09LY_t729RqSsmw2IQi8e6F75FVHaHTdY,2243
24
+ commiter/adapters/raw_sql.py,sha256=yrAVLf-KqZDIddYPeMb5Beqg-74esKo5W9GrJRJjZi8,6743
25
+ commiter/adapters/react.py,sha256=R-lX8kLCs5kVgAtHLrXVuBUnAPQ5XyjH9lCsXwT44R0,4825
26
+ commiter/adapters/sqlalchemy.py,sha256=DLqAAlPtHIE_upLS0I6MXJ5L3iiiyVC0cFSGANdKS8Y,3148
27
+ commiter/adapters/supabase.py,sha256=nAdPrFfR47rBLxsEAPPQgXLFNuldZmqwrfVEf82cW38,2350
28
+ commiter/extractors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ commiter/extractors/api_calls.py,sha256=EvHeyVbfmG7XTpr9A-j53QLgTL6uNvmAEyuYQAZF34s,3420
30
+ commiter/extractors/api_endpoints.py,sha256=7Rt-yzyJNbvPYVr6KgqLBi8uJHmZR2AISxVsEYLZyek,15144
31
+ commiter/extractors/backend_files.py,sha256=D4RTYDee2kqyw4Al1Uued9qyJbS5V6RsFaHfpfbLC3k,1269
32
+ commiter/extractors/base.py,sha256=oKWcgJMsyJxNs2Mms_8EykLySi62gYzzG7uSvg2SQks,1286
33
+ commiter/extractors/db_operations.py,sha256=iwOTN8DuuSyuVDLzFCjEtYkK8Z2RzXPa9EBiZGYCE7A,2524
34
+ commiter/extractors/dependencies.py,sha256=X_mIV6gzgkKwRk66HRq3JtXYMxp_xZqowPh7yIbEQrw,8933
35
+ commiter/report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ commiter/report/ai.py,sha256=pDrVe8v770rGbqhx7KMJ9BRpoWoSLOKJuDq-kuv_wzY,4497
37
+ commiter/report/api_guide.py,sha256=qro4j9dKDY5nt7ga68iBjpZGjVBuYyYA_X5M3h6zSbg,7049
38
+ commiter/report/architecture.py,sha256=vNnOhp6aIm7uKQ67QV5O95JrVsAwhggpVAbB7hgmSTA,34369
39
+ commiter/report/console.py,sha256=9BHwg__YNVNIqk89eqtbfL4zOWhceuCzC3iI0cWidEw,9544
40
+ commiter/report/json_output.py,sha256=KVPtPMlXvQW_NgrWt0qYZeSvImhBY30o70AiA_Zu7h4,4247
41
+ commiter/report/markdown.py,sha256=ZlmUWejjeOgEPQ3zfC-yqiBeCQo7MWdqOffeFml3brg,6092
42
+ commiter/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
+ commiter/utils/env_reader.py,sha256=p1475wifO226p5a5EHSlRD70AWhEbx_PaqhGyebdpw8,2318
44
+ commiter/utils/file_classifier.py,sha256=VHoyAPcCXn9hWDKdUjsGxnp6SUDWUwJW065rZjRFmcA,6620
45
+ commiter/utils/path_helpers.py,sha256=zrERWLwwjNj4_6G3DoSQt3AabdsByVEGa4IsyOjRAIo,2140
46
+ commiter/utils/tsconfig_resolver.py,sha256=lDtrSKe48e9q_-4qhZkI0Pw4cK6YfmiZBSLVjJjHVe4,9813
47
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ tests/test_architecture.py,sha256=xAx6ol3t7i8MbRtKqtHMd2TQo5p5icyr4c5Tpu4kcDs,31654
49
+ tests/fixtures/arch_backend/app.py,sha256=gSsfB3rQbLahdI3Ob4pEp-MpD8A6vuxOMnogNCVGxA8,683
50
+ tests/fixtures/arch_backend/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ tests/fixtures/arch_backend/middleware/rate_limit.py,sha256=hat4OUddjtd8bTnsFqBT2DqBBXfXGgxb1LktJSEhLto,103
52
+ tests/fixtures/arch_backend/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ tests/fixtures/arch_backend/routes/analytics.py,sha256=R5hyIlFFowHg7q76SVj3QCNXWWsnpnK0R8U-XjYzyco,679
54
+ tests/fixtures/arch_backend/routes/auth.py,sha256=GnlDo8r0N1pdF-LO1SFnHEQRgv0njZjH7pJR7lFXJ70,1113
55
+ tests/fixtures/arch_backend/routes/projects.py,sha256=9vK4j23PFyxMw5pOE5hyOBEY92sWOV7Jo1T8oLlaibQ,2018
56
+ tests/fixtures/arch_backend/routes/users.py,sha256=7WivpdDMLo8l958vg5pLwrhKk0SFcZe9cv8wFaLFx0g,1733
57
+ tests/fixtures/arch_monorepo/apps/api/app.py,sha256=jBufss2dNYrY3otjVrPxCcqjFS4V77cxMS_qNh8FQ44,963
58
+ tests/fixtures/arch_monorepo/apps/api/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
+ tests/fixtures/arch_monorepo/apps/api/middleware/auth.py,sha256=N0IVIS5nRwRfnB-PTMDYlxkudSb7SnA14rlblSo47sY,584
60
+ tests/fixtures/arch_monorepo/apps/api/middleware/rate_limit.py,sha256=MxA_Li2qxAeZ1wvYYEQZROio8iHV1BF1ul9YUFrxaNc,271
61
+ tests/fixtures/arch_monorepo/apps/api/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
+ tests/fixtures/arch_monorepo/apps/api/routes/auth.py,sha256=zKHlSGffcZySsKM3yChla0HCEtevQ8mqvMx1k4J-znM,1572
63
+ tests/fixtures/arch_monorepo/apps/api/routes/invites.py,sha256=3kgBa7PflXaMPNCZJhsIwzmrdcl5L9FeAdHuGIMhUQE,967
64
+ tests/fixtures/arch_monorepo/apps/api/routes/notifications.py,sha256=3ulnftBHAWHdogUH2ADj6sbXnkx_oIOgYtAHtKzbRNk,899
65
+ tests/fixtures/arch_monorepo/apps/api/routes/projects.py,sha256=83xiFFFNO4bVMeXGQrY_2B0KEZDRUlD5rTbAnPvdQks,3001
66
+ tests/fixtures/arch_monorepo/apps/api/routes/tasks.py,sha256=OHx_Z-90SMk8VEOErw52tb46LKuKKNlb8McO020u7Tc,3098
67
+ tests/fixtures/arch_monorepo/apps/api/routes/users.py,sha256=hRHlLldKPr0IaK0rLPWVvB_g7szXjF8CQSe6wowjJVQ,1518
68
+ tests/fixtures/arch_monorepo/apps/api/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
+ tests/fixtures/arch_monorepo/apps/api/services/email.py,sha256=ej056Mh71IVoOfNUYC7evmTxr1Rre8Swx5-SW23_NrI,311
70
+ tests/fixtures/backend_b/app.py,sha256=KrusADfRoADGFxbDuWBHuFJcdYuDFCB1Dh7zJ-PKzTE,620
71
+ tests/fixtures/fastapi_app/app.py,sha256=HBAGTumuLPuS9i7vv0F-XHqB9Gzb2dUycPojqEKYmAo,1005
72
+ tests/fixtures/fastapi_crossfile/routes.py,sha256=DL4eh_3OaLy-Wm0Ogp3jJbo1BOVZo20SJ82L7Fh0tYE,409
73
+ tests/fixtures/fastapi_crossfile/schemas.py,sha256=Bqs-RMfN1JniVW0GoVyXNeFWmtamV6FoQVgUzgezsgw,375
74
+ tests/fixtures/flask_app/app.py,sha256=PLba9Sz99b3fDMSCMp7hKy3b0QTCwP6nxZx15qbfsW4,990
75
+ tests/fixtures/flask_blueprint/app.py,sha256=k8bvY-iCrzNTigS-vMxtB9hvOcyCRMaMmrBnySefqUM,246
76
+ tests/fixtures/flask_blueprint/routes/items.py,sha256=5qyjOk-EIh_51iPpf7RUMuw7SO9pBPcViylUtxCy3EY,264
77
+ tests/fixtures/flask_blueprint/routes/users.py,sha256=0rJwDeynoOej4K1INRJSsP-datsYjXdDdd39emYQrGQ,472
78
+ tests/fixtures/middleware_test_flask/routes/public.py,sha256=y7Qpn7C-cfidH84I0fYHe4c3z1Dq1_92k4DHSofRDww,167
79
+ tests/fixtures/middleware_test_flask/routes/users.py,sha256=h5hiXzOckXaKmzuPjUed5d91pRQGvxMoTdkXl1GObuI,623
80
+ tests/fixtures/python_deep_imports/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
+ tests/fixtures/python_deep_imports/app/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
+ tests/fixtures/python_deep_imports/app/api/health.py,sha256=Gi6BcH25pT52fytzPAhW9n5rfJWYTbymEGMLeaeShL8,255
83
+ tests/fixtures/python_deep_imports/app/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
+ tests/fixtures/python_deep_imports/app/api/v1/items.py,sha256=T6W5rERDi1BWUnz91UAod4R-tlmj3pveY9yuhRcdEss,533
85
+ tests/fixtures/python_deep_imports/app/api/v1/users.py,sha256=p040s51LK6yyFBLx2e-LXR70-DHYWRRZkinl8tiS760,892
86
+ tests/fixtures/python_deep_imports/app/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
+ tests/fixtures/python_deep_imports/app/schemas/item.py,sha256=VWmjI_A78OQR10eOskum9yGvGkZ-DiY4XE6G24bw0dw,268
88
+ tests/fixtures/python_deep_imports/app/schemas/user.py,sha256=JM4u4nxdjuGEJoqpegvsNZIV1p6Kfa6LemKmCS7zhUA,269
89
+ tests/fixtures/python_deep_imports/app/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
+ tests/fixtures/python_deep_imports/app/shared/models.py,sha256=MqAwNXRc4T2m1znLOgpeo4Q5awqNOx9-WHr2QHb7U6Y,148
91
+ tests/fixtures/raw_sql_test/app.py,sha256=PK0EuSgSQRt1QFX3sf9DlIubEOlUbQJ_emAtwbtRzPo,1665
92
+ commiter_cli-0.3.0.dist-info/METADATA,sha256=etW7HEWkKq_Fan97Nps-jybqbwwdYDN0Gu-n5Esprz8,523
93
+ commiter_cli-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
94
+ commiter_cli-0.3.0.dist-info/entry_points.txt,sha256=rfydR0cFujz7uMaNk3jquW3lBGB1JxITlbQv-SG80AU,47
95
+ commiter_cli-0.3.0.dist-info/top_level.txt,sha256=FSBMqcZffZuLmPcEgl2XsjzHNI_CbnwrWWHvzf6CwBM,15
96
+ commiter_cli-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ commiter = commiter.cli:main
@@ -0,0 +1,2 @@
1
+ commiter
2
+ tests
tests/__init__.py ADDED
File without changes
@@ -0,0 +1,22 @@
1
+ from flask import Flask
2
+ from flask_cors import CORS
3
+ from routes.auth import auth_bp
4
+ from routes.users import users_bp
5
+ from routes.projects import projects_bp
6
+ from routes.analytics import analytics_bp
7
+ from middleware.rate_limit import rate_limit
8
+
9
+ app = Flask(__name__)
10
+ CORS(app, supports_credentials=True, origins=["https://myapp.vercel.app"])
11
+
12
+ @app.before_request
13
+ def log_request():
14
+ pass
15
+
16
+ app.register_blueprint(auth_bp, url_prefix="/v1/auth")
17
+ app.register_blueprint(users_bp, url_prefix="/v1/users")
18
+ app.register_blueprint(projects_bp, url_prefix="/v1/projects")
19
+ app.register_blueprint(analytics_bp, url_prefix="/v1/analytics")
20
+
21
+ if __name__ == "__main__":
22
+ app.run(port=5000)
File without changes
@@ -0,0 +1,4 @@
1
+ def rate_limit(max_requests=60, window=60):
2
+ def decorator(f):
3
+ return f
4
+ return decorator
File without changes
@@ -0,0 +1,20 @@
1
+ from flask import Blueprint, request, jsonify
2
+
3
+ analytics_bp = Blueprint("analytics", __name__)
4
+
5
+
6
+ @analytics_bp.route("/commits", methods=["GET"])
7
+ @login_required
8
+ def get_commit_stats():
9
+ """Get commit analysis for a project."""
10
+ project_id = request.args.get("project_id")
11
+ stats = db.table("commit_analysis").select("*").eq("project_id", project_id).execute()
12
+ return jsonify(stats=stats.data)
13
+
14
+
15
+ @analytics_bp.route("/activity", methods=["GET"])
16
+ @login_required
17
+ def get_activity():
18
+ """Get recent activity feed for the user."""
19
+ activity = db.table("activity_log").select("*").eq("user_id", request.user_id).execute()
20
+ return jsonify(activity=activity.data)
@@ -0,0 +1,29 @@
1
+ from flask import Blueprint, request, jsonify
2
+
3
+ auth_bp = Blueprint("auth", __name__)
4
+
5
+
6
+ @auth_bp.route("/login", methods=["POST"])
7
+ def login():
8
+ """Exchange credentials for a JWT token."""
9
+ email = request.json["email"]
10
+ password = request.json["password"]
11
+ user = db.table("users").select("*").eq("email", email).single().execute()
12
+ session = db.table("sessions").insert({"user_id": user.data["id"]}).execute()
13
+ return jsonify(access_token="jwt...", expires_in=3600)
14
+
15
+
16
+ @auth_bp.route("/refresh", methods=["POST"])
17
+ def refresh_token():
18
+ """Refresh an expired access token using the refresh cookie."""
19
+ session = db.table("sessions").select("*").eq("token", request.cookies.get("refresh")).single().execute()
20
+ db.table("sessions").update({"refreshed_at": "now()"}).eq("id", session.data["id"]).execute()
21
+ return jsonify(access_token="jwt...", expires_in=3600)
22
+
23
+
24
+ @auth_bp.route("/logout", methods=["POST"])
25
+ @login_required
26
+ def logout():
27
+ """Invalidate the current session."""
28
+ db.table("sessions").delete().eq("user_id", request.user_id).execute()
29
+ return jsonify(success=True)
@@ -0,0 +1,60 @@
1
+ from flask import Blueprint, request, jsonify
2
+
3
+ projects_bp = Blueprint("projects", __name__)
4
+
5
+
6
+ @projects_bp.route("/", methods=["GET"])
7
+ @login_required
8
+ def list_projects():
9
+ """List projects for the authenticated user."""
10
+ projects = db.table("projects").select("*").eq("owner_id", request.user_id).execute()
11
+ return jsonify(projects=projects.data)
12
+
13
+
14
+ @projects_bp.route("/", methods=["POST"])
15
+ @login_required
16
+ def create_project():
17
+ """Create a new project."""
18
+ name = request.json["name"]
19
+ description = request.json.get("description", "")
20
+ repo_url = request.json.get("repo_url")
21
+ project = db.table("projects").insert({
22
+ "name": name,
23
+ "description": description,
24
+ "repo_url": repo_url,
25
+ "owner_id": request.user_id,
26
+ }).execute()
27
+ return jsonify(project=project.data[0])
28
+
29
+
30
+ @projects_bp.route("/<project_id>", methods=["GET"])
31
+ @login_required
32
+ def get_project(project_id):
33
+ """Get a single project with its milestones."""
34
+ project = db.table("projects").select("*").eq("id", project_id).single().execute()
35
+ milestones = db.table("milestones").select("*").eq("project_id", project_id).execute()
36
+ return jsonify(project=project.data, milestones=milestones.data)
37
+
38
+
39
+ @projects_bp.route("/<project_id>", methods=["DELETE"])
40
+ @login_required
41
+ def delete_project(project_id):
42
+ """Delete a project and its milestones."""
43
+ db.table("milestones").delete().eq("project_id", project_id).execute()
44
+ db.table("projects").delete().eq("id", project_id).execute()
45
+ return jsonify(success=True)
46
+
47
+
48
+ @projects_bp.route("/<project_id>/milestones", methods=["POST"])
49
+ @login_required
50
+ def create_milestone(project_id):
51
+ """Add a milestone to a project."""
52
+ title = request.json["title"]
53
+ due_date = request.json.get("due_date")
54
+ milestone = db.table("milestones").insert({
55
+ "project_id": project_id,
56
+ "title": title,
57
+ "due_date": due_date,
58
+ "status": "open",
59
+ }).execute()
60
+ return jsonify(milestone=milestone.data[0])
@@ -0,0 +1,55 @@
1
+ from flask import Blueprint, request, jsonify
2
+
3
+ users_bp = Blueprint("users", __name__)
4
+
5
+
6
+ @users_bp.route("/", methods=["GET"])
7
+ @login_required
8
+ def list_users():
9
+ """List all users with pagination."""
10
+ users = db.table("users").select("id, display_name, avatar_url").execute()
11
+ return jsonify(users=users.data, total=len(users.data))
12
+
13
+
14
+ @users_bp.route("/<user_id>", methods=["GET"])
15
+ @login_required
16
+ def get_user(user_id):
17
+ """Get a single user profile."""
18
+ user = db.table("users").select("*").eq("id", user_id).single().execute()
19
+ return jsonify(user=user.data)
20
+
21
+
22
+ @users_bp.route("/me", methods=["PUT"])
23
+ @login_required
24
+ def update_profile():
25
+ """Update the authenticated user's profile."""
26
+ display_name = request.json.get("display_name")
27
+ bio = request.json.get("bio")
28
+ avatar_url = request.json.get("avatar_url")
29
+ user = db.table("users").update({
30
+ "display_name": display_name,
31
+ "bio": bio,
32
+ "avatar_url": avatar_url,
33
+ }).eq("id", request.user_id).execute()
34
+ return jsonify(user=user.data[0])
35
+
36
+
37
+ @users_bp.route("/me/connections", methods=["GET"])
38
+ @login_required
39
+ def list_connections():
40
+ """List the authenticated user's connections."""
41
+ connections = db.table("connections").select("*").eq("user_id", request.user_id).execute()
42
+ return jsonify(connections=connections.data)
43
+
44
+
45
+ @users_bp.route("/me/connections", methods=["POST"])
46
+ @login_required
47
+ def create_connection():
48
+ """Send a connection request."""
49
+ target_id = request.json["target_user_id"]
50
+ conn = db.table("connections").insert({
51
+ "user_id": request.user_id,
52
+ "target_user_id": target_id,
53
+ "status": "pending",
54
+ }).execute()
55
+ return jsonify(connection=conn.data[0])
@@ -0,0 +1,30 @@
1
+ from flask import Flask
2
+ from flask_cors import CORS
3
+ from routes.auth import auth_bp
4
+ from routes.users import users_bp
5
+ from routes.projects import projects_bp
6
+ from routes.tasks import tasks_bp
7
+ from routes.notifications import notifications_bp
8
+ from routes.invites import invites_bp
9
+ from middleware.auth import require_auth
10
+ from middleware.rate_limit import rate_limit
11
+
12
+ app = Flask(__name__)
13
+ CORS(app, supports_credentials=True, origins=[
14
+ "https://teamboard.dev",
15
+ "http://localhost:3000",
16
+ ])
17
+
18
+ @app.before_request
19
+ def log_request():
20
+ pass
21
+
22
+ app.register_blueprint(auth_bp, url_prefix="/v1/auth")
23
+ app.register_blueprint(users_bp, url_prefix="/v1/users")
24
+ app.register_blueprint(projects_bp, url_prefix="/v1/projects")
25
+ app.register_blueprint(tasks_bp, url_prefix="/v1/tasks")
26
+ app.register_blueprint(notifications_bp, url_prefix="/v1/notifications")
27
+ app.register_blueprint(invites_bp, url_prefix="/v1/invites")
28
+
29
+ if __name__ == "__main__":
30
+ app.run(port=5000)