clevertouch 0.4.0__tar.gz → 0.4.2b2__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.
Files changed (23) hide show
  1. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/PKG-INFO +1 -1
  2. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/pyproject.toml +1 -1
  3. clevertouch-0.4.2b2/src/clevertouch/api.py +257 -0
  4. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/devices/radiator.py +12 -19
  5. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/info.py +1 -1
  6. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/objects.py +4 -5
  7. clevertouch-0.4.0/src/clevertouch/api.py +0 -223
  8. clevertouch-0.4.0/tests/.pytest_cache/.gitignore +0 -2
  9. clevertouch-0.4.0/tests/.pytest_cache/CACHEDIR.TAG +0 -4
  10. clevertouch-0.4.0/tests/.pytest_cache/README.md +0 -8
  11. clevertouch-0.4.0/tests/.pytest_cache/v/cache/lastfailed +0 -4
  12. clevertouch-0.4.0/tests/.pytest_cache/v/cache/nodeids +0 -5
  13. clevertouch-0.4.0/tests/.pytest_cache/v/cache/stepwise +0 -1
  14. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/LICENSE +0 -0
  15. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/README.md +0 -0
  16. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/__init__.py +0 -0
  17. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/devices/__init__.py +0 -0
  18. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/devices/const.py +0 -0
  19. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/devices/device.py +0 -0
  20. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/devices/factory.py +0 -0
  21. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/devices/onoff.py +0 -0
  22. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/src/clevertouch/util.py +0 -0
  23. {clevertouch-0.4.0 → clevertouch-0.4.2b2}/tests/authenticate_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: clevertouch
3
- Version: 0.4.0
3
+ Version: 0.4.2b2
4
4
  Summary: A Python API for interacting with LVI/Purmo Touch E3-connected radiators
5
5
  License: MIT
6
6
  Keywords: LVI,home automation,heating
@@ -13,7 +13,7 @@ log_cli_level = "DEBUG"
13
13
 
14
14
  [project]
15
15
  name = "clevertouch"
16
- version = "0.4.0"
16
+ version = "0.4.2b2"
17
17
  description = "A Python API for interacting with LVI/Purmo Touch E3-connected radiators"
18
18
  keywords = [
19
19
  "LVI",
@@ -0,0 +1,257 @@
1
+ """Interface to the cloud API."""
2
+
3
+ from __future__ import annotations
4
+ import logging
5
+ from typing import Optional, NamedTuple, Any
6
+ from hashlib import md5
7
+ from aiohttp import ClientSession, ClientConnectionError, ClientError
8
+ import time
9
+
10
+ from .util import ApiError
11
+
12
+ _LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class ApiStatus(NamedTuple):
16
+ """Status of an API call."""
17
+
18
+ code: int
19
+ key: str
20
+ value: str
21
+
22
+ def __str__(self) -> str:
23
+ return f"{self.key}({self.code}): {self.value}"
24
+
25
+ def __repr__(self) -> str:
26
+ return f"code={self.code}, key={self.key}, value={self.value}"
27
+
28
+
29
+ class ApiResult(NamedTuple):
30
+ """Result of an API call."""
31
+
32
+ status: ApiStatus
33
+ data: dict[str, Any]
34
+ parameters: dict[str, Any]
35
+
36
+
37
+ class ApiCallError(ApiError):
38
+ """Exception raised on errors when calling the cloud API."""
39
+
40
+ def __init__(self, status: ApiStatus, *args):
41
+ super().__init__(*args)
42
+ self.status: ApiStatus = status
43
+
44
+
45
+ class ApiAuthError(ApiError):
46
+ """Exception raised on authentication errors when calling the cloud API."""
47
+
48
+
49
+ class ApiConnectError(ApiError):
50
+ """Exception raised on connection errors when calling the cloud API."""
51
+
52
+
53
+ class ApiSession:
54
+ """Interface to the cloud API."""
55
+
56
+ API_LANG = "en_GB"
57
+ API_PATH = "/api/v0.1/"
58
+ CLIENT_ID = "app-front"
59
+
60
+ _DEFAULT_HOST = "e3.lvi.eu"
61
+ _DEFAULT_MANUFACTURER = "purmo"
62
+
63
+ def __init__(
64
+ self,
65
+ email: Optional[str] = None,
66
+ refresh_token: Optional[str] = None,
67
+ *,
68
+ host: Optional[str] = None,
69
+ manufacturer: Optional[str] = None,
70
+ ) -> None:
71
+ host = host or self._DEFAULT_HOST
72
+ manufacturer = manufacturer or self._DEFAULT_MANUFACTURER
73
+ self._token_url = (
74
+ f"https://auth.{host}/realms/{manufacturer}/protocol/openid-connect/token"
75
+ )
76
+ self._api_base = f"https://{host}{self.API_PATH}"
77
+ self._http_session: ClientSession = ClientSession()
78
+ self.email: Optional[str] = email
79
+ self.access_token: Optional[str] = None
80
+ self.refresh_token: Optional[str] = refresh_token
81
+ self.expires_at: float = 0.0
82
+
83
+ async def __aenter__(self) -> "ApiSession":
84
+ return self
85
+
86
+ async def __aexit__(self, *excinfo) -> None:
87
+ await self.close()
88
+
89
+ async def close(self):
90
+ """Close the connection to the cloud API."""
91
+ if self._http_session:
92
+ await self._http_session.close()
93
+ self._http_session = None
94
+
95
+ async def authenticate(self, email: str, password: str):
96
+ """Authenticate with the cloud API.
97
+ On success, sets self.access_token, self.refresh_token, self.expires_at.
98
+ """
99
+ self.email = email
100
+
101
+ data = {
102
+ "grant_type": "password",
103
+ "client_id": self.CLIENT_ID,
104
+ "username": email,
105
+ "password": password,
106
+ }
107
+
108
+ try:
109
+ _LOGGER.debug("Login to openid at '%s' with data '%s'", self._token_url, data)
110
+ async with self._http_session.post(self._token_url, data=data) as response:
111
+ if response.status != 200:
112
+ _LOGGER.error("OIDC login failed (%s)", response.status)
113
+ raise ApiAuthError("Invalid credentials or error fetching token.")
114
+ token_data = await response.json()
115
+ except ClientError as exc:
116
+ raise ApiConnectError(
117
+ f"Error connecting to {self._token_url}: {exc}"
118
+ ) from exc
119
+
120
+ self.access_token = token_data["access_token"]
121
+ self.refresh_token = token_data.get("refresh_token")
122
+ expires_in = token_data.get("expires_in", 3600) # default 1h
123
+ self.expires_at = time.time() + expires_in
124
+
125
+ async def refresh_openid(self) -> None:
126
+ """Refresh the existing token using the 'refresh_token' grant."""
127
+ if not self.refresh_token:
128
+ raise ApiAuthError("No refresh token stored; cannot refresh.")
129
+
130
+ data = {
131
+ "grant_type": "refresh_token",
132
+ "client_id": self.CLIENT_ID,
133
+ "refresh_token": self.refresh_token,
134
+ }
135
+ try:
136
+ async with self._http_session.post(self._token_url, data=data) as response:
137
+ if response.status != 200:
138
+ _LOGGER.error("OIDC refresh failed (%s)", response.status)
139
+ raise ApiAuthError("Refresh token invalid or expired.")
140
+ token_data = await response.json()
141
+ except ClientError as exc:
142
+ raise ApiConnectError(
143
+ f"Error connecting to {self._token_url}: {exc}"
144
+ ) from exc
145
+
146
+ self.access_token = token_data["access_token"]
147
+ self.refresh_token = token_data.get("refresh_token")
148
+ expires_in = token_data.get("expires_in", 3600)
149
+ self.expires_at = time.time() + expires_in
150
+
151
+ async def _ensure_valid_token(self) -> None:
152
+ """Refresh if our token is expired (simple check)."""
153
+ if time.time() >= self.expires_at:
154
+ await self.refresh_openid()
155
+
156
+ async def _read(
157
+ self,
158
+ endpoint: str,
159
+ payload: dict,
160
+ *,
161
+ throw_on_error: bool = True,
162
+ ) -> ApiResult:
163
+ result = await self._post(endpoint, payload)
164
+ if throw_on_error:
165
+ if result.status.code not in [1, 8]:
166
+ raise ApiCallError(result.status, f"Read failed with {result.status}")
167
+ return result
168
+
169
+ async def _write(
170
+ self,
171
+ endpoint: str,
172
+ payload: dict,
173
+ *,
174
+ throw_on_error: bool = True,
175
+ ) -> ApiResult:
176
+ result = await self._post(endpoint, payload)
177
+ if throw_on_error:
178
+ if result.status.code != 8:
179
+ raise ApiCallError(result.status, f"Write failed with {result.status}")
180
+ return result
181
+
182
+ async def read_user_data(self) -> dict[str, Any]:
183
+ """Read account user data from the cloud API."""
184
+ endpoint = "human/user/read/"
185
+ payload = {
186
+ "email": self.email,
187
+ }
188
+ result = await self._read(endpoint, payload)
189
+ return result.data
190
+
191
+ async def read_home_data(self, home_id: str) -> dict[str, Any]:
192
+ """Read home data from the cloud API."""
193
+ endpoint = "human/smarthome/read/"
194
+ payload = {
195
+ "smarthome_id": home_id,
196
+ }
197
+ result = await self._read(endpoint, payload)
198
+ return result.data
199
+
200
+ async def write_query(self, home_id: str, query_params: dict[str, Any]):
201
+ """Write an update query to the cloud API."""
202
+ endpoint = "human/query/push/"
203
+ payload = {
204
+ "smarthome_id": home_id,
205
+ "context": 1,
206
+ "peremption": 15000,
207
+ }
208
+ for key, value in query_params.items():
209
+ payload[f"query[{key}]"] = value
210
+
211
+ return await self._write(endpoint, payload)
212
+
213
+ def _parse_api_json(self, json: dict) -> ApiResult:
214
+ try:
215
+ code = json["code"]
216
+ data = json["data"]
217
+ parameters = json.get("parameters")
218
+ except Exception as ex:
219
+ raise ApiError("Unexpected JSON format in API response") from ex
220
+
221
+ try:
222
+ code_num = int(code["code"])
223
+ code_key = code["key"]
224
+ code_value = code["value"]
225
+ status = ApiStatus(code_num, code_key, code_value)
226
+ except Exception as ex:
227
+ raise ApiError("API status is malformed") from ex
228
+
229
+ return ApiResult(status, data, parameters)
230
+
231
+ async def _post(self, endpoint: str, payload: dict | None = None) -> dict:
232
+ """Make an authenticated API request"""
233
+ await self._ensure_valid_token()
234
+ headers = {
235
+ "Authorization": f"Bearer {self.access_token}",
236
+ }
237
+
238
+ url = f"{self._api_base}{endpoint.lstrip('/')}"
239
+
240
+ try:
241
+ async with self._http_session.post(
242
+ url, data=payload or {}, headers=headers
243
+ ) as resp:
244
+ resp.raise_for_status()
245
+ json_data = await resp.json()
246
+ except ClientError as exc:
247
+ _LOGGER.exception("API request failed: %s", exc)
248
+ raise ApiConnectError(f"API request failed: {exc}") from exc
249
+
250
+ try:
251
+ result = self._parse_api_json(json_data)
252
+ _LOGGER.debug("%s called with status %s", endpoint, result)
253
+ except Exception as ex:
254
+ _LOGGER.exception("Could not parse JSON result from API request")
255
+ result = None
256
+
257
+ return result
@@ -147,25 +147,18 @@ class Radiator(Device):
147
147
  )
148
148
  # Read boost settings
149
149
  # 'boost_time' is the user writable boost time
150
- try:
151
- self.boost_time = int(data["time_boost"])
152
- except KeyError: # The key doesn't exist
153
- self.boost_time = 0
154
- except ValueError: # The value can not be converted to an int
155
- self.boost_time = 0
150
+ self.boost_time = int(data.get("time_boost") or 0)
156
151
  # 'time_boost_format_chrono' holds remaining boost time with higher resolution
157
- try:
158
- node = data["time_boost_format_chrono"]
152
+ node = data.get("time_boost_format_chrono")
153
+ if node:
159
154
  self.boost_remaining = (
160
- int(node["d"]) * 24 * 60 * 60
161
- + int(node["h"]) * 60 * 60
162
- + int(node["m"]) * 60
163
- + int(node["s"])
155
+ int(node.get("d") or 0) * 24 * 60 * 60
156
+ + int(node.get("h") or 0) * 60 * 60
157
+ + int(node.get("m") or 0) * 60
158
+ + int(node.get("s") or 0)
164
159
  )
165
- except KeyError:
166
- self.boost_remaining = 0
167
- except ValueError:
168
- self.boost_remaining = 0
160
+ else:
161
+ self.boost_remaining = None
169
162
 
170
163
  async def set_temperature(self, temp_type: str, temp_value: float, unit: str):
171
164
  """Set a specific temperature for a radiator"""
@@ -252,8 +245,8 @@ class Radiator(Device):
252
245
 
253
246
  if heat_mode not in self._HEAT_MODE_TO_DEVICE:
254
247
  raise ApiError(f"Heating mode {heat_mode} not available.")
255
- if (temp_value or temp_unit) and (not temp_value or not temp_unit):
256
- raise ApiError("Both temp value and unit must be set.")
248
+ if temp_value and not temp_unit:
249
+ raise ApiError("Unit must be set when a temp value is provided.")
257
250
  if temp_value and heat_mode not in self._HEAT_MODE_TO_WRITABLE_TEMP_TYPE:
258
251
  raise ApiError(f"Temperature can not be set for {heat_mode}.")
259
252
  if boost_time and heat_mode != HeatMode.BOOST:
@@ -264,7 +257,7 @@ class Radiator(Device):
264
257
  query_params["gv_mode"] = self._HEAT_MODE_TO_DEVICE[heat_mode]
265
258
  query_params["nv_mode"] = self._HEAT_MODE_TO_DEVICE[heat_mode]
266
259
 
267
- if temp_value:
260
+ if temp_value and temp_unit:
268
261
  temp_type = self._HEAT_MODE_TO_WRITABLE_TEMP_TYPE[heat_mode]
269
262
  new_temp = Temperature(
270
263
  temp_value, temp_unit, is_writable=True, name=temp_type
@@ -26,7 +26,7 @@ class HomeInfo:
26
26
  self.zones = {
27
27
  zone.id_local: zone
28
28
  for zone in [
29
- ZoneInfo(zone_data) for zone_data in data["zones"].values()
29
+ ZoneInfo(zone_data) for zone_data in data["zones"]
30
30
  ]
31
31
  }
32
32
  else:
@@ -37,11 +37,10 @@ class Account:
37
37
  async def authenticate(
38
38
  self,
39
39
  email: str,
40
- password: Optional[str] = None,
41
- password_hash: Optional[str] = None,
40
+ password: Optional[str] = None
42
41
  ) -> None:
43
42
  """Authenticate with the cloud API and store credentials."""
44
- await self._api_session.authenticate(email, password, password_hash)
43
+ await self._api_session.authenticate(email, password)
45
44
  self.email = self._api_session.email
46
45
 
47
46
  async def get_user(self) -> User:
@@ -91,7 +90,7 @@ class Home:
91
90
  def _update(self, data):
92
91
  self.info.update(data)
93
92
 
94
- for device_data in data["devices"].values():
93
+ for device_data in data["devices"]:
95
94
  device_id = Device.get_id(device_data)
96
95
  device = self.devices.get(device_id)
97
96
  if device is None:
@@ -126,6 +125,6 @@ class User:
126
125
  self.homes = {
127
126
  home.home_id: home
128
127
  for home in [
129
- HomeInfo(data=home_data) for home_data in data["smarthomes"].values()
128
+ HomeInfo(data=home_data) for home_data in data["smarthomes"]
130
129
  ]
131
130
  }
@@ -1,223 +0,0 @@
1
- """Interface to the cloud API."""
2
- from __future__ import annotations
3
- import logging
4
- from typing import Optional, NamedTuple, Any
5
- from hashlib import md5
6
- from aiohttp import ClientSession, ClientConnectionError, ClientError
7
-
8
- from .util import ApiError
9
-
10
- _LOGGER = logging.getLogger(__name__)
11
-
12
-
13
- class ApiStatus(NamedTuple):
14
- """Status of an API call."""
15
-
16
- code: int
17
- key: str
18
- value: str
19
-
20
- def __str__(self) -> str:
21
- return f"{self.key}({self.code}): {self.value}"
22
-
23
- def __repr__(self) -> str:
24
- return f"code={self.code}, key={self.key}, value={self.value}"
25
-
26
-
27
- class ApiResult(NamedTuple):
28
- """Result of an API call."""
29
-
30
- status: ApiStatus
31
- data: dict[str, Any]
32
- parameters: dict[str, Any]
33
-
34
-
35
- class ApiCallError(ApiError):
36
- """Exception raised on errors when calling the cloud API."""
37
-
38
- def __init__(self, status: ApiStatus, *args):
39
- super().__init__(*args)
40
- self.status: ApiStatus = status
41
-
42
-
43
- class ApiAuthError(ApiError):
44
- """Exception raised on authentication errors when calling the cloud API."""
45
-
46
-
47
- class ApiConnectError(ApiError):
48
- """Exception raised on connection errors when calling the cloud API."""
49
-
50
-
51
- class ApiSession:
52
- """Interface to the cloud API."""
53
-
54
- API_LANG = "en_GB"
55
- _LVI_API_BASE = "https://e3.lvi.eu"
56
- API_PATH = "/api/v0.1/"
57
-
58
- def __init__(
59
- self,
60
- email: Optional[str] = None,
61
- token: Optional[str] = None,
62
- *,
63
- host: Optional[str] = None
64
- ) -> None:
65
- api_base = host or self._LVI_API_BASE
66
- self._http_session: ClientSession = ClientSession(api_base)
67
- self.email: Optional[str] = email
68
- self.token: Optional[str] = token
69
-
70
- async def __aenter__(self):
71
- return self
72
-
73
- async def __aexit__(self, *excinfo):
74
- await self.close()
75
-
76
- async def close(self):
77
- """Close the connection to the cloud API."""
78
- await self._http_session.close()
79
-
80
- async def authenticate(
81
- self,
82
- email: str,
83
- password: Optional[str] = None,
84
- password_hash: Optional[str] = None,
85
- ):
86
- """Authenticate with the cloud API."""
87
- self.email = email
88
- if password_hash is None or password_hash == "":
89
- if password is None:
90
- raise ApiError("No password provided")
91
- password_hash = md5(password.encode()).hexdigest()
92
-
93
- endpoint = "human/user/auth/"
94
- payload = {
95
- "email": email,
96
- "password": password_hash,
97
- "remember_me": "true",
98
- "lang": self.API_LANG,
99
- }
100
-
101
- result = await self._post(endpoint, payload, authenticate=False)
102
-
103
- status = result.status
104
- if status.key != "OK":
105
- if status.key == "ERR_ROUTE":
106
- raise ApiConnectError(status)
107
- if status.key == "ERR_PARAM":
108
- raise ApiAuthError("Invalid email or password")
109
- raise ApiCallError(status, "Authentication failed %s", status)
110
-
111
- self.email = result.data["user_infos"]["email"]
112
- self.token = result.data["token"]
113
-
114
- async def _read(
115
- self,
116
- endpoint: str,
117
- payload: dict,
118
- *,
119
- authenticate: bool = True,
120
- throw_on_error: bool = True,
121
- ) -> ApiResult:
122
- result = await self._post(endpoint, payload, authenticate=authenticate)
123
- if throw_on_error:
124
- if result.status.code != 1:
125
- raise ApiCallError(result.status, f"Read failed with {result.status}")
126
- return result
127
-
128
- async def _write(
129
- self,
130
- endpoint: str,
131
- payload: dict,
132
- *,
133
- authenticate: bool = True,
134
- throw_on_error: bool = True,
135
- ) -> ApiResult:
136
- result = await self._post(endpoint, payload, authenticate=authenticate)
137
- if throw_on_error:
138
- if result.status.code != 8:
139
- raise ApiCallError(result.status, f"Write failed with {result.status}")
140
- return result
141
-
142
- async def read_user_data(self) -> dict[str, Any]:
143
- """Read account user data from the cloud API."""
144
- endpoint = "human/user/read/"
145
- payload = {
146
- "email": self.email,
147
- }
148
- result = await self._read(endpoint, payload)
149
- return result.data
150
-
151
- async def read_home_data(self, home_id: str) -> dict[str, Any]:
152
- """Read home data from the cloud API."""
153
- endpoint = "human/smarthome/read/"
154
- payload = {
155
- "smarthome_id": home_id,
156
- }
157
- result = await self._read(endpoint, payload)
158
- return result.data
159
-
160
- async def write_query(self, home_id: str, query_params: dict[str, Any]):
161
- """Write an update query to the cloud API."""
162
- endpoint = "human/query/push/"
163
- payload = {
164
- "smarthome_id": home_id,
165
- "context": 1,
166
- "peremption": 15000,
167
- }
168
- for key, value in query_params.items():
169
- payload[f"query[{key}]"] = value
170
-
171
- return await self._write(endpoint, payload)
172
-
173
- def _parse_api_json(self, json: dict) -> ApiResult:
174
- try:
175
- code = json["code"]
176
- data = json["data"]
177
- parameters = json["parameters"]
178
- except Exception as ex:
179
- raise ApiError("Unexpected JSON format in API response") from ex
180
-
181
- try:
182
- code_num = int(code["code"])
183
- code_key = code["key"]
184
- code_value = code["value"]
185
- status = ApiStatus(code_num, code_key, code_value)
186
- except Exception as ex:
187
- raise ApiError("API status is malformed") from ex
188
-
189
- return ApiResult(status, data, parameters)
190
-
191
- async def _post(
192
- self, endpoint: str, payload: dict[str, Any], *, authenticate: bool
193
- ) -> ApiResult:
194
- if authenticate:
195
- payload.update(
196
- {
197
- "token": self.token,
198
- "lang": self.API_LANG,
199
- }
200
- )
201
-
202
- try:
203
- async with self._http_session.post(
204
- self.API_PATH + endpoint, data=payload
205
- ) as response:
206
- response.raise_for_status()
207
- json_data = await response.json()
208
- except ClientConnectionError as ex:
209
- raise ApiConnectError(f"Connection error - {ex}") from ex
210
- except ClientError as ex:
211
- raise ApiConnectError(f"Unexpected cloud API error - {ex}") from ex
212
-
213
- result = self._parse_api_json(json_data)
214
-
215
- _LOGGER.debug(
216
- "%s called with status %s (%s): %s",
217
- endpoint,
218
- result.status.key,
219
- result.status.code,
220
- result.status.value,
221
- )
222
-
223
- return result
@@ -1,2 +0,0 @@
1
- # Created by pytest automatically.
2
- *
@@ -1,4 +0,0 @@
1
- Signature: 8a477f597d28d172789f06886806bc55
2
- # This file is a cache directory tag created by pytest.
3
- # For information about cache directory tags, see:
4
- # https://bford.info/cachedir/spec.html
@@ -1,8 +0,0 @@
1
- # pytest cache directory #
2
-
3
- This directory contains data from the pytest's cache plugin,
4
- which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
-
6
- **Do not** commit this to version control.
7
-
8
- See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
@@ -1,4 +0,0 @@
1
- {
2
- "authenticate.py::test_authentication": true,
3
- "authenticate_test.py::test_authentication": true
4
- }
@@ -1,5 +0,0 @@
1
- [
2
- "authenticate.py::test_authentication",
3
- "authenticate_test.py::test_authentication",
4
- "authenticate_test.py::test_true"
5
- ]
File without changes
File without changes