mercuto-client 0.2.7__tar.gz → 0.3.0__tar.gz
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-0.3.0/PKG-INFO +72 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/README.md +11 -3
- mercuto_client-0.3.0/mercuto_client/__init__.py +8 -0
- mercuto_client-0.3.0/mercuto_client/_authentication.py +72 -0
- mercuto_client-0.3.0/mercuto_client/_tests/test_ingester/test_parsers.py +145 -0
- mercuto_client-0.3.0/mercuto_client/_tests/test_mocking/conftest.py +13 -0
- mercuto_client-0.3.0/mercuto_client/_tests/test_mocking/test_mock_identity.py +8 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/acl.py +16 -10
- mercuto_client-0.3.0/mercuto_client/client.py +177 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/exceptions.py +5 -1
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/__main__.py +1 -1
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/mercuto.py +15 -16
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/parsers/__init__.py +3 -3
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/parsers/campbell.py +2 -2
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/parsers/generic_csv.py +5 -5
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/parsers/worldsensing.py +4 -3
- mercuto_client-0.3.0/mercuto_client/mocks/__init__.py +92 -0
- mercuto_client-0.3.0/mercuto_client/mocks/_utility.py +69 -0
- mercuto_client-0.3.0/mercuto_client/mocks/mock_data.py +402 -0
- mercuto_client-0.3.0/mercuto_client/mocks/mock_fatigue.py +30 -0
- mercuto_client-0.3.0/mercuto_client/mocks/mock_identity.py +188 -0
- mercuto_client-0.3.0/mercuto_client/modules/__init__.py +19 -0
- mercuto_client-0.3.0/mercuto_client/modules/_util.py +18 -0
- mercuto_client-0.3.0/mercuto_client/modules/core.py +674 -0
- mercuto_client-0.3.0/mercuto_client/modules/data.py +623 -0
- mercuto_client-0.3.0/mercuto_client/modules/fatigue.py +189 -0
- mercuto_client-0.3.0/mercuto_client/modules/identity.py +254 -0
- mercuto_client-0.3.0/mercuto_client/py.typed +0 -0
- {mercuto_client-0.2.7/mercuto_client/ingester → mercuto_client-0.3.0/mercuto_client}/util.py +27 -11
- mercuto_client-0.3.0/mercuto_client.egg-info/PKG-INFO +72 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client.egg-info/SOURCES.txt +17 -6
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client.egg-info/requires.txt +1 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/pyproject.toml +11 -5
- mercuto_client-0.2.7/PKG-INFO +0 -20
- mercuto_client-0.2.7/mercuto_client/__init__.py +0 -30
- mercuto_client-0.2.7/mercuto_client/_tests/test_ingester/test_parsers.py +0 -145
- mercuto_client-0.2.7/mercuto_client/_tests/test_mocking.py +0 -93
- mercuto_client-0.2.7/mercuto_client/_util.py +0 -13
- mercuto_client-0.2.7/mercuto_client/client.py +0 -903
- mercuto_client-0.2.7/mercuto_client/mocks.py +0 -203
- mercuto_client-0.2.7/mercuto_client/types.py +0 -409
- mercuto_client-0.2.7/mercuto_client.egg-info/PKG-INFO +0 -20
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/LICENSE +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/_tests/__init__.py +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/_tests/conftest.py +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/_tests/test_ingester/__init__.py +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/_tests/test_ingester/test_file_processor.py +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/_tests/test_ingester/test_ftp.py +0 -0
- {mercuto_client-0.2.7/mercuto_client/ingester → mercuto_client-0.3.0/mercuto_client/_tests/test_mocking}/__init__.py +0 -0
- /mercuto_client-0.2.7/mercuto_client/py.typed → /mercuto_client-0.3.0/mercuto_client/ingester/__init__.py +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/ftp.py +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client/ingester/processor.py +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client.egg-info/dependency_links.txt +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/mercuto_client.egg-info/top_level.txt +0 -0
- {mercuto_client-0.2.7 → mercuto_client-0.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mercuto-client
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Library for interfacing with Rockfield's Mercuto API
|
|
5
|
+
Author-email: Daniel Whipp <daniel.whipp@rocktech.com.au>
|
|
6
|
+
License-Expression: AGPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://mercuto.rockfieldcloud.com.au
|
|
8
|
+
Project-URL: Repository, https://github.com/RockfieldTechnologiesAustralia/mercuto-client
|
|
9
|
+
Project-URL: Documentation, https://github.com/RockfieldTechnologiesAustralia/mercuto-client/blob/main/README.md
|
|
10
|
+
Keywords: mercuto,rockfield,infratech
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: requests>=2.32
|
|
21
|
+
Requires-Dist: pyftpdlib>=2.0.1
|
|
22
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
23
|
+
Requires-Dist: pytz>=2025.2
|
|
24
|
+
Requires-Dist: schedule>=1.2.2
|
|
25
|
+
Requires-Dist: pydantic>=2.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Mercuto Client Python Library
|
|
29
|
+
|
|
30
|
+
Library for interfacing with Rockfield's Mercuto public API.
|
|
31
|
+
This library is in an early development state and is subject to major structural changes at any time.
|
|
32
|
+
|
|
33
|
+
(Visit our Github Repository)[https://github.com/RockfieldTechnologiesAustralia/mercuto-client]
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
Install from PyPi: `pip install mercuto-client` or adding the same line into your `requirements.txt`.
|
|
37
|
+
|
|
38
|
+
## Basic Usage
|
|
39
|
+
|
|
40
|
+
Use the `connect()` function exposed within the main package and provide your API key.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from mercuto_client import connect
|
|
44
|
+
|
|
45
|
+
client = connect(api_key="<YOUR API KEY>")
|
|
46
|
+
print(client.core().list_projects())
|
|
47
|
+
|
|
48
|
+
# Logout after finished.
|
|
49
|
+
client.logout()
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You can also use the client as a context manager. It will logout automatically.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from mercuto_client import MercutoClient
|
|
57
|
+
|
|
58
|
+
with MercutoClient.as_credentials(api_key='<YOUR API KEY>') as client:
|
|
59
|
+
print(client.core().list_projects())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Current Status
|
|
63
|
+
This library is incomplete and may not be fully compliant with the latest Mercuto version. It is only updated periodically and provided for use without any warranty or guarantees.
|
|
64
|
+
|
|
65
|
+
- [x] API Based login (Completed)
|
|
66
|
+
- [ ] Username/password login
|
|
67
|
+
|
|
68
|
+
## Running tests
|
|
69
|
+
Install test packages:
|
|
70
|
+
`python -m uv sync --group tests`
|
|
71
|
+
Run tests:
|
|
72
|
+
`uv run pytest`
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Library for interfacing with Rockfield's Mercuto public API.
|
|
4
4
|
This library is in an early development state and is subject to major structural changes at any time.
|
|
5
5
|
|
|
6
|
+
(Visit our Github Repository)[https://github.com/RockfieldTechnologiesAustralia/mercuto-client]
|
|
7
|
+
|
|
6
8
|
## Installation
|
|
7
9
|
Install from PyPi: `pip install mercuto-client` or adding the same line into your `requirements.txt`.
|
|
8
10
|
|
|
@@ -14,7 +16,7 @@ Use the `connect()` function exposed within the main package and provide your AP
|
|
|
14
16
|
from mercuto_client import connect
|
|
15
17
|
|
|
16
18
|
client = connect(api_key="<YOUR API KEY>")
|
|
17
|
-
print(client.
|
|
19
|
+
print(client.core().list_projects())
|
|
18
20
|
|
|
19
21
|
# Logout after finished.
|
|
20
22
|
client.logout()
|
|
@@ -27,11 +29,17 @@ You can also use the client as a context manager. It will logout automatically.
|
|
|
27
29
|
from mercuto_client import MercutoClient
|
|
28
30
|
|
|
29
31
|
with MercutoClient.as_credentials(api_key='<YOUR API KEY>') as client:
|
|
30
|
-
print(client.
|
|
32
|
+
print(client.core().list_projects())
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
## Current Status
|
|
34
36
|
This library is incomplete and may not be fully compliant with the latest Mercuto version. It is only updated periodically and provided for use without any warranty or guarantees.
|
|
35
37
|
|
|
36
38
|
- [x] API Based login (Completed)
|
|
37
|
-
- [ ] Username/password login
|
|
39
|
+
- [ ] Username/password login
|
|
40
|
+
|
|
41
|
+
## Running tests
|
|
42
|
+
Install test packages:
|
|
43
|
+
`python -m uv sync --group tests`
|
|
44
|
+
Run tests:
|
|
45
|
+
`uv run pytest`
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .client import MercutoClient
|
|
2
|
+
from .exceptions import MercutoClientException, MercutoHTTPException
|
|
3
|
+
|
|
4
|
+
__all__ = ['MercutoClient', 'MercutoHTTPException', 'MercutoClientException']
|
|
5
|
+
|
|
6
|
+
|
|
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()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from ...ingester.parsers import (detect_parser, parse_campbell_file,
|
|
8
|
+
parse_worldsensing_compact_file,
|
|
9
|
+
parse_worldsensing_standard_file)
|
|
10
|
+
|
|
11
|
+
RESOURCES_DIR = os.path.join(os.path.dirname(__file__), "resources")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_worldsensing_compacted_parser():
|
|
15
|
+
file = os.path.join(RESOURCES_DIR, "worldsensing-compacted-sample-file.dat")
|
|
16
|
+
mapper = {
|
|
17
|
+
"channel1": "12345678",
|
|
18
|
+
"channel2": "abcdefgh",
|
|
19
|
+
}
|
|
20
|
+
samples = parse_worldsensing_compact_file(file, mapper)
|
|
21
|
+
assert len(samples) == 4
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
38
|
+
|
|
39
|
+
def test_worldsensing_standard_parser():
|
|
40
|
+
file = os.path.join(RESOURCES_DIR, "worldsensing-standard-sample-file.csv")
|
|
41
|
+
mapper = {
|
|
42
|
+
"AtmPressure-85544-in-mbar": "12345678",
|
|
43
|
+
"freqSqInDigit-85544-VW-Ch1": "abcdefgh",
|
|
44
|
+
}
|
|
45
|
+
samples = parse_worldsensing_standard_file(file, mapper)
|
|
46
|
+
assert len(samples) == 10
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
63
|
+
|
|
64
|
+
def test_campbells_parser():
|
|
65
|
+
file = os.path.join(RESOURCES_DIR, "campbell-sample-file.dat")
|
|
66
|
+
mapper = {
|
|
67
|
+
"VWu_1": "aaaaaaaa",
|
|
68
|
+
"VWu_2": "bbbbbbbb",
|
|
69
|
+
"Therm(1)": "cccccccc",
|
|
70
|
+
"Therm(2)": "dddddddd",
|
|
71
|
+
"Diag_Max(1)": "eeeeeeee",
|
|
72
|
+
"Diag_Max(2)": "ffffffff",
|
|
73
|
+
}
|
|
74
|
+
samples = parse_campbell_file(file, mapper)
|
|
75
|
+
assert len(samples) == 6*4
|
|
76
|
+
|
|
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
|
+
|
|
81
|
+
for i in range(1, 6):
|
|
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
|
+
|
|
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
|
+
for i in range(7, 12):
|
|
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
|
+
|
|
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
|
+
|
|
101
|
+
for i in range(15, 17):
|
|
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
|
+
|
|
128
|
+
|
|
129
|
+
def test_detect_file_type():
|
|
130
|
+
compacted_file = os.path.join(RESOURCES_DIR, "worldsensing-compacted-sample-file.dat")
|
|
131
|
+
standard_file = os.path.join(RESOURCES_DIR, "worldsensing-standard-sample-file.csv")
|
|
132
|
+
campbell_file = os.path.join(RESOURCES_DIR, "campbell-sample-file.dat")
|
|
133
|
+
|
|
134
|
+
assert detect_parser(compacted_file) == parse_worldsensing_compact_file
|
|
135
|
+
assert detect_parser(standard_file) == parse_worldsensing_standard_file
|
|
136
|
+
assert detect_parser(campbell_file) == parse_campbell_file
|
|
137
|
+
|
|
138
|
+
# Test with an unknown file type
|
|
139
|
+
with tempfile.TemporaryDirectory() as dir:
|
|
140
|
+
unknown_file = os.path.join(dir, "unknown-file.txt")
|
|
141
|
+
with open(unknown_file, "w") as f:
|
|
142
|
+
f.write("This is an unknown file format.")
|
|
143
|
+
|
|
144
|
+
with pytest.raises(ValueError):
|
|
145
|
+
detect_parser(unknown_file)
|
|
@@ -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]
|
|
@@ -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
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json as json_stdlib
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Iterator, Mapping, Optional, Protocol, Type, TypeVar
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
import requests.cookies
|
|
10
|
+
|
|
11
|
+
from ._authentication import (IAuthenticationMethod,
|
|
12
|
+
create_authentication_method)
|
|
13
|
+
from .exceptions import MercutoClientException, MercutoHTTPException
|
|
14
|
+
from .modules.core import MercutoCoreService
|
|
15
|
+
from .modules.data import MercutoDataService
|
|
16
|
+
from .modules.fatigue import MercutoFatigueService
|
|
17
|
+
from .modules.identity import MercutoIdentityService
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _ModuleBase(Protocol):
|
|
23
|
+
def __init__(self, client: 'MercutoClient', *args: Any, **kwargs: Any) -> None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_T = TypeVar('_T', bound=_ModuleBase)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MercutoClient:
|
|
31
|
+
def __init__(self, url: Optional[str] = None, verify_ssl: bool = True, active_session: Optional[requests.Session] = None) -> None:
|
|
32
|
+
if url is None:
|
|
33
|
+
url = os.environ.get('MERCUTO_API_URL', 'https://api.rockfieldcloud.com.au')
|
|
34
|
+
assert isinstance(url, str)
|
|
35
|
+
|
|
36
|
+
if url.endswith('/'):
|
|
37
|
+
url = url[:-1]
|
|
38
|
+
|
|
39
|
+
if not url.startswith('https://'):
|
|
40
|
+
raise ValueError(f'Url must be https, is {url}')
|
|
41
|
+
|
|
42
|
+
self._url = url
|
|
43
|
+
self._verify_ssl = verify_ssl
|
|
44
|
+
|
|
45
|
+
if active_session is None:
|
|
46
|
+
self._current_session = requests.Session()
|
|
47
|
+
else:
|
|
48
|
+
self._current_session = active_session
|
|
49
|
+
|
|
50
|
+
self._auth_method: Optional[IAuthenticationMethod] = None
|
|
51
|
+
self._cookies = requests.cookies.RequestsCookieJar()
|
|
52
|
+
|
|
53
|
+
self._modules: dict[str, _ModuleBase] = {}
|
|
54
|
+
|
|
55
|
+
def url(self) -> str:
|
|
56
|
+
return self._url
|
|
57
|
+
|
|
58
|
+
def credentials_key(self) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Generate a unique key that identifies the current credentials set.
|
|
61
|
+
"""
|
|
62
|
+
if self._auth_method is None:
|
|
63
|
+
raise MercutoClientException("No credentials set")
|
|
64
|
+
return self._auth_method.unique_key()
|
|
65
|
+
|
|
66
|
+
def set_verify_ssl(self, verify_ssl: bool) -> None:
|
|
67
|
+
self._verify_ssl = verify_ssl
|
|
68
|
+
|
|
69
|
+
def copy(self) -> 'MercutoClient':
|
|
70
|
+
return MercutoClient(self._url, self._verify_ssl, self._current_session)
|
|
71
|
+
|
|
72
|
+
@contextlib.contextmanager
|
|
73
|
+
def as_credentials(self, api_key: Optional[str] = None,
|
|
74
|
+
service_token: Optional[str] = None,
|
|
75
|
+
bearer_token: Optional[str] = None,
|
|
76
|
+
headers: Optional[Mapping[str, str]] = None) -> Iterator['MercutoClient']:
|
|
77
|
+
"""
|
|
78
|
+
Same as .connect(), but as a context manager. Will automatically logout when exiting the context.
|
|
79
|
+
"""
|
|
80
|
+
# TODO: We are passing the current session along to re-use connections for speed. Will this cause security issues?
|
|
81
|
+
other = MercutoClient(self._url, self._verify_ssl, self._current_session)
|
|
82
|
+
try:
|
|
83
|
+
yield other.connect(api_key=api_key, service_token=service_token, bearer_token=bearer_token, headers=headers)
|
|
84
|
+
finally:
|
|
85
|
+
other.logout()
|
|
86
|
+
|
|
87
|
+
def connect(self, *, api_key: Optional[str] = None,
|
|
88
|
+
service_token: Optional[str] = None,
|
|
89
|
+
bearer_token: Optional[str] = None,
|
|
90
|
+
headers: Optional[Mapping[str, str]] = None) -> 'MercutoClient':
|
|
91
|
+
"""
|
|
92
|
+
Attempt to connect using any available method.
|
|
93
|
+
if api_key is provided, use the api_key.
|
|
94
|
+
if service_token is provided, use the service_token.
|
|
95
|
+
if headers is provided, attempt to extract either api_key or service_token from given header set.
|
|
96
|
+
headers should be a dictionary of headers that would be sent in a request. Useful for using existing authenation mechanism for forwarding.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
authentication = create_authentication_method(api_key=api_key, service_token=service_token, bearer_token=bearer_token, headers=headers)
|
|
100
|
+
self.login(authentication)
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def _update_headers(self, headers: dict[str, str]) -> dict[str, str]:
|
|
104
|
+
base: dict[str, str] = {}
|
|
105
|
+
|
|
106
|
+
if self._auth_method is not None:
|
|
107
|
+
self._auth_method.update_header(base)
|
|
108
|
+
base.update(headers)
|
|
109
|
+
return base
|
|
110
|
+
|
|
111
|
+
def _http_request(self, url: str, method: str,
|
|
112
|
+
params: Optional[dict[str, Any]] = None,
|
|
113
|
+
json: Optional[dict[str, Any]] = None,
|
|
114
|
+
raise_for_status: bool = True,
|
|
115
|
+
**kwargs: Any) -> requests.Response:
|
|
116
|
+
if url.startswith('/'):
|
|
117
|
+
url = url[1:]
|
|
118
|
+
full_url = f"{self._url}/{url}"
|
|
119
|
+
|
|
120
|
+
if 'timeout' not in kwargs:
|
|
121
|
+
kwargs['timeout'] = 10
|
|
122
|
+
kwargs['headers'] = self._update_headers(kwargs.get('headers', {}))
|
|
123
|
+
|
|
124
|
+
if 'verify' not in kwargs:
|
|
125
|
+
kwargs['verify'] = self._verify_ssl
|
|
126
|
+
|
|
127
|
+
if 'cookies' not in kwargs:
|
|
128
|
+
kwargs['cookies'] = self._cookies
|
|
129
|
+
|
|
130
|
+
# Custom parsing json to support NAN
|
|
131
|
+
if json is not None and kwargs.get('data') is None:
|
|
132
|
+
kwargs['data'] = json_stdlib.dumps(json, allow_nan=True)
|
|
133
|
+
kwargs['headers']['Content-Type'] = 'application/json'
|
|
134
|
+
json = None
|
|
135
|
+
|
|
136
|
+
start = time.time()
|
|
137
|
+
resp = self._current_session.request(method, full_url, params=params, json=json, **kwargs)
|
|
138
|
+
duration = time.time() - start
|
|
139
|
+
logger.debug("Made request to %s %s in %.2f seconds (code=%s)", method, full_url, duration, resp.status_code)
|
|
140
|
+
if raise_for_status and not resp.ok:
|
|
141
|
+
try:
|
|
142
|
+
error_json = resp.json()
|
|
143
|
+
except Exception:
|
|
144
|
+
raise MercutoHTTPException(resp.text, resp.status_code)
|
|
145
|
+
else:
|
|
146
|
+
if 'detail' in error_json and isinstance(error_json['detail'], str):
|
|
147
|
+
raise MercutoHTTPException(error_json['detail'], resp.status_code)
|
|
148
|
+
else:
|
|
149
|
+
raise MercutoHTTPException(resp.text, resp.status_code)
|
|
150
|
+
resp.cookies.update(self._cookies)
|
|
151
|
+
return resp
|
|
152
|
+
|
|
153
|
+
def _add_and_fetch_module(self, name: str, module: Type[_T]) -> _T:
|
|
154
|
+
if name not in self._modules:
|
|
155
|
+
self._modules[name] = module(self)
|
|
156
|
+
return self._modules[name] # type: ignore
|
|
157
|
+
|
|
158
|
+
def identity(self) -> 'MercutoIdentityService':
|
|
159
|
+
return self._add_and_fetch_module('identity', MercutoIdentityService)
|
|
160
|
+
|
|
161
|
+
def fatigue(self) -> 'MercutoFatigueService':
|
|
162
|
+
return self._add_and_fetch_module('fatigue', MercutoFatigueService)
|
|
163
|
+
|
|
164
|
+
def data(self) -> 'MercutoDataService':
|
|
165
|
+
return self._add_and_fetch_module('data', MercutoDataService)
|
|
166
|
+
|
|
167
|
+
def core(self) -> 'MercutoCoreService':
|
|
168
|
+
return self._add_and_fetch_module('core', MercutoCoreService)
|
|
169
|
+
|
|
170
|
+
def login(self, authentication: IAuthenticationMethod) -> None:
|
|
171
|
+
self._auth_method = authentication
|
|
172
|
+
|
|
173
|
+
def logout(self) -> None:
|
|
174
|
+
self._auth_method = None
|
|
175
|
+
|
|
176
|
+
def is_logged_in(self) -> bool:
|
|
177
|
+
return self._auth_method is not None
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class MercutoClientException(Exception):
|
|
@@ -11,5 +12,8 @@ class MercutoHTTPException(MercutoClientException):
|
|
|
11
12
|
self.status_code = status_code
|
|
12
13
|
self.message = message
|
|
13
14
|
|
|
14
|
-
def json(self) ->
|
|
15
|
+
def json(self) -> Any:
|
|
15
16
|
return json.loads(self.message)
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
return f"MercutoHTTPException(status_code='{self.status_code}', message='{self.message}')"
|
|
@@ -8,10 +8,10 @@ from typing import Callable, TypeVar
|
|
|
8
8
|
|
|
9
9
|
import schedule
|
|
10
10
|
|
|
11
|
+
from ..util import get_free_space_excluding_files
|
|
11
12
|
from .ftp import simple_ftp_server
|
|
12
13
|
from .mercuto import MercutoIngester
|
|
13
14
|
from .processor import FileProcessor
|
|
14
|
-
from .util import get_free_space_excluding_files
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|