mercuto-client 0.2.8__py3-none-any.whl → 0.3.0__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.
Files changed (37) hide show
  1. mercuto_client/__init__.py +2 -24
  2. mercuto_client/_authentication.py +72 -0
  3. mercuto_client/_tests/test_ingester/test_parsers.py +67 -67
  4. mercuto_client/_tests/test_mocking/__init__.py +0 -0
  5. mercuto_client/_tests/test_mocking/conftest.py +13 -0
  6. mercuto_client/_tests/test_mocking/test_mock_identity.py +8 -0
  7. mercuto_client/acl.py +16 -10
  8. mercuto_client/client.py +53 -779
  9. mercuto_client/exceptions.py +5 -1
  10. mercuto_client/ingester/__main__.py +1 -1
  11. mercuto_client/ingester/mercuto.py +15 -16
  12. mercuto_client/ingester/parsers/__init__.py +3 -3
  13. mercuto_client/ingester/parsers/campbell.py +2 -2
  14. mercuto_client/ingester/parsers/generic_csv.py +5 -5
  15. mercuto_client/ingester/parsers/worldsensing.py +4 -3
  16. mercuto_client/mocks/__init__.py +92 -0
  17. mercuto_client/mocks/_utility.py +69 -0
  18. mercuto_client/mocks/mock_data.py +402 -0
  19. mercuto_client/mocks/mock_fatigue.py +30 -0
  20. mercuto_client/mocks/mock_identity.py +188 -0
  21. mercuto_client/modules/__init__.py +19 -0
  22. mercuto_client/modules/_util.py +18 -0
  23. mercuto_client/modules/core.py +674 -0
  24. mercuto_client/modules/data.py +623 -0
  25. mercuto_client/modules/fatigue.py +189 -0
  26. mercuto_client/modules/identity.py +254 -0
  27. mercuto_client/{ingester/util.py → util.py} +27 -11
  28. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/METADATA +10 -3
  29. mercuto_client-0.3.0.dist-info/RECORD +41 -0
  30. mercuto_client/_tests/test_mocking.py +0 -93
  31. mercuto_client/_util.py +0 -13
  32. mercuto_client/mocks.py +0 -203
  33. mercuto_client/types.py +0 -409
  34. mercuto_client-0.2.8.dist-info/RECORD +0 -30
  35. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/WHEEL +0 -0
  36. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
  37. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/top_level.txt +0 -0
@@ -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(api_key: Optional[str] = None,
26
- service_token: Optional[str] = None,
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]['channel_code'] == "12345678"
23
- assert math.isclose(samples[0]['value'], -10)
24
- assert samples[0]['timestamp'] == '2025-05-20T15:00:00'
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]['channel_code'] == "abcdefgh"
27
- assert math.isclose(samples[1]['value'], 5)
28
- assert samples[0]['timestamp'] == '2025-05-20T15:00:00'
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]['channel_code'] == "12345678"
31
- assert math.isclose(samples[2]['value'], -12)
32
- assert samples[2]['timestamp'] == '2025-05-20T16:00:00'
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]['channel_code'] == "abcdefgh"
35
- assert math.isclose(samples[3]['value'], 10)
36
- assert samples[3]['timestamp'] == '2025-05-20T16:00:00'
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]['channel_code'] == "12345678"
48
- assert math.isclose(samples[0]['value'], 930.5)
49
- assert samples[0]['timestamp'] == '2024-04-15T12:35:00'
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]['channel_code'] == "abcdefgh"
52
- assert math.isclose(samples[1]['value'], 726.810811024)
53
- assert samples[1]['timestamp'] == '2024-04-15T12:35:00'
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]['channel_code'] == "12345678"
56
- assert math.isclose(samples[8]['value'], 930.4)
57
- assert samples[8]['timestamp'] == '2024-04-15T12:39:00'
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]['channel_code'] == "abcdefgh"
60
- assert math.isclose(samples[9]['value'], 726.841502500)
61
- assert samples[9]['timestamp'] == '2024-04-15T12:39:00'
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]['channel_code'] == "aaaaaaaa"
78
- assert math.isclose(samples[0]['value'], 1234.5)
79
- assert samples[0]['timestamp'] == '2023-12-07T00:01:00'
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]['channel_code'] == list(mapper.values())[i]
83
- assert math.isnan(samples[i]['value'])
84
- assert samples[i]['timestamp'] == '2023-12-07T00:01:00'
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]['channel_code'] == "aaaaaaaa"
87
- assert math.isclose(samples[6]['value'], 1234.5)
88
- assert samples[6]['timestamp'] == '2023-12-07T00:02:00'
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]['channel_code'] == list(mapper.values())[i-6]
91
- assert math.isnan(samples[i]['value'])
92
- assert samples[i]['timestamp'] == '2023-12-07T00:02:00'
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]['channel_code'] == "aaaaaaaa"
95
- assert math.isclose(samples[12]['value'], 1234.5)
96
- assert samples[12]['timestamp'] == '2023-12-07T00:03:00'
97
- assert samples[13]['channel_code'] == "bbbbbbbb"
98
- assert math.isclose(samples[13]['value'], 1234.5)
99
- assert samples[13]['timestamp'] == '2023-12-07T00:03:00'
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]['channel_code'] == list(mapper.values())[i-12]
103
- assert math.isnan(samples[i]['value'])
104
- assert samples[i]['timestamp'] == '2023-12-07T00:03:00'
105
- assert samples[17]['channel_code'] == "ffffffff"
106
- assert math.isclose(samples[17]['value'], 1537)
107
- assert samples[17]['timestamp'] == '2023-12-07T00:03:00'
108
-
109
- assert samples[18]['channel_code'] == "aaaaaaaa"
110
- assert math.isclose(samples[18]['value'], 1234.5)
111
- assert samples[18]['timestamp'] == '2023-12-07T00:04:00'
112
- assert samples[19]['channel_code'] == "bbbbbbbb"
113
- assert math.isclose(samples[19]['value'], 1234.5)
114
- assert samples[19]['timestamp'] == '2023-12-07T00:04:00'
115
- assert samples[20]['channel_code'] == "cccccccc"
116
- assert math.isclose(samples[20]['value'], 27.5)
117
- assert samples[20]['timestamp'] == '2023-12-07T00:04:00'
118
- assert samples[21]['channel_code'] == "dddddddd"
119
- assert math.isclose(samples[21]['value'], 25)
120
- assert samples[21]['timestamp'] == '2023-12-07T00:04:00'
121
- assert samples[22]['channel_code'] == "eeeeeeee"
122
- assert math.isclose(samples[22]['value'], 255)
123
- assert samples[22]['timestamp'] == '2023-12-07T00:04:00'
124
- assert samples[23]['channel_code'] == "ffffffff"
125
- assert math.isclose(samples[23]['value'], 0)
126
- assert samples[23]['timestamp'] == '2023-12-07T00:04:00'
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
- T = TypeVar('T', bound='AclPolicyBuilder')
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[AccessControlListJsonEntry] = []
79
+ self._permissions: list[AclPolicyBuilder._AclEntryType] = []
74
80
 
75
- def allow(self: T, action: str, resource: str) -> T:
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: T, action: str) -> T:
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: T, action: str, project_code: str) -> T:
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: T, action: str, tenant_code: str) -> T:
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) -> AccessControlListJson:
103
+ def as_dict(self) -> _AclPolicyType:
98
104
  return {
99
105
  'version': 1,
100
106
  'permissions': self._permissions