cinchdb 0.1.1__py3-none-any.whl → 0.1.3__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/utils/name_validator.py +6 -6
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/METADATA +16 -18
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/RECORD +6 -22
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/entry_points.txt +0 -1
- cinchdb/api/__init__.py +0 -5
- cinchdb/api/app.py +0 -76
- cinchdb/api/auth.py +0 -290
- cinchdb/api/main.py +0 -137
- cinchdb/api/routers/__init__.py +0 -25
- cinchdb/api/routers/auth.py +0 -135
- cinchdb/api/routers/branches.py +0 -368
- cinchdb/api/routers/codegen.py +0 -164
- cinchdb/api/routers/columns.py +0 -290
- cinchdb/api/routers/data.py +0 -479
- cinchdb/api/routers/databases.py +0 -184
- cinchdb/api/routers/projects.py +0 -133
- cinchdb/api/routers/query.py +0 -156
- cinchdb/api/routers/tables.py +0 -349
- cinchdb/api/routers/tenants.py +0 -216
- cinchdb/api/routers/views.py +0 -219
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.1.dist-info → cinchdb-0.1.3.dist-info}/licenses/LICENSE +0 -0
cinchdb/utils/name_validator.py
CHANGED
@@ -9,7 +9,7 @@ from typing import Optional
|
|
9
9
|
|
10
10
|
|
11
11
|
# Regex pattern for valid names: lowercase letters, numbers, dash, underscore, period
|
12
|
-
VALID_NAME_PATTERN = re.compile(r'^[a-z0-9][a-z0-9
|
12
|
+
VALID_NAME_PATTERN = re.compile(r'^[a-z0-9][a-z0-9\-_\.]*[a-z0-9]$|^[a-z0-9]$')
|
13
13
|
|
14
14
|
# Reserved names that cannot be used
|
15
15
|
RESERVED_NAMES = {'con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4',
|
@@ -26,7 +26,7 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
|
|
26
26
|
"""Validate that a name meets CinchDB naming requirements.
|
27
27
|
|
28
28
|
Valid names must:
|
29
|
-
- Contain only lowercase letters (a-z), numbers (0-9), dash (-), underscore (_)
|
29
|
+
- Contain only lowercase letters (a-z), numbers (0-9), dash (-), and underscore (_)
|
30
30
|
- Start and end with alphanumeric characters
|
31
31
|
- Be at least 1 character long
|
32
32
|
- Not exceed 255 characters (filesystem limit)
|
@@ -64,7 +64,7 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
|
|
64
64
|
)
|
65
65
|
|
66
66
|
# Check for consecutive special characters
|
67
|
-
if '
|
67
|
+
if '--' in name or '__' in name or '-_' in name or '_-' in name or '..' in name or '.-' in name or '-.' in name or '._' in name or '_.' in name:
|
68
68
|
raise InvalidNameError(
|
69
69
|
f"Invalid {entity_type} name '{name}'. "
|
70
70
|
f"Names cannot contain consecutive special characters."
|
@@ -102,13 +102,13 @@ def clean_name(name: str) -> str:
|
|
102
102
|
cleaned = cleaned.replace(' ', '-')
|
103
103
|
|
104
104
|
# Remove invalid characters
|
105
|
-
cleaned = re.sub(r'[^a-z0-9
|
105
|
+
cleaned = re.sub(r'[^a-z0-9\-_\.]', '', cleaned)
|
106
106
|
|
107
107
|
# Remove consecutive special characters
|
108
|
-
cleaned = re.sub(r'[
|
108
|
+
cleaned = re.sub(r'[-_\.]{2,}', '-', cleaned)
|
109
109
|
|
110
110
|
# Remove leading/trailing special characters
|
111
|
-
cleaned = cleaned.strip('
|
111
|
+
cleaned = cleaned.strip('-_.')
|
112
112
|
|
113
113
|
return cleaned
|
114
114
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cinchdb
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.3
|
4
4
|
Summary: A Git-like SQLite database management system with branching and multi-tenancy
|
5
5
|
Project-URL: Homepage, https://github.com/russellromney/cinchdb
|
6
6
|
Project-URL: Documentation, https://russellromney.github.io/cinchdb
|
@@ -18,14 +18,11 @@ Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
20
20
|
Requires-Python: >=3.10
|
21
|
-
Requires-Dist: fastapi>=0.115.0
|
22
21
|
Requires-Dist: pydantic>=2.0.0
|
23
|
-
Requires-Dist: python-multipart>=0.0.12
|
24
22
|
Requires-Dist: requests>=2.28.0
|
25
23
|
Requires-Dist: rich>=13.0.0
|
26
24
|
Requires-Dist: toml>=0.10.0
|
27
25
|
Requires-Dist: typer>=0.9.0
|
28
|
-
Requires-Dist: uvicorn>=0.32.0
|
29
26
|
Description-Content-Type: text/markdown
|
30
27
|
|
31
28
|
# CinchDB
|
@@ -38,7 +35,7 @@ CinchDB is for projects that need fast queries, data isolated data per-tenant [o
|
|
38
35
|
|
39
36
|
On a meta level, I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
|
40
37
|
|
41
|
-
Because it's so lightweight and its only dependencies are
|
38
|
+
Because it's so lightweight and its only dependencies are pydantic, requests, and Typer, it makes for a perfect local development database that can be controlled programmatically.
|
42
39
|
|
43
40
|
|
44
41
|
```bash
|
@@ -61,9 +58,9 @@ cinch branch merge-into-main feature
|
|
61
58
|
cinch tenant create customer_a
|
62
59
|
cinch query "SELECT * FROM users" --tenant customer_a
|
63
60
|
|
64
|
-
#
|
65
|
-
|
66
|
-
cinch
|
61
|
+
# Connect to remote CinchDB instance
|
62
|
+
cinch remote add production https://your-cinchdb-server.com your-api-key
|
63
|
+
cinch remote use production
|
67
64
|
|
68
65
|
# Autogenerate Python SDK from database
|
69
66
|
cinch codegen generate python cinchdb_models/
|
@@ -77,9 +74,9 @@ CinchDB combines SQLite with Git-like workflows for database schema management:
|
|
77
74
|
- **Multi-tenant isolation** - shared schema, isolated data per tenant
|
78
75
|
- **Automatic change tracking** - all schema changes tracked and mergeable
|
79
76
|
- **Safe structure changes** - change merges happen atomically with zero rollback risk (seriously)
|
80
|
-
- **Remote
|
77
|
+
- **Remote connectivity** - Connect to hosted CinchDB instances
|
81
78
|
- **Type-safe SDK** - Python and TypeScript SDKs with full type safety
|
82
|
-
- **
|
79
|
+
- **Remote-capable** - CLI and SDK can connect to remote instances
|
83
80
|
- **SDK generation from database schema** - Generate a typesafe SDK from your database models for CRUD operations
|
84
81
|
|
85
82
|
## Installation
|
@@ -136,24 +133,25 @@ post_id = db.insert("posts", {"title": "Hello World", "content": "First post"})
|
|
136
133
|
db.update("posts", post_id, {"content": "Updated content"})
|
137
134
|
```
|
138
135
|
|
139
|
-
### Remote
|
136
|
+
### Remote Connection
|
140
137
|
|
141
138
|
```python
|
142
|
-
# Connect to remote
|
143
|
-
db = cinchdb.
|
139
|
+
# Connect to remote instance
|
140
|
+
db = cinchdb.connect("myapp", url="https://your-cinchdb-server.com", api_key="your-api-key")
|
144
141
|
|
145
142
|
# Same interface as local
|
146
143
|
results = db.query("SELECT * FROM users")
|
147
144
|
user_id = db.insert("users", {"username": "alice", "email": "alice@example.com"})
|
148
145
|
```
|
149
146
|
|
150
|
-
##
|
147
|
+
## Remote Access
|
151
148
|
|
152
|
-
|
149
|
+
Connect to a remote CinchDB instance:
|
153
150
|
|
154
151
|
```bash
|
155
|
-
cinch-server
|
156
|
-
|
152
|
+
cinch remote add production https://your-cinchdb-server.com your-api-key
|
153
|
+
cinch remote use production
|
154
|
+
# Now all commands will use the remote instance
|
157
155
|
```
|
158
156
|
|
159
157
|
Interactive docs at `/docs`, health check at `/health`.
|
@@ -162,7 +160,7 @@ Interactive docs at `/docs`, health check at `/health`.
|
|
162
160
|
|
163
161
|
- **Python SDK**: Core functionality (local + remote)
|
164
162
|
- **CLI**: Full-featured command-line interface
|
165
|
-
- **
|
163
|
+
- **Remote Access**: Connect to hosted CinchDB instances
|
166
164
|
- **TypeScript SDK**: Browser and Node.js client
|
167
165
|
|
168
166
|
## Development
|
@@ -1,22 +1,6 @@
|
|
1
1
|
cinchdb/__init__.py,sha256=NZdSzfhRguSBTjJ2dcESOQYy53OZEuBndlB7U08GMY0,179
|
2
2
|
cinchdb/__main__.py,sha256=OpkDqn9zkTZhhYgvv_grswWLAHKbmxs4M-8C6Z5HfWY,85
|
3
3
|
cinchdb/config.py,sha256=Exzf0hCJgcg0PRRQz8EG7XFipx6fIVKO7IsQhpc0Q4o,6187
|
4
|
-
cinchdb/api/__init__.py,sha256=5AHDlOXOxxzyI8pW7puntIILp8E7HOy4D62odLN518w,79
|
5
|
-
cinchdb/api/app.py,sha256=iL9tYWYpJAAOzH9UHNbpl82Ibwz_zBCqxDXqARmxjbc,2172
|
6
|
-
cinchdb/api/auth.py,sha256=uxRWWRjE6sQWNPH6BBsdHT_PlWN_iYr_hh76tVG9fTY,8167
|
7
|
-
cinchdb/api/main.py,sha256=FpqhO7zTgh-r6y14mL0Cp-fGQ0TMmxPKCF7cNjDiCcw,4509
|
8
|
-
cinchdb/api/routers/__init__.py,sha256=o4xLUjKRKYnBU-RQiDXQotKml0lZEwIudU5S5bW4gt8,323
|
9
|
-
cinchdb/api/routers/auth.py,sha256=6Si_iQtDwj487Hv87AoV4vMWAJFnvd5JsFXghYqA7ao,3842
|
10
|
-
cinchdb/api/routers/branches.py,sha256=gEYBDqvwSzt5uSSVDo_mo2ysLfUqaavyDClpzRfYTyA,11537
|
11
|
-
cinchdb/api/routers/codegen.py,sha256=As9zfBmT7SMx1lrQfRg8TGofcRSOo_r77fCLmxuYkI4,5069
|
12
|
-
cinchdb/api/routers/columns.py,sha256=bWMIInp0Ma9KPp7PHFSr25ROWNKjOkwl9KNc3bQeqMo,8313
|
13
|
-
cinchdb/api/routers/data.py,sha256=rYnh6TwgLArA85AalOydg4txoXD0UWEItAvISuRVTao,14923
|
14
|
-
cinchdb/api/routers/databases.py,sha256=RyetGWqlnW7H7sCB4hOqWIPz-6cmJfI3f3KsIb4IX40,5282
|
15
|
-
cinchdb/api/routers/projects.py,sha256=70nwkXjGiK_pfTr6hv50viOjvQgv5yfC50mEgTByClE,3847
|
16
|
-
cinchdb/api/routers/query.py,sha256=UEtdKLIeo5OjOHfJl3IU3q2o69NOAX0pCyOwb7ivquQ,5120
|
17
|
-
cinchdb/api/routers/tables.py,sha256=Dg0ANMEVHXZyIO-mJBQCnJKfdRORKFqKaunLfJ_cTlo,10403
|
18
|
-
cinchdb/api/routers/tenants.py,sha256=1xukbM5avvkr1CMuq1pr78CNyE_9xk2Yg35mZFDvl-c,5692
|
19
|
-
cinchdb/api/routers/views.py,sha256=pzovkLttjEAg0ecvPwaN5qFgBqNhD0_g0sqq9UNYAmc,6051
|
20
4
|
cinchdb/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
5
|
cinchdb/cli/main.py,sha256=_vBSigtAcYgGBB4qRt2p3ZH8QZ4Osk5rLRe5cOD5ryU,4335
|
22
6
|
cinchdb/cli/utils.py,sha256=Alh3plAiVOGSk_ETqTmh2rYHHFULelizsQOR4e-_KJw,5661
|
@@ -60,10 +44,10 @@ cinchdb/models/table.py,sha256=s6BGoHDuA-yzqQL9papRTVT2WcHjuY-SnoanGFDlFIE,2793
|
|
60
44
|
cinchdb/models/tenant.py,sha256=Jut8qoHX79OG3zDNXAdDqzGmlGiV8trGe3t5kXbFt1E,967
|
61
45
|
cinchdb/models/view.py,sha256=q6j-jYzFJuhRJO87rKt6Uv8hOizHQx8xwoPKoH6XnNY,530
|
62
46
|
cinchdb/utils/__init__.py,sha256=1mBU1H2C9urYA8Z_v5BTdAfDe0mQ6GMAK0AM3zRPv5k,487
|
63
|
-
cinchdb/utils/name_validator.py,sha256=
|
47
|
+
cinchdb/utils/name_validator.py,sha256=GgzN7Z5XoEIt595n4_6TWwZQZegVNRIB4evzg3r67Rg,4010
|
64
48
|
cinchdb/utils/sql_validator.py,sha256=7STxsVO7bD4gZ8mfimQSt4_Yfckw62plUS_X_xJ48Vo,5427
|
65
|
-
cinchdb-0.1.
|
66
|
-
cinchdb-0.1.
|
67
|
-
cinchdb-0.1.
|
68
|
-
cinchdb-0.1.
|
69
|
-
cinchdb-0.1.
|
49
|
+
cinchdb-0.1.3.dist-info/METADATA,sha256=QYfexlMLVbnspKmQKADSVU3XKLMtUpITicgS5o1fWBE,5802
|
50
|
+
cinchdb-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
51
|
+
cinchdb-0.1.3.dist-info/entry_points.txt,sha256=VBOIzvnGbkKudMCCmNORS3885QSyjZUVKJQ-Syqa62w,47
|
52
|
+
cinchdb-0.1.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
53
|
+
cinchdb-0.1.3.dist-info/RECORD,,
|
cinchdb/api/__init__.py
DELETED
cinchdb/api/app.py
DELETED
@@ -1,76 +0,0 @@
|
|
1
|
-
"""FastAPI application for CinchDB."""
|
2
|
-
|
3
|
-
from fastapi import FastAPI
|
4
|
-
from fastapi.middleware.cors import CORSMiddleware
|
5
|
-
from contextlib import asynccontextmanager
|
6
|
-
|
7
|
-
from cinchdb import __version__
|
8
|
-
from cinchdb.api.routers import (
|
9
|
-
auth,
|
10
|
-
projects,
|
11
|
-
databases,
|
12
|
-
branches,
|
13
|
-
tenants,
|
14
|
-
tables,
|
15
|
-
columns,
|
16
|
-
views,
|
17
|
-
query,
|
18
|
-
data,
|
19
|
-
codegen,
|
20
|
-
)
|
21
|
-
|
22
|
-
|
23
|
-
@asynccontextmanager
|
24
|
-
async def lifespan(app: FastAPI):
|
25
|
-
"""Application lifespan events."""
|
26
|
-
# Startup
|
27
|
-
print(f"Starting CinchDB API v{__version__}")
|
28
|
-
yield
|
29
|
-
# Shutdown
|
30
|
-
print("Shutting down CinchDB API")
|
31
|
-
|
32
|
-
|
33
|
-
# Create FastAPI app
|
34
|
-
app = FastAPI(
|
35
|
-
title="CinchDB API",
|
36
|
-
description="Git-like SQLite database management system",
|
37
|
-
version=__version__,
|
38
|
-
lifespan=lifespan,
|
39
|
-
docs_url="/docs",
|
40
|
-
redoc_url="/redoc",
|
41
|
-
openapi_url="/openapi.json",
|
42
|
-
)
|
43
|
-
|
44
|
-
# Configure CORS
|
45
|
-
app.add_middleware(
|
46
|
-
CORSMiddleware,
|
47
|
-
allow_origins=["*"], # Configure appropriately for production
|
48
|
-
allow_credentials=True,
|
49
|
-
allow_methods=["*"],
|
50
|
-
allow_headers=["*"],
|
51
|
-
)
|
52
|
-
|
53
|
-
# Include routers
|
54
|
-
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
|
55
|
-
app.include_router(projects.router, prefix="/api/v1/projects", tags=["projects"])
|
56
|
-
app.include_router(databases.router, prefix="/api/v1/databases", tags=["databases"])
|
57
|
-
app.include_router(branches.router, prefix="/api/v1/branches", tags=["branches"])
|
58
|
-
app.include_router(tenants.router, prefix="/api/v1/tenants", tags=["tenants"])
|
59
|
-
app.include_router(tables.router, prefix="/api/v1/tables", tags=["tables"])
|
60
|
-
app.include_router(columns.router, prefix="/api/v1/columns", tags=["columns"])
|
61
|
-
app.include_router(views.router, prefix="/api/v1/views", tags=["views"])
|
62
|
-
app.include_router(query.router, prefix="/api/v1/query", tags=["query"])
|
63
|
-
app.include_router(data.router, prefix="/api/v1/tables", tags=["data"])
|
64
|
-
app.include_router(codegen.router, prefix="/api/v1/codegen", tags=["codegen"])
|
65
|
-
|
66
|
-
|
67
|
-
@app.get("/")
|
68
|
-
async def root():
|
69
|
-
"""Root endpoint."""
|
70
|
-
return {"name": "CinchDB API", "version": __version__, "status": "running"}
|
71
|
-
|
72
|
-
|
73
|
-
@app.get("/health")
|
74
|
-
async def health_check():
|
75
|
-
"""Health check endpoint."""
|
76
|
-
return {"status": "healthy"}
|
cinchdb/api/auth.py
DELETED
@@ -1,290 +0,0 @@
|
|
1
|
-
"""Authentication and API key management for CinchDB API."""
|
2
|
-
|
3
|
-
import uuid
|
4
|
-
from datetime import datetime, timezone
|
5
|
-
from typing import Optional, List, Literal
|
6
|
-
from pathlib import Path
|
7
|
-
|
8
|
-
from fastapi import HTTPException, Security, Depends, Query
|
9
|
-
from fastapi.security import APIKeyHeader
|
10
|
-
from pydantic import BaseModel, Field
|
11
|
-
|
12
|
-
from cinchdb.config import Config
|
13
|
-
from cinchdb.core.path_utils import get_project_root
|
14
|
-
|
15
|
-
|
16
|
-
# API Key header
|
17
|
-
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
18
|
-
|
19
|
-
|
20
|
-
class APIKey(BaseModel):
|
21
|
-
"""API key model."""
|
22
|
-
|
23
|
-
key: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
24
|
-
name: str
|
25
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
26
|
-
permissions: Literal["read", "write"] = "read"
|
27
|
-
branches: Optional[List[str]] = None # None means all branches
|
28
|
-
active: bool = True
|
29
|
-
|
30
|
-
|
31
|
-
class APIKeyManager:
|
32
|
-
"""Manages API keys in the project configuration."""
|
33
|
-
|
34
|
-
def __init__(self, project_dir: Optional[Path] = None):
|
35
|
-
"""Initialize API key manager.
|
36
|
-
|
37
|
-
Args:
|
38
|
-
project_dir: Project directory path
|
39
|
-
"""
|
40
|
-
self.project_dir = project_dir or Path.cwd()
|
41
|
-
self.config = Config(self.project_dir)
|
42
|
-
|
43
|
-
def create_key(
|
44
|
-
self,
|
45
|
-
name: str,
|
46
|
-
permissions: Literal["read", "write"] = "read",
|
47
|
-
branches: Optional[List[str]] = None,
|
48
|
-
) -> APIKey:
|
49
|
-
"""Create a new API key.
|
50
|
-
|
51
|
-
Args:
|
52
|
-
name: Name for the API key
|
53
|
-
permissions: Permission level (read or write)
|
54
|
-
branches: List of allowed branches (None for all)
|
55
|
-
|
56
|
-
Returns:
|
57
|
-
Created API key
|
58
|
-
"""
|
59
|
-
api_key = APIKey(name=name, permissions=permissions, branches=branches)
|
60
|
-
|
61
|
-
# Load config and add key
|
62
|
-
config_data = self.config.load()
|
63
|
-
if not config_data.api_keys:
|
64
|
-
config_data.api_keys = {}
|
65
|
-
|
66
|
-
config_data.api_keys[api_key.key] = {
|
67
|
-
"name": api_key.name,
|
68
|
-
"created_at": api_key.created_at.isoformat(),
|
69
|
-
"permissions": api_key.permissions,
|
70
|
-
"branches": api_key.branches,
|
71
|
-
"active": api_key.active,
|
72
|
-
}
|
73
|
-
|
74
|
-
self.config.save(config_data)
|
75
|
-
return api_key
|
76
|
-
|
77
|
-
def get_key(self, key: str) -> Optional[APIKey]:
|
78
|
-
"""Get an API key by its value.
|
79
|
-
|
80
|
-
Args:
|
81
|
-
key: API key string
|
82
|
-
|
83
|
-
Returns:
|
84
|
-
API key if found and active, None otherwise
|
85
|
-
"""
|
86
|
-
config_data = self.config.load()
|
87
|
-
if not config_data.api_keys or key not in config_data.api_keys:
|
88
|
-
return None
|
89
|
-
|
90
|
-
key_data = config_data.api_keys[key]
|
91
|
-
if not key_data.get("active", True):
|
92
|
-
return None
|
93
|
-
|
94
|
-
return APIKey(
|
95
|
-
key=key,
|
96
|
-
name=key_data["name"],
|
97
|
-
created_at=datetime.fromisoformat(key_data["created_at"]),
|
98
|
-
permissions=key_data.get("permissions", "read"),
|
99
|
-
branches=key_data.get("branches"),
|
100
|
-
active=key_data.get("active", True),
|
101
|
-
)
|
102
|
-
|
103
|
-
def list_keys(self) -> List[APIKey]:
|
104
|
-
"""List all API keys.
|
105
|
-
|
106
|
-
Returns:
|
107
|
-
List of API keys
|
108
|
-
"""
|
109
|
-
config_data = self.config.load()
|
110
|
-
if not config_data.api_keys:
|
111
|
-
return []
|
112
|
-
|
113
|
-
keys = []
|
114
|
-
for key, key_data in config_data.api_keys.items():
|
115
|
-
keys.append(
|
116
|
-
APIKey(
|
117
|
-
key=key,
|
118
|
-
name=key_data["name"],
|
119
|
-
created_at=datetime.fromisoformat(key_data["created_at"]),
|
120
|
-
permissions=key_data.get("permissions", "read"),
|
121
|
-
branches=key_data.get("branches"),
|
122
|
-
active=key_data.get("active", True),
|
123
|
-
)
|
124
|
-
)
|
125
|
-
|
126
|
-
return keys
|
127
|
-
|
128
|
-
def revoke_key(self, key: str) -> bool:
|
129
|
-
"""Revoke an API key.
|
130
|
-
|
131
|
-
Args:
|
132
|
-
key: API key to revoke
|
133
|
-
|
134
|
-
Returns:
|
135
|
-
True if revoked, False if not found
|
136
|
-
"""
|
137
|
-
config_data = self.config.load()
|
138
|
-
if not config_data.api_keys or key not in config_data.api_keys:
|
139
|
-
return False
|
140
|
-
|
141
|
-
config_data.api_keys[key]["active"] = False
|
142
|
-
self.config.save(config_data)
|
143
|
-
return True
|
144
|
-
|
145
|
-
def delete_key(self, key: str) -> bool:
|
146
|
-
"""Delete an API key.
|
147
|
-
|
148
|
-
Args:
|
149
|
-
key: API key to delete
|
150
|
-
|
151
|
-
Returns:
|
152
|
-
True if deleted, False if not found
|
153
|
-
"""
|
154
|
-
config_data = self.config.load()
|
155
|
-
if not config_data.api_keys or key not in config_data.api_keys:
|
156
|
-
return False
|
157
|
-
|
158
|
-
del config_data.api_keys[key]
|
159
|
-
self.config.save(config_data)
|
160
|
-
return True
|
161
|
-
|
162
|
-
|
163
|
-
class AuthContext(BaseModel):
|
164
|
-
"""Authentication context for requests."""
|
165
|
-
|
166
|
-
api_key: APIKey
|
167
|
-
project_dir: Path
|
168
|
-
|
169
|
-
|
170
|
-
async def get_current_project(project_dir: Optional[str] = None) -> Path:
|
171
|
-
"""Get the current project directory.
|
172
|
-
|
173
|
-
Args:
|
174
|
-
project_dir: Optional project directory path
|
175
|
-
|
176
|
-
Returns:
|
177
|
-
Project directory path
|
178
|
-
|
179
|
-
Raises:
|
180
|
-
HTTPException: If project not found
|
181
|
-
"""
|
182
|
-
if project_dir:
|
183
|
-
path = Path(project_dir)
|
184
|
-
if not path.exists() or not (path / ".cinchdb").exists():
|
185
|
-
raise HTTPException(status_code=404, detail="Project not found")
|
186
|
-
return path
|
187
|
-
|
188
|
-
# Try to find project from current directory
|
189
|
-
project_root = get_project_root(Path.cwd())
|
190
|
-
if not project_root:
|
191
|
-
raise HTTPException(
|
192
|
-
status_code=404,
|
193
|
-
detail="No CinchDB project found. Specify project_dir parameter.",
|
194
|
-
)
|
195
|
-
|
196
|
-
return project_root
|
197
|
-
|
198
|
-
|
199
|
-
async def verify_api_key(
|
200
|
-
api_key_header: Optional[str] = Security(api_key_header),
|
201
|
-
api_key_query: Optional[str] = Query(None, alias="api_key"),
|
202
|
-
project_dir: Path = Depends(get_current_project),
|
203
|
-
) -> AuthContext:
|
204
|
-
"""Verify API key and return auth context.
|
205
|
-
|
206
|
-
Args:
|
207
|
-
api_key_header: API key from header
|
208
|
-
api_key_query: API key from query parameter
|
209
|
-
project_dir: Project directory
|
210
|
-
|
211
|
-
Returns:
|
212
|
-
Authentication context
|
213
|
-
|
214
|
-
Raises:
|
215
|
-
HTTPException: If authentication fails
|
216
|
-
"""
|
217
|
-
# Check header first, then query parameter
|
218
|
-
api_key = api_key_header or api_key_query
|
219
|
-
|
220
|
-
if not api_key:
|
221
|
-
raise HTTPException(
|
222
|
-
status_code=401,
|
223
|
-
detail="API key required",
|
224
|
-
headers={"WWW-Authenticate": "ApiKey"},
|
225
|
-
)
|
226
|
-
|
227
|
-
manager = APIKeyManager(project_dir)
|
228
|
-
key_obj = manager.get_key(api_key)
|
229
|
-
|
230
|
-
if not key_obj:
|
231
|
-
raise HTTPException(
|
232
|
-
status_code=401,
|
233
|
-
detail="Invalid API key",
|
234
|
-
headers={"WWW-Authenticate": "ApiKey"},
|
235
|
-
)
|
236
|
-
|
237
|
-
return AuthContext(api_key=key_obj, project_dir=project_dir)
|
238
|
-
|
239
|
-
|
240
|
-
async def require_write_permission(
|
241
|
-
auth: AuthContext = Depends(verify_api_key), branch: Optional[str] = None
|
242
|
-
) -> AuthContext:
|
243
|
-
"""Require write permission for the request.
|
244
|
-
|
245
|
-
Args:
|
246
|
-
auth: Authentication context
|
247
|
-
branch: Optional branch name to check
|
248
|
-
|
249
|
-
Returns:
|
250
|
-
Authentication context
|
251
|
-
|
252
|
-
Raises:
|
253
|
-
HTTPException: If permission denied
|
254
|
-
"""
|
255
|
-
if auth.api_key.permissions != "write":
|
256
|
-
raise HTTPException(status_code=403, detail="Write permission required")
|
257
|
-
|
258
|
-
# Check branch-specific permissions
|
259
|
-
if branch and auth.api_key.branches and branch not in auth.api_key.branches:
|
260
|
-
raise HTTPException(
|
261
|
-
status_code=403, detail=f"Access denied for branch '{branch}'"
|
262
|
-
)
|
263
|
-
|
264
|
-
return auth
|
265
|
-
|
266
|
-
|
267
|
-
async def require_read_permission(
|
268
|
-
auth: AuthContext = Depends(verify_api_key), branch: Optional[str] = None
|
269
|
-
) -> AuthContext:
|
270
|
-
"""Require read permission for the request.
|
271
|
-
|
272
|
-
Args:
|
273
|
-
auth: Authentication context
|
274
|
-
branch: Optional branch name to check
|
275
|
-
|
276
|
-
Returns:
|
277
|
-
Authentication context
|
278
|
-
|
279
|
-
Raises:
|
280
|
-
HTTPException: If permission denied
|
281
|
-
"""
|
282
|
-
# All valid API keys have at least read permission
|
283
|
-
|
284
|
-
# Check branch-specific permissions
|
285
|
-
if branch and auth.api_key.branches and branch not in auth.api_key.branches:
|
286
|
-
raise HTTPException(
|
287
|
-
status_code=403, detail=f"Access denied for branch '{branch}'"
|
288
|
-
)
|
289
|
-
|
290
|
-
return auth
|