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/config.py
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
"""Configuration management for CinchDB projects."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional, Dict, Any
|
6
|
+
import toml
|
7
|
+
from pydantic import BaseModel, Field, ConfigDict
|
8
|
+
|
9
|
+
|
10
|
+
class RemoteConfig(BaseModel):
|
11
|
+
"""Configuration for a remote CinchDB instance."""
|
12
|
+
|
13
|
+
url: str = Field(description="Base URL of the remote CinchDB API")
|
14
|
+
key: str = Field(description="API key for authentication")
|
15
|
+
|
16
|
+
|
17
|
+
class ProjectConfig(BaseModel):
|
18
|
+
"""Configuration for a CinchDB project stored in .cinchdb/config.toml."""
|
19
|
+
|
20
|
+
model_config = ConfigDict(
|
21
|
+
extra="allow"
|
22
|
+
) # Allow additional fields for extensibility
|
23
|
+
|
24
|
+
active_database: str = Field(
|
25
|
+
default="main", description="Currently active database"
|
26
|
+
)
|
27
|
+
active_branch: str = Field(default="main", description="Currently active branch")
|
28
|
+
active_remote: Optional[str] = Field(
|
29
|
+
default=None, description="Currently active remote alias"
|
30
|
+
)
|
31
|
+
remotes: Dict[str, RemoteConfig] = Field(
|
32
|
+
default_factory=dict, description="Remote configurations by alias"
|
33
|
+
)
|
34
|
+
api_keys: Dict[str, Dict[str, Any]] = Field(
|
35
|
+
default_factory=dict, description="API key configurations (deprecated)"
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
class Config:
|
40
|
+
"""Manages CinchDB project configuration."""
|
41
|
+
|
42
|
+
def __init__(self, project_dir: Optional[Path] = None):
|
43
|
+
"""Initialize config manager.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
project_dir: Path to project directory. If None, uses CINCHDB_PROJECT_DIR env var or current directory.
|
47
|
+
"""
|
48
|
+
# Check environment variable first
|
49
|
+
if project_dir is None:
|
50
|
+
env_dir = os.environ.get("CINCHDB_PROJECT_DIR")
|
51
|
+
if env_dir:
|
52
|
+
project_dir = Path(env_dir)
|
53
|
+
|
54
|
+
self.project_dir = Path(project_dir) if project_dir else Path.cwd()
|
55
|
+
self.config_dir = self.project_dir / ".cinchdb"
|
56
|
+
self.config_path = self.config_dir / "config.toml"
|
57
|
+
self._config: Optional[ProjectConfig] = None
|
58
|
+
|
59
|
+
@property
|
60
|
+
def exists(self) -> bool:
|
61
|
+
"""Check if config file exists."""
|
62
|
+
return self.config_path.exists()
|
63
|
+
|
64
|
+
def load(self) -> ProjectConfig:
|
65
|
+
"""Load configuration from disk, with environment variable overrides."""
|
66
|
+
if not self.exists:
|
67
|
+
raise FileNotFoundError(f"Config file not found at {self.config_path}")
|
68
|
+
|
69
|
+
with open(self.config_path, "r") as f:
|
70
|
+
data = toml.load(f)
|
71
|
+
|
72
|
+
# Apply environment variable overrides
|
73
|
+
self._apply_env_overrides(data)
|
74
|
+
|
75
|
+
# Convert remote dicts to RemoteConfig objects
|
76
|
+
if "remotes" in data:
|
77
|
+
for alias, remote_data in data["remotes"].items():
|
78
|
+
if isinstance(remote_data, dict):
|
79
|
+
data["remotes"][alias] = RemoteConfig(**remote_data)
|
80
|
+
|
81
|
+
self._config = ProjectConfig(**data)
|
82
|
+
return self._config
|
83
|
+
|
84
|
+
def _apply_env_overrides(self, data: Dict[str, Any]) -> None:
|
85
|
+
"""Apply environment variable overrides to configuration data."""
|
86
|
+
# Override database and branch
|
87
|
+
if env_db := os.environ.get("CINCHDB_DATABASE"):
|
88
|
+
data["active_database"] = env_db
|
89
|
+
|
90
|
+
if env_branch := os.environ.get("CINCHDB_BRANCH"):
|
91
|
+
data["active_branch"] = env_branch
|
92
|
+
|
93
|
+
# Override or create remote configuration
|
94
|
+
env_url = os.environ.get("CINCHDB_REMOTE_URL")
|
95
|
+
env_key = os.environ.get("CINCHDB_API_KEY")
|
96
|
+
|
97
|
+
if env_url and env_key:
|
98
|
+
# Create or update "env" remote
|
99
|
+
if "remotes" not in data:
|
100
|
+
data["remotes"] = {}
|
101
|
+
|
102
|
+
data["remotes"]["env"] = {
|
103
|
+
"url": env_url.rstrip("/"), # Remove trailing slash
|
104
|
+
"key": env_key
|
105
|
+
}
|
106
|
+
|
107
|
+
# Make it active if no other remote is set
|
108
|
+
if not data.get("active_remote"):
|
109
|
+
data["active_remote"] = "env"
|
110
|
+
|
111
|
+
def save(self, config: Optional[ProjectConfig] = None) -> None:
|
112
|
+
"""Save configuration to disk.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
config: Configuration to save. If None, saves current config.
|
116
|
+
"""
|
117
|
+
if config:
|
118
|
+
self._config = config
|
119
|
+
|
120
|
+
if not self._config:
|
121
|
+
raise ValueError("No configuration to save")
|
122
|
+
|
123
|
+
# Ensure config directory exists
|
124
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
125
|
+
|
126
|
+
# Save to TOML - need to properly serialize RemoteConfig objects
|
127
|
+
config_dict = self._config.model_dump()
|
128
|
+
# Convert RemoteConfig objects to dicts for TOML serialization
|
129
|
+
if "remotes" in config_dict:
|
130
|
+
for alias, remote in config_dict["remotes"].items():
|
131
|
+
if isinstance(remote, dict):
|
132
|
+
config_dict["remotes"][alias] = remote
|
133
|
+
|
134
|
+
with open(self.config_path, "w") as f:
|
135
|
+
toml.dump(config_dict, f)
|
136
|
+
|
137
|
+
def init_project(self) -> ProjectConfig:
|
138
|
+
"""Initialize a new CinchDB project with default configuration."""
|
139
|
+
if self.exists:
|
140
|
+
raise FileExistsError(f"Project already exists at {self.config_dir}")
|
141
|
+
|
142
|
+
# Create default config
|
143
|
+
config = ProjectConfig()
|
144
|
+
self.save(config)
|
145
|
+
|
146
|
+
# Create default database structure
|
147
|
+
self._create_default_structure()
|
148
|
+
|
149
|
+
return config
|
150
|
+
|
151
|
+
def _create_default_structure(self) -> None:
|
152
|
+
"""Create default project structure."""
|
153
|
+
# Create main database with main branch
|
154
|
+
db_path = self.config_dir / "databases" / "main" / "branches" / "main"
|
155
|
+
db_path.mkdir(parents=True, exist_ok=True)
|
156
|
+
|
157
|
+
# Create metadata files
|
158
|
+
from datetime import datetime, timezone
|
159
|
+
|
160
|
+
metadata = {"created_at": datetime.now(timezone.utc).isoformat()}
|
161
|
+
|
162
|
+
import json
|
163
|
+
|
164
|
+
with open(db_path / "metadata.json", "w") as f:
|
165
|
+
json.dump(metadata, f, indent=2)
|
166
|
+
|
167
|
+
# Create empty changes file
|
168
|
+
with open(db_path / "changes.json", "w") as f:
|
169
|
+
json.dump([], f, indent=2)
|
170
|
+
|
171
|
+
# Create main tenant directory and database
|
172
|
+
tenant_dir = db_path / "tenants"
|
173
|
+
tenant_dir.mkdir(exist_ok=True)
|
174
|
+
|
175
|
+
# Create main tenant database file
|
176
|
+
main_db = tenant_dir / "main.db"
|
177
|
+
main_db.touch()
|
cinchdb/core/__init__.py
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
"""SQLite connection management for CinchDB."""
|
2
|
+
|
3
|
+
import sqlite3
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional, Dict, List
|
6
|
+
from contextlib import contextmanager
|
7
|
+
from datetime import datetime
|
8
|
+
|
9
|
+
|
10
|
+
# Custom datetime adapter and converter for SQLite
|
11
|
+
def adapt_datetime(dt):
|
12
|
+
"""Convert datetime to ISO 8601 string."""
|
13
|
+
return dt.isoformat()
|
14
|
+
|
15
|
+
|
16
|
+
def convert_datetime(val):
|
17
|
+
"""Convert ISO 8601 string to datetime."""
|
18
|
+
return datetime.fromisoformat(val.decode())
|
19
|
+
|
20
|
+
|
21
|
+
# Register the adapter and converter
|
22
|
+
sqlite3.register_adapter(datetime, adapt_datetime)
|
23
|
+
sqlite3.register_converter("TIMESTAMP", convert_datetime)
|
24
|
+
sqlite3.register_converter("DATETIME", convert_datetime)
|
25
|
+
|
26
|
+
|
27
|
+
class DatabaseConnection:
|
28
|
+
"""Manages a SQLite database connection with WAL mode."""
|
29
|
+
|
30
|
+
def __init__(self, path: Path):
|
31
|
+
"""Initialize database connection.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
path: Path to SQLite database file
|
35
|
+
"""
|
36
|
+
self.path = Path(path)
|
37
|
+
self._conn: Optional[sqlite3.Connection] = None
|
38
|
+
self._connect()
|
39
|
+
|
40
|
+
def _connect(self) -> None:
|
41
|
+
"""Establish database connection and configure WAL mode."""
|
42
|
+
# Ensure directory exists
|
43
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
44
|
+
|
45
|
+
# Connect with row factory for dict-like access
|
46
|
+
# detect_types=PARSE_DECLTYPES tells SQLite to use our registered converters
|
47
|
+
self._conn = sqlite3.connect(
|
48
|
+
str(self.path),
|
49
|
+
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
|
50
|
+
)
|
51
|
+
self._conn.row_factory = sqlite3.Row
|
52
|
+
|
53
|
+
# Configure WAL mode and settings
|
54
|
+
self._conn.execute("PRAGMA journal_mode = WAL")
|
55
|
+
self._conn.execute("PRAGMA synchronous = NORMAL")
|
56
|
+
self._conn.execute("PRAGMA wal_autocheckpoint = 0")
|
57
|
+
self._conn.execute("PRAGMA foreign_keys = ON")
|
58
|
+
self._conn.commit()
|
59
|
+
|
60
|
+
def execute(self, sql: str, params: Optional[tuple] = None) -> sqlite3.Cursor:
|
61
|
+
"""Execute a SQL statement.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
sql: SQL statement to execute
|
65
|
+
params: Optional parameters for parameterized queries
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
Cursor with results
|
69
|
+
"""
|
70
|
+
if not self._conn:
|
71
|
+
raise RuntimeError("Connection is closed")
|
72
|
+
|
73
|
+
if params:
|
74
|
+
return self._conn.execute(sql, params)
|
75
|
+
return self._conn.execute(sql)
|
76
|
+
|
77
|
+
def executemany(self, sql: str, params: List[tuple]) -> sqlite3.Cursor:
|
78
|
+
"""Execute a SQL statement multiple times with different parameters.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
sql: SQL statement to execute
|
82
|
+
params: List of parameter tuples
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
Cursor
|
86
|
+
"""
|
87
|
+
if not self._conn:
|
88
|
+
raise RuntimeError("Connection is closed")
|
89
|
+
|
90
|
+
return self._conn.executemany(sql, params)
|
91
|
+
|
92
|
+
@contextmanager
|
93
|
+
def transaction(self):
|
94
|
+
"""Context manager for database transactions.
|
95
|
+
|
96
|
+
Automatically commits on success or rolls back on exception.
|
97
|
+
"""
|
98
|
+
if not self._conn:
|
99
|
+
raise RuntimeError("Connection is closed")
|
100
|
+
|
101
|
+
try:
|
102
|
+
yield self
|
103
|
+
self._conn.commit()
|
104
|
+
except Exception:
|
105
|
+
self._conn.rollback()
|
106
|
+
raise
|
107
|
+
|
108
|
+
def commit(self) -> None:
|
109
|
+
"""Commit the current transaction."""
|
110
|
+
if self._conn:
|
111
|
+
self._conn.commit()
|
112
|
+
|
113
|
+
def rollback(self) -> None:
|
114
|
+
"""Rollback the current transaction."""
|
115
|
+
if self._conn:
|
116
|
+
self._conn.rollback()
|
117
|
+
|
118
|
+
def close(self) -> None:
|
119
|
+
"""Close the database connection."""
|
120
|
+
if self._conn:
|
121
|
+
self._conn.close()
|
122
|
+
self._conn = None
|
123
|
+
|
124
|
+
def __enter__(self):
|
125
|
+
"""Enter context manager."""
|
126
|
+
return self
|
127
|
+
|
128
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
129
|
+
"""Exit context manager."""
|
130
|
+
# Parameters are required by context manager protocol but not used
|
131
|
+
_ = (exc_type, exc_val, exc_tb)
|
132
|
+
self.close()
|
133
|
+
return False
|
134
|
+
|
135
|
+
|
136
|
+
class ConnectionPool:
|
137
|
+
"""Manages a pool of database connections."""
|
138
|
+
|
139
|
+
def __init__(self):
|
140
|
+
"""Initialize connection pool."""
|
141
|
+
self._connections: Dict[Path, DatabaseConnection] = {}
|
142
|
+
|
143
|
+
def get_connection(self, path: Path) -> DatabaseConnection:
|
144
|
+
"""Get or create a connection for the given path.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
path: Database file path
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Database connection
|
151
|
+
"""
|
152
|
+
path = Path(path).resolve()
|
153
|
+
|
154
|
+
if path not in self._connections:
|
155
|
+
self._connections[path] = DatabaseConnection(path)
|
156
|
+
|
157
|
+
return self._connections[path]
|
158
|
+
|
159
|
+
def close_connection(self, path: Path) -> None:
|
160
|
+
"""Close and remove a specific connection.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
path: Database file path
|
164
|
+
"""
|
165
|
+
path = Path(path).resolve()
|
166
|
+
|
167
|
+
if path in self._connections:
|
168
|
+
self._connections[path].close()
|
169
|
+
del self._connections[path]
|
170
|
+
|
171
|
+
def close_all(self) -> None:
|
172
|
+
"""Close all connections in the pool."""
|
173
|
+
for conn in self._connections.values():
|
174
|
+
conn.close()
|
175
|
+
self._connections.clear()
|