cinchdb 0.1.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 (68) hide show
  1. cinchdb/__init__.py +7 -0
  2. cinchdb/__main__.py +6 -0
  3. cinchdb/api/__init__.py +5 -0
  4. cinchdb/api/app.py +76 -0
  5. cinchdb/api/auth.py +290 -0
  6. cinchdb/api/main.py +137 -0
  7. cinchdb/api/routers/__init__.py +25 -0
  8. cinchdb/api/routers/auth.py +135 -0
  9. cinchdb/api/routers/branches.py +368 -0
  10. cinchdb/api/routers/codegen.py +164 -0
  11. cinchdb/api/routers/columns.py +290 -0
  12. cinchdb/api/routers/data.py +479 -0
  13. cinchdb/api/routers/databases.py +177 -0
  14. cinchdb/api/routers/projects.py +133 -0
  15. cinchdb/api/routers/query.py +156 -0
  16. cinchdb/api/routers/tables.py +349 -0
  17. cinchdb/api/routers/tenants.py +216 -0
  18. cinchdb/api/routers/views.py +219 -0
  19. cinchdb/cli/__init__.py +0 -0
  20. cinchdb/cli/commands/__init__.py +1 -0
  21. cinchdb/cli/commands/branch.py +479 -0
  22. cinchdb/cli/commands/codegen.py +176 -0
  23. cinchdb/cli/commands/column.py +308 -0
  24. cinchdb/cli/commands/database.py +212 -0
  25. cinchdb/cli/commands/query.py +136 -0
  26. cinchdb/cli/commands/remote.py +144 -0
  27. cinchdb/cli/commands/table.py +289 -0
  28. cinchdb/cli/commands/tenant.py +173 -0
  29. cinchdb/cli/commands/view.py +189 -0
  30. cinchdb/cli/handlers/__init__.py +5 -0
  31. cinchdb/cli/handlers/codegen_handler.py +189 -0
  32. cinchdb/cli/main.py +137 -0
  33. cinchdb/cli/utils.py +182 -0
  34. cinchdb/config.py +177 -0
  35. cinchdb/core/__init__.py +5 -0
  36. cinchdb/core/connection.py +175 -0
  37. cinchdb/core/database.py +537 -0
  38. cinchdb/core/maintenance.py +73 -0
  39. cinchdb/core/path_utils.py +153 -0
  40. cinchdb/managers/__init__.py +26 -0
  41. cinchdb/managers/branch.py +167 -0
  42. cinchdb/managers/change_applier.py +414 -0
  43. cinchdb/managers/change_comparator.py +194 -0
  44. cinchdb/managers/change_tracker.py +182 -0
  45. cinchdb/managers/codegen.py +523 -0
  46. cinchdb/managers/column.py +579 -0
  47. cinchdb/managers/data.py +455 -0
  48. cinchdb/managers/merge_manager.py +429 -0
  49. cinchdb/managers/query.py +214 -0
  50. cinchdb/managers/table.py +383 -0
  51. cinchdb/managers/tenant.py +258 -0
  52. cinchdb/managers/view.py +252 -0
  53. cinchdb/models/__init__.py +27 -0
  54. cinchdb/models/base.py +44 -0
  55. cinchdb/models/branch.py +26 -0
  56. cinchdb/models/change.py +47 -0
  57. cinchdb/models/database.py +20 -0
  58. cinchdb/models/project.py +20 -0
  59. cinchdb/models/table.py +86 -0
  60. cinchdb/models/tenant.py +19 -0
  61. cinchdb/models/view.py +15 -0
  62. cinchdb/utils/__init__.py +15 -0
  63. cinchdb/utils/sql_validator.py +137 -0
  64. cinchdb-0.1.0.dist-info/METADATA +195 -0
  65. cinchdb-0.1.0.dist-info/RECORD +68 -0
  66. cinchdb-0.1.0.dist-info/WHEEL +4 -0
  67. cinchdb-0.1.0.dist-info/entry_points.txt +3 -0
  68. cinchdb-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,479 @@
1
+ """Branch management commands for CinchDB CLI."""
2
+
3
+ import typer
4
+ from typing import Optional
5
+ from pathlib import Path
6
+ from rich.console import Console
7
+ from rich.table import Table as RichTable
8
+
9
+ from cinchdb.config import Config
10
+ from cinchdb.core.path_utils import get_project_root
11
+ from cinchdb.managers.branch import BranchManager
12
+ from cinchdb.cli.utils import (
13
+ get_config_with_data,
14
+ set_active_branch,
15
+ validate_required_arg,
16
+ )
17
+
18
+ app = typer.Typer(help="Branch management commands", invoke_without_command=True)
19
+ console = Console()
20
+
21
+
22
+ @app.callback()
23
+ def callback(ctx: typer.Context):
24
+ """Show help when no subcommand is provided."""
25
+ if ctx.invoked_subcommand is None:
26
+ console.print(ctx.get_help())
27
+ raise typer.Exit(0)
28
+
29
+
30
+ def get_config() -> Config:
31
+ """Get config from current directory."""
32
+ project_root = get_project_root(Path.cwd())
33
+ if not project_root:
34
+ console.print("[red]❌ Not in a CinchDB project directory[/red]")
35
+ raise typer.Exit(1)
36
+ return Config(project_root)
37
+
38
+
39
+ @app.command(name="list")
40
+ def list_branches():
41
+ """List all branches in the current database."""
42
+ config, config_data = get_config_with_data()
43
+ db_name = config_data.active_database
44
+
45
+ branch_mgr = BranchManager(config.project_dir, db_name)
46
+ branches = branch_mgr.list_branches()
47
+
48
+ if not branches:
49
+ console.print("[yellow]No branches found[/yellow]")
50
+ return
51
+
52
+ # Create a table
53
+ table = RichTable(title=f"Branches in '{db_name}'")
54
+ table.add_column("Name", style="cyan")
55
+ table.add_column("Active", style="green")
56
+ table.add_column("Parent", style="yellow")
57
+ table.add_column("Protected", style="red")
58
+
59
+ current_branch = config_data.active_branch
60
+
61
+ for branch in branches:
62
+ is_active = "✓" if branch.name == current_branch else ""
63
+ is_protected = "✓" if branch.name == "main" else ""
64
+ parent = branch.parent_branch or "-"
65
+ table.add_row(branch.name, is_active, parent, is_protected)
66
+
67
+ console.print(table)
68
+
69
+
70
+ @app.command()
71
+ def create(
72
+ ctx: typer.Context,
73
+ name: Optional[str] = typer.Argument(None, help="Name of the new branch"),
74
+ source: Optional[str] = typer.Option(
75
+ None, "--source", "-s", help="Source branch (default: current)"
76
+ ),
77
+ switch: bool = typer.Option(
78
+ False, "--switch", help="Switch to the new branch after creation"
79
+ ),
80
+ ):
81
+ """Create a new branch."""
82
+ name = validate_required_arg(name, "name", ctx)
83
+ config, config_data = get_config_with_data()
84
+ db_name = config_data.active_database
85
+ source_branch = source or config_data.active_branch
86
+
87
+ try:
88
+ branch_mgr = BranchManager(config.project_dir, db_name)
89
+ branch_mgr.create_branch(source_branch, name)
90
+ console.print(
91
+ f"[green]✅ Created branch '{name}' from '{source_branch}'[/green]"
92
+ )
93
+
94
+ if switch:
95
+ set_active_branch(config, name)
96
+ console.print(f"[green]✅ Switched to branch '{name}'[/green]")
97
+
98
+ except ValueError as e:
99
+ console.print(f"[red]❌ {e}[/red]")
100
+ raise typer.Exit(1)
101
+
102
+
103
+ @app.command()
104
+ def delete(
105
+ ctx: typer.Context,
106
+ name: Optional[str] = typer.Argument(None, help="Name of the branch to delete"),
107
+ force: bool = typer.Option(
108
+ False, "--force", "-f", help="Force deletion without confirmation"
109
+ ),
110
+ ):
111
+ """Delete a branch."""
112
+ name = validate_required_arg(name, "name", ctx)
113
+ config, config_data = get_config_with_data()
114
+ db_name = config_data.active_database
115
+
116
+ if name == "main":
117
+ console.print("[red]❌ Cannot delete the main branch[/red]")
118
+ raise typer.Exit(1)
119
+
120
+ # Confirmation
121
+ if not force:
122
+ confirm = typer.confirm(f"Are you sure you want to delete branch '{name}'?")
123
+ if not confirm:
124
+ console.print("[yellow]Cancelled[/yellow]")
125
+ raise typer.Exit(0)
126
+
127
+ try:
128
+ branch_mgr = BranchManager(config.project_dir, db_name)
129
+ branch_mgr.delete_branch(name)
130
+ console.print(f"[green]✅ Deleted branch '{name}'[/green]")
131
+
132
+ except ValueError as e:
133
+ console.print(f"[red]❌ {e}[/red]")
134
+ raise typer.Exit(1)
135
+
136
+
137
+ @app.command()
138
+ def switch(
139
+ ctx: typer.Context,
140
+ name: Optional[str] = typer.Argument(None, help="Name of the branch to switch to"),
141
+ ):
142
+ """Switch to a different branch."""
143
+ name = validate_required_arg(name, "name", ctx)
144
+ config, config_data = get_config_with_data()
145
+ db_name = config_data.active_database
146
+
147
+ try:
148
+ BranchManager(config.project_dir, db_name)
149
+ set_active_branch(config, name)
150
+ console.print(f"[green]✅ Switched to branch '{name}'[/green]")
151
+
152
+ except ValueError as e:
153
+ console.print(f"[red]❌ {e}[/red]")
154
+ raise typer.Exit(1)
155
+
156
+
157
+ @app.command()
158
+ def info(
159
+ name: Optional[str] = typer.Argument(None, help="Branch name (default: current)"),
160
+ ):
161
+ """Show information about a branch."""
162
+ config, config_data = get_config_with_data()
163
+ db_name = config_data.active_database
164
+ branch_name = name or config_data.active_branch
165
+
166
+ try:
167
+ branch_mgr = BranchManager(config.project_dir, db_name)
168
+ branches = branch_mgr.list_branches()
169
+
170
+ branch = next((b for b in branches if b.name == branch_name), None)
171
+ if not branch:
172
+ console.print(f"[red]❌ Branch '{branch_name}' does not exist[/red]")
173
+ raise typer.Exit(1)
174
+
175
+ # Display info
176
+ console.print(f"\n[bold]Branch: {branch.name}[/bold]")
177
+ console.print(f"Database: {db_name}")
178
+ console.print(f"Parent: {branch.parent_branch or 'None'}")
179
+ created_at = branch.metadata.get("created_at", "Unknown")
180
+ if created_at != "Unknown":
181
+ from datetime import datetime
182
+
183
+ try:
184
+ dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
185
+ created_at = dt.strftime("%Y-%m-%d %H:%M:%S")
186
+ except Exception:
187
+ pass
188
+ console.print(f"Created: {created_at}")
189
+ console.print(f"Protected: {'Yes' if branch.name == 'main' else 'No'}")
190
+
191
+ # Count tenants
192
+ from cinchdb.managers.tenant import TenantManager
193
+
194
+ tenant_mgr = TenantManager(config.project_dir, db_name, branch_name)
195
+ tenants = tenant_mgr.list_tenants()
196
+ console.print(f"Tenants: {len(tenants)}")
197
+
198
+ # Count changes
199
+ from cinchdb.managers.change_tracker import ChangeTracker
200
+
201
+ tracker = ChangeTracker(config.project_dir, db_name, branch_name)
202
+ changes = tracker.get_changes()
203
+ unapplied = tracker.get_unapplied_changes()
204
+ console.print(f"Total Changes: {len(changes)}")
205
+ console.print(f"Unapplied Changes: {len(unapplied)}")
206
+
207
+ except ValueError as e:
208
+ console.print(f"[red]❌ {e}[/red]")
209
+ raise typer.Exit(1)
210
+
211
+
212
+ @app.command()
213
+ def merge(
214
+ ctx: typer.Context,
215
+ source: Optional[str] = typer.Argument(None, help="Source branch to merge from"),
216
+ target: Optional[str] = typer.Option(
217
+ None, "--target", "-t", help="Target branch (default: current)"
218
+ ),
219
+ force: bool = typer.Option(
220
+ False, "--force", "-f", help="Force merge even with conflicts"
221
+ ),
222
+ preview: bool = typer.Option(
223
+ False, "--preview", "-p", help="Show merge preview without executing"
224
+ ),
225
+ dry_run: bool = typer.Option(
226
+ False,
227
+ "--dry-run",
228
+ help="Show SQL statements that would be executed without applying them",
229
+ ),
230
+ ):
231
+ """Merge changes from source branch into target branch."""
232
+ source = validate_required_arg(source, "source", ctx)
233
+ config, config_data = get_config_with_data()
234
+ db_name = config_data.active_database
235
+ target_branch = target or config_data.active_branch
236
+
237
+ from cinchdb.managers.merge_manager import MergeManager, MergeError
238
+
239
+ try:
240
+ merge_mgr = MergeManager(config.project_dir, db_name)
241
+
242
+ if preview:
243
+ # Show merge preview
244
+ preview_result = merge_mgr.get_merge_preview(source, target_branch)
245
+
246
+ if not preview_result["can_merge"]:
247
+ console.print(f"[red]❌ Cannot merge: {preview_result['reason']}[/red]")
248
+ if "conflicts" in preview_result:
249
+ console.print("[yellow]Conflicts:[/yellow]")
250
+ for conflict in preview_result["conflicts"]:
251
+ console.print(f" • {conflict}")
252
+ raise typer.Exit(1)
253
+
254
+ console.print(f"\n[bold]Merge Preview: {source} → {target_branch}[/bold]")
255
+ console.print(f"Merge Type: {preview_result['merge_type']}")
256
+ console.print(f"Changes to merge: {preview_result['changes_to_merge']}")
257
+ console.print(
258
+ f"Target has changes: {preview_result.get('target_has_changes', False)}"
259
+ )
260
+
261
+ if preview_result["changes_by_type"]:
262
+ console.print("\n[bold]Changes by type:[/bold]")
263
+ for entity_type, changes in preview_result["changes_by_type"].items():
264
+ console.print(f" {entity_type}: {len(changes)} changes")
265
+ for change in changes[:3]: # Show first 3
266
+ console.print(
267
+ f" • {change['operation']} {change['entity_name']}"
268
+ )
269
+ if len(changes) > 3:
270
+ console.print(f" • ... and {len(changes) - 3} more")
271
+
272
+ return
273
+
274
+ # Handle dry-run
275
+ if dry_run:
276
+ result = merge_mgr.merge_branches(
277
+ source, target_branch, force=force, dry_run=True
278
+ )
279
+
280
+ console.print(f"\n[bold]Dry Run: {source} → {target_branch}[/bold]")
281
+ console.print(f"Merge Type: {result.get('merge_type', 'unknown')}")
282
+ console.print(f"Changes to merge: {result.get('changes_to_merge', 0)}")
283
+
284
+ if result.get("sql_statements"):
285
+ console.print("\n[bold]SQL statements that would be executed:[/bold]")
286
+ for stmt in result["sql_statements"]:
287
+ console.print(
288
+ f"\n[cyan]Change {stmt['change_id']} ({stmt['change_type']}): {stmt['entity_name']}[/cyan]"
289
+ )
290
+ if "step" in stmt:
291
+ console.print(f" Step: {stmt['step']}")
292
+ console.print(f" SQL: [yellow]{stmt['sql']}[/yellow]")
293
+ else:
294
+ console.print("\n[yellow]No SQL statements to execute[/yellow]")
295
+
296
+ return
297
+
298
+ # Perform actual merge
299
+ result = merge_mgr.merge_branches(source, target_branch, force=force)
300
+
301
+ if result["success"]:
302
+ console.print(f"[green]✅ {result['message']}[/green]")
303
+ console.print(f"Merge type: {result.get('merge_type', 'unknown')}")
304
+ else:
305
+ console.print(
306
+ f"[red]❌ Merge failed: {result.get('message', 'Unknown error')}[/red]"
307
+ )
308
+ raise typer.Exit(1)
309
+
310
+ except MergeError as e:
311
+ console.print(f"[red]❌ {e}[/red]")
312
+ raise typer.Exit(1)
313
+ except ValueError as e:
314
+ console.print(f"[red]❌ {e}[/red]")
315
+ raise typer.Exit(1)
316
+
317
+
318
+ @app.command()
319
+ def changes(
320
+ name: Optional[str] = typer.Argument(None, help="Branch name (default: current)"),
321
+ format: str = typer.Option(
322
+ "table", "--format", "-f", help="Output format (table, json)"
323
+ ),
324
+ ):
325
+ """List all changes in a branch."""
326
+ config, config_data = get_config_with_data()
327
+ db_name = config_data.active_database
328
+ branch_name = name or config_data.active_branch
329
+
330
+ try:
331
+ from cinchdb.managers.change_tracker import ChangeTracker
332
+
333
+ tracker = ChangeTracker(config.project_dir, db_name, branch_name)
334
+ changes = tracker.get_changes()
335
+
336
+ if not changes:
337
+ console.print(f"[yellow]No changes found in branch '{branch_name}'[/yellow]")
338
+ return
339
+
340
+ if format == "json":
341
+ # JSON output
342
+ import json
343
+ from datetime import datetime
344
+
345
+ changes_data = []
346
+ for change in changes:
347
+ change_dict = change.model_dump(mode='json')
348
+ changes_data.append(change_dict)
349
+
350
+ console.print(json.dumps(changes_data, indent=2, default=str))
351
+ else:
352
+ # Table output
353
+ table = RichTable(title=f"Changes in '{branch_name}' branch")
354
+ table.add_column("ID", style="cyan", no_wrap=True)
355
+ table.add_column("Type", style="yellow")
356
+ table.add_column("Entity", style="green")
357
+ table.add_column("Entity Type", style="blue")
358
+ table.add_column("Applied", style="magenta")
359
+ table.add_column("Created", style="dim")
360
+
361
+ for change in changes:
362
+ created_at = change.created_at.strftime("%Y-%m-%d %H:%M:%S") if change.created_at else "Unknown"
363
+ applied_status = "✓" if change.applied else "✗"
364
+ table.add_row(
365
+ change.id[:8] if change.id else "Unknown",
366
+ change.type.value if hasattr(change.type, 'value') else str(change.type),
367
+ change.entity_name,
368
+ change.entity_type,
369
+ applied_status,
370
+ created_at
371
+ )
372
+
373
+ console.print(table)
374
+
375
+ # Summary
376
+ total = len(changes)
377
+ applied = sum(1 for c in changes if c.applied)
378
+ unapplied = total - applied
379
+ console.print(f"\n[bold]Total:[/bold] {total} changes ({applied} applied, {unapplied} unapplied)")
380
+
381
+ except ValueError as e:
382
+ console.print(f"[red]❌ {e}[/red]")
383
+ raise typer.Exit(1)
384
+
385
+
386
+ @app.command()
387
+ def merge_into_main(
388
+ ctx: typer.Context,
389
+ source: Optional[str] = typer.Argument(
390
+ None, help="Source branch to merge into main"
391
+ ),
392
+ preview: bool = typer.Option(
393
+ False, "--preview", "-p", help="Show merge preview without executing"
394
+ ),
395
+ dry_run: bool = typer.Option(
396
+ False,
397
+ "--dry-run",
398
+ help="Show SQL statements that would be executed without applying them",
399
+ ),
400
+ ):
401
+ """Merge a branch into main branch (the primary way to get changes into main)."""
402
+ source = validate_required_arg(source, "source", ctx)
403
+ config, config_data = get_config_with_data()
404
+ db_name = config_data.active_database
405
+
406
+ from cinchdb.managers.merge_manager import MergeManager, MergeError
407
+
408
+ try:
409
+ merge_mgr = MergeManager(config.project_dir, db_name)
410
+
411
+ if preview:
412
+ # Show merge preview for main branch
413
+ preview_result = merge_mgr.get_merge_preview(source, "main")
414
+
415
+ if not preview_result["can_merge"]:
416
+ console.print(
417
+ f"[red]❌ Cannot merge into main: {preview_result['reason']}[/red]"
418
+ )
419
+ if "conflicts" in preview_result:
420
+ console.print("[yellow]Conflicts:[/yellow]")
421
+ for conflict in preview_result["conflicts"]:
422
+ console.print(f" • {conflict}")
423
+ raise typer.Exit(1)
424
+
425
+ console.print(f"\n[bold]Merge Preview: {source} → main[/bold]")
426
+ console.print(f"Merge Type: {preview_result['merge_type']}")
427
+ console.print(f"Changes to merge: {preview_result['changes_to_merge']}")
428
+
429
+ if preview_result["changes_by_type"]:
430
+ console.print("\n[bold]Changes to be merged:[/bold]")
431
+ for entity_type, changes in preview_result["changes_by_type"].items():
432
+ console.print(f" {entity_type}: {len(changes)} changes")
433
+ for change in changes:
434
+ console.print(
435
+ f" • {change['operation']} {change['entity_name']}"
436
+ )
437
+
438
+ return
439
+
440
+ # Handle dry-run
441
+ if dry_run:
442
+ result = merge_mgr.merge_into_main(source, dry_run=True)
443
+
444
+ console.print(f"\n[bold]Dry Run: {source} → main[/bold]")
445
+ console.print(f"Merge Type: {result.get('merge_type', 'unknown')}")
446
+ console.print(f"Changes to merge: {result.get('changes_to_merge', 0)}")
447
+
448
+ if result.get("sql_statements"):
449
+ console.print("\n[bold]SQL statements that would be executed:[/bold]")
450
+ for stmt in result["sql_statements"]:
451
+ console.print(
452
+ f"\n[cyan]Change {stmt['change_id']} ({stmt['change_type']}): {stmt['entity_name']}[/cyan]"
453
+ )
454
+ if "step" in stmt:
455
+ console.print(f" Step: {stmt['step']}")
456
+ console.print(f" SQL: [yellow]{stmt['sql']}[/yellow]")
457
+ else:
458
+ console.print("\n[yellow]No SQL statements to execute[/yellow]")
459
+
460
+ return
461
+
462
+ # Perform merge into main
463
+ result = merge_mgr.merge_into_main(source)
464
+
465
+ if result["success"]:
466
+ console.print(f"[green]✅ {result['message']}[/green]")
467
+ console.print("[green]Main branch has been updated![/green]")
468
+ else:
469
+ console.print(
470
+ f"[red]❌ Merge into main failed: {result.get('message', 'Unknown error')}[/red]"
471
+ )
472
+ raise typer.Exit(1)
473
+
474
+ except MergeError as e:
475
+ console.print(f"[red]❌ {e}[/red]")
476
+ raise typer.Exit(1)
477
+ except ValueError as e:
478
+ console.print(f"[red]❌ {e}[/red]")
479
+ raise typer.Exit(1)
@@ -0,0 +1,176 @@
1
+ """Codegen CLI commands."""
2
+
3
+ import typer
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+ from rich.table import Table as RichTable
7
+ from typing import Optional
8
+
9
+ from ..utils import get_config_with_data, get_config_dict, validate_required_arg
10
+ from ..handlers import CodegenHandler
11
+
12
+ console = Console()
13
+
14
+ app = typer.Typer(
15
+ help="Generate models from database schemas", invoke_without_command=True
16
+ )
17
+
18
+
19
+ @app.callback(invoke_without_command=True)
20
+ def main(ctx: typer.Context):
21
+ """Generate models from database schemas."""
22
+ if ctx.invoked_subcommand is None:
23
+ print(ctx.get_help())
24
+ raise typer.Exit(0)
25
+
26
+
27
+ @app.command()
28
+ def languages():
29
+ """List available code generation languages."""
30
+ try:
31
+ config, config_data = get_config_with_data()
32
+ config_dict = get_config_dict()
33
+
34
+ # Create handler to get supported languages
35
+ handler = CodegenHandler(config_dict)
36
+ supported = handler.get_supported_languages(project_root=config.project_dir)
37
+
38
+ table = RichTable(title="Supported Languages")
39
+ table.add_column("Language", style="cyan")
40
+ table.add_column("Status", style="green")
41
+ table.add_column("Source", style="blue")
42
+
43
+ for lang in supported:
44
+ status = "Available" if lang == "python" else "Coming Soon"
45
+ source = "Remote API" if handler.is_remote else "Local"
46
+ table.add_row(lang, status, source)
47
+
48
+ console.print(table)
49
+
50
+ except Exception as e:
51
+ console.print(f"[red]❌ Error: {e}[/red]")
52
+ raise typer.Exit(1)
53
+
54
+
55
+ @app.command()
56
+ def generate(
57
+ ctx: typer.Context,
58
+ language: Optional[str] = typer.Argument(
59
+ None, help="Target language (python, typescript)"
60
+ ),
61
+ output_dir: Optional[str] = typer.Argument(
62
+ None, help="Output directory for generated models"
63
+ ),
64
+ database: Optional[str] = typer.Option(
65
+ None, "--database", "-d", help="Database name (defaults to active)"
66
+ ),
67
+ branch: Optional[str] = typer.Option(
68
+ None, "--branch", "-b", help="Branch name (defaults to active)"
69
+ ),
70
+ tenant: str = typer.Option("main", "--tenant", "-t", help="Tenant name"),
71
+ include_tables: bool = typer.Option(
72
+ True, "--tables/--no-tables", help="Include table models"
73
+ ),
74
+ include_views: bool = typer.Option(
75
+ True, "--views/--no-views", help="Include view models"
76
+ ),
77
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
78
+ api_url: Optional[str] = typer.Option(
79
+ None, "--api-url", help="API URL for remote generation"
80
+ ),
81
+ api_key: Optional[str] = typer.Option(
82
+ None, "--api-key", help="API key for remote generation"
83
+ ),
84
+ local: bool = typer.Option(
85
+ False, "--local", help="Force local generation even if API configured"
86
+ ),
87
+ ):
88
+ """Generate model files for the specified language."""
89
+ language = validate_required_arg(language, "language", ctx)
90
+ output_dir = validate_required_arg(output_dir, "output_dir", ctx)
91
+ try:
92
+ config, config_data = get_config_with_data()
93
+ config_dict = get_config_dict()
94
+
95
+ # Use provided values or defaults from config
96
+ db_name = database or config_data.active_database
97
+ branch_name = branch or config_data.active_branch
98
+
99
+ # Create output directory path
100
+ output_path = Path(output_dir).resolve()
101
+
102
+ # Check if output directory exists and has files
103
+ if output_path.exists() and any(output_path.iterdir()) and not force:
104
+ console.print(
105
+ f"[yellow]⚠️ Output directory '{output_path}' already contains files.[/yellow]"
106
+ )
107
+ console.print("Use --force to overwrite existing files.")
108
+ raise typer.Exit(1)
109
+
110
+ # Create handler with API options
111
+ handler = CodegenHandler(
112
+ config_data=config_dict, api_url=api_url, api_key=api_key, force_local=local
113
+ )
114
+
115
+ # Validate language
116
+ supported = handler.get_supported_languages(project_root=config.project_dir)
117
+ if language not in supported:
118
+ console.print(f"[red]❌ Language '{language}' not supported.[/red]")
119
+ console.print(f"Available languages: {', '.join(supported)}")
120
+ raise typer.Exit(1)
121
+
122
+ # Show what will be generated
123
+ source = "Remote API" if handler.is_remote else "Local"
124
+ console.print(f"[blue]🔧 Generating {language} models via {source}...[/blue]")
125
+ console.print(f"Database: {db_name}")
126
+ console.print(f"Branch: {branch_name}")
127
+ console.print(f"Tenant: {tenant}")
128
+ console.print(f"Output: {output_path}")
129
+ console.print(f"Include tables: {include_tables}")
130
+ console.print(f"Include views: {include_views}")
131
+ if handler.is_remote:
132
+ console.print(f"API URL: {handler.api_url}")
133
+ console.print()
134
+
135
+ # Generate models
136
+ results = handler.generate_models(
137
+ language=language,
138
+ output_dir=output_path,
139
+ database=db_name,
140
+ branch=branch_name,
141
+ tenant=tenant,
142
+ include_tables=include_tables,
143
+ include_views=include_views,
144
+ project_root=config.project_dir,
145
+ )
146
+
147
+ # Display results
148
+ console.print(
149
+ f"[green]✅ Generated {len(results['files_generated'])} files[/green]"
150
+ )
151
+
152
+ if results.get("tables_processed"):
153
+ console.print(f"Tables processed: {', '.join(results['tables_processed'])}")
154
+
155
+ if results.get("views_processed"):
156
+ console.print(f"Views processed: {', '.join(results['views_processed'])}")
157
+
158
+ console.print(f"Output directory: {results['output_dir']}")
159
+
160
+ # Show generated files
161
+ if results["files_generated"]:
162
+ table = RichTable(title="Generated Files")
163
+ table.add_column("File", style="cyan")
164
+
165
+ for filename in results["files_generated"]:
166
+ table.add_row(filename)
167
+
168
+ console.print(table)
169
+
170
+ except Exception as e:
171
+ console.print(f"[red]❌ Error: {e}[/red]")
172
+ raise typer.Exit(1)
173
+
174
+
175
+ if __name__ == "__main__":
176
+ app()