pykoplenti 1.2.1__py3-none-any.whl → 1.3.0__py3-none-any.whl
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/api.py +55 -67
- pykoplenti/cli.py +1 -1
- pykoplenti/model.py +28 -6
- {pykoplenti-1.2.1.dist-info → pykoplenti-1.3.0.dist-info}/METADATA +10 -5
- pykoplenti-1.3.0.dist-info/RECORD +12 -0
- {pykoplenti-1.2.1.dist-info → pykoplenti-1.3.0.dist-info}/WHEEL +1 -1
- pykoplenti-1.2.1.dist-info/RECORD +0 -12
- {pykoplenti-1.2.1.dist-info → pykoplenti-1.3.0.dist-info}/LICENSE +0 -0
- {pykoplenti-1.2.1.dist-info → pykoplenti-1.3.0.dist-info}/entry_points.txt +0 -0
- {pykoplenti-1.2.1.dist-info → pykoplenti-1.3.0.dist-info}/top_level.txt +0 -0
pykoplenti/api.py
CHANGED
|
@@ -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(
|
pykoplenti/cli.py
CHANGED
pykoplenti/model.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import Iterator, Mapping
|
|
2
|
+
from typing import Final, Iterator, Mapping, Optional
|
|
3
3
|
|
|
4
|
+
import pydantic
|
|
4
5
|
from pydantic import BaseModel, Field
|
|
5
6
|
|
|
6
7
|
|
|
@@ -55,7 +56,7 @@ class ProcessDataCollection(Mapping):
|
|
|
55
56
|
try:
|
|
56
57
|
return next(x for x in self._process_data if x.id == item)
|
|
57
58
|
except StopIteration:
|
|
58
|
-
raise KeyError(item)
|
|
59
|
+
raise KeyError(item) from None
|
|
59
60
|
|
|
60
61
|
def __eq__(self, __other: object) -> bool:
|
|
61
62
|
if not isinstance(__other, ProcessDataCollection):
|
|
@@ -77,11 +78,11 @@ class ProcessDataCollection(Mapping):
|
|
|
77
78
|
class SettingsData(BaseModel):
|
|
78
79
|
"""Represents a single settings data."""
|
|
79
80
|
|
|
80
|
-
min: str
|
|
81
|
-
max: str
|
|
82
|
-
default: str
|
|
81
|
+
min: Optional[str]
|
|
82
|
+
max: Optional[str]
|
|
83
|
+
default: Optional[str]
|
|
83
84
|
access: str
|
|
84
|
-
unit: str
|
|
85
|
+
unit: Optional[str]
|
|
85
86
|
id: str
|
|
86
87
|
type: str
|
|
87
88
|
|
|
@@ -97,3 +98,24 @@ class EventData(BaseModel):
|
|
|
97
98
|
description: str
|
|
98
99
|
group: str
|
|
99
100
|
is_active: bool
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# pydantic version specific code
|
|
104
|
+
# In pydantic 2.x `parse_obj_as` is no longer supported. To stay compatible to
|
|
105
|
+
# both version a small wrapper function is used.
|
|
106
|
+
|
|
107
|
+
if pydantic.VERSION.startswith("2."):
|
|
108
|
+
from pydantic import TypeAdapter
|
|
109
|
+
|
|
110
|
+
_process_list_adapter: Final = TypeAdapter(list[ProcessData])
|
|
111
|
+
|
|
112
|
+
def process_data_list(json) -> list[ProcessData]:
|
|
113
|
+
"""Process json as a list of ProcessData objects."""
|
|
114
|
+
return _process_list_adapter.validate_python(json)
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
from pydantic import parse_obj_as
|
|
118
|
+
|
|
119
|
+
def process_data_list(json) -> list[ProcessData]:
|
|
120
|
+
"""Process json as a list of ProcessData objects."""
|
|
121
|
+
return parse_obj_as(list[ProcessData], json)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pykoplenti
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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,14 +13,16 @@ 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
|
-
Requires-Dist: aiohttp ~=3.8
|
|
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
28
|
Requires-Dist: click >=7.1 ; extra == 'cli'
|
|
@@ -147,6 +149,7 @@ await client.login(my_master_key, service_code=my_service_code)
|
|
|
147
149
|
- [Command Line Interface](doc/command_line.md)
|
|
148
150
|
- [Examples](examples/)
|
|
149
151
|
- [Virtual Process Data](doc/virtual_process_data.md)
|
|
152
|
+
- [Notes about Process Data](doc/process_data.md)
|
|
150
153
|
|
|
151
154
|
## Built With
|
|
152
155
|
|
|
@@ -154,9 +157,11 @@ await client.login(my_master_key, service_code=my_service_code)
|
|
|
154
157
|
- [click](https://click.palletsprojects.com/) - command line interface framework
|
|
155
158
|
- [black](https://github.com/psf/black) - Python code formatter
|
|
156
159
|
- [ruff](https://github.com/astral-sh/ruff) - Python linter
|
|
160
|
+
- [pydantic](https://docs.pydantic.dev/latest/) - Data validation library
|
|
157
161
|
- [pytest](https://docs.pytest.org/) - Python test framework
|
|
158
162
|
- [mypy](https://mypy-lang.org/) - Python type checker
|
|
159
163
|
- [setuptools](https://github.com/pypa/setuptools) - Python packager
|
|
164
|
+
- [tox](https://tox.wiki) - Automate testing
|
|
160
165
|
|
|
161
166
|
## License
|
|
162
167
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pykoplenti/__init__.py,sha256=w9ooy6_JaT9lGvMDAaBHyvYu1uTFTkb1g21WRxp3Kzw,756
|
|
2
|
+
pykoplenti/api.py,sha256=ViT25KQt3caH78fO30G014IawcWQ1lMvy28M9QLBypg,26111
|
|
3
|
+
pykoplenti/cli.py,sha256=KIgpQ1QdM9_nz7S-oQpfpU6ov3o8N8tobw9_R4X0_Nw,12973
|
|
4
|
+
pykoplenti/extended.py,sha256=_MCDtP-6BaAiTFqJ44CLK_Ihkh6nCC0vWWv1lsQfEFs,9311
|
|
5
|
+
pykoplenti/model.py,sha256=lFOHDJvWyhOdQrcoun6HeT-4XaGY5I2gy1j6M5u3u6A,3016
|
|
6
|
+
pykoplenti/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
pykoplenti-1.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
8
|
+
pykoplenti-1.3.0.dist-info/METADATA,sha256=llPxFNe-fpRJN5cIrWt-T2EOT3PpCt1T_BqQUZw4kI8,5847
|
|
9
|
+
pykoplenti-1.3.0.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
10
|
+
pykoplenti-1.3.0.dist-info/entry_points.txt,sha256=Ix-p1uzKNyOMTK0TOlAC0nsfWS6ATXVuaNDJ5-TwBJw,56
|
|
11
|
+
pykoplenti-1.3.0.dist-info/top_level.txt,sha256=Bi915FGIFYzCujwn5Kwhu3B-sxElgc7gX3gNaYjl4j8,11
|
|
12
|
+
pykoplenti-1.3.0.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pykoplenti/__init__.py,sha256=w9ooy6_JaT9lGvMDAaBHyvYu1uTFTkb1g21WRxp3Kzw,756
|
|
2
|
-
pykoplenti/api.py,sha256=gYWWd8yZ9winj7qPt1-rzx4stX_DLPfy5nOduuamtH4,26431
|
|
3
|
-
pykoplenti/cli.py,sha256=LAiQHlSgoJz07kTtFh0bNyahyYz7gCenhRfradex5wE,12972
|
|
4
|
-
pykoplenti/extended.py,sha256=_MCDtP-6BaAiTFqJ44CLK_Ihkh6nCC0vWWv1lsQfEFs,9311
|
|
5
|
-
pykoplenti/model.py,sha256=g-KyYTF1M1p6OAebyA74OAP_-561u6Hylhgy_jnpMto,2266
|
|
6
|
-
pykoplenti/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
pykoplenti-1.2.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
8
|
-
pykoplenti-1.2.1.dist-info/METADATA,sha256=ztvYvxRUUOWJYIbUJ4icUf1X14C_55BJMoA6GiN3ABI,5577
|
|
9
|
-
pykoplenti-1.2.1.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
|
10
|
-
pykoplenti-1.2.1.dist-info/entry_points.txt,sha256=Ix-p1uzKNyOMTK0TOlAC0nsfWS6ATXVuaNDJ5-TwBJw,56
|
|
11
|
-
pykoplenti-1.2.1.dist-info/top_level.txt,sha256=Bi915FGIFYzCujwn5Kwhu3B-sxElgc7gX3gNaYjl4j8,11
|
|
12
|
-
pykoplenti-1.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|