Source code for ptr.esa.pointing_tool
"""ESA pointing tool API module."""
import json
from hashlib import md5
from pathlib import Path
from tempfile import gettempdir
from urllib import request
PT_CACHE = Path(gettempdir()) / 'pointing-tool-api-cache'
PT_CACHE.mkdir(parents=True, exist_ok=True)
[docs]def pt_api(url, use_cache=True):
"""Pointing tool API request.
Parameters
----------
url: str
Endpoint URL to request.
use_cache: bool, optional
Cache the API request and fallback to the cache
if the API resource is not available
Returns
-------
list or dict
Parsed JSON response.
Raises
------
FileNotFoundError
If API service is not available and no cache was found.
"""
try:
with request.urlopen(url) as resp:
data = json.loads(resp.read())
if use_cache:
pt_cache(url).write_text(json.dumps(data))
except request.URLError:
if use_cache and (cache := pt_cache(url)).exists():
data = json.loads(cache.read_text())
else:
raise FileNotFoundError(
'Pointing tool service unavailable and cache not found.'
) from None
return data
[docs]def pt_cache(url) -> Path:
"""Pointing tool cache file based on URL."""
return PT_CACHE / (md5(url.encode()).hexdigest() + '.json')
[docs]class PointingToolApi:
"""Pointing tool API object.
Parameters
----------
endpoint: str
API endpoint url.
cache: bool, optional
Enable API caching (default: True).
"""
def __init__(self, endpoint, use_cache=True):
self.endpoint = endpoint
self.use_cache = use_cache
self._contexts = None
def __str__(self):
return self.endpoint
def __repr__(self):
contexts = '\n- '.join([
'Contexts:',
*[str(context) for context in self]
])
return f'<{self.__class__.__name__}> {self} | {contexts}'
def __contains__(self, item):
for context in self.contexts:
if context == item:
return True
return False
def __getitem__(self, item):
for context in self.contexts:
if context == item:
return context
raise KeyError(f'Context: `{item}` not found.')
def __len__(self):
return len(self.contexts)
def __iter__(self):
return iter(self.contexts)
@property
def agm_url(self):
"""PT AGM endpoint URL."""
return f'{self}/agm'
@property
def url_contexts(self):
"""PT trajectory contexts URL."""
return f'{self}/assets/trajectory_contexts.json'
@property
def contexts(self):
"""Pointing tool context."""
if self._contexts is None:
self._contexts = self._load_contexts()
return self._contexts
def _load_contexts(self):
"""Load contexts list from the API."""
return [
PointingToolContext(self, **context)
for context in reversed(pt_api(self.url_contexts, use_cache=self.use_cache))
]
[docs]class PointingToolContext:
"""Pointing tool API context object.
Parameters
----------
name: str
API endpoint url.
context: str
API context key.
"""
def __init__(self, api, name=None, context=None):
self.api = api
self.name = name
self.context_id = context
self._info = None
def __str__(self):
return self.name
def __repr__(self):
infos = '\n- '.join([
f'{self}',
*[f'{k}: {v}' for k, v in self.info.items() if not isinstance(v, list)]
])
return f'<{self.__class__.__name__}> {infos}'
def __eq__(self, other):
return self.name == str(other) or self.context_id == str(other)
@property
def url(self):
"""Context definition URL."""
return f'{self.api}/{self.context_id}/serviceinfo'
@property
def info(self):
"""Context infos."""
if self._info is None:
self._info = pt_api(self.url, use_cache=self.api.use_cache)
return self._info
@property
def mk(self):
"""Context metakernel."""
return self.info['metakernel']
JUICE_POINTING_TOOL = PointingToolApi('https://juicept.esac.esa.int')
POINTING_TOOL_ENDPOINTS = {
'JUICE_API': JUICE_POINTING_TOOL,
}