updates2mqtt 1.5.0__tar.gz → 1.5.1__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 (69) hide show
  1. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/CHANGELOG.md +6 -0
  2. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/PKG-INFO +1 -1
  3. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/conftest.py +2 -2
  4. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/configuration.md +22 -2
  5. updates2mqtt-1.5.1/docs/examples/config_minimal.md +7 -0
  6. updates2mqtt-1.5.1/docs/examples/env.md +15 -0
  7. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/installation.md +1 -1
  8. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/pyproject.toml +1 -1
  9. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/config.py +13 -6
  10. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/hass_formatter.py +6 -15
  11. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/integrations/docker.py +14 -3
  12. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/model.py +11 -1
  13. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/mqtt.py +0 -2
  14. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/tests/test_config.py +31 -1
  15. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/tests/test_docker.py +1 -1
  16. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/tests/test_hass_formatter.py +5 -8
  17. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/tests/test_mqtt.py +20 -16
  18. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/uv.lock +64 -64
  19. updates2mqtt-1.5.0/docs/examples/config_minimal.md +0 -7
  20. updates2mqtt-1.5.0/docs/examples/env.md +0 -7
  21. updates2mqtt-1.5.0/no_config.yaml +0 -33
  22. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.dockerignore +0 -0
  23. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.github/dependabot.yml +0 -0
  24. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.github/workflows/auto_assign_issue.yml +0 -0
  25. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.github/workflows/auto_assign_pr.yml +0 -0
  26. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.github/workflows/docker-publish.yml +0 -0
  27. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.github/workflows/main.yml +0 -0
  28. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.github/workflows/pypi-publish.yml +0 -0
  29. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.github/workflows/python-package.yml +0 -0
  30. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.gitignore +0 -0
  31. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.hintrc +0 -0
  32. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.pre-commit-config.yaml +0 -0
  33. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.python-version +0 -0
  34. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/.safety-project.ini +0 -0
  35. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/Dockerfile +0 -0
  36. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/LICENSE +0 -0
  37. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/README.md +0 -0
  38. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/SECURITY.md +0 -0
  39. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/common_packages.yaml +0 -0
  40. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/examples/config_maximal.md +0 -0
  41. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/examples/docker_compose.md +0 -0
  42. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/examples/index.md +0 -0
  43. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/home_assistant.md +0 -0
  44. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/images/ha_entities.png +0 -0
  45. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/images/ha_mqtt_discovery.png +0 -0
  46. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/images/ha_update_detail.png +0 -0
  47. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/images/ha_update_dialog.png +0 -0
  48. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/images/ha_update_page.png +0 -0
  49. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/images/logo-blank-256x256.png +0 -0
  50. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/images/updates2mqtt-dark-256x256.png +0 -0
  51. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/index.md +0 -0
  52. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/local_builds.md +0 -0
  53. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/robots.txt +0 -0
  54. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/docs/troubleshooting.md +0 -0
  55. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/examples/config.yaml.maximal +0 -0
  56. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/examples/config.yaml.minimal +0 -0
  57. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/examples/docker-compose.yaml +0 -0
  58. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/mkdocs.yml +0 -0
  59. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/refresh-deps.sh +0 -0
  60. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/scripts/healthcheck.sh +0 -0
  61. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/__init__.py +0 -0
  62. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/__main__.py +0 -0
  63. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/app.py +0 -0
  64. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/integrations/__init__.py +0 -0
  65. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/integrations/git_utils.py +0 -0
  66. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/src/updates2mqtt/py.typed +0 -0
  67. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/tests/__init__.py +0 -0
  68. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/tests/test_app.py +0 -0
  69. {updates2mqtt-1.5.0 → updates2mqtt-1.5.1}/tests/test_git_utils.py +0 -0
@@ -1,5 +1,11 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.5.1
4
+ - `MQTT_VERSION` environment variable added, defaults to `3.11`
5
+ - `U2M_AUTOGEN_CONFIG` environment variable added to control auto-generation of config files and directories
6
+ - `U2M_LOG_LEVEL` environment variable added to set log level without config file
7
+ - Title generation for Docker images reverts to same whether HA device set or not
8
+ - Test added to ensure component always functions without a config file, if no env var present
3
9
  ## 1.5.0
4
10
  - Target specific service on docker compose commands, where available from `com.docker.compose.service` label
5
11
  - Log level in config is now an enum, and forced to be upper case
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: updates2mqtt
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: System update and docker image notification and execution over MQTT
5
5
  Project-URL: Homepage, https://updates2mqtt.rhizomatics.org.uk
6
6
  Project-URL: Repository, https://github.com/rhizomatics/updates2mqtt
@@ -35,7 +35,7 @@ def app_with_mocked_external_dependencies(
35
35
 
36
36
  @pytest.fixture
37
37
  def mock_discoveries(mock_provider: ReleaseProvider) -> list[Discovery]:
38
- return [Discovery(mock_provider, "thing-1", "test001", can_update=True)]
38
+ return [Discovery(mock_provider, "thing-1", "test001", "testbed01", can_update=True, update_type="TestRun")]
39
39
 
40
40
 
41
41
  @pytest.fixture
@@ -71,7 +71,7 @@ def mock_provider() -> ReleaseProvider:
71
71
  provider.source_type = "unit_test"
72
72
  provider.command.return_value = True # type: ignore[attr-defined]
73
73
  provider.resolve.return_value = Discovery( # type: ignore[attr-defined]
74
- provider, "fooey", session="test-mqtt-123", current_version="v2", latest_version="v2"
74
+ provider, "fooey", session="test-mqtt-123", node="node002", current_version="v2", latest_version="v2"
75
75
  )
76
76
  provider.hass_state_format.return_value = {"fixture": "test_exec"} # type: ignore[attr-defined]
77
77
  return provider
@@ -1,6 +1,26 @@
1
- # Configuration
1
+ # Minimal Configuration
2
2
 
3
- Create file `config.yaml` in `conf` directory. If the file is not present, a default file will be generated.
3
+ The core configuration can be supplied by environment variables, everything else will default, either to fixed values built into Updates2MQTT, or in the case of the node name, taken from the operating system.
4
+
5
+ | Env Var | Default |
6
+ |---------------|--------------|
7
+ | MQTT_HOST | localhost |
8
+ | MQTT_PORT | 1883 |
9
+ | MQTT_USER | *NO DEFAULT* |
10
+ | MQTT_PASSWORD | *NO DEFAULT* |
11
+ | MQTT_VERSION | 3.11. |
12
+ | U2M_LOG_LEVEL | INFO |
13
+
14
+ Startup will fail if `MQTT_USER` and `MQTT_PASSWORD` are not defined some how.
15
+
16
+ The example [docker-compose.yaml](docker_compose.md) and [.env](env.md) demonstrate one way of doing this, or skip
17
+ the `.env` file and use an `environment` section in the Compose file.
18
+
19
+ # Configuration File
20
+
21
+ The configuration file is optional, and only needed if you have to override the defaults.
22
+
23
+ Create file `config.yaml` in `conf` directory. If the file is not present, a default file will be generated, and the parent director if necessary. If you don't want that to happen, then set `U2M_AUTOGEN_CONFIG=0`.
4
24
 
5
25
  ### Example configuration file
6
26
 
@@ -0,0 +1,7 @@
1
+ # Minimal Configuration
2
+
3
+ Smallest working configuration ( although the actual smallest is no file at all and using only environment variables for MQTT )
4
+
5
+ ``` yaml
6
+ --8<-- "examples/config.yaml.minimal"
7
+ ```
@@ -0,0 +1,15 @@
1
+ # Environment File
2
+
3
+ Example `.env` file for Docker Compose configuration to separately store the environment variables.
4
+
5
+ ``` bash
6
+ # Example env file for docker-compose.yaml
7
+ # Used by both the app itself and the healthcheck script
8
+
9
+ # To use these, add to config.yaml like ${oc.env:MQTT_HOST}
10
+
11
+ MQTT_HOST=192.168.0.1
12
+ MQTT_PORT=1883
13
+ MQTT_USER=my_mqtt_user
14
+ MQTT_PASS=my_mqtt_secret
15
+ ```
@@ -4,7 +4,7 @@
4
4
 
5
5
  updates2mqtt prefers to be run inside a Docker container, though can run standalone, for example scripted via cron or systemd.
6
6
 
7
- The only mandatory configuration is the MQTT broker host, user name and password, which can be set by environment variable, or the config file. See [Configuration](configuration.md).
7
+ The only mandatory configuration is the MQTT broker host, user name and password, which can be set by environment variables, or the config file. The node name will be taken from the operating system if there's no config file. See [Configuration](configuration.md) for details.
8
8
 
9
9
  ### Docker
10
10
 
@@ -7,7 +7,7 @@ authors = [
7
7
  ]
8
8
 
9
9
  requires-python = ">=3.13"
10
- version = "1.5.0"
10
+ version = "1.5.1"
11
11
  license="Apache-2.0"
12
12
  keywords=["mqtt", "docker", "updates", "automation","home-assistant","homeassistant","selfhosting"]
13
13
 
@@ -25,7 +25,7 @@ class MqttConfig:
25
25
  password: str = f"${{oc.env:MQTT_PASS,{MISSING}}}"
26
26
  port: int = "${oc.decode:${oc.env:MQTT_PORT,1883}}" # type: ignore[assignment]
27
27
  topic_root: str = "updates2mqtt"
28
- protocol: str = "3.11"
28
+ protocol: str = "${oc.env:MQTT_VERSION,3.11}"
29
29
 
30
30
 
31
31
  @dataclass
@@ -80,12 +80,12 @@ class NodeConfig:
80
80
 
81
81
  @dataclass
82
82
  class LogConfig:
83
- level: LogLevel = LogLevel.INFO
83
+ level: LogLevel = "${oc.decode:${oc.env:U2M_LOG_LEVEL,INFO}}" # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
84
84
 
85
85
 
86
86
  @dataclass
87
87
  class Config:
88
- log: LogConfig = field(default_factory=LogConfig)
88
+ log: LogConfig = field(default_factory=LogConfig) # pyright: ignore[reportArgumentType, reportCallIssue]
89
89
  node: NodeConfig = field(default_factory=NodeConfig)
90
90
  mqtt: MqttConfig = field(default_factory=MqttConfig) # pyright: ignore[reportArgumentType, reportCallIssue]
91
91
  homeassistant: HomeAssistantConfig = field(default_factory=HomeAssistantConfig)
@@ -132,22 +132,29 @@ def load_package_info(pkginfo_file_path: Path) -> dict[str, PackageUpdateInfo]:
132
132
  raise
133
133
 
134
134
 
135
+ def is_autogen_config() -> bool:
136
+ env_var: str | None = os.environ.get("U2M_AUTOGEN_CONFIG")
137
+ return not (env_var and env_var.lower() in ("no", "0", "false"))
138
+
139
+
135
140
  def load_app_config(conf_file_path: Path, return_invalid: bool = False) -> Config | None:
136
141
  base_cfg: DictConfig = OmegaConf.structured(Config)
137
142
  if conf_file_path.exists():
138
143
  cfg: DictConfig = typing.cast("DictConfig", OmegaConf.merge(base_cfg, OmegaConf.load(conf_file_path)))
139
- else:
144
+ elif is_autogen_config():
140
145
  if not conf_file_path.parent.exists():
141
146
  try:
142
147
  log.debug(f"Creating config directory {conf_file_path.parent} if not already present")
143
148
  conf_file_path.parent.mkdir(parents=True, exist_ok=True)
144
149
  except Exception:
145
- log.exception("Unable to create config directory", path=conf_file_path.parent)
150
+ log.warning("Unable to create config directory", path=conf_file_path.parent)
146
151
  try:
147
152
  conf_file_path.write_text(OmegaConf.to_yaml(base_cfg))
148
153
  log.info(f"Auto-generated a new config file at {conf_file_path}")
149
154
  except Exception:
150
- log.exception("Unable to write config file", path=conf_file_path)
155
+ log.warning("Unable to write config file", path=conf_file_path)
156
+ cfg = base_cfg
157
+ else:
151
158
  cfg = base_cfg
152
159
 
153
160
  try:
@@ -21,7 +21,6 @@ HASS_UPDATE_SCHEMA = [
21
21
  def hass_format_config(
22
22
  discovery: Discovery,
23
23
  object_id: str,
24
- node_name: str,
25
24
  state_topic: str,
26
25
  command_topic: str | None,
27
26
  force_command_topic: bool | None,
@@ -29,13 +28,8 @@ def hass_format_config(
29
28
  area: str | None = None,
30
29
  session: str | None = None,
31
30
  ) -> dict[str, Any]:
32
- if device_creation:
33
- # avoid duplication, since Home Assistant will concatenate device and entity name on update
34
- name: str = f"{discovery.name} {discovery.source_type}"
35
- else:
36
- name = f"{discovery.name} {discovery.source_type} on {node_name}"
37
31
  config: dict[str, Any] = {
38
- "name": name,
32
+ "name": discovery.title,
39
33
  "device_class": None, # not firmware, so defaults to null
40
34
  "unique_id": object_id,
41
35
  "state_topic": state_topic,
@@ -46,7 +40,7 @@ def hass_format_config(
46
40
  "can_restart": discovery.can_restart,
47
41
  "update_policy": discovery.update_policy,
48
42
  "origin": {
49
- "name": f"{node_name} updates2mqtt",
43
+ "name": f"{discovery.node} updates2mqtt",
50
44
  "sw_version": updates2mqtt.version, # pyright: ignore[reportAttributeAccessIssue]
51
45
  "support_url": "https://github.com/rhizomatics/updates2mqtt/issues",
52
46
  },
@@ -57,10 +51,10 @@ def hass_format_config(
57
51
  config["icon"] = discovery.device_icon
58
52
  if device_creation:
59
53
  config["device"] = {
60
- "name": f"{node_name} updates2mqtt",
54
+ "name": f"{discovery.node} updates2mqtt",
61
55
  "sw_version": updates2mqtt.version, # pyright: ignore[reportAttributeAccessIssue]
62
56
  "manufacturer": "rhizomatics",
63
- "identifiers": [f"{node_name}.updates2mqtt"],
57
+ "identifiers": [f"{discovery.node}.updates2mqtt"],
64
58
  }
65
59
  if area:
66
60
  config["device"]["suggested_area"] = area
@@ -74,14 +68,11 @@ def hass_format_config(
74
68
  return config
75
69
 
76
70
 
77
- def hass_format_state(discovery: Discovery, node_name: str, session: str, in_progress: bool = False) -> dict[str, Any]: # noqa: ARG001
78
- title: str = (
79
- discovery.title_template.format(name=discovery.name, node=node_name) if discovery.title_template else discovery.name
80
- )
71
+ def hass_format_state(discovery: Discovery, session: str, in_progress: bool = False) -> dict[str, Any]: # noqa: ARG001
81
72
  state = {
82
73
  "installed_version": discovery.current_version,
83
74
  "latest_version": discovery.latest_version,
84
- "title": title,
75
+ "title": discovery.title,
85
76
  "in_progress": in_progress,
86
77
  }
87
78
  if discovery.release_summary:
@@ -101,18 +101,22 @@ class DockerProvider(ReleaseProvider):
101
101
  if not cwd or not Path(cwd).is_dir():
102
102
  logger.warn("Invalid compose path, skipped %s", command)
103
103
  return False
104
- logger.info(f"Executing compose {command} {args} {service}")
104
+
105
105
  cmd: str = "docker-compose" if self.cfg.compose_version == "v1" else "docker compose"
106
+ logger.info(f"Executing {cmd} {command} {args} {service}")
106
107
  cmd = cmd + " " + command.value
107
108
  if args:
108
109
  cmd = cmd + " " + args
109
110
  if service:
110
111
  cmd = cmd + " " + service
111
112
 
112
- proc = subprocess.run(cmd, check=False, shell=True, cwd=cwd)
113
+ proc: subprocess.CompletedProcess[str] = subprocess.run(cmd, check=False, shell=True, cwd=cwd, text=True)
113
114
  if proc.returncode == 0:
114
115
  logger.info(f"{command} via compose successful")
115
116
  return True
117
+ if proc.stderr and "unknown command: docker compose" in proc.stderr:
118
+ logger.warning("docker compose set to wrong version, seems like v1 installed")
119
+ self.cfg.compose_version = "v1"
116
120
  logger.warn(
117
121
  f"{command} failed: %s",
118
122
  proc.returncode,
@@ -275,6 +279,12 @@ class DockerProvider(ReleaseProvider):
275
279
  logger.info(f"Update not available, can_pull:{can_pull}, can_build:{can_build},can_restart{can_restart}")
276
280
  if relnotes_url:
277
281
  features.append("RELEASE_NOTES")
282
+ if can_pull:
283
+ update_type: str = "Docker Image"
284
+ elif can_build:
285
+ update_type = "Docker Build"
286
+ else:
287
+ update_type = "Unavailable"
278
288
  custom["can_pull"] = can_pull
279
289
 
280
290
  logger.debug("Analyze generated discovery", discovery_name=c.name, current_version=local_version)
@@ -282,15 +292,16 @@ class DockerProvider(ReleaseProvider):
282
292
  self,
283
293
  c.name,
284
294
  session,
295
+ node=self.node_cfg.name,
285
296
  entity_picture_url=picture_url,
286
297
  release_url=relnotes_url,
287
298
  current_version=local_version,
288
299
  update_policy=update_policy,
289
300
  update_last_attempt=(original_discovery and original_discovery.update_last_attempt) or None,
290
301
  latest_version=latest_version if latest_version != NO_KNOWN_IMAGE else local_version,
291
- title_template="Docker image update for {name} on {node}",
292
302
  device_icon=self.cfg.device_icon,
293
303
  can_update=can_update,
304
+ update_type=update_type,
294
305
  can_build=can_build,
295
306
  can_restart=can_restart,
296
307
  status=(c.status == "running" and "on") or "off",
@@ -14,6 +14,7 @@ class Discovery:
14
14
  provider: "ReleaseProvider",
15
15
  name: str,
16
16
  session: str,
17
+ node: str,
17
18
  entity_picture_url: str | None = None,
18
19
  current_version: str | None = None,
19
20
  latest_version: str | None = None,
@@ -21,11 +22,12 @@ class Discovery:
21
22
  can_build: bool = False,
22
23
  can_restart: bool = False,
23
24
  status: str = "on",
25
+ update_type: str | None = "Update",
24
26
  update_policy: str | None = None,
25
27
  update_last_attempt: float | None = None,
26
28
  release_url: str | None = None,
27
29
  release_summary: str | None = None,
28
- title_template: str = "Update for {name} on {node}",
30
+ title_template: str = "{discovery.update_type} for {discovery.name} on {discovery.node}",
29
31
  device_icon: str | None = None,
30
32
  custom: dict[str, Any] | None = None,
31
33
  features: list[str] | None = None,
@@ -34,6 +36,7 @@ class Discovery:
34
36
  self.source_type: str = provider.source_type
35
37
  self.session: str = session
36
38
  self.name: str = name
39
+ self.node: str = node
37
40
  self.entity_picture_url: str | None = entity_picture_url
38
41
  self.current_version: str | None = current_version
39
42
  self.latest_version: str | None = latest_version
@@ -44,6 +47,7 @@ class Discovery:
44
47
  self.release_summary: str | None = release_summary
45
48
  self.title_template: str | None = title_template
46
49
  self.device_icon: str | None = device_icon
50
+ self.update_type: str | None = update_type
47
51
  self.status: str = status
48
52
  self.update_policy: str | None = update_policy
49
53
  self.update_last_attempt: float | None = update_last_attempt
@@ -54,6 +58,12 @@ class Discovery:
54
58
  """Build a custom string representation"""
55
59
  return f"Discovery('{self.name}','{self.source_type}',current={self.current_version},latest={self.latest_version})"
56
60
 
61
+ @property
62
+ def title(self) -> str:
63
+ if self.title_template:
64
+ return self.title_template.format(discovery=self)
65
+ return self.name
66
+
57
67
 
58
68
  class ReleaseProvider:
59
69
  """Abstract base class for release providers, such as container scanners or package managers API calls"""
@@ -309,7 +309,6 @@ class MqttPublisher:
309
309
  self.state_topic(discovery),
310
310
  hass_format_state(
311
311
  discovery,
312
- self.node_cfg.name,
313
312
  discovery.session,
314
313
  in_progress=in_progress,
315
314
  ),
@@ -322,7 +321,6 @@ class MqttPublisher:
322
321
  hass_format_config(
323
322
  discovery=discovery,
324
323
  object_id=object_id,
325
- node_name=self.node_cfg.name,
326
324
  area=self.hass_cfg.area,
327
325
  state_topic=self.state_topic(discovery),
328
326
  command_topic=self.command_topic(discovery.provider),
@@ -1,11 +1,14 @@
1
+ import os
1
2
  import tempfile
2
3
  import uuid
4
+ from collections import namedtuple
3
5
  from pathlib import Path
6
+ from unittest.mock import Mock, patch
4
7
 
5
8
  import pytest
6
9
  from omegaconf import OmegaConf
7
10
 
8
- from updates2mqtt.config import MqttConfig, PackageUpdateInfo, load_app_config, load_package_info
11
+ from updates2mqtt.config import LogLevel, MqttConfig, PackageUpdateInfo, load_app_config, load_package_info
9
12
 
10
13
  EXAMPLES_ROOT = "examples"
11
14
  examples = [str(p.name) for p in Path(EXAMPLES_ROOT).iterdir() if p.name.startswith("config")]
@@ -57,6 +60,33 @@ def test_round_trip_config() -> None:
57
60
  assert reloaded_config.mqtt.user == "myuser"
58
61
 
59
62
 
63
+ uname: type = namedtuple("uname", ["nodename"])
64
+
65
+
66
+ @patch.dict(
67
+ os.environ,
68
+ {
69
+ "MQTT_HOST": "192.168.3.4",
70
+ "MQTT_PORT": "2883",
71
+ "MQTT_USER": "u2macct",
72
+ "MQTT_PASS": "toosecret123!",
73
+ "U2M_LOG_LEVEL": "WARNING",
74
+ "U2M_AUTOGEN_CONFIG": "0",
75
+ },
76
+ )
77
+ @patch("os.uname", Mock(return_value=uname("xunit003a")))
78
+ def test_env_only_config() -> None:
79
+ generated_config = load_app_config(Path("no_such_dir/no_such_file.yaml"))
80
+ assert generated_config is not None
81
+ assert generated_config.mqtt.host == "192.168.3.4"
82
+ assert generated_config.mqtt.port == 2883
83
+ assert generated_config.mqtt.user == "u2macct"
84
+ assert generated_config.mqtt.password == "toosecret123!" # noqa: S105
85
+ assert generated_config.node.name == "xunit003a"
86
+ assert generated_config.log.level == LogLevel.WARNING
87
+ assert not Path("no_such_dir").exists()
88
+
89
+
60
90
  def test_package_config() -> None:
61
91
  validated_pkg_info: dict[str, PackageUpdateInfo] = load_package_info(Path("common_packages.yaml"))
62
92
  assert validated_pkg_info is not None
@@ -80,7 +80,7 @@ def test_discover_metadata(httpx_mock: HTTPXMock, mock_docker_client: DockerClie
80
80
  def test_build(mock_docker_client: DockerClient, fake_process: FakeProcess, tmpdir: Path) -> None:
81
81
  with patch("docker.from_env", return_value=mock_docker_client):
82
82
  uut = mut.DockerProvider(mut.DockerConfig(discover_metadata={}), {}, mut.NodeConfig())
83
- d = Discovery(uut, "build-test-dummy", session="test-123")
83
+ d = Discovery(uut, "build-test-dummy", "test-123", "node003")
84
84
  fake_process.register("docker compose build", returncode=0)
85
85
  assert uut.build(d, str(tmpdir))
86
86
  fake_process.register("docker compose build", returncode=33)
@@ -8,7 +8,6 @@ def test_formatter_includes_device(mock_discoveries: list[Discovery], monkeypatc
8
8
  msg = hass_format_config(
9
9
  mock_discoveries[0],
10
10
  "obj001",
11
- "testbed01",
12
11
  "state_topic_1",
13
12
  "command_topic_1",
14
13
  force_command_topic=False,
@@ -16,7 +15,7 @@ def test_formatter_includes_device(mock_discoveries: list[Discovery], monkeypatc
16
15
  area="Basement",
17
16
  )
18
17
  assert msg == {
19
- "name": "thing-1 unit_test",
18
+ "name": "TestRun for thing-1 on testbed01",
20
19
  "unique_id": "obj001",
21
20
  "update_policy": None,
22
21
  "can_build": False,
@@ -45,11 +44,9 @@ def test_formatter_includes_device(mock_discoveries: list[Discovery], monkeypatc
45
44
 
46
45
  def test_formatter_excludes_device(mock_discoveries: list[Discovery], monkeypatch) -> None: # noqa: ANN001
47
46
  monkeypatch.setattr(updates2mqtt, "version", "3.0.0")
48
- msg = hass_format_config(
49
- mock_discoveries[0], "obj001", "testbed01", "state_topic_1", "command_topic_1", True, device_creation=False
50
- )
47
+ msg = hass_format_config(mock_discoveries[0], "obj001", "state_topic_1", "command_topic_1", True, device_creation=False)
51
48
  assert msg == {
52
- "name": "thing-1 unit_test on testbed01",
49
+ "name": "TestRun for thing-1 on testbed01",
53
50
  "unique_id": "obj001",
54
51
  "update_policy": None,
55
52
  "can_build": False,
@@ -72,7 +69,7 @@ def test_formatter_excludes_device(mock_discoveries: list[Discovery], monkeypatc
72
69
  def test_formatter_forces_command_topic(mock_discoveries: list[Discovery]) -> None:
73
70
  discovery = mock_discoveries[0]
74
71
  discovery.can_update = False
75
- msg = hass_format_config(discovery, "obj001", "testbed01", "state_topic_1", "command_topic_1", True)
72
+ msg = hass_format_config(discovery, "obj001", "state_topic_1", "command_topic_1", True)
76
73
  assert msg["command_topic"] == "command_topic_1"
77
74
  assert "payload_install" not in msg
78
75
 
@@ -80,6 +77,6 @@ def test_formatter_forces_command_topic(mock_discoveries: list[Discovery]) -> No
80
77
  def test_formatter_no_update_suppresses_command_topic(mock_discoveries: list[Discovery]) -> None:
81
78
  discovery = mock_discoveries[0]
82
79
  discovery.can_update = False
83
- msg = hass_format_config(discovery, "obj001", "testbed01", "state_topic_1", "command_topic_1", False)
80
+ msg = hass_format_config(discovery, "obj001", "state_topic_1", "command_topic_1", False)
84
81
  assert "command_topic" not in msg
85
82
  assert "payload_install" not in msg
@@ -6,6 +6,7 @@ from unittest.mock import Mock, patch
6
6
 
7
7
  import paho.mqtt.client
8
8
  import pytest
9
+ from omegaconf import OmegaConf
9
10
  from paho.mqtt.client import MQTTMessage
10
11
 
11
12
  from updates2mqtt.config import HomeAssistantConfig, MqttConfig, NodeConfig
@@ -30,9 +31,9 @@ def test_publish(mock_mqtt_client: Mock, protocol: str) -> None:
30
31
 
31
32
  @pytest.mark.asyncio
32
33
  async def test_handler(mock_mqtt_client: Mock) -> None:
33
- config = MqttConfig()
34
- hass_config = HomeAssistantConfig()
35
- node_config = NodeConfig()
34
+ config = OmegaConf.structured(MqttConfig)
35
+ hass_config = OmegaConf.structured(HomeAssistantConfig)
36
+ node_config = OmegaConf.structured(NodeConfig)
36
37
  node_config.name = "testing"
37
38
  with patch("updates2mqtt.mqtt.mqtt.Client", new=mock_mqtt_client):
38
39
  uut = MqttPublisher(config, node_config, hass_config)
@@ -40,7 +41,7 @@ async def test_handler(mock_mqtt_client: Mock) -> None:
40
41
 
41
42
  provider = Mock(spec=ReleaseProvider)
42
43
  provider.source_type = "unit_test"
43
- discovery = Discovery(provider, "qux", session="test-mqtt-123")
44
+ discovery = Discovery(provider, "qux", "test-mqtt-123", "node004")
44
45
  provider.command.return_value = discovery
45
46
  provider.hass_state_format.return_value = {}
46
47
 
@@ -58,9 +59,10 @@ async def test_handler(mock_mqtt_client: Mock) -> None:
58
59
 
59
60
 
60
61
  async def test_execute_command_remote(mock_mqtt_client: Mock, mock_provider: ReleaseProvider) -> None:
61
- config = MqttConfig()
62
- hass_config = HomeAssistantConfig()
63
- node_config = NodeConfig("TESTBED")
62
+ config = OmegaConf.structured(MqttConfig)
63
+ hass_config = OmegaConf.structured(HomeAssistantConfig)
64
+ node_config = OmegaConf.structured(NodeConfig)
65
+ node_config.name = "TESTBED"
64
66
 
65
67
  with patch.object(paho.mqtt.client.Client, "__new__", lambda *_args, **_kwargs: mock_mqtt_client):
66
68
  uut = MqttPublisher(config, node_config, hass_config)
@@ -79,7 +81,7 @@ async def test_execute_command_remote(mock_mqtt_client: Mock, mock_provider: Rel
79
81
  {
80
82
  "installed_version": "v2",
81
83
  "latest_version": "v2",
82
- "title": "Update for fooey on TESTBED",
84
+ "title": "Update for fooey on node002",
83
85
  "in_progress": False,
84
86
  }
85
87
  ),
@@ -90,9 +92,10 @@ async def test_execute_command_remote(mock_mqtt_client: Mock, mock_provider: Rel
90
92
 
91
93
  @pytest.mark.asyncio
92
94
  async def test_execute_command_local(mock_mqtt_client: Mock, mock_provider: ReleaseProvider) -> None:
93
- config = MqttConfig()
94
- hass_config = HomeAssistantConfig()
95
- node_config = NodeConfig("TESTBED")
95
+ config = OmegaConf.structured(MqttConfig)
96
+ hass_config = OmegaConf.structured(HomeAssistantConfig)
97
+ node_config = OmegaConf.structured(NodeConfig)
98
+ node_config.name = "TESTBED"
96
99
 
97
100
  with patch.object(paho.mqtt.client.Client, "__new__", lambda *_args, **_kwargs: mock_mqtt_client):
98
101
  uut = MqttPublisher(config, node_config, hass_config)
@@ -101,7 +104,8 @@ async def test_execute_command_local(mock_mqtt_client: Mock, mock_provider: Rele
101
104
 
102
105
  uut.subscribe_hass_command(mock_provider)
103
106
 
104
- discovery = Discovery(mock_provider, "fooey", session="test-mqtt-123", current_version="v1")
107
+ discovery = mock_provider.resolve("test")
108
+ assert discovery is not None
105
109
  uut.local_message(discovery, "install")
106
110
  await asyncio.sleep(1)
107
111
 
@@ -111,7 +115,7 @@ async def test_execute_command_local(mock_mqtt_client: Mock, mock_provider: Rele
111
115
  {
112
116
  "installed_version": "v2",
113
117
  "latest_version": "v2",
114
- "title": "Update for fooey on TESTBED",
118
+ "title": "Update for fooey on node002",
115
119
  "in_progress": False,
116
120
  }
117
121
  ),
@@ -122,9 +126,9 @@ async def test_execute_command_local(mock_mqtt_client: Mock, mock_provider: Rele
122
126
 
123
127
  @pytest.mark.asyncio
124
128
  async def test_stop(mock_mqtt_client: Mock, mock_provider: ReleaseProvider) -> None:
125
- config = MqttConfig()
126
- hass_config = HomeAssistantConfig()
127
- node_config = NodeConfig()
129
+ config = OmegaConf.structured(MqttConfig)
130
+ hass_config = OmegaConf.structured(HomeAssistantConfig)
131
+ node_config = OmegaConf.structured(NodeConfig)
128
132
 
129
133
  with patch.object(paho.mqtt.client.Client, "__new__", lambda *_args, **_kwargs: mock_mqtt_client):
130
134
  uut = MqttPublisher(config, node_config, hass_config)
@@ -150,63 +150,63 @@ wheels = [
150
150
 
151
151
  [[package]]
152
152
  name = "coverage"
153
- version = "7.12.0"
154
- source = { registry = "https://pypi.org/simple" }
155
- sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" }
156
- wheels = [
157
- { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" },
158
- { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" },
159
- { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" },
160
- { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" },
161
- { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" },
162
- { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" },
163
- { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" },
164
- { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" },
165
- { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" },
166
- { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" },
167
- { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" },
168
- { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" },
169
- { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" },
170
- { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" },
171
- { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" },
172
- { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" },
173
- { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" },
174
- { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" },
175
- { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" },
176
- { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" },
177
- { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" },
178
- { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" },
179
- { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" },
180
- { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" },
181
- { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" },
182
- { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" },
183
- { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" },
184
- { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" },
185
- { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" },
186
- { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" },
187
- { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" },
188
- { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" },
189
- { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" },
190
- { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" },
191
- { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" },
192
- { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" },
193
- { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" },
194
- { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" },
195
- { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" },
196
- { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" },
197
- { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" },
198
- { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" },
199
- { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" },
200
- { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" },
201
- { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" },
202
- { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" },
203
- { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" },
204
- { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" },
205
- { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" },
206
- { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" },
207
- { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" },
208
- { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" },
209
- { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" },
153
+ version = "7.13.0"
154
+ source = { registry = "https://pypi.org/simple" }
155
+ sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" }
156
+ wheels = [
157
+ { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" },
158
+ { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" },
159
+ { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" },
160
+ { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" },
161
+ { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" },
162
+ { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" },
163
+ { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" },
164
+ { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" },
165
+ { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" },
166
+ { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" },
167
+ { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" },
168
+ { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" },
169
+ { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" },
170
+ { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" },
171
+ { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" },
172
+ { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" },
173
+ { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" },
174
+ { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" },
175
+ { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" },
176
+ { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" },
177
+ { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" },
178
+ { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" },
179
+ { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" },
180
+ { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" },
181
+ { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" },
182
+ { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" },
183
+ { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" },
184
+ { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" },
185
+ { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" },
186
+ { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" },
187
+ { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" },
188
+ { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" },
189
+ { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" },
190
+ { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" },
191
+ { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" },
192
+ { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" },
193
+ { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" },
194
+ { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" },
195
+ { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" },
196
+ { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" },
197
+ { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" },
198
+ { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" },
199
+ { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" },
200
+ { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" },
201
+ { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" },
202
+ { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" },
203
+ { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" },
204
+ { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" },
205
+ { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" },
206
+ { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" },
207
+ { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" },
208
+ { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" },
209
+ { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" },
210
210
  ]
211
211
 
212
212
  [[package]]
@@ -996,15 +996,15 @@ wheels = [
996
996
 
997
997
  [[package]]
998
998
  name = "pymdown-extensions"
999
- version = "10.17.2"
999
+ version = "10.18"
1000
1000
  source = { registry = "https://pypi.org/simple" }
1001
1001
  dependencies = [
1002
1002
  { name = "markdown" },
1003
1003
  { name = "pyyaml" },
1004
1004
  ]
1005
- sdist = { url = "https://files.pythonhosted.org/packages/25/6d/af5378dbdb379fddd9a277f8b9888c027db480cde70028669ebd009d642a/pymdown_extensions-10.17.2.tar.gz", hash = "sha256:26bb3d7688e651606260c90fb46409fbda70bf9fdc3623c7868643a1aeee4713", size = 847344, upload-time = "2025-11-26T15:43:57.004Z" }
1005
+ sdist = { url = "https://files.pythonhosted.org/packages/d4/95/e4fa281e3f13b3d9c4aaebb21ef44879840325fa418276dd921209a5e9f9/pymdown_extensions-10.18.tar.gz", hash = "sha256:20252abe6367354b24191431617a072ee6be9f68c5afcc74ea5573508a61f9e5", size = 847697, upload-time = "2025-12-07T17:22:12.857Z" }
1006
1006
  wheels = [
1007
- { url = "https://files.pythonhosted.org/packages/93/78/b93cb80bd673bdc9f6ede63d8eb5b4646366953df15667eb3603be57a2b1/pymdown_extensions-10.17.2-py3-none-any.whl", hash = "sha256:bffae79a2e8b9e44aef0d813583a8fea63457b7a23643a43988055b7b79b4992", size = 266556, upload-time = "2025-11-26T15:43:55.162Z" },
1007
+ { url = "https://files.pythonhosted.org/packages/46/a4/aa2bada4a2fd648f40f19affa55d2c01dc7ff5ea9cffd3dfdeb6114951db/pymdown_extensions-10.18-py3-none-any.whl", hash = "sha256:090bca72be43f7d3186374e23c782899dbef9dc153ef24c59dcd3c346f9ffcae", size = 266703, upload-time = "2025-12-07T17:22:11.22Z" },
1008
1008
  ]
1009
1009
 
1010
1010
  [[package]]
@@ -1314,7 +1314,7 @@ wheels = [
1314
1314
 
1315
1315
  [[package]]
1316
1316
  name = "updates2mqtt"
1317
- version = "1.5.0"
1317
+ version = "1.5.1"
1318
1318
  source = { editable = "." }
1319
1319
  dependencies = [
1320
1320
  { name = "docker" },
@@ -1399,11 +1399,11 @@ mkdocs = [
1399
1399
 
1400
1400
  [[package]]
1401
1401
  name = "urllib3"
1402
- version = "2.6.0"
1402
+ version = "2.6.1"
1403
1403
  source = { registry = "https://pypi.org/simple" }
1404
- sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585, upload-time = "2025-12-05T15:08:47.885Z" }
1404
+ sdist = { url = "https://files.pythonhosted.org/packages/5e/1d/0f3a93cca1ac5e8287842ed4eebbd0f7a991315089b1a0b01c7788aa7b63/urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f", size = 432678, upload-time = "2025-12-08T15:25:26.773Z" }
1405
1405
  wheels = [
1406
- { url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083, upload-time = "2025-12-05T15:08:45.983Z" },
1406
+ { url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" },
1407
1407
  ]
1408
1408
 
1409
1409
  [[package]]
@@ -1,7 +0,0 @@
1
- # Minimal Configuration
2
-
3
- Smallest working configuration
4
-
5
- ``` yaml
6
- --8<-- "examples/config.yaml.minimal"
7
- ```
@@ -1,7 +0,0 @@
1
- # Environment File
2
-
3
- Example `.env` file for Docker Compose configuration to separately store the environment variables.
4
-
5
- ``` bash
6
- --8<-- "examples/.env"
7
- ```
@@ -1,33 +0,0 @@
1
- log:
2
- level: INFO
3
- node:
4
- name: Jeys-MacBook-Pro
5
- git_path: /usr/bin/git
6
- healthcheck:
7
- enabled: true
8
- interval: 300
9
- topic_template: healthcheck/{node_name}/updates2mqtt
10
- mqtt:
11
- host: ${oc.env:MQTT_HOST,localhost}
12
- user: ${oc.env:MQTT_USER,null}
13
- password: ${oc.env:MQTT_PASS,null}
14
- port: ${oc.decode:${oc.env:MQTT_PORT,1883}}
15
- topic_root: updates2mqtt
16
- homeassistant:
17
- discovery:
18
- prefix: homeassistant
19
- enabled: true
20
- state_topic_suffix: state
21
- docker:
22
- enabled: true
23
- allow_pull: true
24
- allow_restart: true
25
- allow_build: true
26
- compose_version: v2
27
- default_entity_picture_url: https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png
28
- device_icon: mdi:docker
29
- discover_metadata:
30
- linuxserver.io:
31
- enabled: true
32
- cache_ttl: 604800
33
- scan_interval: 10800
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