pypck 0.8.9__tar.gz → 0.8.11__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 (30) hide show
  1. {pypck-0.8.9/pypck.egg-info → pypck-0.8.11}/PKG-INFO +1 -1
  2. pypck-0.8.11/VERSION +1 -0
  3. {pypck-0.8.9 → pypck-0.8.11}/pypck/connection.py +4 -4
  4. {pypck-0.8.9 → pypck-0.8.11}/pypck/helpers.py +3 -3
  5. {pypck-0.8.9 → pypck-0.8.11}/pypck/lcn_defs.py +46 -44
  6. pypck-0.8.11/pypck/py.typed +0 -0
  7. {pypck-0.8.9 → pypck-0.8.11}/pypck/request_handlers.py +2 -1
  8. {pypck-0.8.9 → pypck-0.8.11/pypck.egg-info}/PKG-INFO +1 -1
  9. {pypck-0.8.9 → pypck-0.8.11}/pypck.egg-info/SOURCES.txt +2 -0
  10. {pypck-0.8.9 → pypck-0.8.11}/pyproject.toml +3 -0
  11. {pypck-0.8.9 → pypck-0.8.11}/tests/test_commands.py +20 -16
  12. pypck-0.8.11/tests/test_connection.py +155 -0
  13. {pypck-0.8.9 → pypck-0.8.11}/tests/test_dyn_text.py +13 -12
  14. pypck-0.8.11/tests/test_input.py +46 -0
  15. {pypck-0.8.9 → pypck-0.8.11}/tests/test_messages.py +6 -1
  16. {pypck-0.8.9 → pypck-0.8.11}/tests/test_vars.py +4 -2
  17. pypck-0.8.9/VERSION +0 -1
  18. pypck-0.8.9/tests/test_connection.py +0 -420
  19. {pypck-0.8.9 → pypck-0.8.11}/LICENSE +0 -0
  20. {pypck-0.8.9 → pypck-0.8.11}/README.md +0 -0
  21. {pypck-0.8.9 → pypck-0.8.11}/pypck/__init__.py +0 -0
  22. {pypck-0.8.9 → pypck-0.8.11}/pypck/inputs.py +0 -0
  23. {pypck-0.8.9 → pypck-0.8.11}/pypck/lcn_addr.py +0 -0
  24. {pypck-0.8.9 → pypck-0.8.11}/pypck/module.py +0 -0
  25. {pypck-0.8.9 → pypck-0.8.11}/pypck/pck_commands.py +0 -0
  26. {pypck-0.8.9 → pypck-0.8.11}/pypck/timeout_retry.py +0 -0
  27. {pypck-0.8.9 → pypck-0.8.11}/pypck.egg-info/dependency_links.txt +0 -0
  28. {pypck-0.8.9 → pypck-0.8.11}/pypck.egg-info/not-zip-safe +0 -0
  29. {pypck-0.8.9 → pypck-0.8.11}/pypck.egg-info/top_level.txt +0 -0
  30. {pypck-0.8.9 → pypck-0.8.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.8.9
3
+ Version: 0.8.11
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
pypck-0.8.11/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.8.11
@@ -13,7 +13,7 @@ from pypck import inputs, lcn_defs
13
13
  from pypck.helpers import TaskRegistry
14
14
  from pypck.lcn_addr import LcnAddr
15
15
  from pypck.lcn_defs import LcnEvent
16
- from pypck.module import AbstractConnection, GroupConnection, ModuleConnection
16
+ from pypck.module import GroupConnection, ModuleConnection
17
17
  from pypck.pck_commands import PckGenerator
18
18
 
19
19
  _LOGGER = logging.getLogger(__name__)
@@ -243,7 +243,7 @@ class PchkConnectionManager:
243
243
  if isinstance(exc, (ConnectionRefusedError, OSError)):
244
244
  raise PchkConnectionRefusedError()
245
245
  else:
246
- raise awaitable.exception() # type: ignore
246
+ raise exc
247
247
 
248
248
  if pending:
249
249
  for awaitable in pending:
@@ -405,8 +405,8 @@ class PchkConnectionManager:
405
405
 
406
406
  def get_address_conn(
407
407
  self, addr: LcnAddr, request_serials: bool = True
408
- ) -> AbstractConnection:
409
- """Create and/or return an AbstractConnection to the given module or group."""
408
+ ) -> ModuleConnection | GroupConnection:
409
+ """Create and/or return a connection to the given module or group."""
410
410
  if addr.is_group:
411
411
  return self.get_group_conn(addr)
412
412
  return self.get_module_conn(addr, request_serials)
@@ -1,7 +1,7 @@
1
1
  """Helper functions for pypck."""
2
2
 
3
3
  import asyncio
4
- from collections.abc import Awaitable
4
+ from collections.abc import Coroutine
5
5
  from typing import Any
6
6
 
7
7
 
@@ -30,9 +30,9 @@ class TaskRegistry:
30
30
  if task in self.tasks:
31
31
  self.tasks.remove(task)
32
32
 
33
- def create_task(self, coro: Awaitable[Any]) -> "asyncio.Task[None]":
33
+ def create_task(self, coro: Coroutine[Any, Any, Any]) -> "asyncio.Task[None]":
34
34
  """Create a task and store a reference in the task registry."""
35
- task = asyncio.create_task(coro) # type: ignore
35
+ task: asyncio.Task[Any] = asyncio.create_task(coro)
36
36
  task.add_done_callback(self.remove_task)
37
37
  self.tasks.append(task)
38
38
  return task
@@ -360,6 +360,44 @@ class Var(Enum):
360
360
  S0INPUT3 = auto()
361
361
  S0INPUT4 = auto() # LCN-BU4LJVarValue
362
362
 
363
+ @classmethod
364
+ def variables(cls) -> list[Var]:
365
+ """Return a list of all variable types."""
366
+ return [
367
+ cls.VAR1ORTVAR,
368
+ cls.VAR2ORR1VAR,
369
+ cls.VAR3ORR2VAR,
370
+ cls.VAR4,
371
+ cls.VAR5,
372
+ cls.VAR6,
373
+ cls.VAR7,
374
+ cls.VAR8,
375
+ cls.VAR9,
376
+ cls.VAR10,
377
+ cls.VAR11,
378
+ cls.VAR12,
379
+ ]
380
+
381
+ @classmethod
382
+ def set_points(cls) -> list[Var]:
383
+ """Return a list of all set-point variable types."""
384
+ return [cls.R1VARSETPOINT, cls.R2VARSETPOINT]
385
+
386
+ @classmethod
387
+ def thresholds(cls) -> list[list[Var]]:
388
+ """Return a list of all threshold variable types."""
389
+ return [
390
+ [cls.THRS1, cls.THRS2, cls.THRS3, cls.THRS4, cls.THRS5],
391
+ [cls.THRS2_1, cls.THRS2_2, cls.THRS2_3, cls.THRS2_4],
392
+ [cls.THRS3_1, cls.THRS3_2, cls.THRS3_3, cls.THRS3_4],
393
+ [cls.THRS4_1, cls.THRS4_2, cls.THRS4_3, cls.THRS4_4],
394
+ ]
395
+
396
+ @classmethod
397
+ def s0s(cls) -> list[Var]:
398
+ """Return a list of all S0-input variable types."""
399
+ return [cls.S0INPUT1, cls.S0INPUT2, cls.S0INPUT3, cls.S0INPUT4]
400
+
363
401
  @staticmethod
364
402
  def var_id_to_var(var_id: int) -> Var:
365
403
  """Translate a given id into a variable type.
@@ -369,9 +407,9 @@ class Var(Enum):
369
407
  :returns: The translated variable enum.
370
408
  :rtype: Var
371
409
  """
372
- if (var_id < 0) or (var_id >= len(Var.variables)): # type: ignore
410
+ if (var_id < 0) or (var_id >= len(Var.variables())):
373
411
  raise ValueError("Bad var_id.")
374
- return Var.variables[var_id] # type: ignore
412
+ return Var.variables()[var_id]
375
413
 
376
414
  @staticmethod
377
415
  def set_point_id_to_var(set_point_id: int) -> Var:
@@ -382,9 +420,9 @@ class Var(Enum):
382
420
  :return: The translated var
383
421
  :rtype: Var
384
422
  """
385
- if (set_point_id < 0) or (set_point_id >= len(Var.set_points)): # type: ignore
423
+ if (set_point_id < 0) or (set_point_id >= len(Var.set_points())):
386
424
  raise ValueError("Bad set_point_id.")
387
- return Var.set_points[set_point_id] # type: ignore
425
+ return Var.set_points()[set_point_id]
388
426
 
389
427
  @staticmethod
390
428
  def thrs_id_to_var(register_id: int, thrs_id: int) -> Var:
@@ -399,12 +437,12 @@ class Var(Enum):
399
437
  """
400
438
  if (
401
439
  (register_id < 0)
402
- or (register_id >= len(Var.thresholds)) # type: ignore
440
+ or (register_id >= len(Var.thresholds()))
403
441
  or (thrs_id < 0)
404
442
  or (thrs_id >= (5 if (register_id == 0) else 4))
405
443
  ):
406
444
  raise ValueError("Bad register_id and/or thrs_id.")
407
- return Var.thresholds[register_id][thrs_id] # type: ignore
445
+ return Var.thresholds()[register_id][thrs_id]
408
446
 
409
447
  @staticmethod
410
448
  def s0_id_to_var(s0_id: int) -> Var:
@@ -415,9 +453,9 @@ class Var(Enum):
415
453
  :return: The translated var
416
454
  :rtype: Var
417
455
  """
418
- if (s0_id < 0) or (s0_id >= len(Var.s0s)): # type: ignore
456
+ if (s0_id < 0) or (s0_id >= len(Var.s0s())):
419
457
  raise ValueError("Bad s0_id.")
420
- return Var.s0s[s0_id] # type: ignore
458
+ return Var.s0s()[s0_id]
421
459
 
422
460
  @staticmethod
423
461
  def to_var_id(var: Var) -> int:
@@ -658,42 +696,6 @@ class Var(Enum):
658
696
  return (not lock_state) and (software_serial < 0x170206)
659
697
 
660
698
 
661
- # Helper list to get var by numeric id.
662
- Var.variables = [ # type: ignore
663
- Var.VAR1ORTVAR,
664
- Var.VAR2ORR1VAR,
665
- Var.VAR3ORR2VAR,
666
- Var.VAR4,
667
- Var.VAR5,
668
- Var.VAR6,
669
- Var.VAR7,
670
- Var.VAR8,
671
- Var.VAR9,
672
- Var.VAR10,
673
- Var.VAR11,
674
- Var.VAR12,
675
- ]
676
-
677
- # Helper list to get set-point var by numeric id.
678
- Var.set_points = [Var.R1VARSETPOINT, Var.R2VARSETPOINT] # type: ignore
679
-
680
- # Helper list to get threshold var by numeric id.
681
- Var.thresholds = [ # type: ignore
682
- [Var.THRS1, Var.THRS2, Var.THRS3, Var.THRS4, Var.THRS5],
683
- [Var.THRS2_1, Var.THRS2_2, Var.THRS2_3, Var.THRS2_4],
684
- [Var.THRS3_1, Var.THRS3_2, Var.THRS3_3, Var.THRS3_4],
685
- [Var.THRS4_1, Var.THRS4_2, Var.THRS4_3, Var.THRS4_4],
686
- ]
687
-
688
- # Helper list to get S0-input var by numeric id.
689
- Var.s0s = [ # type: ignore
690
- Var.S0INPUT1,
691
- Var.S0INPUT2,
692
- Var.S0INPUT3,
693
- Var.S0INPUT4,
694
- ]
695
-
696
-
697
699
  class VarUnit(Enum):
698
700
  """Measurement units used with LCN variables."""
699
701
 
File without changes
@@ -660,6 +660,7 @@ class StatusRequestsHandler:
660
660
  async def activate_all(self, activate_s0: bool = False) -> None:
661
661
  """Activate all status requests."""
662
662
  await self.addr_conn.conn.segment_scan_completed_event.wait()
663
+ var_s0s = lcn_defs.Var.s0s()
663
664
  for item in (
664
665
  list(lcn_defs.OutputPort)
665
666
  + list(lcn_defs.RelayPort)
@@ -673,7 +674,7 @@ class StatusRequestsHandler:
673
674
  if (
674
675
  (not activate_s0)
675
676
  and isinstance(item, lcn_defs.Var)
676
- and (item in lcn_defs.Var.s0s) # type: ignore
677
+ and (item in var_s0s)
677
678
  ):
678
679
  continue
679
680
  await self.activate(item)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.8.9
3
+ Version: 0.8.11
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
@@ -11,6 +11,7 @@ pypck/lcn_addr.py
11
11
  pypck/lcn_defs.py
12
12
  pypck/module.py
13
13
  pypck/pck_commands.py
14
+ pypck/py.typed
14
15
  pypck/request_handlers.py
15
16
  pypck/timeout_retry.py
16
17
  pypck.egg-info/PKG-INFO
@@ -21,5 +22,6 @@ pypck.egg-info/top_level.txt
21
22
  tests/test_commands.py
22
23
  tests/test_connection.py
23
24
  tests/test_dyn_text.py
25
+ tests/test_input.py
24
26
  tests/test_messages.py
25
27
  tests/test_vars.py
@@ -38,6 +38,9 @@ version = {file = "VERSION"}
38
38
  [tool.setuptools.packages.find]
39
39
  include = ["pypck*"]
40
40
 
41
+ [tool.setuptools.package-data]
42
+ mypkg = ["py.typed"]
43
+
41
44
  [tool.black]
42
45
  target-version = ["py311", "py312", "py313"]
43
46
 
@@ -1,5 +1,7 @@
1
1
  """Tests for command generation directed at bus modules and groups."""
2
2
 
3
+ from typing import Any
4
+
3
5
  import pytest
4
6
 
5
7
  from pypck.lcn_addr import LcnAddr
@@ -24,7 +26,7 @@ from pypck.pck_commands import PckGenerator
24
26
 
25
27
  NEW_VAR_SW_AGE = 0x170206
26
28
 
27
- COMMANDS = {
29
+ COMMANDS: dict[str | bytes, Any] = {
28
30
  # Host commands
29
31
  **{
30
32
  f"^ping{counter:d}": (PckGenerator.ping, counter)
@@ -93,7 +95,7 @@ COMMANDS = {
93
95
  var,
94
96
  NEW_VAR_SW_AGE,
95
97
  )
96
- for var in Var.variables # type: ignore
98
+ for var in Var.variables()
97
99
  },
98
100
  **{
99
101
  f"MWS{Var.to_set_point_id(var) + 1:03d}": (
@@ -101,7 +103,7 @@ COMMANDS = {
101
103
  var,
102
104
  NEW_VAR_SW_AGE,
103
105
  )
104
- for var in Var.set_points # type: ignore
106
+ for var in Var.set_points()
105
107
  },
106
108
  **{
107
109
  f"MWC{Var.to_s0_id(var) + 1:03d}": (
@@ -109,7 +111,7 @@ COMMANDS = {
109
111
  var,
110
112
  NEW_VAR_SW_AGE,
111
113
  )
112
- for var in Var.s0s # type: ignore
114
+ for var in Var.s0s()
113
115
  },
114
116
  **{
115
117
  f"SE{Var.to_thrs_register_id(var) + 1:03d}": (
@@ -117,7 +119,7 @@ COMMANDS = {
117
119
  var,
118
120
  NEW_VAR_SW_AGE,
119
121
  )
120
- for reg in Var.thresholds # type: ignore
122
+ for reg in Var.thresholds()
121
123
  for var in reg
122
124
  },
123
125
  # Variable status (legacy commands)
@@ -128,7 +130,7 @@ COMMANDS = {
128
130
  "MWSB": (PckGenerator.request_var_status, Var.R2VARSETPOINT, NEW_VAR_SW_AGE - 1),
129
131
  **{
130
132
  "SL1": (PckGenerator.request_var_status, var, NEW_VAR_SW_AGE - 1)
131
- for var in Var.thresholds[0] # type: ignore
133
+ for var in Var.thresholds()[0]
132
134
  },
133
135
  # Output manipulation
134
136
  **{
@@ -305,7 +307,7 @@ COMMANDS = {
305
307
  # Variable manipulation
306
308
  **{
307
309
  f"X2{var.value | 0x40:03d}016225": (PckGenerator.update_status_var, var, 4321)
308
- for var in Var.variables # type: ignore
310
+ for var in Var.variables()
309
311
  },
310
312
  "X2030044129": (PckGenerator.var_abs, Var.R1VARSETPOINT, 4201),
311
313
  "X2030108129": (PckGenerator.var_abs, Var.R2VARSETPOINT, 4201),
@@ -314,7 +316,7 @@ COMMANDS = {
314
316
  "ZS30000": (PckGenerator.var_reset, Var.TVAR, 0x170205),
315
317
  **{
316
318
  f"Z-{var.value + 1:03d}4090": (PckGenerator.var_reset, var, 0x170206)
317
- for var in Var.variables # type: ignore
319
+ for var in Var.variables()
318
320
  },
319
321
  "ZA23423": (PckGenerator.var_rel, Var.TVAR, RelVarRef.CURRENT, 23423, 0x170205),
320
322
  "ZS23423": (PckGenerator.var_rel, Var.TVAR, RelVarRef.CURRENT, -23423, 0x170205),
@@ -326,7 +328,7 @@ COMMANDS = {
326
328
  -3000,
327
329
  0x170206,
328
330
  )
329
- for var in Var.variables # type: ignore
331
+ for var in Var.variables()
330
332
  if var != Var.TVAR
331
333
  },
332
334
  **{
@@ -337,7 +339,7 @@ COMMANDS = {
337
339
  -500,
338
340
  sw_age,
339
341
  )
340
- for nvar, var in enumerate(Var.set_points) # type: ignore
342
+ for nvar, var in enumerate(Var.set_points())
341
343
  for nref, ref in enumerate(RelVarRef)
342
344
  for sw_age in (0x170206, 0x170205)
343
345
  },
@@ -349,14 +351,14 @@ COMMANDS = {
349
351
  500,
350
352
  sw_age,
351
353
  )
352
- for nvar, var in enumerate(Var.set_points) # type: ignore
354
+ for nvar, var in enumerate(Var.set_points())
353
355
  for nref, ref in enumerate(RelVarRef)
354
356
  for sw_age in (0x170206, 0x170205)
355
357
  },
356
358
  **{
357
359
  f"SS{('R', 'E')[nref]}0500SR{r + 1}{i + 1}": (
358
360
  PckGenerator.var_rel,
359
- Var.thresholds[r][i], # type: ignore
361
+ Var.thresholds()[r][i],
360
362
  ref,
361
363
  -500,
362
364
  0x170206,
@@ -368,7 +370,7 @@ COMMANDS = {
368
370
  **{
369
371
  f"SS{('R', 'E')[nref]}0500AR{r + 1}{i + 1}": (
370
372
  PckGenerator.var_rel,
371
- Var.thresholds[r][i], # type: ignore
373
+ Var.thresholds()[r][i],
372
374
  ref,
373
375
  500,
374
376
  0x170206,
@@ -380,7 +382,7 @@ COMMANDS = {
380
382
  **{
381
383
  f"SS{('R', 'E')[nref]}0500S{1 << (4 - i):05b}": (
382
384
  PckGenerator.var_rel,
383
- Var.thresholds[0][i], # type: ignore
385
+ Var.thresholds()[0][i],
384
386
  ref,
385
387
  -500,
386
388
  0x170205,
@@ -391,7 +393,7 @@ COMMANDS = {
391
393
  **{
392
394
  f"SS{('R', 'E')[nref]}0500A{1 << (4 - i):05b}": (
393
395
  PckGenerator.var_rel,
394
- Var.thresholds[0][i], # type: ignore
396
+ Var.thresholds()[0][i],
395
397
  ref,
396
398
  500,
397
399
  0x170205,
@@ -562,6 +564,8 @@ COMMANDS = {
562
564
 
563
565
 
564
566
  @pytest.mark.parametrize("expected, command", COMMANDS.items())
565
- def test_command_generation_single_mod_noack(expected, command):
567
+ def test_command_generation_single_mod_noack(
568
+ expected: str, command: tuple[Any, ...]
569
+ ) -> None:
566
570
  """Test if InputMod parses message correctly."""
567
571
  assert expected == command[0](*command[1:])
@@ -0,0 +1,155 @@
1
+ """Connection tests."""
2
+
3
+ import asyncio
4
+ from unittest.mock import AsyncMock, Mock, call, patch
5
+
6
+ import pytest
7
+
8
+ from pypck import inputs
9
+ from pypck.connection import (
10
+ PchkAuthenticationError,
11
+ PchkConnectionFailedError,
12
+ PchkConnectionManager,
13
+ PchkConnectionRefusedError,
14
+ PchkLicenseError,
15
+ )
16
+ from pypck.lcn_defs import LcnEvent
17
+ from pypck.pck_commands import PckGenerator
18
+
19
+ from .conftest import HOST, PASSWORD, PORT, USERNAME, MockPchkConnectionManager
20
+
21
+
22
+ async def test_close_without_connect(pypck_client: MockPchkConnectionManager) -> None:
23
+ """Test closing of PchkConnectionManager without connecting."""
24
+ await pypck_client.async_close()
25
+
26
+
27
+ @patch.object(PchkConnectionManager, "open_connection")
28
+ @patch.object(PchkConnectionManager, "scan_segment_couplers")
29
+ async def test_async_connect(
30
+ mock_scan_segment_couplers: AsyncMock,
31
+ mock_open_connection: AsyncMock,
32
+ ) -> None:
33
+ """Test successful connection."""
34
+ pypck_client = PchkConnectionManager(HOST, PORT, USERNAME, PASSWORD)
35
+ connect_task = asyncio.create_task(pypck_client.async_connect())
36
+ await asyncio.sleep(0)
37
+
38
+ pypck_client.license_error_future.set_result(True)
39
+ pypck_client.authentication_completed_future.set_result(True)
40
+ pypck_client.segment_scan_completed_event.set()
41
+
42
+ await connect_task
43
+
44
+ mock_scan_segment_couplers.assert_awaited()
45
+ mock_open_connection.assert_awaited()
46
+ assert pypck_client.is_ready()
47
+
48
+
49
+ @patch.object(PchkConnectionManager, "ping")
50
+ @patch.object(PchkConnectionManager, "open_connection")
51
+ @patch.object(PchkConnectionManager, "scan_segment_couplers")
52
+ @patch.object(PchkConnectionManager, "send_command")
53
+ async def test_successful_connection_procedure(
54
+ mock_send_command: AsyncMock,
55
+ mock_scan_segment_couplers: AsyncMock,
56
+ mock_open_connection: AsyncMock,
57
+ mock_ping: AsyncMock,
58
+ ) -> None:
59
+ """Test successful connection procedure."""
60
+ pypck_client = PchkConnectionManager(HOST, PORT, USERNAME, PASSWORD)
61
+ connect_task = asyncio.create_task(pypck_client.async_connect())
62
+ await asyncio.sleep(0)
63
+
64
+ await pypck_client.async_process_input(inputs.AuthUsername())
65
+ mock_send_command.assert_awaited_with(USERNAME, to_host=True)
66
+
67
+ await pypck_client.async_process_input(inputs.AuthPassword())
68
+ mock_send_command.assert_awaited_with(PASSWORD, to_host=True)
69
+
70
+ await pypck_client.async_process_input(inputs.AuthOk())
71
+ mock_send_command.assert_awaited_with(PckGenerator.set_dec_mode(), to_host=True)
72
+ assert pypck_client.authentication_completed_future.result()
73
+
74
+ await pypck_client.async_process_input(inputs.DecModeSet())
75
+ mock_send_command.assert_awaited_with(
76
+ PckGenerator.set_operation_mode(
77
+ pypck_client.dim_mode, pypck_client.status_mode
78
+ ),
79
+ to_host=True,
80
+ )
81
+ assert pypck_client.license_error_future.result()
82
+
83
+ await connect_task
84
+
85
+ mock_open_connection.assert_awaited()
86
+ mock_scan_segment_couplers.assert_awaited()
87
+ mock_ping.assert_awaited()
88
+
89
+
90
+ @pytest.mark.parametrize("side_effect", [ConnectionRefusedError, OSError])
91
+ async def test_connection_error(side_effect: ConnectionRefusedError | OSError) -> None:
92
+ """Test connection error."""
93
+ with (
94
+ patch.object(PchkConnectionManager, "open_connection", side_effect=side_effect),
95
+ pytest.raises(PchkConnectionRefusedError),
96
+ ):
97
+ pypck_client = PchkConnectionManager(HOST, PORT, USERNAME, PASSWORD)
98
+ await pypck_client.async_connect()
99
+
100
+
101
+ @patch.object(PchkConnectionManager, "open_connection")
102
+ async def test_authentication_error(mock_open_connection: AsyncMock) -> None:
103
+ """Test wrong login credentials."""
104
+ pypck_client = PchkConnectionManager(HOST, PORT, USERNAME, PASSWORD)
105
+ connect_task = asyncio.create_task(pypck_client.async_connect())
106
+ await asyncio.sleep(0)
107
+ await pypck_client.async_process_input(inputs.AuthFailed())
108
+
109
+ with (
110
+ pytest.raises(PchkAuthenticationError),
111
+ ):
112
+ await connect_task
113
+
114
+
115
+ @patch.object(PchkConnectionManager, "open_connection")
116
+ async def test_license_error(mock_open_connection: AsyncMock) -> None:
117
+ """Test wrong login credentials."""
118
+ pypck_client = PchkConnectionManager(HOST, PORT, USERNAME, PASSWORD)
119
+ connect_task = asyncio.create_task(pypck_client.async_connect())
120
+ await asyncio.sleep(0)
121
+ await pypck_client.async_process_input(inputs.LicenseError())
122
+
123
+ with (
124
+ pytest.raises(PchkLicenseError),
125
+ ):
126
+ await connect_task
127
+
128
+
129
+ @patch.object(PchkConnectionManager, "open_connection")
130
+ async def test_timeout_error(mock_open_connection: AsyncMock) -> None:
131
+ """Test timeout when connecting."""
132
+ with pytest.raises(PchkConnectionFailedError):
133
+ pypck_client = PchkConnectionManager(HOST, PORT, USERNAME, PASSWORD)
134
+ await pypck_client.async_connect(timeout=0)
135
+
136
+
137
+ async def test_lcn_connected(pypck_client: MockPchkConnectionManager) -> None:
138
+ """Test lcn connected events."""
139
+ event_callback = Mock()
140
+ pypck_client.register_for_events(event_callback)
141
+ await pypck_client.async_connect()
142
+
143
+ # bus disconnected
144
+ await pypck_client.async_process_input(inputs.LcnConnState(is_lcn_connected=False))
145
+ assert not pypck_client.is_lcn_connected
146
+ event_callback.assert_has_calls(
147
+ (call(LcnEvent.BUS_CONNECTION_STATUS_CHANGED), call(LcnEvent.BUS_DISCONNECTED))
148
+ )
149
+
150
+ # bus connected
151
+ await pypck_client.async_process_input(inputs.LcnConnState(is_lcn_connected=True))
152
+ assert pypck_client.is_lcn_connected
153
+ event_callback.assert_has_calls(
154
+ (call(LcnEvent.BUS_CONNECTION_STATUS_CHANGED), call(LcnEvent.BUS_CONNECTED))
155
+ )
@@ -1,10 +1,10 @@
1
1
  """Module connection tests."""
2
2
 
3
- from unittest.mock import patch
4
-
5
3
  import pytest
4
+
6
5
  from pypck.lcn_addr import LcnAddr
7
- from pypck.module import ModuleConnection
6
+
7
+ from .conftest import MockPchkConnectionManager
8
8
 
9
9
  TEST_VECTORS = {
10
10
  # empty
@@ -55,19 +55,20 @@ TEST_VECTORS = {
55
55
  }
56
56
 
57
57
 
58
- @pytest.mark.asyncio
59
58
  @pytest.mark.parametrize("text, parts", TEST_VECTORS.items())
60
- async def test_dyn_text(pypck_client, text, parts):
61
- """dyn_text."""
62
- # await pypck_client.async_connect()
59
+ async def test_dyn_text(
60
+ pypck_client: MockPchkConnectionManager,
61
+ text: str,
62
+ parts: tuple[bytes, bytes, bytes, bytes, bytes],
63
+ ) -> None:
64
+ """Tests for dynamic text."""
63
65
  module = pypck_client.get_address_conn(LcnAddr(0, 10, False))
64
66
 
65
- with patch.object(ModuleConnection, "send_command") as send_command:
66
- await module.dyn_text(3, text)
67
+ await module.dyn_text(3, text)
67
68
 
68
- send_command.assert_awaited()
69
- await_args = (call.args for call in send_command.await_args_list)
69
+ module.send_command.assert_awaited() # type: ignore[attr-defined]
70
+ await_args = (call.args for call in module.send_command.await_args_list) # type: ignore[attr-defined]
70
71
  _, commands = zip(*await_args)
71
72
 
72
73
  for i, part in enumerate(parts):
73
- assert f"GTDT4{i+1:d}".encode() + part in commands
74
+ assert f"GTDT4{i + 1:d}".encode() + part in commands
@@ -0,0 +1,46 @@
1
+ """Test the data flow for Input objects."""
2
+
3
+ from unittest.mock import patch
4
+
5
+ from pypck.inputs import Input, ModInput
6
+ from pypck.lcn_addr import LcnAddr
7
+ from pypck.module import ModuleConnection
8
+
9
+ from .conftest import MockPchkConnectionManager
10
+
11
+
12
+ async def test_message_to_input(pypck_client: MockPchkConnectionManager) -> None:
13
+ """Test data flow from message to input."""
14
+ inp = Input()
15
+ message = "dummy_message"
16
+ with patch.object(
17
+ pypck_client, "async_process_input"
18
+ ) as pypck_client_process_input:
19
+ with patch("pypck.inputs.InputParser.parse", return_value=[inp]) as inp_parse:
20
+ await pypck_client.process_message(message)
21
+
22
+ inp_parse.assert_called_with(message)
23
+ pypck_client_process_input.assert_awaited_with(inp)
24
+
25
+
26
+ async def test_physical_to_logical_segment_id(
27
+ pypck_client: MockPchkConnectionManager,
28
+ ) -> None:
29
+ """Test conversion from logical to physical segment id."""
30
+ pypck_client.local_seg_id = 20
31
+ module = pypck_client.get_address_conn(LcnAddr(20, 7, False))
32
+ assert isinstance(module, ModuleConnection)
33
+ with (
34
+ patch("tests.conftest.MockPchkConnectionManager.is_ready", return_value=True),
35
+ patch.object(module, "async_process_input") as module_process_input,
36
+ ):
37
+ inp = ModInput(LcnAddr(20, 7, False))
38
+ await pypck_client.async_process_input(inp)
39
+
40
+ inp = ModInput(LcnAddr(0, 7, False))
41
+ await pypck_client.async_process_input(inp)
42
+
43
+ inp = ModInput(LcnAddr(4, 7, False))
44
+ await pypck_client.async_process_input(inp)
45
+
46
+ assert module_process_input.await_count == 3
@@ -1,5 +1,7 @@
1
1
  """Tests for input message parsing for bus messages."""
2
2
 
3
+ from typing import Any
4
+
3
5
  import pytest
4
6
 
5
7
  from pypck.inputs import (
@@ -348,7 +350,10 @@ MESSAGES = {
348
350
 
349
351
 
350
352
  @pytest.mark.parametrize("message, expected", MESSAGES.items())
351
- def test_message_parsing_mod_inputs(message, expected):
353
+ def test_message_parsing_mod_inputs(
354
+ message: str,
355
+ expected: list[tuple[Any, ...]],
356
+ ) -> None:
352
357
  """Test if InputMod parses message correctly."""
353
358
  inputs = InputParser.parse(message)
354
359
  assert len(inputs) == len(expected)
@@ -1,7 +1,9 @@
1
1
  """Tests for variable value and unit handling."""
2
2
 
3
3
  import math
4
+
4
5
  import pytest
6
+
5
7
  from pypck.lcn_defs import VarUnit, VarValue
6
8
 
7
9
  VARIABLE_TEST_VALUES = (
@@ -108,7 +110,7 @@ CALIBRATION_TEST_VECTORS = (
108
110
 
109
111
 
110
112
  @pytest.mark.parametrize("unit, native, expected", ROUNDTRIP_TEST_VECTORS)
111
- def test_roundtrip(unit, native, expected):
113
+ def test_roundtrip(unit: VarUnit, native: int, expected: VarValue) -> None:
112
114
  """Test that variable conversion roundtrips."""
113
115
  assert (
114
116
  expected
@@ -119,6 +121,6 @@ def test_roundtrip(unit, native, expected):
119
121
 
120
122
 
121
123
  @pytest.mark.parametrize("unit, native, value", CALIBRATION_TEST_VECTORS)
122
- def test_calibration(unit, native, value):
124
+ def test_calibration(unit: VarUnit, native: int, value: int | float) -> None:
123
125
  """Test proper calibration of variable conversion."""
124
126
  assert value == VarValue.to_var_unit(VarValue.from_native(native), unit)
pypck-0.8.9/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.8.9
@@ -1,420 +0,0 @@
1
- """Connection tests."""
2
-
3
- import asyncio
4
- import json
5
- from unittest.mock import Mock, call
6
-
7
- import pytest
8
-
9
- from pypck.connection import (
10
- PchkAuthenticationError,
11
- PchkConnectionFailedError,
12
- PchkConnectionRefusedError,
13
- PchkLicenseError,
14
- )
15
- from pypck.lcn_addr import LcnAddr
16
- from pypck.lcn_defs import LcnEvent
17
- from pypck.module import ModuleConnection
18
-
19
-
20
- @pytest.mark.asyncio
21
- async def test_close_without_connect(pypck_client):
22
- """Test closing of PchkConnectionManager without connecting."""
23
- await pypck_client.async_close()
24
-
25
-
26
- @pytest.mark.asyncio
27
- async def test_authenticate(pchk_server, pypck_client):
28
- """Test authentication procedure."""
29
- await pypck_client.async_connect()
30
- assert pypck_client.is_ready()
31
-
32
-
33
- @pytest.mark.asyncio
34
- async def test_port_error(pchk_server, pypck_client):
35
- """Test wrong port."""
36
- pypck_client.port = 55555
37
- with pytest.raises(PchkConnectionRefusedError):
38
- await pypck_client.async_connect()
39
-
40
-
41
- @pytest.mark.asyncio
42
- async def test_authentication_error(pchk_server, pypck_client):
43
- """Test wrong login credentials."""
44
- pypck_client.password = "wrong_password"
45
- with pytest.raises(PchkAuthenticationError):
46
- await pypck_client.async_connect()
47
-
48
-
49
- @pytest.mark.asyncio
50
- async def test_license_error(pchk_server, pypck_client):
51
- """Test license error."""
52
- pchk_server.set_license_error(True)
53
-
54
- with pytest.raises(PchkLicenseError):
55
- await pypck_client.async_connect()
56
-
57
-
58
- @pytest.mark.asyncio
59
- async def test_timeout_error(pchk_server, pypck_client):
60
- """Test timeout when connecting."""
61
- with pytest.raises(PchkConnectionFailedError):
62
- await pypck_client.async_connect(timeout=0)
63
-
64
-
65
- @pytest.mark.asyncio
66
- async def test_lcn_connected(pchk_server, pypck_client):
67
- """Test lcn disconnected event."""
68
- event_callback = Mock()
69
- pypck_client.register_for_events(event_callback)
70
- await pypck_client.async_connect()
71
- await pchk_server.send_message("$io:#LCN:connected")
72
- await pypck_client.received("$io:#LCN:connected")
73
-
74
- event_callback.assert_has_calls(
75
- [
76
- call(LcnEvent.BUS_CONNECTION_STATUS_CHANGED),
77
- call(LcnEvent.BUS_CONNECTED),
78
- ]
79
- )
80
-
81
-
82
- @pytest.mark.asyncio
83
- async def test_lcn_disconnected(pchk_server, pypck_client):
84
- """Test lcn disconnected event."""
85
- event_callback = Mock()
86
- pypck_client.register_for_events(event_callback)
87
- await pypck_client.async_connect()
88
- await pchk_server.send_message("$io:#LCN:disconnected")
89
- await pypck_client.received("$io:#LCN:disconnected")
90
-
91
- event_callback.assert_has_calls(
92
- [call(LcnEvent.BUS_CONNECTION_STATUS_CHANGED), call(LcnEvent.BUS_DISCONNECTED)]
93
- )
94
-
95
-
96
- @pytest.mark.asyncio
97
- async def test_connection_lost(pchk_server, pypck_client):
98
- """Test pchk server connection close."""
99
- event_callback = Mock()
100
- pypck_client.register_for_events(event_callback)
101
- await pypck_client.async_connect()
102
-
103
- await pchk_server.stop()
104
- # ensure that pypck_client is about to be closed
105
- await pypck_client.wait_closed()
106
-
107
- event_callback.assert_has_calls([call(LcnEvent.CONNECTION_LOST)])
108
-
109
-
110
- @pytest.mark.asyncio
111
- async def test_multiple_connections(
112
- pchk_server, pypck_client, pchk_server_2, pypck_client_2
113
- ):
114
- """Test that two independent connections can coexists."""
115
- await pypck_client_2.async_connect()
116
-
117
- event_callback = Mock()
118
- pypck_client.register_for_events(event_callback)
119
- await pypck_client.async_connect()
120
-
121
- await pchk_server.stop()
122
- await pypck_client.wait_closed()
123
-
124
- event_callback.assert_has_calls([call(LcnEvent.CONNECTION_LOST)])
125
-
126
- assert len(pypck_client.task_registry.tasks) == 0
127
- assert len(pypck_client_2.task_registry.tasks) > 0
128
-
129
-
130
- @pytest.mark.asyncio
131
- async def test_segment_coupler_search(pchk_server, pypck_client):
132
- """Test segment coupler search."""
133
- await pypck_client.async_connect()
134
- await pypck_client.scan_segment_couplers(3, 0)
135
-
136
- assert await pchk_server.received(">G003003.SK")
137
- assert await pchk_server.received(">G003003.SK")
138
- assert await pchk_server.received(">G003003.SK")
139
-
140
- assert pypck_client.is_ready()
141
-
142
-
143
- @pytest.mark.asyncio
144
- async def test_segment_coupler_response(pchk_server, pypck_client):
145
- """Test segment coupler response."""
146
- await pypck_client.async_connect()
147
-
148
- assert pypck_client.local_seg_id == 0
149
-
150
- await pchk_server.send_message("=M000005.SK020")
151
- await pchk_server.send_message("=M021021.SK021")
152
- await pchk_server.send_message("=M022010.SK022")
153
- assert await pypck_client.received("=M000005.SK020")
154
- assert await pypck_client.received("=M021021.SK021")
155
- assert await pypck_client.received("=M022010.SK022")
156
-
157
- assert pypck_client.local_seg_id == 20
158
- assert set(pypck_client.segment_coupler_ids) == {20, 21, 22}
159
-
160
-
161
- @pytest.mark.asyncio
162
- async def test_module_scan(pchk_server, pypck_client):
163
- """Test module scan."""
164
- await pypck_client.async_connect()
165
- await pypck_client.scan_modules(3, 0)
166
-
167
- assert await pchk_server.received(">G000003!LEER")
168
- assert await pchk_server.received(">G000003!LEER")
169
- assert await pchk_server.received(">G000003!LEER")
170
-
171
-
172
- @pytest.mark.asyncio
173
- async def test_module_sn_response(pchk_server, pypck_client):
174
- """Test module scan."""
175
- await pypck_client.async_connect()
176
- module = pypck_client.get_address_conn(LcnAddr(0, 7, False))
177
-
178
- message = "=M000007.SN1AB20A123401FW190B11HW015"
179
- await pchk_server.send_message(message)
180
- assert await pypck_client.received(message)
181
-
182
- assert await module.serial_known
183
- assert module.hardware_serial == 0x1AB20A1234
184
- assert module.manu == 1
185
- assert module.software_serial == 0x190B11
186
- assert module.hardware_type.value == 15
187
-
188
-
189
- @pytest.mark.asyncio
190
- async def test_send_command_to_server(pchk_server, pypck_client):
191
- """Test sending a command to the PCHK server."""
192
- await pypck_client.async_connect()
193
- message = ">M000007.PIN003"
194
- await pypck_client.send_command(message)
195
- assert await pchk_server.received(message)
196
-
197
-
198
- @pytest.mark.asyncio
199
- async def test_ping(pchk_server, pypck_client):
200
- """Test if pings are send."""
201
- await pypck_client.async_connect()
202
- assert await pchk_server.received("^ping0")
203
-
204
-
205
- @pytest.mark.asyncio
206
- async def test_add_address_connections(pypck_client):
207
- """Test if new address connections are added on request."""
208
- lcn_addr = LcnAddr(0, 10, False)
209
- assert lcn_addr not in pypck_client.address_conns
210
-
211
- addr_conn = pypck_client.get_address_conn(lcn_addr)
212
- assert isinstance(addr_conn, ModuleConnection)
213
-
214
- assert lcn_addr in pypck_client.address_conns
215
-
216
-
217
- @pytest.mark.asyncio
218
- async def test_add_address_connections_by_message(pchk_server, pypck_client):
219
- """Test if new address connections are added by received message."""
220
- await pypck_client.async_connect()
221
- lcn_addr = LcnAddr(0, 10, False)
222
- assert lcn_addr not in pypck_client.address_conns
223
-
224
- message = ":M000010A1050"
225
- await pchk_server.send_message(message)
226
- assert await pypck_client.received(message)
227
-
228
- assert lcn_addr in pypck_client.address_conns
229
-
230
-
231
- @pytest.mark.asyncio
232
- async def test_groups_static_membership_discovery(pchk_server, pypck_client):
233
- """Test module scan."""
234
- await pypck_client.async_connect()
235
- module = pypck_client.get_address_conn(LcnAddr(0, 10, False))
236
-
237
- task = asyncio.create_task(module.request_static_groups())
238
- assert await pchk_server.received(">M000010.GP")
239
- await pchk_server.send_message("=M000010.GP012011200051")
240
- assert await task == {
241
- LcnAddr(0, 11, True),
242
- LcnAddr(0, 200, True),
243
- LcnAddr(0, 51, True),
244
- }
245
-
246
-
247
- @pytest.mark.asyncio
248
- async def test_groups_dynamic_membership_discovery(pchk_server, pypck_client):
249
- """Test module scan."""
250
- await pypck_client.async_connect()
251
- module = pypck_client.get_address_conn(LcnAddr(0, 10, False))
252
-
253
- task = asyncio.create_task(module.request_dynamic_groups())
254
- assert await pchk_server.received(">M000010.GD")
255
- await pchk_server.send_message("=M000010.GD008011200051")
256
- assert await task == {
257
- LcnAddr(0, 11, True),
258
- LcnAddr(0, 200, True),
259
- LcnAddr(0, 51, True),
260
- }
261
-
262
-
263
- @pytest.mark.asyncio
264
- async def test_groups_membership_discovery(pchk_server, pypck_client):
265
- """Test module scan."""
266
- await pypck_client.async_connect()
267
- module = pypck_client.get_address_conn(LcnAddr(0, 10, False))
268
-
269
- task = asyncio.create_task(module.request_groups())
270
- assert await pchk_server.received(">M000010.GP")
271
- await pchk_server.send_message("=M000010.GP012011200051")
272
- assert await pchk_server.received(">M000010.GD")
273
- await pchk_server.send_message("=M000010.GD008015100052")
274
- assert await task == {
275
- LcnAddr(0, 11, True),
276
- LcnAddr(0, 200, True),
277
- LcnAddr(0, 51, True),
278
- LcnAddr(0, 15, True),
279
- LcnAddr(0, 100, True),
280
- LcnAddr(0, 52, True),
281
- }
282
-
283
-
284
- @pytest.mark.asyncio
285
- async def test_multiple_serial_requests(pchk_server, pypck_client):
286
- """Test module scan."""
287
- await pypck_client.async_connect()
288
-
289
- pypck_client.get_address_conn(LcnAddr(0, 10, False))
290
- pypck_client.get_address_conn(LcnAddr(0, 11, False))
291
- pypck_client.get_address_conn(LcnAddr(0, 12, False))
292
-
293
- assert await pchk_server.received(">M000010.SN")
294
- assert await pchk_server.received(">M000011.SN")
295
- assert await pchk_server.received(">M000012.SN")
296
-
297
- message = "=M000010.SN1AB20A123401FW190B11HW015"
298
- await pchk_server.send_message(message)
299
- assert await pypck_client.received(message)
300
-
301
- await pypck_client.async_close()
302
-
303
-
304
- @pytest.mark.asyncio
305
- async def test_dump_modules_no_segement_couplers(pchk_server, pypck_client):
306
- """Test module information dumping."""
307
- await pypck_client.async_connect()
308
-
309
- for msg in (
310
- "=M000007.SN1AB20A123401FW190B11HW015",
311
- "=M000008.SN1BB20A123401FW1A0B11HW015",
312
- "=M000007.GP012011200051",
313
- "=M000008.GP012011220051",
314
- "=M000007.GD008015100052",
315
- "=M000008.GD008015120052",
316
- ):
317
- await pchk_server.send_message(msg)
318
- assert await pypck_client.received(msg)
319
-
320
- dump = pypck_client.dump_modules()
321
- json.dumps(dump)
322
-
323
- assert dump == {
324
- "0": {
325
- "7": {
326
- "segment": 0,
327
- "address": 7,
328
- "is_local_segment": True,
329
- "serials": {
330
- "hardware_serial": "1AB20A1234",
331
- "manu": "01",
332
- "software_serial": "190B11",
333
- "hardware_type": "15",
334
- "hardware_name": "LCN-SH-Plus",
335
- },
336
- "name": "",
337
- "comment": "",
338
- "oem_text": ["", "", "", ""],
339
- "groups": {"static": [11, 51, 200], "dynamic": [15, 52, 100]},
340
- },
341
- "8": {
342
- "segment": 0,
343
- "address": 8,
344
- "is_local_segment": True,
345
- "serials": {
346
- "hardware_serial": "1BB20A1234",
347
- "manu": "01",
348
- "software_serial": "1A0B11",
349
- "hardware_type": "15",
350
- "hardware_name": "LCN-SH-Plus",
351
- },
352
- "name": "",
353
- "comment": "",
354
- "oem_text": ["", "", "", ""],
355
- "groups": {"static": [11, 51, 220], "dynamic": [15, 52, 120]},
356
- },
357
- }
358
- }
359
-
360
-
361
- @pytest.mark.asyncio
362
- async def test_dump_modules_multi_segment(pchk_server, pypck_client):
363
- """Test module information dumping."""
364
- await pypck_client.async_connect()
365
-
366
- # Populate the bus topology information
367
- for msg in (
368
- "=M000007.SK020",
369
- "=M022008.SK022",
370
- "=M000007.SN1AB20A123401FW190B11HW015",
371
- "=M022008.SN1BB20A123401FW1A0B11HW015",
372
- "=M000007.GP012011200051",
373
- "=M022008.GP012011220051",
374
- "=M000007.GD008015100052",
375
- "=M022008.GD008015120052",
376
- ):
377
- await pchk_server.send_message(msg)
378
- assert await pypck_client.received(msg)
379
-
380
- dump = pypck_client.dump_modules()
381
- json.dumps(dump)
382
-
383
- assert dump == {
384
- "20": {
385
- "7": {
386
- "segment": 20,
387
- "address": 7,
388
- "is_local_segment": True,
389
- "serials": {
390
- "hardware_serial": "1AB20A1234",
391
- "manu": "01",
392
- "software_serial": "190B11",
393
- "hardware_type": "15",
394
- "hardware_name": "LCN-SH-Plus",
395
- },
396
- "name": "",
397
- "comment": "",
398
- "oem_text": ["", "", "", ""],
399
- "groups": {"static": [11, 51, 200], "dynamic": [15, 52, 100]},
400
- },
401
- },
402
- "22": {
403
- "8": {
404
- "segment": 22,
405
- "address": 8,
406
- "is_local_segment": False,
407
- "serials": {
408
- "hardware_serial": "1BB20A1234",
409
- "manu": "01",
410
- "software_serial": "1A0B11",
411
- "hardware_type": "15",
412
- "hardware_name": "LCN-SH-Plus",
413
- },
414
- "name": "",
415
- "comment": "",
416
- "oem_text": ["", "", "", ""],
417
- "groups": {"static": [11, 51, 220], "dynamic": [15, 52, 120]},
418
- },
419
- },
420
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes