pykoplenti 1.2.2__tar.gz → 1.4.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.
Potentially problematic release.
This version of pykoplenti might be problematic. Click here for more details.
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/PKG-INFO +24 -13
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/README.md +15 -7
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti/api.py +55 -67
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti/cli.py +200 -60
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti/model.py +28 -6
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti.egg-info/PKG-INFO +24 -13
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti.egg-info/SOURCES.txt +1 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti.egg-info/requires.txt +2 -2
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/setup.cfg +7 -5
- pykoplenti-1.4.0/tests/test_cli.py +151 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/tests/test_extendedapiclient.py +26 -10
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/tests/test_pykoplenti.py +15 -4
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/LICENSE +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti/__init__.py +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti/extended.py +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti/py.typed +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti.egg-info/dependency_links.txt +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti.egg-info/entry_points.txt +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pykoplenti.egg-info/top_level.txt +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/pyproject.toml +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/setup.py +0 -0
- {pykoplenti-1.2.2 → pykoplenti-1.4.0}/tests/test_smoketest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pykoplenti
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Python REST-Client for Kostal Plenticore Solar Inverters
|
|
5
5
|
Home-page: https://github.com/stegm/pyclient_koplenti
|
|
6
6
|
Author: @stegm
|
|
@@ -13,17 +13,20 @@ Classifier: Environment :: Console
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
19
21
|
Description-Content-Type: text/markdown
|
|
20
22
|
License-File: LICENSE
|
|
21
23
|
Requires-Dist: aiohttp~=3.8
|
|
22
24
|
Requires-Dist: pycryptodome~=3.19
|
|
23
|
-
Requires-Dist: pydantic
|
|
25
|
+
Requires-Dist: pydantic>=1.10
|
|
24
26
|
Provides-Extra: cli
|
|
25
27
|
Requires-Dist: prompt_toolkit>=3.0; extra == "cli"
|
|
26
|
-
Requires-Dist: click>=
|
|
28
|
+
Requires-Dist: click>=8.0; extra == "cli"
|
|
29
|
+
Dynamic: license-file
|
|
27
30
|
|
|
28
31
|
# Python Library for Accessing Kostal Plenticore Inverters
|
|
29
32
|
|
|
@@ -75,22 +78,28 @@ Installing the libray with `CLI` provides a new command.
|
|
|
75
78
|
|
|
76
79
|
```shell
|
|
77
80
|
$ pykoplenti --help
|
|
78
|
-
Usage: pykoplenti [OPTIONS] COMMAND [ARGS]...
|
|
81
|
+
Usage: python -m pykoplenti.cli [OPTIONS] COMMAND [ARGS]...
|
|
79
82
|
|
|
80
83
|
Handling of global arguments with click
|
|
81
84
|
|
|
82
85
|
Options:
|
|
83
|
-
--host TEXT
|
|
84
|
-
--port INTEGER
|
|
85
|
-
--password TEXT
|
|
86
|
-
--
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
--host TEXT Hostname or IP of the inverter
|
|
87
|
+
--port INTEGER Port of the inverter [default: 80]
|
|
88
|
+
--password TEXT Password or master key (also device id)
|
|
89
|
+
--service-code TEXT service code for installer access
|
|
90
|
+
--password-file FILE Path to password file - deprecated, use --credentials
|
|
91
|
+
[default: secrets]
|
|
92
|
+
--credentials FILE Path to the credentials file. This has a simple ini-
|
|
93
|
+
format without sections. For user access, use the
|
|
94
|
+
'password'. For installer access, use the 'master-key'
|
|
95
|
+
and 'service-key'.
|
|
89
96
|
--help Show this message and exit.
|
|
90
97
|
|
|
91
98
|
Commands:
|
|
92
99
|
all-processdata Returns a list of all available process data.
|
|
93
100
|
all-settings Returns the ids of all settings.
|
|
101
|
+
download-log Download the log data from the inverter to a file.
|
|
102
|
+
read-events Returns the last events
|
|
94
103
|
read-processdata Returns the values of the given process data.
|
|
95
104
|
read-settings Read the value of the given settings.
|
|
96
105
|
repl Provides a simple REPL for executing API requests to...
|
|
@@ -155,9 +164,11 @@ await client.login(my_master_key, service_code=my_service_code)
|
|
|
155
164
|
- [click](https://click.palletsprojects.com/) - command line interface framework
|
|
156
165
|
- [black](https://github.com/psf/black) - Python code formatter
|
|
157
166
|
- [ruff](https://github.com/astral-sh/ruff) - Python linter
|
|
167
|
+
- [pydantic](https://docs.pydantic.dev/latest/) - Data validation library
|
|
158
168
|
- [pytest](https://docs.pytest.org/) - Python test framework
|
|
159
169
|
- [mypy](https://mypy-lang.org/) - Python type checker
|
|
160
170
|
- [setuptools](https://github.com/pypa/setuptools) - Python packager
|
|
171
|
+
- [tox](https://tox.wiki) - Automate testing
|
|
161
172
|
|
|
162
173
|
## License
|
|
163
174
|
|
|
@@ -48,22 +48,28 @@ Installing the libray with `CLI` provides a new command.
|
|
|
48
48
|
|
|
49
49
|
```shell
|
|
50
50
|
$ pykoplenti --help
|
|
51
|
-
Usage: pykoplenti [OPTIONS] COMMAND [ARGS]...
|
|
51
|
+
Usage: python -m pykoplenti.cli [OPTIONS] COMMAND [ARGS]...
|
|
52
52
|
|
|
53
53
|
Handling of global arguments with click
|
|
54
54
|
|
|
55
55
|
Options:
|
|
56
|
-
--host TEXT
|
|
57
|
-
--port INTEGER
|
|
58
|
-
--password TEXT
|
|
59
|
-
--
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
--host TEXT Hostname or IP of the inverter
|
|
57
|
+
--port INTEGER Port of the inverter [default: 80]
|
|
58
|
+
--password TEXT Password or master key (also device id)
|
|
59
|
+
--service-code TEXT service code for installer access
|
|
60
|
+
--password-file FILE Path to password file - deprecated, use --credentials
|
|
61
|
+
[default: secrets]
|
|
62
|
+
--credentials FILE Path to the credentials file. This has a simple ini-
|
|
63
|
+
format without sections. For user access, use the
|
|
64
|
+
'password'. For installer access, use the 'master-key'
|
|
65
|
+
and 'service-key'.
|
|
62
66
|
--help Show this message and exit.
|
|
63
67
|
|
|
64
68
|
Commands:
|
|
65
69
|
all-processdata Returns a list of all available process data.
|
|
66
70
|
all-settings Returns the ids of all settings.
|
|
71
|
+
download-log Download the log data from the inverter to a file.
|
|
72
|
+
read-events Returns the last events
|
|
67
73
|
read-processdata Returns the values of the given process data.
|
|
68
74
|
read-settings Read the value of the given settings.
|
|
69
75
|
repl Provides a simple REPL for executing API requests to...
|
|
@@ -128,9 +134,11 @@ await client.login(my_master_key, service_code=my_service_code)
|
|
|
128
134
|
- [click](https://click.palletsprojects.com/) - command line interface framework
|
|
129
135
|
- [black](https://github.com/psf/black) - Python code formatter
|
|
130
136
|
- [ruff](https://github.com/astral-sh/ruff) - Python linter
|
|
137
|
+
- [pydantic](https://docs.pydantic.dev/latest/) - Data validation library
|
|
131
138
|
- [pytest](https://docs.pytest.org/) - Python test framework
|
|
132
139
|
- [mypy](https://mypy-lang.org/) - Python type checker
|
|
133
140
|
- [setuptools](https://github.com/pypa/setuptools) - Python packager
|
|
141
|
+
- [tox](https://tox.wiki) - Automate testing
|
|
134
142
|
|
|
135
143
|
## License
|
|
136
144
|
|
|
@@ -13,17 +13,16 @@ import warnings
|
|
|
13
13
|
|
|
14
14
|
from Crypto.Cipher import AES
|
|
15
15
|
from aiohttp import ClientResponse, ClientSession, ClientTimeout
|
|
16
|
-
from pydantic import parse_obj_as
|
|
17
16
|
from yarl import URL
|
|
18
17
|
|
|
19
18
|
from .model import (
|
|
20
19
|
EventData,
|
|
21
20
|
MeData,
|
|
22
21
|
ModuleData,
|
|
23
|
-
ProcessData,
|
|
24
22
|
ProcessDataCollection,
|
|
25
23
|
SettingsData,
|
|
26
24
|
VersionData,
|
|
25
|
+
process_data_list,
|
|
27
26
|
)
|
|
28
27
|
|
|
29
28
|
_logger: Final = logging.getLogger(__name__)
|
|
@@ -86,6 +85,20 @@ class ModuleNotFoundException(ApiException):
|
|
|
86
85
|
self.error = error
|
|
87
86
|
|
|
88
87
|
|
|
88
|
+
def _relogin(fn):
|
|
89
|
+
"""Decorator for automatic re-login if session was expired."""
|
|
90
|
+
|
|
91
|
+
@functools.wraps(fn)
|
|
92
|
+
async def _wrapper(self: "ApiClient", *args, **kwargs):
|
|
93
|
+
with contextlib.suppress(AuthenticationException, NotAuthorizedException):
|
|
94
|
+
return await fn(self, *args, **kwargs)
|
|
95
|
+
_logger.debug("Request failed - try to re-login")
|
|
96
|
+
await self._login()
|
|
97
|
+
return await fn(self, *args, **kwargs)
|
|
98
|
+
|
|
99
|
+
return _wrapper
|
|
100
|
+
|
|
101
|
+
|
|
89
102
|
class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
90
103
|
"""Client for the REST-API of Kostal Plenticore inverters.
|
|
91
104
|
|
|
@@ -265,7 +278,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
265
278
|
client_signature = hmac.new(
|
|
266
279
|
stored_key, auth_msg.encode("utf-8"), hashlib.sha256
|
|
267
280
|
).digest()
|
|
268
|
-
client_proof = bytes(
|
|
281
|
+
client_proof = bytes(a ^ b for a, b in zip(client_key, client_signature))
|
|
269
282
|
|
|
270
283
|
server_key = hmac.new(
|
|
271
284
|
salted_passwd, "Server Key".encode("utf-8"), hashlib.sha256
|
|
@@ -343,47 +356,32 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
343
356
|
"""Check if the given response contains an error and throws
|
|
344
357
|
the appropriate exception."""
|
|
345
358
|
|
|
346
|
-
if resp.status
|
|
347
|
-
|
|
348
|
-
response = await resp.json()
|
|
349
|
-
error = response["message"]
|
|
350
|
-
except Exception:
|
|
351
|
-
error = None
|
|
352
|
-
|
|
353
|
-
if resp.status == 400:
|
|
354
|
-
raise AuthenticationException(resp.status, error)
|
|
359
|
+
if resp.status == 200:
|
|
360
|
+
return
|
|
355
361
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
362
|
+
try:
|
|
363
|
+
response = await resp.json()
|
|
364
|
+
error = response["message"]
|
|
365
|
+
except Exception:
|
|
366
|
+
error = None
|
|
361
367
|
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
if resp.status == 400:
|
|
369
|
+
raise AuthenticationException(resp.status, error)
|
|
364
370
|
|
|
365
|
-
|
|
366
|
-
|
|
371
|
+
if resp.status == 401:
|
|
372
|
+
raise NotAuthorizedException(resp.status, error)
|
|
367
373
|
|
|
368
|
-
|
|
369
|
-
raise
|
|
374
|
+
if resp.status == 403:
|
|
375
|
+
raise UserLockedException(resp.status, error)
|
|
370
376
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"""Decorator for automatic re-login if session was expired."""
|
|
377
|
+
if resp.status == 404:
|
|
378
|
+
raise ModuleNotFoundException(resp.status, error)
|
|
374
379
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
try:
|
|
378
|
-
return await fn(self, *args, **kwargs)
|
|
379
|
-
except (AuthenticationException, NotAuthorizedException):
|
|
380
|
-
pass
|
|
380
|
+
if resp.status == 503:
|
|
381
|
+
raise InternalCommunicationException(resp.status, error)
|
|
381
382
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return await fn(self, *args, **kwargs)
|
|
385
|
-
|
|
386
|
-
return _wrapper
|
|
383
|
+
# we got an undocumented status code
|
|
384
|
+
raise ApiException(f"Unknown API response [{resp.status}] - {error}")
|
|
387
385
|
|
|
388
386
|
async def logout(self):
|
|
389
387
|
"""Logs the current user out."""
|
|
@@ -422,7 +420,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
422
420
|
if lang is None:
|
|
423
421
|
lang = locale.getlocale()[0]
|
|
424
422
|
|
|
425
|
-
language = lang[
|
|
423
|
+
language = lang[:2].lower()
|
|
426
424
|
variant = lang[3:5].lower()
|
|
427
425
|
if language not in ApiClient.SUPPORTED_LANGUAGES.keys():
|
|
428
426
|
# Fallback to default
|
|
@@ -466,38 +464,33 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
466
464
|
self,
|
|
467
465
|
module_id: str,
|
|
468
466
|
processdata_id: str,
|
|
469
|
-
) -> Mapping[str, ProcessDataCollection]:
|
|
470
|
-
...
|
|
467
|
+
) -> Mapping[str, ProcessDataCollection]: ...
|
|
471
468
|
|
|
472
469
|
@overload
|
|
473
470
|
async def get_process_data_values(
|
|
474
471
|
self,
|
|
475
472
|
module_id: str,
|
|
476
473
|
processdata_id: Iterable[str],
|
|
477
|
-
) -> Mapping[str, ProcessDataCollection]:
|
|
478
|
-
...
|
|
474
|
+
) -> Mapping[str, ProcessDataCollection]: ...
|
|
479
475
|
|
|
480
476
|
@overload
|
|
481
477
|
async def get_process_data_values(
|
|
482
478
|
self,
|
|
483
479
|
module_id: str,
|
|
484
|
-
) -> Mapping[str, ProcessDataCollection]:
|
|
485
|
-
...
|
|
480
|
+
) -> Mapping[str, ProcessDataCollection]: ...
|
|
486
481
|
|
|
487
482
|
@overload
|
|
488
483
|
async def get_process_data_values(
|
|
489
484
|
self,
|
|
490
485
|
module_id: Mapping[str, Iterable[str]],
|
|
491
|
-
) -> Mapping[str, ProcessDataCollection]:
|
|
492
|
-
...
|
|
486
|
+
) -> Mapping[str, ProcessDataCollection]: ...
|
|
493
487
|
|
|
494
488
|
@overload
|
|
495
489
|
async def get_process_data_values(
|
|
496
490
|
self,
|
|
497
491
|
module_id: Union[str, Mapping[str, Iterable[str]]],
|
|
498
492
|
processdata_id: Union[str, Iterable[str], None] = None,
|
|
499
|
-
) -> Mapping[str, ProcessDataCollection]:
|
|
500
|
-
...
|
|
493
|
+
) -> Mapping[str, ProcessDataCollection]: ...
|
|
501
494
|
|
|
502
495
|
@_relogin
|
|
503
496
|
async def get_process_data_values(
|
|
@@ -523,7 +516,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
523
516
|
data_response = await resp.json()
|
|
524
517
|
return {
|
|
525
518
|
data_response[0]["moduleid"]: ProcessDataCollection(
|
|
526
|
-
|
|
519
|
+
process_data_list(data_response[0]["processdata"])
|
|
527
520
|
)
|
|
528
521
|
}
|
|
529
522
|
|
|
@@ -536,7 +529,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
536
529
|
data_response = await resp.json()
|
|
537
530
|
return {
|
|
538
531
|
data_response[0]["moduleid"]: ProcessDataCollection(
|
|
539
|
-
|
|
532
|
+
process_data_list(data_response[0]["processdata"])
|
|
540
533
|
)
|
|
541
534
|
}
|
|
542
535
|
|
|
@@ -552,7 +545,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
552
545
|
data_response = await resp.json()
|
|
553
546
|
return {
|
|
554
547
|
data_response[0]["moduleid"]: ProcessDataCollection(
|
|
555
|
-
|
|
548
|
+
process_data_list(data_response[0]["processdata"])
|
|
556
549
|
)
|
|
557
550
|
}
|
|
558
551
|
|
|
@@ -562,7 +555,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
562
555
|
for mid, pids in module_id.items():
|
|
563
556
|
# the json encoder expects that iterables are either list or tuples,
|
|
564
557
|
# other types has to be converted
|
|
565
|
-
if isinstance(pids, list
|
|
558
|
+
if isinstance(pids, (list, tuple)):
|
|
566
559
|
request.append(dict(moduleid=mid, processdataids=pids))
|
|
567
560
|
else:
|
|
568
561
|
request.append(dict(moduleid=mid, processdataids=list(pids)))
|
|
@@ -574,7 +567,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
574
567
|
data_response = await resp.json()
|
|
575
568
|
return {
|
|
576
569
|
x["moduleid"]: ProcessDataCollection(
|
|
577
|
-
|
|
570
|
+
process_data_list(x["processdata"])
|
|
578
571
|
)
|
|
579
572
|
for x in data_response
|
|
580
573
|
}
|
|
@@ -588,9 +581,9 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
588
581
|
response = await resp.json()
|
|
589
582
|
result: Dict[str, List[SettingsData]] = {}
|
|
590
583
|
for module in response:
|
|
591
|
-
|
|
592
|
-
data =
|
|
593
|
-
result[
|
|
584
|
+
mid = module["moduleid"]
|
|
585
|
+
data = [SettingsData(**x) for x in module["settings"]]
|
|
586
|
+
result[mid] = data
|
|
594
587
|
|
|
595
588
|
return result
|
|
596
589
|
|
|
@@ -599,30 +592,26 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
599
592
|
self,
|
|
600
593
|
module_id: str,
|
|
601
594
|
setting_id: str,
|
|
602
|
-
) -> Mapping[str, Mapping[str, str]]:
|
|
603
|
-
...
|
|
595
|
+
) -> Mapping[str, Mapping[str, str]]: ...
|
|
604
596
|
|
|
605
597
|
@overload
|
|
606
598
|
async def get_setting_values(
|
|
607
599
|
self,
|
|
608
600
|
module_id: str,
|
|
609
601
|
setting_id: Iterable[str],
|
|
610
|
-
) -> Mapping[str, Mapping[str, str]]:
|
|
611
|
-
...
|
|
602
|
+
) -> Mapping[str, Mapping[str, str]]: ...
|
|
612
603
|
|
|
613
604
|
@overload
|
|
614
605
|
async def get_setting_values(
|
|
615
606
|
self,
|
|
616
607
|
module_id: str,
|
|
617
|
-
) -> Mapping[str, Mapping[str, str]]:
|
|
618
|
-
...
|
|
608
|
+
) -> Mapping[str, Mapping[str, str]]: ...
|
|
619
609
|
|
|
620
610
|
@overload
|
|
621
611
|
async def get_setting_values(
|
|
622
612
|
self,
|
|
623
613
|
module_id: Mapping[str, Iterable[str]],
|
|
624
|
-
) -> Mapping[str, Mapping[str, str]]:
|
|
625
|
-
...
|
|
614
|
+
) -> Mapping[str, Mapping[str, str]]: ...
|
|
626
615
|
|
|
627
616
|
@_relogin
|
|
628
617
|
async def get_setting_values(
|
|
@@ -672,12 +661,11 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
672
661
|
for mid, pids in module_id.items():
|
|
673
662
|
# the json encoder expects that iterables are either list or tuples,
|
|
674
663
|
# other types has to be converted
|
|
675
|
-
if isinstance(pids, list
|
|
664
|
+
if isinstance(pids, (list, tuple)):
|
|
676
665
|
request.append(dict(moduleid=mid, settingids=pids))
|
|
677
666
|
else:
|
|
678
667
|
request.append(dict(moduleid=mid, settingids=list(pids)))
|
|
679
668
|
|
|
680
|
-
|
|
681
669
|
async with self._session_request(
|
|
682
670
|
"settings", method="POST", json=request
|
|
683
671
|
) as resp:
|
|
@@ -696,7 +684,7 @@ class ApiClient(contextlib.AbstractAsyncContextManager):
|
|
|
696
684
|
request = [
|
|
697
685
|
{
|
|
698
686
|
"moduleid": module_id,
|
|
699
|
-
"settings":
|
|
687
|
+
"settings": [dict(value=v, id=k) for k, v in values.items()],
|
|
700
688
|
}
|
|
701
689
|
]
|
|
702
690
|
async with self._session_request(
|