mercuto-client 0.2.7__py3-none-any.whl → 0.3.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mercuto-client might be problematic. Click here for more details.
- mercuto_client/__init__.py +2 -24
- mercuto_client/_authentication.py +72 -0
- mercuto_client/_tests/test_ingester/test_parsers.py +67 -67
- mercuto_client/_tests/test_mocking/__init__.py +0 -0
- mercuto_client/_tests/test_mocking/conftest.py +13 -0
- mercuto_client/_tests/test_mocking/test_mock_identity.py +8 -0
- mercuto_client/acl.py +16 -10
- mercuto_client/client.py +53 -779
- mercuto_client/exceptions.py +5 -1
- mercuto_client/ingester/__main__.py +1 -1
- mercuto_client/ingester/mercuto.py +15 -16
- mercuto_client/ingester/parsers/__init__.py +3 -3
- mercuto_client/ingester/parsers/campbell.py +2 -2
- mercuto_client/ingester/parsers/generic_csv.py +5 -5
- mercuto_client/ingester/parsers/worldsensing.py +4 -3
- mercuto_client/mocks/__init__.py +92 -0
- mercuto_client/mocks/_utility.py +69 -0
- mercuto_client/mocks/mock_data.py +402 -0
- mercuto_client/mocks/mock_fatigue.py +30 -0
- mercuto_client/mocks/mock_identity.py +188 -0
- mercuto_client/modules/__init__.py +19 -0
- mercuto_client/modules/_util.py +18 -0
- mercuto_client/modules/core.py +674 -0
- mercuto_client/modules/data.py +623 -0
- mercuto_client/modules/fatigue.py +189 -0
- mercuto_client/modules/identity.py +254 -0
- mercuto_client/{ingester/util.py → util.py} +27 -11
- mercuto_client-0.3.0a0.dist-info/METADATA +72 -0
- mercuto_client-0.3.0a0.dist-info/RECORD +41 -0
- mercuto_client/_tests/test_mocking.py +0 -93
- mercuto_client/_util.py +0 -13
- mercuto_client/mocks.py +0 -203
- mercuto_client/types.py +0 -409
- mercuto_client-0.2.7.dist-info/METADATA +0 -20
- mercuto_client-0.2.7.dist-info/RECORD +0 -30
- {mercuto_client-0.2.7.dist-info → mercuto_client-0.3.0a0.dist-info}/WHEEL +0 -0
- {mercuto_client-0.2.7.dist-info → mercuto_client-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
- {mercuto_client-0.2.7.dist-info → mercuto_client-0.3.0a0.dist-info}/top_level.txt +0 -0
mercuto_client/__init__.py
CHANGED
|
@@ -1,30 +1,8 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Copyright (C) 2025 Rockfield Technologies Australia Pty Ltd
|
|
3
|
-
|
|
4
|
-
This program is free software: you can redistribute it and/or modify
|
|
5
|
-
it under the terms of the GNU Affero General Public License as published
|
|
6
|
-
by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
-
(at your option) any later version.
|
|
8
|
-
|
|
9
|
-
This program is distributed in the hope that it will be useful,
|
|
10
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
GNU Affero General Public License for more details.
|
|
13
|
-
|
|
14
|
-
You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
"""
|
|
17
|
-
from typing import Mapping, Optional
|
|
18
|
-
|
|
19
1
|
from .client import MercutoClient
|
|
20
2
|
from .exceptions import MercutoClientException, MercutoHTTPException
|
|
21
3
|
|
|
22
4
|
__all__ = ['MercutoClient', 'MercutoHTTPException', 'MercutoClientException']
|
|
23
5
|
|
|
24
6
|
|
|
25
|
-
def connect(
|
|
26
|
-
|
|
27
|
-
headers: Optional[Mapping[str, str]] = None) -> MercutoClient:
|
|
28
|
-
return MercutoClient().connect(api_key=api_key,
|
|
29
|
-
service_token=service_token,
|
|
30
|
-
headers=headers)
|
|
7
|
+
def connect(*args, **kwargs) -> MercutoClient:
|
|
8
|
+
return MercutoClient().connect(*args, **kwargs)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Mapping, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IAuthenticationMethod:
|
|
5
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
6
|
+
return
|
|
7
|
+
|
|
8
|
+
def unique_key(self) -> str:
|
|
9
|
+
raise NotImplementedError(f"unique_key not implemented for type {self.__class__.__name__}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiKeyAuthentication(IAuthenticationMethod):
|
|
13
|
+
def __init__(self, api_key: str) -> None:
|
|
14
|
+
self.api_key = api_key
|
|
15
|
+
|
|
16
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
17
|
+
header['X-Api-Key'] = self.api_key
|
|
18
|
+
|
|
19
|
+
def unique_key(self) -> str:
|
|
20
|
+
return f'api-key:{self.api_key}'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ServiceTokenAuthentication(IAuthenticationMethod):
|
|
24
|
+
def __init__(self, service_token: str) -> None:
|
|
25
|
+
self.service_token = service_token
|
|
26
|
+
|
|
27
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
28
|
+
header['X-Service-Token'] = self.service_token
|
|
29
|
+
|
|
30
|
+
def unique_key(self) -> str:
|
|
31
|
+
return f'service-token:{self.service_token}'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuthorizationHeaderAuthentication(IAuthenticationMethod):
|
|
35
|
+
def __init__(self, authorization_header: str) -> None:
|
|
36
|
+
if not authorization_header.startswith('Bearer '):
|
|
37
|
+
authorization_header = 'Bearer ' + authorization_header
|
|
38
|
+
self.authorization_header = authorization_header
|
|
39
|
+
|
|
40
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
41
|
+
header['Authorization'] = self.authorization_header
|
|
42
|
+
|
|
43
|
+
def unique_key(self) -> str:
|
|
44
|
+
return f'auth-bearer:{self.authorization_header}'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NullAuthenticationMethod(IAuthenticationMethod):
|
|
48
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def unique_key(self) -> str:
|
|
52
|
+
return 'null-authentication'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_authentication_method(api_key: Optional[str] = None,
|
|
56
|
+
service_token: Optional[str] = None,
|
|
57
|
+
bearer_token: Optional[str] = None,
|
|
58
|
+
headers: Optional[Mapping[str, str]] = None) -> IAuthenticationMethod:
|
|
59
|
+
if api_key is not None and service_token is not None and headers is not None and bearer_token is not None:
|
|
60
|
+
raise ValueError("Only one of api_key or service_token or bearer_token can be provided")
|
|
61
|
+
if headers is not None:
|
|
62
|
+
api_key = headers.get('X-Api-Key', None)
|
|
63
|
+
service_token = headers.get('X-Service-Token', None)
|
|
64
|
+
bearer_token = headers.get('Authorization', None)
|
|
65
|
+
if api_key is not None:
|
|
66
|
+
return ApiKeyAuthentication(api_key)
|
|
67
|
+
elif service_token is not None:
|
|
68
|
+
return ServiceTokenAuthentication(service_token)
|
|
69
|
+
elif bearer_token is not None:
|
|
70
|
+
return AuthorizationHeaderAuthentication(bearer_token)
|
|
71
|
+
else:
|
|
72
|
+
return NullAuthenticationMethod()
|
|
@@ -19,21 +19,21 @@ def test_worldsensing_compacted_parser():
|
|
|
19
19
|
}
|
|
20
20
|
samples = parse_worldsensing_compact_file(file, mapper)
|
|
21
21
|
assert len(samples) == 4
|
|
22
|
-
assert samples[0]
|
|
23
|
-
assert math.isclose(samples[0]
|
|
24
|
-
assert samples[0]
|
|
22
|
+
assert samples[0].channel == "12345678"
|
|
23
|
+
assert math.isclose(samples[0].value, -10)
|
|
24
|
+
assert samples[0].timestamp.isoformat() == '2025-05-20T15:00:00'
|
|
25
25
|
|
|
26
|
-
assert samples[1]
|
|
27
|
-
assert math.isclose(samples[1]
|
|
28
|
-
assert samples[0]
|
|
26
|
+
assert samples[1].channel == "abcdefgh"
|
|
27
|
+
assert math.isclose(samples[1].value, 5)
|
|
28
|
+
assert samples[0].timestamp.isoformat() == '2025-05-20T15:00:00'
|
|
29
29
|
|
|
30
|
-
assert samples[2]
|
|
31
|
-
assert math.isclose(samples[2]
|
|
32
|
-
assert samples[2]
|
|
30
|
+
assert samples[2].channel == "12345678"
|
|
31
|
+
assert math.isclose(samples[2].value, -12)
|
|
32
|
+
assert samples[2].timestamp.isoformat() == '2025-05-20T16:00:00'
|
|
33
33
|
|
|
34
|
-
assert samples[3]
|
|
35
|
-
assert math.isclose(samples[3]
|
|
36
|
-
assert samples[3]
|
|
34
|
+
assert samples[3].channel == "abcdefgh"
|
|
35
|
+
assert math.isclose(samples[3].value, 10)
|
|
36
|
+
assert samples[3].timestamp.isoformat() == '2025-05-20T16:00:00'
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def test_worldsensing_standard_parser():
|
|
@@ -44,21 +44,21 @@ def test_worldsensing_standard_parser():
|
|
|
44
44
|
}
|
|
45
45
|
samples = parse_worldsensing_standard_file(file, mapper)
|
|
46
46
|
assert len(samples) == 10
|
|
47
|
-
assert samples[0]
|
|
48
|
-
assert math.isclose(samples[0]
|
|
49
|
-
assert samples[0]
|
|
47
|
+
assert samples[0].channel == "12345678"
|
|
48
|
+
assert math.isclose(samples[0].value, 930.5)
|
|
49
|
+
assert samples[0].timestamp.isoformat() == '2024-04-15T12:35:00'
|
|
50
50
|
|
|
51
|
-
assert samples[1]
|
|
52
|
-
assert math.isclose(samples[1]
|
|
53
|
-
assert samples[1]
|
|
51
|
+
assert samples[1].channel == "abcdefgh"
|
|
52
|
+
assert math.isclose(samples[1].value, 726.810811024)
|
|
53
|
+
assert samples[1].timestamp.isoformat() == '2024-04-15T12:35:00'
|
|
54
54
|
|
|
55
|
-
assert samples[8]
|
|
56
|
-
assert math.isclose(samples[8]
|
|
57
|
-
assert samples[8]
|
|
55
|
+
assert samples[8].channel == "12345678"
|
|
56
|
+
assert math.isclose(samples[8].value, 930.4)
|
|
57
|
+
assert samples[8].timestamp.isoformat() == '2024-04-15T12:39:00'
|
|
58
58
|
|
|
59
|
-
assert samples[9]
|
|
60
|
-
assert math.isclose(samples[9]
|
|
61
|
-
assert samples[9]
|
|
59
|
+
assert samples[9].channel == "abcdefgh"
|
|
60
|
+
assert math.isclose(samples[9].value, 726.841502500)
|
|
61
|
+
assert samples[9].timestamp.isoformat() == '2024-04-15T12:39:00'
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def test_campbells_parser():
|
|
@@ -74,56 +74,56 @@ def test_campbells_parser():
|
|
|
74
74
|
samples = parse_campbell_file(file, mapper)
|
|
75
75
|
assert len(samples) == 6*4
|
|
76
76
|
|
|
77
|
-
assert samples[0]
|
|
78
|
-
assert math.isclose(samples[0]
|
|
79
|
-
assert samples[0]
|
|
77
|
+
assert samples[0].channel == "aaaaaaaa"
|
|
78
|
+
assert math.isclose(samples[0].value, 1234.5)
|
|
79
|
+
assert samples[0].timestamp.isoformat() == '2023-12-07T00:01:00'
|
|
80
80
|
|
|
81
81
|
for i in range(1, 6):
|
|
82
|
-
assert samples[i]
|
|
83
|
-
assert math.isnan(samples[i]
|
|
84
|
-
assert samples[i]
|
|
82
|
+
assert samples[i].channel == list(mapper.values())[i]
|
|
83
|
+
assert math.isnan(samples[i].value)
|
|
84
|
+
assert samples[i].timestamp.isoformat() == '2023-12-07T00:01:00'
|
|
85
85
|
|
|
86
|
-
assert samples[6]
|
|
87
|
-
assert math.isclose(samples[6]
|
|
88
|
-
assert samples[6]
|
|
86
|
+
assert samples[6].channel == "aaaaaaaa"
|
|
87
|
+
assert math.isclose(samples[6].value, 1234.5)
|
|
88
|
+
assert samples[6].timestamp.isoformat() == '2023-12-07T00:02:00'
|
|
89
89
|
for i in range(7, 12):
|
|
90
|
-
assert samples[i]
|
|
91
|
-
assert math.isnan(samples[i]
|
|
92
|
-
assert samples[i]
|
|
90
|
+
assert samples[i].channel == list(mapper.values())[i-6]
|
|
91
|
+
assert math.isnan(samples[i].value)
|
|
92
|
+
assert samples[i].timestamp.isoformat() == '2023-12-07T00:02:00'
|
|
93
93
|
|
|
94
|
-
assert samples[12]
|
|
95
|
-
assert math.isclose(samples[12]
|
|
96
|
-
assert samples[12]
|
|
97
|
-
assert samples[13]
|
|
98
|
-
assert math.isclose(samples[13]
|
|
99
|
-
assert samples[13]
|
|
94
|
+
assert samples[12].channel == "aaaaaaaa"
|
|
95
|
+
assert math.isclose(samples[12].value, 1234.5)
|
|
96
|
+
assert samples[12].timestamp.isoformat() == '2023-12-07T00:03:00'
|
|
97
|
+
assert samples[13].channel == "bbbbbbbb"
|
|
98
|
+
assert math.isclose(samples[13].value, 1234.5)
|
|
99
|
+
assert samples[13].timestamp.isoformat() == '2023-12-07T00:03:00'
|
|
100
100
|
|
|
101
101
|
for i in range(15, 17):
|
|
102
|
-
assert samples[i]
|
|
103
|
-
assert math.isnan(samples[i]
|
|
104
|
-
assert samples[i]
|
|
105
|
-
assert samples[17]
|
|
106
|
-
assert math.isclose(samples[17]
|
|
107
|
-
assert samples[17]
|
|
108
|
-
|
|
109
|
-
assert samples[18]
|
|
110
|
-
assert math.isclose(samples[18]
|
|
111
|
-
assert samples[18]
|
|
112
|
-
assert samples[19]
|
|
113
|
-
assert math.isclose(samples[19]
|
|
114
|
-
assert samples[19]
|
|
115
|
-
assert samples[20]
|
|
116
|
-
assert math.isclose(samples[20]
|
|
117
|
-
assert samples[20]
|
|
118
|
-
assert samples[21]
|
|
119
|
-
assert math.isclose(samples[21]
|
|
120
|
-
assert samples[21]
|
|
121
|
-
assert samples[22]
|
|
122
|
-
assert math.isclose(samples[22]
|
|
123
|
-
assert samples[22]
|
|
124
|
-
assert samples[23]
|
|
125
|
-
assert math.isclose(samples[23]
|
|
126
|
-
assert samples[23]
|
|
102
|
+
assert samples[i].channel == list(mapper.values())[i-12]
|
|
103
|
+
assert math.isnan(samples[i].value)
|
|
104
|
+
assert samples[i].timestamp.isoformat() == '2023-12-07T00:03:00'
|
|
105
|
+
assert samples[17].channel == "ffffffff"
|
|
106
|
+
assert math.isclose(samples[17].value, 1537)
|
|
107
|
+
assert samples[17].timestamp.isoformat() == '2023-12-07T00:03:00'
|
|
108
|
+
|
|
109
|
+
assert samples[18].channel == "aaaaaaaa"
|
|
110
|
+
assert math.isclose(samples[18].value, 1234.5)
|
|
111
|
+
assert samples[18].timestamp.isoformat() == '2023-12-07T00:04:00'
|
|
112
|
+
assert samples[19].channel == "bbbbbbbb"
|
|
113
|
+
assert math.isclose(samples[19].value, 1234.5)
|
|
114
|
+
assert samples[19].timestamp.isoformat() == '2023-12-07T00:04:00'
|
|
115
|
+
assert samples[20].channel == "cccccccc"
|
|
116
|
+
assert math.isclose(samples[20].value, 27.5)
|
|
117
|
+
assert samples[20].timestamp.isoformat() == '2023-12-07T00:04:00'
|
|
118
|
+
assert samples[21].channel == "dddddddd"
|
|
119
|
+
assert math.isclose(samples[21].value, 25)
|
|
120
|
+
assert samples[21].timestamp.isoformat() == '2023-12-07T00:04:00'
|
|
121
|
+
assert samples[22].channel == "eeeeeeee"
|
|
122
|
+
assert math.isclose(samples[22].value, 255)
|
|
123
|
+
assert samples[22].timestamp.isoformat() == '2023-12-07T00:04:00'
|
|
124
|
+
assert samples[23].channel == "ffffffff"
|
|
125
|
+
assert math.isclose(samples[23].value, 0)
|
|
126
|
+
assert samples[23].timestamp.isoformat() == '2023-12-07T00:04:00'
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
def test_detect_file_type():
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from ... import MercutoClient
|
|
6
|
+
from ...mocks import mock_mercuto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def client() -> Iterator[MercutoClient]:
|
|
11
|
+
with mock_mercuto():
|
|
12
|
+
client = MercutoClient("https://testserver")
|
|
13
|
+
yield client
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from ... import MercutoClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_add_and_list_users(client: MercutoClient) -> None:
|
|
5
|
+
client.identity().create_user("test_user", "test-tenant", "Description", "Test Group")
|
|
6
|
+
users = client.identity().list_users()
|
|
7
|
+
assert len(users) == 1
|
|
8
|
+
assert "test_user" in [user.username for user in users]
|
mercuto_client/acl.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import TypeVar
|
|
3
|
-
|
|
4
|
-
from .types import AccessControlListJson, AccessControlListJsonEntry
|
|
2
|
+
from typing import TypedDict, TypeVar
|
|
5
3
|
|
|
6
4
|
|
|
7
5
|
class ResourceTypes:
|
|
@@ -65,36 +63,44 @@ class ServiceTypes:
|
|
|
65
63
|
WILDCARD = '*'
|
|
66
64
|
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
_T = TypeVar('_T', bound='AclPolicyBuilder')
|
|
69
67
|
|
|
70
68
|
|
|
71
69
|
class AclPolicyBuilder:
|
|
70
|
+
class _AclEntryType(TypedDict):
|
|
71
|
+
action: str
|
|
72
|
+
resource: str
|
|
73
|
+
|
|
74
|
+
class _AclPolicyType(TypedDict):
|
|
75
|
+
version: int
|
|
76
|
+
permissions: list['AclPolicyBuilder._AclEntryType']
|
|
77
|
+
|
|
72
78
|
def __init__(self) -> None:
|
|
73
|
-
self._permissions: list[
|
|
79
|
+
self._permissions: list[AclPolicyBuilder._AclEntryType] = []
|
|
74
80
|
|
|
75
|
-
def allow(self:
|
|
81
|
+
def allow(self: _T, action: str, resource: str) -> _T:
|
|
76
82
|
self._permissions.append({
|
|
77
83
|
'action': action,
|
|
78
84
|
'resource': resource
|
|
79
85
|
})
|
|
80
86
|
return self
|
|
81
87
|
|
|
82
|
-
def allow_all(self:
|
|
88
|
+
def allow_all(self: _T, action: str) -> _T:
|
|
83
89
|
self.allow(action, f"mrn:{ServiceTypes.WILDCARD}:{ResourceTypes.WILDCARD}/{ResourceTypes.WILDCARD}")
|
|
84
90
|
return self
|
|
85
91
|
|
|
86
|
-
def allow_project(self:
|
|
92
|
+
def allow_project(self: _T, action: str, project_code: str) -> _T:
|
|
87
93
|
self.allow(action, f"mrn:{ServiceTypes.MERCUTO}:{ResourceTypes.Mercuto.PROJECT}/{project_code}")
|
|
88
94
|
return self
|
|
89
95
|
|
|
90
|
-
def allow_tenant(self:
|
|
96
|
+
def allow_tenant(self: _T, action: str, tenant_code: str) -> _T:
|
|
91
97
|
self.allow(action, f"mrn:{ServiceTypes.IDENTITY}:{ResourceTypes.Identity.TENANT}/{tenant_code}")
|
|
92
98
|
return self
|
|
93
99
|
|
|
94
100
|
def as_string(self) -> str:
|
|
95
101
|
return json.dumps(self.as_dict())
|
|
96
102
|
|
|
97
|
-
def as_dict(self) ->
|
|
103
|
+
def as_dict(self) -> _AclPolicyType:
|
|
98
104
|
return {
|
|
99
105
|
'version': 1,
|
|
100
106
|
'permissions': self._permissions
|