cinchdb 0.1.1__py3-none-any.whl → 0.1.3__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/api/main.py DELETED
@@ -1,137 +0,0 @@
1
- """Main entry point for CinchDB API server."""
2
-
3
- import uvicorn
4
- from pathlib import Path
5
- from typing import Optional
6
-
7
- import typer
8
- from rich.console import Console
9
-
10
- from cinchdb.api.auth import APIKeyManager
11
- from cinchdb.core.path_utils import get_project_root
12
-
13
-
14
- cli = typer.Typer(
15
- name="cinch-server",
16
- help="CinchDB API server",
17
- add_completion=False,
18
- )
19
- console = Console()
20
-
21
-
22
- @cli.command()
23
- def serve(
24
- host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
25
- port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
26
- reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload"),
27
- project_dir: Optional[Path] = typer.Option(
28
- None, "--project-dir", "-d", help="Project directory"
29
- ),
30
- create_key: bool = typer.Option(
31
- False, "--create-key", help="Create an API key on startup"
32
- ),
33
- ):
34
- """Start the CinchDB API server."""
35
- # Find project directory
36
- if project_dir:
37
- project_path = Path(project_dir)
38
- else:
39
- project_path = get_project_root(Path.cwd())
40
-
41
- if not project_path or not (project_path / ".cinchdb").exists():
42
- console.print("[red]❌ No CinchDB project found[/red]")
43
- console.print("[yellow]Run 'cinch init' to create a project[/yellow]")
44
- raise typer.Exit(1)
45
-
46
- console.print("[green]Starting CinchDB API server[/green]")
47
- console.print(f"Project: {project_path}")
48
- console.print(f"Host: {host}:{port}")
49
-
50
- # Create API key if requested
51
- if create_key:
52
- manager = APIKeyManager(project_path)
53
- api_key = manager.create_key("Initial API Key", permissions="write")
54
- console.print("\n[bold green]Created API key:[/bold green]")
55
- console.print(f"[yellow]Key: {api_key.key}[/yellow]")
56
- console.print(f"[yellow]Permissions: {api_key.permissions}[/yellow]")
57
- console.print("\n[bold]Save this key - it won't be shown again![/bold]\n")
58
-
59
- # Start server
60
- uvicorn.run(
61
- "cinchdb.api.app:app", host=host, port=port, reload=reload, log_level="info"
62
- )
63
-
64
-
65
- @cli.command()
66
- def create_key(
67
- name: str = typer.Argument(..., help="Name for the API key"),
68
- permissions: str = typer.Option(
69
- "read", "--permissions", "-p", help="Permissions (read/write)"
70
- ),
71
- project_dir: Optional[Path] = typer.Option(
72
- None, "--project-dir", "-d", help="Project directory"
73
- ),
74
- ):
75
- """Create a new API key."""
76
- # Find project directory
77
- if project_dir:
78
- project_path = Path(project_dir)
79
- else:
80
- project_path = get_project_root(Path.cwd())
81
-
82
- if not project_path or not (project_path / ".cinchdb").exists():
83
- console.print("[red]❌ No CinchDB project found[/red]")
84
- raise typer.Exit(1)
85
-
86
- if permissions not in ["read", "write"]:
87
- console.print("[red]❌ Invalid permissions. Must be 'read' or 'write'[/red]")
88
- raise typer.Exit(1)
89
-
90
- manager = APIKeyManager(project_path)
91
- api_key = manager.create_key(name, permissions=permissions)
92
-
93
- console.print(f"[green]✅ Created API key '{name}'[/green]")
94
- console.print(f"[yellow]Key: {api_key.key}[/yellow]")
95
- console.print(f"[yellow]Permissions: {api_key.permissions}[/yellow]")
96
- console.print("\n[bold]Save this key - it won't be shown again![/bold]")
97
-
98
-
99
- @cli.command()
100
- def list_keys(
101
- project_dir: Optional[Path] = typer.Option(
102
- None, "--project-dir", "-d", help="Project directory"
103
- ),
104
- ):
105
- """List all API keys."""
106
- # Find project directory
107
- if project_dir:
108
- project_path = Path(project_dir)
109
- else:
110
- project_path = get_project_root(Path.cwd())
111
-
112
- if not project_path or not (project_path / ".cinchdb").exists():
113
- console.print("[red]❌ No CinchDB project found[/red]")
114
- raise typer.Exit(1)
115
-
116
- manager = APIKeyManager(project_path)
117
- keys = manager.list_keys()
118
-
119
- if not keys:
120
- console.print("[yellow]No API keys found[/yellow]")
121
- return
122
-
123
- console.print("\n[bold]API Keys:[/bold]")
124
- for key in keys:
125
- status = "[green]Active[/green]" if key.active else "[red]Revoked[/red]"
126
- branches = (
127
- f"Branches: {', '.join(key.branches)}" if key.branches else "All branches"
128
- )
129
- console.print(f"\n{key.name} - {status}")
130
- console.print(f" Key: {key.key}")
131
- console.print(f" Permissions: {key.permissions}")
132
- console.print(f" {branches}")
133
- console.print(f" Created: {key.created_at}")
134
-
135
-
136
- if __name__ == "__main__":
137
- cli()
@@ -1,25 +0,0 @@
1
- """API routers package."""
2
-
3
- from cinchdb.api.routers import (
4
- auth,
5
- projects,
6
- databases,
7
- branches,
8
- tenants,
9
- tables,
10
- columns,
11
- views,
12
- query,
13
- )
14
-
15
- __all__ = [
16
- "auth",
17
- "projects",
18
- "databases",
19
- "branches",
20
- "tenants",
21
- "tables",
22
- "columns",
23
- "views",
24
- "query",
25
- ]
@@ -1,135 +0,0 @@
1
- """Authentication router for CinchDB API."""
2
-
3
- from typing import List
4
- from fastapi import APIRouter, Depends, HTTPException
5
- from pydantic import BaseModel
6
-
7
- from cinchdb.api.auth import (
8
- APIKeyManager,
9
- AuthContext,
10
- verify_api_key,
11
- get_current_project,
12
- )
13
- from pathlib import Path
14
-
15
-
16
- router = APIRouter()
17
-
18
-
19
- class CreateAPIKeyRequest(BaseModel):
20
- """Request model for creating API key."""
21
-
22
- name: str
23
- permissions: str = "read" # "read" or "write"
24
- branches: List[str] = None
25
-
26
-
27
- class APIKeyResponse(BaseModel):
28
- """Response model for API key."""
29
-
30
- key: str
31
- name: str
32
- created_at: str
33
- permissions: str
34
- branches: List[str] = None
35
- active: bool
36
-
37
-
38
- @router.post("/keys", response_model=APIKeyResponse)
39
- async def create_api_key(
40
- request: CreateAPIKeyRequest,
41
- project_dir: Path = Depends(get_current_project),
42
- auth: AuthContext = Depends(verify_api_key),
43
- ):
44
- """Create a new API key (requires existing API key with write permission)."""
45
- # Only write permission can create new keys
46
- if auth.api_key.permissions != "write":
47
- raise HTTPException(
48
- status_code=403, detail="Write permission required to create API keys"
49
- )
50
-
51
- manager = APIKeyManager(project_dir)
52
-
53
- # Validate permissions
54
- if request.permissions not in ["read", "write"]:
55
- raise HTTPException(
56
- status_code=400, detail="Invalid permissions. Must be 'read' or 'write'"
57
- )
58
-
59
- api_key = manager.create_key(
60
- name=request.name, permissions=request.permissions, branches=request.branches
61
- )
62
-
63
- return APIKeyResponse(
64
- key=api_key.key,
65
- name=api_key.name,
66
- created_at=api_key.created_at.isoformat(),
67
- permissions=api_key.permissions,
68
- branches=api_key.branches,
69
- active=api_key.active,
70
- )
71
-
72
-
73
- @router.get("/keys", response_model=List[APIKeyResponse])
74
- async def list_api_keys(
75
- project_dir: Path = Depends(get_current_project),
76
- auth: AuthContext = Depends(verify_api_key),
77
- ):
78
- """List all API keys (requires write permission)."""
79
- # Only write permission can list keys
80
- if auth.api_key.permissions != "write":
81
- raise HTTPException(
82
- status_code=403, detail="Write permission required to list API keys"
83
- )
84
-
85
- manager = APIKeyManager(project_dir)
86
- keys = manager.list_keys()
87
-
88
- return [
89
- APIKeyResponse(
90
- key=k.key,
91
- name=k.name,
92
- created_at=k.created_at.isoformat(),
93
- permissions=k.permissions,
94
- branches=k.branches,
95
- active=k.active,
96
- )
97
- for k in keys
98
- ]
99
-
100
-
101
- @router.delete("/keys/{key}")
102
- async def revoke_api_key(
103
- key: str,
104
- project_dir: Path = Depends(get_current_project),
105
- auth: AuthContext = Depends(verify_api_key),
106
- ):
107
- """Revoke an API key (requires write permission)."""
108
- # Only write permission can revoke keys
109
- if auth.api_key.permissions != "write":
110
- raise HTTPException(
111
- status_code=403, detail="Write permission required to revoke API keys"
112
- )
113
-
114
- # Cannot revoke your own key
115
- if auth.api_key.key == key:
116
- raise HTTPException(status_code=400, detail="Cannot revoke your own API key")
117
-
118
- manager = APIKeyManager(project_dir)
119
- if not manager.revoke_key(key):
120
- raise HTTPException(status_code=404, detail="API key not found")
121
-
122
- return {"message": "API key revoked"}
123
-
124
-
125
- @router.get("/me")
126
- async def get_current_key_info(auth: AuthContext = Depends(verify_api_key)):
127
- """Get information about the current API key."""
128
- return APIKeyResponse(
129
- key=auth.api_key.key,
130
- name=auth.api_key.name,
131
- created_at=auth.api_key.created_at.isoformat(),
132
- permissions=auth.api_key.permissions,
133
- branches=auth.api_key.branches,
134
- active=auth.api_key.active,
135
- )
@@ -1,368 +0,0 @@
1
- """Branches router for CinchDB API."""
2
-
3
- from typing import List, Optional, Dict, Any
4
- from fastapi import APIRouter, Depends, HTTPException, Query
5
- from pydantic import BaseModel
6
- from datetime import datetime
7
-
8
- from cinchdb.config import Config
9
- from cinchdb.core.database import CinchDB
10
- from cinchdb.managers.merge_manager import MergeError
11
- from cinchdb.managers.change_comparator import ChangeComparator
12
- from cinchdb.models import Change
13
- from cinchdb.api.auth import (
14
- AuthContext,
15
- require_write_permission,
16
- require_read_permission,
17
- )
18
-
19
-
20
- router = APIRouter()
21
-
22
-
23
- class BranchInfo(BaseModel):
24
- """Branch information."""
25
-
26
- name: str
27
- parent: Optional[str]
28
- created_at: datetime
29
- is_active: bool
30
- tenant_count: int
31
-
32
-
33
- class CreateBranchRequest(BaseModel):
34
- """Request to create a branch."""
35
-
36
- name: str
37
- source: str = "main"
38
-
39
-
40
- class MergeBranchRequest(BaseModel):
41
- """Request to merge branches."""
42
-
43
- source: str
44
- target: str
45
- force: bool = False
46
-
47
-
48
- class BranchComparisonResult(BaseModel):
49
- """Result of branch comparison."""
50
-
51
- source_branch: str
52
- target_branch: str
53
- source_only_changes: int
54
- target_only_changes: int
55
- common_ancestor: Optional[str]
56
- can_fast_forward: bool
57
-
58
-
59
- class MergeCheckResult(BaseModel):
60
- """Result of merge feasibility check."""
61
-
62
- can_merge: bool
63
- reason: Optional[str] = None
64
- merge_type: Optional[str] = None
65
- changes_to_merge: Optional[int] = None
66
- target_changes: Optional[int] = None
67
- conflicts: Optional[List[Dict[str, Any]]] = None
68
-
69
-
70
- @router.get("/", response_model=List[BranchInfo])
71
- async def list_branches(
72
- database: str = Query(..., description="Database name"),
73
- auth: AuthContext = Depends(require_read_permission),
74
- ):
75
- """List all branches in a database."""
76
- config = Config(auth.project_dir)
77
- config_data = config.load()
78
-
79
- db_name = database
80
-
81
- try:
82
- db = CinchDB(
83
- database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
84
- )
85
- branches = db.branches.list_branches()
86
-
87
- result = []
88
- for branch in branches:
89
- # Count tenants
90
- tenants_dir = (
91
- auth.project_dir
92
- / ".cinchdb"
93
- / "databases"
94
- / db_name
95
- / "branches"
96
- / branch.name
97
- / "tenants"
98
- )
99
- tenant_count = (
100
- len(list(tenants_dir.glob("*.db"))) if tenants_dir.exists() else 0
101
- )
102
-
103
- result.append(
104
- BranchInfo(
105
- name=branch.name,
106
- parent=branch.parent_branch,
107
- created_at=branch.metadata.get("created_at", "Unknown"),
108
- is_active=branch.name == config_data.active_branch
109
- and db_name == config_data.active_database,
110
- tenant_count=tenant_count,
111
- )
112
- )
113
-
114
- return result
115
-
116
- except ValueError as e:
117
- raise HTTPException(status_code=404, detail=str(e))
118
-
119
-
120
- @router.post("/")
121
- async def create_branch(
122
- request: CreateBranchRequest,
123
- database: str = Query(..., description="Database name"),
124
- auth: AuthContext = Depends(require_write_permission),
125
- ):
126
- """Create a new branch."""
127
- db_name = database
128
-
129
- # Check branch permissions
130
- if auth.api_key.branches and request.source not in auth.api_key.branches:
131
- raise HTTPException(
132
- status_code=403,
133
- detail=f"Access denied for source branch '{request.source}'",
134
- )
135
-
136
- if auth.api_key.branches and request.name not in auth.api_key.branches:
137
- raise HTTPException(
138
- status_code=403,
139
- detail=f"Cannot create branch '{request.name}' - not in allowed branches",
140
- )
141
-
142
- try:
143
- db = CinchDB(
144
- database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
145
- )
146
- branch = db.branches.create_branch(request.name, request.source)
147
-
148
- return {
149
- "message": f"Created branch '{request.name}' from '{request.source}'",
150
- "branch": {
151
- "name": branch.name,
152
- "parent": branch.parent_branch,
153
- "created_at": branch.metadata.get("created_at","Unknown"),
154
- },
155
- }
156
-
157
- except ValueError as e:
158
- raise HTTPException(status_code=400, detail=str(e))
159
-
160
-
161
- @router.delete("/{name}")
162
- async def delete_branch(
163
- name: str,
164
- database: str = Query(..., description="Database name"),
165
- auth: AuthContext = Depends(require_write_permission),
166
- ):
167
- """Delete a branch."""
168
- if name == "main":
169
- raise HTTPException(status_code=400, detail="Cannot delete the main branch")
170
-
171
- config = Config(auth.project_dir)
172
- config_data = config.load()
173
-
174
- db_name = database
175
-
176
- # Check branch permissions
177
- if auth.api_key.branches and name not in auth.api_key.branches:
178
- raise HTTPException(
179
- status_code=403, detail=f"Access denied for branch '{name}'"
180
- )
181
-
182
- try:
183
- db = CinchDB(
184
- database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
185
- )
186
- db.branches.delete_branch(name)
187
-
188
- # If this was the active branch, switch to main
189
- if config_data.active_branch == name and config_data.active_database == db_name:
190
- config_data.active_branch = "main"
191
- config.save(config_data)
192
-
193
- return {"message": f"Deleted branch '{name}'"}
194
-
195
- except ValueError as e:
196
- raise HTTPException(status_code=404, detail=str(e))
197
-
198
-
199
-
200
- @router.get("/{source}/compare/{target}", response_model=BranchComparisonResult)
201
- async def compare_branches(
202
- source: str,
203
- target: str,
204
- database: str = Query(..., description="Database name"),
205
- auth: AuthContext = Depends(require_read_permission),
206
- ):
207
- """Compare two branches to see their differences."""
208
- db_name = database
209
-
210
- # Check branch permissions
211
- await require_read_permission(auth, source)
212
- await require_read_permission(auth, target)
213
-
214
- try:
215
- comparator = ChangeComparator(auth.project_dir, db_name)
216
-
217
- # Get divergent changes
218
- source_only, target_only = comparator.get_divergent_changes(source, target)
219
-
220
- # Find common ancestor
221
- common_ancestor = comparator.find_common_ancestor(source, target)
222
-
223
- # Check if fast-forward merge is possible
224
- can_fast_forward = comparator.can_fast_forward_merge(source, target)
225
-
226
- return BranchComparisonResult(
227
- source_branch=source,
228
- target_branch=target,
229
- source_only_changes=len(source_only),
230
- target_only_changes=len(target_only),
231
- common_ancestor=common_ancestor,
232
- can_fast_forward=can_fast_forward,
233
- )
234
-
235
- except ValueError as e:
236
- raise HTTPException(status_code=404, detail=str(e))
237
-
238
-
239
- @router.get("/{source}/can-merge/{target}", response_model=MergeCheckResult)
240
- async def check_merge_feasibility(
241
- source: str,
242
- target: str,
243
- database: str = Query(..., description="Database name"),
244
- auth: AuthContext = Depends(require_read_permission),
245
- ):
246
- """Check if source branch can be merged into target branch."""
247
- db_name = database
248
-
249
- # Check branch permissions
250
- await require_read_permission(auth, source)
251
- await require_read_permission(auth, target)
252
-
253
- try:
254
- db = CinchDB(
255
- database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
256
- )
257
- result = db.merge.can_merge(source, target)
258
-
259
- return MergeCheckResult(**result)
260
-
261
- except ValueError as e:
262
- raise HTTPException(status_code=404, detail=str(e))
263
-
264
-
265
- @router.post("/{source}/merge/{target}")
266
- async def merge_branches(
267
- source: str,
268
- target: str,
269
- database: str = Query(..., description="Database name"),
270
- force: bool = Query(False, description="Force merge even with conflicts"),
271
- dry_run: bool = Query(False, description="Preview merge without executing"),
272
- auth: AuthContext = Depends(require_write_permission),
273
- ):
274
- """Merge source branch into target branch."""
275
- db_name = database
276
-
277
- # Check branch permissions for both branches
278
- await require_write_permission(auth, source)
279
- await require_write_permission(auth, target)
280
-
281
- try:
282
- db = CinchDB(
283
- database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
284
- )
285
-
286
- if target == "main":
287
- # Use the merge_into_main method for main branch protection
288
- result = db.merge.merge_into_main(source, force=force, dry_run=dry_run)
289
- else:
290
- # Use internal merge method for non-main branches
291
- result = db.merge._merge_branches_internal(
292
- source, target, force=force, dry_run=dry_run
293
- )
294
-
295
- return result
296
-
297
- except MergeError as e:
298
- raise HTTPException(status_code=409, detail=str(e))
299
- except ValueError as e:
300
- raise HTTPException(status_code=400, detail=str(e))
301
-
302
-
303
- @router.post("/{source}/merge-into-main")
304
- async def merge_into_main_branch(
305
- source: str,
306
- database: str = Query(..., description="Database name"),
307
- force: bool = Query(False, description="Force merge even with conflicts"),
308
- dry_run: bool = Query(False, description="Preview merge without executing"),
309
- auth: AuthContext = Depends(require_write_permission),
310
- ):
311
- """Merge source branch into the main branch with additional protections."""
312
- db_name = database
313
-
314
- # Check branch permissions
315
- await require_write_permission(auth, source)
316
- await require_write_permission(auth, "main")
317
-
318
- try:
319
- db = CinchDB(
320
- database=db_name, branch="main", tenant="main", project_dir=auth.project_dir
321
- )
322
- result = db.merge.merge_into_main(source, force=force, dry_run=dry_run)
323
-
324
- return result
325
-
326
- except MergeError as e:
327
- raise HTTPException(status_code=409, detail=str(e))
328
- except ValueError as e:
329
- raise HTTPException(status_code=400, detail=str(e))
330
-
331
-
332
-
333
- @router.get("/{branch}/changes", response_model=Dict[str, Any])
334
- async def list_branch_changes(
335
- branch: str,
336
- database: str = Query(..., description="Database name"),
337
- auth: AuthContext = Depends(require_read_permission),
338
- ):
339
- """List all changes in a branch."""
340
- db_name = database
341
-
342
- # Check branch permissions
343
- if auth.api_key.branches and branch not in auth.api_key.branches:
344
- raise HTTPException(
345
- status_code=403, detail=f"Access denied for branch '{branch}'"
346
- )
347
-
348
- try:
349
- from cinchdb.managers.change_tracker import ChangeTracker
350
-
351
- tracker = ChangeTracker(auth.project_dir, db_name, branch)
352
- changes = tracker.get_changes()
353
-
354
- # Convert Change objects to dicts for JSON serialization
355
- changes_data = []
356
- for change in changes:
357
- change_dict = change.model_dump()
358
- # Convert datetime to string for JSON serialization
359
- if change_dict.get("created_at") and hasattr(change_dict["created_at"], "isoformat"):
360
- change_dict["created_at"] = change_dict["created_at"].isoformat()
361
- if change_dict.get("updated_at") and hasattr(change_dict.get("updated_at"), "isoformat"):
362
- change_dict["updated_at"] = change_dict["updated_at"].isoformat()
363
- changes_data.append(change_dict)
364
-
365
- return {"changes": changes_data}
366
-
367
- except ValueError as e:
368
- raise HTTPException(status_code=404, detail=str(e))