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
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()
@@ -0,0 +1,5 @@
1
+ """Core CinchDB functionality."""
2
+
3
+ from cinchdb.core.database import CinchDB, connect, connect_api
4
+
5
+ __all__ = ["CinchDB", "connect", "connect_api"]
@@ -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()