dimo-python-sdk 1.3.2__tar.gz → 1.3.4__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.
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/PKG-INFO +1 -1
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/api/auth.py +35 -5
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/api/token_exchange.py +3 -0
- dimo_python_sdk-1.3.4/dimo/dimo.py +93 -0
- dimo_python_sdk-1.3.4/dimo/request.py +57 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo_python_sdk.egg-info/PKG-INFO +1 -1
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo_python_sdk.egg-info/SOURCES.txt +5 -1
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/pyproject.toml +1 -1
- dimo_python_sdk-1.3.4/tests/test_dimo.py +60 -0
- dimo_python_sdk-1.3.4/tests/test_errors.py +33 -0
- dimo_python_sdk-1.3.4/tests/test_permission_decoder.py +13 -0
- dimo_python_sdk-1.3.4/tests/test_request.py +90 -0
- dimo_python_sdk-1.3.2/dimo/dimo.py +0 -65
- dimo_python_sdk-1.3.2/dimo/request.py +0 -38
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/LICENSE +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/README.md +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/__init__.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/api/__init__.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/api/attestation.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/api/device_definitions.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/api/trips.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/api/valuations.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/constants.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/environments.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/errors.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/eth_signer.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/graphql/__init__.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/graphql/identity.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/graphql/telemetry.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo/permission_decoder.py +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo_python_sdk.egg-info/dependency_links.txt +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo_python_sdk.egg-info/requires.txt +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo_python_sdk.egg-info/top_level.txt +0 -0
- {dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/setup.cfg +0 -0
|
@@ -2,6 +2,7 @@ from dimo.eth_signer import EthSigner
|
|
|
2
2
|
from dimo.errors import check_type, check_optional_type
|
|
3
3
|
from urllib.parse import urlencode
|
|
4
4
|
from typing import Dict, Optional
|
|
5
|
+
import json
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class Auth:
|
|
@@ -36,13 +37,18 @@ class Auth:
|
|
|
36
37
|
"address": address,
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
response = self._request(
|
|
40
41
|
"POST",
|
|
41
42
|
"Auth",
|
|
42
43
|
"/auth/web3/generate_challenge",
|
|
43
44
|
data=urlencode(body),
|
|
44
45
|
headers=headers,
|
|
45
46
|
)
|
|
47
|
+
|
|
48
|
+
if isinstance(response, bytes):
|
|
49
|
+
response = json.loads(response.decode('utf-8'))
|
|
50
|
+
|
|
51
|
+
return response
|
|
46
52
|
|
|
47
53
|
def sign_challenge(self, message: str, private_key: str) -> str:
|
|
48
54
|
check_type("message", message, str)
|
|
@@ -78,13 +84,18 @@ class Auth:
|
|
|
78
84
|
|
|
79
85
|
encoded_data = urlencode(form_data)
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
response = self._request(
|
|
82
88
|
"POST",
|
|
83
89
|
"Auth",
|
|
84
90
|
"/auth/web3/submit_challenge",
|
|
85
91
|
data=encoded_data,
|
|
86
92
|
headers=headers,
|
|
87
93
|
)
|
|
94
|
+
|
|
95
|
+
if isinstance(response, bytes):
|
|
96
|
+
response = json.loads(response.decode('utf-8'))
|
|
97
|
+
|
|
98
|
+
return response
|
|
88
99
|
|
|
89
100
|
# Requires client_id, domain, and private_key. Address defaults to client_id.
|
|
90
101
|
def get_dev_jwt(
|
|
@@ -96,7 +107,18 @@ class Auth:
|
|
|
96
107
|
scope="openid email",
|
|
97
108
|
response_type="code",
|
|
98
109
|
) -> Dict:
|
|
99
|
-
|
|
110
|
+
"""
|
|
111
|
+
Generate a signed developer JWT in one step.
|
|
112
|
+
For testing, mocks and POCs.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
client_id (str): The Ethereum address of the client
|
|
116
|
+
domain (str): The domain name for the client
|
|
117
|
+
private_key (str): The private key to sign the challenge
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
dict: The authentication response containing access_token
|
|
121
|
+
"""
|
|
100
122
|
check_type("client_id", client_id, str)
|
|
101
123
|
check_type("domain", domain, str)
|
|
102
124
|
check_type("private_key", private_key, str)
|
|
@@ -109,6 +131,7 @@ class Auth:
|
|
|
109
131
|
|
|
110
132
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
111
133
|
|
|
134
|
+
# Generate a challenge
|
|
112
135
|
challenge = self.generate_challenge(
|
|
113
136
|
headers=headers,
|
|
114
137
|
client_id=client_id,
|
|
@@ -117,14 +140,21 @@ class Auth:
|
|
|
117
140
|
response_type=response_type,
|
|
118
141
|
address=address,
|
|
119
142
|
)
|
|
120
|
-
|
|
143
|
+
|
|
144
|
+
if isinstance(challenge, bytes):
|
|
145
|
+
challenge = json.loads(challenge.decode('utf-8'))
|
|
146
|
+
|
|
121
147
|
sign = self.sign_challenge(
|
|
122
148
|
message=challenge["challenge"],
|
|
123
149
|
private_key=private_key,
|
|
124
150
|
)
|
|
125
|
-
|
|
151
|
+
|
|
126
152
|
state = challenge["state"]
|
|
127
153
|
signature = sign
|
|
128
154
|
|
|
129
155
|
submit = self.submit_challenge(client_id, domain, state, signature, headers)
|
|
156
|
+
|
|
157
|
+
if isinstance(submit, bytes):
|
|
158
|
+
submit = json.loads(submit.decode('utf-8'))
|
|
159
|
+
|
|
130
160
|
return submit
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from dimo.constants import dimo_constants
|
|
2
2
|
from dimo.errors import check_type, check_optional_type
|
|
3
3
|
from dimo.permission_decoder import PermissionDecoder
|
|
4
|
+
import json
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class TokenExchange:
|
|
@@ -17,6 +18,8 @@ class TokenExchange:
|
|
|
17
18
|
def _decode_vehicle_permissions(self, token_id: int, client_id: str) -> dict:
|
|
18
19
|
response = self._identity.check_vehicle_privileges(token_id)
|
|
19
20
|
try:
|
|
21
|
+
# If response is bytes
|
|
22
|
+
response = json.loads(response.decode('utf-8'))
|
|
20
23
|
nodes = (
|
|
21
24
|
response.get("data", {})
|
|
22
25
|
.get("vehicle", {})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing_extensions import Optional
|
|
2
|
+
from requests import Session
|
|
3
|
+
|
|
4
|
+
from .api.attestation import Attestation
|
|
5
|
+
from .api.auth import Auth
|
|
6
|
+
from .api.device_definitions import DeviceDefinitions
|
|
7
|
+
from .api.token_exchange import TokenExchange
|
|
8
|
+
from .api.trips import Trips
|
|
9
|
+
from .api.valuations import Valuations
|
|
10
|
+
|
|
11
|
+
from .graphql.identity import Identity
|
|
12
|
+
from .graphql.telemetry import Telemetry
|
|
13
|
+
|
|
14
|
+
from .request import Request
|
|
15
|
+
from .environments import dimo_environment
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from typing_extensions import Dict
|
|
18
|
+
from typing import Any
|
|
19
|
+
from urllib.parse import urljoin
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DIMO:
|
|
23
|
+
|
|
24
|
+
def __init__(self, env: str = "Production", session: Optional[Session] = None) -> None:
|
|
25
|
+
|
|
26
|
+
self.env = env
|
|
27
|
+
# Assert valid environment specified
|
|
28
|
+
if env not in dimo_environment:
|
|
29
|
+
raise ValueError(f"Unknown environment: {env}")
|
|
30
|
+
|
|
31
|
+
self.urls = dimo_environment[env]
|
|
32
|
+
|
|
33
|
+
self._client_id: Optional[str] = None
|
|
34
|
+
self._services: Dict[str, Any] = {}
|
|
35
|
+
self.session = session or Session() # Use the provided session or create a new one
|
|
36
|
+
|
|
37
|
+
# Creates a full path for endpoints combining DIMO service, specific endpoint, and optional params
|
|
38
|
+
def _get_full_path(self, service: str, path: str, params=None) -> str:
|
|
39
|
+
base_path = self.urls[service]
|
|
40
|
+
path_formatted = path.format(**(params or {}))
|
|
41
|
+
return urljoin(base_path, path_formatted)
|
|
42
|
+
|
|
43
|
+
# Sets headers based on access_token or privileged_token
|
|
44
|
+
def _get_auth_headers(self, token):
|
|
45
|
+
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
46
|
+
|
|
47
|
+
# request method for HTTP requests for the REST API
|
|
48
|
+
def request(self, http_method, service, path, **kwargs):
|
|
49
|
+
full_path = self._get_full_path(service, path)
|
|
50
|
+
return Request(http_method, full_path, self.session)(**kwargs)
|
|
51
|
+
|
|
52
|
+
# query method for graphQL queries, identity and telemetry
|
|
53
|
+
def query(self, service, query, variables=None, token=None):
|
|
54
|
+
headers = self._get_auth_headers(token) if token else {}
|
|
55
|
+
headers["Content-Type"] = "application/json"
|
|
56
|
+
headers["User-Agent"] = "dimo-python-sdk"
|
|
57
|
+
|
|
58
|
+
data = {"query": query, "variables": variables or {}}
|
|
59
|
+
|
|
60
|
+
response = self.request("POST", service, "", headers=headers, data=data)
|
|
61
|
+
return response
|
|
62
|
+
|
|
63
|
+
def __getattr__(self, name: str) -> Any:
|
|
64
|
+
"""
|
|
65
|
+
Lazy-load and cache service modules as attributes
|
|
66
|
+
"""
|
|
67
|
+
# If service is already created, return from cache
|
|
68
|
+
if name in self._services:
|
|
69
|
+
return self._services[name]
|
|
70
|
+
# Otherwise, see if its a known service
|
|
71
|
+
mapping = {
|
|
72
|
+
"attestation": (Attestation, ("request", "_get_auth_headers")),
|
|
73
|
+
"auth": (Auth, ("request", "_get_auth_headers", "env", "self")),
|
|
74
|
+
"device_definitions": (DeviceDefinitions, ("request", "_get_auth_headers")),
|
|
75
|
+
"token_exchange": (
|
|
76
|
+
TokenExchange,
|
|
77
|
+
("request", "_get_auth_headers", "identity", "self"),
|
|
78
|
+
),
|
|
79
|
+
"trips": (Trips, ("request", "_get_auth_headers")),
|
|
80
|
+
"valuations": (Valuations, ("request", "_get_auth_headers")),
|
|
81
|
+
"identity": (Identity, ("self",)),
|
|
82
|
+
"telemetry": (Telemetry, ("self",)),
|
|
83
|
+
}
|
|
84
|
+
if name in mapping:
|
|
85
|
+
cls, deps = mapping[name]
|
|
86
|
+
args = [getattr(self, dep) if dep != "self" else self for dep in deps]
|
|
87
|
+
instance = cls(*args)
|
|
88
|
+
# And cache the service for future use
|
|
89
|
+
self._services[name] = instance
|
|
90
|
+
return instance
|
|
91
|
+
raise AttributeError(
|
|
92
|
+
f"{self.__class__.__name__!r} object has no attribute {name!r}"
|
|
93
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
from requests import Session, RequestException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HTTPError(Exception):
|
|
7
|
+
"""Http error wrapper with status code and (optional) response body"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, status: int, message: str, body: Any = None):
|
|
10
|
+
super().__init__(f"HTTP {status}: {message}")
|
|
11
|
+
self.status = status
|
|
12
|
+
self.body = body
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Request:
|
|
16
|
+
|
|
17
|
+
def __init__(self, http_method: str, url: str, session: Session):
|
|
18
|
+
self.http_method = http_method
|
|
19
|
+
self.url = url
|
|
20
|
+
self.session = session
|
|
21
|
+
|
|
22
|
+
def __call__(self, headers=None, data=None, params=None, **kwargs):
|
|
23
|
+
headers = headers or {}
|
|
24
|
+
headers.update(kwargs.pop("headers", {}))
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
data
|
|
28
|
+
and isinstance(data, dict)
|
|
29
|
+
and headers.get("Content-Type") == "application/json"
|
|
30
|
+
):
|
|
31
|
+
data = json.dumps(data)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
response = self.session.request(
|
|
35
|
+
method=self.http_method,
|
|
36
|
+
url=self.url,
|
|
37
|
+
headers=headers,
|
|
38
|
+
params=params,
|
|
39
|
+
data=data,
|
|
40
|
+
**kwargs,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
except RequestException as exc:
|
|
45
|
+
status = getattr(exc.response, "status_code", None)
|
|
46
|
+
body = None
|
|
47
|
+
try:
|
|
48
|
+
body = exc.response.json()
|
|
49
|
+
except Exception:
|
|
50
|
+
body = exc.response.txt if exc.response else None
|
|
51
|
+
raise HTTPError(status=status or -1, message=str(exc), body=body)
|
|
52
|
+
|
|
53
|
+
content_type = response.headers.get("Content-Type", "")
|
|
54
|
+
if "application/json" in content_type:
|
|
55
|
+
return response.json()
|
|
56
|
+
|
|
57
|
+
return response.content
|
|
@@ -23,4 +23,8 @@ dimo_python_sdk.egg-info/PKG-INFO
|
|
|
23
23
|
dimo_python_sdk.egg-info/SOURCES.txt
|
|
24
24
|
dimo_python_sdk.egg-info/dependency_links.txt
|
|
25
25
|
dimo_python_sdk.egg-info/requires.txt
|
|
26
|
-
dimo_python_sdk.egg-info/top_level.txt
|
|
26
|
+
dimo_python_sdk.egg-info/top_level.txt
|
|
27
|
+
tests/test_dimo.py
|
|
28
|
+
tests/test_errors.py
|
|
29
|
+
tests/test_permission_decoder.py
|
|
30
|
+
tests/test_request.py
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
from dimo.dimo import DIMO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_get_full_path_no_params():
|
|
7
|
+
client = DIMO(env="Dev")
|
|
8
|
+
result = client._get_full_path("Valuations", "/v2/vehicles/1234/valuations")
|
|
9
|
+
assert result == "https://valuations-api.dev.dimo.zone/v2/vehicles/1234/valuations"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_get_full_path_with_params():
|
|
13
|
+
client = DIMO(env="Dev")
|
|
14
|
+
result = client._get_full_path(
|
|
15
|
+
"Telemetry",
|
|
16
|
+
"/items/{item_id}",
|
|
17
|
+
{"item_id": 123},
|
|
18
|
+
)
|
|
19
|
+
assert result == "https://telemetry-api.dev.dimo.zone/items/123"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_get_auth_headers():
|
|
23
|
+
client = DIMO(env="Dev")
|
|
24
|
+
headers = client._get_auth_headers("token123")
|
|
25
|
+
assert headers == {
|
|
26
|
+
"Authorization": "Bearer token123",
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_query_calls_request_with_correct_payload(monkeypatch):
|
|
32
|
+
client = DIMO(env="Dev")
|
|
33
|
+
# Create a fake request method on the client
|
|
34
|
+
fake_request = MagicMock(return_value={"data": {"result": True}})
|
|
35
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
36
|
+
|
|
37
|
+
query_str = "query { test }"
|
|
38
|
+
variables = {"key": "value"}
|
|
39
|
+
result = client.query("Trips", query_str, variables=variables, token="mocked_token")
|
|
40
|
+
|
|
41
|
+
# Verify the fake request was invoked once
|
|
42
|
+
fake_request.assert_called_once()
|
|
43
|
+
# Inspect call arguments
|
|
44
|
+
args, kwargs = fake_request.call_args
|
|
45
|
+
assert args[0] == "POST"
|
|
46
|
+
assert args[1] == "Trips"
|
|
47
|
+
assert args[2] == ""
|
|
48
|
+
|
|
49
|
+
# Assert correct headers
|
|
50
|
+
headers = kwargs["headers"]
|
|
51
|
+
assert headers["Authorization"] == "Bearer mocked_token"
|
|
52
|
+
assert headers["Content-Type"] == "application/json"
|
|
53
|
+
assert headers["User-Agent"] == "dimo-python-sdk"
|
|
54
|
+
|
|
55
|
+
# Check payload data
|
|
56
|
+
data = kwargs["data"]
|
|
57
|
+
assert data["query"] == query_str
|
|
58
|
+
assert data["variables"] == variables
|
|
59
|
+
|
|
60
|
+
assert result == {"data": {"result": True}}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from dimo.errors import DimoTypeError, check_type, check_optional_type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_check_type_passes_for_correct_type():
|
|
6
|
+
# call check_type with valid args which should not raise anything
|
|
7
|
+
check_type("count", 5, int)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_check_type_raises_for_incorrect_type():
|
|
11
|
+
with pytest.raises(DimoTypeError) as exc:
|
|
12
|
+
check_type("name", 123, str)
|
|
13
|
+
err = exc.value
|
|
14
|
+
assert err.param_name == "name"
|
|
15
|
+
assert err.expected_type is str
|
|
16
|
+
assert isinstance(err.actual_value, int)
|
|
17
|
+
|
|
18
|
+
assert "name must be a str" in str(err)
|
|
19
|
+
assert "but was entered as type int" in str(err)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_check_optional_type_allows_none():
|
|
23
|
+
# None is allowed
|
|
24
|
+
check_optional_type("maybe", None, dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_check_optional_type_raises_for_wrong_non_none():
|
|
28
|
+
with pytest.raises(DimoTypeError) as exc:
|
|
29
|
+
check_optional_type("maybe", 3.14, str)
|
|
30
|
+
err = exc.value
|
|
31
|
+
assert err.param_name == "maybe"
|
|
32
|
+
assert err.expected_type is str
|
|
33
|
+
assert isinstance(err.actual_value, float)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dimo.permission_decoder import PermissionDecoder
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_dimo_client_hex_to_permissions():
|
|
5
|
+
permissions_hex = "0x3ffc"
|
|
6
|
+
permissions_list = PermissionDecoder.decode_permission_bits(permissions_hex)
|
|
7
|
+
assert permissions_list == [1, 2, 3, 4, 5, 6]
|
|
8
|
+
|
|
9
|
+
one_to_five_hex = "0xffc"
|
|
10
|
+
assert PermissionDecoder.decode_permission_bits(one_to_five_hex) == [1, 2, 3, 4, 5]
|
|
11
|
+
|
|
12
|
+
another_hex = "0x3fcc"
|
|
13
|
+
assert PermissionDecoder.decode_permission_bits(another_hex) == [1, 3, 4, 5, 6]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pytest
|
|
3
|
+
from requests import RequestException
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
from dimo.request import Request, HTTPError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DummyResponse:
|
|
10
|
+
def __init__(
|
|
11
|
+
self, status_code=200, headers=None, json_data=None, content=b"", txt=""
|
|
12
|
+
):
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.headers = headers or {}
|
|
15
|
+
self._json_data = json_data
|
|
16
|
+
self.content = content
|
|
17
|
+
self.txt = txt
|
|
18
|
+
# .raise_for_status will be set per-test via side_effect
|
|
19
|
+
|
|
20
|
+
def json(self):
|
|
21
|
+
if isinstance(self._json_data, Exception):
|
|
22
|
+
raise self._json_data
|
|
23
|
+
return self._json_data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def session():
|
|
28
|
+
return Mock()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def make_request(session, **kwargs):
|
|
32
|
+
req = Request("POST", "https://api.example.com/endpoint", session)
|
|
33
|
+
return req(**kwargs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_json_request_body_is_serialized_and_passed_through(session):
|
|
37
|
+
data = {"foo": "bar"}
|
|
38
|
+
headers = {"Content-Type": "application/json"}
|
|
39
|
+
resp = DummyResponse(
|
|
40
|
+
status_code=200,
|
|
41
|
+
headers={"Content-Type": "application/json"},
|
|
42
|
+
json_data={"ok": True},
|
|
43
|
+
)
|
|
44
|
+
resp.raise_for_status = Mock()
|
|
45
|
+
session.request.return_value = resp
|
|
46
|
+
|
|
47
|
+
result = make_request(session, headers=headers, data=data)
|
|
48
|
+
|
|
49
|
+
# ensure data was JSON-dumped
|
|
50
|
+
_, call_kwargs = session.request.call_args
|
|
51
|
+
assert call_kwargs["data"] == json.dumps(data)
|
|
52
|
+
|
|
53
|
+
# ensure we got the parsed JSON back
|
|
54
|
+
assert result == {"ok": True}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_non_json_response_returns_raw_content(session):
|
|
58
|
+
resp = DummyResponse(
|
|
59
|
+
status_code=200,
|
|
60
|
+
headers={"Content-Type": "text/plain"},
|
|
61
|
+
content=b"hello world",
|
|
62
|
+
)
|
|
63
|
+
resp.raise_for_status = Mock()
|
|
64
|
+
session.request.return_value = resp
|
|
65
|
+
|
|
66
|
+
result = make_request(session)
|
|
67
|
+
assert result == b"hello world"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_http_error_wraps_json_body(session):
|
|
71
|
+
# prepare a RequestException with a response whose .json() returns a dict
|
|
72
|
+
err_resp = DummyResponse(
|
|
73
|
+
status_code=400,
|
|
74
|
+
json_data={"error": "Bad things"},
|
|
75
|
+
txt="Bad things text",
|
|
76
|
+
)
|
|
77
|
+
exc = RequestException("Bad Request")
|
|
78
|
+
exc.response = err_resp
|
|
79
|
+
|
|
80
|
+
# have raise_for_status raise that exception
|
|
81
|
+
good_resp = DummyResponse()
|
|
82
|
+
good_resp.raise_for_status = Mock(side_effect=exc)
|
|
83
|
+
session.request.return_value = good_resp
|
|
84
|
+
|
|
85
|
+
with pytest.raises(HTTPError) as ei:
|
|
86
|
+
make_request(session)
|
|
87
|
+
err = ei.value
|
|
88
|
+
assert err.status == 400
|
|
89
|
+
assert err.body == {"error": "Bad things"}
|
|
90
|
+
assert "Bad Request" in str(err)
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
from .api.attestation import Attestation
|
|
2
|
-
from .api.auth import Auth
|
|
3
|
-
from .api.device_definitions import DeviceDefinitions
|
|
4
|
-
from .api.token_exchange import TokenExchange
|
|
5
|
-
from .api.trips import Trips
|
|
6
|
-
from .api.valuations import Valuations
|
|
7
|
-
|
|
8
|
-
from .graphql.identity import Identity
|
|
9
|
-
from .graphql.telemetry import Telemetry
|
|
10
|
-
|
|
11
|
-
from .request import Request
|
|
12
|
-
from .environments import dimo_environment
|
|
13
|
-
import re
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class DIMO:
|
|
17
|
-
|
|
18
|
-
def __init__(self, env="Production"):
|
|
19
|
-
self.env = env
|
|
20
|
-
self.urls = dimo_environment[env]
|
|
21
|
-
self._client_id = None
|
|
22
|
-
self.attestation = Attestation(self.request, self._get_auth_headers)
|
|
23
|
-
self.auth = Auth(self.request, self._get_auth_headers, self.env, self)
|
|
24
|
-
self.device_definitions = DeviceDefinitions(
|
|
25
|
-
self.request, self._get_auth_headers
|
|
26
|
-
)
|
|
27
|
-
self.identity = Identity(self)
|
|
28
|
-
self.token_exchange = TokenExchange(
|
|
29
|
-
self.request, self._get_auth_headers, self.identity, self
|
|
30
|
-
)
|
|
31
|
-
self.trips = Trips(self.request, self._get_auth_headers)
|
|
32
|
-
self.valuations = Valuations(self.request, self._get_auth_headers)
|
|
33
|
-
self.telemetry = Telemetry(self)
|
|
34
|
-
self._session = Request.session
|
|
35
|
-
|
|
36
|
-
# Creates a full path for endpoints combining DIMO service, specific endpoint, and optional params
|
|
37
|
-
def _get_full_path(self, service, path, params=None):
|
|
38
|
-
base_path = self.urls[service]
|
|
39
|
-
full_path = f"{base_path}{path}"
|
|
40
|
-
|
|
41
|
-
if params:
|
|
42
|
-
for key, value in params.items():
|
|
43
|
-
pattern = f":{key}"
|
|
44
|
-
full_path = re.sub(pattern, str(value), full_path)
|
|
45
|
-
return full_path
|
|
46
|
-
|
|
47
|
-
# Sets headers based on access_token or privileged_token
|
|
48
|
-
def _get_auth_headers(self, token):
|
|
49
|
-
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
50
|
-
|
|
51
|
-
# request method for HTTP requests for the REST API
|
|
52
|
-
def request(self, http_method, service, path, **kwargs):
|
|
53
|
-
full_path = self._get_full_path(service, path)
|
|
54
|
-
return Request(http_method, full_path)(**kwargs)
|
|
55
|
-
|
|
56
|
-
# query method for graphQL queries, identity and telemetry
|
|
57
|
-
def query(self, service, query, variables=None, token=None):
|
|
58
|
-
headers = self._get_auth_headers(token) if token else {}
|
|
59
|
-
headers["Content-Type"] = "application/json"
|
|
60
|
-
headers["User-Agent"] = "dimo-python-sdk"
|
|
61
|
-
|
|
62
|
-
data = {"query": query, "variables": variables or {}}
|
|
63
|
-
|
|
64
|
-
response = self.request("POST", service, "", headers=headers, data=data)
|
|
65
|
-
return response
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import requests
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Request:
|
|
6
|
-
|
|
7
|
-
session = requests.Session()
|
|
8
|
-
|
|
9
|
-
def __init__(self, http_method, url):
|
|
10
|
-
self.http_method = http_method
|
|
11
|
-
self.url = url
|
|
12
|
-
|
|
13
|
-
def __call__(self, headers=None, data=None, params=None, **kwargs):
|
|
14
|
-
headers = headers or {}
|
|
15
|
-
headers.update(kwargs.pop("headers", {}))
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
data
|
|
19
|
-
and isinstance(data, dict)
|
|
20
|
-
and headers.get("Content-Type") == "application/json"
|
|
21
|
-
):
|
|
22
|
-
data = json.dumps(data)
|
|
23
|
-
|
|
24
|
-
response = self.session.request(
|
|
25
|
-
method=self.http_method,
|
|
26
|
-
url=self.url,
|
|
27
|
-
headers=headers,
|
|
28
|
-
params=params,
|
|
29
|
-
data=data,
|
|
30
|
-
**kwargs,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
# TODO: Better error responses
|
|
34
|
-
response.raise_for_status()
|
|
35
|
-
|
|
36
|
-
if response.content:
|
|
37
|
-
return response.json()
|
|
38
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dimo_python_sdk-1.3.2 → dimo_python_sdk-1.3.4}/dimo_python_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|