pybluecurrent 0.0.4__tar.gz → 0.1.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.
Files changed (23) hide show
  1. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/PKG-INFO +29 -45
  2. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/README.md +26 -42
  3. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/pyproject.toml +3 -3
  4. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/_version.py +3 -3
  5. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/client.py +31 -38
  6. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/PKG-INFO +29 -45
  7. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/requires.txt +2 -2
  8. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/tests/test_client.py +24 -26
  9. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.github/workflows/publish.yaml +0 -0
  10. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.github/workflows/test.yaml +0 -0
  11. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.gitignore +0 -0
  12. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/.pre-commit-config.yaml +0 -0
  13. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/LICENSE +0 -0
  14. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/setup.cfg +0 -0
  15. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/__init__.py +0 -0
  16. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/exceptions.py +0 -0
  17. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/py.typed +0 -0
  18. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent/utilities.py +0 -0
  19. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/SOURCES.txt +0 -0
  20. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/dependency_links.txt +0 -0
  21. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/src/pybluecurrent.egg-info/top_level.txt +0 -0
  22. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/tests/conftest.py +0 -0
  23. {pybluecurrent-0.0.4 → pybluecurrent-0.1.0}/tests/test_utilities.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybluecurrent
3
- Version: 0.0.4
3
+ Version: 0.1.0
4
4
  Summary: Python client for BlueCurrent charge points.
5
5
  Author-email: Rogier van der Geer <rogier@vander-geer.nl>
6
6
  License: MIT
@@ -15,9 +15,9 @@ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: asyncio-multisubscriber-queue>=0.4.1
18
- Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: httpx>=0.28
19
19
  Requires-Dist: sjcl>=0.2.1
20
- Requires-Dist: websockets>=11.0.3
20
+ Requires-Dist: websockets>=14.0
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: black==23.3.0; extra == "dev"
23
23
  Requires-Dist: pre-commit>=3.3.3; extra == "dev"
@@ -43,36 +43,32 @@ from pybluecurrent import BlueCurrentClient
43
43
  client = BlueCurrentClient("your_username", "your_secret_password")
44
44
 
45
45
  async with client:
46
- charge_points = await client.get_charge_points()
47
-
48
- transactions = client.get_transactions(charge_points[0]["evse_id"])
46
+ charge_points = await client.get_charge_points()
47
+ transactions = await client.get_transactions(charge_points[0]["evse_id"])
49
48
  ```
50
49
 
51
50
  ## Methods
52
51
 
53
- BlueCurrent exposes two APIs, a synchronous REST API as well as an asynchronous Websocket API.
54
- As a result, the `BlueCurrentClient` also exposes synchronous as well as asynchronous methods:
52
+ The `BlueCurrentClient` exposes the following methods:
55
53
 
56
- - [Asynchronous](#asynchronous)
57
- - [`get_account`](#getaccount---get-your-account-information)
58
- - [`get_charge_cards`](#getchargecards---get-your-charge-cards)
59
- - [`get_charge_points`](#getchargepoints---get-your-charge-points)
60
- - [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
61
- - [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
62
- - [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
63
- - [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
64
- - [`set_status`](#setstatus---enable-or-disable-a-charge-point)
65
- - [Synchronous](#synchronous)
66
- - [`login`](#login---log-in)
67
- - [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
68
- - [`get_contracts`](#getcontracts---get-your-contracts)
69
- - [`get_grids`](#getgrids---get-your-grid-connections)
70
- - [`get_transactions`](#gettransactions---get-a-list-of-transactions)
71
- - [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
72
-
73
- ### Asynchronous
74
-
75
- The async methods can only be used when the websocket client is connected. For example:
54
+ - [`get_account`](#getaccount---get-your-account-information)
55
+ - [`get_charge_cards`](#getchargecards---get-your-charge-cards)
56
+ - [`get_charge_points`](#getchargepoints---get-your-charge-points)
57
+ - [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
58
+ - [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
59
+ - [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
60
+ - [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
61
+ - [`set_status`](#setstatus---enable-or-disable-a-charge-point)
62
+ - [`login`](#login---log-in)
63
+ - [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
64
+ - [`get_contracts`](#getcontracts---get-your-contracts)
65
+ - [`get_grids`](#getgrids---get-your-grid-connections)
66
+ - [`get_transactions`](#gettransactions---get-a-list-of-transactions)
67
+ - [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
68
+
69
+ ### Connection
70
+
71
+ The client can only be used when the websocket client is connected. For example:
76
72
  ```python
77
73
  client = BlueCurrentClient("your_username", "your_secret_password")
78
74
  async with client:
@@ -248,22 +244,10 @@ async def set_status(self, evse_id: str, enabled: bool) -> None
248
244
  - `evse_id`: The ID of the charge point.
249
245
  - `enabled`: Boolean that indicates the desired status.
250
246
 
251
- ### Synchronous
252
-
253
- #### `login` - Log in
254
- ```python
255
- def login(self) -> None
256
- ```
257
-
258
- This method does not do anything if the client is already logged in.
259
- Connection to the websocket api (async with client) automatically logs in the client,
260
- so this endpoint is not needed when using the async API.
261
-
262
-
263
247
  #### `get_charge_point_status` - Get the status of a charge point.
264
248
 
265
249
  ```python
266
- def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
250
+ async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
267
251
  ```
268
252
 
269
253
  ##### Arguments
@@ -296,7 +280,7 @@ A dictionary with the chargepoint status:
296
280
  #### `get_contracts` - Get your contracts.
297
281
 
298
282
  ```python
299
- def get_contracts(self) -> list[dict[str, str]]
283
+ async def get_contracts(self) -> list[dict[str, str]]
300
284
  ```
301
285
 
302
286
  ##### Returns
@@ -316,7 +300,7 @@ A list of dictionaries, each representing a contract:
316
300
  #### `get_grids` - Get your grid connections.
317
301
 
318
302
  ```python
319
- def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
303
+ async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
320
304
  ```
321
305
 
322
306
  ##### Returns
@@ -335,7 +319,7 @@ A list of dictionaries, each representing a grid:
335
319
  #### `get_transactions` - Get a list of transactions.
336
320
 
337
321
  ```python
338
- def get_transactions(
322
+ async def get_transactions(
339
323
  self, evse_id: str, newest_first: bool = True, page: int = 1
340
324
  ) -> dict[str, int | list[dict[str, Any]]]
341
325
  ```
@@ -377,7 +361,7 @@ A dictionary like this:
377
361
  #### `iterate_transactions` - Iterate through your transactions
378
362
 
379
363
  ```python
380
- def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> Iterable[dict[str, Any]]
364
+ async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]
381
365
  ```
382
366
 
383
367
  ##### Arguments
@@ -16,36 +16,32 @@ from pybluecurrent import BlueCurrentClient
16
16
  client = BlueCurrentClient("your_username", "your_secret_password")
17
17
 
18
18
  async with client:
19
- charge_points = await client.get_charge_points()
20
-
21
- transactions = client.get_transactions(charge_points[0]["evse_id"])
19
+ charge_points = await client.get_charge_points()
20
+ transactions = await client.get_transactions(charge_points[0]["evse_id"])
22
21
  ```
23
22
 
24
23
  ## Methods
25
24
 
26
- BlueCurrent exposes two APIs, a synchronous REST API as well as an asynchronous Websocket API.
27
- As a result, the `BlueCurrentClient` also exposes synchronous as well as asynchronous methods:
25
+ The `BlueCurrentClient` exposes the following methods:
28
26
 
29
- - [Asynchronous](#asynchronous)
30
- - [`get_account`](#getaccount---get-your-account-information)
31
- - [`get_charge_cards`](#getchargecards---get-your-charge-cards)
32
- - [`get_charge_points`](#getchargepoints---get-your-charge-points)
33
- - [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
34
- - [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
35
- - [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
36
- - [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
37
- - [`set_status`](#setstatus---enable-or-disable-a-charge-point)
38
- - [Synchronous](#synchronous)
39
- - [`login`](#login---log-in)
40
- - [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
41
- - [`get_contracts`](#getcontracts---get-your-contracts)
42
- - [`get_grids`](#getgrids---get-your-grid-connections)
43
- - [`get_transactions`](#gettransactions---get-a-list-of-transactions)
44
- - [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
45
-
46
- ### Asynchronous
47
-
48
- The async methods can only be used when the websocket client is connected. For example:
27
+ - [`get_account`](#getaccount---get-your-account-information)
28
+ - [`get_charge_cards`](#getchargecards---get-your-charge-cards)
29
+ - [`get_charge_points`](#getchargepoints---get-your-charge-points)
30
+ - [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
31
+ - [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
32
+ - [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
33
+ - [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
34
+ - [`set_status`](#setstatus---enable-or-disable-a-charge-point)
35
+ - [`login`](#login---log-in)
36
+ - [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
37
+ - [`get_contracts`](#getcontracts---get-your-contracts)
38
+ - [`get_grids`](#getgrids---get-your-grid-connections)
39
+ - [`get_transactions`](#gettransactions---get-a-list-of-transactions)
40
+ - [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
41
+
42
+ ### Connection
43
+
44
+ The client can only be used when the websocket client is connected. For example:
49
45
  ```python
50
46
  client = BlueCurrentClient("your_username", "your_secret_password")
51
47
  async with client:
@@ -221,22 +217,10 @@ async def set_status(self, evse_id: str, enabled: bool) -> None
221
217
  - `evse_id`: The ID of the charge point.
222
218
  - `enabled`: Boolean that indicates the desired status.
223
219
 
224
- ### Synchronous
225
-
226
- #### `login` - Log in
227
- ```python
228
- def login(self) -> None
229
- ```
230
-
231
- This method does not do anything if the client is already logged in.
232
- Connection to the websocket api (async with client) automatically logs in the client,
233
- so this endpoint is not needed when using the async API.
234
-
235
-
236
220
  #### `get_charge_point_status` - Get the status of a charge point.
237
221
 
238
222
  ```python
239
- def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
223
+ async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
240
224
  ```
241
225
 
242
226
  ##### Arguments
@@ -269,7 +253,7 @@ A dictionary with the chargepoint status:
269
253
  #### `get_contracts` - Get your contracts.
270
254
 
271
255
  ```python
272
- def get_contracts(self) -> list[dict[str, str]]
256
+ async def get_contracts(self) -> list[dict[str, str]]
273
257
  ```
274
258
 
275
259
  ##### Returns
@@ -289,7 +273,7 @@ A list of dictionaries, each representing a contract:
289
273
  #### `get_grids` - Get your grid connections.
290
274
 
291
275
  ```python
292
- def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
276
+ async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
293
277
  ```
294
278
 
295
279
  ##### Returns
@@ -308,7 +292,7 @@ A list of dictionaries, each representing a grid:
308
292
  #### `get_transactions` - Get a list of transactions.
309
293
 
310
294
  ```python
311
- def get_transactions(
295
+ async def get_transactions(
312
296
  self, evse_id: str, newest_first: bool = True, page: int = 1
313
297
  ) -> dict[str, int | list[dict[str, Any]]]
314
298
  ```
@@ -350,7 +334,7 @@ A dictionary like this:
350
334
  #### `iterate_transactions` - Iterate through your transactions
351
335
 
352
336
  ```python
353
- def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> Iterable[dict[str, Any]]
337
+ async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]
354
338
  ```
355
339
 
356
340
  ##### Arguments
@@ -21,13 +21,13 @@ classifiers = [
21
21
  "License :: OSI Approved :: MIT License",
22
22
  "Operating System :: OS Independent",
23
23
  "Programming Language :: Python :: 3",
24
- "Typing :: Typed"
24
+ "Typing :: Typed",
25
25
  ]
26
26
  dependencies = [
27
27
  "asyncio-multisubscriber-queue>=0.4.1",
28
- "requests>=2.31.0",
28
+ "httpx>=0.28",
29
29
  "sjcl>=0.2.1",
30
- "websockets>=11.0.3",
30
+ "websockets>=14.0",
31
31
  ]
32
32
  dynamic = ["version"]
33
33
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.4'
32
- __version_tuple__ = version_tuple = (0, 0, 4)
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
33
 
34
- __commit_id__ = commit_id = 'gdbea48bcd'
34
+ __commit_id__ = commit_id = 'g1fb6788fb'
@@ -1,14 +1,14 @@
1
- from asyncio import Task, create_task, run, wait_for
1
+ from asyncio import Task, create_task, wait_for
2
2
  from datetime import date, datetime
3
3
  from json import dumps, loads
4
4
  from logging import getLogger
5
- from typing import Any, Iterable
5
+ from typing import Any, AsyncIterable
6
6
  from uuid import uuid4
7
7
 
8
8
  from asyncio_multisubscriber_queue import MultisubscriberQueue
9
- from requests import get, post
9
+ from httpx import AsyncClient
10
10
  from sjcl import SJCL
11
- from websockets import WebSocketClientProtocol, connect
11
+ from websockets.asyncio.client import ClientConnection, connect
12
12
 
13
13
  from pybluecurrent._version import __version__
14
14
  from pybluecurrent.exceptions import AuthenticationFailed, BlueCurrentException
@@ -24,17 +24,18 @@ class BlueCurrentClient:
24
24
  self.consumer: Task | None = None
25
25
  self.credentials: tuple[str, str] = (username, password)
26
26
  self.logger = getLogger("BlueCurrentClient")
27
+ self.httpx_client: AsyncClient | None = None
27
28
  self.queue = MultisubscriberQueue()
28
- self.socket: WebSocketClientProtocol | None = None
29
+ self.socket: ClientConnection | None = None
29
30
  self.token: str | None = None
30
31
 
31
- ## Asynchronous API ##
32
-
33
32
  async def __aenter__(self) -> "BlueCurrentClient":
34
33
  self.logger.debug("Creating BlueCurrent websocket connection")
35
34
  self.connection = connect(self.socket_url, user_agent_header=self._user_agent)
36
35
  self.socket = await self.connection.__aenter__()
37
36
  self.consumer = create_task(self._handler())
37
+ self.httpx_client = AsyncClient()
38
+ await self.httpx_client.__aenter__()
38
39
  if self.token is None:
39
40
  await self._login()
40
41
  await self._hello()
@@ -44,7 +45,8 @@ class BlueCurrentClient:
44
45
  self.logger.debug("Closing BlueCurrent connection")
45
46
  self.consumer.cancel()
46
47
  await self.connection.__aexit__(exc_type, exc_val, exc_tb)
47
- self.consumer, self.socket = None, None
48
+ await self.httpx_client.__aexit__(exc_type, exc_val, exc_tb)
49
+ self.consumer, self.socket, self.httpx_client = None, None, None
48
50
 
49
51
  async def get_account(self) -> dict[str, bool | date | str]:
50
52
  """
@@ -263,25 +265,7 @@ class BlueCurrentClient:
263
265
  await self._receive("RECEIVED_SOFT_RESET")
264
266
  return await self._receive("STATUS_SOFT_RESET", timeout=30)
265
267
 
266
- ## Synchronous API ##
267
-
268
- def login(self) -> None:
269
- """
270
- Retrieves a connection token.
271
-
272
- This method does not do anything if the client is already logged in.
273
- Connection to the websocket api (async with client) automatically logs in the client,
274
- so this endpoint is not needed when using the async API.
275
- """
276
-
277
- async def _login(client: BlueCurrentClient) -> None:
278
- async with client:
279
- pass
280
-
281
- if self.token is None:
282
- run(_login(self))
283
-
284
- def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]:
268
+ async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]:
285
269
  """
286
270
  Get the status of a charge point.
287
271
 
@@ -307,7 +291,9 @@ class BlueCurrentClient:
307
291
  "evse_id": "BCU123456",
308
292
  }
309
293
  """
310
- response = get(
294
+ if self.httpx_client is None:
295
+ raise RuntimeError(f"{self.__class__.__name__} is not connected.")
296
+ response = await self.httpx_client.get(
311
297
  f"{self.api_url}/chargepointstatus?evse_id={evse_id}",
312
298
  headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
313
299
  )
@@ -321,7 +307,7 @@ class BlueCurrentClient:
321
307
  },
322
308
  )
323
309
 
324
- def get_contracts(self) -> list[dict[str, str]]:
310
+ async def get_contracts(self) -> list[dict[str, str]]:
325
311
  """
326
312
  Get your contracts.
327
313
 
@@ -337,14 +323,16 @@ class BlueCurrentClient:
337
323
  }
338
324
  ]
339
325
  """
340
- response = get(
326
+ if self.httpx_client is None:
327
+ raise RuntimeError(f"{self.__class__.__name__} is not connected.")
328
+ response = await self.httpx_client.get(
341
329
  f"{self.api_url}/getcontracts",
342
330
  headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
343
331
  )
344
332
  response.raise_for_status()
345
333
  return response.json()["contracts"]
346
334
 
347
- def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]:
335
+ async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]:
348
336
  """
349
337
  Get your grid connections.
350
338
 
@@ -359,14 +347,16 @@ class BlueCurrentClient:
359
347
  }
360
348
  ]
361
349
  """
362
- response = get(
350
+ if self.httpx_client is None:
351
+ raise RuntimeError(f"{self.__class__.__name__} is not connected.")
352
+ response = await self.httpx_client.get(
363
353
  f"{self.api_url}/getgrids",
364
354
  headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
365
355
  )
366
356
  response.raise_for_status()
367
357
  return response.json()["grids"]
368
358
 
369
- def get_transactions(
359
+ async def get_transactions(
370
360
  self, evse_id: str, newest_first: bool = True, page: int = 1
371
361
  ) -> dict[str, int | list[dict[str, Any]]]:
372
362
  """
@@ -405,13 +395,15 @@ class BlueCurrentClient:
405
395
  }
406
396
 
407
397
  """
408
- response = post(
398
+ if self.httpx_client is None:
399
+ raise RuntimeError(f"{self.__class__.__name__} is not connected.")
400
+ response = await self.httpx_client.post(
409
401
  f"{self.api_url}/gettransactions?"
410
402
  f"page={page}&"
411
403
  f"sort_field_order={'DESC' if newest_first else 'ASC'}&"
412
404
  f"sort_field=stoppedtimestamp",
413
405
  headers={"Authorization": f"Token {self.token}", "User-Agent": self._user_agent},
414
- data=dumps({"chargepoints": [{"chargepoint_id": evse_id}]}).encode("UTF-8"),
406
+ data=dumps({"chargepoints": [{"chargepoint_id": evse_id}]}),
415
407
  )
416
408
  response.raise_for_status()
417
409
  result = response.json()["data"]
@@ -421,7 +413,7 @@ class BlueCurrentClient:
421
413
  )
422
414
  return result
423
415
 
424
- def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> Iterable[dict[str, Any]]:
416
+ async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]:
425
417
  """
426
418
  Iterate through your transactions.
427
419
 
@@ -450,8 +442,9 @@ class BlueCurrentClient:
450
442
  """
451
443
  next_page = 1
452
444
  while next_page is not None:
453
- transactions = self.get_transactions(evse_id=evse_id, newest_first=newest_first, page=next_page)
454
- yield from transactions["transactions"] # type: ignore
445
+ transactions = await self.get_transactions(evse_id=evse_id, newest_first=newest_first, page=next_page)
446
+ for tx in transactions["transactions"]: # type: ignore
447
+ yield tx
455
448
  next_page = transactions["next_page"] # type: ignore
456
449
 
457
450
  async def _login(self) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybluecurrent
3
- Version: 0.0.4
3
+ Version: 0.1.0
4
4
  Summary: Python client for BlueCurrent charge points.
5
5
  Author-email: Rogier van der Geer <rogier@vander-geer.nl>
6
6
  License: MIT
@@ -15,9 +15,9 @@ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: asyncio-multisubscriber-queue>=0.4.1
18
- Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: httpx>=0.28
19
19
  Requires-Dist: sjcl>=0.2.1
20
- Requires-Dist: websockets>=11.0.3
20
+ Requires-Dist: websockets>=14.0
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: black==23.3.0; extra == "dev"
23
23
  Requires-Dist: pre-commit>=3.3.3; extra == "dev"
@@ -43,36 +43,32 @@ from pybluecurrent import BlueCurrentClient
43
43
  client = BlueCurrentClient("your_username", "your_secret_password")
44
44
 
45
45
  async with client:
46
- charge_points = await client.get_charge_points()
47
-
48
- transactions = client.get_transactions(charge_points[0]["evse_id"])
46
+ charge_points = await client.get_charge_points()
47
+ transactions = await client.get_transactions(charge_points[0]["evse_id"])
49
48
  ```
50
49
 
51
50
  ## Methods
52
51
 
53
- BlueCurrent exposes two APIs, a synchronous REST API as well as an asynchronous Websocket API.
54
- As a result, the `BlueCurrentClient` also exposes synchronous as well as asynchronous methods:
52
+ The `BlueCurrentClient` exposes the following methods:
55
53
 
56
- - [Asynchronous](#asynchronous)
57
- - [`get_account`](#getaccount---get-your-account-information)
58
- - [`get_charge_cards`](#getchargecards---get-your-charge-cards)
59
- - [`get_charge_points`](#getchargepoints---get-your-charge-points)
60
- - [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
61
- - [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
62
- - [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
63
- - [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
64
- - [`set_status`](#setstatus---enable-or-disable-a-charge-point)
65
- - [Synchronous](#synchronous)
66
- - [`login`](#login---log-in)
67
- - [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
68
- - [`get_contracts`](#getcontracts---get-your-contracts)
69
- - [`get_grids`](#getgrids---get-your-grid-connections)
70
- - [`get_transactions`](#gettransactions---get-a-list-of-transactions)
71
- - [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
72
-
73
- ### Asynchronous
74
-
75
- The async methods can only be used when the websocket client is connected. For example:
54
+ - [`get_account`](#getaccount---get-your-account-information)
55
+ - [`get_charge_cards`](#getchargecards---get-your-charge-cards)
56
+ - [`get_charge_points`](#getchargepoints---get-your-charge-points)
57
+ - [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
58
+ - [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
59
+ - [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
60
+ - [`set_plug_and_charge_charge_card`](#setplugandchargechargecard---set-the-charge-card-for-plug-and-charge)
61
+ - [`set_status`](#setstatus---enable-or-disable-a-charge-point)
62
+ - [`login`](#login---log-in)
63
+ - [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
64
+ - [`get_contracts`](#getcontracts---get-your-contracts)
65
+ - [`get_grids`](#getgrids---get-your-grid-connections)
66
+ - [`get_transactions`](#gettransactions---get-a-list-of-transactions)
67
+ - [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
68
+
69
+ ### Connection
70
+
71
+ The client can only be used when the websocket client is connected. For example:
76
72
  ```python
77
73
  client = BlueCurrentClient("your_username", "your_secret_password")
78
74
  async with client:
@@ -248,22 +244,10 @@ async def set_status(self, evse_id: str, enabled: bool) -> None
248
244
  - `evse_id`: The ID of the charge point.
249
245
  - `enabled`: Boolean that indicates the desired status.
250
246
 
251
- ### Synchronous
252
-
253
- #### `login` - Log in
254
- ```python
255
- def login(self) -> None
256
- ```
257
-
258
- This method does not do anything if the client is already logged in.
259
- Connection to the websocket api (async with client) automatically logs in the client,
260
- so this endpoint is not needed when using the async API.
261
-
262
-
263
247
  #### `get_charge_point_status` - Get the status of a charge point.
264
248
 
265
249
  ```python
266
- def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
250
+ async def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
267
251
  ```
268
252
 
269
253
  ##### Arguments
@@ -296,7 +280,7 @@ A dictionary with the chargepoint status:
296
280
  #### `get_contracts` - Get your contracts.
297
281
 
298
282
  ```python
299
- def get_contracts(self) -> list[dict[str, str]]
283
+ async def get_contracts(self) -> list[dict[str, str]]
300
284
  ```
301
285
 
302
286
  ##### Returns
@@ -316,7 +300,7 @@ A list of dictionaries, each representing a contract:
316
300
  #### `get_grids` - Get your grid connections.
317
301
 
318
302
  ```python
319
- def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
303
+ async def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
320
304
  ```
321
305
 
322
306
  ##### Returns
@@ -335,7 +319,7 @@ A list of dictionaries, each representing a grid:
335
319
  #### `get_transactions` - Get a list of transactions.
336
320
 
337
321
  ```python
338
- def get_transactions(
322
+ async def get_transactions(
339
323
  self, evse_id: str, newest_first: bool = True, page: int = 1
340
324
  ) -> dict[str, int | list[dict[str, Any]]]
341
325
  ```
@@ -377,7 +361,7 @@ A dictionary like this:
377
361
  #### `iterate_transactions` - Iterate through your transactions
378
362
 
379
363
  ```python
380
- def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> Iterable[dict[str, Any]]
364
+ async def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> AsyncIterable[dict[str, Any]]
381
365
  ```
382
366
 
383
367
  ##### Arguments
@@ -1,7 +1,7 @@
1
1
  asyncio-multisubscriber-queue>=0.4.1
2
- requests>=2.31.0
2
+ httpx>=0.28
3
3
  sjcl>=0.2.1
4
- websockets>=11.0.3
4
+ websockets>=14.0
5
5
 
6
6
  [dev]
7
7
  black==23.3.0
@@ -1,8 +1,7 @@
1
1
  from datetime import date
2
- from itertools import islice
3
2
  from os import environ
4
3
 
5
- from pytest import fixture, mark, raises, skip
4
+ from pytest import mark, raises, skip
6
5
 
7
6
  from pybluecurrent import BlueCurrentClient
8
7
  from pybluecurrent.exceptions import AuthenticationFailed, BlueCurrentException
@@ -26,10 +25,6 @@ class TestAuthentication:
26
25
  async with client:
27
26
  pass
28
27
 
29
- def test_login(self, client_with_auth: BlueCurrentClient):
30
- client_with_auth.login()
31
- assert client_with_auth.token is not None
32
-
33
28
 
34
29
  class TestSocketApi:
35
30
  async def test_get_account(self, connected_client: BlueCurrentClient):
@@ -122,13 +117,13 @@ class TestSocketApi:
122
117
 
123
118
  @mark.skipif(environ.get("BLUECURRENT_READ_ONLY", "TRUE") != "FALSE", reason="Running read-only tests.")
124
119
  async def test_set_status(self, connected_client: BlueCurrentClient, evse_id: str):
125
- before_status = connected_client.get_charge_point_status(evse_id=evse_id)
120
+ before_status = await connected_client.get_charge_point_status(evse_id=evse_id)
126
121
  if before_status["activity"] != "available":
127
122
  skip(reason="Only perform this test if the charge point is available.")
128
123
  await connected_client.set_status(evse_id=evse_id, enabled=False)
129
- assert connected_client.get_charge_point_status(evse_id=evse_id)["activity"] == "unavailable"
124
+ assert (await connected_client.get_charge_point_status(evse_id=evse_id))["activity"] == "unavailable"
130
125
  await connected_client.set_status(evse_id=evse_id, enabled=True)
131
- assert connected_client.get_charge_point_status(evse_id=evse_id)["activity"] == "available"
126
+ assert (await connected_client.get_charge_point_status(evse_id=evse_id))["activity"] == "available"
132
127
 
133
128
  async def test_error(self, connected_client: BlueCurrentClient):
134
129
  with raises(BlueCurrentException) as e:
@@ -137,33 +132,36 @@ class TestSocketApi:
137
132
 
138
133
 
139
134
  class TestRestApi:
140
- @fixture(scope="class")
141
- def authenticated_client(self, client_with_auth: BlueCurrentClient) -> BlueCurrentClient:
142
- client_with_auth.login()
143
- return client_with_auth
144
-
145
- def test_get_contracts(self, authenticated_client: BlueCurrentClient):
146
- contracts = authenticated_client.get_contracts()
135
+ async def test_get_contracts(self, connected_client: BlueCurrentClient):
136
+ contracts = await connected_client.get_contracts()
147
137
  assert len(contracts) > 0
148
138
  assert "contract_id" in contracts[0]
149
139
 
150
- def test_get_charge_point_status(self, authenticated_client: BlueCurrentClient, evse_id: str):
151
- status = authenticated_client.get_charge_point_status(evse_id)
140
+ async def test_get_charge_point_status(self, connected_client: BlueCurrentClient, evse_id: str):
141
+ status = await connected_client.get_charge_point_status(evse_id)
152
142
  assert status["evse_id"] == evse_id
153
143
  assert "activity" in status
154
144
 
155
- def test_get_grids(self, authenticated_client: BlueCurrentClient):
156
- grids = authenticated_client.get_grids()
145
+ async def test_get_grids(self, connected_client: BlueCurrentClient):
146
+ grids = await connected_client.get_grids()
157
147
  assert len(grids) > 0
158
148
  assert "id" in grids[0]
159
149
 
160
- def test_get_transactions(self, authenticated_client: BlueCurrentClient, evse_id: str):
161
- transactions = authenticated_client.get_transactions(evse_id)
150
+ async def test_get_transactions(self, connected_client: BlueCurrentClient, evse_id: str):
151
+ transactions = await connected_client.get_transactions(evse_id)
162
152
  assert "transactions" in transactions
163
153
 
164
- def test_iterate_transactions(self, authenticated_client: BlueCurrentClient, evse_id: str):
165
- transactions = authenticated_client.get_transactions(evse_id)
166
- # If there are less than three pages, there might be less than 30.
154
+ async def test_iterate_transactions(self, connected_client: BlueCurrentClient, evse_id: str):
155
+ transactions = await connected_client.get_transactions(evse_id)
156
+ # If there are less than three pages, there might be fewer than 30.
167
157
  if transactions["total_pages"] < 3: # type: ignore
168
158
  skip("Not enough transactions.")
169
- assert len({t["transaction_id"] for t in islice(authenticated_client.iterate_transactions(evse_id), 30)}) == 30
159
+ n_transactions = 0
160
+ # Verify pagination works correctly - we get 30 unique transactions from multiple pages.
161
+ unique_transactions = set()
162
+ async for transaction in connected_client.iterate_transactions(evse_id):
163
+ n_transactions += 1
164
+ unique_transactions.add(transaction["transaction_id"])
165
+ if n_transactions >= 30:
166
+ break
167
+ assert len(unique_transactions) == 30
File without changes
File without changes
File without changes