Skip to content

Commit 0772810

Browse files
committed
cocalc-api: implement project deletion in conat and support a full project lifecycle as part of the cocalc-api tests
1 parent 4cf7197 commit 0772810

File tree

9 files changed

+300
-131
lines changed

9 files changed

+300
-131
lines changed

src/AGENTS.md

Lines changed: 57 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ CoCalc is organized as a monorepo with key packages:
105105
5. **Authentication**: Each conat request includes account_id and is subject to permission checks at the hub level
106106
6. **Subjects**: Messages are routed using hierarchical subjects like `hub.account.{uuid}.{service}` or `project.{uuid}.{compute_server_id}.{service}`
107107

108+
#### CoCalc Conat Hub API Architecture
109+
110+
**API Method Registration Pattern:**
111+
- **Registry**: `packages/conat/hub/api/projects.ts` contains `export const projects = { methodName: authFirstRequireAccount }`
112+
- **Implementation**: `packages/server/conat/api/projects.ts` contains `export async function methodName() { ... }`
113+
- **Flow**: Python client `@api_method("projects.methodName")` → POST `/api/conat/hub``hubBridge()` → conat subject `hub.account.{account_id}.api` → registry lookup → implementation
114+
115+
**Example - projects.createProject:**
116+
1. **Python**: `@api_method("projects.createProject")` decorator
117+
2. **HTTP**: `POST /api/conat/hub {"name": "projects.createProject", "args": [...]}`
118+
3. **Bridge**: `hubBridge()` routes to conat subject
119+
4. **Registry**: `packages/conat/hub/api/projects.ts: createProject: authFirstRequireAccount`
120+
5. **Implementation**: `packages/server/conat/api/projects.ts: export { createProject }``@cocalc/server/projects/create`
121+
108122
### Key Technologies
109123

110124
- **TypeScript**: Primary language for all new code
@@ -216,92 +230,67 @@ Same flow as above, but **before 3. i18n:upload**, delete the key. Only new keys
216230
- Ignore everything in `node_modules` or `dist` directories
217231
- Ignore all files not tracked by Git, unless they are newly created files
218232

219-
# CoCalc Python API Client
233+
# CoCalc Python API Client Investigation
220234

221235
## Overview
222236

223237
The `python/cocalc-api/` directory contains a Python client library for the CoCalc API, published as the `cocalc-api` package on PyPI.
224238

225-
## Architecture
226-
227-
### Package Structure
228-
229-
- **`src/cocalc_api/`** - Main Python package source code
230-
- `__init__.py` - Package exports (Hub, Project classes)
231-
- `hub.py` - Hub client for account-level API operations
232-
- `project.py` - Project client for project-specific operations
233-
- `api_types.py` - TypedDict definitions for API responses
234-
- `util.py` - Utility functions and decorators
235-
236-
### Key Classes
237-
238-
#### Hub Client (`hub.py`)
239-
240-
Account-level API client that provides access to:
241-
242-
- **System** - Server ping, user search, account name resolution
243-
- **Projects** - Project management (create, start, stop, collaborators)
244-
- **Jupyter** - Global Jupyter kernel execution
245-
- **Database** - Direct PostgreSQL database queries
246-
- **Messages** - Send/receive messages between users
247-
- **Organizations** - Organization management (admin functions)
248-
- **Sync** - File history and synchronization
249-
250-
#### Project Client (`project.py`)
251-
252-
Project-specific API client for:
253-
254-
- **System** - Project ping, shell command execution, Jupyter execution
255-
256-
### Development Tools
257-
258-
- **Package Manager**: `uv` (modern Python package manager)
259-
- **Code Formatter**: `yapf` (Python code formatter following Google style)
260-
- **Code Quality**: `ruff` (linting), `mypy` (type checking), `pyright` (additional type checking)
261-
- **Documentation**: `mkdocs` with material theme
262-
- **Testing**: `pytest`
239+
## Client-Server Architecture Investigation
263240

264-
### Development Commands
241+
### API Call Flow
265242

266-
```bash
267-
# Setup and install dependencies
268-
make install # or: uv sync --dev && uv pip install -e .
243+
1. **cocalc-api Client** (Python) → HTTP POST requests
244+
2. **Next.js API Routes** (`/api/conat/{hub,project}`) → Bridge to conat messaging
245+
3. **ConatClient** (server-side) → NATS-like messaging protocol
246+
4. **Hub API Implementation** (`packages/conat/hub/api/`) → Actual business logic
269247

270-
# Format Python code
271-
make format # or: uv run yapf --in-place --recursive src/
248+
### Endpoints Discovered
272249

273-
# Code quality checks
274-
make check # or: uv run ruff check src/ && uv run mypy src/ && uv run pyright src/
250+
#### Hub API: `POST /api/conat/hub`
251+
- **Bridge**: `packages/next/pages/api/conat/hub.ts``hubBridge()` → conat subject `hub.account.{account_id}.api`
252+
- **Implementation**: `packages/conat/hub/api/projects.ts`
253+
- **Available Methods**: `createProject`, `start`, `stop`, `setQuotas`, `addCollaborator`, `removeCollaborator`, etc.
254+
- **Missing**: ❌ **No `delete` method implemented in conat hub API**
275255

276-
# Documentation
277-
make serve-docs # or: uv run mkdocs serve
278-
make build-docs # or: uv run mkdocs build
256+
#### Project API: `POST /api/conat/project`
257+
- **Bridge**: `packages/next/pages/api/conat/project.ts``projectBridge()` → conat project subjects
258+
- **Implementation**: `packages/conat/project/api/` (system.ping, system.exec, system.jupyterExecute)
279259

280-
# Publishing
281-
make publish # or: uv build && uv publish
260+
### Project Deletion Investigation
282261

283-
# Cleanup
284-
make clean
285-
```
262+
#### ✅ Next.js v2 API Route Available
263+
- **Endpoint**: `packages/next/pages/api/v2/projects/delete.ts`
264+
- **Functionality**: Sets deleted=true, removes licenses, stops project
265+
- **Authentication**: Requires collaborator access or admin
286266

287-
### API Design Patterns
267+
#### ❌ Missing Conat Hub API Method
268+
- **Current Methods**: Only CRUD operations (create, start, stop, quotas, collaborators)
269+
- **Gap**: No `delete` method exposed through conat hub API used by cocalc-api
288270

289-
- **Decorator-based Methods**: Uses `@api_method()` decorator to automatically convert method calls to API requests
290-
- **TypedDict Responses**: All API responses use TypedDict for type safety
291-
- **Error Handling**: Centralized error handling via `handle_error()` utility
292-
- **HTTP Client**: Uses `httpx` for HTTP requests with authentication
293-
- **Nested Namespaces**: API organized into logical namespaces (system, projects, jupyter, etc.)
271+
#### Frontend Implementation
272+
- **Location**: `packages/frontend/projects/actions.ts:delete_project()`
273+
- **Method**: Direct database table update via `projects_table_set({deleted: true})`
294274

295-
### Authentication
275+
## Implementation
296276

297-
- Supports both account-level and project-specific API keys
298-
- Account API keys provide full access to all hub functionality
299-
- Project API keys are limited to project-specific operations
277+
### Solution Implemented: Direct v2 API Call
278+
- **Added**: `hub.projects.delete(project_id)` method to cocalc-api Python client
279+
- **Implementation**: Direct HTTP POST to `/api/v2/projects/delete` endpoint
280+
- **Reasoning**: Fastest path to complete project lifecycle without requiring conat hub API changes
281+
- **Consistency**: Uses same authentication and error handling patterns as other methods
300282

301-
### Connection Endpoints
283+
### Code Changes
284+
1. **`src/cocalc_api/hub.py`**: Added `delete()` method to Projects class
285+
2. **`tests/conftest.py`**: Updated cleanup to use new delete method
286+
3. **`tests/test_hub.py`**: Added test for delete method availability
302287

303-
- **Hub API**: `POST /api/conat/hub` - Account-level operations
304-
- **Project API**: `POST /api/conat/project` - Project-specific operations
288+
## Current Status
289+
- ✅ pytest test framework established with automatic project lifecycle
290+
- ✅ Project creation/start/stop working via conat hub API
291+
- ✅ Project deletion implemented by calling v2 API route directly
292+
- ✅ Complete project lifecycle management: create → start → test → stop → delete
293+
- ✅ All 14 tests passing with proper resource cleanup
305294

306295
# Important Instruction Reminders
307296

src/packages/conat/hub/api/projects.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const projects = {
1212
setQuotas: authFirstRequireAccount,
1313
start: authFirstRequireAccount,
1414
stop: authFirstRequireAccount,
15+
deleteProject: authFirstRequireAccount,
1516
};
1617

1718
export type AddCollaborator =
@@ -103,4 +104,5 @@ export interface Projects {
103104

104105
start: (opts: { account_id: string; project_id: string }) => Promise<void>;
105106
stop: (opts: { account_id: string; project_id: string }) => Promise<void>;
107+
deleteProject: (opts: { account_id: string; project_id: string }) => Promise<void>;
106108
}

src/packages/next/pages/api/v2/projects/delete.ts

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
/*
22
API endpoint to delete a project, which sets the "delete" flag to `true` in the database.
33
*/
4-
import isCollaborator from "@cocalc/server/projects/is-collaborator";
5-
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
6-
import removeAllLicensesFromProject from "@cocalc/server/licenses/remove-all-from-project";
7-
import { getProject } from "@cocalc/server/projects/control";
8-
import userQuery from "@cocalc/database/user-query";
9-
import { isValidUUID } from "@cocalc/util/misc";
4+
5+
import deleteProject from "@cocalc/server/projects/delete";
106

117
import getAccountId from "lib/account/get-account";
128
import getParams from "lib/api/get-params";
@@ -21,45 +17,12 @@ async function handle(req, res) {
2117
const { project_id } = getParams(req);
2218
const account_id = await getAccountId(req);
2319

24-
try {
25-
if (!isValidUUID(project_id)) {
26-
throw Error("project_id must be a valid uuid");
27-
}
28-
if (!account_id) {
29-
throw Error("must be signed in");
30-
}
31-
32-
// If client is not an administrator, they must be a project collaborator in order to
33-
// delete a project.
34-
if (
35-
!(await userIsInGroup(account_id, "admin")) &&
36-
!(await isCollaborator({ account_id, project_id }))
37-
) {
38-
throw Error("must be an owner to delete a project");
39-
}
40-
41-
// Remove all project licenses
42-
//
43-
await removeAllLicensesFromProject({ project_id });
44-
45-
// Stop project
46-
//
47-
const project = getProject(project_id);
48-
await project.stop();
49-
50-
// Set "deleted" flag. We do this last to ensure that the project is not consuming any
51-
// resources while it is in the deleted state.
52-
//
53-
await userQuery({
54-
account_id,
55-
query: {
56-
projects: {
57-
project_id,
58-
deleted: true,
59-
},
60-
},
61-
});
20+
if (!account_id) {
21+
throw Error("must be signed in");
22+
}
6223

24+
try {
25+
await deleteProject({ account_id, project_id });
6326
res.json(OkStatus);
6427
} catch (err) {
6528
res.json({ error: err.message });

src/packages/server/conat/api/projects.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,6 @@ export async function stop({
103103
const project = await getProject(project_id);
104104
await project.stop();
105105
}
106+
107+
import deleteProject from "@cocalc/server/projects/delete";
108+
export { deleteProject };
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.
3+
* License: MS-RSL – see LICENSE.md for details
4+
*/
5+
6+
/*
7+
Project delete functionality
8+
9+
Extracted from the v2 API to be reusable by both REST API and Conat API.
10+
*/
11+
12+
import isCollaborator from "@cocalc/server/projects/is-collaborator";
13+
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
14+
import removeAllLicensesFromProject from "@cocalc/server/licenses/remove-all-from-project";
15+
import { getProject } from "@cocalc/server/projects/control";
16+
import userQuery from "@cocalc/database/user-query";
17+
import { isValidUUID } from "@cocalc/util/misc";
18+
19+
export default async function deleteProject({
20+
account_id,
21+
project_id,
22+
}: {
23+
account_id: string;
24+
project_id: string;
25+
}): Promise<void> {
26+
if (!isValidUUID(project_id)) {
27+
throw Error("project_id must be a valid UUID");
28+
}
29+
if (!isValidUUID(account_id)) {
30+
throw Error("account_id must be a valid UUID");
31+
}
32+
33+
// If client is not an administrator, they must be a project collaborator in order to
34+
// delete a project.
35+
if (
36+
!(await userIsInGroup(account_id, "admin")) &&
37+
!(await isCollaborator({ account_id, project_id }))
38+
) {
39+
throw Error("must be an owner to delete a project");
40+
}
41+
42+
// Remove all project licenses
43+
await removeAllLicensesFromProject({ project_id });
44+
45+
// Stop project
46+
const project = getProject(project_id);
47+
await project.stop();
48+
49+
// Set "deleted" flag. We do this last to ensure that the project is not consuming any
50+
// resources while it is in the deleted state.
51+
//
52+
await userQuery({
53+
account_id,
54+
query: {
55+
projects: {
56+
project_id,
57+
deleted: true,
58+
},
59+
},
60+
});
61+
}

0 commit comments

Comments
 (0)