A python library for interacting with HAL+JSON APIs
—
Enhanced collections and helper functions for HAL processing. RestNavigator provides specialized data structures and utilities that support CURIE prefixes, link disambiguation, and HAL-specific data manipulation.
Specialized collection classes that provide HAL-specific functionality for managing links and data.
class CurieDict(dict):
def __init__(self, default_curie: str, d: dict):
"""
Dictionary supporting CURIE prefixes for link relations.
Parameters:
- default_curie: Default CURIE prefix for unprefixed relations
- d: Initial dictionary data
"""
@property
def default_curie(self) -> str:
"""Default CURIE prefix"""class LinkList(list):
def get_by(self, prop: str, val, raise_exc: bool = False):
"""
Get single item by property value.
Parameters:
- prop: Property name to search by
- val: Property value to match
- raise_exc: Whether to raise exception if not found
Returns:
First matching item or None
"""
def getall_by(self, prop: str, val) -> list:
"""
Get all items matching property value.
Parameters:
- prop: Property name to search by
- val: Property value to match
Returns:
List of all matching items
"""
def named(self, name: str):
"""
Get item by name property (HAL standard).
Parameters:
- name: Name value to search for
Returns:
Item with matching name property
"""
def append_with(self, obj, **properties) -> None:
"""
Add item with metadata properties.
Parameters:
- obj: Object to add to list
- **properties: Metadata properties to associate
"""Essential helper functions for URL processing, data manipulation, and HAL-specific operations.
def fix_scheme(url: str) -> str:
"""
Add http:// scheme if missing, validate scheme.
Parameters:
- url: URL to fix
Returns:
URL with proper scheme
Raises:
WileECoyoteException: For invalid schemes
ZachMorrisException: For multiple schemes
"""
def normalize_getitem_args(args) -> list:
"""
Normalize __getitem__ arguments to list.
Parameters:
- args: Arguments from __getitem__ call
Returns:
Normalized list of arguments
"""
def namify(root_uri: str) -> str:
"""
Convert URI to readable API name.
Parameters:
- root_uri: API root URI
Returns:
Human-readable API name
"""
def objectify_uri(relative_uri: str) -> str:
"""
Convert URI to object notation string.
Parameters:
- relative_uri: Relative URI path
Returns:
Object notation representation
"""
def parse_media_type(media_type: str) -> tuple:
"""
Parse HTTP media type header.
Parameters:
- media_type: Media type string from HTTP header
Returns:
Tuple of (type, subtype, parameters)
"""
def getpath(d: dict, json_path: str, default=None, sep: str = '.') -> any:
"""
Get nested dictionary value by path.
Parameters:
- d: Dictionary to search
- json_path: Dot-separated path to value
- default: Default value if path not found
- sep: Path separator character
Returns:
Value at path or default
"""
def getstate(d: dict) -> dict:
"""
Deep copy dict removing HAL keys (_links, _embedded).
Parameters:
- d: Dictionary to clean
Returns:
Clean copy without HAL metadata
"""# CurieDict automatically handles CURIE prefixes
links = CurieDict('ex', {})
# These are equivalent when default_curie is 'ex'
links['ex:users'] = user_navigator
links['users'] = user_navigator # Automatically becomes 'ex:users'
# Access with or without CURIE
user_nav = links['users'] # Works
user_nav = links['ex:users'] # Also works
# IANA standard relations have precedence
links['next'] = next_page # Standard 'next' relation
links['ex:next'] = custom_next # Custom next relation
print(links['next']) # Returns standard 'next', not 'ex:next'# LinkList for managing HAL link arrays
links = LinkList()
# Add links with properties
links.append_with(widget1_nav, name='widget1', profile='widget')
links.append_with(widget2_nav, name='widget2', profile='widget')
links.append_with(gadget_nav, name='gadget1', profile='gadget')
# Find by property
widget1 = links.get_by('name', 'widget1')
gadget = links.get_by('profile', 'gadget')
# Get all matching items
all_widgets = links.getall_by('profile', 'widget')
# Use standard HAL name property
specific_item = links.named('gadget1') # Same as get_by('name', 'gadget1')
# Handle missing items
missing = links.get_by('name', 'nonexistent', raise_exc=False) # Returns Nonefrom restnavigator.utils import fix_scheme, namify, objectify_uri
# Fix URL schemes
clean_url = fix_scheme('api.example.com') # Returns 'http://api.example.com'
clean_url = fix_scheme('https://api.example.com') # Returns unchanged
# Generate API names
api_name = namify('https://api.github.com/v3') # Returns 'Github'
api_name = namify('http://haltalk.herokuapp.com') # Returns 'Haltalk'
# Convert URIs to object notation
obj_notation = objectify_uri('/users/123/posts') # Returns 'users.123.posts'
obj_notation = objectify_uri('/api/v1/repos') # Returns 'api.v1.repos'from restnavigator.utils import getpath, getstate
# Navigate nested data structures
data = {
'user': {
'profile': {
'name': 'John Doe',
'settings': {
'theme': 'dark'
}
}
}
}
# Get nested values
name = getpath(data, 'user.profile.name') # Returns 'John Doe'
theme = getpath(data, 'user.profile.settings.theme') # Returns 'dark'
missing = getpath(data, 'user.missing.field', 'default') # Returns 'default'
# Custom separator
theme = getpath(data, 'user/profile/settings/theme', sep='/') # Returns 'dark'from restnavigator.utils import getstate
# Remove HAL metadata from response data
hal_response = {
'id': 1,
'name': 'John Doe',
'_links': {
'self': {'href': '/users/1'},
'posts': {'href': '/users/1/posts'}
},
'_embedded': {
'posts': [
{
'id': 1,
'title': 'First Post',
'_links': {'self': {'href': '/posts/1'}}
}
]
}
}
# Get clean state without HAL metadata
clean_data = getstate(hal_response)
# Returns: {'id': 1, 'name': 'John Doe'}from restnavigator.utils import parse_media_type
# Parse content-type headers
main_type, sub_type, params = parse_media_type('application/hal+json; charset=utf-8')
# main_type: 'application'
# sub_type: 'hal+json'
# params: {'charset': 'utf-8'}
# Handle complex media types
main_type, sub_type, params = parse_media_type('text/html; charset=utf-8; boundary=something')
# params: {'charset': 'utf-8', 'boundary': 'something'}from restnavigator.utils import normalize_getitem_args
# Normalize different argument patterns
args1 = normalize_getitem_args('single') # Returns ['single']
args2 = normalize_getitem_args(('a', 'b')) # Returns ['a', 'b']
args3 = normalize_getitem_args(['x', 'y', 'z']) # Returns ['x', 'y', 'z']
# Used internally for bracket notation like:
# api['users', 'posts', 0] # Gets normalized to ['users', 'posts', 0]# Building dynamic link collections
def build_link_collection(api_links):
"""Build organized link collection from API links"""
organized = {}
for rel, links in api_links.items():
if isinstance(links, list):
link_list = LinkList()
for link in links:
# Extract properties from link metadata
props = getattr(link, 'props', {})
link_list.append_with(link, **props)
organized[rel] = link_list
else:
organized[rel] = links
return organized
# Custom CURIE handling
def smart_curie_lookup(curie_dict, relation):
"""Smart lookup with fallback logic"""
# Try exact match first
if relation in curie_dict:
return curie_dict[relation]
# Try with default CURIE
if curie_dict.default_curie:
full_rel = f"{curie_dict.default_curie}:{relation}"
if full_rel in curie_dict:
return curie_dict[full_rel]
# Try without CURIE if relation includes one
if ':' in relation:
bare_rel = relation.split(':', 1)[1]
if bare_rel in curie_dict:
return curie_dict[bare_rel]
return NoneInstall with Tessl CLI
npx tessl i tessl/pypi-restnavigator