1
|
import json
|
2
|
import requests
|
3
|
import logging
|
4
|
from time import sleep
|
5
|
from diskcache import Deque
|
6
|
from requests import HTTPError, ConnectionError
|
7
|
from requests.exceptions import InvalidSchema
|
8
|
|
9
|
|
10
|
_uri = None # server uri (url, port, and endpoint)
|
11
|
_cache = None # cache (failed payloads)
|
12
|
_config = None # instance of Config
|
13
|
|
14
|
|
15
|
def api_client_set_config(config):
|
16
|
"""Initializes the client API module.
|
17
|
|
18
|
This function is meant to be called prior to calling any other function
|
19
|
of the API module. It stores the instance of Config (config manager)
|
20
|
into a private variable. It also initializes the cache for unsuccessful
|
21
|
payloads and constructs a URI (endpoint on the server side).
|
22
|
|
23
|
:param config: instance of Config which holds all values defined
|
24
|
in the configuration file.
|
25
|
"""
|
26
|
# Store the variables globally within the module (file).
|
27
|
global _config, _cache, _uri
|
28
|
|
29
|
# Store the instance of Config and initialize the cache.
|
30
|
_config = config
|
31
|
_cache = _init_cache()
|
32
|
|
33
|
# Creates the URI which is made of the server url, port, and path (endpoint).
|
34
|
_uri = config.server_url + ":" + config.server_port + config.server_endpoint
|
35
|
|
36
|
|
37
|
def _init_cache():
|
38
|
""" Initializes and returns a disk-based cache.
|
39
|
|
40
|
The cache holds payloads that the application failed
|
41
|
to send to the server. It periodically attempts to resent
|
42
|
them to the server. All parameters can be seen in the
|
43
|
configuration file.
|
44
|
|
45
|
:return: instance of a new cache (Deque - FIFO)
|
46
|
"""
|
47
|
return Deque(directory=_config.cache_dir)
|
48
|
|
49
|
|
50
|
def send_data(payload: dict):
|
51
|
"""Sends a payload off to the server.
|
52
|
|
53
|
This function is called whenever a USB is connected
|
54
|
or disconnected. If there is no internet connection or the
|
55
|
server is not up and running, the payload will be stored
|
56
|
into the disk cache.
|
57
|
|
58
|
:param payload:
|
59
|
"""
|
60
|
# Make sure that the URI has been constructed properly.
|
61
|
# It's supposed to be done by calling the api_client_set_config function
|
62
|
# with appropriate parameters.
|
63
|
if _uri is None:
|
64
|
logging.warning(f"sending payload = {payload} failed because uri is set to None")
|
65
|
_cache_failed_payload(payload)
|
66
|
return
|
67
|
try:
|
68
|
logging.info(f"sending payload = {payload} to {_uri}")
|
69
|
response = requests.post(url=_uri, data=json.dumps(payload))
|
70
|
logging.info(f"response text: {response.text}")
|
71
|
except (ConnectionError, InvalidSchema):
|
72
|
logging.warning(f"sending payload = {payload} to {_uri} failed")
|
73
|
_cache_failed_payload(payload)
|
74
|
except HTTPError as error:
|
75
|
logging.error(f"HTTP Error ({_uri}) payload = {payload}, {error}")
|
76
|
_cache_failed_payload(payload)
|
77
|
|
78
|
|
79
|
def _cache_failed_payload(payload: dict):
|
80
|
""" Caches a payload.
|
81
|
|
82
|
This function is called when the application fails to send a payload
|
83
|
to the server. The payload gets stored into a file-based cache from which
|
84
|
it will be periodically retrieved as the client will attempt to send
|
85
|
it to the server again. All parameters regarding the cache can be found
|
86
|
in the configuration file.
|
87
|
|
88
|
:param payload: payload to be cached
|
89
|
"""
|
90
|
# If the cache is "full", discard the oldest record.
|
91
|
if len(_cache) >= _config.cache_max_entries:
|
92
|
oldest_payload = _cache.pop()
|
93
|
logging.warning(f"cache is full - discarding payload = {oldest_payload}")
|
94
|
|
95
|
# Store the payload into the cache.
|
96
|
logging.info(f"adding payload = {payload} into cache")
|
97
|
_cache.append(payload)
|
98
|
|
99
|
|
100
|
def _resend_cached_payloads():
|
101
|
"""Reattempts to send cached payloads to the server (API).
|
102
|
|
103
|
In the configuration file, there is a predefined number of
|
104
|
payloads that can be sent to the server with each call of this function.
|
105
|
This function is called periodically from api_client_run in order
|
106
|
to resend failed payloads to the server.
|
107
|
|
108
|
"""
|
109
|
# Calculate how many payload will be sent to the server
|
110
|
retries = min(_config.cache_max_retries, len(_cache))
|
111
|
logging.info(f"emptying the cache ({retries} records)")
|
112
|
|
113
|
# Send the payloads to the server one by one.
|
114
|
for _ in range(0, retries):
|
115
|
payload = _cache.pop()
|
116
|
send_data(payload)
|
117
|
|
118
|
|
119
|
def api_client_run():
|
120
|
""" Keeps resending failed payloads to the server.
|
121
|
|
122
|
This function is instantiated as a thread that periodically
|
123
|
calls the _resend_cached_payloads function in order to empty
|
124
|
the cache (failed payloads). The period can be set in the
|
125
|
configuration file.
|
126
|
"""
|
127
|
while True:
|
128
|
# Resend a predefined amount of failed payloads to the server.
|
129
|
_resend_cached_payloads()
|
130
|
|
131
|
# Sleep for a predefined amount of seconds.
|
132
|
sleep(_config.cache_retry_period_seconds)
|