pykoplenti 1.2.2__tar.gz → 1.3.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pykoplenti
3
- Version: 1.2.2
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.7
17
- Classifier: Programming Language :: Python :: 3.8
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~=1.10
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"
@@ -155,9 +157,11 @@ await client.login(my_master_key, service_code=my_service_code)
155
157
  - [click](https://click.palletsprojects.com/) - command line interface framework
156
158
  - [black](https://github.com/psf/black) - Python code formatter
157
159
  - [ruff](https://github.com/astral-sh/ruff) - Python linter
160
+ - [pydantic](https://docs.pydantic.dev/latest/) - Data validation library
158
161
  - [pytest](https://docs.pytest.org/) - Python test framework
159
162
  - [mypy](https://mypy-lang.org/) - Python type checker
160
163
  - [setuptools](https://github.com/pypa/setuptools) - Python packager
164
+ - [tox](https://tox.wiki) - Automate testing
161
165
 
162
166
  ## License
163
167
 
@@ -128,9 +128,11 @@ await client.login(my_master_key, service_code=my_service_code)
128
128
  - [click](https://click.palletsprojects.com/) - command line interface framework
129
129
  - [black](https://github.com/psf/black) - Python code formatter
130
130
  - [ruff](https://github.com/astral-sh/ruff) - Python linter
131
+ - [pydantic](https://docs.pydantic.dev/latest/) - Data validation library
131
132
  - [pytest](https://docs.pytest.org/) - Python test framework
132
133
  - [mypy](https://mypy-lang.org/) - Python type checker
133
134
  - [setuptools](https://github.com/pypa/setuptools) - Python packager
135
+ - [tox](https://tox.wiki) - Automate testing
134
136
 
135
137
  ## License
136
138
 
@@ -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([a ^ b for a, b in zip(client_key, client_signature)])
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 != 200:
347
- try:
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
- if resp.status == 401:
357
- raise NotAuthorizedException(resp.status, error)
358
-
359
- if resp.status == 403:
360
- raise UserLockedException(resp.status, error)
362
+ try:
363
+ response = await resp.json()
364
+ error = response["message"]
365
+ except Exception:
366
+ error = None
361
367
 
362
- if resp.status == 404:
363
- raise ModuleNotFoundException(resp.status, error)
368
+ if resp.status == 400:
369
+ raise AuthenticationException(resp.status, error)
364
370
 
365
- if resp.status == 503:
366
- raise InternalCommunicationException(resp.status, error)
371
+ if resp.status == 401:
372
+ raise NotAuthorizedException(resp.status, error)
367
373
 
368
- # we got an undocumented status code
369
- raise ApiException(f"Unknown API response [{resp.status}] - {error}")
374
+ if resp.status == 403:
375
+ raise UserLockedException(resp.status, error)
370
376
 
371
- @staticmethod
372
- def _relogin(fn):
373
- """Decorator for automatic re-login if session was expired."""
377
+ if resp.status == 404:
378
+ raise ModuleNotFoundException(resp.status, error)
374
379
 
375
- @functools.wraps(fn)
376
- async def _wrapper(self, *args, **kwargs):
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
- _logger.debug("Request failed - try to re-login")
383
- await self._login()
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[0:2].lower()
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
- parse_obj_as(list[ProcessData], data_response[0]["processdata"])
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
- parse_obj_as(list[ProcessData], data_response[0]["processdata"])
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
- parse_obj_as(list[ProcessData], data_response[0]["processdata"])
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) or isinstance(pids, tuple):
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
- parse_obj_as(List[ProcessData], x["processdata"])
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
- id = module["moduleid"]
592
- data = list([SettingsData(**x) for x in module["settings"]])
593
- result[id] = data
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) or isinstance(pids, tuple):
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": list([dict(value=v, id=k) for k, v in values.items()]),
687
+ "settings": [dict(value=v, id=k) for k, v in values.items()],
700
688
  }
701
689
  ]
702
690
  async with self._session_request(
@@ -236,7 +236,7 @@ def read_events(global_args, lang, count):
236
236
  for event in data:
237
237
  print(
238
238
  f"{event.is_active < 5} {event.start_time} {event.end_time} "
239
- "{event.description}"
239
+ f"{event.description}"
240
240
  )
241
241
 
242
242
  asyncio.run(
@@ -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 | None
81
- max: str | None
82
- default: str | None
81
+ min: Optional[str]
82
+ max: Optional[str]
83
+ default: Optional[str]
83
84
  access: str
84
- unit: str | None
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.2.2
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.7
17
- Classifier: Programming Language :: Python :: 3.8
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~=1.10
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"
@@ -155,9 +157,11 @@ await client.login(my_master_key, service_code=my_service_code)
155
157
  - [click](https://click.palletsprojects.com/) - command line interface framework
156
158
  - [black](https://github.com/psf/black) - Python code formatter
157
159
  - [ruff](https://github.com/astral-sh/ruff) - Python linter
160
+ - [pydantic](https://docs.pydantic.dev/latest/) - Data validation library
158
161
  - [pytest](https://docs.pytest.org/) - Python test framework
159
162
  - [mypy](https://mypy-lang.org/) - Python type checker
160
163
  - [setuptools](https://github.com/pypa/setuptools) - Python packager
164
+ - [tox](https://tox.wiki) - Automate testing
161
165
 
162
166
  ## License
163
167
 
@@ -1,6 +1,6 @@
1
1
  aiohttp~=3.8
2
2
  pycryptodome~=3.19
3
- pydantic~=1.10
3
+ pydantic>=1.10
4
4
 
5
5
  [CLI]
6
6
  prompt_toolkit>=3.0
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = pykoplenti
3
- version = 1.2.2
3
+ version = 1.3.0
4
4
  description = Python REST-Client for Kostal Plenticore Solar Inverters
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -17,8 +17,10 @@ classifiers =
17
17
  Intended Audience :: Developers
18
18
  License :: OSI Approved :: Apache Software License
19
19
  Programming Language :: Python :: 3
20
- Programming Language :: Python :: 3.7
21
- Programming Language :: Python :: 3.8
20
+ Programming Language :: Python :: 3.9
21
+ Programming Language :: Python :: 3.10
22
+ Programming Language :: Python :: 3.11
23
+ Programming Language :: Python :: 3.12
22
24
  Topic :: Software Development :: Libraries
23
25
 
24
26
  [options]
@@ -26,7 +28,7 @@ packages = pykoplenti
26
28
  install_requires =
27
29
  aiohttp ~= 3.8
28
30
  pycryptodome ~= 3.19
29
- pydantic ~= 1.10
31
+ pydantic >= 1.10
30
32
 
31
33
  [options.package_data]
32
34
  pykoplenti = py.typed
@@ -1,4 +1,4 @@
1
- from typing import Any, Callable, Iterable
1
+ from typing import Any, Callable, Iterable, Union
2
2
  from unittest.mock import ANY, MagicMock, call
3
3
 
4
4
  import pytest
@@ -40,7 +40,9 @@ class TestVirtualProcessDataValuesDcSum:
40
40
  async def test_virtual_process_data(
41
41
  self,
42
42
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
43
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
43
+ client_response_factory: Callable[
44
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
45
+ ],
44
46
  websession: MagicMock,
45
47
  ):
46
48
  """Test virtual process data for PV power if depencies are present."""
@@ -70,7 +72,9 @@ class TestVirtualProcessDataValuesDcSum:
70
72
  async def test_virtual_process_data_value(
71
73
  self,
72
74
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
73
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
75
+ client_response_factory: Callable[
76
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
77
+ ],
74
78
  websession: MagicMock,
75
79
  ):
76
80
  """Test virtual process data for PV power."""
@@ -142,7 +146,9 @@ class TestVirtualProcessDataValuesEnergyToGrid:
142
146
  async def test_virtual_process_data(
143
147
  self,
144
148
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
145
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
149
+ client_response_factory: Callable[
150
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
151
+ ],
146
152
  websession: MagicMock,
147
153
  scope: str,
148
154
  ):
@@ -183,7 +189,9 @@ class TestVirtualProcessDataValuesEnergyToGrid:
183
189
  async def test_virtual_process_data_value(
184
190
  self,
185
191
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
186
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
192
+ client_response_factory: Callable[
193
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
194
+ ],
187
195
  websession: MagicMock,
188
196
  scope: str,
189
197
  ):
@@ -269,10 +277,12 @@ class TestVirtualProcessDataValuesEnergyToGrid:
269
277
  @pytest.mark.asyncio
270
278
  async def test_virtual_process_data_no_dc_sum(
271
279
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
272
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
280
+ client_response_factory: Callable[
281
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
282
+ ],
273
283
  websession: MagicMock,
274
284
  ):
275
- """Test if no virtual process data is present if dependecies are missing."""
285
+ """Test if no virtual process data is present if dependencies are missing."""
276
286
  client_response_factory(
277
287
  200,
278
288
  [
@@ -297,7 +307,9 @@ async def test_virtual_process_data_no_dc_sum(
297
307
  @pytest.mark.asyncio
298
308
  async def test_virtual_process_data_and_normal_process_data(
299
309
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
300
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
310
+ client_response_factory: Callable[
311
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
312
+ ],
301
313
  websession: MagicMock,
302
314
  ):
303
315
  """Test if virtual and non-virtual process values can be requested."""
@@ -362,7 +374,9 @@ async def test_virtual_process_data_and_normal_process_data(
362
374
  @pytest.mark.asyncio
363
375
  async def test_virtual_process_data_not_all_requested(
364
376
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
365
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
377
+ client_response_factory: Callable[
378
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
379
+ ],
366
380
  websession: MagicMock,
367
381
  ):
368
382
  """Test if not all available virtual process data are requested."""
@@ -429,7 +443,9 @@ async def test_virtual_process_data_not_all_requested(
429
443
  @pytest.mark.asyncio
430
444
  async def test_virtual_process_data_multiple_requested(
431
445
  pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
432
- client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
446
+ client_response_factory: Callable[
447
+ [int, Union[list[Any], dict[Any, Any]]], MagicMock
448
+ ],
433
449
  websession: MagicMock,
434
450
  ):
435
451
  """Test if multiple virtual process data are requested."""
@@ -3,7 +3,6 @@ import json
3
3
  from typing import Any, Callable
4
4
  from unittest.mock import ANY, MagicMock
5
5
 
6
- from pydantic import parse_obj_as
7
6
  import pytest
8
7
 
9
8
  import pykoplenti
@@ -123,13 +122,25 @@ def test_settings_parsing():
123
122
  assert settings_data.access == "readonly"
124
123
 
125
124
 
125
+ def test_process_data_list():
126
+ json = [
127
+ {"id": "Statistic:Yield:Day", "unit": "%", "value": 1},
128
+ {"id": "Statistic:Yield:Month", "unit": "%", "value": 2},
129
+ ]
130
+
131
+ assert pykoplenti.model.process_data_list(json) == [
132
+ pykoplenti.ProcessData(id="Statistic:Yield:Day", unit="%", value="1"),
133
+ pykoplenti.ProcessData(id="Statistic:Yield:Month", unit="%", value="2"),
134
+ ]
135
+
136
+
126
137
  def test_process_data_collection_indicates_length():
127
138
  raw_response = (
128
139
  '[{"id": "Statistic:Yield:Day", "unit": "", "value": 1}, '
129
140
  '{"id": "Statistic:Yield:Month", "unit": "", "value": 2}]'
130
141
  )
131
142
  pdc = pykoplenti.ProcessDataCollection(
132
- parse_obj_as(list[pykoplenti.ProcessData], json.loads(raw_response))
143
+ pykoplenti.model.process_data_list(json.loads(raw_response))
133
144
  )
134
145
 
135
146
  assert len(pdc) == 2
@@ -141,7 +152,7 @@ def test_process_data_collection_index_returns_processdata():
141
152
  '{"id": "Statistic:Yield:Month", "unit": "", "value": 2}]'
142
153
  )
143
154
  pdc = pykoplenti.ProcessDataCollection(
144
- parse_obj_as(list[pykoplenti.ProcessData], json.loads(raw_response))
155
+ pykoplenti.model.process_data_list(json.loads(raw_response))
145
156
  )
146
157
 
147
158
  result = pdc["Statistic:Yield:Month"]
@@ -158,7 +169,7 @@ def test_process_data_collection_can_be_iterated():
158
169
  '{"id": "Statistic:Yield:Month", "unit": "", "value": 2}]'
159
170
  )
160
171
  pdc = pykoplenti.ProcessDataCollection(
161
- parse_obj_as(list[pykoplenti.ProcessData], json.loads(raw_response))
172
+ pykoplenti.model.process_data_list(json.loads(raw_response))
162
173
  )
163
174
 
164
175
  result = list(pdc)
File without changes
File without changes
File without changes