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.
- 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.2.8.dist-info → mercuto_client-0.3.0.dist-info}/METADATA +10 -3
- mercuto_client-0.3.0.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.8.dist-info/RECORD +0 -30
- {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/WHEEL +0 -0
- {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING, Literal, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, TypeAdapter
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ..client import MercutoClient
|
|
8
|
+
|
|
9
|
+
from . import _PayloadType
|
|
10
|
+
from ._util import BaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RainflowConfiguration(BaseModel):
|
|
14
|
+
project: str
|
|
15
|
+
max_bins: int
|
|
16
|
+
bin_size: float
|
|
17
|
+
multiplier: float
|
|
18
|
+
reservoir_adjustment: bool
|
|
19
|
+
sources: list[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FatigueConnection(BaseModel):
|
|
23
|
+
project: str
|
|
24
|
+
code: str
|
|
25
|
+
label: str
|
|
26
|
+
multiplier: float
|
|
27
|
+
c_d: float
|
|
28
|
+
m: float
|
|
29
|
+
s_0: float
|
|
30
|
+
bs7608_failure_probability: Optional[float]
|
|
31
|
+
bs7608_detail_category: Optional[str]
|
|
32
|
+
initial_date: datetime
|
|
33
|
+
initial_damage: float
|
|
34
|
+
sources: list[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConnectionRemnantCapacity(BaseModel):
|
|
38
|
+
connection: FatigueConnection
|
|
39
|
+
remaining_life_years: float = Field(description="Remaining life of the connection in years")
|
|
40
|
+
total_damage: float = Field(description="Total damage accumulated in the connection up to the 'end_time' specified")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Healthcheck(BaseModel):
|
|
44
|
+
status: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_RainflowConfigurationlistAdapter = TypeAdapter(list[RainflowConfiguration])
|
|
48
|
+
_FatigueConnectionlistAdapter = TypeAdapter(list[FatigueConnection])
|
|
49
|
+
_ConnectionRemnantCapacitylistAdapter = TypeAdapter(list[ConnectionRemnantCapacity])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MercutoFatigueService:
|
|
53
|
+
def __init__(self, client: 'MercutoClient', path: str = '/fatigue') -> None:
|
|
54
|
+
self._client = client
|
|
55
|
+
self._path = path
|
|
56
|
+
|
|
57
|
+
def healthcheck(self) -> Healthcheck:
|
|
58
|
+
r = self._client._http_request(f"{self._path}/healthcheck", "GET")
|
|
59
|
+
return Healthcheck.model_validate_json(r.text)
|
|
60
|
+
|
|
61
|
+
# --- Rainflow routes ---
|
|
62
|
+
|
|
63
|
+
def list_rainflow_config(self, project: str) -> list[RainflowConfiguration]:
|
|
64
|
+
params: _PayloadType = {"project": project}
|
|
65
|
+
r = self._client._http_request(f"{self._path}/rainflow/setup", "GET", params=params)
|
|
66
|
+
return _RainflowConfigurationlistAdapter.validate_json(r.text)
|
|
67
|
+
|
|
68
|
+
def setup_rainflow(
|
|
69
|
+
self,
|
|
70
|
+
project: str,
|
|
71
|
+
max_bins: int,
|
|
72
|
+
bin_size: float,
|
|
73
|
+
multiplier: float,
|
|
74
|
+
reservoir_adjustment: bool,
|
|
75
|
+
sources: list[str]
|
|
76
|
+
) -> RainflowConfiguration:
|
|
77
|
+
payload: _PayloadType = {
|
|
78
|
+
"project": project,
|
|
79
|
+
"max_bins": max_bins,
|
|
80
|
+
"bin_size": bin_size,
|
|
81
|
+
"multiplier": multiplier,
|
|
82
|
+
"reservoir_adjustment": reservoir_adjustment,
|
|
83
|
+
"sources": sources,
|
|
84
|
+
}
|
|
85
|
+
r = self._client._http_request(f"{self._path}/rainflow/setup", "PUT", json=payload)
|
|
86
|
+
return RainflowConfiguration.model_validate_json(r.text)
|
|
87
|
+
|
|
88
|
+
def get_cycle_counts(
|
|
89
|
+
self, project: str, start_time: datetime, end_time: datetime
|
|
90
|
+
) -> bytes:
|
|
91
|
+
params: _PayloadType = {
|
|
92
|
+
"project": project,
|
|
93
|
+
"start_time": start_time.isoformat(),
|
|
94
|
+
"end_time": end_time.isoformat(),
|
|
95
|
+
}
|
|
96
|
+
r = self._client._http_request(
|
|
97
|
+
f"{self._path}/rainflow/cycle_counts", "GET", params=params, stream=True
|
|
98
|
+
)
|
|
99
|
+
return r.content
|
|
100
|
+
|
|
101
|
+
def delete_cycle_counts(
|
|
102
|
+
self, project: str, start_time: datetime, end_time: datetime, ignore_if_not_configured: bool = False
|
|
103
|
+
) -> None:
|
|
104
|
+
params: _PayloadType = {
|
|
105
|
+
"project": project,
|
|
106
|
+
"start_time": start_time.isoformat(),
|
|
107
|
+
"end_time": end_time.isoformat(),
|
|
108
|
+
"ignore_if_not_configured": ignore_if_not_configured,
|
|
109
|
+
}
|
|
110
|
+
self._client._http_request(
|
|
111
|
+
f"{self._path}/rainflow/cycle_counts", "DELETE", params=params
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def calculate_cycle_counts(
|
|
115
|
+
self,
|
|
116
|
+
project: str,
|
|
117
|
+
event: str,
|
|
118
|
+
presigned_url: str,
|
|
119
|
+
mime_type: Literal['application/feather'],
|
|
120
|
+
url_expiry: Optional[datetime] = None,
|
|
121
|
+
ignore_if_not_configured: bool = False
|
|
122
|
+
) -> None:
|
|
123
|
+
payload: _PayloadType = {
|
|
124
|
+
"project": project,
|
|
125
|
+
"event": event,
|
|
126
|
+
"presigned_url": presigned_url,
|
|
127
|
+
"mime_type": mime_type,
|
|
128
|
+
}
|
|
129
|
+
if url_expiry is not None:
|
|
130
|
+
payload["url_expiry"] = url_expiry.isoformat()
|
|
131
|
+
params = {"ignore_if_not_configured": ignore_if_not_configured}
|
|
132
|
+
self._client._http_request(
|
|
133
|
+
f"{self._path}/rainflow/cycle_counts/calculate", "PUT", json=payload, params=params
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# --- Fatigue Connections routes ---
|
|
137
|
+
|
|
138
|
+
def get_connections(self, project: str) -> list[FatigueConnection]:
|
|
139
|
+
params: _PayloadType = {"project": project}
|
|
140
|
+
r = self._client._http_request(f"{self._path}/connections", "GET", params=params)
|
|
141
|
+
return _FatigueConnectionlistAdapter.validate_json(r.text)
|
|
142
|
+
|
|
143
|
+
def add_connection(
|
|
144
|
+
self,
|
|
145
|
+
project: str,
|
|
146
|
+
label: str,
|
|
147
|
+
multiplier: float,
|
|
148
|
+
c_d: float,
|
|
149
|
+
m: float,
|
|
150
|
+
s_0: float,
|
|
151
|
+
bs7608_failure_probability: float,
|
|
152
|
+
bs7608_detail_category: str,
|
|
153
|
+
initial_date: datetime,
|
|
154
|
+
initial_damage: float,
|
|
155
|
+
sources: list[str]
|
|
156
|
+
) -> FatigueConnection:
|
|
157
|
+
payload: _PayloadType = {
|
|
158
|
+
"project": project,
|
|
159
|
+
"label": label,
|
|
160
|
+
"multiplier": multiplier,
|
|
161
|
+
"c_d": c_d,
|
|
162
|
+
"m": m,
|
|
163
|
+
"s_0": s_0,
|
|
164
|
+
"bs7608_failure_probability": bs7608_failure_probability,
|
|
165
|
+
"bs7608_detail_category": bs7608_detail_category,
|
|
166
|
+
"initial_date": initial_date.isoformat(),
|
|
167
|
+
"initial_damage": initial_damage,
|
|
168
|
+
"sources": sources,
|
|
169
|
+
}
|
|
170
|
+
r = self._client._http_request(f"{self._path}/connections", "PUT", json=payload)
|
|
171
|
+
return FatigueConnection.model_validate_json(r.text)
|
|
172
|
+
|
|
173
|
+
def delete_connection(self, connection_code: str) -> None:
|
|
174
|
+
self._client._http_request(f"{self._path}/connections/{connection_code}", "DELETE")
|
|
175
|
+
|
|
176
|
+
# --- Connection Data routes ---
|
|
177
|
+
|
|
178
|
+
def get_connection_remnant_capacity(
|
|
179
|
+
self, project: str, start_time: datetime, end_time: datetime
|
|
180
|
+
) -> list[ConnectionRemnantCapacity]:
|
|
181
|
+
params: _PayloadType = {
|
|
182
|
+
"project": project,
|
|
183
|
+
"start_time": start_time.isoformat(),
|
|
184
|
+
"end_time": end_time.isoformat(),
|
|
185
|
+
}
|
|
186
|
+
r = self._client._http_request(
|
|
187
|
+
f"{self._path}/connection_data/remnant-capacity", "GET", params=params
|
|
188
|
+
)
|
|
189
|
+
return _ConnectionRemnantCapacitylistAdapter.validate_json(r.text)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import TypeAdapter
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..client import MercutoClient
|
|
7
|
+
|
|
8
|
+
from . import _PayloadType
|
|
9
|
+
from ._util import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PermissionGroup(BaseModel):
|
|
13
|
+
tenant: str
|
|
14
|
+
code: str
|
|
15
|
+
label: str
|
|
16
|
+
acl_policy: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Tenant(BaseModel):
|
|
20
|
+
code: str
|
|
21
|
+
name: str
|
|
22
|
+
description: str
|
|
23
|
+
logo_url: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HiddenUserAPIKey(BaseModel):
|
|
27
|
+
code: str
|
|
28
|
+
description: str
|
|
29
|
+
last_used: Optional[str]
|
|
30
|
+
custom_policy: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UserDetails(BaseModel):
|
|
34
|
+
code: str
|
|
35
|
+
username: Optional[str] = None
|
|
36
|
+
email_address: Optional[str] = None
|
|
37
|
+
mobile_number: Optional[str] = None
|
|
38
|
+
first_name: Optional[str] = None
|
|
39
|
+
last_name: Optional[str] = None
|
|
40
|
+
api_keys: list[HiddenUserAPIKey] = []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class User(BaseModel):
|
|
44
|
+
code: str
|
|
45
|
+
username: Optional[str] = None
|
|
46
|
+
description: str
|
|
47
|
+
tenant: str
|
|
48
|
+
permission_group: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CurrentUser(BaseModel):
|
|
52
|
+
code: str
|
|
53
|
+
username: Optional[str] = None
|
|
54
|
+
description: str
|
|
55
|
+
tenant: Tenant
|
|
56
|
+
permission_group: PermissionGroup
|
|
57
|
+
current_permission_policy: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class VisibleUserAPIKey(BaseModel):
|
|
61
|
+
code: str
|
|
62
|
+
new_api_key: str
|
|
63
|
+
description: str
|
|
64
|
+
custom_policy: Optional[str]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class VerifyMyPermissions(BaseModel):
|
|
68
|
+
user: Optional[str]
|
|
69
|
+
acl_policy: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Healthcheck(BaseModel):
|
|
73
|
+
status: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- TypeAdapters for lists ---
|
|
77
|
+
_PermissionGrouplistAdapter = TypeAdapter(list[PermissionGroup])
|
|
78
|
+
_TenantlistAdapter = TypeAdapter(list[Tenant])
|
|
79
|
+
_UserlistAdapter = TypeAdapter(list[User])
|
|
80
|
+
_HiddenUserAPIKeylistAdapter = TypeAdapter(list[HiddenUserAPIKey])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MercutoIdentityService:
|
|
84
|
+
def __init__(self, client: 'MercutoClient', path: str = '/identity') -> None:
|
|
85
|
+
self._client = client
|
|
86
|
+
self._path = path
|
|
87
|
+
|
|
88
|
+
def healthcheck(self) -> Healthcheck:
|
|
89
|
+
r = self._client._http_request(f"{self._path}/healthcheck", "GET")
|
|
90
|
+
return Healthcheck.model_validate_json(r.text)
|
|
91
|
+
|
|
92
|
+
# --- Verify routes ---
|
|
93
|
+
|
|
94
|
+
def get_my_permissions(self) -> VerifyMyPermissions:
|
|
95
|
+
r = self._client._http_request(f"{self._path}/verify/me", "GET")
|
|
96
|
+
return VerifyMyPermissions.model_validate_json(r.text)
|
|
97
|
+
|
|
98
|
+
# --- User routes ---
|
|
99
|
+
|
|
100
|
+
def list_users(self, tenant: Optional[str] = None) -> list[User]:
|
|
101
|
+
params: _PayloadType = {}
|
|
102
|
+
if tenant is not None:
|
|
103
|
+
params["tenant"] = tenant
|
|
104
|
+
r = self._client._http_request(f"{self._path}/users", "GET", params=params)
|
|
105
|
+
return _UserlistAdapter.validate_json(r.text)
|
|
106
|
+
|
|
107
|
+
def create_user(
|
|
108
|
+
self,
|
|
109
|
+
username: str,
|
|
110
|
+
tenant: str,
|
|
111
|
+
description: str,
|
|
112
|
+
group: str,
|
|
113
|
+
default_password: Optional[str] = None
|
|
114
|
+
) -> User:
|
|
115
|
+
payload: _PayloadType = {
|
|
116
|
+
"username": username,
|
|
117
|
+
"tenant_code": tenant,
|
|
118
|
+
"description": description,
|
|
119
|
+
"group_code": group,
|
|
120
|
+
"default_password": default_password,
|
|
121
|
+
}
|
|
122
|
+
r = self._client._http_request(f"{self._path}/users", "PUT", json=payload)
|
|
123
|
+
return User.model_validate_json(r.text)
|
|
124
|
+
|
|
125
|
+
def get_current_user(self) -> CurrentUser:
|
|
126
|
+
r = self._client._http_request(f"{self._path}/users/me", "GET")
|
|
127
|
+
return CurrentUser.model_validate_json(r.text)
|
|
128
|
+
|
|
129
|
+
def get_user(self, code: str) -> User:
|
|
130
|
+
r = self._client._http_request(f"{self._path}/users/{code}", "GET")
|
|
131
|
+
return User.model_validate_json(r.text)
|
|
132
|
+
|
|
133
|
+
def delete_user(self, code: str) -> None:
|
|
134
|
+
self._client._http_request(f"{self._path}/users/{code}", "DELETE")
|
|
135
|
+
|
|
136
|
+
def edit_user(
|
|
137
|
+
self,
|
|
138
|
+
code: str,
|
|
139
|
+
description: str,
|
|
140
|
+
group: str
|
|
141
|
+
) -> User:
|
|
142
|
+
payload: _PayloadType = {
|
|
143
|
+
"description": description,
|
|
144
|
+
"group_code": group,
|
|
145
|
+
}
|
|
146
|
+
r = self._client._http_request(f"{self._path}/users/{code}", "PATCH", json=payload)
|
|
147
|
+
return User.model_validate_json(r.text)
|
|
148
|
+
|
|
149
|
+
def get_user_details(self, code: str) -> UserDetails:
|
|
150
|
+
r = self._client._http_request(f"{self._path}/users/{code}/details", "GET")
|
|
151
|
+
return UserDetails.model_validate_json(r.text)
|
|
152
|
+
|
|
153
|
+
def set_user_details(
|
|
154
|
+
self,
|
|
155
|
+
code: str,
|
|
156
|
+
email_address: Optional[str] = None,
|
|
157
|
+
mobile_number: Optional[str] = None,
|
|
158
|
+
first_name: Optional[str] = None,
|
|
159
|
+
last_name: Optional[str] = None
|
|
160
|
+
) -> UserDetails:
|
|
161
|
+
payload: _PayloadType = {
|
|
162
|
+
"email_address": email_address,
|
|
163
|
+
"mobile_number": mobile_number,
|
|
164
|
+
"first_name": first_name,
|
|
165
|
+
"last_name": last_name,
|
|
166
|
+
}
|
|
167
|
+
r = self._client._http_request(f"{self._path}/users/{code}/details", "PATCH", json=payload)
|
|
168
|
+
return UserDetails.model_validate_json(r.text)
|
|
169
|
+
|
|
170
|
+
def get_user_api_keys(self, user: str) -> list[HiddenUserAPIKey]:
|
|
171
|
+
r = self._client._http_request(f"{self._path}/users/{user}/api_keys", "GET")
|
|
172
|
+
return _HiddenUserAPIKeylistAdapter.validate_json(r.text)
|
|
173
|
+
|
|
174
|
+
def generate_api_key_for_user(
|
|
175
|
+
self,
|
|
176
|
+
user: str,
|
|
177
|
+
description: str,
|
|
178
|
+
custom_policy: Optional[str] = None
|
|
179
|
+
) -> VisibleUserAPIKey:
|
|
180
|
+
payload: _PayloadType = {
|
|
181
|
+
"description": description,
|
|
182
|
+
"custom_policy": custom_policy,
|
|
183
|
+
}
|
|
184
|
+
r = self._client._http_request(f"{self._path}/users/{user}/api_keys", "POST", json=payload)
|
|
185
|
+
return VisibleUserAPIKey.model_validate_json(r.text)
|
|
186
|
+
|
|
187
|
+
def delete_api_key(self, user: str, key_code: str) -> None:
|
|
188
|
+
self._client._http_request(f"{self._path}/users/{user}/api_keys/{key_code}", "DELETE")
|
|
189
|
+
|
|
190
|
+
# --- Tenants routes ---
|
|
191
|
+
|
|
192
|
+
def list_tenants(self) -> list[Tenant]:
|
|
193
|
+
r = self._client._http_request(f"{self._path}/tenants", "GET")
|
|
194
|
+
return _TenantlistAdapter.validate_json(r.text)
|
|
195
|
+
|
|
196
|
+
def get_tenant(self, code: str) -> Tenant:
|
|
197
|
+
r = self._client._http_request(f"{self._path}/tenants/{code}", "GET")
|
|
198
|
+
return Tenant.model_validate_json(r.text)
|
|
199
|
+
|
|
200
|
+
def create_tenant(
|
|
201
|
+
self,
|
|
202
|
+
name: str,
|
|
203
|
+
description: str,
|
|
204
|
+
logo_url: Optional[str] = None
|
|
205
|
+
) -> Tenant:
|
|
206
|
+
payload: _PayloadType = {
|
|
207
|
+
"name": name,
|
|
208
|
+
"description": description,
|
|
209
|
+
"logo_url": logo_url,
|
|
210
|
+
}
|
|
211
|
+
r = self._client._http_request(f"{self._path}/tenants", "PUT", json=payload)
|
|
212
|
+
return Tenant.model_validate_json(r.text)
|
|
213
|
+
|
|
214
|
+
# --- Permission Groups routes ---
|
|
215
|
+
|
|
216
|
+
def get_permission_groups(self, tenant: Optional[str] = None) -> list[PermissionGroup]:
|
|
217
|
+
params = {}
|
|
218
|
+
if tenant is not None:
|
|
219
|
+
params["tenant"] = tenant
|
|
220
|
+
r = self._client._http_request(f"{self._path}/permissions", "GET", params=params)
|
|
221
|
+
return _PermissionGrouplistAdapter.validate_json(r.text)
|
|
222
|
+
|
|
223
|
+
def create_permission_group(
|
|
224
|
+
self,
|
|
225
|
+
tenant: str,
|
|
226
|
+
label: str,
|
|
227
|
+
acl_policy: str
|
|
228
|
+
) -> PermissionGroup:
|
|
229
|
+
payload: _PayloadType = {
|
|
230
|
+
"tenant": tenant,
|
|
231
|
+
"label": label,
|
|
232
|
+
"acl_policy": acl_policy,
|
|
233
|
+
}
|
|
234
|
+
r = self._client._http_request(f"{self._path}/permissions", "PUT", json=payload)
|
|
235
|
+
return PermissionGroup.model_validate_json(r.text)
|
|
236
|
+
|
|
237
|
+
def get_permission_group(self, group: str) -> PermissionGroup:
|
|
238
|
+
r = self._client._http_request(f"{self._path}/permissions/{group}", "GET")
|
|
239
|
+
return PermissionGroup.model_validate_json(r.text)
|
|
240
|
+
|
|
241
|
+
def delete_permission_group(self, group: str) -> None:
|
|
242
|
+
self._client._http_request(f"{self._path}/permissions/{group}", "DELETE")
|
|
243
|
+
|
|
244
|
+
def modify_permission_group(
|
|
245
|
+
self,
|
|
246
|
+
group: str,
|
|
247
|
+
label: str,
|
|
248
|
+
acl_policy: str
|
|
249
|
+
) -> None:
|
|
250
|
+
payload: _PayloadType = {
|
|
251
|
+
"label": label,
|
|
252
|
+
"acl_policy": acl_policy,
|
|
253
|
+
}
|
|
254
|
+
self._client._http_request(f"{self._path}/permissions/{group}", "PATCH", json=payload)
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
import shutil
|
|
3
|
+
from datetime import timedelta
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Iterable, Iterator, TypeVar
|
|
5
6
|
|
|
6
7
|
import requests
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
def timedelta_isoformat(td: timedelta) -> str:
|
|
11
|
+
"""
|
|
12
|
+
ISO 8601 encoding for Python timedelta object.
|
|
13
|
+
Taken from pydantic source:
|
|
14
|
+
https://github.com/pydantic/pydantic/blob/3704eccce4661455acdda1cdcf716bd4b3382e08/pydantic/deprecated/json.py#L135-L140
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
minutes, seconds = divmod(td.seconds, 60)
|
|
18
|
+
hours, minutes = divmod(minutes, 60)
|
|
19
|
+
return f'{"-" if td.days < 0 else ""}P{abs(td.days)}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S'
|
|
20
|
+
|
|
21
|
+
|
|
9
22
|
def get_my_public_ip() -> str:
|
|
10
23
|
"""
|
|
11
24
|
Fetches the public IP address of the machine making the request.
|
|
@@ -51,14 +64,17 @@ def get_free_space_excluding_files(directory: str) -> int:
|
|
|
51
64
|
|
|
52
65
|
T = TypeVar('T')
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
try:
|
|
68
|
+
from itertools import batched # type: ignore
|
|
69
|
+
except ImportError:
|
|
70
|
+
# Python < 3.12
|
|
71
|
+
def batched(iterable: Iterable[T], n: int) -> Iterator[tuple[T, ...]]: # type: ignore[no-redef]
|
|
72
|
+
"""
|
|
73
|
+
Implementation of itertools.batched for < Python 3.12
|
|
74
|
+
"""
|
|
75
|
+
it = iter(iterable)
|
|
76
|
+
while True:
|
|
77
|
+
chunk = tuple(itertools.islice(it, n))
|
|
78
|
+
if not chunk:
|
|
79
|
+
break
|
|
80
|
+
yield chunk
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mercuto-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Library for interfacing with Rockfield's Mercuto API
|
|
5
5
|
Author-email: Daniel Whipp <daniel.whipp@rocktech.com.au>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -22,6 +22,7 @@ Requires-Dist: pyftpdlib>=2.0.1
|
|
|
22
22
|
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
23
23
|
Requires-Dist: pytz>=2025.2
|
|
24
24
|
Requires-Dist: schedule>=1.2.2
|
|
25
|
+
Requires-Dist: pydantic>=2.0
|
|
25
26
|
Dynamic: license-file
|
|
26
27
|
|
|
27
28
|
# Mercuto Client Python Library
|
|
@@ -42,7 +43,7 @@ Use the `connect()` function exposed within the main package and provide your AP
|
|
|
42
43
|
from mercuto_client import connect
|
|
43
44
|
|
|
44
45
|
client = connect(api_key="<YOUR API KEY>")
|
|
45
|
-
print(client.
|
|
46
|
+
print(client.core().list_projects())
|
|
46
47
|
|
|
47
48
|
# Logout after finished.
|
|
48
49
|
client.logout()
|
|
@@ -55,7 +56,7 @@ You can also use the client as a context manager. It will logout automatically.
|
|
|
55
56
|
from mercuto_client import MercutoClient
|
|
56
57
|
|
|
57
58
|
with MercutoClient.as_credentials(api_key='<YOUR API KEY>') as client:
|
|
58
|
-
print(client.
|
|
59
|
+
print(client.core().list_projects())
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
## Current Status
|
|
@@ -63,3 +64,9 @@ This library is incomplete and may not be fully compliant with the latest Mercut
|
|
|
63
64
|
|
|
64
65
|
- [x] API Based login (Completed)
|
|
65
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`
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
mercuto_client/__init__.py,sha256=wsBZ73gKjbKLHfJD0nSSl1xOY69HxwALGX73vm7m6iA,283
|
|
2
|
+
mercuto_client/_authentication.py,sha256=M213OBPYs7x5anK9PL92K_8w-orh9QNYm1-Rsdze6gQ,2700
|
|
3
|
+
mercuto_client/acl.py,sha256=_1ogLx62JrYe3igqZwgop-m5NbLoUZPXCo0pEpSR6pc,2927
|
|
4
|
+
mercuto_client/client.py,sha256=GnKB-q0IWPcMkLYr4WL_Mu8p8IzRRYY2FCNO1pi3JvM,6931
|
|
5
|
+
mercuto_client/exceptions.py,sha256=xyUXVSUOXKoMV23hh0qHrRtQuojDBv5e-I0dU7x6a0c,507
|
|
6
|
+
mercuto_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
mercuto_client/util.py,sha256=PWNp8BU9wa_sKMPxW0R1yveEF1XslZXG8sw6BZvUoO8,2603
|
|
8
|
+
mercuto_client/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
mercuto_client/_tests/conftest.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
mercuto_client/_tests/test_ingester/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
mercuto_client/_tests/test_ingester/test_file_processor.py,sha256=kC1DC0phmjl7jBMMBJYrs9Tx4NL9xKJNmqVX5FNH59s,7399
|
|
12
|
+
mercuto_client/_tests/test_ingester/test_ftp.py,sha256=w1CHAGcZy88D2-nY61Gj16l1nHcer9LIKaMc_DXk23o,1318
|
|
13
|
+
mercuto_client/_tests/test_ingester/test_parsers.py,sha256=R9GnzAaGu_tva0s23VpwEVfEsEUcto3kN3EloIxZvVY,5917
|
|
14
|
+
mercuto_client/_tests/test_mocking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
mercuto_client/_tests/test_mocking/conftest.py,sha256=M-HikiSj_uuYAEYQnFA0uDtSBpZjO2YXjzxNbluQdMQ,266
|
|
16
|
+
mercuto_client/_tests/test_mocking/test_mock_identity.py,sha256=394r6A_xK78tse4CZA345JpIlIKF_GnPz2xXEjc_Nho,313
|
|
17
|
+
mercuto_client/ingester/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
mercuto_client/ingester/__main__.py,sha256=HZisjXuj5wzdUJ9vLqjhCDqLobflLLVLuI-7PclvVgw,6762
|
|
19
|
+
mercuto_client/ingester/ftp.py,sha256=3-gMzoRCWjLZWeynjkwOXV59B4f0F2VnWp97fuUFTX4,4441
|
|
20
|
+
mercuto_client/ingester/mercuto.py,sha256=djlP9HVoBoB2R-pQVogyMvHpPpbWEjCX5vbMhlEhc8U,6077
|
|
21
|
+
mercuto_client/ingester/processor.py,sha256=XlMMM0taSHZzth39qVMsUkPO0g_ahC7Xcb01rOjQp3I,11906
|
|
22
|
+
mercuto_client/ingester/parsers/__init__.py,sha256=lOt4TyP08hK56wl7XHMlqmj62mDE0Idx8UJL0wljtwM,1398
|
|
23
|
+
mercuto_client/ingester/parsers/campbell.py,sha256=S5enYbajVTm3zSQYkEP6JRVUw94Z7ky100j8p5qLCls,441
|
|
24
|
+
mercuto_client/ingester/parsers/generic_csv.py,sha256=v4rwO4oJb1Ue6zirO0TGtvewOSp9f6ZUT_sWvozvQHo,4051
|
|
25
|
+
mercuto_client/ingester/parsers/worldsensing.py,sha256=rY3Io4mh8htfV4TghnCDkehPZIVoMqGWxEWnDVmZM6I,1028
|
|
26
|
+
mercuto_client/mocks/__init__.py,sha256=Y01V4eyzC5hGX3A9XwqPC82glTSIaWLAPB3lOyBymqg,2988
|
|
27
|
+
mercuto_client/mocks/_utility.py,sha256=YiUm_LzDOdHEhgkve_A5AEUAr9FmcPdVJi3R_izO8yw,2174
|
|
28
|
+
mercuto_client/mocks/mock_data.py,sha256=PqVtKe6eShkfAvMUkZyf_w4-yed2TEx3CncwGIRmxWA,17964
|
|
29
|
+
mercuto_client/mocks/mock_fatigue.py,sha256=GYcud-Rmpyz9wTYY9rsePvCUviIvu6c8wGXqe3L0MDk,940
|
|
30
|
+
mercuto_client/mocks/mock_identity.py,sha256=2Qc9hze9lpPnfa8aj25NDYdDii15hgqej0ymBc3DWQQ,7318
|
|
31
|
+
mercuto_client/modules/__init__.py,sha256=oPbPfkgL0JfCYRITS8O7uL8Zh8pjJTTPoEpK-nHmRuU,747
|
|
32
|
+
mercuto_client/modules/_util.py,sha256=edPLMJsdxd3oIqMDYNtUIAbg_wKypmr-C8ilBzmafDY,403
|
|
33
|
+
mercuto_client/modules/core.py,sha256=KzSmKH71qK9wDM6I-LvXY9oA2nBJFLoQXawK2ei3uCY,22828
|
|
34
|
+
mercuto_client/modules/data.py,sha256=hMhzkkr0f7N9kVKvUQE65uXDpcCxGILcNUhZLSD3nD4,20742
|
|
35
|
+
mercuto_client/modules/fatigue.py,sha256=SPftwW5rMk6LcsIGWO8OHc9r6DA99qt0AyAVOaNRt_8,6397
|
|
36
|
+
mercuto_client/modules/identity.py,sha256=VeMGsq-s5B9xorP9G2TxwIDYUSyyDGuFAJ8Ilg7_mn4,7898
|
|
37
|
+
mercuto_client-0.3.0.dist-info/licenses/LICENSE,sha256=0R2QbX4pr5XSiwUc2JoGS7Ja4npcQHyZlGJsR-E73I8,32386
|
|
38
|
+
mercuto_client-0.3.0.dist-info/METADATA,sha256=ouV5JGG54QsV1TZBHgO1aD9qp1wb7uh-geGg9qjtAMs,2421
|
|
39
|
+
mercuto_client-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
40
|
+
mercuto_client-0.3.0.dist-info/top_level.txt,sha256=ecV4spooVaOU8AlclvojxY1LzLW1byDywh-ayLHvKCs,15
|
|
41
|
+
mercuto_client-0.3.0.dist-info/RECORD,,
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from .. import MercutoClient
|
|
4
|
-
from ..mocks import mock_client
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_mock_injection_before_client_creation():
|
|
8
|
-
count = 0
|
|
9
|
-
|
|
10
|
-
def on_get_healthcheck(*args, **kwargs):
|
|
11
|
-
nonlocal count
|
|
12
|
-
count += 1
|
|
13
|
-
return 'mocked'
|
|
14
|
-
with mock_client() as mock:
|
|
15
|
-
mock.on('GET', '/healthcheck', on_get_healthcheck)
|
|
16
|
-
client = MercutoClient()
|
|
17
|
-
assert client.healthcheck() == 'mocked'
|
|
18
|
-
assert count == 1
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_mock_injection_after_client_creation():
|
|
22
|
-
count = 0
|
|
23
|
-
client = MercutoClient()
|
|
24
|
-
|
|
25
|
-
def on_get_healthcheck(*args, **kwargs):
|
|
26
|
-
nonlocal count
|
|
27
|
-
count += 1
|
|
28
|
-
return 'mocked'
|
|
29
|
-
with mock_client() as mock:
|
|
30
|
-
mock.on('GET', '/healthcheck', on_get_healthcheck)
|
|
31
|
-
assert client.healthcheck() == 'mocked'
|
|
32
|
-
assert count == 1
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_mock_releases_after_end_of_context():
|
|
36
|
-
client = MercutoClient()
|
|
37
|
-
with mock_client() as mock:
|
|
38
|
-
key = mock.add_user(user='this is a test')
|
|
39
|
-
client.connect(api_key=key)
|
|
40
|
-
assert client.identity().verify_me()['user'] == 'this is a test'
|
|
41
|
-
|
|
42
|
-
with pytest.raises(Exception):
|
|
43
|
-
client.identity().verify_me()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_mock_verify_me():
|
|
47
|
-
client = MercutoClient()
|
|
48
|
-
with mock_client() as mock:
|
|
49
|
-
with pytest.raises(Exception):
|
|
50
|
-
client.identity().verify_me()
|
|
51
|
-
|
|
52
|
-
client.connect(api_key='bad api key')
|
|
53
|
-
with pytest.raises(Exception):
|
|
54
|
-
client.identity().verify_me()
|
|
55
|
-
|
|
56
|
-
key = mock.add_user(user='this is a test user',
|
|
57
|
-
tenant='test-tenant', permission_group='test-group')
|
|
58
|
-
client.connect(api_key=key)
|
|
59
|
-
assert client.identity().verify_me()['user'] == 'this is a test user'
|
|
60
|
-
assert client.identity().verify_me()['tenant'] == 'test-tenant'
|
|
61
|
-
assert client.identity().verify_me()[
|
|
62
|
-
'permission_group'] == 'test-group'
|
|
63
|
-
|
|
64
|
-
mock.delete_user(key)
|
|
65
|
-
with pytest.raises(Exception):
|
|
66
|
-
client.identity().verify_me()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_mock_get_user():
|
|
70
|
-
client = MercutoClient()
|
|
71
|
-
with mock_client() as mock:
|
|
72
|
-
client.connect(api_key='bad api key')
|
|
73
|
-
with pytest.raises(Exception):
|
|
74
|
-
client.identity().get_user('12345')
|
|
75
|
-
|
|
76
|
-
key = mock.add_user(user='code1', tenant='test-tenant', permission_group='test-group',
|
|
77
|
-
username='testing@example.com')
|
|
78
|
-
client.connect(api_key=key)
|
|
79
|
-
|
|
80
|
-
assert client.identity().get_user('code1')['code'] == 'code1'
|
|
81
|
-
assert client.identity().get_user(
|
|
82
|
-
'code1')['username'] == 'testing@example.com'
|
|
83
|
-
|
|
84
|
-
mock.delete_user(key)
|
|
85
|
-
with pytest.raises(Exception):
|
|
86
|
-
client.identity().verify_me()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def test_mock_unsupported_endpoint():
|
|
90
|
-
client = MercutoClient()
|
|
91
|
-
with mock_client():
|
|
92
|
-
with pytest.raises(NotImplementedError):
|
|
93
|
-
client.identity().list_tenants()
|
mercuto_client/_util.py
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from datetime import timedelta
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def timedelta_isoformat(td: timedelta) -> str:
|
|
5
|
-
"""
|
|
6
|
-
ISO 8601 encoding for Python timedelta object.
|
|
7
|
-
Taken from pydantic source:
|
|
8
|
-
https://github.com/pydantic/pydantic/blob/3704eccce4661455acdda1cdcf716bd4b3382e08/pydantic/deprecated/json.py#L135-L140
|
|
9
|
-
|
|
10
|
-
"""
|
|
11
|
-
minutes, seconds = divmod(td.seconds, 60)
|
|
12
|
-
hours, minutes = divmod(minutes, 60)
|
|
13
|
-
return f'{"-" if td.days < 0 else ""}P{abs(td.days)}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S'
|