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.
@@ -1,133 +0,0 @@
1
- """Projects router for CinchDB API."""
2
-
3
- from pathlib import Path
4
- from typing import Optional
5
- from fastapi import APIRouter, Depends, HTTPException
6
- from pydantic import BaseModel
7
-
8
- from cinchdb.config import Config
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 ProjectInfo(BaseModel):
20
- """Project information response."""
21
-
22
- path: str
23
- active_database: str
24
- active_branch: str
25
- has_api_keys: bool
26
-
27
-
28
- class InitProjectRequest(BaseModel):
29
- """Request to initialize a new project."""
30
-
31
- path: str
32
-
33
-
34
- @router.post("/init")
35
- async def init_project(
36
- request: InitProjectRequest, auth: AuthContext = Depends(require_write_permission)
37
- ):
38
- """Initialize a new CinchDB project."""
39
- project_path = Path(request.path)
40
-
41
- # Check if directory exists
42
- if not project_path.exists():
43
- try:
44
- project_path.mkdir(parents=True)
45
- except Exception as e:
46
- raise HTTPException(
47
- status_code=400, detail=f"Failed to create directory: {e}"
48
- )
49
-
50
- # Initialize project
51
- try:
52
- config = Config(project_path)
53
- config.init_project()
54
- return {"message": f"Initialized CinchDB project in {project_path}"}
55
- except FileExistsError:
56
- raise HTTPException(status_code=400, detail="Project already exists")
57
- except Exception as e:
58
- raise HTTPException(
59
- status_code=500, detail=f"Failed to initialize project: {e}"
60
- )
61
-
62
-
63
- @router.get("/info")
64
- async def get_project_info(
65
- auth: AuthContext = Depends(require_read_permission),
66
- ) -> ProjectInfo:
67
- """Get information about the current project."""
68
- config = Config(auth.project_dir)
69
-
70
- try:
71
- config_data = config.load()
72
- has_keys = bool(config_data.api_keys)
73
-
74
- return ProjectInfo(
75
- path=str(auth.project_dir),
76
- active_database=config_data.active_database,
77
- active_branch=config_data.active_branch,
78
- has_api_keys=has_keys,
79
- )
80
- except FileNotFoundError:
81
- raise HTTPException(status_code=404, detail="Project configuration not found")
82
-
83
-
84
- class SetActiveRequest(BaseModel):
85
- """Request to set active database or branch."""
86
-
87
- database: Optional[str] = None
88
- branch: Optional[str] = None
89
-
90
-
91
- @router.put("/active")
92
- async def set_active(
93
- request: SetActiveRequest, auth: AuthContext = Depends(require_write_permission)
94
- ):
95
- """Set the active database and/or branch."""
96
- config = Config(auth.project_dir)
97
-
98
- try:
99
- config_data = config.load()
100
-
101
- if request.database:
102
- # Verify database exists
103
- db_path = auth.project_dir / ".cinchdb" / "databases" / request.database
104
- if not db_path.exists():
105
- raise HTTPException(
106
- status_code=404, detail=f"Database '{request.database}' not found"
107
- )
108
- config_data.active_database = request.database
109
-
110
- if request.branch:
111
- # Verify branch exists in active database
112
- branch_path = (
113
- auth.project_dir
114
- / ".cinchdb"
115
- / "databases"
116
- / config_data.active_database
117
- / "branches"
118
- / request.branch
119
- )
120
- if not branch_path.exists():
121
- raise HTTPException(
122
- status_code=404, detail=f"Branch '{request.branch}' not found"
123
- )
124
- config_data.active_branch = request.branch
125
-
126
- config.save(config_data)
127
-
128
- return {
129
- "active_database": config_data.active_database,
130
- "active_branch": config_data.active_branch,
131
- }
132
- except FileNotFoundError:
133
- raise HTTPException(status_code=404, detail="Project configuration not found")
@@ -1,156 +0,0 @@
1
- """Query router for CinchDB API."""
2
-
3
- from typing import List, Dict, Any, Optional
4
- from fastapi import APIRouter, Depends, HTTPException, Query as QueryParam
5
- from pydantic import BaseModel
6
-
7
- from cinchdb.core.path_utils import get_tenant_db_path
8
- from cinchdb.core.connection import DatabaseConnection
9
- from cinchdb.api.auth import (
10
- AuthContext,
11
- require_read_permission,
12
- require_write_permission,
13
- )
14
- from cinchdb.utils import validate_sql_query, SQLValidationError, SQLOperation
15
-
16
-
17
- router = APIRouter()
18
-
19
-
20
- class QueryRequest(BaseModel):
21
- """Request to execute a query."""
22
-
23
- sql: str
24
- limit: Optional[int] = None
25
-
26
-
27
- class QueryResult(BaseModel):
28
- """Query execution result."""
29
-
30
- columns: List[str]
31
- rows: List[List[Any]]
32
- row_count: int
33
- affected_rows: Optional[int] = None
34
-
35
-
36
- @router.post("/execute")
37
- async def execute_query(
38
- request: QueryRequest,
39
- database: str = QueryParam(..., description="Database name"),
40
- branch: str = QueryParam(..., description="Branch name"),
41
- tenant: str = QueryParam(..., description="Tenant name"),
42
- auth: AuthContext = Depends(require_read_permission),
43
- ) -> QueryResult:
44
- """Execute a SQL query.
45
-
46
- SELECT queries require read permission.
47
- INSERT/UPDATE/DELETE queries require write permission.
48
- """
49
- db_name = database
50
- branch_name = branch
51
-
52
- # Validate the SQL query first
53
- try:
54
- is_valid, error_msg, operation = validate_sql_query(request.sql)
55
- if not is_valid:
56
- raise HTTPException(status_code=400, detail=error_msg)
57
- except Exception as e:
58
- raise HTTPException(status_code=400, detail=str(e))
59
-
60
- # Check if this is a write operation based on validated operation
61
- sql_upper = request.sql.strip().upper()
62
- is_write = operation in (SQLOperation.INSERT, SQLOperation.UPDATE, SQLOperation.DELETE)
63
-
64
- if is_write:
65
- # Require write permission for write operations
66
- await require_write_permission(auth, branch_name)
67
- else:
68
- # Check branch permissions for read
69
- await require_read_permission(auth, branch_name)
70
-
71
- # Add LIMIT if specified and not already present
72
- query_sql = request.sql
73
- if request.limit and "LIMIT" not in sql_upper:
74
- query_sql = f"{request.sql} LIMIT {request.limit}"
75
-
76
- # Get database path
77
- db_path = get_tenant_db_path(auth.project_dir, db_name, branch_name, tenant)
78
-
79
- try:
80
- with DatabaseConnection(db_path) as conn:
81
- cursor = conn.execute(query_sql)
82
-
83
- # Check if this is a SELECT query
84
- is_select = sql_upper.startswith("SELECT")
85
-
86
- if is_select:
87
- rows = cursor.fetchall()
88
-
89
- # Get column names
90
- columns = (
91
- [desc[0] for desc in cursor.description]
92
- if cursor.description
93
- else []
94
- )
95
-
96
- # Convert rows to lists (from sqlite3.Row objects)
97
- row_data = []
98
- for row in rows:
99
- row_data.append(list(row))
100
-
101
- return QueryResult(
102
- columns=columns,
103
- rows=row_data,
104
- row_count=len(rows),
105
- affected_rows=None,
106
- )
107
- else:
108
- # For INSERT/UPDATE/DELETE, commit and return affected rows
109
- conn.commit()
110
- affected = cursor.rowcount
111
-
112
- return QueryResult(
113
- columns=[], rows=[], row_count=0, affected_rows=affected
114
- )
115
-
116
- except Exception as e:
117
- raise HTTPException(status_code=400, detail=f"Query error: {str(e)}")
118
-
119
-
120
- @router.get("/tables/{table}/data")
121
- async def get_table_data(
122
- table: str,
123
- database: str = QueryParam(..., description="Database name"),
124
- branch: str = QueryParam(..., description="Branch name"),
125
- tenant: str = QueryParam(..., description="Tenant name"),
126
- limit: int = QueryParam(100, description="Maximum rows to return"),
127
- offset: int = QueryParam(0, description="Number of rows to skip"),
128
- auth: AuthContext = Depends(require_read_permission),
129
- ) -> QueryResult:
130
- """Get data from a specific table with pagination."""
131
- # Build query
132
- query = QueryRequest(
133
- sql=f"SELECT * FROM {table} LIMIT {limit} OFFSET {offset}",
134
- limit=None, # Already included in SQL
135
- )
136
-
137
- return await execute_query(query, database, branch, tenant, auth)
138
-
139
-
140
- @router.get("/tables/{table}/count")
141
- async def get_table_count(
142
- table: str,
143
- database: str = QueryParam(..., description="Database name"),
144
- branch: str = QueryParam(..., description="Branch name"),
145
- tenant: str = QueryParam(..., description="Tenant name"),
146
- auth: AuthContext = Depends(require_read_permission),
147
- ) -> Dict[str, int]:
148
- """Get the row count for a table."""
149
- # Build query
150
- query = QueryRequest(sql=f"SELECT COUNT(*) as count FROM {table}", limit=None)
151
-
152
- result = await execute_query(query, database, branch, tenant, auth)
153
-
154
- if result.rows and result.rows[0]:
155
- return {"count": result.rows[0][0]}
156
- return {"count": 0}
@@ -1,349 +0,0 @@
1
- """Tables router for CinchDB API."""
2
-
3
- from typing import List, Optional
4
- from fastapi import APIRouter, Depends, HTTPException, Query
5
- from pydantic import BaseModel
6
-
7
- from cinchdb.core.database import CinchDB
8
- from cinchdb.managers.change_applier import ChangeApplier
9
- from cinchdb.models import Column, ForeignKeyRef
10
- from cinchdb.api.auth import (
11
- AuthContext,
12
- require_write_permission,
13
- require_read_permission,
14
- )
15
-
16
-
17
- router = APIRouter()
18
-
19
-
20
- class ForeignKeySchema(BaseModel):
21
- """Foreign key schema for requests."""
22
-
23
- table: str
24
- column: str = "id"
25
- on_delete: str = "RESTRICT"
26
- on_update: str = "RESTRICT"
27
-
28
-
29
- class ColumnSchema(BaseModel):
30
- """Column schema for requests."""
31
-
32
- name: str
33
- type: str
34
- nullable: bool = False
35
- default: Optional[str] = None
36
- primary_key: bool = False
37
- unique: bool = False
38
- foreign_key: Optional[ForeignKeySchema] = None
39
-
40
-
41
- class TableInfo(BaseModel):
42
- """Table information."""
43
-
44
- name: str
45
- column_count: int
46
- columns: List[ColumnSchema]
47
-
48
-
49
- class CreateTableRequest(BaseModel):
50
- """Request to create a table."""
51
-
52
- name: str
53
- columns: List[ColumnSchema]
54
-
55
-
56
- class CopyTableRequest(BaseModel):
57
- """Request to copy a table."""
58
-
59
- source: str
60
- target: str
61
- copy_data: bool = True
62
-
63
-
64
- @router.get("/", response_model=List[TableInfo])
65
- async def list_tables(
66
- database: str = Query(..., description="Database name"),
67
- branch: str = Query(..., description="Branch name"),
68
- auth: AuthContext = Depends(require_read_permission),
69
- ):
70
- """List all tables in a branch."""
71
- db_name = database
72
- branch_name = branch
73
-
74
- # Check branch permissions
75
- await require_read_permission(auth, branch_name)
76
-
77
- try:
78
- db = CinchDB(
79
- database=db_name,
80
- branch=branch_name,
81
- tenant="main",
82
- project_dir=auth.project_dir,
83
- )
84
- tables = db.tables.list_tables()
85
-
86
- result = []
87
- for table in tables:
88
- # Convert columns
89
- columns = []
90
- for col in table.columns:
91
- # Convert foreign key if present
92
- fk_schema = None
93
- if col.foreign_key:
94
- fk_schema = ForeignKeySchema(
95
- table=col.foreign_key.table,
96
- column=col.foreign_key.column,
97
- on_delete=col.foreign_key.on_delete,
98
- on_update=col.foreign_key.on_update,
99
- )
100
-
101
- columns.append(
102
- ColumnSchema(
103
- name=col.name,
104
- type=col.type,
105
- nullable=col.nullable,
106
- default=col.default,
107
- primary_key=col.primary_key,
108
- unique=col.unique,
109
- foreign_key=fk_schema,
110
- )
111
- )
112
-
113
- # Count user-defined columns
114
- user_columns = [
115
- c
116
- for c in table.columns
117
- if c.name not in ["id", "created_at", "updated_at"]
118
- ]
119
-
120
- result.append(
121
- TableInfo(
122
- name=table.name, column_count=len(user_columns), columns=columns
123
- )
124
- )
125
-
126
- return result
127
-
128
- except ValueError as e:
129
- raise HTTPException(status_code=404, detail=str(e))
130
-
131
-
132
- @router.post("/")
133
- async def create_table(
134
- request: CreateTableRequest,
135
- database: str = Query(..., description="Database name"),
136
- branch: str = Query(..., description="Branch name"),
137
- apply: bool = Query(True, description="Apply changes to all tenants"),
138
- auth: AuthContext = Depends(require_write_permission),
139
- ):
140
- """Create a new table."""
141
- db_name = database
142
- branch_name = branch
143
-
144
- # Check branch permissions
145
- await require_write_permission(auth, branch_name)
146
-
147
- # Convert columns
148
- columns = []
149
- for col_schema in request.columns:
150
- # Validate type
151
- if col_schema.type.upper() not in [
152
- "TEXT",
153
- "INTEGER",
154
- "REAL",
155
- "BLOB",
156
- "NUMERIC",
157
- ]:
158
- raise HTTPException(
159
- status_code=400, detail=f"Invalid column type: {col_schema.type}"
160
- )
161
-
162
- # Convert foreign key if present
163
- fk_ref = None
164
- if col_schema.foreign_key:
165
- # Validate FK action
166
- if col_schema.foreign_key.on_delete not in ["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]:
167
- raise HTTPException(
168
- status_code=400,
169
- detail=f"Invalid on_delete action: {col_schema.foreign_key.on_delete}"
170
- )
171
- if col_schema.foreign_key.on_update not in ["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]:
172
- raise HTTPException(
173
- status_code=400,
174
- detail=f"Invalid on_update action: {col_schema.foreign_key.on_update}"
175
- )
176
-
177
- fk_ref = ForeignKeyRef(
178
- table=col_schema.foreign_key.table,
179
- column=col_schema.foreign_key.column,
180
- on_delete=col_schema.foreign_key.on_delete,
181
- on_update=col_schema.foreign_key.on_update,
182
- )
183
-
184
- columns.append(
185
- Column(
186
- name=col_schema.name,
187
- type=col_schema.type.upper(),
188
- nullable=col_schema.nullable,
189
- default=col_schema.default,
190
- primary_key=col_schema.primary_key,
191
- unique=col_schema.unique,
192
- foreign_key=fk_ref,
193
- )
194
- )
195
-
196
- try:
197
- db = CinchDB(
198
- database=db_name,
199
- branch=branch_name,
200
- tenant="main",
201
- project_dir=auth.project_dir,
202
- )
203
- db.tables.create_table(request.name, columns)
204
-
205
- # Apply to all tenants if requested
206
- if apply:
207
- applier = ChangeApplier(auth.project_dir, db_name, branch_name)
208
- applier.apply_all_unapplied()
209
-
210
- return {
211
- "message": f"Created table '{request.name}' with {len(columns)} columns"
212
- }
213
-
214
- except ValueError as e:
215
- raise HTTPException(status_code=400, detail=str(e))
216
-
217
-
218
- @router.delete("/{name}")
219
- async def delete_table(
220
- name: str,
221
- database: str = Query(..., description="Database name"),
222
- branch: str = Query(..., description="Branch name"),
223
- apply: bool = Query(True, description="Apply changes to all tenants"),
224
- auth: AuthContext = Depends(require_write_permission),
225
- ):
226
- """Delete a table."""
227
- db_name = database
228
- branch_name = branch
229
-
230
- # Check branch permissions
231
- await require_write_permission(auth, branch_name)
232
-
233
- try:
234
- db = CinchDB(
235
- database=db_name,
236
- branch=branch_name,
237
- tenant="main",
238
- project_dir=auth.project_dir,
239
- )
240
- db.tables.delete_table(name)
241
-
242
- # Apply to all tenants if requested
243
- if apply:
244
- applier = ChangeApplier(auth.project_dir, db_name, branch_name)
245
- applier.apply_all_unapplied()
246
-
247
- return {"message": f"Deleted table '{name}'"}
248
-
249
- except ValueError as e:
250
- raise HTTPException(status_code=404, detail=str(e))
251
-
252
-
253
- @router.post("/copy")
254
- async def copy_table(
255
- request: CopyTableRequest,
256
- database: str = Query(..., description="Database name"),
257
- branch: str = Query(..., description="Branch name"),
258
- apply: bool = Query(True, description="Apply changes to all tenants"),
259
- auth: AuthContext = Depends(require_write_permission),
260
- ):
261
- """Copy a table to a new table."""
262
- db_name = database
263
- branch_name = branch
264
-
265
- # Check branch permissions
266
- await require_write_permission(auth, branch_name)
267
-
268
- try:
269
- db = CinchDB(
270
- database=db_name,
271
- branch=branch_name,
272
- tenant="main",
273
- project_dir=auth.project_dir,
274
- )
275
- db.tables.copy_table(request.source, request.target, request.copy_data)
276
-
277
- # Apply to all tenants if requested
278
- if apply:
279
- applier = ChangeApplier(auth.project_dir, db_name, branch_name)
280
- applier.apply_all_unapplied()
281
-
282
- data_msg = "with data" if request.copy_data else "without data"
283
- return {
284
- "message": f"Copied table '{request.source}' to '{request.target}' {data_msg}"
285
- }
286
-
287
- except ValueError as e:
288
- raise HTTPException(status_code=400, detail=str(e))
289
-
290
-
291
- @router.get("/{name}")
292
- async def get_table_info(
293
- name: str,
294
- database: str = Query(..., description="Database name"),
295
- branch: str = Query(..., description="Branch name"),
296
- auth: AuthContext = Depends(require_read_permission),
297
- ) -> TableInfo:
298
- """Get information about a specific table."""
299
- db_name = database
300
- branch_name = branch
301
-
302
- # Check branch permissions
303
- await require_read_permission(auth, branch_name)
304
-
305
- try:
306
- db = CinchDB(
307
- database=db_name,
308
- branch=branch_name,
309
- tenant="main",
310
- project_dir=auth.project_dir,
311
- )
312
- table = db.tables.get_table(name)
313
-
314
- # Convert columns
315
- columns = []
316
- for col in table.columns:
317
- # Convert foreign key if present
318
- fk_schema = None
319
- if col.foreign_key:
320
- fk_schema = ForeignKeySchema(
321
- table=col.foreign_key.table,
322
- column=col.foreign_key.column,
323
- on_delete=col.foreign_key.on_delete,
324
- on_update=col.foreign_key.on_update,
325
- )
326
-
327
- columns.append(
328
- ColumnSchema(
329
- name=col.name,
330
- type=col.type,
331
- nullable=col.nullable,
332
- default=col.default,
333
- primary_key=col.primary_key,
334
- unique=col.unique,
335
- foreign_key=fk_schema,
336
- )
337
- )
338
-
339
- # Count user-defined columns
340
- user_columns = [
341
- c for c in table.columns if c.name not in ["id", "created_at", "updated_at"]
342
- ]
343
-
344
- return TableInfo(
345
- name=table.name, column_count=len(user_columns), columns=columns
346
- )
347
-
348
- except ValueError as e:
349
- raise HTTPException(status_code=404, detail=str(e))