sintonia/api_client/api_clients/utils.py

225 lines
6.6 KiB
Python
Raw Normal View History

import datetime
import json
2020-01-30 14:47:36 +01:00
import logging
import socket
2021-09-29 12:46:31 +02:00
from time import sleep
2020-01-30 14:47:36 +01:00
import requests
from requests.auth import AuthBase
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
def get_protocol(config):
2021-05-27 16:23:02 +02:00
positive_values = ["Yes", "yes", "True", "true", True]
port = config["general"].get("base_port", 80)
force_ssl = config["general"].get("force_ssl", False)
2020-01-30 14:47:36 +01:00
if force_ssl in positive_values:
2021-05-27 16:23:02 +02:00
protocol = "https"
2020-01-30 14:47:36 +01:00
else:
2021-05-27 16:23:02 +02:00
protocol = config["general"].get("protocol")
2020-01-30 14:47:36 +01:00
if not protocol:
protocol = str(("http", "https")[int(port) == 443])
return protocol
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
class UrlParamDict(dict):
def __missing__(self, key):
2021-05-27 16:23:02 +02:00
return "{" + key + "}"
class UrlException(Exception):
pass
2020-01-30 14:47:36 +01:00
class IncompleteUrl(UrlException):
def __init__(self, url):
self.url = url
def __str__(self):
return "Incomplete url: '{}'".format(self.url)
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
class UrlBadParam(UrlException):
def __init__(self, url, param):
self.url = url
self.param = param
def __str__(self):
return "Bad param '{}' passed into url: '{}'".format(self.param, self.url)
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
class KeyAuth(AuthBase):
def __init__(self, key):
self.key = key
def __call__(self, r):
2021-05-27 16:23:02 +02:00
r.headers["Authorization"] = "Api-Key {}".format(self.key)
2020-01-30 14:47:36 +01:00
return r
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
class ApcUrl:
2021-05-27 16:23:02 +02:00
"""A safe abstraction and testable for filling in parameters in
2020-01-30 14:47:36 +01:00
api_client.cfg"""
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
def __init__(self, base_url):
self.base_url = base_url
def params(self, **params):
temp_url = self.base_url
for k, v in params.items():
wrapped_param = "{" + k + "}"
if not wrapped_param in temp_url:
raise UrlBadParam(self.base_url, k)
temp_url = temp_url.format_map(UrlParamDict(**params))
return ApcUrl(temp_url)
def url(self):
2021-05-27 16:23:02 +02:00
if "{" in self.base_url:
2020-01-30 14:47:36 +01:00
raise IncompleteUrl(self.base_url)
else:
return self.base_url
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
class ApiRequest:
2021-05-27 16:23:02 +02:00
API_HTTP_REQUEST_TIMEOUT = 30 # 30 second HTTP request timeout
2020-01-30 14:47:36 +01:00
def __init__(self, name, url, logger=None, api_key=None):
self.name = name
2021-05-27 16:23:02 +02:00
self.url = url
2020-01-30 14:47:36 +01:00
self.__req = None
if logger is None:
self.logger = logging
else:
self.logger = logger
self.auth = KeyAuth(api_key)
2021-08-31 20:11:39 +02:00
def __call__(self, *, _post_data=None, _put_data=None, params=None, **kwargs):
2020-01-30 14:47:36 +01:00
final_url = self.url.params(**kwargs).url()
self.logger.debug(final_url)
try:
if _post_data is not None:
res = requests.post(
2021-05-27 16:23:02 +02:00
final_url,
data=_post_data,
auth=self.auth,
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT,
)
elif _put_data is not None:
res = requests.put(
final_url,
data=_put_data,
auth=self.auth,
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT,
)
2020-01-30 14:47:36 +01:00
else:
res = requests.get(
2021-05-27 16:23:02 +02:00
final_url,
params=params,
auth=self.auth,
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT,
)
# Check for bad HTTP status code
res.raise_for_status()
if "application/json" in res.headers["content-type"]:
return res.json()
return res
2020-01-30 14:47:36 +01:00
except requests.exceptions.Timeout:
2021-05-27 16:23:02 +02:00
self.logger.error("HTTP request to %s timed out", final_url)
2020-01-30 14:47:36 +01:00
raise
except requests.exceptions.HTTPError:
self.logger.error(
f"{res.request.method} {res.request.url} request failed '{res.status_code}':"
f"\nPayload: {res.request.body}"
f"\nResponse: {res.text}"
)
raise
2020-01-30 14:47:36 +01:00
def req(self, *args, **kwargs):
2021-05-27 16:23:02 +02:00
self.__req = lambda: self(*args, **kwargs)
2020-01-30 14:47:36 +01:00
return self
def retry(self, n, delay=5):
"""Try to send request n times. If after n times it fails then
we finally raise exception"""
2021-05-27 16:23:02 +02:00
for i in range(0, n - 1):
2020-01-30 14:47:36 +01:00
try:
return self.__req()
except Exception:
2021-09-29 12:46:31 +02:00
sleep(delay)
2020-01-30 14:47:36 +01:00
return self.__req()
2021-05-27 16:23:02 +02:00
2020-01-30 14:47:36 +01:00
class RequestProvider:
2021-05-27 16:23:02 +02:00
"""Creates the available ApiRequest instance that can be read from
a config file"""
2020-01-30 14:47:36 +01:00
def __init__(self, cfg, endpoints):
self.config = cfg
self.requests = {}
if self.config["general"]["base_dir"].startswith("/"):
self.config["general"]["base_dir"] = self.config["general"]["base_dir"][1:]
protocol = get_protocol(self.config)
2021-05-27 16:23:02 +02:00
base_port = self.config["general"]["base_port"]
base_url = self.config["general"]["base_url"]
base_dir = self.config["general"]["base_dir"]
api_base = self.config["api_base"]
2020-01-30 14:47:36 +01:00
api_url = "{protocol}://{base_url}:{base_port}/{base_dir}{api_base}/{action}".format_map(
2021-05-27 16:23:02 +02:00
UrlParamDict(
protocol=protocol,
base_url=base_url,
base_port=base_port,
base_dir=base_dir,
api_base=api_base,
)
)
2020-01-30 14:47:36 +01:00
self.url = ApcUrl(api_url)
# Now we must discover the possible actions
for action_name, action_value in endpoints.items():
new_url = self.url.params(action=action_value)
2021-05-27 16:23:02 +02:00
if "{api_key}" in action_value:
new_url = new_url.params(api_key=self.config["general"]["api_key"])
self.requests[action_name] = ApiRequest(
action_name, new_url, api_key=self.config["general"]["api_key"]
)
2020-01-30 14:47:36 +01:00
def available_requests(self):
return list(self.requests.keys())
def __contains__(self, request):
return request in self.requests
def __getattr__(self, attr):
if attr in self:
return self.requests[attr]
else:
return super(RequestProvider, self).__getattribute__(attr)
2021-05-27 16:23:02 +02:00
2021-09-29 12:46:31 +02:00
def time_in_seconds(value):
2021-05-27 16:23:02 +02:00
return (
2021-09-29 12:46:31 +02:00
value.hour * 60 * 60
+ value.minute * 60
+ value.second
+ value.microsecond / 1000000.0
2021-05-27 16:23:02 +02:00
)
2020-01-30 14:47:36 +01:00
2021-09-29 12:46:31 +02:00
def time_in_milliseconds(value):
return time_in_seconds(value) * 1000
2020-01-30 14:47:36 +01:00
2021-05-27 16:23:02 +02:00
def fromisoformat(time_string):
"""
This is required for Python 3.6 support. datetime.time.fromisoformat was
only added in Python 3.7. Until LibreTime drops Python 3.6 support, this
wrapper uses the old way of doing it.
"""
2021-06-04 20:01:17 +02:00
try:
datetime_obj = datetime.datetime.strptime(time_string, "%H:%M:%S.%f")
except ValueError:
datetime_obj = datetime.datetime.strptime(time_string, "%H:%M:%S")
return datetime_obj.time()