mirror of
https://github.com/bleak-ai/gcontext.git
synced 2026-08-11 13:19:23 +02:00
release: api-v0.3.0
Anonymous install telemetry: POST /api/telemetry accepts install events from the CLI, GET /api/admin/installs lists them (auth-protected). Install model and installs table auto-created on startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6cbcb51f97
commit
9af1dcfe69
@@ -6,6 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from .db import init_db
|
||||
from .routes_admin import router as admin_router
|
||||
from .routes_public import router as public_router
|
||||
from .routes_telemetry import router as telemetry_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -28,6 +29,7 @@ app.add_middleware(
|
||||
)
|
||||
app.include_router(public_router)
|
||||
app.include_router(admin_router)
|
||||
app.include_router(telemetry_router)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
@@ -13,3 +13,14 @@ class Workflow(Base):
|
||||
downloads: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
|
||||
|
||||
class Install(Base):
|
||||
__tablename__ = "installs"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
install_id: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
version: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
os: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
platform: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
created_at: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
+18
-1
@@ -4,7 +4,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from . import settings
|
||||
from .db import get_session
|
||||
from .models import Workflow
|
||||
from .models import Install, Workflow
|
||||
from .schemas import WorkflowOut
|
||||
|
||||
|
||||
@@ -24,3 +24,20 @@ def list_all(session: Session = Depends(get_session)):
|
||||
select(Workflow).order_by(Workflow.downloads.desc())
|
||||
).all()
|
||||
return [WorkflowOut(id=w.id, downloads=w.downloads) for w in rows]
|
||||
|
||||
|
||||
@router.get("/installs")
|
||||
def list_installs(session: Session = Depends(get_session)):
|
||||
rows = session.scalars(
|
||||
select(Install).order_by(Install.id.desc()).limit(200)
|
||||
).all()
|
||||
return [
|
||||
{
|
||||
"install_id": r.install_id,
|
||||
"version": r.version,
|
||||
"os": r.os,
|
||||
"platform": r.platform,
|
||||
"created_at": r.created_at,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .db import get_session
|
||||
from .models import Install
|
||||
from .schemas import InstallIn
|
||||
|
||||
router = APIRouter(tags=["telemetry"])
|
||||
|
||||
|
||||
@router.post("/api/telemetry", status_code=204)
|
||||
def record_install(data: InstallIn, session: Session = Depends(get_session)):
|
||||
session.add(Install(
|
||||
install_id=data.install_id,
|
||||
version=data.version,
|
||||
os=data.os,
|
||||
platform=data.platform,
|
||||
created_at=datetime.now(timezone.utc).isoformat(),
|
||||
))
|
||||
session.commit()
|
||||
return Response(status_code=204)
|
||||
@@ -4,3 +4,10 @@ from pydantic import BaseModel
|
||||
class WorkflowOut(BaseModel):
|
||||
id: str
|
||||
downloads: int
|
||||
|
||||
|
||||
class InstallIn(BaseModel):
|
||||
install_id: str
|
||||
version: str
|
||||
os: str
|
||||
platform: str
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "gcontext-workflows-api"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "Install-counter service for gcontext workflow templates."
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
def test_telemetry_stores_install(client):
|
||||
payload = {
|
||||
"install_id": "abc-123",
|
||||
"version": "0.5.0",
|
||||
"os": "Darwin",
|
||||
"platform": "arm64",
|
||||
}
|
||||
resp = client.post("/api/telemetry", json=payload)
|
||||
assert resp.status_code == 204
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
|
||||
with engine().begin() as conn:
|
||||
rows = conn.execute(text(
|
||||
"SELECT install_id, version, os, platform FROM installs"
|
||||
)).fetchall()
|
||||
assert len(rows) == 1
|
||||
assert tuple(rows[0]) == ("abc-123", "0.5.0", "Darwin", "arm64")
|
||||
|
||||
|
||||
def test_telemetry_rejects_missing_fields(client):
|
||||
resp = client.post("/api/telemetry", json={"install_id": "x"})
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
def test_telemetry_duplicate_install_id_allowed(client):
|
||||
payload = {
|
||||
"install_id": "dup-1",
|
||||
"version": "0.5.0",
|
||||
"os": "Linux",
|
||||
"platform": "x86_64",
|
||||
}
|
||||
resp1 = client.post("/api/telemetry", json=payload)
|
||||
resp2 = client.post("/api/telemetry", json=payload)
|
||||
assert resp1.status_code == 204
|
||||
assert resp2.status_code == 204
|
||||
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def test_admin_list_installs(client):
|
||||
client.post("/api/telemetry", json={
|
||||
"install_id": "admin-test",
|
||||
"version": "0.5.0",
|
||||
"os": "Darwin",
|
||||
"platform": "arm64",
|
||||
})
|
||||
resp = client.get(
|
||||
"/api/admin/installs",
|
||||
headers={"Authorization": f"Bearer {os.environ['ADMIN_TOKEN']}"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert any(i["install_id"] == "admin-test" for i in data)
|
||||
|
||||
|
||||
def test_admin_installs_requires_auth(client):
|
||||
resp = client.get("/api/admin/installs")
|
||||
assert resp.status_code == 401
|
||||
Reference in New Issue
Block a user