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.
- cinchdb/__init__.py +7 -0
- cinchdb/__main__.py +6 -0
- cinchdb/api/__init__.py +5 -0
- cinchdb/api/app.py +76 -0
- cinchdb/api/auth.py +290 -0
- cinchdb/api/main.py +137 -0
- cinchdb/api/routers/__init__.py +25 -0
- cinchdb/api/routers/auth.py +135 -0
- cinchdb/api/routers/branches.py +368 -0
- cinchdb/api/routers/codegen.py +164 -0
- cinchdb/api/routers/columns.py +290 -0
- cinchdb/api/routers/data.py +479 -0
- cinchdb/api/routers/databases.py +177 -0
- cinchdb/api/routers/projects.py +133 -0
- cinchdb/api/routers/query.py +156 -0
- cinchdb/api/routers/tables.py +349 -0
- cinchdb/api/routers/tenants.py +216 -0
- cinchdb/api/routers/views.py +219 -0
- cinchdb/cli/__init__.py +0 -0
- cinchdb/cli/commands/__init__.py +1 -0
- cinchdb/cli/commands/branch.py +479 -0
- cinchdb/cli/commands/codegen.py +176 -0
- cinchdb/cli/commands/column.py +308 -0
- cinchdb/cli/commands/database.py +212 -0
- cinchdb/cli/commands/query.py +136 -0
- cinchdb/cli/commands/remote.py +144 -0
- cinchdb/cli/commands/table.py +289 -0
- cinchdb/cli/commands/tenant.py +173 -0
- cinchdb/cli/commands/view.py +189 -0
- cinchdb/cli/handlers/__init__.py +5 -0
- cinchdb/cli/handlers/codegen_handler.py +189 -0
- cinchdb/cli/main.py +137 -0
- cinchdb/cli/utils.py +182 -0
- cinchdb/config.py +177 -0
- cinchdb/core/__init__.py +5 -0
- cinchdb/core/connection.py +175 -0
- cinchdb/core/database.py +537 -0
- cinchdb/core/maintenance.py +73 -0
- cinchdb/core/path_utils.py +153 -0
- cinchdb/managers/__init__.py +26 -0
- cinchdb/managers/branch.py +167 -0
- cinchdb/managers/change_applier.py +414 -0
- cinchdb/managers/change_comparator.py +194 -0
- cinchdb/managers/change_tracker.py +182 -0
- cinchdb/managers/codegen.py +523 -0
- cinchdb/managers/column.py +579 -0
- cinchdb/managers/data.py +455 -0
- cinchdb/managers/merge_manager.py +429 -0
- cinchdb/managers/query.py +214 -0
- cinchdb/managers/table.py +383 -0
- cinchdb/managers/tenant.py +258 -0
- cinchdb/managers/view.py +252 -0
- cinchdb/models/__init__.py +27 -0
- cinchdb/models/base.py +44 -0
- cinchdb/models/branch.py +26 -0
- cinchdb/models/change.py +47 -0
- cinchdb/models/database.py +20 -0
- cinchdb/models/project.py +20 -0
- cinchdb/models/table.py +86 -0
- cinchdb/models/tenant.py +19 -0
- cinchdb/models/view.py +15 -0
- cinchdb/utils/__init__.py +15 -0
- cinchdb/utils/sql_validator.py +137 -0
- cinchdb-0.1.0.dist-info/METADATA +195 -0
- cinchdb-0.1.0.dist-info/RECORD +68 -0
- cinchdb-0.1.0.dist-info/WHEEL +4 -0
- cinchdb-0.1.0.dist-info/entry_points.txt +3 -0
- cinchdb-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
"""View management commands for CinchDB CLI."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing import Optional
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table as RichTable
|
7
|
+
|
8
|
+
from cinchdb.managers.view import ViewModel
|
9
|
+
from cinchdb.managers.change_applier import ChangeApplier
|
10
|
+
from cinchdb.cli.utils import get_config_with_data, validate_required_arg
|
11
|
+
|
12
|
+
app = typer.Typer(help="View management commands", invoke_without_command=True)
|
13
|
+
console = Console()
|
14
|
+
|
15
|
+
|
16
|
+
@app.callback()
|
17
|
+
def callback(ctx: typer.Context):
|
18
|
+
"""Show help when no subcommand is provided."""
|
19
|
+
if ctx.invoked_subcommand is None:
|
20
|
+
console.print(ctx.get_help())
|
21
|
+
raise typer.Exit(0)
|
22
|
+
|
23
|
+
|
24
|
+
@app.command(name="list")
|
25
|
+
def list_views():
|
26
|
+
"""List all views in the current branch."""
|
27
|
+
config, config_data = get_config_with_data()
|
28
|
+
db_name = config_data.active_database
|
29
|
+
branch_name = config_data.active_branch
|
30
|
+
|
31
|
+
view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
|
32
|
+
views = view_mgr.list_views()
|
33
|
+
|
34
|
+
if not views:
|
35
|
+
console.print("[yellow]No views found[/yellow]")
|
36
|
+
return
|
37
|
+
|
38
|
+
# Create a table
|
39
|
+
table = RichTable(
|
40
|
+
title=f"Views db={db_name} branch={branch_name}", title_justify="left"
|
41
|
+
)
|
42
|
+
table.add_column("Name", style="cyan")
|
43
|
+
table.add_column("SQL Length", style="green")
|
44
|
+
table.add_column("Created", style="yellow")
|
45
|
+
|
46
|
+
for view in views:
|
47
|
+
sql_length = len(view.sql_statement) if view.sql_statement else 0
|
48
|
+
table.add_row(view.name, str(sql_length), "-")
|
49
|
+
|
50
|
+
console.print(table)
|
51
|
+
|
52
|
+
|
53
|
+
@app.command()
|
54
|
+
def create(
|
55
|
+
ctx: typer.Context,
|
56
|
+
name: Optional[str] = typer.Argument(None, help="Name of the view"),
|
57
|
+
sql: Optional[str] = typer.Argument(None, help="SQL query for the view"),
|
58
|
+
apply: bool = typer.Option(
|
59
|
+
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
60
|
+
),
|
61
|
+
):
|
62
|
+
"""Create a new view.
|
63
|
+
|
64
|
+
Examples:
|
65
|
+
cinch view create active_users "SELECT * FROM users WHERE is_active = 1"
|
66
|
+
cinch view create user_stats "SELECT age, COUNT(*) as count FROM users GROUP BY age"
|
67
|
+
"""
|
68
|
+
name = validate_required_arg(name, "name", ctx)
|
69
|
+
sql = validate_required_arg(sql, "sql", ctx)
|
70
|
+
config, config_data = get_config_with_data()
|
71
|
+
db_name = config_data.active_database
|
72
|
+
branch_name = config_data.active_branch
|
73
|
+
|
74
|
+
try:
|
75
|
+
view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
|
76
|
+
view_mgr.create_view(name, sql)
|
77
|
+
console.print(f"[green]✅ Created view '{name}'[/green]")
|
78
|
+
|
79
|
+
if apply:
|
80
|
+
# Apply to all tenants
|
81
|
+
applier = ChangeApplier(config.project_dir, db_name, branch_name)
|
82
|
+
applied = applier.apply_all_unapplied()
|
83
|
+
if applied > 0:
|
84
|
+
console.print("[green]✅ Applied changes to all tenants[/green]")
|
85
|
+
|
86
|
+
except ValueError as e:
|
87
|
+
console.print(f"[red]❌ {e}[/red]")
|
88
|
+
raise typer.Exit(1)
|
89
|
+
|
90
|
+
|
91
|
+
@app.command()
|
92
|
+
def update(
|
93
|
+
ctx: typer.Context,
|
94
|
+
name: Optional[str] = typer.Argument(None, help="Name of the view to update"),
|
95
|
+
sql: Optional[str] = typer.Argument(None, help="New SQL query for the view"),
|
96
|
+
apply: bool = typer.Option(
|
97
|
+
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
98
|
+
),
|
99
|
+
):
|
100
|
+
"""Update an existing view's SQL."""
|
101
|
+
name = validate_required_arg(name, "name", ctx)
|
102
|
+
sql = validate_required_arg(sql, "sql", ctx)
|
103
|
+
config, config_data = get_config_with_data()
|
104
|
+
db_name = config_data.active_database
|
105
|
+
branch_name = config_data.active_branch
|
106
|
+
|
107
|
+
try:
|
108
|
+
view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
|
109
|
+
view_mgr.update_view(name, sql)
|
110
|
+
console.print(f"[green]✅ Updated view '{name}'[/green]")
|
111
|
+
|
112
|
+
if apply:
|
113
|
+
# Apply to all tenants
|
114
|
+
applier = ChangeApplier(config.project_dir, db_name, branch_name)
|
115
|
+
applied = applier.apply_all_unapplied()
|
116
|
+
if applied > 0:
|
117
|
+
console.print("[green]✅ Applied changes to all tenants[/green]")
|
118
|
+
|
119
|
+
except ValueError as e:
|
120
|
+
console.print(f"[red]❌ {e}[/red]")
|
121
|
+
raise typer.Exit(1)
|
122
|
+
|
123
|
+
|
124
|
+
@app.command()
|
125
|
+
def delete(
|
126
|
+
ctx: typer.Context,
|
127
|
+
name: Optional[str] = typer.Argument(None, help="Name of the view to delete"),
|
128
|
+
force: bool = typer.Option(
|
129
|
+
False, "--force", "-f", help="Force deletion without confirmation"
|
130
|
+
),
|
131
|
+
apply: bool = typer.Option(
|
132
|
+
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
133
|
+
),
|
134
|
+
):
|
135
|
+
"""Delete a view."""
|
136
|
+
name = validate_required_arg(name, "name", ctx)
|
137
|
+
config, config_data = get_config_with_data()
|
138
|
+
db_name = config_data.active_database
|
139
|
+
branch_name = config_data.active_branch
|
140
|
+
|
141
|
+
# Confirmation
|
142
|
+
if not force:
|
143
|
+
confirm = typer.confirm(f"Are you sure you want to delete view '{name}'?")
|
144
|
+
if not confirm:
|
145
|
+
console.print("[yellow]Cancelled[/yellow]")
|
146
|
+
raise typer.Exit(0)
|
147
|
+
|
148
|
+
try:
|
149
|
+
view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
|
150
|
+
view_mgr.delete_view(name)
|
151
|
+
console.print(f"[green]✅ Deleted view '{name}'[/green]")
|
152
|
+
|
153
|
+
if apply:
|
154
|
+
# Apply to all tenants
|
155
|
+
applier = ChangeApplier(config.project_dir, db_name, branch_name)
|
156
|
+
applied = applier.apply_all_unapplied()
|
157
|
+
if applied > 0:
|
158
|
+
console.print("[green]✅ Applied changes to all tenants[/green]")
|
159
|
+
|
160
|
+
except ValueError as e:
|
161
|
+
console.print(f"[red]❌ {e}[/red]")
|
162
|
+
raise typer.Exit(1)
|
163
|
+
|
164
|
+
|
165
|
+
@app.command()
|
166
|
+
def info(
|
167
|
+
ctx: typer.Context, name: Optional[str] = typer.Argument(None, help="View name")
|
168
|
+
):
|
169
|
+
"""Show detailed information about a view."""
|
170
|
+
name = validate_required_arg(name, "name", ctx)
|
171
|
+
config, config_data = get_config_with_data()
|
172
|
+
db_name = config_data.active_database
|
173
|
+
branch_name = config_data.active_branch
|
174
|
+
|
175
|
+
try:
|
176
|
+
view_mgr = ViewModel(config.project_dir, db_name, branch_name, "main")
|
177
|
+
view = view_mgr.get_view(name)
|
178
|
+
|
179
|
+
# Display info
|
180
|
+
console.print(f"\n[bold]View: {view.name}[/bold]")
|
181
|
+
console.print(f"Database: {db_name}")
|
182
|
+
console.print(f"Branch: {branch_name}")
|
183
|
+
console.print("Tenant: main")
|
184
|
+
console.print("\n[bold]SQL:[/bold]")
|
185
|
+
console.print(view.sql_statement)
|
186
|
+
|
187
|
+
except ValueError as e:
|
188
|
+
console.print(f"[red]❌ {e}[/red]")
|
189
|
+
raise typer.Exit(1)
|
@@ -0,0 +1,189 @@
|
|
1
|
+
"""Unified codegen handler for local and remote operations."""
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Dict, Any, Optional, List
|
6
|
+
from rich.console import Console
|
7
|
+
|
8
|
+
from ...managers import CodegenManager
|
9
|
+
|
10
|
+
console = Console()
|
11
|
+
|
12
|
+
|
13
|
+
class CodegenHandler:
|
14
|
+
"""Handles both local and remote code generation operations."""
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
config_data: Dict[str, Any],
|
19
|
+
api_url: Optional[str] = None,
|
20
|
+
api_key: Optional[str] = None,
|
21
|
+
force_local: bool = False,
|
22
|
+
):
|
23
|
+
"""Initialize the handler.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
config_data: Configuration data from config.toml
|
27
|
+
api_url: Optional API URL for remote generation
|
28
|
+
api_key: Optional API key for remote generation
|
29
|
+
force_local: Force local generation even if API configured
|
30
|
+
"""
|
31
|
+
self.config_data = config_data
|
32
|
+
self.force_local = force_local
|
33
|
+
|
34
|
+
# Determine if we should use remote API
|
35
|
+
self.api_url = api_url or config_data.get("api", {}).get("url")
|
36
|
+
self.api_key = api_key or config_data.get("api", {}).get("key")
|
37
|
+
self.is_remote = bool(self.api_url and self.api_key and not force_local)
|
38
|
+
|
39
|
+
def generate_models(
|
40
|
+
self,
|
41
|
+
language: str,
|
42
|
+
output_dir: Path,
|
43
|
+
database: str,
|
44
|
+
branch: str,
|
45
|
+
tenant: str = "main",
|
46
|
+
include_tables: bool = True,
|
47
|
+
include_views: bool = True,
|
48
|
+
project_root: Optional[Path] = None,
|
49
|
+
) -> Dict[str, Any]:
|
50
|
+
"""Generate models using local or remote approach.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Dict with generation results in consistent format
|
54
|
+
"""
|
55
|
+
if self.is_remote:
|
56
|
+
return self._generate_remote(
|
57
|
+
language=language,
|
58
|
+
output_dir=output_dir,
|
59
|
+
database=database,
|
60
|
+
branch=branch,
|
61
|
+
tenant=tenant,
|
62
|
+
include_tables=include_tables,
|
63
|
+
include_views=include_views,
|
64
|
+
)
|
65
|
+
else:
|
66
|
+
return self._generate_local(
|
67
|
+
language=language,
|
68
|
+
output_dir=output_dir,
|
69
|
+
database=database,
|
70
|
+
branch=branch,
|
71
|
+
tenant=tenant,
|
72
|
+
include_tables=include_tables,
|
73
|
+
include_views=include_views,
|
74
|
+
project_root=project_root,
|
75
|
+
)
|
76
|
+
|
77
|
+
def _generate_local(
|
78
|
+
self,
|
79
|
+
language: str,
|
80
|
+
output_dir: Path,
|
81
|
+
database: str,
|
82
|
+
branch: str,
|
83
|
+
tenant: str = "main",
|
84
|
+
include_tables: bool = True,
|
85
|
+
include_views: bool = True,
|
86
|
+
project_root: Optional[Path] = None,
|
87
|
+
) -> Dict[str, Any]:
|
88
|
+
"""Generate models locally using CodegenManager."""
|
89
|
+
if not project_root:
|
90
|
+
raise ValueError("project_root is required for local generation")
|
91
|
+
|
92
|
+
manager = CodegenManager(
|
93
|
+
project_root=project_root, database=database, branch=branch, tenant=tenant
|
94
|
+
)
|
95
|
+
|
96
|
+
return manager.generate_models(
|
97
|
+
language=language,
|
98
|
+
output_dir=output_dir,
|
99
|
+
include_tables=include_tables,
|
100
|
+
include_views=include_views,
|
101
|
+
)
|
102
|
+
|
103
|
+
def _generate_remote(
|
104
|
+
self,
|
105
|
+
language: str,
|
106
|
+
output_dir: Path,
|
107
|
+
database: str,
|
108
|
+
branch: str,
|
109
|
+
tenant: str = "main",
|
110
|
+
include_tables: bool = True,
|
111
|
+
include_views: bool = True,
|
112
|
+
) -> Dict[str, Any]:
|
113
|
+
"""Generate models remotely using API."""
|
114
|
+
try:
|
115
|
+
# Prepare request payload
|
116
|
+
payload = {
|
117
|
+
"language": language,
|
118
|
+
"include_tables": include_tables,
|
119
|
+
"include_views": include_views,
|
120
|
+
}
|
121
|
+
|
122
|
+
# Prepare query parameters - database and branch required, tenant not needed for codegen
|
123
|
+
params = {"database": database, "branch": branch}
|
124
|
+
|
125
|
+
# Make API request to generate files endpoint (returns JSON content)
|
126
|
+
response = requests.post(
|
127
|
+
f"{self.api_url}/api/v1/codegen/generate/files",
|
128
|
+
json=payload,
|
129
|
+
params=params,
|
130
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
131
|
+
)
|
132
|
+
response.raise_for_status()
|
133
|
+
|
134
|
+
# Parse response
|
135
|
+
result = response.json()
|
136
|
+
files_data = result["files"]
|
137
|
+
|
138
|
+
# Create output directory
|
139
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
140
|
+
|
141
|
+
# Write files to local filesystem
|
142
|
+
files_generated = []
|
143
|
+
for file_info in files_data:
|
144
|
+
file_path = output_dir / file_info["filename"]
|
145
|
+
file_path.write_text(file_info["content"])
|
146
|
+
files_generated.append(file_info["filename"])
|
147
|
+
|
148
|
+
# Return consistent format matching local generation
|
149
|
+
return {
|
150
|
+
"files_generated": files_generated,
|
151
|
+
"tables_processed": result.get("tables_processed", []),
|
152
|
+
"views_processed": result.get("views_processed", []),
|
153
|
+
"output_dir": str(output_dir),
|
154
|
+
"language": language,
|
155
|
+
"remote": True,
|
156
|
+
}
|
157
|
+
|
158
|
+
except requests.RequestException as e:
|
159
|
+
raise RuntimeError(f"Remote codegen failed: {e}")
|
160
|
+
except KeyError as e:
|
161
|
+
raise RuntimeError(f"Invalid API response format: missing {e}")
|
162
|
+
|
163
|
+
def get_supported_languages(self, project_root: Optional[Path] = None) -> List[str]:
|
164
|
+
"""Get supported languages from local or remote source."""
|
165
|
+
if self.is_remote:
|
166
|
+
try:
|
167
|
+
response = requests.get(
|
168
|
+
f"{self.api_url}/api/v1/codegen/languages",
|
169
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
170
|
+
)
|
171
|
+
response.raise_for_status()
|
172
|
+
result = response.json()
|
173
|
+
return [lang["name"] for lang in result["languages"]]
|
174
|
+
except requests.RequestException:
|
175
|
+
# Fall back to local if remote fails
|
176
|
+
pass
|
177
|
+
|
178
|
+
# Use local manager for supported languages
|
179
|
+
if not project_root:
|
180
|
+
# Return hardcoded list if no project available
|
181
|
+
return ["python"]
|
182
|
+
|
183
|
+
manager = CodegenManager(
|
184
|
+
project_root=project_root,
|
185
|
+
database=self.config_data.get("active_database", "main"),
|
186
|
+
branch=self.config_data.get("active_branch", "main"),
|
187
|
+
tenant="main",
|
188
|
+
)
|
189
|
+
return manager.get_supported_languages()
|
cinchdb/cli/main.py
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
"""Main CLI entry point for CinchDB."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing import Optional
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
# Import command groups
|
8
|
+
from cinchdb.cli.commands import (
|
9
|
+
database,
|
10
|
+
branch,
|
11
|
+
tenant,
|
12
|
+
table,
|
13
|
+
column,
|
14
|
+
view,
|
15
|
+
codegen,
|
16
|
+
remote,
|
17
|
+
)
|
18
|
+
|
19
|
+
app = typer.Typer(
|
20
|
+
name="cinch",
|
21
|
+
help="CinchDB - A Git-like SQLite database management system",
|
22
|
+
add_completion=False,
|
23
|
+
invoke_without_command=True,
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
@app.callback()
|
28
|
+
def main(ctx: typer.Context):
|
29
|
+
"""
|
30
|
+
CinchDB - A Git-like SQLite database management system
|
31
|
+
"""
|
32
|
+
if ctx.invoked_subcommand is None:
|
33
|
+
# No subcommand was invoked, show help
|
34
|
+
print(ctx.get_help())
|
35
|
+
raise typer.Exit(0)
|
36
|
+
|
37
|
+
|
38
|
+
# Add command groups
|
39
|
+
app.add_typer(database.app, name="db", help="Database management commands")
|
40
|
+
app.add_typer(branch.app, name="branch", help="Branch management commands")
|
41
|
+
app.add_typer(tenant.app, name="tenant", help="Tenant management commands")
|
42
|
+
app.add_typer(table.app, name="table", help="Table management commands")
|
43
|
+
app.add_typer(column.app, name="column", help="Column management commands")
|
44
|
+
app.add_typer(view.app, name="view", help="View management commands")
|
45
|
+
app.add_typer(codegen.app, name="codegen", help="Code generation commands")
|
46
|
+
app.add_typer(remote.app, name="remote", help="Remote instance management")
|
47
|
+
|
48
|
+
|
49
|
+
# Add query as direct command instead of subtyper
|
50
|
+
@app.command()
|
51
|
+
def query(
|
52
|
+
sql: str = typer.Argument(..., help="SQL query to execute"),
|
53
|
+
tenant: Optional[str] = typer.Option("main", "--tenant", "-t", help="Tenant name"),
|
54
|
+
format: Optional[str] = typer.Option(
|
55
|
+
"table", "--format", "-f", help="Output format (table, json, csv)"
|
56
|
+
),
|
57
|
+
limit: Optional[int] = typer.Option(
|
58
|
+
None, "--limit", "-l", help="Limit number of rows"
|
59
|
+
),
|
60
|
+
local: bool = typer.Option(False, "--local", "-L", help="Force local connection"),
|
61
|
+
remote: Optional[str] = typer.Option(
|
62
|
+
None, "--remote", "-r", help="Use specific remote alias"
|
63
|
+
),
|
64
|
+
):
|
65
|
+
"""Execute a SQL query."""
|
66
|
+
from cinchdb.cli.commands.query import execute_query
|
67
|
+
|
68
|
+
execute_query(sql, tenant, format, limit, force_local=local, remote_alias=remote)
|
69
|
+
|
70
|
+
|
71
|
+
@app.command()
|
72
|
+
def init(
|
73
|
+
path: Optional[Path] = typer.Argument(
|
74
|
+
None, help="Directory to initialize project in (default: current directory)"
|
75
|
+
),
|
76
|
+
):
|
77
|
+
"""Initialize a new CinchDB project."""
|
78
|
+
from cinchdb.config import Config
|
79
|
+
|
80
|
+
project_path = path or Path.cwd()
|
81
|
+
|
82
|
+
try:
|
83
|
+
config = Config(project_path)
|
84
|
+
config.init_project()
|
85
|
+
typer.secho(
|
86
|
+
f"✅ Initialized CinchDB project in {project_path}", fg=typer.colors.GREEN
|
87
|
+
)
|
88
|
+
except FileExistsError:
|
89
|
+
typer.secho(f"❌ Project already exists in {project_path}", fg=typer.colors.RED)
|
90
|
+
raise typer.Exit(1)
|
91
|
+
|
92
|
+
|
93
|
+
@app.command()
|
94
|
+
def version():
|
95
|
+
"""Show CinchDB version."""
|
96
|
+
from cinchdb import __version__
|
97
|
+
|
98
|
+
typer.echo(f"CinchDB version {__version__}")
|
99
|
+
|
100
|
+
|
101
|
+
@app.command()
|
102
|
+
def status():
|
103
|
+
"""Show CinchDB status including configuration and environment variables."""
|
104
|
+
from cinchdb.cli.utils import get_config_with_data, show_env_config
|
105
|
+
from rich.console import Console
|
106
|
+
from rich.table import Table as RichTable
|
107
|
+
|
108
|
+
console = Console()
|
109
|
+
|
110
|
+
# Show project configuration
|
111
|
+
try:
|
112
|
+
config, config_data = get_config_with_data()
|
113
|
+
|
114
|
+
console.print("\n[bold]CinchDB Status[/bold]")
|
115
|
+
console.print(f"Project: {config.project_dir}")
|
116
|
+
console.print(f"Active Database: {config_data.active_database}")
|
117
|
+
console.print(f"Active Branch: {config_data.active_branch}")
|
118
|
+
|
119
|
+
if config_data.active_remote:
|
120
|
+
console.print(f"Active Remote: {config_data.active_remote}")
|
121
|
+
if config_data.active_remote in config_data.remotes:
|
122
|
+
remote = config_data.remotes[config_data.active_remote]
|
123
|
+
console.print(f" URL: {remote.url}")
|
124
|
+
console.print(f" Key: ***{remote.key[-8:] if len(remote.key) > 8 else '*' * len(remote.key)}")
|
125
|
+
else:
|
126
|
+
console.print("Active Remote: [dim]None (local mode)[/dim]")
|
127
|
+
|
128
|
+
# Show environment variables
|
129
|
+
show_env_config()
|
130
|
+
|
131
|
+
except Exception as e:
|
132
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
133
|
+
raise typer.Exit(1)
|
134
|
+
|
135
|
+
|
136
|
+
if __name__ == "__main__":
|
137
|
+
app()
|
cinchdb/cli/utils.py
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
"""Utility functions for CLI commands."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import typer
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Optional
|
7
|
+
from rich.console import Console
|
8
|
+
|
9
|
+
from cinchdb.config import Config
|
10
|
+
from cinchdb.core.path_utils import get_project_root
|
11
|
+
from cinchdb.core.database import CinchDB
|
12
|
+
|
13
|
+
console = Console()
|
14
|
+
|
15
|
+
|
16
|
+
def get_config_with_data():
|
17
|
+
"""Get config and load data from current directory.
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
tuple: (config, config_data)
|
21
|
+
"""
|
22
|
+
project_root = get_project_root(Path.cwd())
|
23
|
+
if not project_root:
|
24
|
+
console.print("[red]❌ Not in a CinchDB project directory[/red]")
|
25
|
+
raise typer.Exit(1)
|
26
|
+
|
27
|
+
config = Config(project_root)
|
28
|
+
try:
|
29
|
+
config_data = config.load()
|
30
|
+
except FileNotFoundError:
|
31
|
+
console.print("[red]❌ Config file not found. Run 'cinch init' first.[/red]")
|
32
|
+
raise typer.Exit(1)
|
33
|
+
|
34
|
+
return config, config_data
|
35
|
+
|
36
|
+
|
37
|
+
def get_config_dict():
|
38
|
+
"""Get config data as dictionary, including API configuration if present.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
dict: Configuration data
|
42
|
+
"""
|
43
|
+
config, config_data = get_config_with_data()
|
44
|
+
|
45
|
+
# Convert config_data to dict-like structure for handlers
|
46
|
+
config_dict = {
|
47
|
+
"active_database": getattr(config_data, "active_database", None),
|
48
|
+
"active_branch": getattr(config_data, "active_branch", "main"),
|
49
|
+
"project_root": config.project_dir,
|
50
|
+
}
|
51
|
+
|
52
|
+
# Add API configuration if present in raw config
|
53
|
+
if hasattr(config_data, "api") and config_data.api:
|
54
|
+
config_dict["api"] = {
|
55
|
+
"url": getattr(config_data.api, "url", None),
|
56
|
+
"key": getattr(config_data.api, "key", None),
|
57
|
+
}
|
58
|
+
|
59
|
+
return config_dict
|
60
|
+
|
61
|
+
|
62
|
+
def set_active_database(config: Config, database: str):
|
63
|
+
"""Set the active database in config."""
|
64
|
+
config_data = config.load()
|
65
|
+
config_data.active_database = database
|
66
|
+
config.save(config_data)
|
67
|
+
|
68
|
+
|
69
|
+
def set_active_branch(config: Config, branch: str):
|
70
|
+
"""Set the active branch in config."""
|
71
|
+
config_data = config.load()
|
72
|
+
config_data.active_branch = branch
|
73
|
+
config.save(config_data)
|
74
|
+
|
75
|
+
|
76
|
+
def validate_required_arg(
|
77
|
+
value: Optional[str], arg_name: str, ctx: typer.Context
|
78
|
+
) -> str:
|
79
|
+
"""Validate a required argument and show help if missing.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
value: The argument value
|
83
|
+
arg_name: Name of the argument (for error message)
|
84
|
+
ctx: Typer context
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
The validated value
|
88
|
+
|
89
|
+
Raises:
|
90
|
+
typer.Exit: If value is None
|
91
|
+
"""
|
92
|
+
if value is None:
|
93
|
+
console.print(ctx.get_help())
|
94
|
+
console.print(f"\n[red]❌ Error: Missing argument '{arg_name.upper()}'.[/red]")
|
95
|
+
raise typer.Exit(1)
|
96
|
+
return value
|
97
|
+
|
98
|
+
|
99
|
+
def get_cinchdb_instance(
|
100
|
+
database: Optional[str] = None,
|
101
|
+
branch: Optional[str] = None,
|
102
|
+
tenant: str = "main",
|
103
|
+
force_local: bool = False,
|
104
|
+
remote_alias: Optional[str] = None,
|
105
|
+
) -> CinchDB:
|
106
|
+
"""Get a CinchDB instance configured for local or remote access.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
database: Database name (uses active database if None)
|
110
|
+
branch: Branch name (uses active branch if None)
|
111
|
+
tenant: Tenant name (default: main)
|
112
|
+
force_local: Force local connection even if remote is configured
|
113
|
+
remote_alias: Use specific remote alias (overrides active remote)
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
CinchDB instance
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
typer.Exit: If configuration is invalid
|
120
|
+
"""
|
121
|
+
config, config_data = get_config_with_data()
|
122
|
+
|
123
|
+
# Use provided or active database/branch
|
124
|
+
database = database or config_data.active_database
|
125
|
+
branch = branch or config_data.active_branch
|
126
|
+
|
127
|
+
# Determine if we should use remote connection
|
128
|
+
use_remote = False
|
129
|
+
remote_config = None
|
130
|
+
|
131
|
+
if not force_local:
|
132
|
+
if remote_alias:
|
133
|
+
# Use specific remote alias
|
134
|
+
if remote_alias not in config_data.remotes:
|
135
|
+
console.print(f"[red]❌ Remote '{remote_alias}' not found[/red]")
|
136
|
+
raise typer.Exit(1)
|
137
|
+
remote_config = config_data.remotes[remote_alias]
|
138
|
+
use_remote = True
|
139
|
+
elif config_data.active_remote:
|
140
|
+
# Use active remote
|
141
|
+
if config_data.active_remote not in config_data.remotes:
|
142
|
+
console.print(f"[red]❌ Active remote '{config_data.active_remote}' not found[/red]")
|
143
|
+
raise typer.Exit(1)
|
144
|
+
remote_config = config_data.remotes[config_data.active_remote]
|
145
|
+
use_remote = True
|
146
|
+
|
147
|
+
if use_remote and remote_config:
|
148
|
+
# Create remote connection
|
149
|
+
return CinchDB(
|
150
|
+
database=database,
|
151
|
+
branch=branch,
|
152
|
+
tenant=tenant,
|
153
|
+
api_url=remote_config.url,
|
154
|
+
api_key=remote_config.key,
|
155
|
+
)
|
156
|
+
else:
|
157
|
+
# Create local connection
|
158
|
+
return CinchDB(
|
159
|
+
database=database,
|
160
|
+
branch=branch,
|
161
|
+
tenant=tenant,
|
162
|
+
project_dir=config.project_dir,
|
163
|
+
)
|
164
|
+
|
165
|
+
|
166
|
+
def show_env_config():
|
167
|
+
"""Display active environment variable configuration."""
|
168
|
+
env_vars = {
|
169
|
+
"CINCHDB_PROJECT_DIR": os.environ.get("CINCHDB_PROJECT_DIR"),
|
170
|
+
"CINCHDB_DATABASE": os.environ.get("CINCHDB_DATABASE"),
|
171
|
+
"CINCHDB_BRANCH": os.environ.get("CINCHDB_BRANCH"),
|
172
|
+
"CINCHDB_REMOTE_URL": os.environ.get("CINCHDB_REMOTE_URL"),
|
173
|
+
"CINCHDB_API_KEY": "***" if "CINCHDB_API_KEY" in os.environ else None,
|
174
|
+
}
|
175
|
+
|
176
|
+
active = {k: v for k, v in env_vars.items() if v}
|
177
|
+
if active:
|
178
|
+
console.print("\n[yellow]Active environment variables:[/yellow]")
|
179
|
+
for key, value in active.items():
|
180
|
+
console.print(f" {key}={value}")
|
181
|
+
else:
|
182
|
+
console.print("\n[dim]No CinchDB environment variables set[/dim]")
|