esphome 2025.2.1__py3-none-any.whl → 2025.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. esphome/__main__.py +9 -1
  2. esphome/components/api/api_connection.cpp +426 -70
  3. esphome/components/api/api_connection.h +117 -25
  4. esphome/components/api/api_pb2.cpp +33 -0
  5. esphome/components/api/api_pb2.h +4 -0
  6. esphome/components/api/api_server.cpp +2 -2
  7. esphome/components/api/list_entities.cpp +76 -22
  8. esphome/components/api/list_entities.h +1 -0
  9. esphome/components/api/subscribe_state.h +2 -0
  10. esphome/components/audio/__init__.py +1 -1
  11. esphome/components/audio/audio_decoder.cpp +43 -11
  12. esphome/components/audio/audio_reader.cpp +9 -9
  13. esphome/components/audio/audio_reader.h +1 -1
  14. esphome/components/audio/audio_resampler.cpp +4 -2
  15. esphome/components/audio/audio_transfer_buffer.cpp +19 -9
  16. esphome/components/audio/audio_transfer_buffer.h +7 -2
  17. esphome/components/bluetooth_proxy/bluetooth_proxy.h +8 -0
  18. esphome/components/bmp085/bmp085.cpp +1 -1
  19. esphome/components/chsc6x/__init__.py +2 -0
  20. esphome/components/chsc6x/chsc6x_touchscreen.cpp +47 -0
  21. esphome/components/chsc6x/chsc6x_touchscreen.h +34 -0
  22. esphome/components/chsc6x/touchscreen.py +33 -0
  23. esphome/components/climate/__init__.py +0 -1
  24. esphome/components/cst816/binary_sensor/__init__.py +2 -25
  25. esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +3 -14
  26. esphome/components/cst816/touchscreen/cst816_touchscreen.h +0 -4
  27. esphome/components/esp32_ble_beacon/__init__.py +3 -1
  28. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +2 -2
  29. esphome/components/esp8266/gpio.py +1 -2
  30. esphome/components/font/__init__.py +198 -215
  31. esphome/components/font/font.cpp +4 -4
  32. esphome/components/font/font.h +1 -0
  33. esphome/components/graph/graph.cpp +4 -0
  34. esphome/components/graph/graph.h +4 -0
  35. esphome/components/haier/climate.py +11 -10
  36. esphome/components/hbridge/switch/hbridge_switch.cpp +2 -2
  37. esphome/components/heatpumpir/climate.py +2 -1
  38. esphome/components/heatpumpir/heatpumpir.cpp +1 -0
  39. esphome/components/heatpumpir/heatpumpir.h +1 -0
  40. esphome/components/i2c/__init__.py +6 -6
  41. esphome/components/i2c/i2c_bus_esp_idf.cpp +6 -2
  42. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
  43. esphome/components/ili9xxx/display.py +1 -0
  44. esphome/components/ili9xxx/ili9xxx_display.h +5 -0
  45. esphome/components/ili9xxx/ili9xxx_init.h +59 -0
  46. esphome/components/ld2450/__init__.py +51 -0
  47. esphome/components/ld2450/binary_sensor.py +47 -0
  48. esphome/components/ld2450/button/__init__.py +45 -0
  49. esphome/components/ld2450/button/reset_button.cpp +9 -0
  50. esphome/components/ld2450/button/reset_button.h +18 -0
  51. esphome/components/ld2450/button/restart_button.cpp +9 -0
  52. esphome/components/ld2450/button/restart_button.h +18 -0
  53. esphome/components/ld2450/ld2450.cpp +876 -0
  54. esphome/components/ld2450/ld2450.h +234 -0
  55. esphome/components/ld2450/number/__init__.py +121 -0
  56. esphome/components/ld2450/number/presence_timeout_number.cpp +12 -0
  57. esphome/components/ld2450/number/presence_timeout_number.h +18 -0
  58. esphome/components/ld2450/number/zone_coordinate_number.cpp +14 -0
  59. esphome/components/ld2450/number/zone_coordinate_number.h +19 -0
  60. esphome/components/ld2450/select/__init__.py +56 -0
  61. esphome/components/ld2450/select/baud_rate_select.cpp +12 -0
  62. esphome/components/ld2450/select/baud_rate_select.h +18 -0
  63. esphome/components/ld2450/select/zone_type_select.cpp +12 -0
  64. esphome/components/ld2450/select/zone_type_select.h +18 -0
  65. esphome/components/ld2450/sensor.py +156 -0
  66. esphome/components/ld2450/switch/__init__.py +45 -0
  67. esphome/components/ld2450/switch/bluetooth_switch.cpp +12 -0
  68. esphome/components/ld2450/switch/bluetooth_switch.h +18 -0
  69. esphome/components/ld2450/switch/multi_target_switch.cpp +12 -0
  70. esphome/components/ld2450/switch/multi_target_switch.h +18 -0
  71. esphome/components/ld2450/text_sensor.py +62 -0
  72. esphome/components/ltr390/ltr390.cpp +7 -7
  73. esphome/components/ltr390/ltr390.h +0 -1
  74. esphome/components/lvgl/defines.py +0 -2
  75. esphome/components/lvgl/font.cpp +1 -1
  76. esphome/components/lvgl/lvgl_esphome.cpp +27 -19
  77. esphome/components/lvgl/widgets/img.py +1 -3
  78. esphome/components/mcp2515/mcp2515.cpp +1 -0
  79. esphome/components/mdns/__init__.py +1 -1
  80. esphome/components/mixer/speaker/mixer_speaker.cpp +6 -1
  81. esphome/components/mixer/speaker/mixer_speaker.h +2 -0
  82. esphome/components/mlx90393/sensor.py +53 -33
  83. esphome/components/mlx90393/sensor_mlx90393.cpp +4 -0
  84. esphome/components/mlx90393/sensor_mlx90393.h +8 -3
  85. esphome/components/mqtt/__init__.py +2 -2
  86. esphome/components/msa3xx/__init__.py +189 -0
  87. esphome/components/msa3xx/binary_sensor.py +40 -0
  88. esphome/components/msa3xx/msa3xx.cpp +417 -0
  89. esphome/components/msa3xx/msa3xx.h +311 -0
  90. esphome/components/msa3xx/sensor.py +42 -0
  91. esphome/components/msa3xx/text_sensor.py +38 -0
  92. esphome/components/nfc/binary_sensor/__init__.py +4 -4
  93. esphome/components/opentherm/binary_sensor/__init__.py +4 -4
  94. esphome/components/opentherm/generate.py +6 -6
  95. esphome/components/opentherm/sensor/__init__.py +5 -6
  96. esphome/components/packages/__init__.py +35 -11
  97. esphome/components/pn532/binary_sensor.py +4 -4
  98. esphome/components/rc522/binary_sensor.py +4 -4
  99. esphome/components/resampler/speaker/resampler_speaker.h +2 -0
  100. esphome/components/socket/bsd_sockets_impl.cpp +1 -0
  101. esphome/components/socket/lwip_sockets_impl.cpp +1 -0
  102. esphome/components/socket/socket.h +3 -1
  103. esphome/components/speaker/speaker.h +2 -2
  104. esphome/components/ssd1306_base/__init__.py +7 -7
  105. esphome/components/thermostat/climate.py +1 -1
  106. esphome/components/tmp1075/tmp1075.cpp +7 -11
  107. esphome/components/tmp1075/tmp1075.h +1 -2
  108. esphome/components/tormatic/__init__.py +1 -0
  109. esphome/components/tormatic/cover.py +47 -0
  110. esphome/components/tormatic/tormatic_cover.cpp +355 -0
  111. esphome/components/tormatic/tormatic_cover.h +60 -0
  112. esphome/components/tormatic/tormatic_protocol.h +211 -0
  113. esphome/components/touchscreen/binary_sensor/__init__.py +3 -0
  114. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +7 -1
  115. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +3 -1
  116. esphome/components/touchscreen/touchscreen.cpp +3 -4
  117. esphome/components/udp/udp_component.h +4 -1
  118. esphome/components/web_server/list_entities.cpp +70 -66
  119. esphome/components/web_server/list_entities.h +43 -22
  120. esphome/components/web_server/web_server.cpp +345 -68
  121. esphome/components/web_server/web_server.h +138 -6
  122. esphome/components/web_server_base/__init__.py +1 -1
  123. esphome/components/web_server_idf/__init__.py +2 -0
  124. esphome/components/web_server_idf/web_server_idf.cpp +177 -30
  125. esphome/components/web_server_idf/web_server_idf.h +53 -4
  126. esphome/config_validation.py +23 -125
  127. esphome/const.py +5 -1
  128. esphome/core/config.py +15 -6
  129. esphome/core/defines.h +1 -1
  130. esphome/core/helpers.h +24 -3
  131. esphome/core/time.cpp +1 -0
  132. esphome/cpp_generator.py +3 -3
  133. esphome/dashboard/core.py +30 -21
  134. esphome/dashboard/dns.py +7 -1
  135. esphome/dashboard/entries.py +83 -16
  136. esphome/dashboard/settings.py +0 -4
  137. esphome/dashboard/status/mdns.py +43 -14
  138. esphome/dashboard/status/mqtt.py +22 -9
  139. esphome/dashboard/status/ping.py +54 -10
  140. esphome/dashboard/web_server.py +56 -24
  141. esphome/storage_json.py +4 -0
  142. esphome/wizard.py +13 -17
  143. esphome/writer.py +1 -3
  144. esphome/yaml_util.py +36 -33
  145. esphome/zeroconf.py +9 -21
  146. {esphome-2025.2.1.dist-info → esphome-2025.3.0.dist-info}/METADATA +7 -7
  147. {esphome-2025.2.1.dist-info → esphome-2025.3.0.dist-info}/RECORD +151 -111
  148. esphome/components/cst816/binary_sensor/cst816_button.h +0 -27
  149. {esphome-2025.2.1.dist-info → esphome-2025.3.0.dist-info}/LICENSE +0 -0
  150. {esphome-2025.2.1.dist-info → esphome-2025.3.0.dist-info}/WHEEL +0 -0
  151. {esphome-2025.2.1.dist-info → esphome-2025.3.0.dist-info}/entry_points.txt +0 -0
  152. {esphome-2025.2.1.dist-info → esphome-2025.3.0.dist-info}/top_level.txt +0 -0
esphome/wizard.py CHANGED
@@ -144,17 +144,17 @@ def wizard_file(**kwargs):
144
144
 
145
145
  # Configure API
146
146
  if "password" in kwargs:
147
- config += f" password: \"{kwargs['password']}\"\n"
147
+ config += f' password: "{kwargs["password"]}"\n'
148
148
  if "api_encryption_key" in kwargs:
149
- config += f" encryption:\n key: \"{kwargs['api_encryption_key']}\"\n"
149
+ config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n'
150
150
 
151
151
  # Configure OTA
152
152
  config += "\nota:\n"
153
153
  config += " - platform: esphome\n"
154
154
  if "ota_password" in kwargs:
155
- config += f" password: \"{kwargs['ota_password']}\""
155
+ config += f' password: "{kwargs["ota_password"]}"'
156
156
  elif "password" in kwargs:
157
- config += f" password: \"{kwargs['password']}\""
157
+ config += f' password: "{kwargs["password"]}"'
158
158
 
159
159
  # Configuring wifi
160
160
  config += "\n\nwifi:\n"
@@ -181,18 +181,14 @@ def wizard_file(**kwargs):
181
181
  password: "{fallback_psk}"
182
182
 
183
183
  captive_portal:
184
- """.format(
185
- **kwargs
186
- )
184
+ """.format(**kwargs)
187
185
  else:
188
186
  config += """
189
187
  # Enable fallback hotspot in case wifi connection fails
190
188
  ap:
191
189
  ssid: "{fallback_name}"
192
190
  password: "{fallback_psk}"
193
- """.format(
194
- **kwargs
195
- )
191
+ """.format(**kwargs)
196
192
 
197
193
  return config
198
194
 
@@ -388,19 +384,19 @@ def wizard(path):
388
384
  safe_print()
389
385
  # Don't sleep because user needs to copy link
390
386
  if platform == "ESP32":
391
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcu-32s')}\".")
387
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcu-32s")}".')
392
388
  boards_list = esp32_boards.BOARDS.items()
393
389
  elif platform == "ESP8266":
394
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcuv2')}\".")
390
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcuv2")}".')
395
391
  boards_list = esp8266_boards.BOARDS.items()
396
392
  elif platform == "BK72XX":
397
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'cb2s')}\".")
393
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "cb2s")}".')
398
394
  boards_list = bk72xx_boards.BOARDS.items()
399
395
  elif platform == "RTL87XX":
400
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'wr3')}\".")
396
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "wr3")}".')
401
397
  boards_list = rtl87xx_boards.BOARDS.items()
402
398
  elif platform == "RP2040":
403
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'rpipicow')}\".")
399
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "rpipicow")}".')
404
400
  boards_list = rp2040_boards.BOARDS.items()
405
401
 
406
402
  else:
@@ -439,7 +435,7 @@ def wizard(path):
439
435
  f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?"
440
436
  )
441
437
  sleep(1.5)
442
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".")
438
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "Abraham Linksys")}".')
443
439
  while True:
444
440
  ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): "))
445
441
  try:
@@ -465,7 +461,7 @@ def wizard(path):
465
461
  f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)"
466
462
  )
467
463
  safe_print()
468
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"")
464
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "PASSWORD42")}"')
469
465
  sleep(0.5)
470
466
  psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): "))
471
467
  safe_print(
esphome/writer.py CHANGED
@@ -212,9 +212,7 @@ def write_platformio_project():
212
212
  write_platformio_ini(content)
213
213
 
214
214
 
215
- DEFINES_H_FORMAT = (
216
- ESPHOME_H_FORMAT
217
- ) = """\
215
+ DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
218
216
  #pragma once
219
217
  #include "esphome/core/macros.h"
220
218
  {}
esphome/yaml_util.py CHANGED
@@ -273,48 +273,18 @@ class ESPHomeLoaderMixin:
273
273
 
274
274
  @_add_data_ref
275
275
  def construct_include(self, node):
276
+ from esphome.const import CONF_VARS
277
+
276
278
  def extract_file_vars(node):
277
279
  fields = self.construct_yaml_map(node)
278
280
  file = fields.get("file")
279
281
  if file is None:
280
282
  raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark)
281
- vars = fields.get("vars")
283
+ vars = fields.get(CONF_VARS)
282
284
  if vars:
283
285
  vars = {k: str(v) for k, v in vars.items()}
284
286
  return file, vars
285
287
 
286
- def substitute_vars(config, vars):
287
- from esphome.components import substitutions
288
- from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS
289
-
290
- org_subs = None
291
- result = config
292
- if not isinstance(config, dict):
293
- # when the included yaml contains a list or a scalar
294
- # wrap it into an OrderedDict because do_substitution_pass expects it
295
- result = OrderedDict([("yaml", config)])
296
- elif CONF_SUBSTITUTIONS in result:
297
- org_subs = result.pop(CONF_SUBSTITUTIONS)
298
-
299
- defaults = {}
300
- if CONF_DEFAULTS in result:
301
- defaults = result.pop(CONF_DEFAULTS)
302
-
303
- result[CONF_SUBSTITUTIONS] = vars
304
- for k, v in defaults.items():
305
- if k not in result[CONF_SUBSTITUTIONS]:
306
- result[CONF_SUBSTITUTIONS][k] = v
307
-
308
- # Ignore missing vars that refer to the top level substitutions
309
- substitutions.do_substitution_pass(result, None, ignore_missing=True)
310
- result.pop(CONF_SUBSTITUTIONS)
311
-
312
- if not isinstance(config, dict):
313
- result = result["yaml"] # unwrap the result
314
- elif org_subs:
315
- result[CONF_SUBSTITUTIONS] = org_subs
316
- return result
317
-
318
288
  if isinstance(node, yaml.nodes.MappingNode):
319
289
  file, vars = extract_file_vars(node)
320
290
  else:
@@ -432,6 +402,39 @@ def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any:
432
402
  )
433
403
 
434
404
 
405
+ def substitute_vars(config, vars):
406
+ from esphome.components import substitutions
407
+ from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS
408
+
409
+ org_subs = None
410
+ result = config
411
+ if not isinstance(config, dict):
412
+ # when the included yaml contains a list or a scalar
413
+ # wrap it into an OrderedDict because do_substitution_pass expects it
414
+ result = OrderedDict([("yaml", config)])
415
+ elif CONF_SUBSTITUTIONS in result:
416
+ org_subs = result.pop(CONF_SUBSTITUTIONS)
417
+
418
+ defaults = {}
419
+ if CONF_DEFAULTS in result:
420
+ defaults = result.pop(CONF_DEFAULTS)
421
+
422
+ result[CONF_SUBSTITUTIONS] = vars
423
+ for k, v in defaults.items():
424
+ if k not in result[CONF_SUBSTITUTIONS]:
425
+ result[CONF_SUBSTITUTIONS][k] = v
426
+
427
+ # Ignore missing vars that refer to the top level substitutions
428
+ substitutions.do_substitution_pass(result, None, ignore_missing=True)
429
+ result.pop(CONF_SUBSTITUTIONS)
430
+
431
+ if not isinstance(config, dict):
432
+ result = result["yaml"] # unwrap the result
433
+ elif org_subs:
434
+ result[CONF_SUBSTITUTIONS] = org_subs
435
+ return result
436
+
437
+
435
438
  def _load_yaml_internal(fname: str) -> Any:
436
439
  """Load a YAML file."""
437
440
  try:
esphome/zeroconf.py CHANGED
@@ -5,7 +5,13 @@ from dataclasses import dataclass
5
5
  import logging
6
6
  from typing import Callable
7
7
 
8
- from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf
8
+ from zeroconf import (
9
+ AddressResolver,
10
+ IPVersion,
11
+ ServiceInfo,
12
+ ServiceStateChange,
13
+ Zeroconf,
14
+ )
9
15
  from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
10
16
 
11
17
  from esphome.storage_json import StorageJSON, ext_storage_path
@@ -16,15 +22,6 @@ _LOGGER = logging.getLogger(__name__)
16
22
  _BACKGROUND_TASKS: set[asyncio.Task] = set()
17
23
 
18
24
 
19
- class HostResolver(ServiceInfo):
20
- """Resolve a host name to an IP address."""
21
-
22
- @property
23
- def _is_complete(self) -> bool:
24
- """The ServiceInfo has all expected properties."""
25
- return bool(self._ipv4_addresses)
26
-
27
-
28
25
  class DashboardStatus:
29
26
  def __init__(self, on_update: Callable[[dict[str, bool | None], []]]) -> None:
30
27
  """Initialize the dashboard status."""
@@ -166,19 +163,10 @@ class DashboardImportDiscovery:
166
163
  )
167
164
 
168
165
 
169
- def _make_host_resolver(host: str) -> HostResolver:
170
- """Create a new HostResolver for the given host name."""
171
- name = host.partition(".")[0]
172
- info = HostResolver(
173
- ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}", server=f"{name}.local."
174
- )
175
- return info
176
-
177
-
178
166
  class EsphomeZeroconf(Zeroconf):
179
167
  def resolve_host(self, host: str, timeout: float = 3.0) -> list[str] | None:
180
168
  """Resolve a host name to an IP address."""
181
- info = _make_host_resolver(host)
169
+ info = AddressResolver(f"{host.partition('.')[0]}.local.")
182
170
  if (
183
171
  info.load_from_cache(self)
184
172
  or (timeout and info.request(self, timeout * 1000))
@@ -192,7 +180,7 @@ class AsyncEsphomeZeroconf(AsyncZeroconf):
192
180
  self, host: str, timeout: float = 3.0
193
181
  ) -> list[str] | None:
194
182
  """Resolve a host name to an IP address."""
195
- info = _make_host_resolver(host)
183
+ info = AddressResolver(f"{host.partition('.')[0]}.local.")
196
184
  if (
197
185
  info.load_from_cache(self.zeroconf)
198
186
  or (timeout and await info.async_request(self.zeroconf, timeout * 1000))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esphome
3
- Version: 2025.2.1
3
+ Version: 2025.3.0
4
4
  Summary: ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems.
5
5
  Author-email: The ESPHome Authors <esphome@nabucasa.com>
6
6
  License: MIT
@@ -23,22 +23,22 @@ Classifier: Topic :: Home Automation
23
23
  Requires-Python: >=3.9.0
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
- Requires-Dist: cryptography ==43.0.0
26
+ Requires-Dist: cryptography ==44.0.2
27
27
  Requires-Dist: voluptuous ==0.14.2
28
28
  Requires-Dist: PyYAML ==6.0.2
29
29
  Requires-Dist: paho-mqtt ==1.6.1
30
30
  Requires-Dist: colorama ==0.4.6
31
31
  Requires-Dist: icmplib ==3.0.4
32
- Requires-Dist: tornado ==6.4
32
+ Requires-Dist: tornado ==6.4.2
33
33
  Requires-Dist: tzlocal ==5.2
34
34
  Requires-Dist: tzdata >=2021.1
35
35
  Requires-Dist: pyserial ==3.5
36
36
  Requires-Dist: platformio ==6.1.16
37
- Requires-Dist: esptool ==4.7.0
37
+ Requires-Dist: esptool ==4.8.1
38
38
  Requires-Dist: click ==8.1.7
39
39
  Requires-Dist: esphome-dashboard ==20250212.0
40
- Requires-Dist: aioesphomeapi ==29.1.1
41
- Requires-Dist: zeroconf ==0.145.1
40
+ Requires-Dist: aioesphomeapi ==29.6.0
41
+ Requires-Dist: zeroconf ==0.146.1
42
42
  Requires-Dist: puremagic ==1.27
43
43
  Requires-Dist: ruamel.yaml ==0.18.6
44
44
  Requires-Dist: esphome-glyphsets ==0.1.0
@@ -57,7 +57,7 @@ Requires-Dist: cairosvg ==2.7.1 ; extra == 'displays'
57
57
  Provides-Extra: test
58
58
  Requires-Dist: pylint ==3.2.7 ; extra == 'test'
59
59
  Requires-Dist: flake8 ==7.0.0 ; extra == 'test'
60
- Requires-Dist: black ==24.4.2 ; extra == 'test'
60
+ Requires-Dist: ruff ==0.9.2 ; extra == 'test'
61
61
  Requires-Dist: pyupgrade ==3.15.2 ; extra == 'test'
62
62
  Requires-Dist: pre-commit ; extra == 'test'
63
63
  Requires-Dist: pytest ==8.2.0 ; extra == 'test'