Skip to content

Commit fbe8e0f

Browse files
feat(skills): add FastAPI routes for Skills API endpoints (#6066)
## Summary - Add `src/ogx_api/skills/fastapi_routes.py` with all 11 Skills API route handlers at `/v1alpha` prefix - Multipart upload routes use `read_upload_with_size_limit` for bounded reads and `PreReadUploadFile` to avoid double-reads - List endpoints use `create_query_dependency()` for pagination parameter extraction - Content download routes return raw `Response` with `application/zip` media type - Follows the same patterns as `files/fastapi_routes.py` Ref: #5891 (task 4) ## Test plan - [x] `uv run python -c "from ogx_api.skills.fastapi_routes import create_router"` — import succeeds - [x] All 11 protocol methods have corresponding route handlers - [x] All `impl.*()` calls match protocol signatures (argument count and types verified) - [x] `uv run pytest tests/unit/server/ tests/unit/registry/ -x --tb=short` — 238 passed - [x] `uv run pre-commit run --all-files` — all hooks pass - [ ] End-to-end test after provider implementation lands (task 7) Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5ceb0c9 commit fbe8e0f

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Copyright (c) The OGX Contributors.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the terms described in the LICENSE file in
5+
# the root directory of this source tree.
6+
7+
from typing import Annotated
8+
9+
from fastapi import APIRouter, Depends, UploadFile
10+
from fastapi.param_functions import File, Form
11+
from fastapi.responses import Response
12+
13+
from ogx_api.common.upload_limits import (
14+
PreReadUploadFile,
15+
read_upload_with_size_limit,
16+
)
17+
from ogx_api.router_utils import create_query_dependency, standard_responses
18+
from ogx_api.version import OGX_API_V1ALPHA
19+
20+
from .api import Skills
21+
from .models import (
22+
MAX_ZIP_SIZE_BYTES,
23+
ListSkillsRequest,
24+
ListSkillsResponse,
25+
ListSkillVersionsRequest,
26+
ListSkillVersionsResponse,
27+
Skill,
28+
SkillDeleteResponse,
29+
SkillUpdateRequest,
30+
SkillVersion,
31+
SkillVersionCreateRequest,
32+
SkillVersionDeleteResponse,
33+
)
34+
35+
get_list_skills_request = create_query_dependency(ListSkillsRequest)
36+
get_list_skill_versions_request = create_query_dependency(ListSkillVersionsRequest)
37+
38+
39+
def create_router(impl: Skills) -> APIRouter:
40+
router = APIRouter(
41+
prefix=f"/{OGX_API_V1ALPHA}",
42+
tags=["Skills"],
43+
responses=standard_responses,
44+
)
45+
46+
@router.post(
47+
"/skills",
48+
response_model=Skill,
49+
summary="Create skill",
50+
description="Create a skill by uploading a zip bundle containing a SKILL.md manifest.",
51+
)
52+
async def create_skill(
53+
file: Annotated[UploadFile, File(description="Zip archive containing the skill bundle.")],
54+
) -> Skill:
55+
content = await read_upload_with_size_limit(file, MAX_ZIP_SIZE_BYTES)
56+
safe_file = PreReadUploadFile(content, filename=file.filename, content_type=file.content_type)
57+
return await impl.create_skill(safe_file)
58+
59+
@router.get(
60+
"/skills",
61+
response_model=ListSkillsResponse,
62+
summary="List skills",
63+
description="List all skills.",
64+
)
65+
async def list_skills(
66+
request: Annotated[ListSkillsRequest, Depends(get_list_skills_request)],
67+
) -> ListSkillsResponse:
68+
return await impl.list_skills(request)
69+
70+
@router.get(
71+
"/skills/{skill_id}",
72+
response_model=Skill,
73+
summary="Get skill",
74+
description="Get metadata for a specific skill.",
75+
)
76+
async def get_skill(skill_id: str) -> Skill:
77+
return await impl.get_skill(skill_id)
78+
79+
@router.post(
80+
"/skills/{skill_id}",
81+
response_model=Skill,
82+
summary="Update skill",
83+
description="Update a skill's default version.",
84+
)
85+
async def update_skill(skill_id: str, request: SkillUpdateRequest) -> Skill:
86+
return await impl.update_skill(skill_id, request)
87+
88+
@router.delete(
89+
"/skills/{skill_id}",
90+
response_model=SkillDeleteResponse,
91+
summary="Delete skill",
92+
description="Delete a skill and all its versions.",
93+
)
94+
async def delete_skill(skill_id: str) -> SkillDeleteResponse:
95+
return await impl.delete_skill(skill_id)
96+
97+
@router.get(
98+
"/skills/{skill_id}/content",
99+
summary="Get skill content",
100+
description="Download the default version's zip bundle.",
101+
responses={
102+
200: {
103+
"description": "The skill bundle as a zip archive.",
104+
"content": {"application/zip": {}},
105+
},
106+
},
107+
)
108+
async def get_skill_content(skill_id: str) -> Response:
109+
return await impl.get_skill_content(skill_id)
110+
111+
@router.post(
112+
"/skills/{skill_id}/versions",
113+
response_model=SkillVersion,
114+
summary="Create skill version",
115+
description="Upload a new version of a skill.",
116+
)
117+
async def create_skill_version(
118+
skill_id: str,
119+
file: Annotated[UploadFile, File(description="Zip archive containing the skill bundle.")],
120+
default: Annotated[bool, Form(description="Whether to set this version as the default.")] = False,
121+
) -> SkillVersion:
122+
content = await read_upload_with_size_limit(file, MAX_ZIP_SIZE_BYTES)
123+
safe_file = PreReadUploadFile(content, filename=file.filename, content_type=file.content_type)
124+
request = SkillVersionCreateRequest(default=default)
125+
return await impl.create_skill_version(skill_id, request, safe_file)
126+
127+
@router.get(
128+
"/skills/{skill_id}/versions",
129+
response_model=ListSkillVersionsResponse,
130+
summary="List skill versions",
131+
description="List all versions of a skill.",
132+
)
133+
async def list_skill_versions(
134+
skill_id: str,
135+
request: Annotated[ListSkillVersionsRequest, Depends(get_list_skill_versions_request)],
136+
) -> ListSkillVersionsResponse:
137+
return await impl.list_skill_versions(skill_id, request)
138+
139+
@router.get(
140+
"/skills/{skill_id}/versions/{version}",
141+
response_model=SkillVersion,
142+
summary="Get skill version",
143+
description="Get metadata for a specific skill version.",
144+
)
145+
async def get_skill_version(skill_id: str, version: str) -> SkillVersion:
146+
return await impl.get_skill_version(skill_id, version)
147+
148+
@router.get(
149+
"/skills/{skill_id}/versions/{version}/content",
150+
summary="Get skill version content",
151+
description="Download a specific version's zip bundle.",
152+
responses={
153+
200: {
154+
"description": "The skill bundle as a zip archive.",
155+
"content": {"application/zip": {}},
156+
},
157+
},
158+
)
159+
async def get_skill_version_content(skill_id: str, version: str) -> Response:
160+
return await impl.get_skill_version_content(skill_id, version)
161+
162+
@router.delete(
163+
"/skills/{skill_id}/versions/{version}",
164+
response_model=SkillVersionDeleteResponse,
165+
summary="Delete skill version",
166+
description="Delete a specific version of a skill.",
167+
)
168+
async def delete_skill_version(skill_id: str, version: str) -> SkillVersionDeleteResponse:
169+
return await impl.delete_skill_version(skill_id, version)
170+
171+
return router

0 commit comments

Comments
 (0)