pybluecurrent 0.0.1__tar.gz → 0.0.2__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.
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/.github/workflows/publish.yaml +1 -1
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/.github/workflows/test.yaml +1 -1
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/.gitignore +3 -0
- pybluecurrent-0.0.2/PKG-INFO +395 -0
- pybluecurrent-0.0.2/README.md +369 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/pyproject.toml +4 -1
- pybluecurrent-0.0.2/src/pybluecurrent/_version.py +16 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/src/pybluecurrent/client.py +61 -24
- pybluecurrent-0.0.2/src/pybluecurrent/utilities.py +36 -0
- pybluecurrent-0.0.2/src/pybluecurrent.egg-info/PKG-INFO +395 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/src/pybluecurrent.egg-info/SOURCES.txt +3 -1
- pybluecurrent-0.0.2/src/pybluecurrent.egg-info/requires.txt +10 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/tests/test_client.py +13 -0
- pybluecurrent-0.0.2/tests/test_utilities.py +41 -0
- pybluecurrent-0.0.1/PKG-INFO +0 -21
- pybluecurrent-0.0.1/README.md +0 -3
- pybluecurrent-0.0.1/src/pybluecurrent/_version.py +0 -4
- pybluecurrent-0.0.1/src/pybluecurrent.egg-info/PKG-INFO +0 -21
- pybluecurrent-0.0.1/src/pybluecurrent.egg-info/requires.txt +0 -6
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/.pre-commit-config.yaml +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/LICENSE +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/setup.cfg +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/src/pybluecurrent/__init__.py +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/src/pybluecurrent/exceptions.py +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/src/pybluecurrent/py.typed +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/src/pybluecurrent.egg-info/dependency_links.txt +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/src/pybluecurrent.egg-info/top_level.txt +0 -0
- {pybluecurrent-0.0.1 → pybluecurrent-0.0.2}/tests/conftest.py +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pybluecurrent
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Python client for BlueCurrent charge points.
|
|
5
|
+
Author-email: Rogier van der Geer <rogier@vander-geer.nl>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/rogiervandergeer/pybluecurrent
|
|
8
|
+
Keywords: api,blue current,electric vehicle,ev
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: asyncio-multisubscriber-queue>=0.4.1
|
|
18
|
+
Requires-Dist: requests>=2.31.0
|
|
19
|
+
Requires-Dist: sjcl>=0.2.1
|
|
20
|
+
Requires-Dist: websockets>=11.0.3
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: black==23.3.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pre-commit>=3.3.3; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=7.3.1; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21.1; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# pybluecurrent
|
|
28
|
+
|
|
29
|
+
Python client for [BlueCurrent](https://www.bluecurrent.nl) charge points.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Using the client is as simple as:
|
|
39
|
+
```python
|
|
40
|
+
from pybluecurrent import BlueCurrentClient
|
|
41
|
+
|
|
42
|
+
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
43
|
+
|
|
44
|
+
async with client:
|
|
45
|
+
charge_points = await client.get_charge_points()
|
|
46
|
+
|
|
47
|
+
transactions = client.get_transactions(charge_points[0]["evse_id"])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Methods
|
|
51
|
+
|
|
52
|
+
BlueCurrent exposes two APIs, a synchronous REST API as well as an asynchronous Websocket API.
|
|
53
|
+
As a result, the `BlueCurrentClient` also exposes synchronous as well as asynchronous methods:
|
|
54
|
+
|
|
55
|
+
- [Asynchronous](#asynchronous)
|
|
56
|
+
- [`get_account`](#getaccount---get-your-account-information)
|
|
57
|
+
- [`get_charge_cards`](#getchargecards---get-your-charge-cards)
|
|
58
|
+
- [`get_charge_points`](#getchargepoints---get-your-charge-points)
|
|
59
|
+
- [`get_charge_point_settings`](#getchargepointsettings---get-the-settings-of-a-charge-point)
|
|
60
|
+
- [`get_grid_status`](#getgridstatus---get-the-grid-status-associated-to-a-charge-point)
|
|
61
|
+
- [`get_sustainability_status`](#getsustainabilitystatus---get-statistics-on-the-sustainability-of-all-your-charge-points)
|
|
62
|
+
- [`set_status`](#setstatus---enable-or-disable-a-charge-point)
|
|
63
|
+
- [Synchronous](#synchronous)
|
|
64
|
+
- [`login`](#login---log-in)
|
|
65
|
+
- [`get_charge_point_status`](#getchargepointstatus---get-the-status-of-a-charge-point)
|
|
66
|
+
- [`get_contracts`](#getcontracts---get-your-contracts)
|
|
67
|
+
- [`get_grids`](#getgrids---get-your-grid-connections)
|
|
68
|
+
- [`get_transactions`](#gettransactions---get-a-list-of-transactions)
|
|
69
|
+
- [`iterate_transactions`](#iteratetransactions---iterate-through-your-transactions)
|
|
70
|
+
|
|
71
|
+
### Asynchronous
|
|
72
|
+
|
|
73
|
+
The async methods can only be used when the websocket client is connected. For example:
|
|
74
|
+
```python
|
|
75
|
+
client = BlueCurrentClient("your_username", "your_secret_password")
|
|
76
|
+
async with client:
|
|
77
|
+
result = await client.get_account()
|
|
78
|
+
```
|
|
79
|
+
Entering the async context will automatically login.
|
|
80
|
+
|
|
81
|
+
#### `get_account` - Get your account information.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
async def get_account(self) -> dict[str, bool | str]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
##### Returns
|
|
88
|
+
A dictionary describing your account:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
{
|
|
92
|
+
"full_name": "Your Full Name",
|
|
93
|
+
"email": "your@email.address",
|
|
94
|
+
"login": "your@email.address",
|
|
95
|
+
"should_reset_password": False,
|
|
96
|
+
"developer_mode_enabled": False,
|
|
97
|
+
"tel": "",
|
|
98
|
+
"marketing_target": "bluecurrent",
|
|
99
|
+
"first_login_app": date(2020, 1, 1),
|
|
100
|
+
"hubspot_user_identity": "a_very_long_string"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `get_charge_cards` - Get your charge cards.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
async def get_charge_cards(self) -> list[dict[str, date | int | str | None]]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
##### Returns
|
|
111
|
+
|
|
112
|
+
A list of dictionaries, each representing a charge card:
|
|
113
|
+
```python
|
|
114
|
+
{
|
|
115
|
+
"uid": "A1B2C3D4E5F6",
|
|
116
|
+
"id": "NL-ABC-123456-0",
|
|
117
|
+
"name": "My Charge Cards",
|
|
118
|
+
"customer_name": "Your Name",
|
|
119
|
+
"valid": 1,
|
|
120
|
+
"date_created": date(2023, 6, 27),
|
|
121
|
+
"date_modified": date(2023, 7, 11),
|
|
122
|
+
"date_became_invalid": None
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### `get_charge_points` - Get your charge points.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
async def get_charge_points(self) -> list[dict[str, bool | dict | str]]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
##### Returns
|
|
133
|
+
|
|
134
|
+
A list of dictionaries, each representing a charge card:
|
|
135
|
+
```python
|
|
136
|
+
{
|
|
137
|
+
"evse_id": "BCU123456",
|
|
138
|
+
"name": "",
|
|
139
|
+
"model_type": "H:MOVE-C32T2",
|
|
140
|
+
"chargepoint_type": "HIDDEN",
|
|
141
|
+
"is_cable": True,
|
|
142
|
+
"public_charging": {"value": False, "permission": "write"},
|
|
143
|
+
"default_card": {"uid": "A1B2C3D4E5F6", "id": "NL-ABC-123456-0", "name": "Your Card",
|
|
144
|
+
"customer_name": "Your Name", "valid": 1},
|
|
145
|
+
"preferred_card": {"uid": "A1B2C3D4E5F6", "id": "NL-ABC-123456-0", "name": "Your Card",
|
|
146
|
+
"customer_name": "Your Name", "valid": 1},
|
|
147
|
+
"plug_and_charge_card": {"uid": "A1B2C3D4E5F6", "id": "NL-ABC-123456-0", "name": "Your Card",
|
|
148
|
+
"customer_name": "Your Name", "valid": 1},
|
|
149
|
+
"tariff": {"tariff_id","NLBCUT58", "price_ex_vat": 0.2, "start_price_ex_vat": 0, "price_in_vat": 0.242,
|
|
150
|
+
"start_price_in_vat": 0, "currency": "EUR", "vat_percentage": 21},
|
|
151
|
+
"plug_and_charge_notification": False,
|
|
152
|
+
"plug_and_charge": {"value": True, "permission": "write"},
|
|
153
|
+
"led_interaction": {"value": False, "permission": "read"},
|
|
154
|
+
"publish_location": {"value": False, "permission": "write"},
|
|
155
|
+
"smart_charging": True,
|
|
156
|
+
"smart_charging_dynamic": True,
|
|
157
|
+
"activity": "available",
|
|
158
|
+
"location": {"x_coord": 50.1234, "y_coord": 5.01234, "street": "Europalaan", "housenumber": "100",
|
|
159
|
+
"zipcode": "3526KS", "city": "Utrecht", "country": "NL"},
|
|
160
|
+
"delayed_charging": {"value": False, "permission": "none"}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `get_charge_point_settings` - Get the settings of a charge point.
|
|
165
|
+
|
|
166
|
+
All the information returned by this endpoint is already included
|
|
167
|
+
in the response of [`get_charge_points`](#getchargepoints---get-your-charge-points).
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
async def get_charge_point_settings(self, evse_id: str) -> dict[str, bool | dict | str]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
##### Arguments
|
|
174
|
+
- `evse_id`: The ID of the charge point.
|
|
175
|
+
|
|
176
|
+
##### Returns
|
|
177
|
+
A dictionary describing the settings:
|
|
178
|
+
```python
|
|
179
|
+
{
|
|
180
|
+
"evse_id": "BCU123456",
|
|
181
|
+
"plug_and_charge": {"value": True, "permission": "write"},
|
|
182
|
+
"public_charging": {"value": False, "permission": "write"},
|
|
183
|
+
"default_card": {"uid": "A1B2C3D4E5F6", "id": "NL-ABC-123456-0", "name": "Your Card",
|
|
184
|
+
"customer_name": "Your Name", "valid": 1},
|
|
185
|
+
"preferred_card": {"uid": "A1B2C3D4E5F6", "id": "NL-ABC-123456-0", "name": "Your Card",
|
|
186
|
+
"customer_name": "Your Name", "valid": 1},
|
|
187
|
+
"plug_and_charge_card": {"uid": "A1B2C3D4E5F6", "id": "NL-ABC-123456-0", "name": "Your Card",
|
|
188
|
+
"customer_name": "Your Name", "valid": 1},
|
|
189
|
+
"smart_charging": True,
|
|
190
|
+
"smart_charging_dynamic": True,
|
|
191
|
+
"model_type": "H:MOVE-C32T2",
|
|
192
|
+
"is_cable": True,
|
|
193
|
+
"chargepoint_type": "HIDDEN",
|
|
194
|
+
"plug_and_charge_notification": False,
|
|
195
|
+
"led_intensity": {"value": 0, "permission": "none"},
|
|
196
|
+
"led_interaction": {"value": False, "permission": "none"}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### `get_grid_status` - Get the grid status associated to a charge point.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
async def get_grid_status(self, evse_id: str) -> dict[str, int | str]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
##### Arguments
|
|
207
|
+
- `evse_id`: The ID of the charge point.
|
|
208
|
+
|
|
209
|
+
##### Returns
|
|
210
|
+
A dictionary describing the actual grid current and maximum current in amps:
|
|
211
|
+
```python
|
|
212
|
+
{
|
|
213
|
+
"id": "GRID-BCU123456",
|
|
214
|
+
"grid_actual_p1": 1,
|
|
215
|
+
"grid_actual_p2": 2,
|
|
216
|
+
"grid_actual_p3": 3,
|
|
217
|
+
"grid_max_install": 25,
|
|
218
|
+
"grid_max_reserved": 25
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### `get_sustainability_status` - Get statistics on the sustainability of all your charge points.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
async def get_sustainability_status(self) -> dict[str, float | int]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
##### Returns
|
|
229
|
+
A dictionary with two keys: `{"trees": 1, "co2": 12.345}`
|
|
230
|
+
|
|
231
|
+
#### `set_status` - Enable or disable a charge point.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
async def set_status(self, evse_id: str, enabled: bool) -> None
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
##### Arguments
|
|
238
|
+
- `evse_id`: The ID of the charge point.
|
|
239
|
+
- `enabled`: Boolean that indicates the desired status.
|
|
240
|
+
|
|
241
|
+
### Synchronous
|
|
242
|
+
|
|
243
|
+
#### `login` - Log in
|
|
244
|
+
```python
|
|
245
|
+
def login(self) -> None
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This method does not do anything if the client is already logged in.
|
|
249
|
+
Connection to the websocket api (async with client) automatically logs in the client,
|
|
250
|
+
so this endpoint is not needed when using the async API.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
#### `get_charge_point_status` - Get the status of a charge point.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
def get_charge_point_status(self, evse_id: str) -> dict[str, datetime | float | int | str | None]
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
##### Arguments
|
|
260
|
+
- `evse_id`: The ID of the charge point.
|
|
261
|
+
|
|
262
|
+
##### Returns
|
|
263
|
+
A dictionary with the chargepoint status:
|
|
264
|
+
```python
|
|
265
|
+
{
|
|
266
|
+
"actual_p1": 0,
|
|
267
|
+
"actual_p2": 0,
|
|
268
|
+
"actual_p3": 0,
|
|
269
|
+
"activity": "available",
|
|
270
|
+
"actual_v1": 0,
|
|
271
|
+
"actual_v2": 0,
|
|
272
|
+
"actual_v3": 0,
|
|
273
|
+
"actual_kwh": 0,
|
|
274
|
+
"max_usage": 20,
|
|
275
|
+
"smartcharging_max_usage": 6,
|
|
276
|
+
"max_offline": 10,
|
|
277
|
+
"offline_since": "",
|
|
278
|
+
"start_datetime": datetime(2023, 7, 24, 15, 25, 33),
|
|
279
|
+
"stop_datetime": datetime(2023, 7, 26, 7, 48, 40),
|
|
280
|
+
"total_cost": 9.93,
|
|
281
|
+
"vehicle_status": "A",
|
|
282
|
+
"evse_id": "BCU123456",
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### `get_contracts` - Get your contracts.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
def get_contracts(self) -> list[dict[str, str]]
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
##### Returns
|
|
293
|
+
A list of dictionaries, each representing a contract:
|
|
294
|
+
```python
|
|
295
|
+
[
|
|
296
|
+
{
|
|
297
|
+
"contract_id": "BCU12345678",
|
|
298
|
+
"contact_email": "your@email.address",
|
|
299
|
+
"subscription_type": "BASIS",
|
|
300
|
+
"beneficiary_name": "Your Name",
|
|
301
|
+
"iban_beneficiary": "NL00ABCD0123456789"
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### `get_grids` - Get your grid connections.
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
def get_grids(self) -> list[dict[str, bool | dict[str, str] | str]]
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
##### Returns
|
|
313
|
+
A list of dictionaries, each representing a grid:
|
|
314
|
+
```python
|
|
315
|
+
[
|
|
316
|
+
{
|
|
317
|
+
"address": {"street": "Street Name", "housenumber": "1", "postal_code": "1234AB",
|
|
318
|
+
"city": "Amsterdam", "country": "NL", "region": ""},
|
|
319
|
+
"smart_charging": True,
|
|
320
|
+
"id": "GRID-BCU123456"
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### `get_transactions` - Get a list of transactions.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
def get_transactions(
|
|
329
|
+
self, evse_id: str, newest_first: bool = True, page: int = 1
|
|
330
|
+
) -> dict[str, int | list[dict[str, Any]]]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
##### Arguments
|
|
334
|
+
- `evse_id`: The ID of the charge point.
|
|
335
|
+
- `newest_first`: If `True`, start with the most recent transaction. Defaults to `True`.
|
|
336
|
+
- `page`: Page number to get. Defaults to `1`.
|
|
337
|
+
|
|
338
|
+
##### Returns
|
|
339
|
+
A dictionary like this:
|
|
340
|
+
```python
|
|
341
|
+
{
|
|
342
|
+
"current_page": 1,
|
|
343
|
+
"next_page": 2, # This is None when there is no next page.
|
|
344
|
+
"max_per_page": 25,
|
|
345
|
+
"total_pages": 8,
|
|
346
|
+
"transactions: [
|
|
347
|
+
{
|
|
348
|
+
"transaction_id": 12345678,
|
|
349
|
+
"chargepoint_id": "BCU123456",
|
|
350
|
+
"chargepoint_type": "HIDDEN",
|
|
351
|
+
"evse_name": "Charge Point Name",
|
|
352
|
+
"started_at": datetime(2023, 7, 1, 12, 34, 56),
|
|
353
|
+
"end_time": datetime(2023, 7, 1, 14, 0, 0),
|
|
354
|
+
"kwh": 12.34,
|
|
355
|
+
"card_id": "NL-ABC-123456-0",
|
|
356
|
+
"card_name": "Card Name",
|
|
357
|
+
"total_costs": 5.97,
|
|
358
|
+
"total_costs_ex_vat": 4.93,
|
|
359
|
+
"vat": 21,
|
|
360
|
+
"currency": "EUR"
|
|
361
|
+
},
|
|
362
|
+
...
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### `iterate_transactions` - Iterate through your transactions
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
def iterate_transactions(self, evse_id: str, newest_first: bool = True) -> Iterable[dict[str, Any]]
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
##### Arguments
|
|
374
|
+
- `evse_id`: The ID of the charge point.
|
|
375
|
+
- `newest_first`: If `True`, start with the most recent transaction. Defaults to `True`.
|
|
376
|
+
|
|
377
|
+
##### Returns
|
|
378
|
+
An iterable of dictionaries describing the transactions. Each dictionary looks like this:
|
|
379
|
+
```python
|
|
380
|
+
{
|
|
381
|
+
"transaction_id": 12345678,
|
|
382
|
+
"chargepoint_id": "BCU123456",
|
|
383
|
+
"chargepoint_type": "HIDDEN",
|
|
384
|
+
"evse_name": "Charge Point Name",
|
|
385
|
+
"started_at": datetime(2023, 7, 1, 12, 34, 56),
|
|
386
|
+
"end_time": datetime(2023, 7, 1, 14, 0, 0),
|
|
387
|
+
"kwh": 12.34,
|
|
388
|
+
"card_id": "NL-ABC-123456-0",
|
|
389
|
+
"card_name": "Card Name",
|
|
390
|
+
"total_costs": 5.97,
|
|
391
|
+
"total_costs_ex_vat": 4.93,
|
|
392
|
+
"vat": 21,
|
|
393
|
+
"currency": "EUR"
|
|
394
|
+
}
|
|
395
|
+
```
|