Skip to content

Commit c0ac978

Browse files
committed
Fully working metrics request
1 parent 41b59d6 commit c0ac978

10 files changed

Lines changed: 217 additions & 14 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
*.pyc
22
dist
3-
openpaygo.egg-info
3+
openpaygo.egg-info
4+
.DS_store

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ You can install the library by running `pip install openpaygo` or adding `openpa
4040

4141
## Getting Started - OpenPAYGO Token
4242

43-
### Generating Tokens
43+
### Generating Tokens (Server Side)
4444

4545
You can use the `generate_token()` function to generate an OpenPAYGOToken Token. The function takes the following parameters, and they should match the configuration in the hardware of the device:
4646

@@ -95,11 +95,11 @@ device.save() # We save the new count that we set for the device
9595
```
9696

9797

98-
### Decoding Tokens
98+
### Decoding Tokens (Device Side)
9999

100100
You can use the `decode_token()` function to generate an OpenPAYGOToken Token. The function takes the following parameters, and they should match the configuration in the hardware of the device:
101101

102-
- `token` (required): The token that was given by the user
102+
- `token` (required): The token that was given by the user, as a string
103103
- `secret_key` (required): The secret key of the device
104104
- `count` (required): The token count of the last valid token. When a device is new, this is 1.
105105
- `used_counts` (optional): An array of recently used token counts, as returned by the function itself after the last valid token was decoded. This allows for handling unordered token entry.
@@ -159,9 +159,23 @@ elif token_type == TokenType.INVALID:
159159

160160
## Getting Started - OpenPAYGO Metrics
161161

162+
### Generating a Request (Device Side)
162163

163-
You can use the `MetricsHandler` object to process your OpenPAYGO Metrics request from start to finish. It accepts the following initial inputs:
164-
- `metrics_payload` (required): The OpenPAYGO Metrics payload, in a JSON string format.
164+
You can use the `MetricsRequestHandler` object to create a new OpenPAYGO Metrics request from start to finish. It accepts the following initial inputs:
165+
- `serial_number` (required): The serial number of the device
166+
- `data_format` (optional): The data format, provided as dictionnary matching the data format object specifications.
167+
- `secret_key` (optional): The secret key provided as a string containing 32 hexadecimal characters. Required if `auth_method` is defined.
168+
- `auth_method` (optional): One of the auth method contained in the `AuthMethod` class.
169+
170+
It provides the following methods:
171+
- `set_timestamp(timestamp)`: Used to set the `timestamp` of the request.
172+
- `set_request_count(request_count)`: Used to set the `request_count` of the request.
173+
174+
175+
### Handling a Request and Generating a Response (Server Side)
176+
177+
You can use the `MetricsResponseHandler` object to process your OpenPAYGO Metrics request from start to finish. It accepts the following initial inputs:
178+
- `metrics_payload` (required): The OpenPAYGO Metrics payload, as a string containing the JSON payload.
165179
- `secret_key` (optional): The secret key provided as a string containing 32 hexadecimal characters
166180
- `data_format` (optional): The data format, provided as dictionnary matching the data format object specifications.
167181

@@ -223,6 +237,9 @@ def device_data():
223237

224238
## Changelog
225239

240+
### 2023-10-09 - v0.3.0
241+
- Fix token generation issue
242+
226243
### 2023-10-03 - v0.2.0
227244
- First working version published on PyPI
228245
- Has support for OpenPAYGO Token

openpaygo/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
from .encode_token import OpenPAYGOTokenEncoder
2-
from .decode_token import OpenPAYGOTokenDecoder
3-
from .shared import TokenType
1+
from .token_encode import OpenPAYGOTokenEncoder
2+
from .token_decode import OpenPAYGOTokenDecoder
3+
from .token_shared import TokenType
4+
from .metrics_request import MetricsRequestHandler
5+
from .metrics_response import MetricsResponseHandler
6+
from .metrics_shared import AuthMethod
47

58

69
def generate_token(**kwargs):

openpaygo/metrics_request.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from .metrics_shared import OpenPAYGOMetricsShared
2+
import copy
3+
4+
5+
class MetricsRequestHandler(object):
6+
7+
def __init__(self, serial_number, data_format=None, secret_key=None, auth_method=None):
8+
self.secret_key = secret_key
9+
self.auth_method = auth_method
10+
self.request_dict = {
11+
'serial_number': serial_number,
12+
}
13+
self.data_format = data_format
14+
if self.data_format:
15+
if self.data_format.get('id'):
16+
self.request_dict['data_format_id'] = data_format.get('id')
17+
else:
18+
self.request_dict['data_format'] = data_format
19+
self.data = {}
20+
self.historical_data = {}
21+
22+
def set_request_count(self, request_count):
23+
self.request_dict['request_count'] = request_count
24+
25+
def set_timestamp(self, timestamp):
26+
self.request_dict['timestamp'] = timestamp
27+
28+
def set_data(self, data):
29+
self.data = data
30+
31+
def set_historical_data(self, historical_data):
32+
if not self.data_format.get('historical_data_interval'):
33+
for time_step in historical_data:
34+
if not time_step.get('timestamp'):
35+
raise ValueError('Historical Data objects must have a time stamp if no historical_data_interval is defined.')
36+
self.historical_data = historical_data
37+
38+
def get_simple_request_payload(self):
39+
payload = self.get_simple_request_dict()
40+
return OpenPAYGOMetricsShared.convert_to_metrics_json(payload)
41+
42+
def get_simple_request_dict(self):
43+
simple_request = self.request_dict
44+
simple_request['data'] = self.data
45+
simple_request['historical_data'] = self.historical_data
46+
# We prepare the auth
47+
if self.auth_method:
48+
simple_request['auth'] = OpenPAYGOMetricsShared.generate_request_signature_from_data(simple_request, self.auth_method, self.secret_key)
49+
return simple_request
50+
51+
def get_condensed_request_payload(self):
52+
payload = self.get_condensed_request_dict()
53+
return OpenPAYGOMetricsShared.convert_to_metrics_json(payload)
54+
55+
def get_condensed_request_dict(self):
56+
if not self.data_format:
57+
raise ValueError('No data format provided for condensed request')
58+
condensed_request = self.request_dict
59+
condensed_request['data'] = []
60+
condensed_request['historical_data'] = []
61+
# We add the data
62+
data_copy = copy.deepcopy(self.data)
63+
for var in self.data_format.get('data_order'):
64+
condensed_request['data'].append(data_copy.pop(var) if var in data_copy else None)
65+
if len(data_copy) > 0:
66+
raise ValueError('Additional variables not present in the data format: '+str(data_copy))
67+
condensed_request['data'] = OpenPAYGOMetricsShared.remove_trailing_empty_elements(condensed_request['data'])
68+
# We add the historical data
69+
historical_data_copy = copy.deepcopy(self.historical_data)
70+
for time_step in historical_data_copy:
71+
time_step_data = []
72+
for var in self.data_format.get('historical_data_order'):
73+
time_step_data.append(time_step.pop(var) if var in time_step else None)
74+
if len(time_step) > 0:
75+
raise ValueError('Additional variables not present in the historical data format: '+str(time_step))
76+
time_step_data = OpenPAYGOMetricsShared.remove_trailing_empty_elements(time_step_data)
77+
condensed_request['historical_data'].append(time_step_data)
78+
# We prepare the auth
79+
if self.auth_method:
80+
condensed_request['auth'] = OpenPAYGOMetricsShared.generate_request_signature_from_data(condensed_request, self.auth_method, self.secret_key)
81+
# We replace the key names by the condensed ones
82+
condensed_request = OpenPAYGOMetricsShared.convert_dict_keys_to_condensed(condensed_request)
83+
return condensed_request
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22

33

4-
class MetricsHandler(object):
4+
class MetricsResponseHandler(object):
55

66
def __init__(self, received_metrics, data_format=None, secret_key=None):
77
self.received_metrics = received_metrics

openpaygo/metrics_shared.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import siphash
2+
import codecs
3+
import json
4+
5+
6+
class AuthMethod(object):
7+
SIMPLE_AUTH = 'sa'
8+
TIMESTAMP_AUTH = 'ta'
9+
COUNTER_AUTH = 'ca'
10+
DATA_AUTH = 'da'
11+
RECURSIVE_DATA_AUTH = 'ra'
12+
13+
14+
class OpenPAYGOMetricsShared(object):
15+
16+
CONDENSED_KEY_NAMES = {
17+
'serial_number': 'sn',
18+
'timestamp': 'ts',
19+
'auth': 'a',
20+
'request_count': 'rc',
21+
'data_collection_timestamp': 'dtc',
22+
'data_format_id': 'df',
23+
'data_format': 'dfo',
24+
'data': 'd',
25+
'historical_data': 'hd',
26+
'accessories': 'acc'
27+
}
28+
29+
@classmethod
30+
def convert_dict_keys_to_condensed(cls, simple_dict):
31+
condensed_dict = {}
32+
for key in simple_dict:
33+
if key in cls.CONDENSED_KEY_NAMES:
34+
condensed_dict[cls.CONDENSED_KEY_NAMES[key]] = simple_dict[key]
35+
else:
36+
condensed_dict[key] = simple_dict[key]
37+
return condensed_dict
38+
39+
@classmethod
40+
def remove_trailing_empty_elements(cls, list_with_empty):
41+
while list_with_empty and list_with_empty[-1] is None:
42+
list_with_empty.pop()
43+
return list_with_empty
44+
45+
@classmethod
46+
def convert_to_metrics_json(cls, data):
47+
return json.dumps(data, separators=(',', ':'))
48+
49+
@classmethod
50+
def generate_request_signature_from_data(cls, data, auth_method, secret_key):
51+
if auth_method == AuthMethod.SIMPLE_AUTH:
52+
signature = cls.generate_hash_string(data.get('serial_number'), secret_key)
53+
elif auth_method == AuthMethod.TIMESTAMP_AUTH:
54+
if not data.get('timestamp', None):
55+
raise ValueError('Timestamp is required for Timestamp Auth')
56+
signature = cls.generate_hash_string(data.get('serial_number') + str(data.get('timestamp')), secret_key)
57+
elif auth_method == AuthMethod.COUNTER_AUTH:
58+
if not data.get('request_count', None):
59+
raise ValueError('Request Count is required for Counter Auth')
60+
signature = cls.generate_hash_string(data.get('serial_number') + str(data.get('request_count')), secret_key)
61+
elif auth_method == AuthMethod.DATA_AUTH:
62+
payload = data.get('serial_number')
63+
if data.get('timestamp'):
64+
payload += str(data.get('timestamp'))
65+
elif data.get('request_count'):
66+
payload += str(data.get('request_count'))
67+
payload += cls.convert_to_metrics_json(data.get('data', []))
68+
payload += cls.convert_to_metrics_json(data.get('historical_data', []))
69+
signature = cls.generate_hash_string(payload, secret_key)
70+
elif auth_method == AuthMethod.RECURSIVE_DATA_AUTH:
71+
payload = data.get('serial_number')
72+
payload = cls.generate_hash_string(payload, secret_key)
73+
if data.get('timestamp'):
74+
payload = cls.generate_hash_string(payload+str(data.get('timestamp')), secret_key)
75+
elif data.get('request_count'):
76+
payload = cls.generate_hash_string(payload+str(data.get('request_count')), secret_key)
77+
payload = cls.generate_hash_string(payload+cls.convert_to_metrics_json(data.get('data', [])), secret_key)
78+
for time_step_data in data.get('historical_data', []):
79+
payload = cls.generate_hash_string(payload+cls.convert_to_metrics_json(time_step_data), secret_key)
80+
signature = payload
81+
else:
82+
raise ValueError('Invalid Authentication Method')
83+
return auth_method+signature
84+
85+
86+
@classmethod
87+
def generate_hash_string(cls, input_string, secret_key):
88+
key = cls.load_secret_key_from_hex(secret_key)
89+
hash = siphash.SipHash_2_4(key, codecs.encode(input_string, 'utf-8')).hash()
90+
hash_string = '{:x}'.format(hash)
91+
return hash_string
92+
93+
@classmethod
94+
def load_secret_key_from_hex(cls, secret_key):
95+
try:
96+
return codecs.decode(secret_key, 'hex')
97+
except Exception as e:
98+
raise ValueError('The secret key provided is not correctly formatted, it should be 32 hexadecimal characters. ')
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from .shared import OpenPAYGOTokenShared, TokenType
2-
from .shared_extended import OpenPAYGOTokenSharedExtended
1+
from .token_shared import OpenPAYGOTokenShared, TokenType
2+
from .token_shared_extended import OpenPAYGOTokenSharedExtended
33

44

55
class OpenPAYGOTokenDecoder(object):
@@ -27,6 +27,7 @@ def decode_token(cls, token, secret_key, count, used_counts=None, starting_code=
2727
extended_token = True
2828
else:
2929
raise ValueError("Token is too long")
30+
token = int(token)
3031
if not extended_token:
3132
value, token_type, count, updated_counts = cls.get_activation_value_count_and_type_from_token(token, starting_code, secret_key, count, restricted_digit_set, used_counts)
3233
else:
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from .shared import OpenPAYGOTokenShared, TokenType
2-
from .shared_extended import OpenPAYGOTokenSharedExtended
1+
from .token_shared import OpenPAYGOTokenShared, TokenType
2+
from .token_shared_extended import OpenPAYGOTokenSharedExtended
33

44

55
class OpenPAYGOTokenEncoder(object):

0 commit comments

Comments
 (0)