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
@@ -0,0 +1,523 @@
|
|
1
|
+
"""Code generation manager for creating models from database schemas."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import List, Dict, Any, Literal
|
5
|
+
from ..core.connection import DatabaseConnection
|
6
|
+
from ..core.path_utils import get_tenant_db_path
|
7
|
+
from ..models import Table, Column, View
|
8
|
+
from ..managers.table import TableManager
|
9
|
+
from ..managers.view import ViewModel
|
10
|
+
|
11
|
+
|
12
|
+
LanguageType = Literal["python", "typescript"]
|
13
|
+
|
14
|
+
|
15
|
+
class CodegenManager:
|
16
|
+
"""Manages code generation for database schemas."""
|
17
|
+
|
18
|
+
SUPPORTED_LANGUAGES = ["python", "typescript"]
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self, project_root: Path, database: str, branch: str, tenant: str = "main"
|
22
|
+
):
|
23
|
+
"""Initialize codegen manager.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
project_root: Path to project root
|
27
|
+
database: Database name
|
28
|
+
branch: Branch name
|
29
|
+
tenant: Tenant name (defaults to main)
|
30
|
+
"""
|
31
|
+
self.project_root = Path(project_root)
|
32
|
+
self.database = database
|
33
|
+
self.branch = branch
|
34
|
+
self.tenant = tenant
|
35
|
+
self.db_path = get_tenant_db_path(project_root, database, branch, tenant)
|
36
|
+
|
37
|
+
# Initialize managers for data access
|
38
|
+
self.table_manager = TableManager(project_root, database, branch, tenant)
|
39
|
+
self.view_manager = ViewModel(project_root, database, branch, tenant)
|
40
|
+
|
41
|
+
def get_supported_languages(self) -> List[str]:
|
42
|
+
"""Get list of supported code generation languages.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
List of supported language names
|
46
|
+
"""
|
47
|
+
return self.SUPPORTED_LANGUAGES.copy()
|
48
|
+
|
49
|
+
def generate_models(
|
50
|
+
self,
|
51
|
+
language: LanguageType,
|
52
|
+
output_dir: Path,
|
53
|
+
include_tables: bool = True,
|
54
|
+
include_views: bool = True,
|
55
|
+
) -> Dict[str, Any]:
|
56
|
+
"""Generate model files for the specified language.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
language: Target language for generation
|
60
|
+
output_dir: Directory to write generated files
|
61
|
+
include_tables: Whether to generate table models
|
62
|
+
include_views: Whether to generate view models
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Dictionary with generation results
|
66
|
+
|
67
|
+
Raises:
|
68
|
+
ValueError: If language not supported or output directory invalid
|
69
|
+
"""
|
70
|
+
if language not in self.SUPPORTED_LANGUAGES:
|
71
|
+
raise ValueError(
|
72
|
+
f"Language '{language}' not supported. Available: {self.SUPPORTED_LANGUAGES}"
|
73
|
+
)
|
74
|
+
|
75
|
+
output_path = Path(output_dir)
|
76
|
+
if not output_path.exists():
|
77
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
78
|
+
|
79
|
+
results = {
|
80
|
+
"language": language,
|
81
|
+
"output_dir": str(output_path),
|
82
|
+
"files_generated": [],
|
83
|
+
"tables_processed": [],
|
84
|
+
"views_processed": [],
|
85
|
+
}
|
86
|
+
|
87
|
+
if language == "python":
|
88
|
+
return self._generate_python_models(
|
89
|
+
output_path, include_tables, include_views, results
|
90
|
+
)
|
91
|
+
elif language == "typescript":
|
92
|
+
return self._generate_typescript_models(
|
93
|
+
output_path, include_tables, include_views, results
|
94
|
+
)
|
95
|
+
|
96
|
+
return results
|
97
|
+
|
98
|
+
def _generate_python_models(
|
99
|
+
self,
|
100
|
+
output_dir: Path,
|
101
|
+
include_tables: bool,
|
102
|
+
include_views: bool,
|
103
|
+
results: Dict[str, Any],
|
104
|
+
) -> Dict[str, Any]:
|
105
|
+
"""Generate Python Pydantic models."""
|
106
|
+
|
107
|
+
# Generate __init__.py
|
108
|
+
init_content = ['"""Generated CinchDB models."""\n']
|
109
|
+
model_imports = []
|
110
|
+
|
111
|
+
if include_tables:
|
112
|
+
# Get all tables
|
113
|
+
tables = self.table_manager.list_tables()
|
114
|
+
|
115
|
+
for table in tables:
|
116
|
+
# Generate model for each table
|
117
|
+
model_content = self._generate_python_table_model(table)
|
118
|
+
model_filename = f"{self._to_snake_case(table.name)}.py"
|
119
|
+
model_path = output_dir / model_filename
|
120
|
+
|
121
|
+
with open(model_path, "w") as f:
|
122
|
+
f.write(model_content)
|
123
|
+
|
124
|
+
results["files_generated"].append(model_filename)
|
125
|
+
results["tables_processed"].append(table.name)
|
126
|
+
|
127
|
+
# Add to imports
|
128
|
+
class_name = self._to_pascal_case(table.name)
|
129
|
+
model_imports.append(
|
130
|
+
f"from .{self._to_snake_case(table.name)} import {class_name}"
|
131
|
+
)
|
132
|
+
|
133
|
+
if include_views:
|
134
|
+
# Get all views
|
135
|
+
views = self.view_manager.list_views()
|
136
|
+
|
137
|
+
for view in views:
|
138
|
+
# Generate model for each view (read-only)
|
139
|
+
model_content = self._generate_python_view_model(view)
|
140
|
+
model_filename = f"{self._to_snake_case(view.name)}_view.py"
|
141
|
+
model_path = output_dir / model_filename
|
142
|
+
|
143
|
+
with open(model_path, "w") as f:
|
144
|
+
f.write(model_content)
|
145
|
+
|
146
|
+
results["files_generated"].append(model_filename)
|
147
|
+
results["views_processed"].append(view.name)
|
148
|
+
|
149
|
+
# Add to imports
|
150
|
+
class_name = f"{self._to_pascal_case(view.name)}View"
|
151
|
+
model_imports.append(
|
152
|
+
f"from .{self._to_snake_case(view.name)}_view import {class_name}"
|
153
|
+
)
|
154
|
+
|
155
|
+
# Generate CinchModels container class
|
156
|
+
cinch_models_content = self._generate_cinch_models_class()
|
157
|
+
cinch_models_path = output_dir / "cinch_models.py"
|
158
|
+
with open(cinch_models_path, "w") as f:
|
159
|
+
f.write(cinch_models_content)
|
160
|
+
results["files_generated"].append("cinch_models.py")
|
161
|
+
|
162
|
+
# Write __init__.py with all imports and factory function
|
163
|
+
if model_imports:
|
164
|
+
# Import CinchModels
|
165
|
+
init_content.append("from .cinch_models import CinchModels")
|
166
|
+
init_content.extend(model_imports)
|
167
|
+
init_content.append("") # Empty line
|
168
|
+
|
169
|
+
# Create model registry
|
170
|
+
init_content.append("# Model registry for dynamic loading")
|
171
|
+
init_content.append("_MODEL_REGISTRY = {")
|
172
|
+
if include_tables:
|
173
|
+
for table_name in results["tables_processed"]:
|
174
|
+
class_name = self._to_pascal_case(table_name)
|
175
|
+
init_content.append(f" '{class_name}': {class_name},")
|
176
|
+
if include_views:
|
177
|
+
for view_name in results["views_processed"]:
|
178
|
+
class_name = f"{self._to_pascal_case(view_name)}View"
|
179
|
+
init_content.append(f" '{class_name}': {class_name},")
|
180
|
+
init_content.append("}")
|
181
|
+
init_content.append("")
|
182
|
+
|
183
|
+
# Add factory function
|
184
|
+
init_content.extend(
|
185
|
+
[
|
186
|
+
"def create_models(connection) -> CinchModels:",
|
187
|
+
' """Create unified models interface.',
|
188
|
+
"",
|
189
|
+
" Args:",
|
190
|
+
" connection: CinchDB instance (local or remote)",
|
191
|
+
"",
|
192
|
+
" Returns:",
|
193
|
+
" CinchModels container with all generated models",
|
194
|
+
' """',
|
195
|
+
" models = CinchModels(connection)",
|
196
|
+
" models._model_registry = _MODEL_REGISTRY",
|
197
|
+
" return models",
|
198
|
+
"",
|
199
|
+
]
|
200
|
+
)
|
201
|
+
|
202
|
+
# Add __all__ export
|
203
|
+
all_exports = ["'CinchModels'", "'create_models'"]
|
204
|
+
if include_tables:
|
205
|
+
for table_name in results["tables_processed"]:
|
206
|
+
all_exports.append(f'"{self._to_pascal_case(table_name)}"')
|
207
|
+
if include_views:
|
208
|
+
for view_name in results["views_processed"]:
|
209
|
+
all_exports.append(f'"{self._to_pascal_case(view_name)}View"')
|
210
|
+
|
211
|
+
init_content.append(f"__all__ = [{', '.join(all_exports)}]")
|
212
|
+
|
213
|
+
init_path = output_dir / "__init__.py"
|
214
|
+
with open(init_path, "w") as f:
|
215
|
+
f.write("\n".join(init_content))
|
216
|
+
|
217
|
+
results["files_generated"].append("__init__.py")
|
218
|
+
|
219
|
+
return results
|
220
|
+
|
221
|
+
def _generate_python_table_model(self, table: Table) -> str:
|
222
|
+
"""Generate Python Pydantic model for a table."""
|
223
|
+
class_name = self._to_pascal_case(table.name)
|
224
|
+
|
225
|
+
content = [
|
226
|
+
f'"""Generated model for {table.name} table."""',
|
227
|
+
"",
|
228
|
+
"from typing import Optional, List, ClassVar, Union",
|
229
|
+
"from datetime import datetime",
|
230
|
+
"from pathlib import Path",
|
231
|
+
"from pydantic import BaseModel, Field, ConfigDict",
|
232
|
+
"",
|
233
|
+
"from cinchdb.managers.data import DataManager",
|
234
|
+
"",
|
235
|
+
"",
|
236
|
+
f"class {class_name}(BaseModel):",
|
237
|
+
f' """Model for {table.name} table with CRUD operations."""',
|
238
|
+
" model_config = ConfigDict(",
|
239
|
+
" from_attributes=True,",
|
240
|
+
f' json_schema_extra={{"table_name": "{table.name}"}}',
|
241
|
+
" )",
|
242
|
+
"",
|
243
|
+
" # Class variables for database connection info",
|
244
|
+
" _data_manager: ClassVar[Optional[DataManager]] = None",
|
245
|
+
"",
|
246
|
+
]
|
247
|
+
|
248
|
+
# Generate fields for each column
|
249
|
+
for column in table.columns:
|
250
|
+
field_content = self._generate_python_field(column)
|
251
|
+
content.append(f" {field_content}")
|
252
|
+
|
253
|
+
content.extend(
|
254
|
+
[
|
255
|
+
"",
|
256
|
+
" @classmethod",
|
257
|
+
" def _get_data_manager(cls) -> DataManager:",
|
258
|
+
' """Get data manager instance (set by CinchModels container)."""',
|
259
|
+
" if cls._data_manager is None:",
|
260
|
+
' raise RuntimeError("Model not initialized. Access models through CinchModels container.")',
|
261
|
+
" return cls._data_manager",
|
262
|
+
"",
|
263
|
+
" @classmethod",
|
264
|
+
f" def select(cls, limit: Optional[int] = None, offset: Optional[int] = None, **filters) -> List['{class_name}']:",
|
265
|
+
' """Select records with optional filtering."""',
|
266
|
+
" return cls._get_data_manager().select(cls, limit=limit, offset=offset, **filters)",
|
267
|
+
"",
|
268
|
+
" @classmethod",
|
269
|
+
f" def find_by_id(cls, record_id: str) -> Optional['{class_name}']:",
|
270
|
+
' """Find a single record by ID."""',
|
271
|
+
" return cls._get_data_manager().find_by_id(cls, record_id)",
|
272
|
+
"",
|
273
|
+
" @classmethod",
|
274
|
+
f" def create(cls, **data) -> '{class_name}':",
|
275
|
+
' """Create a new record."""',
|
276
|
+
" instance = cls(**data)",
|
277
|
+
" return cls._get_data_manager().create(instance)",
|
278
|
+
"",
|
279
|
+
" @classmethod",
|
280
|
+
f" def bulk_create(cls, records: List[dict]) -> List['{class_name}']:",
|
281
|
+
' """Create multiple records in a single transaction."""',
|
282
|
+
" instances = [cls(**record) for record in records]",
|
283
|
+
" return cls._get_data_manager().bulk_create(instances)",
|
284
|
+
"",
|
285
|
+
" @classmethod",
|
286
|
+
" def count(cls, **filters) -> int:",
|
287
|
+
' """Count records with optional filtering."""',
|
288
|
+
" return cls._get_data_manager().count(cls, **filters)",
|
289
|
+
"",
|
290
|
+
" @classmethod",
|
291
|
+
" def delete_records(cls, **filters) -> int:",
|
292
|
+
' """Delete records matching filters."""',
|
293
|
+
" return cls._get_data_manager().delete(cls, **filters)",
|
294
|
+
"",
|
295
|
+
f" def save(self) -> '{class_name}':",
|
296
|
+
' """Save (upsert) this record."""',
|
297
|
+
" return self._get_data_manager().save(self)",
|
298
|
+
"",
|
299
|
+
f" def update(self) -> '{class_name}':",
|
300
|
+
' """Update this existing record."""',
|
301
|
+
" return self._get_data_manager().update(self)",
|
302
|
+
"",
|
303
|
+
" def delete(self) -> bool:",
|
304
|
+
' """Delete this record."""',
|
305
|
+
" if not self.id:",
|
306
|
+
' raise ValueError("Cannot delete record without ID")',
|
307
|
+
" return self._get_data_manager().delete_by_id(type(self), self.id)",
|
308
|
+
]
|
309
|
+
)
|
310
|
+
|
311
|
+
return "\n".join(content)
|
312
|
+
|
313
|
+
def _generate_python_view_model(self, view: View) -> str:
|
314
|
+
"""Generate Python Pydantic model for a view (read-only)."""
|
315
|
+
class_name = f"{self._to_pascal_case(view.name)}View"
|
316
|
+
|
317
|
+
# Get view schema by inspecting the view
|
318
|
+
columns = self._get_view_columns(view.name)
|
319
|
+
|
320
|
+
content = [
|
321
|
+
f'"""Generated model for {view.name} view."""',
|
322
|
+
"",
|
323
|
+
"from typing import Optional, Any",
|
324
|
+
"from pydantic import BaseModel, Field, ConfigDict",
|
325
|
+
"",
|
326
|
+
"",
|
327
|
+
f"class {class_name}(BaseModel):",
|
328
|
+
f' """Read-only model for {view.name} view."""',
|
329
|
+
" model_config = ConfigDict(",
|
330
|
+
" from_attributes=True,",
|
331
|
+
f' json_schema_extra={{"view_name": "{view.name}", "readonly": True}}',
|
332
|
+
" )",
|
333
|
+
"",
|
334
|
+
]
|
335
|
+
|
336
|
+
# Generate fields for each column (all Optional since we can't know schema exactly)
|
337
|
+
for column_name, column_type in columns.items():
|
338
|
+
python_type = self._sqlite_to_python_type(column_type)
|
339
|
+
content.append(
|
340
|
+
f" {column_name}: Optional[{python_type}] = Field(default=None)"
|
341
|
+
)
|
342
|
+
|
343
|
+
return "\n".join(content)
|
344
|
+
|
345
|
+
def _generate_python_field(self, column: Column) -> str:
|
346
|
+
"""Generate Python field definition for a column."""
|
347
|
+
python_type = self._sqlite_to_python_type(column.type, column.name)
|
348
|
+
|
349
|
+
# Handle nullable columns - all fields should be Optional except required business fields
|
350
|
+
# ID is always optional (auto-generated), timestamps are optional for model creation
|
351
|
+
if column.nullable or column.name in ["id", "created_at", "updated_at"]:
|
352
|
+
python_type = f"Optional[{python_type}]"
|
353
|
+
|
354
|
+
# Create field definition
|
355
|
+
field_parts = []
|
356
|
+
|
357
|
+
# Add description
|
358
|
+
field_parts.append(f'description="{column.name} field"')
|
359
|
+
|
360
|
+
# Add default values
|
361
|
+
if column.name == "id":
|
362
|
+
field_parts.append("default=None") # Auto-generated by DataManager
|
363
|
+
elif column.name == "created_at":
|
364
|
+
field_parts.append("default=None") # Set by DataManager on create
|
365
|
+
elif column.name == "updated_at":
|
366
|
+
field_parts.append("default=None") # Set by DataManager on create/update
|
367
|
+
elif column.nullable:
|
368
|
+
field_parts.append("default=None")
|
369
|
+
|
370
|
+
field_def = f"Field({', '.join(field_parts)})" if field_parts else "Field()"
|
371
|
+
|
372
|
+
return f"{column.name}: {python_type} = {field_def}"
|
373
|
+
|
374
|
+
def _generate_typescript_models(
|
375
|
+
self,
|
376
|
+
output_dir: Path,
|
377
|
+
include_tables: bool,
|
378
|
+
include_views: bool,
|
379
|
+
results: Dict[str, Any],
|
380
|
+
) -> Dict[str, Any]:
|
381
|
+
"""Generate TypeScript interface models."""
|
382
|
+
# TODO: Implement TypeScript generation
|
383
|
+
# For now, return placeholder
|
384
|
+
results["files_generated"].append("typescript_generation_todo.md")
|
385
|
+
|
386
|
+
placeholder_path = output_dir / "typescript_generation_todo.md"
|
387
|
+
with open(placeholder_path, "w") as f:
|
388
|
+
content = "# TypeScript Generation\n\nTypeScript model generation will be implemented in a future update.\n"
|
389
|
+
if include_tables:
|
390
|
+
content += "\nTables will be generated as TypeScript interfaces.\n"
|
391
|
+
if include_views:
|
392
|
+
content += (
|
393
|
+
"\nViews will be generated as read-only TypeScript interfaces.\n"
|
394
|
+
)
|
395
|
+
f.write(content)
|
396
|
+
|
397
|
+
return results
|
398
|
+
|
399
|
+
def _get_view_columns(self, view_name: str) -> Dict[str, str]:
|
400
|
+
"""Get column information for a view by querying PRAGMA."""
|
401
|
+
columns = {}
|
402
|
+
|
403
|
+
with DatabaseConnection(self.db_path) as conn:
|
404
|
+
try:
|
405
|
+
# Try to get view column info
|
406
|
+
cursor = conn.execute(f"PRAGMA table_info('{view_name}')")
|
407
|
+
for row in cursor.fetchall():
|
408
|
+
column_name = row["name"]
|
409
|
+
column_type = (
|
410
|
+
row["type"] or "TEXT"
|
411
|
+
) # Default to TEXT if type is empty
|
412
|
+
columns[column_name] = column_type.upper()
|
413
|
+
except Exception:
|
414
|
+
# If we can't get schema, use generic approach
|
415
|
+
columns = {"data": "TEXT"} # Fallback
|
416
|
+
|
417
|
+
return columns
|
418
|
+
|
419
|
+
def _sqlite_to_python_type(self, sqlite_type: str, column_name: str = "") -> str:
|
420
|
+
"""Convert SQLite type to Python type string."""
|
421
|
+
sqlite_type = sqlite_type.upper()
|
422
|
+
|
423
|
+
# Special case for timestamp fields
|
424
|
+
if column_name in ["created_at", "updated_at"]:
|
425
|
+
return "datetime"
|
426
|
+
|
427
|
+
if "INT" in sqlite_type:
|
428
|
+
return "int"
|
429
|
+
elif sqlite_type in ["REAL", "FLOAT", "DOUBLE"]:
|
430
|
+
return "float"
|
431
|
+
elif sqlite_type == "BLOB":
|
432
|
+
return "bytes"
|
433
|
+
elif "NUMERIC" in sqlite_type:
|
434
|
+
return "float" # Could be Decimal, but float is simpler
|
435
|
+
else:
|
436
|
+
# TEXT, VARCHAR, etc.
|
437
|
+
return "str"
|
438
|
+
|
439
|
+
def _to_snake_case(self, name: str) -> str:
|
440
|
+
"""Convert name to snake_case."""
|
441
|
+
import re
|
442
|
+
|
443
|
+
# Insert underscore before uppercase letters (except first)
|
444
|
+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
445
|
+
# Insert underscore before uppercase letters preceded by lowercase
|
446
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
447
|
+
|
448
|
+
def _to_pascal_case(self, name: str) -> str:
|
449
|
+
"""Convert name to PascalCase."""
|
450
|
+
# If already PascalCase, return as-is
|
451
|
+
if name and name[0].isupper() and "_" not in name and "-" not in name:
|
452
|
+
return name
|
453
|
+
|
454
|
+
# Split on underscores and capitalize each part
|
455
|
+
parts = name.replace("-", "_").split("_")
|
456
|
+
return "".join(word.capitalize() for word in parts if word)
|
457
|
+
|
458
|
+
def _generate_cinch_models_class(self) -> str:
|
459
|
+
"""Generate the CinchModels container class."""
|
460
|
+
content = [
|
461
|
+
'"""CinchModels container class for unified model access."""',
|
462
|
+
"",
|
463
|
+
"from typing import Dict, Any, Optional",
|
464
|
+
"from cinchdb.core.database import CinchDB",
|
465
|
+
"from cinchdb.managers.data import DataManager",
|
466
|
+
"",
|
467
|
+
"",
|
468
|
+
"class CinchModels:",
|
469
|
+
' """Unified interface for generated models."""',
|
470
|
+
"",
|
471
|
+
" def __init__(self, connection: CinchDB):",
|
472
|
+
' """Initialize with a CinchDB connection.',
|
473
|
+
"",
|
474
|
+
" Args:",
|
475
|
+
" connection: CinchDB instance (local or remote)",
|
476
|
+
' """',
|
477
|
+
" if not isinstance(connection, CinchDB):",
|
478
|
+
' raise TypeError("CinchModels requires a CinchDB connection instance")',
|
479
|
+
"",
|
480
|
+
" self._connection = connection",
|
481
|
+
" self._models = {} # Lazy loaded model cache",
|
482
|
+
" self._model_registry = {} # Map of model names to classes",
|
483
|
+
" self._tenant_override = None # Optional tenant override",
|
484
|
+
"",
|
485
|
+
" def __getattr__(self, name: str):",
|
486
|
+
' """Lazy load and return model class with connection set."""',
|
487
|
+
" if name not in self._models:",
|
488
|
+
" if name not in self._model_registry:",
|
489
|
+
" raise AttributeError(f\"Model '{name}' not found\")",
|
490
|
+
"",
|
491
|
+
" model_class = self._model_registry[name]",
|
492
|
+
"",
|
493
|
+
" # Determine tenant to use (override or connection default)",
|
494
|
+
" tenant = self._tenant_override or self._connection.tenant",
|
495
|
+
"",
|
496
|
+
" # Initialize model with connection context",
|
497
|
+
" if self._connection.is_local:",
|
498
|
+
" # Create DataManager for local connections",
|
499
|
+
" data_manager = DataManager(",
|
500
|
+
" self._connection.project_dir,",
|
501
|
+
" self._connection.database,",
|
502
|
+
" self._connection.branch,",
|
503
|
+
" tenant",
|
504
|
+
" )",
|
505
|
+
" model_class._data_manager = data_manager",
|
506
|
+
" else:",
|
507
|
+
" # Remote connections not yet supported in codegen",
|
508
|
+
' raise NotImplementedError("Remote connections not yet supported in generated models")',
|
509
|
+
"",
|
510
|
+
" self._models[name] = model_class",
|
511
|
+
"",
|
512
|
+
" return self._models[name]",
|
513
|
+
"",
|
514
|
+
" def with_tenant(self, tenant: str) -> 'CinchModels':",
|
515
|
+
' """Create models interface for a specific tenant with connection context override."""',
|
516
|
+
" # Create a new CinchModels with same connection but different tenant context",
|
517
|
+
" new_models = CinchModels(self._connection)",
|
518
|
+
" new_models._tenant_override = tenant",
|
519
|
+
" new_models._model_registry = self._model_registry",
|
520
|
+
" return new_models",
|
521
|
+
]
|
522
|
+
|
523
|
+
return "\n".join(content)
|