Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 9, 2025

This PR adds the current git revision hash to the /api endpoint response, allowing clients to identify which version of the code is running.

Changes

API Response

The /api endpoint now includes a version field at the root level of the response:

{
  "version": "779cdb5",
  "http://example.com/api": {
    "description": "List all APIs"
  },
  "http://example.com/api/:id/manifest.json": {
    "description": "Generate readium manifest for resource",
    "args": {"source": ["ia", "ol"], "format": ["epub", "pdf"]}
  }
}

Implementation Details

prs/routes/api.py:

  • Added get_git_revision_short_hash() function that uses subprocess.check_output() to execute git rev-parse --short HEAD
  • Returns the 7-character short commit hash, or None if git is unavailable
  • Uses only Python standard library (subprocess)
  • Updated apis() endpoint to call this function and include the version in the response

docker/Dockerfile:

  • Added git installation to ensure the git executable is available at runtime
  • Uses apt-get with --no-install-recommends to minimize image size

Benefits

  • Runtime version identification: Easily identify which commit is deployed
  • No hardcoded values: Version is queried dynamically from the git repository
  • Minimal dependencies: Uses only Python standard library, no additional Python packages required
  • Graceful degradation: Returns null if git is not available instead of crashing

The .git directory is already mounted in the container via the compose.yaml volume mapping (.:/app), so the git repository is accessible at runtime.

Original prompt

Update the /api endpoint so that it includes the current version of the code (the short git revision hash). Do not hardcode or write the value to a file; instead, query it from the project's git repository at runtime. Avoid introducing large dependencies—this should use only the Python standard library. The version should be included in the root /api response. Ensure the required package (git executable) is available, but do not add non-stdlib dependencies to requirements.txt or similar. Tests do not need to be updated for this small change.

The following is the updated code for prs/routes/api.py:

#!/usr/bin/env python

"""
API routes for PRS,

:copyright: (c) 2025 by AUTHORS
:license: see LICENSE for more details

"""

import base64
import requests
import internetarchive as ia
from urllib.parse import quote
from fastapi import (
APIRouter,
Request,
HTTPException,
status
)
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from prs.configs import BASE_URL, PORT, READIUM_HOST_PORT
import subprocess

router = APIRouter()

def get_git_revision_short_hash():
try:
return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
cwd=None
).decode("ascii").strip()
except Exception:
return None

def ia_get_epub_filepath(item_id):
if '$' in item_id:
item_id, filepath = item_id.replace('$', '/').split('/', 1)
else:
item = ia.get_item(item_id)
for file in item.files:
if file['name'].endswith('.epub'):
filepath = file['name']
return f"https://archive.org/download/{item_id}/{filepath}"

def ol_get_epub_filepath(olid):
# fetch olid from openlibrary and get url (if ends with epub)
pass

def encode_book_path(source: str, book_id: str) -> str:
source_to_filepath = {
"ia": ia_get_epub_filepath,
#"ol": lambda: f"" # use openlibrary.org/search.json to get url of ebook
}
filepath = source_to_filepathsource
encoded_filepath = base64.b64encode(filepath.encode()).decode()
return encoded_filepath.replace('/', '_').replace('+', '-').replace('=', '')

def prs_uri(request: Request):
if host := request.headers.get('x-forwarded-host'):
return f"{request.url.scheme}://{host}{BASE_URL}"
port = f":{PORT}" if PORT not in {80, 443} else ""
return f"{request.url.scheme}://{request.url.hostname}{port}{BASE_URL}"

@router.get('/', status_code=status.HTTP_200_OK)
async def apis(request: Request):
version = get_git_revision_short_hash()
return {
"version": version,
f"{prs_uri(request)}/api": {
"description": "List all APIs",
},
f"{prs_uri(request)}/api/:id/manifest.json": {
"description": "Generate readium manifest for resource",
"args": {"source": ["ia", "ol"], "format": ["epub", "pdf"]}
}
}

@router.get("/{source}/{book_id}/read")
async def redirect_reader(request: Request, source: str, book_id: str):
manifest_url = f"{prs_uri(request)}/api/{source}/{book_id}/manifest.json"
manifest_uri = quote(manifest_url, safe='')
return RedirectResponse(f"https://playground.readium.org/read/manifest/{manifest_uri}")

@router.get("/{source}/{book_id}/manifest.json")
async def get_manifest(request: Request, source: str, book_id: str):
def patch_manifest(manifest):
manifest_uri = f"{prs_uri(request)}/api/{source}/{book_id}/manifest.json"
encoded_manifest_uri = quote(manifest_uri, safe='')
for i in range(len(manifest['links'])):
if manifest['links'][i].get('rel') == 'self':
manifest['links'][i]['href'] = encoded_manifest_uri
return manifest

# TODO: s3 permission/auth checks go here for protected epubs, or decorate this route

readium_uri = f"http://{READIUM_HOST_PORT}/{encode_book_path(source, book_id)}/manifest.json"
manifest = requests.get(readium_uri).json()
return patch_manifest(manifest)

Proxy for all other readium requests

@router.get("/{source}/{book_id}/{readium_uri:path}")
async def proxy_readium(request: Request, source: str, book_id: str, readium_uri: str, format: str=".epub"):
# TODO: permission/auth checks go here, or decorate this route
readium_url = f"http://{READIUM_HOST_PORT}/{encode_book_path(source, book_id)}/{readium_uri}"
r = requests.get(readium_url, params=dict(request.query_params))
if readium_url.endswith('.json'):
return r.json()
content_type = r.headers.get("Content-Type", "application/octet-stream")
return Response(content=r.content, media_type=content_type)

*This pull request was created as a result of the following prompt from Copilot chat.* > Update the /api endpoint so that it includes the current version of the code (the short git revision hash). Do not hardcode or write the value to a file; instead, query it from the project's git repository at runtime. Avoid introducing large dependencies—this should use only the Python standard library. The version should be included in the root /api response. Ensure the required package (git executable) is available, but do not add non-stdlib dependencies to requirements.txt or similar. Tests do not need to be updated for this small change. > > The following is the updated code for prs/routes/api.py: > > #!/usr/bin/env python > > """ > API routes for PRS, > > ©️ (c) 2025 by AUTHORS > :license: see LICENSE for more details > """ > > import base64 > import requests > import internetarchive as ia > from urllib.parse import quote > from fastapi import ( > APIRouter, > Request, > HTTPException, > status > ) > from fastapi.responses import HTMLResponse, RedirectResponse, Response > from prs.configs import BASE_URL, PORT, READIUM_HOST_PORT > import subprocess > > router = APIRouter() > > def get_git_revision_short_hash(): > try: > return subprocess.check_output( > ["git", "rev-parse", "--short", "HEAD"], > cwd=None > ).decode("ascii").strip() > except Exception: > return None > > def ia_get_epub_filepath(item_id): > if '$' in item_id: > item_id, filepath = item_id.replace('$', '/').split('/', 1) > else: > item = ia.get_item(item_id) > for file in item.files: > if file['name'].endswith('.epub'): > filepath = file['name'] > return f"https://archive.org/download/{item_id}/{filepath}" > > def ol_get_epub_filepath(olid): > # fetch olid from openlibrary and get url (if ends with epub) > pass > > def encode_book_path(source: str, book_id: str) -> str: > source_to_filepath = { > "ia": ia_get_epub_filepath, > #"ol": lambda: f"" # use openlibrary.org/search.json to get url of ebook > } > filepath = source_to_filepath[source](book_id) > encoded_filepath = base64.b64encode(filepath.encode()).decode() > return encoded_filepath.replace('/', '_').replace('+', '-').replace('=', '') > > def prs_uri(request: Request): > if host := request.headers.get('x-forwarded-host'): > return f"{request.url.scheme}://{host}{BASE_URL}" > port = f":{PORT}" if PORT not in {80, 443} else "" > return f"{request.url.scheme}://{request.url.hostname}{port}{BASE_URL}" > > @router.get('/', status_code=status.HTTP_200_OK) > async def apis(request: Request): > version = get_git_revision_short_hash() > return { > "version": version, > f"{prs_uri(request)}/api": { > "description": "List all APIs", > }, > f"{prs_uri(request)}/api/:id/manifest.json": { > "description": "Generate readium manifest for resource", > "args": {"source": ["ia", "ol"], "format": ["epub", "pdf"]} > } > } > > @router.get("/{source}/{book_id}/read") > async def redirect_reader(request: Request, source: str, book_id: str): > manifest_url = f"{prs_uri(request)}/api/{source}/{book_id}/manifest.json" > manifest_uri = quote(manifest_url, safe='') > return RedirectResponse(f"https://playground.readium.org/read/manifest/{manifest_uri}") > > @router.get("/{source}/{book_id}/manifest.json") > async def get_manifest(request: Request, source: str, book_id: str): > def patch_manifest(manifest): > manifest_uri = f"{prs_uri(request)}/api/{source}/{book_id}/manifest.json" > encoded_manifest_uri = quote(manifest_uri, safe='') > for i in range(len(manifest['links'])): > if manifest['links'][i].get('rel') == 'self': > manifest['links'][i]['href'] = encoded_manifest_uri > return manifest > > # TODO: s3 permission/auth checks go here for protected epubs, or decorate this route > > readium_uri = f"http://{READIUM_HOST_PORT}/{encode_book_path(source, book_id)}/manifest.json" > manifest = requests.get(readium_uri).json() > return patch_manifest(manifest) > > # Proxy for all other readium requests > @router.get("/{source}/{book_id}/{readium_uri:path}") > async def proxy_readium(request: Request, source: str, book_id: str, readium_uri: str, format: str=".epub"): > # TODO: permission/auth checks go here, or decorate this route > readium_url = f"http://{READIUM_HOST_PORT}/{encode_book_path(source, book_id)}/{readium_uri}" > r = requests.get(readium_url, params=dict(request.query_params)) > if readium_url.endswith('.json'): > return r.json() > content_type = r.headers.get("Content-Type", "application/octet-stream") > return Response(content=r.content, media_type=content_type) >

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: mekarpeles <978325+mekarpeles@users.noreply.github.com>
Copilot AI changed the title [WIP] Update /api endpoint to include current git version Add git version to /api endpoint response Oct 9, 2025
Copilot AI requested a review from mekarpeles October 9, 2025 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants