"""Helper utilities for working with HAL+JSON responses from OpenProject API."""
from typing import Any
def get_link_href(hal_object: dict[str, Any], link_name: str) -> str | None:
"""Extract href from HAL _links section.
Args:
hal_object: HAL+JSON object containing _links
link_name: Name of the link to extract (e.g., 'self', 'update', 'project')
Returns:
The href string if found, None otherwise
"""
links = hal_object.get("_links", {})
link = links.get(link_name, {})
return link.get("href")
def get_embedded_resource(hal_object: dict[str, Any], resource_name: str) -> dict[str, Any] | None:
"""Extract a single embedded resource from HAL _embedded section.
Args:
hal_object: HAL+JSON object containing _embedded
resource_name: Name of the embedded resource (e.g., 'assignee', 'project')
Returns:
The embedded resource dict if found, None otherwise
"""
embedded = hal_object.get("_embedded", {})
return embedded.get(resource_name)
def get_embedded_collection(hal_object: dict[str, Any], collection_name: str) -> list[dict[str, Any]]:
"""Extract a collection of elements from HAL _embedded section.
OpenProject typically nests collections in _embedded.elements.
Args:
hal_object: HAL+JSON object containing _embedded
collection_name: Name of the collection (usually the response itself has the collection)
Returns:
List of elements from the collection, empty list if not found
"""
embedded = hal_object.get("_embedded", {})
# Check if elements are directly in _embedded
if "elements" in embedded:
return embedded.get("elements", [])
# Otherwise, check if collection_name contains nested elements
collection = embedded.get(collection_name, {})
if isinstance(collection, dict):
nested_embedded = collection.get("_embedded", {})
return nested_embedded.get("elements", [])
return []
def extract_id_from_href(href: str) -> int | None:
"""Extract numeric ID from an API href.
Args:
href: API href like '/api/v3/work_packages/123'
Returns:
The numeric ID if found, None otherwise
"""
if not href:
return None
try:
# Remove trailing slash and split by /
parts = href.rstrip("/").split("/")
# Get the last part and convert to int
return int(parts[-1])
except (ValueError, IndexError):
return None
def build_formattable(text: str, format: str = "markdown") -> dict[str, str]:
"""Build an OpenProject Formattable object for text content.
Formattable objects are used for comments, descriptions, and other text fields.
Args:
text: The text content
format: The format type (default: 'markdown')
Returns:
A dict with 'format' and 'raw' keys
"""
return {
"format": format,
"raw": text
}
def build_link(href: str, title: str | None = None) -> dict[str, Any]:
"""Build a HAL link object.
Args:
href: The link href (e.g., '/api/v3/projects/test')
title: Optional link title
Returns:
A dict with 'href' and optionally 'title'
"""
link = {"href": href}
if title:
link["title"] = title
return link