bcf_api: add topic filtering; documents_api: add GET /document-metadata

BCF list_topics now maps topic_status, assigned_to, label, modified_after,
modified_before query params to Forgejo issue API params.

documents_api adds GET /document-metadata/{document_id} which decodes the
document_id and proxies Forgejo's contents endpoint for file metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bruno Postle 2026-04-29 22:24:52 +01:00
parent b4e051ecb7
commit 81d3a3b6cb
5 changed files with 133 additions and 5 deletions

View file

@ -48,8 +48,8 @@
{"id":"ifcurl-ach","title":"Deploy Phase 3b footer template to Forgejo","description":"The footer.tmpl file that injects a 'View in 3D' button on .ifc file pages has been written but not yet deployed. Deploy it to /etc/forgejo/templates/custom/footer.tmpl and verify the button appears when viewing an .ifc file on localhost:3000.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:47:53Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:35Z","closed_at":"2026-04-23T06:14:35Z","close_reason":"Split into atomic tasks: ifcurl-ohq, ifcurl-49o","dependencies":[{"issue_id":"ifcurl-ach","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:40Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"ifcurl-ach","title":"Deploy Phase 3b footer template to Forgejo","description":"The footer.tmpl file that injects a 'View in 3D' button on .ifc file pages has been written but not yet deployed. Deploy it to /etc/forgejo/templates/custom/footer.tmpl and verify the button appears when viewing an .ifc file on localhost:3000.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:47:53Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:35Z","closed_at":"2026-04-23T06:14:35Z","close_reason":"Split into atomic tasks: ifcurl-ohq, ifcurl-49o","dependencies":[{"issue_id":"ifcurl-ach","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:40Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-xfi","title":"Implement OpenCDE document picker (select-documents flow)","description":"The OpenCDE Documents API defines a browser-redirect flow for document discovery. Implement POST /documents/1.0/select-documents which returns a select_documents_url. That URL serves a small HTML page: owner/repo selector then file-tree browser. When user selects an IFC file, generate the document_id and redirect to the callback_url with document_ids[] params.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:16Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-xfi","title":"Implement OpenCDE document picker (select-documents flow)","description":"The OpenCDE Documents API defines a browser-redirect flow for document discovery. Implement POST /documents/1.0/select-documents which returns a select_documents_url. That URL serves a small HTML page: owner/repo selector then file-tree browser. When user selects an IFC file, generate the document_id and redirect to the callback_url with document_ids[] params.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:16Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:16Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-bil","title":"Implement OpenCDE OAuth2 proxy routes","description":"The OpenCDE Foundation API requires OAuth2 endpoints. Forgejo already has OAuth2 support — proxy through to it. Routes needed: GET /foundation/1.0/oauth2/auth_url (return Forgejo OAuth2 auth URL), POST /foundation/1.0/oauth2/token (proxy code exchange to Forgejo token endpoint), POST /foundation/1.0/oauth2/token_refresh (proxy refresh). Requires a one-time step of registering an OAuth2 app in Forgejo admin and storing client_id/client_secret as env vars.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:15Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:15Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-bil","title":"Implement OpenCDE OAuth2 proxy routes","description":"The OpenCDE Foundation API requires OAuth2 endpoints. Forgejo already has OAuth2 support — proxy through to it. Routes needed: GET /foundation/1.0/oauth2/auth_url (return Forgejo OAuth2 auth URL), POST /foundation/1.0/oauth2/token (proxy code exchange to Forgejo token endpoint), POST /foundation/1.0/oauth2/token_refresh (proxy refresh). Requires a one-time step of registering an OAuth2 app in Forgejo admin and storing client_id/client_secret as env vars.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:15Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:15Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-k3v","title":"Add GET /documents/1.0/document-metadata/{document_id}","description":"The OpenCDE Documents API spec includes a GET endpoint for individual file metadata. Implement it: decode the document_id, fetch the file's latest info from Forgejo (name, size, last-modified), return as a metadata response.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:13Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:13Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-k3v","title":"Add GET /documents/1.0/document-metadata/{document_id}","description":"The OpenCDE Documents API spec includes a GET endpoint for individual file metadata. Implement it: decode the document_id, fetch the file's latest info from Forgejo (name, size, last-modified), return as a metadata response.","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:13Z","created_by":"Bruno Postle","updated_at":"2026-04-29T21:22:34Z","started_at":"2026-04-29T21:22:34Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-ye6","title":"Add BCF topic filtering query params","description":"The BCF 3.0 REST spec defines query parameters for GET /topics (topic_status, topic_type, priority, assignee, etc.). Currently we return all topics unfiltered. Translate BCF filter params to Forgejo issues API params (state, labels, assignee) and pass them through.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:00Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:00Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-ye6","title":"Add BCF topic filtering query params","description":"The BCF 3.0 REST spec defines query parameters for GET /topics (topic_status, topic_type, priority, assignee, etc.). Currently we return all topics unfiltered. Translate BCF filter params to Forgejo issues API params (state, labels, assignee) and pass them through.","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:00Z","created_by":"Bruno Postle","updated_at":"2026-04-29T21:22:21Z","started_at":"2026-04-29T21:22:21Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-lwl","title":"Add OpenCDE Foundation API discovery endpoint","description":"Expose GET /foundation/versions on the Forgejo instance so OpenCDE clients can auto-discover which APIs the server supports. Returns JSON declaring BCF API 3.0 and Documents API with their base URLs and OAuth2 configuration.\n\n## Architecture\n\nStandalone Python daemon, proxied at /foundation/ on the Forgejo hostname. Static or config-driven JSON response — no Forgejo internals needed, no auth required (Foundation API discovery endpoint is public by spec). Likely a small route in the existing preview service.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-28T22:41:06Z","created_by":"Bruno Postle","updated_at":"2026-04-29T18:21:36Z","started_at":"2026-04-29T18:13:51Z","closed_at":"2026-04-29T18:21:36Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"ifcurl-lwl","title":"Add OpenCDE Foundation API discovery endpoint","description":"Expose GET /foundation/versions on the Forgejo instance so OpenCDE clients can auto-discover which APIs the server supports. Returns JSON declaring BCF API 3.0 and Documents API with their base URLs and OAuth2 configuration.\n\n## Architecture\n\nStandalone Python daemon, proxied at /foundation/ on the Forgejo hostname. Static or config-driven JSON response — no Forgejo internals needed, no auth required (Foundation API discovery endpoint is public by spec). Likely a small route in the existing preview service.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-28T22:41:06Z","created_by":"Bruno Postle","updated_at":"2026-04-29T18:21:36Z","started_at":"2026-04-29T18:13:51Z","closed_at":"2026-04-29T18:21:36Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0}
{"id":"ifcurl-3td","title":"Add status hint when clip mode is active","description":"When the user enters clip mode the cursor changes to a crosshair but there is no other feedback explaining that a double-click is needed to place a plane. Add a short hint (e.g. in the status bar or as a tooltip) saying 'Double-click to place clip plane' while clip mode is active, and clear it when clip mode is deactivated.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-27T19:45:47Z","created_by":"Bruno Postle","updated_at":"2026-04-27T19:45:47Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-3td","title":"Add status hint when clip mode is active","description":"When the user enters clip mode the cursor changes to a crosshair but there is no other feedback explaining that a double-click is needed to place a plane. Add a short hint (e.g. in the status bar or as a tooltip) saying 'Double-click to place clip plane' while clip mode is active, and clear it when clip mode is deactivated.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-27T19:45:47Z","created_by":"Bruno Postle","updated_at":"2026-04-27T19:45:47Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-5u6","title":"Evaluate replacing ifc_url.go markdown extension with JS injection","description":"ifc_url.go is a Goldmark AST extension that replaces ifc:// URLs in rendered markdown with preview figure elements (img + link). This requires patching and recompiling Forgejo. A JS alternative in footer.tmpl could handle the [title](ifc://...) markdown link case by scanning document.querySelectorAll('a[href^=\"ifc://\"]') and replacing matched anchors with figure elements at runtime — no Go changes needed. The limitation: bare ifc://... text in markdown won't be auto-linked by Goldmark (unknown scheme renders as plain text), so bare URLs would need text-node scanning in JS, which is fiddly and causes a render flash. Decision: replace with JS if users can commit to always using [title](ifc://...) link syntax; keep Go extension if bare URLs in markdown are a real use case.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-27T06:05:32Z","created_by":"Bruno Postle","updated_at":"2026-04-27T06:40:02Z","started_at":"2026-04-27T06:39:11Z","closed_at":"2026-04-27T06:40:02Z","close_reason":"Keep Go extension. Issue button (viewer.js:261) emits bare ifc:// URLs into markdown, making bare-URL rendering essential. JS text-node scanning works but causes a render flash. A clean alternative would be registering ifc:// as a Goldmark autolink scheme so bare URLs become \u003ca\u003e elements JS can pick up — worth revisiting if upgrade burden grows, but not worth doing now.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-5u6","title":"Evaluate replacing ifc_url.go markdown extension with JS injection","description":"ifc_url.go is a Goldmark AST extension that replaces ifc:// URLs in rendered markdown with preview figure elements (img + link). This requires patching and recompiling Forgejo. A JS alternative in footer.tmpl could handle the [title](ifc://...) markdown link case by scanning document.querySelectorAll('a[href^=\"ifc://\"]') and replacing matched anchors with figure elements at runtime — no Go changes needed. The limitation: bare ifc://... text in markdown won't be auto-linked by Goldmark (unknown scheme renders as plain text), so bare URLs would need text-node scanning in JS, which is fiddly and causes a render flash. Decision: replace with JS if users can commit to always using [title](ifc://...) link syntax; keep Go extension if bare URLs in markdown are a real use case.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-27T06:05:32Z","created_by":"Bruno Postle","updated_at":"2026-04-27T06:40:02Z","started_at":"2026-04-27T06:39:11Z","closed_at":"2026-04-27T06:40:02Z","close_reason":"Keep Go extension. Issue button (viewer.js:261) emits bare ifc:// URLs into markdown, making bare-URL rendering essential. JS text-node scanning works but causes a render flash. A clean alternative would be registering ifc:// as a Goldmark autolink scheme so bare URLs become \u003ca\u003e elements JS can pick up — worth revisiting if upgrade burden grows, but not worth doing now.","dependency_count":0,"dependent_count":0,"comment_count":0}
@ -112,5 +112,5 @@
{"id":"ifcurl-vam","title":"Phase 6: Bonsai offer ifc:// URL when saving linked model reference","description":"When saving an IFCDOCUMENTREFERENCE in Bonsai, offer the option to write the location as an ifc:// URL rather than a relative path. When a relative path would traverse above the repository root, prompt the user to convert it to an ifc:// URL. Requires Phase 4 federation (ifcurl-bqt).","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:16Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:13Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-vam","depends_on_id":"ifcurl-bqt","type":"blocks","created_at":"2026-04-23T06:49:23Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"ifcurl-vam","title":"Phase 6: Bonsai offer ifc:// URL when saving linked model reference","description":"When saving an IFCDOCUMENTREFERENCE in Bonsai, offer the option to write the location as an ifc:// URL rather than a relative path. When a relative path would traverse above the repository root, prompt the user to convert it to an ifc:// URL. Requires Phase 4 federation (ifcurl-bqt).","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:16Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:13Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-vam","depends_on_id":"ifcurl-bqt","type":"blocks","created_at":"2026-04-23T06:49:23Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-9k2","title":"Phase 5: IFC Viewer integration (equivalent of Phase 4)","description":"Add ifc:// OS protocol handler and 'Copy view URL' button to the IFC Viewer desktop application, equivalent to the Bonsai Phase 4 integration. Depends on Phase 4 being complete as a reference implementation.","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:08Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:12Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-9k2","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:14Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"ifcurl-9k2","title":"Phase 5: IFC Viewer integration (equivalent of Phase 4)","description":"Add ifc:// OS protocol handler and 'Copy view URL' button to the IFC Viewer desktop application, equivalent to the Bonsai Phase 4 integration. Depends on Phase 4 being complete as a reference implementation.","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:08Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:12Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-9k2","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:14Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"id":"ifcurl-prc","title":"Phase 3c: 3D diff view for IFC PR merges","description":"After a PR merge completes, surface a 'View merge in 3D' button using the Phase 3b viewer. Use @thatopen/components highlighter to colour added, removed, and modified elements differently, driven by comparing the merged IFC against either parent. Requires Phase 3b viewer (ifcurl-i1s) to be working.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:30Z","created_by":"Bruno Postle","updated_at":"2026-04-26T17:15:07Z","closed_at":"2026-04-26T17:15:07Z","close_reason":"Rendered diff already implemented in footer.tmpl Case 3; static PNG via /render_diff endpoint, not interactive 3D","dependencies":[{"issue_id":"ifcurl-prc","depends_on_id":"ifcurl-i1s","type":"blocks","created_at":"2026-04-23T06:48:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"ifcurl-prc","title":"Phase 3c: 3D diff view for IFC PR merges","description":"After a PR merge completes, surface a 'View merge in 3D' button using the Phase 3b viewer. Use @thatopen/components highlighter to colour added, removed, and modified elements differently, driven by comparing the merged IFC against either parent. Requires Phase 3b viewer (ifcurl-i1s) to be working.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:30Z","created_by":"Bruno Postle","updated_at":"2026-04-26T17:15:07Z","closed_at":"2026-04-26T17:15:07Z","close_reason":"Rendered diff already implemented in footer.tmpl Case 3; static PNG via /render_diff endpoint, not interactive 3D","dependencies":[{"issue_id":"ifcurl-prc","depends_on_id":"ifcurl-i1s","type":"blocks","created_at":"2026-04-23T06:48:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"memory","key":"forgejo-build-and-deploy-procedure-go-build-c","value":"Forgejo build and deploy procedure: go build -C /home/bruno/src/forgejo -tags 'sqlite sqlite_unlock_notify' -ldflags \"-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'\" -o /home/bruno/src/forgejo/forgejo . — then sudo cp forgejo /usr/bin/forgejo. StaticRootPath=/usr/share/forgejo. CustomPath=/var/lib/forgejo/custom. Custom templates go to /var/lib/forgejo/custom/templates/custom/ (e.g. footer.tmpl → /var/lib/forgejo/custom/templates/custom/footer.tmpl). Custom static files go to /var/lib/forgejo/custom/public/assets/ (NOT /etc/forgejo/public/assets/) and are served at /assets/*."}
{"_type":"memory","key":"torawurl-in-viewer-url-js-must-support-github","value":"toRawUrl in viewer-url.js must support GitHub, GitLab, Forgejo/Gitea (codeberg.org, self-hosted), and localhost. Tests in forgejo/tests/viewer-url.test.js cover all four. The localhost check (http vs https) is based on host.startsWith('localhost'), so subdomain.localhost gets https."} {"_type":"memory","key":"torawurl-in-viewer-url-js-must-support-github","value":"toRawUrl in viewer-url.js must support GitHub, GitLab, Forgejo/Gitea (codeberg.org, self-hosted), and localhost. Tests in forgejo/tests/viewer-url.test.js cover all four. The localhost check (http vs https) is based on host.startsWith('localhost'), so subdomain.localhost gets https."}
{"_type":"memory","key":"forgejo-build-and-deploy-procedure-go-build-c","value":"Forgejo build and deploy procedure: go build -C /home/bruno/src/forgejo -tags 'sqlite sqlite_unlock_notify' -ldflags \"-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'\" -o /home/bruno/src/forgejo/forgejo . — then sudo cp forgejo /usr/bin/forgejo. StaticRootPath=/usr/share/forgejo. CustomPath=/var/lib/forgejo/custom. Custom templates go to /var/lib/forgejo/custom/templates/custom/ (e.g. footer.tmpl → /var/lib/forgejo/custom/templates/custom/footer.tmpl). Custom static files go to /var/lib/forgejo/custom/public/assets/ (NOT /etc/forgejo/public/assets/) and are served at /assets/*."}

View file

@ -246,7 +246,19 @@ def get_extensions(owner: str, repo: str) -> JSONResponse:
@router.get("/projects/{owner}/{repo}/topics") @router.get("/projects/{owner}/{repo}/topics")
def list_topics(owner: str, repo: str, request: Request) -> JSONResponse: def list_topics(owner: str, repo: str, request: Request) -> JSONResponse:
auth = _auth(request) auth = _auth(request)
issues = _fget(f"/api/v1/repos/{owner}/{repo}/issues", auth, params={"type": "issues", "state": "open", "limit": 50}) qp = request.query_params
forgejo_params: dict = {"type": "issues", "limit": 50}
status = qp.get("topic_status")
forgejo_params["state"] = _STATUS_REVERSE.get(status, "open") if status else "open"
if assigned_to := qp.get("assigned_to"):
forgejo_params["assignee"] = assigned_to
if label := qp.get("label"):
forgejo_params["label"] = label
if since := qp.get("modified_after"):
forgejo_params["since"] = since
if before := qp.get("modified_before"):
forgejo_params["before"] = before
issues = _fget(f"/api/v1/repos/{owner}/{repo}/issues", auth, params=forgejo_params)
return JSONResponse([_issue_to_topic(i, owner, repo) for i in (issues if isinstance(issues, list) else [])]) return JSONResponse([_issue_to_topic(i, owner, repo) for i in (issues if isinstance(issues, list) else [])])

View file

@ -41,7 +41,7 @@ from __future__ import annotations
import base64 import base64
from fastapi import APIRouter, Body, Request from fastapi import APIRouter, Body, HTTPException, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from ifcurl.bcf_api import _auth, _fget from ifcurl.bcf_api import _auth, _fget
@ -134,6 +134,24 @@ def _versions_for_file(owner: str, repo: str, path: str, auth: str | None, forge
# Route # Route
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@router.get("/document-metadata/{document_id}")
def get_document_metadata(document_id: str, request: Request) -> JSONResponse:
"""Return metadata for a single document_id."""
auth = _auth(request)
try:
owner, repo, path = decode_document_id(document_id)
except ValueError:
raise HTTPException(status_code=404, detail="Invalid document_id")
info = _fget(f"/api/v1/repos/{owner}/{repo}/contents/{path}", auth)
if not isinstance(info, dict):
raise HTTPException(status_code=404, detail="File not found")
return JSONResponse({
"document_id": document_id,
"name": info.get("name", path.rsplit("/", 1)[-1]),
"description": f"{owner}/{repo}/{path}",
})
@router.post("/document-versions") @router.post("/document-versions")
def document_versions(request: Request, body: dict = Body(default={})) -> JSONResponse: def document_versions(request: Request, body: dict = Body(default={})) -> JSONResponse:
"""Return tagged releases + HEAD for each requested document_id. """Return tagged releases + HEAD for each requested document_id.

View file

@ -196,6 +196,60 @@ class TestTopicRoutes:
assert len(topics) == 2 assert len(topics) == 2
assert topics[0]["title"] == "Crack in wall" assert topics[0]["title"] == "Crack in wall"
def test_list_topics_default_state_is_open(self):
captured = {}
def mock_fget(path, auth, params=None):
captured.update(params or {})
return [ISSUE_1]
with patch("ifcurl.bcf_api._fget", side_effect=mock_fget):
client.get(f"/bcf/3.0/projects/{OWNER}/{REPO}/topics", headers={"Authorization": AUTH})
assert captured.get("state") == "open"
def test_list_topics_filter_closed(self):
captured = {}
def mock_fget(path, auth, params=None):
captured.update(params or {})
return [ISSUE_2]
with patch("ifcurl.bcf_api._fget", side_effect=mock_fget):
client.get(f"/bcf/3.0/projects/{OWNER}/{REPO}/topics?topic_status=Closed", headers={"Authorization": AUTH})
assert captured.get("state") == "closed"
def test_list_topics_filter_open(self):
captured = {}
def mock_fget(path, auth, params=None):
captured.update(params or {})
return [ISSUE_1]
with patch("ifcurl.bcf_api._fget", side_effect=mock_fget):
client.get(f"/bcf/3.0/projects/{OWNER}/{REPO}/topics?topic_status=Open", headers={"Authorization": AUTH})
assert captured.get("state") == "open"
def test_list_topics_filter_assigned_to(self):
captured = {}
def mock_fget(path, auth, params=None):
captured.update(params or {})
return [ISSUE_1]
with patch("ifcurl.bcf_api._fget", side_effect=mock_fget):
client.get(f"/bcf/3.0/projects/{OWNER}/{REPO}/topics?assigned_to=alice", headers={"Authorization": AUTH})
assert captured.get("assignee") == "alice"
def test_list_topics_filter_label(self):
captured = {}
def mock_fget(path, auth, params=None):
captured.update(params or {})
return [ISSUE_1]
with patch("ifcurl.bcf_api._fget", side_effect=mock_fget):
client.get(f"/bcf/3.0/projects/{OWNER}/{REPO}/topics?label=urgent", headers={"Authorization": AUTH})
assert captured.get("label") == "urgent"
def test_list_topics_filter_modified_after(self):
captured = {}
def mock_fget(path, auth, params=None):
captured.update(params or {})
return [ISSUE_1]
with patch("ifcurl.bcf_api._fget", side_effect=mock_fget):
client.get(f"/bcf/3.0/projects/{OWNER}/{REPO}/topics?modified_after=2026-01-01T00:00:00Z", headers={"Authorization": AUTH})
assert captured.get("since") == "2026-01-01T00:00:00Z"
def test_get_topic(self): def test_get_topic(self):
tguid = make_topic_guid(OWNER, REPO, 1) tguid = make_topic_guid(OWNER, REPO, 1)
with patch("ifcurl.bcf_api._fget", return_value=ISSUE_1): with patch("ifcurl.bcf_api._fget", return_value=ISSUE_1):

View file

@ -176,3 +176,47 @@ class TestDocumentVersions:
doc_id = encode_document_id(OWNER, REPO, PATH) doc_id = encode_document_id(OWNER, REPO, PATH)
r = _post([doc_id], tags=[], head_commits=[]) r = _post([doc_id], tags=[], head_commits=[])
assert r.json() == [] assert r.json() == []
# ---------------------------------------------------------------------------
# GET /documents/1.0/document-metadata/{document_id}
# ---------------------------------------------------------------------------
FORGEJO_FILE_INFO = {
"name": "building.ifc",
"path": PATH,
"sha": "abcdef1234567890",
"size": 98765,
"type": "file",
}
class TestDocumentMetadata:
def test_returns_200(self):
doc_id = encode_document_id(OWNER, REPO, PATH)
with patch("ifcurl.documents_api._fget", return_value=FORGEJO_FILE_INFO):
r = client.get(f"/documents/1.0/document-metadata/{doc_id}")
assert r.status_code == 200
def test_returns_document_id(self):
doc_id = encode_document_id(OWNER, REPO, PATH)
with patch("ifcurl.documents_api._fget", return_value=FORGEJO_FILE_INFO):
r = client.get(f"/documents/1.0/document-metadata/{doc_id}")
assert r.json()["document_id"] == doc_id
def test_returns_filename(self):
doc_id = encode_document_id(OWNER, REPO, PATH)
with patch("ifcurl.documents_api._fget", return_value=FORGEJO_FILE_INFO):
r = client.get(f"/documents/1.0/document-metadata/{doc_id}")
assert r.json()["name"] == "building.ifc"
def test_invalid_document_id_returns_404(self):
r = client.get("/documents/1.0/document-metadata/not-valid!!!!")
assert r.status_code == 404
def test_forgejo_404_propagates(self):
from fastapi import HTTPException
doc_id = encode_document_id(OWNER, REPO, PATH)
with patch("ifcurl.documents_api._fget", side_effect=HTTPException(status_code=404)):
r = client.get(f"/documents/1.0/document-metadata/{doc_id}")
assert r.status_code == 404