Skip to content

Commit 0d70752

Browse files
committed
cocalc-api: update, tweaks for jupyter testing, detecting older api key
1 parent ccaf299 commit 0d70752

File tree

5 files changed

+657
-425
lines changed

5 files changed

+657
-425
lines changed

src/packages/server/api/manage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,9 @@ export async function getAccountWithApiKey(
295295
return;
296296
}
297297

298-
// Check for legacy account api key (format: sk_*)
299-
if (secret.startsWith("sk_")) {
298+
// Check for legacy account api key (format historically documented as sk-*, but
299+
// some deployments used sk_*, so accept both to avoid breaking existing keys)
300+
if (secret.startsWith(API_KEY_PREFIX) || secret.startsWith("sk_")) {
300301
const { rows } = await pool.query(
301302
"SELECT account_id FROM accounts WHERE api_key = $1::TEXT",
302303
[secret],

src/python/cocalc-api/src/cocalc_api/hub.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class Hub:
1010
def __init__(self, api_key: str, host: str = "https://cocalc.com"):
1111
self.api_key = api_key
1212
self.host = host
13-
# Use longer timeout for API calls (30 seconds instead of default 5)
14-
self.client = httpx.Client(auth=(api_key, ""), headers={"Content-Type": "application/json"}, timeout=30.0)
13+
# Use longer timeout for API calls (90 seconds instead of default 5) to handle slow operations like Jupyter
14+
self.client = httpx.Client(auth=(api_key, ""), headers={"Content-Type": "application/json"}, timeout=90.0)
1515

1616
def call(self, name: str, arguments: list[Any], timeout: Optional[int] = None) -> Any:
1717
"""
@@ -326,14 +326,15 @@ def kernels(self, project_id: Optional[str] = None) -> list[dict[str, Any]]:
326326
"""
327327
...
328328

329-
@api_method("jupyter.execute")
329+
@api_method("jupyter.execute", timeout_seconds=True)
330330
def execute(
331331
self,
332332
input: str,
333333
kernel: str,
334334
history: Optional[list[str]] = None,
335335
project_id: Optional[str] = None,
336336
path: Optional[str] = None,
337+
timeout: Optional[int] = 30,
337338
) -> dict[str, Any]: # type: ignore[empty-body]
338339
"""
339340
Execute code using a Jupyter kernel.
@@ -344,6 +345,7 @@ def execute(
344345
history (Optional[list[str]]): Array of previous inputs (they get evaluated every time, but without output being captured).
345346
project_id (Optional[str]): Project in which to run the code -- if not given, global anonymous project is used, if available.
346347
path (Optional[str]): File path context for execution.
348+
timeout (Optional[int]): Timeout in SECONDS for the execute call (defaults to 30 seconds).
347349
348350
Returns:
349351
dict[str, Any]: JSON response containing execution results.

src/python/cocalc-api/src/cocalc_api/project.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,14 @@ def exec(
124124
"""
125125
...
126126

127-
@api_method("system.jupyterExecute")
127+
@api_method("system.jupyterExecute", timeout_seconds=True)
128128
def jupyter_execute(
129129
self,
130130
input: str,
131131
kernel: str,
132132
history: Optional[list[str]] = None,
133133
path: Optional[str] = None,
134+
timeout: Optional[int] = 30,
134135
) -> list[dict[str, Any]]: # type: ignore[empty-body]
135136
"""
136137
Execute code using a Jupyter kernel.
@@ -140,6 +141,7 @@ def jupyter_execute(
140141
kernel (str): Name of kernel to use. Get options using hub.jupyter.kernels().
141142
history (Optional[list[str]]): Array of previous inputs (they get evaluated every time, but without output being captured).
142143
path (Optional[str]): File path context for execution.
144+
timeout (Optional[int]): Timeout in SECONDS for the execute call (defaults to 30 seconds).
143145
144146
Returns:
145147
list[dict[str, Any]]: List of output items. Each output item contains

src/python/cocalc-api/tests/conftest.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Pytest configuration and fixtures for cocalc-api tests.
33
"""
4+
import json
45
import os
56
import time
67
import uuid
@@ -93,16 +94,20 @@ def temporary_project(hub, resource_tracker, request):
9394
# Wait for project to be ready (can take 10-15 seconds)
9495
from cocalc_api import Project
9596

97+
test_project = Project(project_id=project_id, api_key=hub.api_key, host=hub.host)
9698
for attempt in range(10):
9799
time.sleep(5) # Wait 5 seconds before checking
98100
try:
99101
# Try to ping the project to see if it's ready
100-
test_project = Project(project_id=project_id, api_key=hub.api_key, host=hub.host)
101102
test_project.system.ping() # If this succeeds, project is ready
102103
break
103104
except Exception:
104105
if attempt == 9: # Last attempt
105106
print(f"Warning: Project {project_id} did not become ready within 50 seconds")
107+
else:
108+
print(f"Warning: Project {project_id} may not be ready yet")
109+
110+
ensure_python3_kernel(test_project)
106111

107112
except Exception as e:
108113
print(f"Warning: Failed to start project {project_id}: {e}")
@@ -120,6 +125,53 @@ def project_client(temporary_project, api_key, cocalc_host):
120125
return Project(project_id=temporary_project['project_id'], api_key=api_key, host=cocalc_host)
121126

122127

128+
def ensure_python3_kernel(project_client: Project):
129+
"""
130+
Ensure the default python3 Jupyter kernel is installed in the project.
131+
132+
If not available, install ipykernel and register the kernelspec.
133+
"""
134+
135+
def has_python_kernel() -> bool:
136+
try:
137+
result = project_client.system.exec(
138+
command="python3",
139+
args=["-m", "jupyter", "kernelspec", "list", "--json"],
140+
timeout=60,
141+
)
142+
data = json.loads(result["stdout"])
143+
kernelspecs = data.get("kernelspecs", {})
144+
return "python3" in kernelspecs
145+
except Exception as err:
146+
print(f"Warning: Failed to list kernelspecs: {err}")
147+
return False
148+
149+
if has_python_kernel():
150+
return
151+
152+
print("Installing python3 kernelspec in project...")
153+
project_client.system.exec(
154+
command="python3",
155+
args=["-m", "pip", "install", "--user", "ipykernel"],
156+
timeout=300,
157+
)
158+
project_client.system.exec(
159+
command="python3",
160+
args=[
161+
"-m",
162+
"ipykernel",
163+
"install",
164+
"--user",
165+
"--name=python3",
166+
"--display-name=Python 3",
167+
],
168+
timeout=120,
169+
)
170+
171+
if not has_python_kernel():
172+
raise RuntimeError("Failed to ensure python3 kernelspec is installed in project")
173+
174+
123175
# ============================================================================
124176
# Database Cleanup Infrastructure
125177
# ============================================================================
@@ -218,7 +270,8 @@ def db_pool(check_cleanup_config):
218270
if not cleanup_enabled:
219271
print("\n⚠ Database cleanup DISABLED via COCALC_TESTS_CLEANUP=false")
220272
print(" Test resources will remain in the database.")
221-
return None
273+
yield None
274+
return
222275

223276
# Get connection parameters with defaults
224277
pguser = os.environ.get("PGUSER", "smc")

0 commit comments

Comments
 (0)