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.
- cinchdb/__init__.py +7 -0
- cinchdb/__main__.py +6 -0
- cinchdb/api/__init__.py +5 -0
- cinchdb/api/app.py +76 -0
- cinchdb/api/auth.py +290 -0
- cinchdb/api/main.py +137 -0
- cinchdb/api/routers/__init__.py +25 -0
- cinchdb/api/routers/auth.py +135 -0
- cinchdb/api/routers/branches.py +368 -0
- cinchdb/api/routers/codegen.py +164 -0
- cinchdb/api/routers/columns.py +290 -0
- cinchdb/api/routers/data.py +479 -0
- cinchdb/api/routers/databases.py +177 -0
- cinchdb/api/routers/projects.py +133 -0
- cinchdb/api/routers/query.py +156 -0
- cinchdb/api/routers/tables.py +349 -0
- cinchdb/api/routers/tenants.py +216 -0
- cinchdb/api/routers/views.py +219 -0
- cinchdb/cli/__init__.py +0 -0
- cinchdb/cli/commands/__init__.py +1 -0
- cinchdb/cli/commands/branch.py +479 -0
- cinchdb/cli/commands/codegen.py +176 -0
- cinchdb/cli/commands/column.py +308 -0
- cinchdb/cli/commands/database.py +212 -0
- cinchdb/cli/commands/query.py +136 -0
- cinchdb/cli/commands/remote.py +144 -0
- cinchdb/cli/commands/table.py +289 -0
- cinchdb/cli/commands/tenant.py +173 -0
- cinchdb/cli/commands/view.py +189 -0
- cinchdb/cli/handlers/__init__.py +5 -0
- cinchdb/cli/handlers/codegen_handler.py +189 -0
- cinchdb/cli/main.py +137 -0
- cinchdb/cli/utils.py +182 -0
- cinchdb/config.py +177 -0
- cinchdb/core/__init__.py +5 -0
- cinchdb/core/connection.py +175 -0
- cinchdb/core/database.py +537 -0
- cinchdb/core/maintenance.py +73 -0
- cinchdb/core/path_utils.py +153 -0
- cinchdb/managers/__init__.py +26 -0
- cinchdb/managers/branch.py +167 -0
- cinchdb/managers/change_applier.py +414 -0
- cinchdb/managers/change_comparator.py +194 -0
- cinchdb/managers/change_tracker.py +182 -0
- cinchdb/managers/codegen.py +523 -0
- cinchdb/managers/column.py +579 -0
- cinchdb/managers/data.py +455 -0
- cinchdb/managers/merge_manager.py +429 -0
- cinchdb/managers/query.py +214 -0
- cinchdb/managers/table.py +383 -0
- cinchdb/managers/tenant.py +258 -0
- cinchdb/managers/view.py +252 -0
- cinchdb/models/__init__.py +27 -0
- cinchdb/models/base.py +44 -0
- cinchdb/models/branch.py +26 -0
- cinchdb/models/change.py +47 -0
- cinchdb/models/database.py +20 -0
- cinchdb/models/project.py +20 -0
- cinchdb/models/table.py +86 -0
- cinchdb/models/tenant.py +19 -0
- cinchdb/models/view.py +15 -0
- cinchdb/utils/__init__.py +15 -0
- cinchdb/utils/sql_validator.py +137 -0
- cinchdb-0.1.0.dist-info/METADATA +195 -0
- cinchdb-0.1.0.dist-info/RECORD +68 -0
- cinchdb-0.1.0.dist-info/WHEEL +4 -0
- cinchdb-0.1.0.dist-info/entry_points.txt +3 -0
- cinchdb-0.1.0.dist-info/licenses/LICENSE +201 -0
cinchdb/core/database.py
ADDED
@@ -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}")
|