cinchdb 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1,479 +0,0 @@
1
- """Data CRUD router for CinchDB API."""
2
-
3
- from typing import List, Dict, Any, Optional
4
- from fastapi import APIRouter, Depends, HTTPException, Query, Path
5
- from pydantic import BaseModel, Field
6
-
7
- from cinchdb.core.database import CinchDB
8
- from cinchdb.api.auth import (
9
- AuthContext,
10
- require_write_permission,
11
- require_read_permission,
12
- )
13
-
14
-
15
- router = APIRouter()
16
-
17
-
18
- class DataRecord(BaseModel):
19
- """Generic data record with dynamic fields."""
20
-
21
- data: Dict[str, Any] = Field(description="Record data as key-value pairs")
22
-
23
- class Config:
24
- extra = "allow"
25
-
26
-
27
- class CreateDataRequest(BaseModel):
28
- """Request to create a new data record."""
29
-
30
- data: Dict[str, Any] = Field(description="Record data as key-value pairs")
31
-
32
-
33
- class UpdateDataRequest(BaseModel):
34
- """Request to update an existing data record."""
35
-
36
- data: Dict[str, Any] = Field(description="Record data to update")
37
-
38
-
39
- class BulkCreateRequest(BaseModel):
40
- """Request to create multiple records."""
41
-
42
- records: List[Dict[str, Any]] = Field(description="List of records to create")
43
-
44
-
45
- class FilterParams(BaseModel):
46
- """Query parameters for filtering data."""
47
-
48
- limit: Optional[int] = Field(
49
- None, description="Maximum number of records to return"
50
- )
51
- offset: Optional[int] = Field(None, description="Number of records to skip")
52
- filters: Optional[Dict[str, Any]] = Field(None, description="Column filters")
53
-
54
-
55
- def parse_query_filters(
56
- limit: Optional[int] = Query(
57
- None, description="Maximum number of records to return"
58
- ),
59
- offset: Optional[int] = Query(None, description="Number of records to skip"),
60
- **query_params,
61
- ) -> Dict[str, Any]:
62
- """Parse query parameters into filters dictionary."""
63
- filters = {}
64
-
65
- # Remove pagination params and non-filter params
66
- excluded_params = {"limit", "offset", "database", "branch", "tenant"}
67
-
68
- for key, value in query_params.items():
69
- if key not in excluded_params and value is not None:
70
- filters[key] = value
71
-
72
- return {"limit": limit, "offset": offset, "filters": filters}
73
-
74
-
75
- # Helper function to create a generic Pydantic model for a table
76
- def create_table_model(table_name: str) -> type:
77
- """Create a generic Pydantic model for a table."""
78
- return type(
79
- f"Table_{table_name}",
80
- (BaseModel,),
81
- {
82
- "__annotations__": {"data": Dict[str, Any]},
83
- "Config": type(
84
- "Config",
85
- (),
86
- {"extra": "allow", "json_schema_extra": {"table_name": table_name}},
87
- ),
88
- },
89
- )
90
-
91
-
92
- @router.get("/{table_name}/data", response_model=List[Dict[str, Any]])
93
- async def list_table_data(
94
- table_name: str = Path(..., description="Table name"),
95
- database: str = Query(..., description="Database name"),
96
- branch: str = Query(..., description="Branch name"),
97
- tenant: str = Query(..., description="Tenant name"),
98
- limit: Optional[int] = Query(
99
- None, description="Maximum number of records to return"
100
- ),
101
- offset: Optional[int] = Query(None, description="Number of records to skip"),
102
- auth: AuthContext = Depends(require_read_permission),
103
- ):
104
- """List all records in a table with optional filtering and pagination."""
105
- db_name = database
106
- branch_name = branch
107
-
108
- # Check branch permissions
109
- await require_read_permission(auth, branch_name)
110
-
111
- try:
112
- # Create CinchDB instance
113
- db = CinchDB(
114
- database=db_name,
115
- branch=branch_name,
116
- tenant=tenant,
117
- project_dir=auth.project_dir,
118
- )
119
-
120
- # Verify table exists
121
- db.tables.get_table(
122
- table_name
123
- ) # This will raise ValueError if table doesn't exist
124
-
125
- # Create a generic model for the table
126
- table_model = create_table_model(table_name)
127
-
128
- # Parse filters from query parameters
129
- # For simplicity, we'll handle basic filters directly in the query
130
- # More complex filtering with operators would require parameter parsing
131
- filters = {}
132
-
133
- # Get records
134
- records = db.data.select(table_model, limit=limit, offset=offset, **filters)
135
-
136
- # Convert to dict format for response
137
- return [
138
- record.model_dump() if hasattr(record, "model_dump") else record.__dict__
139
- for record in records
140
- ]
141
-
142
- except ValueError as e:
143
- raise HTTPException(status_code=404, detail=str(e))
144
-
145
-
146
- @router.get("/{table_name}/data/{record_id}", response_model=Dict[str, Any])
147
- async def get_record_by_id(
148
- table_name: str = Path(..., description="Table name"),
149
- record_id: str = Path(..., description="Record ID"),
150
- database: str = Query(..., description="Database name"),
151
- branch: str = Query(..., description="Branch name"),
152
- tenant: str = Query(..., description="Tenant name"),
153
- auth: AuthContext = Depends(require_read_permission),
154
- ):
155
- """Get a specific record by ID."""
156
- db_name = database
157
- branch_name = branch
158
-
159
- # Check branch permissions
160
- await require_read_permission(auth, branch_name)
161
-
162
- try:
163
- # Create CinchDB instance
164
- db = CinchDB(
165
- database=db_name,
166
- branch=branch_name,
167
- tenant=tenant,
168
- project_dir=auth.project_dir,
169
- )
170
-
171
- # Verify table exists
172
- db.tables.get_table(table_name)
173
-
174
- table_model = create_table_model(table_name)
175
-
176
- record = db.data.find_by_id(table_model, record_id)
177
-
178
- if not record:
179
- raise HTTPException(
180
- status_code=404, detail=f"Record with ID {record_id} not found"
181
- )
182
-
183
- return record.model_dump() if hasattr(record, "model_dump") else record.__dict__
184
-
185
- except ValueError as e:
186
- raise HTTPException(status_code=404, detail=str(e))
187
-
188
-
189
- @router.post("/{table_name}/data", response_model=Dict[str, Any])
190
- async def create_record(
191
- request: CreateDataRequest,
192
- table_name: str = Path(..., description="Table name"),
193
- database: str = Query(..., description="Database name"),
194
- branch: str = Query(..., description="Branch name"),
195
- tenant: str = Query(..., description="Tenant name"),
196
- auth: AuthContext = Depends(require_write_permission),
197
- ):
198
- """Create a new record in the table."""
199
- db_name = database
200
- branch_name = branch
201
-
202
- # Check branch permissions
203
- await require_write_permission(auth, branch_name)
204
-
205
- try:
206
- # Create CinchDB instance
207
- db = CinchDB(
208
- database=db_name,
209
- branch=branch_name,
210
- tenant=tenant,
211
- project_dir=auth.project_dir,
212
- )
213
-
214
- # Verify table exists
215
- db.tables.get_table(table_name)
216
-
217
- table_model = create_table_model(table_name)
218
-
219
- # Create model instance from request data
220
- instance = table_model(**request.data)
221
-
222
- # Create the record
223
- created_record = db.data.create(instance)
224
-
225
- return (
226
- created_record.model_dump()
227
- if hasattr(created_record, "model_dump")
228
- else created_record.__dict__
229
- )
230
-
231
- except ValueError as e:
232
- if "already exists" in str(e):
233
- raise HTTPException(status_code=409, detail=str(e))
234
- raise HTTPException(status_code=400, detail=str(e))
235
-
236
-
237
- @router.put("/{table_name}/data/{record_id}", response_model=Dict[str, Any])
238
- async def update_record(
239
- request: UpdateDataRequest,
240
- table_name: str = Path(..., description="Table name"),
241
- record_id: str = Path(..., description="Record ID"),
242
- database: str = Query(..., description="Database name"),
243
- branch: str = Query(..., description="Branch name"),
244
- tenant: str = Query(..., description="Tenant name"),
245
- auth: AuthContext = Depends(require_write_permission),
246
- ):
247
- """Update an existing record."""
248
- db_name = database
249
- branch_name = branch
250
-
251
- # Check branch permissions
252
- await require_write_permission(auth, branch_name)
253
-
254
- try:
255
- # Create CinchDB instance
256
- db = CinchDB(
257
- database=db_name,
258
- branch=branch_name,
259
- tenant=tenant,
260
- project_dir=auth.project_dir,
261
- )
262
-
263
- # Verify table exists
264
- db.tables.get_table(table_name)
265
-
266
- table_model = create_table_model(table_name)
267
-
268
- # Ensure the ID is in the data
269
- update_data = request.data.copy()
270
- update_data["id"] = record_id
271
-
272
- # Create model instance
273
- instance = table_model(**update_data)
274
-
275
- # Update the record
276
- updated_record = db.data.update(instance)
277
-
278
- return (
279
- updated_record.model_dump()
280
- if hasattr(updated_record, "model_dump")
281
- else updated_record.__dict__
282
- )
283
-
284
- except ValueError as e:
285
- if "not found" in str(e):
286
- raise HTTPException(status_code=404, detail=str(e))
287
- raise HTTPException(status_code=400, detail=str(e))
288
-
289
-
290
- @router.delete("/{table_name}/data/{record_id}")
291
- async def delete_record(
292
- table_name: str = Path(..., description="Table name"),
293
- record_id: str = Path(..., description="Record ID"),
294
- database: str = Query(..., description="Database name"),
295
- branch: str = Query(..., description="Branch name"),
296
- tenant: str = Query(..., description="Tenant name"),
297
- auth: AuthContext = Depends(require_write_permission),
298
- ):
299
- """Delete a specific record by ID."""
300
- db_name = database
301
- branch_name = branch
302
-
303
- # Check branch permissions
304
- await require_write_permission(auth, branch_name)
305
-
306
- try:
307
- # Create CinchDB instance
308
- db = CinchDB(
309
- database=db_name,
310
- branch=branch_name,
311
- tenant=tenant,
312
- project_dir=auth.project_dir,
313
- )
314
-
315
- # Verify table exists
316
- db.tables.get_table(table_name)
317
-
318
- table_model = create_table_model(table_name)
319
-
320
- # Delete the record
321
- deleted = db.data.delete_by_id(table_model, record_id)
322
-
323
- if not deleted:
324
- raise HTTPException(
325
- status_code=404, detail=f"Record with ID {record_id} not found"
326
- )
327
-
328
- return {"message": f"Deleted record {record_id} from table '{table_name}'"}
329
-
330
- except ValueError as e:
331
- raise HTTPException(status_code=404, detail=str(e))
332
-
333
-
334
- @router.post("/{table_name}/data/bulk", response_model=List[Dict[str, Any]])
335
- async def bulk_create_records(
336
- request: BulkCreateRequest,
337
- table_name: str = Path(..., description="Table name"),
338
- database: str = Query(..., description="Database name"),
339
- branch: str = Query(..., description="Branch name"),
340
- tenant: str = Query(..., description="Tenant name"),
341
- auth: AuthContext = Depends(require_write_permission),
342
- ):
343
- """Create multiple records in a single transaction."""
344
- db_name = database
345
- branch_name = branch
346
-
347
- # Check branch permissions
348
- await require_write_permission(auth, branch_name)
349
-
350
- try:
351
- # Create CinchDB instance
352
- db = CinchDB(
353
- database=db_name,
354
- branch=branch_name,
355
- tenant=tenant,
356
- project_dir=auth.project_dir,
357
- )
358
-
359
- # Verify table exists
360
- db.tables.get_table(table_name)
361
-
362
- table_model = create_table_model(table_name)
363
-
364
- # Create model instances from request data
365
- instances = [table_model(**record_data) for record_data in request.records]
366
-
367
- # Bulk create
368
- created_records = db.data.bulk_create(instances)
369
-
370
- return [
371
- record.model_dump() if hasattr(record, "model_dump") else record.__dict__
372
- for record in created_records
373
- ]
374
-
375
- except ValueError as e:
376
- raise HTTPException(status_code=400, detail=str(e))
377
-
378
-
379
- @router.delete("/{table_name}/data")
380
- async def delete_records_with_filters(
381
- table_name: str = Path(..., description="Table name"),
382
- database: str = Query(..., description="Database name"),
383
- branch: str = Query(..., description="Branch name"),
384
- tenant: str = Query(..., description="Tenant name"),
385
- auth: AuthContext = Depends(require_write_permission),
386
- **query_params, # This will capture any additional query parameters as filters
387
- ):
388
- """Delete records matching filters. Requires at least one filter parameter."""
389
- db_name = database
390
- branch_name = branch
391
-
392
- # Check branch permissions
393
- await require_write_permission(auth, branch_name)
394
-
395
- # Extract filters from query parameters
396
- filters = {}
397
- excluded_params = {"database", "branch", "tenant"}
398
-
399
- for key, value in query_params.items():
400
- if key not in excluded_params and value is not None:
401
- filters[key] = value
402
-
403
- if not filters:
404
- raise HTTPException(
405
- status_code=400,
406
- detail="At least one filter parameter is required to prevent accidental deletion of all records",
407
- )
408
-
409
- try:
410
- # Create CinchDB instance
411
- db = CinchDB(
412
- database=db_name,
413
- branch=branch_name,
414
- tenant=tenant,
415
- project_dir=auth.project_dir,
416
- )
417
-
418
- # Verify table exists
419
- db.tables.get_table(table_name)
420
-
421
- table_model = create_table_model(table_name)
422
-
423
- # Delete records with filters
424
- deleted_count = db.data.delete(table_model, **filters)
425
-
426
- return {
427
- "message": f"Deleted {deleted_count} records from table '{table_name}'",
428
- "count": deleted_count,
429
- }
430
-
431
- except ValueError as e:
432
- raise HTTPException(status_code=400, detail=str(e))
433
-
434
-
435
- @router.get("/{table_name}/data/count")
436
- async def count_records(
437
- table_name: str = Path(..., description="Table name"),
438
- database: str = Query(..., description="Database name"),
439
- branch: str = Query(..., description="Branch name"),
440
- tenant: str = Query(..., description="Tenant name"),
441
- auth: AuthContext = Depends(require_read_permission),
442
- **query_params, # This will capture any additional query parameters as filters
443
- ):
444
- """Count records in a table with optional filtering."""
445
- db_name = database
446
- branch_name = branch
447
-
448
- # Check branch permissions
449
- await require_read_permission(auth, branch_name)
450
-
451
- # Extract filters from query parameters
452
- filters = {}
453
- excluded_params = {"database", "branch", "tenant"}
454
-
455
- for key, value in query_params.items():
456
- if key not in excluded_params and value is not None:
457
- filters[key] = value
458
-
459
- try:
460
- # Create CinchDB instance
461
- db = CinchDB(
462
- database=db_name,
463
- branch=branch_name,
464
- tenant=tenant,
465
- project_dir=auth.project_dir,
466
- )
467
-
468
- # Verify table exists
469
- db.tables.get_table(table_name)
470
-
471
- table_model = create_table_model(table_name)
472
-
473
- # Count records
474
- count = db.data.count(table_model, **filters)
475
-
476
- return {"count": count, "table": table_name, "filters": filters}
477
-
478
- except ValueError as e:
479
- raise HTTPException(status_code=404, detail=str(e))
@@ -1,177 +0,0 @@
1
- """Databases 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.config import Config
8
- from cinchdb.core.path_utils import list_databases
9
- from cinchdb.api.auth import (
10
- AuthContext,
11
- require_write_permission,
12
- require_read_permission,
13
- )
14
-
15
-
16
- router = APIRouter()
17
-
18
-
19
- class DatabaseInfo(BaseModel):
20
- """Database information."""
21
-
22
- name: str
23
- is_active: bool
24
- is_protected: bool
25
- branch_count: int
26
-
27
-
28
- class CreateDatabaseRequest(BaseModel):
29
- """Request to create a database."""
30
-
31
- name: str
32
- description: str = None
33
-
34
-
35
- @router.get("/", response_model=List[DatabaseInfo])
36
- async def list_all_databases(auth: AuthContext = Depends(require_read_permission)):
37
- """List all databases in the project."""
38
- config = Config(auth.project_dir)
39
- config_data = config.load()
40
-
41
- databases = list_databases(auth.project_dir)
42
-
43
- result = []
44
- for db_name in databases:
45
- # Count branches
46
- branches_path = (
47
- auth.project_dir / ".cinchdb" / "databases" / db_name / "branches"
48
- )
49
- branch_count = (
50
- len(list(branches_path.iterdir())) if branches_path.exists() else 0
51
- )
52
-
53
- result.append(
54
- DatabaseInfo(
55
- name=db_name,
56
- is_active=db_name == config_data.active_database,
57
- is_protected=db_name == "main",
58
- branch_count=branch_count,
59
- )
60
- )
61
-
62
- return result
63
-
64
-
65
- @router.post("/")
66
- async def create_database(
67
- request: CreateDatabaseRequest,
68
- auth: AuthContext = Depends(require_write_permission),
69
- ):
70
- """Create a new database."""
71
- config = Config(auth.project_dir)
72
-
73
- # Create database directory structure
74
- db_path = auth.project_dir / ".cinchdb" / "databases" / request.name
75
- if db_path.exists():
76
- raise HTTPException(
77
- status_code=400, detail=f"Database '{request.name}' already exists"
78
- )
79
-
80
- try:
81
- # Create the database structure
82
- db_path.mkdir(parents=True)
83
- branches_dir = db_path / "branches"
84
- branches_dir.mkdir()
85
-
86
- # Create main branch
87
- main_branch = branches_dir / "main"
88
- main_branch.mkdir()
89
-
90
- # Create main tenant
91
- tenants_dir = main_branch / "tenants"
92
- tenants_dir.mkdir()
93
- main_tenant = tenants_dir / "main.db"
94
- main_tenant.touch()
95
-
96
- # Create branch metadata
97
- import json
98
- from datetime import datetime, timezone
99
-
100
- metadata = {
101
- "name": "main",
102
- "parent": None,
103
- "created_at": datetime.now(timezone.utc).isoformat(),
104
- }
105
- with open(main_branch / "metadata.json", "w") as f:
106
- json.dump(metadata, f, indent=2)
107
-
108
- # Create empty changes file
109
- with open(main_branch / "changes.json", "w") as f:
110
- json.dump([], f)
111
-
112
- return {"message": f"Created database '{request.name}'"}
113
-
114
- except Exception as e:
115
- # Clean up on failure
116
- if db_path.exists():
117
- import shutil
118
-
119
- shutil.rmtree(db_path)
120
- raise HTTPException(status_code=500, detail=f"Failed to create database: {e}")
121
-
122
-
123
- @router.delete("/{name}")
124
- async def delete_database(
125
- name: str, auth: AuthContext = Depends(require_write_permission)
126
- ):
127
- """Delete a database."""
128
- if name == "main":
129
- raise HTTPException(status_code=400, detail="Cannot delete the main database")
130
-
131
- config = Config(auth.project_dir)
132
- db_path = auth.project_dir / ".cinchdb" / "databases" / name
133
-
134
- if not db_path.exists():
135
- raise HTTPException(status_code=404, detail=f"Database '{name}' not found")
136
-
137
- try:
138
- # Delete the database
139
- import shutil
140
-
141
- shutil.rmtree(db_path)
142
-
143
- # If this was the active database, switch to main
144
- config_data = config.load()
145
- if config_data.active_database == name:
146
- config_data.active_database = "main"
147
- config_data.active_branch = "main"
148
- config.save(config_data)
149
-
150
- return {"message": f"Deleted database '{name}'"}
151
-
152
- except Exception as e:
153
- raise HTTPException(status_code=500, detail=f"Failed to delete database: {e}")
154
-
155
-
156
- @router.get("/{name}")
157
- async def get_database_info(
158
- name: str, auth: AuthContext = Depends(require_read_permission)
159
- ) -> DatabaseInfo:
160
- """Get information about a specific database."""
161
- db_path = auth.project_dir / ".cinchdb" / "databases" / name
162
- if not db_path.exists():
163
- raise HTTPException(status_code=404, detail=f"Database '{name}' not found")
164
-
165
- config = Config(auth.project_dir)
166
- config_data = config.load()
167
-
168
- # Count branches
169
- branches_path = db_path / "branches"
170
- branch_count = len(list(branches_path.iterdir())) if branches_path.exists() else 0
171
-
172
- return DatabaseInfo(
173
- name=name,
174
- is_active=name == config_data.active_database,
175
- is_protected=name == "main",
176
- branch_count=branch_count,
177
- )