codeframe-ai 0.9.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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
codeframe/tui/app.py ADDED
@@ -0,0 +1,256 @@
1
+ """CodeFRAME TUI Dashboard — live terminal dashboard.
2
+
3
+ A Textual application showing tasks, events, and blockers
4
+ with auto-refresh and keyboard navigation.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from textual.app import App, ComposeResult
10
+ from textual.binding import Binding
11
+ from textual.containers import Horizontal, Vertical
12
+ from textual.reactive import reactive
13
+ from textual.widgets import DataTable, Footer, Header, RichLog, Static
14
+
15
+ from codeframe.core.workspace import Workspace
16
+ from codeframe.tui.data_service import DashboardData, load_dashboard_data
17
+
18
+
19
+ # Status → color mapping for task rows
20
+ _STATUS_COLORS: dict[str, str] = {
21
+ "DONE": "green",
22
+ "IN_PROGRESS": "cyan",
23
+ "READY": "yellow",
24
+ "BACKLOG": "dim",
25
+ "BLOCKED": "red",
26
+ "FAILED": "red bold",
27
+ "MERGED": "green dim",
28
+ }
29
+
30
+
31
+ class StatusBar(Static):
32
+ """Top status bar showing task counts and workspace info."""
33
+
34
+ def update_from_data(self, data: DashboardData) -> None:
35
+ counts = data.task_counts
36
+ total = sum(counts.values())
37
+ done = counts.get("DONE", 0) + counts.get("MERGED", 0)
38
+ active = counts.get("IN_PROGRESS", 0)
39
+ blocked = counts.get("BLOCKED", 0) + counts.get("FAILED", 0)
40
+ ready = counts.get("READY", 0) + counts.get("BACKLOG", 0)
41
+
42
+ parts = [
43
+ f"[bold]{data.workspace_name}[/bold]",
44
+ f"Tasks: {total}",
45
+ f"[green]{done} done[/green]",
46
+ f"[cyan]{active} active[/cyan]",
47
+ f"[yellow]{ready} ready[/yellow]",
48
+ ]
49
+ if blocked > 0:
50
+ parts.append(f"[red]{blocked} blocked/failed[/red]")
51
+ if data.blocker_count > 0:
52
+ parts.append(f"[red bold]{data.blocker_count} blockers[/red bold]")
53
+ if data.open_obligation_count > 0:
54
+ parts.append(f"[yellow bold]{data.open_obligation_count} open obligations[/yellow bold]")
55
+
56
+ self.update(" | ".join(parts))
57
+
58
+
59
+ class DashboardApp(App):
60
+ """CodeFRAME TUI Dashboard."""
61
+
62
+ CSS = """
63
+ Screen {
64
+ layout: vertical;
65
+ }
66
+ #status-bar {
67
+ height: 1;
68
+ background: $surface;
69
+ padding: 0 1;
70
+ }
71
+ #main-content {
72
+ height: 1fr;
73
+ }
74
+ #task-panel {
75
+ width: 2fr;
76
+ border: solid $primary;
77
+ }
78
+ #right-panel {
79
+ width: 1fr;
80
+ }
81
+ #event-log {
82
+ height: 2fr;
83
+ border: solid $secondary;
84
+ }
85
+ #blocker-panel {
86
+ height: 1fr;
87
+ border: solid $error;
88
+ }
89
+ #proof-panel {
90
+ height: 1fr;
91
+ border: solid $warning;
92
+ }
93
+ DataTable {
94
+ height: 1fr;
95
+ }
96
+ RichLog {
97
+ height: 1fr;
98
+ }
99
+ .panel-title {
100
+ background: $surface;
101
+ padding: 0 1;
102
+ text-style: bold;
103
+ }
104
+ """
105
+
106
+ TITLE = "CodeFRAME Dashboard"
107
+ BINDINGS = [
108
+ Binding("q", "quit", "Quit"),
109
+ Binding("r", "refresh", "Refresh"),
110
+ Binding("tab", "focus_next", "Next Panel"),
111
+ Binding("shift+tab", "focus_previous", "Prev Panel"),
112
+ ]
113
+
114
+ workspace: Optional[Workspace] = None
115
+ refresh_interval: int = 2
116
+ data: reactive[Optional[DashboardData]] = reactive(None)
117
+
118
+ def __init__(
119
+ self,
120
+ workspace: Workspace,
121
+ refresh_interval: int = 2,
122
+ **kwargs,
123
+ ):
124
+ super().__init__(**kwargs)
125
+ self.workspace = workspace
126
+ self.refresh_interval = refresh_interval
127
+
128
+ def compose(self) -> ComposeResult:
129
+ yield Header()
130
+ yield StatusBar(id="status-bar")
131
+ with Horizontal(id="main-content"):
132
+ with Vertical(id="task-panel"):
133
+ yield Static("Tasks", classes="panel-title")
134
+ yield DataTable(id="task-table")
135
+ with Vertical(id="right-panel"):
136
+ with Vertical(id="event-log"):
137
+ yield Static("Recent Events", classes="panel-title")
138
+ yield RichLog(id="event-log-content", highlight=True, markup=True)
139
+ with Vertical(id="blocker-panel"):
140
+ yield Static("Open Blockers", classes="panel-title")
141
+ yield RichLog(id="blocker-log", highlight=True, markup=True)
142
+ with Vertical(id="proof-panel"):
143
+ yield Static("PROOF9 Obligations", classes="panel-title")
144
+ yield RichLog(id="proof-log", highlight=True, markup=True)
145
+ yield Footer()
146
+
147
+ def on_mount(self) -> None:
148
+ # Set up task table columns
149
+ table = self.query_one("#task-table", DataTable)
150
+ table.add_columns("ID", "Title", "Status", "Priority")
151
+ table.cursor_type = "row"
152
+
153
+ # Initial data load
154
+ self._refresh_data()
155
+
156
+ # Auto-refresh
157
+ self.set_interval(self.refresh_interval, self._refresh_data)
158
+
159
+ def _refresh_data(self) -> None:
160
+ """Load fresh data from the workspace and update all widgets."""
161
+ if not self.workspace:
162
+ return
163
+
164
+ data = load_dashboard_data(self.workspace)
165
+ self.data = data
166
+
167
+ self._update_status_bar(data)
168
+ self._update_task_table(data)
169
+ self._update_event_log(data)
170
+ self._update_blocker_panel(data)
171
+ self._update_proof_panel(data)
172
+
173
+ if data.error:
174
+ self.notify(f"Data loading error: {data.error}", severity="warning")
175
+
176
+ def _update_status_bar(self, data: DashboardData) -> None:
177
+ status_bar = self.query_one("#status-bar", StatusBar)
178
+ status_bar.update_from_data(data)
179
+
180
+ def _update_task_table(self, data: DashboardData) -> None:
181
+ table = self.query_one("#task-table", DataTable)
182
+ table.clear()
183
+
184
+ for task in data.tasks:
185
+ status_val = task.status.value if hasattr(task.status, "value") else str(task.status)
186
+ color = _STATUS_COLORS.get(status_val, "white")
187
+ table.add_row(
188
+ task.id[:8],
189
+ task.title[:50],
190
+ f"[{color}]{status_val}[/{color}]",
191
+ str(task.priority),
192
+ )
193
+
194
+ def _update_event_log(self, data: DashboardData) -> None:
195
+ log = self.query_one("#event-log-content", RichLog)
196
+ log.clear()
197
+
198
+ for event in reversed(data.events): # oldest first
199
+ ts = event.created_at.strftime("%H:%M:%S") if hasattr(event.created_at, "strftime") else str(event.created_at)[:8]
200
+ log.write(f"[dim]{ts}[/dim] {event.event_type}")
201
+
202
+ def _update_blocker_panel(self, data: DashboardData) -> None:
203
+ log = self.query_one("#blocker-log", RichLog)
204
+ log.clear()
205
+
206
+ if not data.blockers:
207
+ log.write("[dim]No open blockers[/dim]")
208
+ return
209
+
210
+ for blocker in data.blockers:
211
+ log.write(f"[red bold]{blocker.id[:8]}[/red bold]: {blocker.question[:60]}")
212
+
213
+ _SEVERITY_COLORS: dict[str, str] = {
214
+ "critical": "red bold",
215
+ "high": "red",
216
+ "medium": "yellow",
217
+ "low": "dim",
218
+ }
219
+
220
+ _OBLIGATION_ICONS: dict[str, str] = {
221
+ "satisfied": "✅",
222
+ "failed": "❌",
223
+ "pending": "⏳",
224
+ }
225
+
226
+ def _update_proof_panel(self, data: DashboardData) -> None:
227
+ log = self.query_one("#proof-log", RichLog)
228
+ log.clear()
229
+
230
+ if not data.open_requirements and not data.expiring_waivers:
231
+ log.write("[dim]No open obligations[/dim]")
232
+ return
233
+
234
+ for req in data.expiring_waivers:
235
+ from datetime import date
236
+ days = (req.waiver.expires - date.today()).days
237
+ log.write(
238
+ f"[yellow bold]⚠ {req.id}[/yellow bold]: waiver expires in {days}d — {req.title[:50]}"
239
+ )
240
+
241
+ for req in data.open_requirements:
242
+ gate_parts = [
243
+ f"{obl.gate.value.upper()} {self._OBLIGATION_ICONS.get(obl.status, '⏳')}"
244
+ for obl in req.obligations
245
+ ]
246
+ gate_summary = " ".join(gate_parts)
247
+ sev_color = self._SEVERITY_COLORS.get(req.severity.value, "white")
248
+ log.write(
249
+ f"[red]{req.id}[/red] [{sev_color}]{req.severity.value}[/{sev_color}]"
250
+ f" {req.title[:45]} | {gate_summary}"
251
+ )
252
+
253
+ def action_refresh(self) -> None:
254
+ """Manual refresh via 'r' key."""
255
+ self._refresh_data()
256
+ self.notify("Refreshed")
@@ -0,0 +1,103 @@
1
+ """Dashboard data service — thin wrapper over core modules.
2
+
3
+ Loads all dashboard data in a single call to minimize DB access.
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional
8
+
9
+ from codeframe.core.workspace import Workspace
10
+
11
+
12
+ @dataclass
13
+ class DashboardData:
14
+ """Snapshot of workspace state for display."""
15
+
16
+ workspace_name: str = ""
17
+ workspace_path: str = ""
18
+ tech_stack: str = ""
19
+
20
+ # Task counts by status
21
+ task_counts: dict[str, int] = field(default_factory=dict)
22
+ tasks: list = field(default_factory=list)
23
+
24
+ # Open blockers
25
+ blockers: list = field(default_factory=list)
26
+ blocker_count: int = 0
27
+
28
+ # Recent events
29
+ events: list = field(default_factory=list)
30
+
31
+ # PROOF9 obligations
32
+ open_requirements: list = field(default_factory=list)
33
+ expiring_waivers: list = field(default_factory=list)
34
+ open_obligation_count: int = 0
35
+
36
+ # Error (if data loading failed)
37
+ error: Optional[str] = None
38
+
39
+
40
+ def load_dashboard_data(
41
+ workspace: Workspace, event_limit: int = 50
42
+ ) -> DashboardData:
43
+ """Load all dashboard data from a workspace.
44
+
45
+ Queries tasks, blockers, and events in one shot.
46
+ Returns a DashboardData snapshot for rendering.
47
+ """
48
+ data = DashboardData(
49
+ workspace_name=workspace.repo_path.name,
50
+ workspace_path=str(workspace.repo_path),
51
+ tech_stack=workspace.tech_stack or "",
52
+ )
53
+
54
+ try:
55
+ from codeframe.core import tasks as task_module
56
+
57
+ all_tasks = task_module.list_tasks(workspace)
58
+ data.tasks = all_tasks
59
+
60
+ counts: dict[str, int] = {}
61
+ for t in all_tasks:
62
+ status_name = t.status.value if hasattr(t.status, "value") else str(t.status)
63
+ counts[status_name] = counts.get(status_name, 0) + 1
64
+ data.task_counts = counts
65
+ except Exception as exc:
66
+ data.error = f"Tasks: {exc}"
67
+
68
+ try:
69
+ from codeframe.core import blockers
70
+ data.blockers = blockers.list_open(workspace)
71
+ data.blocker_count = len(data.blockers)
72
+ except Exception as exc:
73
+ if not data.error:
74
+ data.error = f"Blockers: {exc}"
75
+
76
+ try:
77
+ from codeframe.core.events import list_recent
78
+ data.events = list_recent(workspace, limit=event_limit)
79
+ except Exception as exc:
80
+ if not data.error:
81
+ data.error = f"Events: {exc}"
82
+
83
+ try:
84
+ from datetime import date
85
+ from codeframe.core.proof.ledger import list_requirements
86
+ from codeframe.core.proof.models import ReqStatus
87
+
88
+ open_reqs = list_requirements(workspace, status=ReqStatus.OPEN)
89
+ data.open_requirements = open_reqs
90
+ data.open_obligation_count = len(open_reqs)
91
+
92
+ waived = list_requirements(workspace, status=ReqStatus.WAIVED)
93
+ today = date.today()
94
+ data.expiring_waivers = [
95
+ req for req in waived
96
+ if req.waiver and req.waiver.expires is not None
97
+ and 0 <= (req.waiver.expires - today).days <= 7
98
+ ]
99
+ except Exception as exc:
100
+ if not data.error:
101
+ data.error = f"Proof: {exc}"
102
+
103
+ return data
File without changes
@@ -0,0 +1,103 @@
1
+ """FastAPI dependency injection providers.
2
+
3
+ This module provides dependency injection functions for accessing
4
+ shared application state across all API endpoints.
5
+
6
+ v2-only: All dependencies use codeframe.core modules.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from fastapi import HTTPException, Query, Request
13
+
14
+ from codeframe.workspace import WorkspaceManager
15
+
16
+ # v2 imports
17
+ from codeframe.core.workspace import Workspace, get_workspace, workspace_exists
18
+
19
+
20
+ def get_workspace_manager(request: Request) -> WorkspaceManager:
21
+ """Get workspace manager from application state.
22
+
23
+ Args:
24
+ request: FastAPI request object
25
+
26
+ Returns:
27
+ WorkspaceManager instance from app.state.workspace_manager
28
+
29
+ Usage:
30
+ @router.post("/endpoint")
31
+ async def endpoint(workspace_mgr: WorkspaceManager = Depends(get_workspace_manager)):
32
+ # Use workspace_mgr here
33
+ ...
34
+ """
35
+ return request.app.state.workspace_manager
36
+
37
+
38
+ def get_v2_workspace(
39
+ workspace_path: Optional[str] = Query(
40
+ None,
41
+ description="Path to workspace directory (defaults to server's working directory)",
42
+ ),
43
+ request: Request = None,
44
+ ) -> Workspace:
45
+ """Get v2 Workspace from path or server default.
46
+
47
+ This dependency resolves a Workspace from either:
48
+ 1. An explicit workspace_path query parameter
49
+ 2. The server's default workspace (from app.state.default_workspace_path)
50
+ 3. The server's current working directory
51
+
52
+ Args:
53
+ workspace_path: Optional explicit path to workspace
54
+ request: FastAPI request for accessing app state
55
+
56
+ Returns:
57
+ v2 Workspace instance
58
+
59
+ Raises:
60
+ HTTPException:
61
+ - 400: No workspace path provided and no default configured
62
+ - 404: Workspace not found at path
63
+
64
+ Usage:
65
+ @router.get("/v2/endpoint")
66
+ async def endpoint(workspace: Workspace = Depends(get_v2_workspace)):
67
+ # Use workspace here
68
+ ...
69
+ """
70
+ # Resolve workspace path
71
+ if workspace_path:
72
+ path = Path(workspace_path).resolve()
73
+ elif request and getattr(request.app.state, "default_workspace_path", None):
74
+ path = Path(request.app.state.default_workspace_path).resolve()
75
+ else:
76
+ # Fall back to current working directory
77
+ path = Path.cwd()
78
+
79
+ # Validate workspace exists
80
+ # Note: Avoid exposing full filesystem paths in error messages for hosted deployments
81
+ if not workspace_exists(path):
82
+ raise HTTPException(
83
+ status_code=404,
84
+ detail="Workspace not found at specified path. Initialize with 'cf init <path>'",
85
+ )
86
+
87
+ try:
88
+ workspace = get_workspace(path)
89
+ except FileNotFoundError:
90
+ raise HTTPException(
91
+ status_code=404,
92
+ detail="Workspace not found at specified path. Initialize with 'cf init <path>'",
93
+ )
94
+
95
+ # Note: get_workspace() raises FileNotFoundError rather than returning None,
96
+ # so no additional null check is needed here.
97
+ return workspace
98
+
99
+
100
+ __all__ = [
101
+ "get_workspace_manager",
102
+ "get_v2_workspace",
103
+ ]