"""Defines the API for interacting with the HITRAN database."""
from json import loads
from urllib.error import HTTPError
from urllib.request import build_opener, install_opener, ProxyHandler, urlopen
[docs]class HitranWebApi(object):
"""Controls access to HITRAN's web API.
Attributes:
api_key: String hitran.org api key.
api_version: String version of the api to use.
cross_section_directory: String directory where cross section files are located.
host: URL to retrieve the data from.
parameters: List of Struct objects describing the HITRAN parameters.
proxy: Web proxy (optional).
timestamp: String time stamp telling when the server was accessed.
transition_directory: String directory where transitions files are located.
"""
def __init__(self, api_key, api_version="v2", host="https://hitran.org",
proxy=None):
"""Initializes object.
Args:
api_key: String hitran.org api key.
api_version: String version of the api to use.
host: URL to retrieve the data from.
proxy: Currently not used.
"""
self.api_key = api_key
self.api_version = api_version
self.host = host
self.proxy = proxy
server_info = self._download_server_info()
self.transition_directory = server_info["content"]["data"]["results_dir"]
self.cross_section_directory = server_info["content"]["data"]["xsec_dir"]
self.timestamp = server_info["timestamp"]
self.parameters = self._download_parameters_metadata()
def _download(self, url, chunk):
"""Downloads data from a url.
Args:
url: URL to retrieve data from.
chunk: Size of data reads in bytes.
Returns:
String containing the response data.
"""
if self.proxy:
install_opener(build_opener(ProxyHandler(self.proxy)))
response = urlopen(url)
data = []
while True:
buf = response.read(chunk)
if not buf:
break
data.append(buf.decode("utf-8"))
return "".join(data)
def _download_file(self, prefix, name, chunk=64*1024*1024):
"""Downloads a data file from hitran.org.
Returns:
String containing the contents of the data file.
"""
return self._download("/".join([self.host, prefix, name]), chunk)
def _download_parameters_metadata(self, pattern=None):
"""Downloads metadata about the available HITRAN parameters.
Args:
pattern: Substring pattern.
Returns:
List of Struct objects containing the response data.
"""
query = None if pattern is None else Query(name__icontains=pattern)
return [Struct(**x) for x in
self._download_section("parameter-metas", query)["content"]["data"]]
def _download_section(self, api_section, query=None, chunk=1024*1024):
"""Downloads data from the hitran.org website.
Args:
api_section: String name of the section of the database to use.
query: String describing the HTML options.
chunk: Size of data reads in bytes.
Returns:
JSON string containing the response data.
"""
url = "/".join([self.host, "api", self.api_version, self.api_key, api_section])
if query is not None:
url = "?".join([url, query.string])
if self.proxy:
install_opener(build_opener(ProxyHandler(self.proxy)))
return loads(self._download(url, chunk))
def _download_server_info(self):
"""Downloads internal information about the hitran.org server.
Returns:
JSON string containing the response data.
"""
return self._download_section("info")
[docs] def download_data_sources(self, ids=None):
"""Downloads information about the source of the line data (papers, etc.)
Args:
ids: Isotopologue ids.
Returns:
JSON string containing the response data.
"""
query = None if ids is None else Query(id__in=ids)
return self._download_section("sources", query)["content"]["data"]
[docs] def download_molecules(self):
"""Downloads the molecules available in HITRAN.
Returns:
List of Struct objects containing the response data.
"""
return [Struct(**x) for x in self._download_section("molecules")["content"]["data"]]
[docs] def download_isotopologues(self, molecules):
"""Downloads the isotopologues available in HITRAN.
Args:
molecules: List of Struct objects.
Returns:
List of Struct objects containing the response data.
"""
if type(molecules) not in [list, tuple]:
molecules = [molecules, ]
ids = [x.id for x in molecules]
return [Struct(**x) for x in self._download_section("isotopologues",
Query(molecule_id__in=ids))["content"]["data"]]
[docs] def download_transitions(self, isotopologues, numin, numax, parameters=None):
"""Downloads transitions for isotopologues available in HITRAN.
Args:
isotopologues: List of Struct objects.
numin: Wavenumber lower bound [cm-1].
numax: Wavenumber upper bound [cm-1].
parameters: List of parameters to download.
Returns:
List of Struct objects containing the response data.
"""
if type(isotopologues) not in [list, tuple]:
isotopologues = [isotopologues, ]
ids = [x.id for x in isotopologues]
if not ids:
raise NoIsotopologueError("no isotopologues present.")
if parameters is None:
parameters = [x.name for x in self.parameters][:22]
query = Query(iso_ids_list=ids, numin=numin, numax=numax, head=False,
fixwidth=0, request_params=",".join(parameters))
try:
name = self._download_section("transitions", query)["content"]["data"]
except HTTPError:
raise NoTransitionsError("no transitions found for {}.".format(
isotopologues[0].molecule_alias))
data = self._download_file(self.transition_directory, name)
# Parse the file.
transitions = []
type_mapping = {"float": float, "int": int, "str": str}
types = [type_mapping[x.type] for x in self.parameters]
for line in data.split("\n"):
line = line.strip()
if not line:
continue
try:
transitions.append(Struct(**{x: y(z) for x, y, z in
zip(parameters, types, line.split(","))}))
except ValueError:
print("skipping transition: {}".format(line))
return transitions
[docs] def download_cross_sections(self, molecules):
"""Downloads cross-sections for molecules available in the HITRAN database.
Args:
molecules: List of Struct objcts.
Returns:
List of Struct objects containing the response data.
"""
if type(molecules) not in [list, tuple]:
molecules = [molecules, ]
ids = [x.id for x in molecules]
query = Query(molecule_id__in=ids)
bands = self._download_section("cross-sections", query)["content"]["data"]
cross_sections = []
for band in bands:
data = self._download_file(self.cross_section_directory, band["filename"])
attrs = {"data": data}
attrs.update(band)
cross_sections.append(Struct(**attrs))
return cross_sections
[docs]class NoCrossSectionError(BaseException):
pass
[docs]class NoIsotopologueError(BaseException):
pass
[docs]class NoTransitionsError(BaseException):
pass
[docs]class Query(object):
"""URL parameter (query string) helper class.
Attributes:
string: String containing URL parameters.
"""
def __init__(self, **argv):
self.string = "&".join(["{}={}".format(key, self.process(argv[key])) for key in argv])
def __and__(self, q):
q = Query()
q.string = "&".join([self.string, q.string])
return q
[docs] @staticmethod
def process(val):
"""Process argument value and convert to string."""
if type(val) in [bool, float, int, str]:
return str(val)
if type(val) in [list, set, tuple]:
return ",".join(str(v) for v in val)
raise TypeError("bad type for query: '{}'".format(val))
[docs]class Struct(object):
def __init__(self, **attrs):
self.__dict__.update(attrs)