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,537 @@
1
+ """Unified database connection interface for CinchDB."""
2
+
3
+ from pathlib import Path
4
+ from typing import List, Dict, Any, Optional, TYPE_CHECKING
5
+
6
+ from cinchdb.config import Config
7
+ from cinchdb.models import Column, Change
8
+ from cinchdb.core.path_utils import get_project_root
9
+ from cinchdb.utils import validate_query_safe, SQLValidationError
10
+
11
+ if TYPE_CHECKING:
12
+ from cinchdb.managers.table import TableManager
13
+ from cinchdb.managers.column import ColumnManager
14
+ from cinchdb.managers.query import QueryManager
15
+ from cinchdb.managers.data import DataManager
16
+ from cinchdb.managers.view import ViewModel
17
+ from cinchdb.managers.branch import BranchManager
18
+ from cinchdb.managers.tenant import TenantManager
19
+ from cinchdb.managers.codegen import CodegenManager
20
+ from cinchdb.managers.merge_manager import MergeManager
21
+
22
+
23
+ class CinchDB:
24
+ """Unified interface for CinchDB operations.
25
+
26
+ Provides a simple, user-friendly interface for both local and remote
27
+ connections while preserving access to all manager functionality.
28
+
29
+ Examples:
30
+ # Local connection
31
+ db = CinchDB(project_dir="/path/to/project", database="mydb", branch="dev")
32
+
33
+ # Remote connection
34
+ db = CinchDB(
35
+ api_url="https://api.example.com",
36
+ api_key="your-api-key",
37
+ database="mydb",
38
+ branch="dev"
39
+ )
40
+
41
+ # Execute queries
42
+ results = db.query("SELECT * FROM users WHERE active = ?", [True])
43
+
44
+ # Create tables
45
+ db.create_table("products", [
46
+ Column(name="name", type="TEXT"),
47
+ Column(name="price", type="REAL")
48
+ ])
49
+
50
+ # Access managers for advanced operations (local only)
51
+ if db.is_local:
52
+ db.tables.copy_table("products", "products_backup")
53
+ db.columns.add_column("users", Column(name="phone", type="TEXT"))
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ database: str,
59
+ branch: str = "main",
60
+ tenant: str = "main",
61
+ project_dir: Optional[Path] = None,
62
+ api_url: Optional[str] = None,
63
+ api_key: Optional[str] = None,
64
+ ):
65
+ """Initialize CinchDB connection.
66
+
67
+ Args:
68
+ database: Database name
69
+ branch: Branch name (default: main)
70
+ tenant: Tenant name (default: main)
71
+ project_dir: Path to project directory for local connection
72
+ api_url: Base URL for remote API connection
73
+ api_key: API key for remote connection
74
+
75
+ Raises:
76
+ ValueError: If neither local nor remote connection params provided
77
+ """
78
+ self.database = database
79
+ self.branch = branch
80
+ self.tenant = tenant
81
+
82
+ # Determine connection type
83
+ if project_dir is not None:
84
+ # Local connection
85
+ self.project_dir = Path(project_dir)
86
+ self.api_url = None
87
+ self.api_key = None
88
+ self.is_local = True
89
+ elif api_url is not None and api_key is not None:
90
+ # Remote connection
91
+ self.project_dir = None
92
+ self.api_url = api_url.rstrip("/")
93
+ self.api_key = api_key
94
+ self.is_local = False
95
+ self._session = None
96
+ else:
97
+ raise ValueError(
98
+ "Must provide either project_dir for local connection "
99
+ "or both api_url and api_key for remote connection"
100
+ )
101
+
102
+ # Lazy-loaded managers (local only)
103
+ self._table_manager: Optional["TableManager"] = None
104
+ self._column_manager: Optional["ColumnManager"] = None
105
+ self._query_manager: Optional["QueryManager"] = None
106
+ self._data_manager: Optional["DataManager"] = None
107
+ self._view_manager: Optional["ViewModel"] = None
108
+ self._branch_manager: Optional["BranchManager"] = None
109
+ self._tenant_manager: Optional["TenantManager"] = None
110
+ self._codegen_manager: Optional["CodegenManager"] = None
111
+ self._merge_manager: Optional["MergeManager"] = None
112
+
113
+ @property
114
+ def session(self):
115
+ """Get or create HTTP session for remote connections."""
116
+ if not self.is_local and self._session is None:
117
+ try:
118
+ import requests
119
+ except ImportError:
120
+ raise ImportError(
121
+ "The 'requests' package is required for remote connections. "
122
+ "Install it with: pip install requests"
123
+ )
124
+ self._session = requests.Session()
125
+ self._session.headers.update(
126
+ {"X-API-Key": self.api_key, "Content-Type": "application/json"}
127
+ )
128
+ return self._session
129
+
130
+ def _endpoint_needs_tenant(self, endpoint: str) -> bool:
131
+ """Check if an API endpoint needs tenant parameter.
132
+
133
+ Args:
134
+ endpoint: API endpoint path
135
+
136
+ Returns:
137
+ True if endpoint needs tenant parameter
138
+ """
139
+ # Query operations need tenant
140
+ if endpoint.startswith("/query"):
141
+ return True
142
+
143
+ # Data CRUD operations need tenant (tables/{table}/data)
144
+ if "/data" in endpoint:
145
+ return True
146
+
147
+ # Tenant management operations need tenant
148
+ if endpoint.startswith("/tenants"):
149
+ return True
150
+
151
+ # Schema operations don't need tenant
152
+ return False
153
+
154
+ def _make_request(self, method: str, endpoint: str, **kwargs) -> Any:
155
+ """Make an API request for remote connections.
156
+
157
+ Args:
158
+ method: HTTP method (GET, POST, etc.)
159
+ endpoint: API endpoint path
160
+ **kwargs: Additional request parameters
161
+
162
+ Returns:
163
+ Response data
164
+
165
+ Raises:
166
+ Exception: If request fails
167
+ """
168
+ if self.is_local:
169
+ raise RuntimeError("Cannot make API requests on local connection")
170
+
171
+ url = f"{self.api_url}{endpoint}"
172
+
173
+ # Add default query parameters
174
+ params = kwargs.get("params", {})
175
+ params.update({"database": self.database, "branch": self.branch})
176
+
177
+ # Only add tenant for data operations (query, data CRUD, tenant management)
178
+ if self._endpoint_needs_tenant(endpoint):
179
+ params["tenant"] = self.tenant
180
+
181
+ kwargs["params"] = params
182
+
183
+ response = self.session.request(method, url, **kwargs)
184
+
185
+ if response.status_code >= 400:
186
+ error_detail = response.json().get("detail", "Unknown error")
187
+ raise Exception(f"API Error ({response.status_code}): {error_detail}")
188
+
189
+ return response.json()
190
+
191
+ @property
192
+ def tables(self) -> "TableManager":
193
+ """Access table operations (local only)."""
194
+ if not self.is_local:
195
+ raise RuntimeError(
196
+ "Direct manager access not available for remote connections"
197
+ )
198
+ if self._table_manager is None:
199
+ from cinchdb.managers.table import TableManager
200
+
201
+ self._table_manager = TableManager(
202
+ self.project_dir, self.database, self.branch, self.tenant
203
+ )
204
+ return self._table_manager
205
+
206
+ @property
207
+ def columns(self) -> "ColumnManager":
208
+ """Access column operations (local only)."""
209
+ if not self.is_local:
210
+ raise RuntimeError(
211
+ "Direct manager access not available for remote connections"
212
+ )
213
+ if self._column_manager is None:
214
+ from cinchdb.managers.column import ColumnManager
215
+
216
+ self._column_manager = ColumnManager(
217
+ self.project_dir, self.database, self.branch, self.tenant
218
+ )
219
+ return self._column_manager
220
+
221
+ @property
222
+ def views(self) -> "ViewModel":
223
+ """Access view operations (local only)."""
224
+ if not self.is_local:
225
+ raise RuntimeError(
226
+ "Direct manager access not available for remote connections"
227
+ )
228
+ if self._view_manager is None:
229
+ from cinchdb.managers.view import ViewModel
230
+
231
+ self._view_manager = ViewModel(
232
+ self.project_dir, self.database, self.branch, self.tenant
233
+ )
234
+ return self._view_manager
235
+
236
+ @property
237
+ def branches(self) -> "BranchManager":
238
+ """Access branch operations (local only)."""
239
+ if not self.is_local:
240
+ raise RuntimeError(
241
+ "Direct manager access not available for remote connections"
242
+ )
243
+ if self._branch_manager is None:
244
+ from cinchdb.managers.branch import BranchManager
245
+
246
+ self._branch_manager = BranchManager(self.project_dir, self.database)
247
+ return self._branch_manager
248
+
249
+ @property
250
+ def tenants(self) -> "TenantManager":
251
+ """Access tenant operations (local only)."""
252
+ if not self.is_local:
253
+ raise RuntimeError(
254
+ "Direct manager access not available for remote connections"
255
+ )
256
+ if self._tenant_manager is None:
257
+ from cinchdb.managers.tenant import TenantManager
258
+
259
+ self._tenant_manager = TenantManager(
260
+ self.project_dir, self.database, self.branch
261
+ )
262
+ return self._tenant_manager
263
+
264
+ @property
265
+ def data(self) -> "DataManager":
266
+ """Access data operations (local only)."""
267
+ if not self.is_local:
268
+ raise RuntimeError(
269
+ "Direct manager access not available for remote connections"
270
+ )
271
+ if self._data_manager is None:
272
+ from cinchdb.managers.data import DataManager
273
+
274
+ self._data_manager = DataManager(
275
+ self.project_dir, self.database, self.branch, self.tenant
276
+ )
277
+ return self._data_manager
278
+
279
+ @property
280
+ def codegen(self) -> "CodegenManager":
281
+ """Access code generation operations (local only)."""
282
+ if not self.is_local:
283
+ raise RuntimeError(
284
+ "Direct manager access not available for remote connections"
285
+ )
286
+ if self._codegen_manager is None:
287
+ from cinchdb.managers.codegen import CodegenManager
288
+
289
+ self._codegen_manager = CodegenManager(
290
+ self.project_dir, self.database, self.branch, self.tenant
291
+ )
292
+ return self._codegen_manager
293
+
294
+ @property
295
+ def merge(self) -> "MergeManager":
296
+ """Access merge operations (local only)."""
297
+ if not self.is_local:
298
+ raise RuntimeError(
299
+ "Direct manager access not available for remote connections"
300
+ )
301
+ if self._merge_manager is None:
302
+ from cinchdb.managers.merge_manager import MergeManager
303
+
304
+ self._merge_manager = MergeManager(self.project_dir, self.database)
305
+ return self._merge_manager
306
+
307
+ # Convenience methods for common operations
308
+
309
+ def query(
310
+ self, sql: str, params: Optional[List[Any]] = None, skip_validation: bool = False
311
+ ) -> List[Dict[str, Any]]:
312
+ """Execute a SQL query.
313
+
314
+ Args:
315
+ sql: SQL query to execute
316
+ params: Query parameters (optional)
317
+ skip_validation: Skip SQL validation (default: False)
318
+
319
+ Returns:
320
+ List of result rows as dictionaries
321
+
322
+ Raises:
323
+ SQLValidationError: If the query contains restricted operations
324
+ """
325
+ # Validate query unless explicitly skipped
326
+ if not skip_validation:
327
+ validate_query_safe(sql)
328
+
329
+ if self.is_local:
330
+ if self._query_manager is None:
331
+ from cinchdb.managers.query import QueryManager
332
+
333
+ self._query_manager = QueryManager(
334
+ self.project_dir, self.database, self.branch, self.tenant
335
+ )
336
+ return self._query_manager.execute(sql, params)
337
+ else:
338
+ # Remote query
339
+ data = {"sql": sql}
340
+ if params:
341
+ data["params"] = params
342
+ result = self._make_request("POST", "/query", json=data)
343
+ return result.get("data", [])
344
+
345
+ def create_table(self, name: str, columns: List[Column]) -> None:
346
+ """Create a new table.
347
+
348
+ Args:
349
+ name: Table name
350
+ columns: List of column definitions
351
+ """
352
+ if self.is_local:
353
+ self.tables.create_table(name, columns)
354
+ else:
355
+ # Remote table creation
356
+ columns_data = [
357
+ {"name": col.name, "type": col.type, "nullable": col.nullable}
358
+ for col in columns
359
+ ]
360
+ self._make_request(
361
+ "POST", "/tables", json={"name": name, "columns": columns_data}
362
+ )
363
+
364
+ def insert(self, table: str, data: Dict[str, Any]) -> Dict[str, Any]:
365
+ """Insert a record into a table.
366
+
367
+ Args:
368
+ table: Table name
369
+ data: Record data as dictionary
370
+
371
+ Returns:
372
+ Inserted record with generated fields
373
+ """
374
+ if self.is_local:
375
+ return self.data.create(table, data)
376
+ else:
377
+ # Remote insert - use new data CRUD endpoint
378
+ result = self._make_request(
379
+ "POST", f"/tables/{table}/data", json={"data": data}
380
+ )
381
+ return result
382
+
383
+ def update(self, table: str, id: str, data: Dict[str, Any]) -> Dict[str, Any]:
384
+ """Update a record in a table.
385
+
386
+ Args:
387
+ table: Table name
388
+ id: Record ID
389
+ data: Updated data as dictionary
390
+
391
+ Returns:
392
+ Updated record
393
+ """
394
+ if self.is_local:
395
+ return self.data.update(table, id, data)
396
+ else:
397
+ # Remote update - use new data CRUD endpoint
398
+ result = self._make_request(
399
+ "PUT", f"/tables/{table}/data/{id}", json={"data": data}
400
+ )
401
+ return result
402
+
403
+ def delete(self, table: str, id: str) -> None:
404
+ """Delete a record from a table.
405
+
406
+ Args:
407
+ table: Table name
408
+ id: Record ID
409
+ """
410
+ if self.is_local:
411
+ self.data.delete(table, id)
412
+ else:
413
+ # Remote delete
414
+ self._make_request("DELETE", f"/tables/{table}/data/{id}")
415
+
416
+ def list_changes(self) -> List["Change"]:
417
+ """List all changes for the current branch.
418
+
419
+ Returns:
420
+ List of Change objects containing change history
421
+
422
+ Examples:
423
+ # List all changes
424
+ changes = db.list_changes()
425
+ for change in changes:
426
+ print(f"{change.type}: {change.entity_name} (applied: {change.applied})")
427
+ """
428
+ if self.is_local:
429
+ from cinchdb.managers.change_tracker import ChangeTracker
430
+ tracker = ChangeTracker(self.project_dir, self.database, self.branch)
431
+ return tracker.get_changes()
432
+ else:
433
+ # Remote API call
434
+ result = self._make_request("GET", f"/branches/{self.branch}/changes")
435
+ # Convert API response to Change objects
436
+ from cinchdb.models import Change
437
+ from datetime import datetime
438
+ changes = []
439
+ for data in result.get("changes", []):
440
+ # Convert string dates back to datetime if present
441
+ if data.get("created_at"):
442
+ data["created_at"] = datetime.fromisoformat(data["created_at"])
443
+ if data.get("updated_at"):
444
+ data["updated_at"] = datetime.fromisoformat(data["updated_at"])
445
+ changes.append(Change(**data))
446
+ return changes
447
+
448
+ def close(self):
449
+ """Close any open connections."""
450
+ if not self.is_local and self._session:
451
+ self._session.close()
452
+ self._session = None
453
+
454
+ def __enter__(self):
455
+ """Context manager entry."""
456
+ return self
457
+
458
+ def __exit__(self, exc_type, exc_val, exc_tb):
459
+ """Context manager exit."""
460
+ self.close()
461
+
462
+
463
+ def connect(
464
+ database: str,
465
+ branch: str = "main",
466
+ tenant: str = "main",
467
+ project_dir: Optional[Path] = None,
468
+ ) -> CinchDB:
469
+ """Connect to a local CinchDB database.
470
+
471
+ Args:
472
+ database: Database name
473
+ branch: Branch name (default: main)
474
+ tenant: Tenant name (default: main)
475
+ project_dir: Path to project directory (optional, will search for .cinchdb)
476
+
477
+ Returns:
478
+ CinchDB connection instance
479
+
480
+ Examples:
481
+ # Connect using current directory
482
+ db = connect("mydb")
483
+
484
+ # Connect to specific branch
485
+ db = connect("mydb", "feature-branch")
486
+
487
+ # Connect with explicit project directory
488
+ db = connect("mydb", project_dir=Path("/path/to/project"))
489
+ """
490
+ if project_dir is None:
491
+ try:
492
+ project_dir = get_project_root(Path.cwd())
493
+ except FileNotFoundError:
494
+ raise ValueError("No .cinchdb directory found. Run 'cinchdb init' first.")
495
+
496
+ return CinchDB(
497
+ database=database, branch=branch, tenant=tenant, project_dir=project_dir
498
+ )
499
+
500
+
501
+ def connect_api(
502
+ api_url: str,
503
+ api_key: str,
504
+ database: str,
505
+ branch: str = "main",
506
+ tenant: str = "main",
507
+ ) -> CinchDB:
508
+ """Connect to a remote CinchDB API.
509
+
510
+ Args:
511
+ api_url: Base URL of the CinchDB API
512
+ api_key: API authentication key
513
+ database: Database name
514
+ branch: Branch name (default: main)
515
+ tenant: Tenant name (default: main)
516
+
517
+ Returns:
518
+ CinchDB connection instance for remote API
519
+
520
+ Examples:
521
+ # Connect to remote API
522
+ db = connect_api("https://api.example.com", "your-api-key", "mydb")
523
+
524
+ # Connect to specific branch
525
+ db = connect_api("https://api.example.com", "your-api-key", "mydb", "dev")
526
+
527
+ # Use with context manager
528
+ with connect_api("https://api.example.com", "key", "mydb") as db:
529
+ results = db.query("SELECT * FROM users")
530
+ """
531
+ return CinchDB(
532
+ database=database,
533
+ branch=branch,
534
+ tenant=tenant,
535
+ api_url=api_url,
536
+ api_key=api_key,
537
+ )
@@ -0,0 +1,73 @@
1
+ """Maintenance mode utilities for CinchDB."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ from typing import Dict, Any, Optional
6
+
7
+ from cinchdb.core.path_utils import get_branch_path
8
+
9
+
10
+ class MaintenanceError(Exception):
11
+ """Exception raised when operation blocked by maintenance mode."""
12
+
13
+ pass
14
+
15
+
16
+ def is_branch_in_maintenance(project_root: Path, database: str, branch: str) -> bool:
17
+ """Check if a branch is in maintenance mode.
18
+
19
+ Args:
20
+ project_root: Path to project root
21
+ database: Database name
22
+ branch: Branch name
23
+
24
+ Returns:
25
+ True if in maintenance mode, False otherwise
26
+ """
27
+ branch_path = get_branch_path(project_root, database, branch)
28
+ maintenance_file = branch_path / ".maintenance_mode"
29
+ return maintenance_file.exists()
30
+
31
+
32
+ def get_maintenance_info(
33
+ project_root: Path, database: str, branch: str
34
+ ) -> Optional[Dict[str, Any]]:
35
+ """Get maintenance mode information if active.
36
+
37
+ Args:
38
+ project_root: Path to project root
39
+ database: Database name
40
+ branch: Branch name
41
+
42
+ Returns:
43
+ Maintenance info dict or None if not in maintenance
44
+ """
45
+ branch_path = get_branch_path(project_root, database, branch)
46
+ maintenance_file = branch_path / ".maintenance_mode"
47
+
48
+ if maintenance_file.exists():
49
+ with open(maintenance_file, "r") as f:
50
+ return json.load(f)
51
+
52
+ return None
53
+
54
+
55
+ def check_maintenance_mode(project_root: Path, database: str, branch: str) -> None:
56
+ """Check maintenance mode and raise error if active.
57
+
58
+ Args:
59
+ project_root: Path to project root
60
+ database: Database name
61
+ branch: Branch name
62
+
63
+ Raises:
64
+ MaintenanceError: If branch is in maintenance mode
65
+ """
66
+ if is_branch_in_maintenance(project_root, database, branch):
67
+ info = get_maintenance_info(project_root, database, branch)
68
+ reason = (
69
+ info.get("reason", "Maintenance in progress")
70
+ if info
71
+ else "Maintenance in progress"
72
+ )
73
+ raise MaintenanceError(f"Branch '{branch}' is in maintenance mode: {reason}")