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,368 @@
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))
@@ -0,0 +1,164 @@
1
+ """Code generation router for CinchDB API."""
2
+
3
+ import tempfile
4
+ from typing import List, Dict
5
+ from pathlib import Path
6
+ from fastapi import APIRouter, Depends, HTTPException, Query
7
+ from pydantic import BaseModel
8
+
9
+ from cinchdb.core.database import CinchDB
10
+ from cinchdb.api.auth import AuthContext, require_read_permission
11
+
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ class CodegenLanguage(BaseModel):
17
+ """Supported language for code generation."""
18
+
19
+ name: str
20
+ description: str
21
+
22
+
23
+ class GenerateModelsRequest(BaseModel):
24
+ """Request to generate models."""
25
+
26
+ language: str
27
+ include_tables: bool = True
28
+ include_views: bool = True
29
+
30
+
31
+ @router.get("/languages", response_model=List[CodegenLanguage])
32
+ async def list_supported_languages():
33
+ """List supported code generation languages."""
34
+ # Using a temporary CinchDB to get supported languages
35
+ # This doesn't require specific database/branch so we use dummy values
36
+ temp_project = Path(tempfile.mkdtemp())
37
+ try:
38
+ db = CinchDB(
39
+ database="dummy", branch="dummy", tenant="dummy", project_dir=temp_project
40
+ )
41
+ languages = db.codegen.get_supported_languages()
42
+
43
+ # Map languages to descriptions
44
+ language_info = {
45
+ "python": "Python Pydantic models with full CRUD operations",
46
+ "typescript": "TypeScript interfaces and classes (planned)",
47
+ }
48
+
49
+ return [
50
+ CodegenLanguage(
51
+ name=lang, description=language_info.get(lang, f"{lang.title()} models")
52
+ )
53
+ for lang in languages
54
+ ]
55
+ finally:
56
+ # Clean up temp directory
57
+ import shutil
58
+
59
+ shutil.rmtree(temp_project, ignore_errors=True)
60
+
61
+
62
+ @router.post("/generate/files", response_model=Dict[str, str])
63
+ async def generate_model_files_content(
64
+ request: GenerateModelsRequest,
65
+ database: str = Query(..., description="Database name"),
66
+ branch: str = Query(..., description="Branch name"),
67
+ auth: AuthContext = Depends(require_read_permission),
68
+ ):
69
+ """Generate model files and return their content as JSON (alternative to ZIP download)."""
70
+ db_name = database
71
+ branch_name = branch
72
+
73
+ # Check branch permissions
74
+ await require_read_permission(auth, branch_name)
75
+
76
+ try:
77
+ # Create temporary directory for generation
78
+ temp_dir = Path(tempfile.mkdtemp())
79
+ output_dir = temp_dir / "generated_models"
80
+
81
+ # Initialize CinchDB and get codegen manager
82
+ db = CinchDB(
83
+ database=db_name,
84
+ branch=branch_name,
85
+ tenant="main",
86
+ project_dir=auth.project_dir,
87
+ )
88
+ codegen_mgr = db.codegen
89
+
90
+ # Generate models
91
+ results = codegen_mgr.generate_models(
92
+ language=request.language,
93
+ output_dir=output_dir,
94
+ include_tables=request.include_tables,
95
+ include_views=request.include_views,
96
+ )
97
+
98
+ # Read generated files and return their content
99
+ file_contents = {}
100
+
101
+ for file_name in results["files_generated"]:
102
+ file_path = output_dir / file_name
103
+ if file_path.exists():
104
+ with open(file_path, "r", encoding="utf-8") as f:
105
+ file_contents[file_name] = f.read()
106
+
107
+ # Clean up temp directory
108
+ import shutil
109
+
110
+ shutil.rmtree(temp_dir, ignore_errors=True)
111
+
112
+ return file_contents
113
+
114
+ except ValueError as e:
115
+ raise HTTPException(status_code=400, detail=str(e))
116
+ except Exception as e:
117
+ raise HTTPException(status_code=500, detail=f"Code generation failed: {str(e)}")
118
+
119
+
120
+ @router.get("/info")
121
+ async def get_codegen_info(
122
+ database: str = Query(..., description="Database name"),
123
+ branch: str = Query(..., description="Branch name"),
124
+ auth: AuthContext = Depends(require_read_permission),
125
+ ):
126
+ """Get information about what can be generated for the current database/branch."""
127
+ db_name = database
128
+ branch_name = branch
129
+
130
+ # Check branch permissions
131
+ await require_read_permission(auth, branch_name)
132
+
133
+ try:
134
+ # Initialize CinchDB and get codegen manager
135
+ db = CinchDB(
136
+ database=db_name,
137
+ branch=branch_name,
138
+ tenant="main",
139
+ project_dir=auth.project_dir,
140
+ )
141
+ codegen_mgr = db.codegen
142
+
143
+ # Get available tables and views using CinchDB
144
+ db = CinchDB(
145
+ database=db_name,
146
+ branch=branch_name,
147
+ tenant="main",
148
+ project_dir=auth.project_dir,
149
+ )
150
+ tables = db.tables.list_tables()
151
+ views = db.views.list_views()
152
+
153
+ return {
154
+ "database": db_name,
155
+ "branch": branch_name,
156
+ "tenant": "main",
157
+ "supported_languages": codegen_mgr.get_supported_languages(),
158
+ "available_tables": [table.name for table in tables],
159
+ "available_views": [view.name for view in views],
160
+ "total_entities": len(tables) + len(views),
161
+ }
162
+
163
+ except ValueError as e:
164
+ raise HTTPException(status_code=404, detail=str(e))