diff --git a/.env b/.env index 8004beba..cb704ac7 100644 --- a/.env +++ b/.env @@ -11,7 +11,7 @@ DOCKER_COMPOSE_DATA_DIR=${HOME}/.local/share/net.akkudoktor.eos # ----------------------------------------------------------------------------- # Image / build # ----------------------------------------------------------------------------- -VERSION=0.3.0.dev2607201457168588 +VERSION=0.3.0.dev2607231185985809 PYTHON_VERSION=3.13.9 # ----------------------------------------------------------------------------- diff --git a/config.yaml b/config.yaml index 37862c64..611752ff 100644 --- a/config.yaml +++ b/config.yaml @@ -6,7 +6,7 @@ # the root directory (no add-on folder as usual). name: "Akkudoktor-EOS" -version: "0.3.0.dev2607201457168588" +version: "0.3.0.dev2607231185985809" slug: "eos" description: "Akkudoktor-EOS add-on" url: "https://github.com/Akkudoktor-EOS/EOS" diff --git a/docs/_generated/configserver.md b/docs/_generated/configserver.md index 57e537df..6890ebba 100644 --- a/docs/_generated/configserver.md +++ b/docs/_generated/configserver.md @@ -7,11 +7,11 @@ | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[str]` | `rw` | `None` | EOSdash server IP address. Defaults to EOS server IP address. | -| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `None` | EOSdash server IP port number. Defaults to EOS server IP port number + 1. | +| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `str` | `rw` | `127.0.0.1` | EOSdash server IP address. Defaults to EOS server IP address. | +| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `int` | `rw` | `8504` | EOSdash server IP port number. Defaults to 8504. | | eosdash_supervise_interval_sec | `EOS_SERVER__EOSDASH_SUPERVISE_INTERVAL_SEC` | `int` | `rw` | `10` | Supervision interval for EOS server to supervise EOSdash [seconds]. | -| host | `EOS_SERVER__HOST` | `Optional[str]` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. | -| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. | +| host | `EOS_SERVER__HOST` | `str` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. | +| port | `EOS_SERVER__PORT` | `int` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. | | reload | `EOS_SERVER__RELOAD` | `Optional[bool]` | `rw` | `False` | Enable server auto-reload for debugging or development. Default is False. Monitors the package directory for changes and reloads the server. | | run_as_user | `EOS_SERVER__RUN_AS_USER` | `Optional[str]` | `rw` | `None` | The name of the target user to switch to. If ``None`` (default), the current effective user is used and no privilege change is attempted. | | startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. Defaults to True. | diff --git a/docs/_generated/openapi.md b/docs/_generated/openapi.md index a8fdf9da..89349ccd 100644 --- a/docs/_generated/openapi.md +++ b/docs/_generated/openapi.md @@ -1,6 +1,6 @@ # Akkudoktor-EOS -**Version**: `v0.3.0.dev2607210805856856` +**Version**: `v0.3.0.dev2607231185985809` **Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period. diff --git a/openapi.json b/openapi.json index fa1ae3ab..a8b57f6c 100644 --- a/openapi.json +++ b/openapi.json @@ -8,7 +8,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "v0.3.0.dev2607210805856856" + "version": "v0.3.0.dev2607231185985809" }, "paths": { "/v1/admin/cache/clear": { @@ -8573,14 +8573,7 @@ "ServerCommonSettings": { "properties": { "host": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], + "type": "string", "title": "Host", "description": "EOS server IP address. Defaults to 127.0.0.1.", "default": "127.0.0.1", @@ -8590,14 +8583,7 @@ ] }, "port": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], + "type": "integer", "title": "Port", "description": "EOS server IP port number. Defaults to 8503.", "default": 8503, @@ -8632,32 +8618,20 @@ "default": true }, "eosdash_host": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], + "type": "string", "title": "Eosdash Host", "description": "EOSdash server IP address. Defaults to EOS server IP address.", + "default": "127.0.0.1", "examples": [ "127.0.0.1", "localhost" ] }, "eosdash_port": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], + "type": "integer", "title": "Eosdash Port", - "description": "EOSdash server IP port number. Defaults to EOS server IP port number + 1.", + "description": "EOSdash server IP port number. Defaults to 8504.", + "default": 8504, "examples": [ 8504 ] diff --git a/src/akkudoktoreos/config/config.py b/src/akkudoktoreos/config/config.py index eab796ef..192dd450 100644 --- a/src/akkudoktoreos/config/config.py +++ b/src/akkudoktoreos/config/config.py @@ -464,13 +464,23 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults): # Server host if args.host is not None: - set_nested(settings, "server.host", args.host) - logger.debug(f"CLI arg: server.host set to {args.host}") + if "dash" in sys.argv[0]: + # Config is read from EOSdash + set_nested(settings, "server.eosdash_host", args.host) + logger.debug(f"CLI arg: server.eosdash_host set to {args.host}") + else: + set_nested(settings, "server.host", args.host) + logger.debug(f"CLI arg: server.host set to {args.host}") # Server port if args.port is not None: - set_nested(settings, "server.port", args.port) - logger.debug(f"CLI arg: server.port set to {args.port}") + if "dash" in sys.argv[0]: + # Config is read from EOSdash + set_nested(settings, "server.eosdash_port", args.port) + logger.debug(f"CLI arg: server.eosdash_port set to {args.port}") + else: + set_nested(settings, "server.port", args.port) + logger.debug(f"CLI arg: server.port set to {args.port}") # Server startup_eosdash if args.startup_eosdash is not None: diff --git a/src/akkudoktoreos/server/eosdash.py b/src/akkudoktoreos/server/eosdash.py index 28976390..8870132f 100644 --- a/src/akkudoktoreos/server/eosdash.py +++ b/src/akkudoktoreos/server/eosdash.py @@ -39,6 +39,9 @@ from akkudoktoreos.server.server import ( ) from akkudoktoreos.utils.stringutil import str2bool +# Config is the EOS configuration +# Config uses sysv args for initialisation +# sysv host and port is set for EOSdash but expected to be set for EOS config_eos = get_config(init=True) diff --git a/src/akkudoktoreos/server/server.py b/src/akkudoktoreos/server/server.py index fb112002..aca2edbc 100644 --- a/src/akkudoktoreos/server/server.py +++ b/src/akkudoktoreos/server/server.py @@ -7,13 +7,13 @@ import pwd import re import socket import time -from typing import Optional +from typing import Any, Optional import psutil from loguru import logger from pydantic import Field, field_validator -from akkudoktoreos.config.configabc import SettingsBaseModel +from akkudoktoreos.config.configabc import SettingsBaseModel, is_home_assistant_addon from akkudoktoreos.core.coreabc import get_config @@ -22,6 +22,11 @@ def get_default_host() -> str: return "127.0.0.1" +def get_default_port() -> int: + """Default port for EOS.""" + return 8503 + + def get_host_ip() -> str: """IP address of the host machine. @@ -326,15 +331,15 @@ def fix_data_directories_permissions(run_as_user: Optional[str] = None) -> None: class ServerCommonSettings(SettingsBaseModel): """Server Configuration.""" - host: Optional[str] = Field( + host: str = Field( default=get_default_host(), json_schema_extra={ "description": "EOS server IP address. Defaults to 127.0.0.1.", "examples": ["127.0.0.1", "localhost"], }, ) - port: Optional[int] = Field( - default=8503, + port: int = Field( + default=get_default_port(), json_schema_extra={ "description": "EOS server IP port number. Defaults to 8503.", "examples": [ @@ -349,17 +354,17 @@ class ServerCommonSettings(SettingsBaseModel): default=True, json_schema_extra={"description": "EOS server to start EOSdash server. Defaults to True."}, ) - eosdash_host: Optional[str] = Field( - default=None, + eosdash_host: str = Field( + default=get_default_host(), json_schema_extra={ "description": "EOSdash server IP address. Defaults to EOS server IP address.", "examples": ["127.0.0.1", "localhost"], }, ) - eosdash_port: Optional[int] = Field( - default=None, + eosdash_port: int = Field( + default=get_default_port() + 1, json_schema_extra={ - "description": "EOSdash server IP port number. Defaults to EOS server IP port number + 1.", + "description": "EOSdash server IP port number. Defaults to 8504.", "examples": [ 8504, ], @@ -406,12 +411,34 @@ class ServerCommonSettings(SettingsBaseModel): value = validate_ip_or_hostname(value) return value - @field_validator("port", "eosdash_port") - def validate_server_port(cls, value: Optional[int]) -> Optional[int]: - if value is not None and not (1024 <= value <= 49151): + @field_validator("port", mode="before") + def validate_server_port(cls, value: Any) -> int: + default_port = get_default_port() + if value is None: + return default_port + value = int(value) + if is_home_assistant_addon() and value != default_port: + raise ValueError( + f"Server port number `{default_port}` for Home Assistant add-on can not be changed." + ) + if not (1024 <= value <= 49151): raise ValueError("Server port number must be between 1024 and 49151.") return value + @field_validator("eosdash_port", mode="before") + def validate_eosdash_port(cls, value: Any) -> int: + default_port = get_default_port() + 1 + if value is None: + return default_port + value = int(value) + if is_home_assistant_addon() and value != default_port: + raise ValueError( + f"EOSdash port number `{default_port}` for Home Assistant add-on can not be changed." + ) + if not (1024 <= value <= 49151): + raise ValueError("EOSdash port number must be between 1024 and 49151.") + return value + @field_validator("run_as_user") def validate_user(cls, value: Optional[str]) -> Optional[str]: if value is not None: diff --git a/tests/test_config.py b/tests/test_config.py index 4f485b89..dd8f59d0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -80,7 +80,7 @@ def test_computed_paths(config_eos): def test_config_from_env(monkeypatch, config_eos): """Test configuration from env.""" assert config_eos.server.port == 8503 - assert config_eos.server.eosdash_port is None + assert config_eos.server.eosdash_port == 8504 monkeypatch.setenv("EOS_SERVER__PORT", "8553") monkeypatch.setenv("EOS_SERVER__EOSDASH_PORT", "8555") diff --git a/tests/test_server.py b/tests/test_server.py index abf04fb7..ab8c17b6 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,9 +11,16 @@ import pytest import requests from conftest import cleanup_eos_eosdash from loguru import logger +from pydantic import ValidationError +from akkudoktoreos.config.configabc import is_home_assistant_addon from akkudoktoreos.core.version import __version__ -from akkudoktoreos.server.server import get_default_host, wait_for_port_free +from akkudoktoreos.server.server import ( + ServerCommonSettings, + get_default_host, + get_default_port, + wait_for_port_free, +) class TestServer: @@ -45,6 +52,38 @@ class TestServer: assert str(data_ouput_path).startswith(eos_dir) +class TestServerSettingsValidation: + """Test the port restrictions in ServerCommonSettings when running as HA addon.""" + + def test_ha_addon_default_ports_ok(self, config_eos, monkeypatch): + """Default ports are accepted in HA addon mode.""" + monkeypatch.setattr('akkudoktoreos.server.server.is_home_assistant_addon', lambda: True) + assert config_eos.server.port == get_default_port() # 8503 + assert config_eos.server.eosdash_port == get_default_port() + 1 # 8504 + + def test_server_port_restriction_in_ha_addon(self, config_eos, monkeypatch): + """Server port must be the default (8503) in HA addon mode.""" + monkeypatch.setattr('akkudoktoreos.server.server.is_home_assistant_addon', lambda: True) + with pytest.raises(ValidationError) as excinfo: + config_eos.server.port = 9000 + assert "Server port number `8503` for Home Assistant add-on can not be changed" in str(excinfo.value) + + def test_eosdash_port_restriction_in_ha_addon(self, config_eos, monkeypatch): + """EOSdash port must be the default (8504) in HA addon mode.""" + monkeypatch.setattr('akkudoktoreos.server.server.is_home_assistant_addon', lambda: True) + with pytest.raises(ValidationError) as excinfo: + config_eos.server.eosdash_port = 9001 + assert "EOSdash port number `8504` for Home Assistant add-on can not be changed" in str(excinfo.value) + + def test_ports_allowed_when_not_ha_addon(self, config_eos): + """Custom ports are allowed when not in HA addon mode.""" + # is_home_assistant_addon() returns False by default in this test environment + config_eos.server.port = 9000 + config_eos.server.eosdash_port = 9001 + assert config_eos.server.port == 9000 + assert config_eos.server.eosdash_port == 9001 + + class TestServerStartStop: @pytest.mark.asyncio diff --git a/tests/testdata/docs/_generated/configserver.md b/tests/testdata/docs/_generated/configserver.md index 57e537df..6890ebba 100644 --- a/tests/testdata/docs/_generated/configserver.md +++ b/tests/testdata/docs/_generated/configserver.md @@ -7,11 +7,11 @@ | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | -| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[str]` | `rw` | `None` | EOSdash server IP address. Defaults to EOS server IP address. | -| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `None` | EOSdash server IP port number. Defaults to EOS server IP port number + 1. | +| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `str` | `rw` | `127.0.0.1` | EOSdash server IP address. Defaults to EOS server IP address. | +| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `int` | `rw` | `8504` | EOSdash server IP port number. Defaults to 8504. | | eosdash_supervise_interval_sec | `EOS_SERVER__EOSDASH_SUPERVISE_INTERVAL_SEC` | `int` | `rw` | `10` | Supervision interval for EOS server to supervise EOSdash [seconds]. | -| host | `EOS_SERVER__HOST` | `Optional[str]` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. | -| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. | +| host | `EOS_SERVER__HOST` | `str` | `rw` | `127.0.0.1` | EOS server IP address. Defaults to 127.0.0.1. | +| port | `EOS_SERVER__PORT` | `int` | `rw` | `8503` | EOS server IP port number. Defaults to 8503. | | reload | `EOS_SERVER__RELOAD` | `Optional[bool]` | `rw` | `False` | Enable server auto-reload for debugging or development. Default is False. Monitors the package directory for changes and reloads the server. | | run_as_user | `EOS_SERVER__RUN_AS_USER` | `Optional[str]` | `rw` | `None` | The name of the target user to switch to. If ``None`` (default), the current effective user is used and no privilege change is attempted. | | startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. Defaults to True. | diff --git a/tests/testdata/eos_config_andreas_now.json b/tests/testdata/eos_config_andreas_now.json index bd0f77b2..6068d906 100644 --- a/tests/testdata/eos_config_andreas_now.json +++ b/tests/testdata/eos_config_andreas_now.json @@ -77,7 +77,6 @@ "server": { "host": "0.0.0.0", "verbose": true, - "eosdash_host": "0.0.0.0", - "eosdash_port": 8504 + "eosdash_host": "0.0.0.0" } -} +} \ No newline at end of file diff --git a/tests/testdata/eos_config_minimal_now.json b/tests/testdata/eos_config_minimal_now.json index 5833ee86..73b20100 100644 --- a/tests/testdata/eos_config_minimal_now.json +++ b/tests/testdata/eos_config_minimal_now.json @@ -14,7 +14,6 @@ }, "server": { "host": "0.0.0.0", - "eosdash_host": "0.0.0.0", - "eosdash_port": 8504 + "eosdash_host": "0.0.0.0" } } \ No newline at end of file diff --git a/tests/testdata/eos_config_stripped.json b/tests/testdata/eos_config_stripped.json index a931f291..e16d3381 100644 --- a/tests/testdata/eos_config_stripped.json +++ b/tests/testdata/eos_config_stripped.json @@ -121,9 +121,5 @@ }, "weather": { "provider": "BrightSky" - }, - "server": { - "eosdash_host": "127.0.0.1", - "eosdash_port": 8504 } -} +} \ No newline at end of file diff --git a/uv.lock b/uv.lock index f45ccb8b..aacd13dd 100644 --- a/uv.lock +++ b/uv.lock @@ -90,7 +90,7 @@ requires-dist = [ { name = "docutils", marker = "extra == 'dev'", specifier = "==0.21.2" }, { name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = "==0.139.2" }, { name = "fastapi-cli", specifier = "==0.0.32" }, - { name = "gitpython", marker = "extra == 'dev'", specifier = "==3.1.52" }, + { name = "gitpython", marker = "extra == 'dev'", specifier = "==3.1.53" }, { name = "linkify-it-py", specifier = "==2.1.0" }, { name = "lmdb", specifier = "==2.3.0" }, { name = "loguru", specifier = "==0.7.3" }, @@ -105,8 +105,8 @@ requires-dist = [ { name = "pandas", specifier = "==3.0.3" }, { name = "pandas-stubs", marker = "extra == 'dev'", specifier = "==3.0.3.260530" }, { name = "pendulum", specifier = "==3.2.0" }, - { name = "platformdirs", specifier = "==4.10.1" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.6.0" }, + { name = "platformdirs", specifier = "==4.11.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.6.1" }, { name = "psutil", specifier = "==7.2.2" }, { name = "pvlib", specifier = "==0.15.2" }, { name = "pydantic", specifier = "==2.13.4" }, @@ -1102,14 +1102,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.52" +version = "3.1.53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/fd/df0bafa4eb5ea2f51e1adee9f7a94c8e62c5d180e65117045dfca3439c8a/gitpython-3.1.52.tar.gz", hash = "sha256:de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e", size = 223726, upload-time = "2026-07-16T03:15:59.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/24/0e0c12cb6f7cb864779a9d2fefee9ca91838f6db402c8780c9d28a8d7ebe/gitpython-3.1.53.tar.gz", hash = "sha256:06ae8d9623b0ed0d67b8adeac5c7008d0a5a404b087a9e0d0c7163bdd3a6b497", size = 224597, upload-time = "2026-07-20T13:41:52.839Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/90/04dff7c1e176bb1c3011ef1647393d368790da710d8dde1cdcfad301f45a/gitpython-3.1.52-py3-none-any.whl", hash = "sha256:79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a", size = 215366, upload-time = "2026-07-16T03:15:58.239Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a6/bff12b3238885eeef7d28ef908b24e0cba91c476c31cb876a00a0986ce2c/gitpython-3.1.53-py3-none-any.whl", hash = "sha256:187885556b64ab357bd4ea84e2c4cce2861a613a7f4268b3f7f7ba05f2ce4ab0", size = 216237, upload-time = "2026-07-20T13:41:51.473Z" }, ] [[package]] @@ -2345,11 +2345,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.10.1" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/cd/4f25b2f95b23f5d2c9c1fe43e49841bff5800562149b2666afc09309aa8f/platformdirs-4.10.1.tar.gz", hash = "sha256:ceab4084426fe6319ce18e86deada8ab1b7487c7aee7040c55e277c9ae793695", size = 31678, upload-time = "2026-07-18T03:53:43.808Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/73/6fd0bb9ce84138c3857f12e9de63bc901852975a092d545f18087a204aa2/platformdirs-4.10.1-py3-none-any.whl", hash = "sha256:0e4eff26be2d75293977f7cddc153fd9b8eaa7fb0c7b64ffe4076cb443117443", size = 22906, upload-time = "2026-07-18T03:53:42.576Z" }, + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] [[package]] @@ -2363,7 +2363,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.6.0" +version = "4.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -2372,9 +2372,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, ] [[package]]