axis 72__tar.gz → 74__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 (133) hide show
  1. {axis-72 → axis-74}/PKG-INFO +26 -6
  2. {axis-72 → axis-74}/README.md +16 -0
  3. axis-74/axis/cli/__init__.py +4 -0
  4. axis-74/axis/cli/core/__init__.py +1 -0
  5. axis-74/axis/cli/core/context.py +25 -0
  6. axis-74/axis/cli/core/contracts.py +32 -0
  7. axis-74/axis/cli/core/gateway.py +52 -0
  8. axis-74/axis/cli/core/io.py +25 -0
  9. axis-74/axis/cli/core/registry.py +26 -0
  10. axis-74/axis/cli/core/router.py +114 -0
  11. axis-74/axis/cli/main.py +205 -0
  12. axis-74/axis/cli/packs/__init__.py +1 -0
  13. axis-74/axis/cli/packs/accounts.py +292 -0
  14. axis-74/axis/cli/packs/api.py +429 -0
  15. axis-74/axis/cli/packs/devices.py +999 -0
  16. axis-74/axis/cli/packs/events.py +254 -0
  17. axis-74/axis/cli/packs/navigation.py +290 -0
  18. {axis-72 → axis-74}/axis/interfaces/api_handler.py +4 -0
  19. axis-74/axis/interfaces/temperature_control.py +93 -0
  20. {axis-72 → axis-74}/axis/interfaces/vapix.py +82 -2
  21. {axis-72 → axis-74}/axis/models/configuration.py +4 -0
  22. {axis-72 → axis-74}/axis/models/parameters/ptz.py +8 -8
  23. axis-74/axis/models/temperature_control.py +216 -0
  24. {axis-72 → axis-74}/axis.egg-info/PKG-INFO +26 -6
  25. {axis-72 → axis-74}/axis.egg-info/SOURCES.txt +18 -0
  26. {axis-72 → axis-74}/axis.egg-info/entry_points.txt +1 -0
  27. {axis-72 → axis-74}/axis.egg-info/requires.txt +9 -5
  28. {axis-72 → axis-74}/pyproject.toml +11 -6
  29. {axis-72 → axis-74}/tests/test_request_response_contracts.py +2 -0
  30. axis-74/tests/test_temperature_control.py +249 -0
  31. {axis-72 → axis-74}/tests/test_vapix.py +87 -2
  32. {axis-72 → axis-74}/LICENSE +0 -0
  33. {axis-72 → axis-74}/axis/__init__.py +0 -0
  34. {axis-72 → axis-74}/axis/__main__.py +0 -0
  35. {axis-72 → axis-74}/axis/device.py +0 -0
  36. {axis-72 → axis-74}/axis/errors.py +0 -0
  37. {axis-72 → axis-74}/axis/interfaces/__init__.py +0 -0
  38. {axis-72 → axis-74}/axis/interfaces/aiohttp_digest.py +0 -0
  39. {axis-72 → axis-74}/axis/interfaces/api_discovery.py +0 -0
  40. {axis-72 → axis-74}/axis/interfaces/applications/__init__.py +0 -0
  41. {axis-72 → axis-74}/axis/interfaces/applications/application_handler.py +0 -0
  42. {axis-72 → axis-74}/axis/interfaces/applications/applications.py +0 -0
  43. {axis-72 → axis-74}/axis/interfaces/applications/fence_guard.py +0 -0
  44. {axis-72 → axis-74}/axis/interfaces/applications/loitering_guard.py +0 -0
  45. {axis-72 → axis-74}/axis/interfaces/applications/motion_guard.py +0 -0
  46. {axis-72 → axis-74}/axis/interfaces/applications/object_analytics.py +0 -0
  47. {axis-72 → axis-74}/axis/interfaces/applications/vmd4.py +0 -0
  48. {axis-72 → axis-74}/axis/interfaces/basic_device_info.py +0 -0
  49. {axis-72 → axis-74}/axis/interfaces/event_instances.py +0 -0
  50. {axis-72 → axis-74}/axis/interfaces/event_manager.py +0 -0
  51. {axis-72 → axis-74}/axis/interfaces/light_control.py +0 -0
  52. {axis-72 → axis-74}/axis/interfaces/mqtt.py +0 -0
  53. {axis-72 → axis-74}/axis/interfaces/parameters/__init__.py +0 -0
  54. {axis-72 → axis-74}/axis/interfaces/parameters/brand.py +0 -0
  55. {axis-72 → axis-74}/axis/interfaces/parameters/image.py +0 -0
  56. {axis-72 → axis-74}/axis/interfaces/parameters/io_port.py +0 -0
  57. {axis-72 → axis-74}/axis/interfaces/parameters/param_cgi.py +0 -0
  58. {axis-72 → axis-74}/axis/interfaces/parameters/param_handler.py +0 -0
  59. {axis-72 → axis-74}/axis/interfaces/parameters/properties.py +0 -0
  60. {axis-72 → axis-74}/axis/interfaces/parameters/ptz.py +0 -0
  61. {axis-72 → axis-74}/axis/interfaces/parameters/stream_profile.py +0 -0
  62. {axis-72 → axis-74}/axis/interfaces/pir_sensor_configuration.py +0 -0
  63. {axis-72 → axis-74}/axis/interfaces/port_cgi.py +0 -0
  64. {axis-72 → axis-74}/axis/interfaces/port_management.py +0 -0
  65. {axis-72 → axis-74}/axis/interfaces/ptz.py +0 -0
  66. {axis-72 → axis-74}/axis/interfaces/pwdgrp_cgi.py +0 -0
  67. {axis-72 → axis-74}/axis/interfaces/stream_profiles.py +0 -0
  68. {axis-72 → axis-74}/axis/interfaces/user_groups.py +0 -0
  69. {axis-72 → axis-74}/axis/interfaces/view_areas.py +0 -0
  70. {axis-72 → axis-74}/axis/models/__init__.py +0 -0
  71. {axis-72 → axis-74}/axis/models/api.py +0 -0
  72. {axis-72 → axis-74}/axis/models/api_discovery.py +0 -0
  73. {axis-72 → axis-74}/axis/models/applications/__init__.py +0 -0
  74. {axis-72 → axis-74}/axis/models/applications/application.py +0 -0
  75. {axis-72 → axis-74}/axis/models/applications/fence_guard.py +0 -0
  76. {axis-72 → axis-74}/axis/models/applications/loitering_guard.py +0 -0
  77. {axis-72 → axis-74}/axis/models/applications/motion_guard.py +0 -0
  78. {axis-72 → axis-74}/axis/models/applications/object_analytics.py +0 -0
  79. {axis-72 → axis-74}/axis/models/applications/vmd4.py +0 -0
  80. {axis-72 → axis-74}/axis/models/basic_device_info.py +0 -0
  81. {axis-72 → axis-74}/axis/models/event.py +0 -0
  82. {axis-72 → axis-74}/axis/models/event_instance.py +0 -0
  83. {axis-72 → axis-74}/axis/models/light_control.py +0 -0
  84. {axis-72 → axis-74}/axis/models/mqtt.py +0 -0
  85. {axis-72 → axis-74}/axis/models/parameters/__init__.py +0 -0
  86. {axis-72 → axis-74}/axis/models/parameters/brand.py +0 -0
  87. {axis-72 → axis-74}/axis/models/parameters/image.py +0 -0
  88. {axis-72 → axis-74}/axis/models/parameters/io_port.py +0 -0
  89. {axis-72 → axis-74}/axis/models/parameters/param_cgi.py +0 -0
  90. {axis-72 → axis-74}/axis/models/parameters/properties.py +0 -0
  91. {axis-72 → axis-74}/axis/models/parameters/stream_profile.py +0 -0
  92. {axis-72 → axis-74}/axis/models/pir_sensor_configuration.py +0 -0
  93. {axis-72 → axis-74}/axis/models/port_cgi.py +0 -0
  94. {axis-72 → axis-74}/axis/models/port_management.py +0 -0
  95. {axis-72 → axis-74}/axis/models/ptz_cgi.py +0 -0
  96. {axis-72 → axis-74}/axis/models/pwdgrp_cgi.py +0 -0
  97. {axis-72 → axis-74}/axis/models/stream_profile.py +0 -0
  98. {axis-72 → axis-74}/axis/models/user_group.py +0 -0
  99. {axis-72 → axis-74}/axis/models/view_area.py +0 -0
  100. {axis-72 → axis-74}/axis/py.typed +0 -0
  101. {axis-72 → axis-74}/axis/rtsp.py +0 -0
  102. {axis-72 → axis-74}/axis/stream_manager.py +0 -0
  103. {axis-72 → axis-74}/axis/stream_transport.py +0 -0
  104. {axis-72 → axis-74}/axis/websocket.py +0 -0
  105. {axis-72 → axis-74}/axis.egg-info/dependency_links.txt +0 -0
  106. {axis-72 → axis-74}/axis.egg-info/top_level.txt +0 -0
  107. {axis-72 → axis-74}/setup.cfg +0 -0
  108. {axis-72 → axis-74}/tests/test_api_discovery.py +0 -0
  109. {axis-72 → axis-74}/tests/test_api_handler.py +0 -0
  110. {axis-72 → axis-74}/tests/test_auth_scheme.py +0 -0
  111. {axis-72 → axis-74}/tests/test_basic_device_info.py +0 -0
  112. {axis-72 → axis-74}/tests/test_configuration.py +0 -0
  113. {axis-72 → axis-74}/tests/test_conftest.py +0 -0
  114. {axis-72 → axis-74}/tests/test_device.py +0 -0
  115. {axis-72 → axis-74}/tests/test_event.py +0 -0
  116. {axis-72 → axis-74}/tests/test_event_instances.py +0 -0
  117. {axis-72 → axis-74}/tests/test_event_instances_protocol_parity.py +0 -0
  118. {axis-72 → axis-74}/tests/test_event_stream.py +0 -0
  119. {axis-72 → axis-74}/tests/test_http_client_compat.py +0 -0
  120. {axis-72 → axis-74}/tests/test_light_control.py +0 -0
  121. {axis-72 → axis-74}/tests/test_main_http_client.py +0 -0
  122. {axis-72 → axis-74}/tests/test_mqtt.py +0 -0
  123. {axis-72 → axis-74}/tests/test_pir_sensor_configuration.py +0 -0
  124. {axis-72 → axis-74}/tests/test_port_cgi.py +0 -0
  125. {axis-72 → axis-74}/tests/test_port_management.py +0 -0
  126. {axis-72 → axis-74}/tests/test_ptz.py +0 -0
  127. {axis-72 → axis-74}/tests/test_pwdgrp_cgi.py +0 -0
  128. {axis-72 → axis-74}/tests/test_rtsp.py +0 -0
  129. {axis-72 → axis-74}/tests/test_stream_manager.py +0 -0
  130. {axis-72 → axis-74}/tests/test_stream_profiles.py +0 -0
  131. {axis-72 → axis-74}/tests/test_user_groups.py +0 -0
  132. {axis-72 → axis-74}/tests/test_view_areas.py +0 -0
  133. {axis-72 → axis-74}/tests/test_websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axis
3
- Version: 72
3
+ Version: 74
4
4
  Summary: A Python library for communicating with devices from Axis Communications
5
5
  Author-email: Robert Svensson <Kane610@users.noreply.github.com>
6
6
  License: MIT
@@ -21,19 +21,23 @@ Requires-Dist: aiohttp>=3.12
21
21
  Requires-Dist: faust-cchardet>=2.1.18
22
22
  Requires-Dist: orjson>3.9
23
23
  Requires-Dist: packaging>23
24
+ Requires-Dist: tomli>=2.0
25
+ Requires-Dist: tomli-w>=1.0
24
26
  Requires-Dist: xmltodict>=0.13.0
27
+ Requires-Dist: zeroconf>=0.150.0
25
28
  Provides-Extra: requirements
26
- Requires-Dist: aiohttp==3.13.5; extra == "requirements"
29
+ Requires-Dist: aiohttp==3.14.1; extra == "requirements"
27
30
  Requires-Dist: orjson==3.11.9; extra == "requirements"
28
31
  Requires-Dist: packaging==26.2; extra == "requirements"
29
32
  Requires-Dist: xmltodict==1.0.4; extra == "requirements"
33
+ Requires-Dist: zeroconf>=0.150.0; extra == "requirements"
30
34
  Provides-Extra: requirements-test
31
35
  Requires-Dist: mypy==2.1.0; extra == "requirements-test"
32
- Requires-Dist: pytest==9.0.3; extra == "requirements-test"
33
- Requires-Dist: pytest-aiohttp==1.1.0; extra == "requirements-test"
34
- Requires-Dist: pytest-asyncio==1.3.0; extra == "requirements-test"
36
+ Requires-Dist: pytest==9.1.1; extra == "requirements-test"
37
+ Requires-Dist: pytest-aiohttp==1.1.1; extra == "requirements-test"
38
+ Requires-Dist: pytest-asyncio==1.4.0; extra == "requirements-test"
35
39
  Requires-Dist: pytest-cov==7.1.0; extra == "requirements-test"
36
- Requires-Dist: ruff==0.15.13; extra == "requirements-test"
40
+ Requires-Dist: ruff==0.15.20; extra == "requirements-test"
37
41
  Requires-Dist: types-xmltodict==v1.0.1.20260518; extra == "requirements-test"
38
42
  Provides-Extra: requirements-dev
39
43
  Requires-Dist: pre-commit==4.6.0; extra == "requirements-dev"
@@ -79,6 +83,22 @@ Initial `ty` support is configured as an opt-in check and does not replace `mypy
79
83
  uvx ty check
80
84
  ```
81
85
 
86
+ ## CLI navigation contract
87
+
88
+ The interactive CLI in `axis/cli/` follows a menu-first model.
89
+ Router runtime is the only interactive execution path.
90
+
91
+ - Canonical route graph: `main -> devices -> device_operations -> {api|events|accounts}`.
92
+ - `device_operations` routes to feature submenus using router navigation, not direct feature command execution.
93
+ - Feature nodes (`api`, `events`, `accounts`) own their local menu layout and selected-device context rendering.
94
+ - `b` always navigates to `parent_id`; `e` always exits.
95
+
96
+ To keep the CLI reusable boundary clear:
97
+
98
+ - CLI concerns (menu keys, prompts, render text, router state) stay in `axis/cli/`.
99
+ - `axis/interfaces/` and `axis/models/` must remain presentation-agnostic and reusable by non-CLI consumers.
100
+ - Domain layers return typed data/errors; CLI adapters map them to user-facing text and flow.
101
+
82
102
  ## Initialization architecture
83
103
 
84
104
  Vapix initialization is phase-based and driven by handler metadata:
@@ -38,6 +38,22 @@ Initial `ty` support is configured as an opt-in check and does not replace `mypy
38
38
  uvx ty check
39
39
  ```
40
40
 
41
+ ## CLI navigation contract
42
+
43
+ The interactive CLI in `axis/cli/` follows a menu-first model.
44
+ Router runtime is the only interactive execution path.
45
+
46
+ - Canonical route graph: `main -> devices -> device_operations -> {api|events|accounts}`.
47
+ - `device_operations` routes to feature submenus using router navigation, not direct feature command execution.
48
+ - Feature nodes (`api`, `events`, `accounts`) own their local menu layout and selected-device context rendering.
49
+ - `b` always navigates to `parent_id`; `e` always exits.
50
+
51
+ To keep the CLI reusable boundary clear:
52
+
53
+ - CLI concerns (menu keys, prompts, render text, router state) stay in `axis/cli/`.
54
+ - `axis/interfaces/` and `axis/models/` must remain presentation-agnostic and reusable by non-CLI consumers.
55
+ - Domain layers return typed data/errors; CLI adapters map them to user-facing text and flow.
56
+
41
57
  ## Initialization architecture
42
58
 
43
59
  Vapix initialization is phase-based and driven by handler metadata:
@@ -0,0 +1,4 @@
1
+ """Axis CLI package.
2
+
3
+ This package contains the command-line interface for Axis configuration validation and TOML export.
4
+ """
@@ -0,0 +1 @@
1
+ """Core CLI runtime primitives."""
@@ -0,0 +1,25 @@
1
+ # ruff: noqa: D100,D101,TC003
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING:
10
+ from axis.cli.core.gateway import DeviceGateway
11
+ from axis.cli.core.registry import CommandRegistry
12
+ from axis.cli.core.router import CliRouter
13
+
14
+ DeviceEntry = dict[str, Any]
15
+ DeviceStore = dict[str, DeviceEntry]
16
+
17
+
18
+ @dataclass
19
+ class CliContext:
20
+ config_path: Path
21
+ device_gateway: DeviceGateway
22
+ command_registry: CommandRegistry | None = None
23
+ router: CliRouter | None = None
24
+ selected_serial: str | None = None
25
+ selected_device: DeviceEntry | None = None
@@ -0,0 +1,32 @@
1
+ # ruff: noqa: D100,D101,D102
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Literal, Protocol
7
+
8
+ if TYPE_CHECKING:
9
+ from axis.cli.core.context import CliContext
10
+ from axis.cli.core.io import CliIO
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CommandCapabilities:
15
+ requires_device: bool = False
16
+ destructive: bool = False
17
+ read_only_safe: bool = True
18
+
19
+
20
+ @dataclass
21
+ class CommandResult:
22
+ status: Literal["ok", "error", "cancelled"] = "ok"
23
+ message: str | None = None
24
+ payload: object | None = None
25
+
26
+
27
+ class Command(Protocol):
28
+ id: str
29
+ title: str
30
+ capabilities: CommandCapabilities
31
+
32
+ async def run(self, ctx: CliContext, io: CliIO) -> CommandResult: ...
@@ -0,0 +1,52 @@
1
+ # ruff: noqa: D100,D101,D102
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from aiohttp import ClientSession
8
+
9
+ from axis.device import AxisDevice
10
+ from axis.errors import RequestError
11
+ from axis.models.configuration import Configuration
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Awaitable, Callable
15
+
16
+ from axis.cli.core.context import DeviceEntry
17
+
18
+
19
+ class DeviceGateway:
20
+ async def run[ReturnT](
21
+ self,
22
+ device_entry: DeviceEntry,
23
+ operation: Callable[[AxisDevice], Awaitable[ReturnT]],
24
+ ) -> ReturnT | None:
25
+ config_data = device_entry.get("config", {})
26
+ if not isinstance(config_data, dict):
27
+ print("Stored device config is incomplete. Please re-add the device.") # noqa: T201
28
+ return None
29
+
30
+ host = str(config_data.get("host", "")).strip()
31
+ username = str(config_data.get("username", "")).strip()
32
+ password = str(config_data.get("password", ""))
33
+ if not host or not username:
34
+ print("Stored device config is incomplete. Please re-add the device.") # noqa: T201
35
+ return None
36
+
37
+ try:
38
+ async with ClientSession() as session:
39
+ config = Configuration(
40
+ session=session,
41
+ host=host,
42
+ username=username,
43
+ password=password,
44
+ )
45
+ device = AxisDevice(config)
46
+ return await operation(device)
47
+ except RequestError as exc:
48
+ print(f"Device request failed: {exc}") # noqa: T201
49
+ except (ValueError, KeyError, OSError) as exc:
50
+ print(f"Failed to connect to selected device: {exc}") # noqa: T201
51
+
52
+ return None
@@ -0,0 +1,25 @@
1
+ # ruff: noqa: D100,D101,D102
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ from typing import Protocol
7
+
8
+
9
+ class CliIO(Protocol):
10
+ def prompt(self, text: str) -> str: ...
11
+
12
+ def prompt_password(self, text: str) -> str: ...
13
+
14
+ def write(self, text: str) -> None: ...
15
+
16
+
17
+ class TerminalIO:
18
+ def prompt(self, text: str) -> str:
19
+ return input(text)
20
+
21
+ def prompt_password(self, text: str) -> str:
22
+ return getpass.getpass(text)
23
+
24
+ def write(self, text: str) -> None:
25
+ print(text) # noqa: T201
@@ -0,0 +1,26 @@
1
+ # ruff: noqa: D100,D101,D102
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from axis.cli.core.contracts import Command
10
+
11
+
12
+ @dataclass
13
+ class CommandRegistry:
14
+ _commands: dict[str, Command] = field(default_factory=dict)
15
+
16
+ def register_command(self, command: Command) -> None:
17
+ if command.id in self._commands:
18
+ msg = f"Command '{command.id}' is already registered."
19
+ raise ValueError(msg)
20
+ self._commands[command.id] = command
21
+
22
+ def get_command(self, command_id: str) -> Command:
23
+ return self._commands[command_id]
24
+
25
+ def list_commands(self) -> list[Command]:
26
+ return list(self._commands.values())
@@ -0,0 +1,114 @@
1
+ # ruff: noqa: D100,D101,D102
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable
10
+
11
+ from axis.cli.core.context import CliContext
12
+ from axis.cli.core.io import CliIO
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class MenuItem:
17
+ key: str
18
+ label: str
19
+ action: str
20
+ command_id: str | None = None
21
+ next_node_id: str | None = None
22
+
23
+
24
+ @dataclass
25
+ class MenuNode:
26
+ id: str
27
+ title: str
28
+ items: list[MenuItem]
29
+ parent_id: str | None = None
30
+ render: Callable[[CliContext, CliIO], None] | None = None
31
+
32
+
33
+ @dataclass
34
+ class CliRouter:
35
+ nodes: dict[str, MenuNode] = field(default_factory=dict)
36
+
37
+ def register_node(self, node: MenuNode) -> None:
38
+ if node.id in self.nodes:
39
+ msg = f"Node '{node.id}' is already registered."
40
+ raise ValueError(msg)
41
+ self.nodes[node.id] = node
42
+
43
+ async def run(
44
+ self, ctx: CliContext, io: CliIO, start_node_id: str = "main"
45
+ ) -> None:
46
+ current = start_node_id
47
+ while True:
48
+ node = self.nodes[current]
49
+ if node.render is not None:
50
+ node.render(ctx, io)
51
+ io.write(f"\n{node.title}:")
52
+ for item in node.items:
53
+ io.write(f" {item.key}. {item.label}")
54
+ io.write(" b. Back")
55
+ io.write(" e. Exit")
56
+ selection = io.prompt("Select option: ").strip().lower()
57
+
58
+ if selection == "e":
59
+ io.write("Exiting.")
60
+ raise SystemExit(0)
61
+ if selection == "b":
62
+ if node.parent_id is None:
63
+ continue
64
+ current = node.parent_id
65
+ continue
66
+
67
+ matched = next((item for item in node.items if item.key == selection), None)
68
+ if matched is None:
69
+ io.write("Invalid option.")
70
+ continue
71
+
72
+ if matched.action == "navigate" and matched.next_node_id is not None:
73
+ current = matched.next_node_id
74
+ continue
75
+
76
+ if matched.action == "noop":
77
+ continue
78
+
79
+ if matched.action == "command":
80
+ if matched.command_id is None:
81
+ io.write("Command is not configured.")
82
+ continue
83
+
84
+ registry = ctx.command_registry
85
+ if registry is None:
86
+ io.write("Command registry is unavailable.")
87
+ continue
88
+
89
+ try:
90
+ command = registry.get_command(matched.command_id)
91
+ except KeyError:
92
+ io.write(f"Unknown command: {matched.command_id}")
93
+ continue
94
+
95
+ result = await command.run(ctx, io)
96
+ payload = result.payload
97
+ if result.status == "ok" and isinstance(payload, dict):
98
+ requested_node = payload.get("next_node_id")
99
+ if isinstance(requested_node, str):
100
+ if requested_node in self.nodes:
101
+ if result.message:
102
+ io.write(result.message)
103
+ current = requested_node
104
+ continue
105
+ io.write(f"Unknown node: {requested_node}")
106
+ if result.message:
107
+ io.write(result.message)
108
+ elif result.status == "error":
109
+ io.write("Command failed.")
110
+ elif result.status == "cancelled":
111
+ io.write("Command cancelled.")
112
+ continue
113
+
114
+ io.write("Unsupported action.")
@@ -0,0 +1,205 @@
1
+ """Axis CLI composition root and compatibility exports.
2
+
3
+ This module serves as the explicit bootstrap and import surface for the
4
+ plugin-pack based CLI architecture.
5
+ """
6
+
7
+ # ruff: noqa: RUF022,TC003
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import asyncio
13
+ import logging
14
+ import os
15
+ from pathlib import Path
16
+
17
+ from axis.cli.core.context import CliContext
18
+ from axis.cli.core.gateway import DeviceGateway
19
+ from axis.cli.core.io import TerminalIO
20
+ from axis.cli.core.registry import CommandRegistry
21
+ from axis.cli.core.router import CliRouter
22
+ from axis.cli.packs import (
23
+ accounts as accounts_pack,
24
+ api as api_pack,
25
+ devices as devices_pack,
26
+ events as events_pack,
27
+ navigation as navigation_pack,
28
+ )
29
+
30
+ DeviceEntry = devices_pack.DeviceEntry
31
+ DeviceStore = devices_pack.DeviceStore
32
+
33
+ # Compatibility exports from packs
34
+ get_config_path = devices_pack.get_config_path
35
+ load_devices = devices_pack.load_devices
36
+ save_devices = devices_pack.save_devices
37
+ to_toml_compatible = devices_pack.to_toml_compatible
38
+ fetch_device_serial_and_extra = devices_pack.fetch_device_serial_and_extra
39
+ extract_model_number = devices_pack.extract_model_number
40
+ extract_serial_number = devices_pack.extract_serial_number
41
+ prompt_for_device = devices_pack.prompt_for_device
42
+ config_to_toml_dict = devices_pack.config_to_toml_dict
43
+ _format_device_label = devices_pack._format_device_label
44
+ _format_device_operations_label = devices_pack._format_device_operations_label
45
+ AXIS_OUI = devices_pack.AXIS_OUI
46
+ AXIS_SERVICE_TYPE = devices_pack.AXIS_SERVICE_TYPE
47
+ is_axis_macaddress = devices_pack.is_axis_macaddress
48
+ discover_axis_devices = devices_pack.discover_axis_devices
49
+ select_discovered_device = devices_pack.select_discovered_device
50
+ filter_discovered_devices = devices_pack.filter_discovered_devices
51
+ validate_and_fetch_device = devices_pack.validate_and_fetch_device
52
+ print_registered_devices = devices_pack.print_registered_devices
53
+ migrate_unknown_entry = devices_pack.migrate_unknown_entry
54
+ find_serial_by_host = devices_pack.find_serial_by_host
55
+ select_device = devices_pack.select_device
56
+ get_device_credentials = devices_pack.get_device_credentials
57
+ run_on_selected_device = devices_pack.run_on_selected_device
58
+ register_or_update_device = devices_pack.register_or_update_device
59
+ delete_device = devices_pack.delete_device
60
+ edit_device_credentials = devices_pack.edit_device_credentials
61
+ health_check_device = devices_pack.health_check_device
62
+ HealthCheckResult = devices_pack.HealthCheckResult
63
+
64
+ fetch_supported_apis = api_pack.fetch_supported_apis
65
+ list_supported_apis_async = api_pack.list_supported_apis_async
66
+ run_api_read_action = api_pack.run_api_read_action
67
+ api_drill_down_async = api_pack.api_drill_down_async
68
+
69
+ fetch_event_instances = events_pack.fetch_event_instances
70
+ list_event_instances_async = events_pack.list_event_instances_async
71
+ events_menu_async = events_pack.events_menu_async
72
+ _live_listen_async = events_pack._live_listen_async
73
+
74
+ _validate_username = accounts_pack._validate_username
75
+ _select_privilege = accounts_pack._select_privilege
76
+ _account_init_confirm = accounts_pack._account_init_confirm
77
+ _account_init_operation = accounts_pack._account_init_operation
78
+ account_management_async = accounts_pack.account_management_async
79
+ _list_users_async = accounts_pack._list_users_async
80
+ _create_or_update_user_async = accounts_pack._create_or_update_user_async
81
+ _delete_user_async = accounts_pack._delete_user_async
82
+
83
+ selected_device_operations = navigation_pack.selected_device_operations
84
+
85
+
86
+ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
87
+ parser = argparse.ArgumentParser(description="Axis interactive CLI")
88
+ parser.add_argument(
89
+ "--debug",
90
+ action="store_true",
91
+ help="Enable verbose debug output for raw device/API responses.",
92
+ )
93
+ return parser.parse_args(argv)
94
+
95
+
96
+ def _debug_enabled_from_env() -> bool:
97
+ value = os.getenv("AXIS_CLI_DEBUG", "").strip().lower()
98
+ return value in {"1", "true", "yes", "on"}
99
+
100
+
101
+ def _configure_logging(debug_enabled: bool) -> None:
102
+ loglevel = logging.DEBUG if debug_enabled else logging.INFO
103
+ logging.basicConfig(format="%(message)s", level=loglevel, force=True)
104
+
105
+
106
+ def compose_builtin_packs(registry: CommandRegistry, router: CliRouter) -> None:
107
+ """Register built-in CLI packs explicitly from a single composition root."""
108
+ devices_pack.register(registry, router)
109
+ api_pack.register(registry, router)
110
+ events_pack.register(registry, router)
111
+ accounts_pack.register(registry, router)
112
+ navigation_pack.register(registry, router)
113
+
114
+
115
+ def build_cli_runtime(config_path: Path) -> CliContext:
116
+ """Create shared runtime context for the CLI process."""
117
+ registry = CommandRegistry()
118
+ router = CliRouter()
119
+ compose_builtin_packs(registry, router)
120
+ return CliContext(
121
+ config_path=config_path,
122
+ device_gateway=DeviceGateway(),
123
+ command_registry=registry,
124
+ router=router,
125
+ )
126
+
127
+
128
+ def main(*, debug: bool | None = None) -> None:
129
+ """Run the interactive device registry CLI."""
130
+ debug_enabled = _debug_enabled_from_env() if debug is None else debug
131
+ _configure_logging(debug_enabled)
132
+
133
+ if debug_enabled:
134
+ os.environ["AXIS_CLI_DEBUG"] = "1"
135
+ print("Debug mode enabled. Verbose responses will be shown.") # noqa: T201
136
+
137
+ config_path = get_config_path()
138
+ runtime = build_cli_runtime(config_path)
139
+
140
+ if runtime.router is None:
141
+ msg = "Router runtime is unavailable."
142
+ raise RuntimeError(msg)
143
+
144
+ terminal_io = TerminalIO()
145
+ while True:
146
+ try:
147
+ asyncio.run(runtime.router.run(runtime, terminal_io, start_node_id="main"))
148
+ except KeyboardInterrupt:
149
+ print("\nInterrupted. Use 'e' to exit.") # noqa: T201
150
+ continue
151
+
152
+
153
+ __all__ = [
154
+ "DeviceEntry",
155
+ "DeviceStore",
156
+ "compose_builtin_packs",
157
+ "build_cli_runtime",
158
+ "main",
159
+ "get_config_path",
160
+ "load_devices",
161
+ "save_devices",
162
+ "to_toml_compatible",
163
+ "fetch_device_serial_and_extra",
164
+ "extract_model_number",
165
+ "extract_serial_number",
166
+ "prompt_for_device",
167
+ "config_to_toml_dict",
168
+ "_format_device_label",
169
+ "AXIS_OUI",
170
+ "AXIS_SERVICE_TYPE",
171
+ "is_axis_macaddress",
172
+ "discover_axis_devices",
173
+ "select_discovered_device",
174
+ "filter_discovered_devices",
175
+ "validate_and_fetch_device",
176
+ "print_registered_devices",
177
+ "migrate_unknown_entry",
178
+ "find_serial_by_host",
179
+ "select_device",
180
+ "get_device_credentials",
181
+ "run_on_selected_device",
182
+ "register_or_update_device",
183
+ "fetch_supported_apis",
184
+ "list_supported_apis_async",
185
+ "run_api_read_action",
186
+ "api_drill_down_async",
187
+ "fetch_event_instances",
188
+ "list_event_instances_async",
189
+ "events_menu_async",
190
+ "_live_listen_async",
191
+ "_validate_username",
192
+ "_select_privilege",
193
+ "_account_init_confirm",
194
+ "_account_init_operation",
195
+ "account_management_async",
196
+ "_list_users_async",
197
+ "_create_or_update_user_async",
198
+ "_delete_user_async",
199
+ "selected_device_operations",
200
+ ]
201
+
202
+
203
+ if __name__ == "__main__":
204
+ args = _parse_args()
205
+ main(debug=args.debug)
@@ -0,0 +1 @@
1
+ """Built-in CLI plugin packs."""