mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-06-27 16:36:53 +00:00
ci(ruff): add bandit checks (#575)
Added bandit checks to continuous integration. Updated sources to pass bandit checks: - replaced asserts - added timeouts to requests - added checks for process command execution - changed to 127.0.0.1 as default IP address for EOS and EOSdash for security reasons Added a rudimentary check for outdated config files. BREAKING CHANGE: Default IP address for EOS and EOSdash changed to 127.0.0.1 Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
parent
aa39ff475c
commit
3421b2303b
@ -880,11 +880,11 @@ Validators:
|
|||||||
|
|
||||||
| Name | Environment Variable | Type | Read-Only | Default | Description |
|
| Name | Environment Variable | Type | Read-Only | Default | Description |
|
||||||
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
|
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
|
||||||
| host | `EOS_SERVER__HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `0.0.0.0` | EOS server IP address. |
|
| host | `EOS_SERVER__HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `127.0.0.1` | EOS server IP address. |
|
||||||
| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. |
|
| port | `EOS_SERVER__PORT` | `Optional[int]` | `rw` | `8503` | EOS server IP port number. |
|
||||||
| verbose | `EOS_SERVER__VERBOSE` | `Optional[bool]` | `rw` | `False` | Enable debug output |
|
| verbose | `EOS_SERVER__VERBOSE` | `Optional[bool]` | `rw` | `False` | Enable debug output |
|
||||||
| startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. |
|
| startup_eosdash | `EOS_SERVER__STARTUP_EOSDASH` | `Optional[bool]` | `rw` | `True` | EOS server to start EOSdash server. |
|
||||||
| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `0.0.0.0` | EOSdash server IP address. |
|
| eosdash_host | `EOS_SERVER__EOSDASH_HOST` | `Optional[pydantic.networks.IPvAnyAddress]` | `rw` | `127.0.0.1` | EOSdash server IP address. |
|
||||||
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `8504` | EOSdash server IP port number. |
|
| eosdash_port | `EOS_SERVER__EOSDASH_PORT` | `Optional[int]` | `rw` | `8504` | EOSdash server IP port number. |
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@ -895,11 +895,11 @@ Validators:
|
|||||||
|
|
||||||
{
|
{
|
||||||
"server": {
|
"server": {
|
||||||
"host": "0.0.0.0",
|
"host": "127.0.0.1",
|
||||||
"port": 8503,
|
"port": 8503,
|
||||||
"verbose": false,
|
"verbose": false,
|
||||||
"startup_eosdash": true,
|
"startup_eosdash": true,
|
||||||
"eosdash_host": "0.0.0.0",
|
"eosdash_host": "127.0.0.1",
|
||||||
"eosdash_port": 8504
|
"eosdash_port": 8504
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1054,11 +1054,11 @@ Validators:
|
|||||||
"provider_settings": null
|
"provider_settings": null
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"host": "0.0.0.0",
|
"host": "127.0.0.1",
|
||||||
"port": 8503,
|
"port": 8503,
|
||||||
"verbose": false,
|
"verbose": false,
|
||||||
"startup_eosdash": true,
|
"startup_eosdash": true,
|
||||||
"eosdash_host": "0.0.0.0",
|
"eosdash_host": "127.0.0.1",
|
||||||
"eosdash_port": 8504
|
"eosdash_port": 8504
|
||||||
},
|
},
|
||||||
"utils": {}
|
"utils": {}
|
||||||
|
@ -220,9 +220,9 @@
|
|||||||
"server": {
|
"server": {
|
||||||
"$ref": "#/components/schemas/ServerCommonSettings",
|
"$ref": "#/components/schemas/ServerCommonSettings",
|
||||||
"default": {
|
"default": {
|
||||||
"eosdash_host": "0.0.0.0",
|
"eosdash_host": "127.0.0.1",
|
||||||
"eosdash_port": 8504,
|
"eosdash_port": 8504,
|
||||||
"host": "0.0.0.0",
|
"host": "127.0.0.1",
|
||||||
"port": 8503,
|
"port": 8503,
|
||||||
"startup_eosdash": true,
|
"startup_eosdash": true,
|
||||||
"verbose": false
|
"verbose": false
|
||||||
@ -2279,7 +2279,7 @@
|
|||||||
"type": "null"
|
"type": "null"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": "0.0.0.0",
|
"default": "127.0.0.1",
|
||||||
"description": "EOSdash server IP address.",
|
"description": "EOSdash server IP address.",
|
||||||
"title": "Eosdash Host"
|
"title": "Eosdash Host"
|
||||||
},
|
},
|
||||||
@ -2306,7 +2306,7 @@
|
|||||||
"type": "null"
|
"type": "null"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": "0.0.0.0",
|
"default": "127.0.0.1",
|
||||||
"description": "EOS server IP address.",
|
"description": "EOS server IP address.",
|
||||||
"title": "Host"
|
"title": "Host"
|
||||||
},
|
},
|
||||||
|
@ -43,12 +43,18 @@ profile = "black"
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
exclude = [
|
||||||
|
"tests",
|
||||||
|
"scripts",
|
||||||
|
]
|
||||||
|
output-format = "full"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = [
|
select = [
|
||||||
"F", # Enable all `Pyflakes` rules.
|
"F", # Enable all `Pyflakes` rules.
|
||||||
"D", # Enable all `pydocstyle` rules, limiting to those that adhere to the
|
"D", # Enable all `pydocstyle` rules, limiting to those that adhere to the
|
||||||
# Google convention via `convention = "google"`, below.
|
# Google convention via `convention = "google"`, below.
|
||||||
|
"S", # Enable all `flake8-bandit` rules.
|
||||||
]
|
]
|
||||||
ignore = [
|
ignore = [
|
||||||
# Prevent errors due to ruff false positives
|
# Prevent errors due to ruff false positives
|
||||||
|
@ -298,7 +298,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
config_md = generate_config_md(config_eos)
|
config_md = generate_config_md(config_eos)
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
config_md = config_md.replace("127.0.0.1", "0.0.0.0").replace("\\\\", "/")
|
config_md = config_md.replace("\\\\", "/")
|
||||||
if args.output_file:
|
if args.output_file:
|
||||||
# Write to file
|
# Write to file
|
||||||
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
@ -58,8 +58,6 @@ def main():
|
|||||||
try:
|
try:
|
||||||
openapi_spec = generate_openapi()
|
openapi_spec = generate_openapi()
|
||||||
openapi_spec_str = json.dumps(openapi_spec, indent=2)
|
openapi_spec_str = json.dumps(openapi_spec, indent=2)
|
||||||
if os.name == "nt":
|
|
||||||
openapi_spec_str = openapi_spec_str.replace("127.0.0.1", "0.0.0.0")
|
|
||||||
if args.output_file:
|
if args.output_file:
|
||||||
# Write to file
|
# Write to file
|
||||||
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
@ -286,7 +286,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
openapi_md = generate_openapi_md()
|
openapi_md = generate_openapi_md()
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
openapi_md = openapi_md.replace("127.0.0.1", "0.0.0.0")
|
openapi_md = openapi_md.replace("127.0.0.1", "127.0.0.1")
|
||||||
if args.output_file:
|
if args.output_file:
|
||||||
# Write to file
|
# Write to file
|
||||||
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
@ -376,6 +376,15 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
|||||||
|
|
||||||
def _setup(self, *args: Any, **kwargs: Any) -> None:
|
def _setup(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""Re-initialize global settings."""
|
"""Re-initialize global settings."""
|
||||||
|
# Check for config file content/ version type
|
||||||
|
config_file, exists = self._get_config_file_path()
|
||||||
|
if exists:
|
||||||
|
with config_file.open("r", encoding="utf-8", newline=None) as f_config:
|
||||||
|
config_txt = f_config.read()
|
||||||
|
if '"directories": {' in config_txt or '"server_eos_host": ' in config_txt:
|
||||||
|
error_msg = f"Configuration file '{config_file}' is outdated. Please remove or update manually."
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
# Assure settings base knows EOS configuration
|
# Assure settings base knows EOS configuration
|
||||||
SettingsBaseModel.config = self
|
SettingsBaseModel.config = self
|
||||||
# (Re-)load settings
|
# (Re-)load settings
|
||||||
@ -394,7 +403,9 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
|||||||
ValueError: If the `settings` is not a `SettingsEOS` instance.
|
ValueError: If the `settings` is not a `SettingsEOS` instance.
|
||||||
"""
|
"""
|
||||||
if not isinstance(settings, SettingsEOS):
|
if not isinstance(settings, SettingsEOS):
|
||||||
raise ValueError(f"Settings must be an instance of SettingsEOS: '{settings}'.")
|
error_msg = f"Settings must be an instance of SettingsEOS: '{settings}'."
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
self.merge_settings_from_dict(settings.model_dump(exclude_none=True, exclude_unset=True))
|
self.merge_settings_from_dict(settings.model_dump(exclude_none=True, exclude_unset=True))
|
||||||
|
|
||||||
@ -471,10 +482,10 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_config_file_path(cls) -> tuple[Path, bool]:
|
def _get_config_file_path(cls) -> tuple[Path, bool]:
|
||||||
"""Finds the a valid configuration file or returns the desired path for a new config file.
|
"""Find a valid configuration file or return the desired path for a new config file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple[Path, bool]: The path to the configuration directory and if there is already a config file there
|
tuple[Path, bool]: The path to the configuration file and if there is already a config file there
|
||||||
"""
|
"""
|
||||||
config_dirs = []
|
config_dirs = []
|
||||||
env_base_dir = os.getenv(cls.EOS_DIR)
|
env_base_dir = os.getenv(cls.EOS_DIR)
|
||||||
|
@ -956,7 +956,7 @@ def cache_in_file(
|
|||||||
logger.debug("Used cache file for function: " + func.__name__)
|
logger.debug("Used cache file for function: " + func.__name__)
|
||||||
cache_file.seek(0)
|
cache_file.seek(0)
|
||||||
if "b" in mode:
|
if "b" in mode:
|
||||||
result = pickle.load(cache_file)
|
result = pickle.load(cache_file) # noqa: S301
|
||||||
else:
|
else:
|
||||||
result = cache_file.read()
|
result = cache_file.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -34,7 +34,7 @@ class classproperty:
|
|||||||
argument and returns a value.
|
argument and returns a value.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
AssertionError: If `fget` is not defined when `__get__` is called.
|
RuntimeError: If `fget` is not defined when `__get__` is called.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, fget: Callable[[Any], Any]) -> None:
|
def __init__(self, fget: Callable[[Any], Any]) -> None:
|
||||||
@ -43,5 +43,6 @@ class classproperty:
|
|||||||
def __get__(self, _: Any, owner_cls: Optional[type[Any]] = None) -> Any:
|
def __get__(self, _: Any, owner_cls: Optional[type[Any]] = None) -> Any:
|
||||||
if owner_cls is None:
|
if owner_cls is None:
|
||||||
return self
|
return self
|
||||||
assert self.fget is not None
|
if self.fget is None:
|
||||||
|
raise RuntimeError("'fget' not defined when `__get__` is called")
|
||||||
return self.fget(owner_cls)
|
return self.fget(owner_cls)
|
||||||
|
@ -393,7 +393,8 @@ class EnergyManagement(SingletonMixin, ConfigMixin, PredictionMixin, PydanticBas
|
|||||||
|
|
||||||
# Fetch objects
|
# Fetch objects
|
||||||
battery = self.battery
|
battery = self.battery
|
||||||
assert battery # to please mypy
|
if battery is None:
|
||||||
|
raise ValueError(f"battery not set: {battery}")
|
||||||
ev = self.ev
|
ev = self.ev
|
||||||
home_appliance = self.home_appliance
|
home_appliance = self.home_appliance
|
||||||
inverter = self.inverter
|
inverter = self.inverter
|
||||||
|
@ -450,8 +450,8 @@ class PydanticBaseModel(BaseModel, PydanticModelNestedValueMixin):
|
|||||||
if expected_type is pendulum.DateTime or expected_type is AwareDatetime:
|
if expected_type is pendulum.DateTime or expected_type is AwareDatetime:
|
||||||
try:
|
try:
|
||||||
value = to_datetime(value)
|
value = to_datetime(value)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
raise ValueError(f"Cannot convert {value!r} to datetime: {e}")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# Override Pydantic’s serialization for all DateTime fields
|
# Override Pydantic’s serialization for all DateTime fields
|
||||||
|
@ -121,7 +121,8 @@ class Battery(DeviceBase):
|
|||||||
|
|
||||||
def _setup(self) -> None:
|
def _setup(self) -> None:
|
||||||
"""Sets up the battery parameters based on configuration or provided parameters."""
|
"""Sets up the battery parameters based on configuration or provided parameters."""
|
||||||
assert self.parameters is not None
|
if self.parameters is None:
|
||||||
|
raise ValueError(f"Parameters not set: {self.parameters}")
|
||||||
self.capacity_wh = self.parameters.capacity_wh
|
self.capacity_wh = self.parameters.capacity_wh
|
||||||
self.initial_soc_percentage = self.parameters.initial_soc_percentage
|
self.initial_soc_percentage = self.parameters.initial_soc_percentage
|
||||||
self.charging_efficiency = self.parameters.charging_efficiency
|
self.charging_efficiency = self.parameters.charging_efficiency
|
||||||
|
@ -170,7 +170,8 @@ class DevicesBase(DevicesStartEndMixin, PredictionMixin):
|
|||||||
def add_device(self, device: Optional["DeviceBase"]) -> None:
|
def add_device(self, device: Optional["DeviceBase"]) -> None:
|
||||||
if device is None:
|
if device is None:
|
||||||
return
|
return
|
||||||
assert device.device_id not in self.devices, f"{device.device_id} already registered"
|
if device.device_id in self.devices:
|
||||||
|
raise ValueError(f"{device.device_id} already registered")
|
||||||
self.devices[device.device_id] = device
|
self.devices[device.device_id] = device
|
||||||
|
|
||||||
def remove_device(self, device: Type["DeviceBase"] | str) -> bool:
|
def remove_device(self, device: Type["DeviceBase"] | str) -> bool:
|
||||||
|
@ -34,7 +34,8 @@ class HomeAppliance(DeviceBase):
|
|||||||
super().__init__(parameters)
|
super().__init__(parameters)
|
||||||
|
|
||||||
def _setup(self) -> None:
|
def _setup(self) -> None:
|
||||||
assert self.parameters is not None
|
if self.parameters is None:
|
||||||
|
raise ValueError(f"Parameters not set: {self.parameters}")
|
||||||
self.load_curve = np.zeros(self.hours) # Initialize the load curve with zeros
|
self.load_curve = np.zeros(self.hours) # Initialize the load curve with zeros
|
||||||
self.duration_h = self.parameters.duration_h
|
self.duration_h = self.parameters.duration_h
|
||||||
self.consumption_wh = self.parameters.consumption_wh
|
self.consumption_wh = self.parameters.consumption_wh
|
||||||
|
@ -28,7 +28,8 @@ class Inverter(DeviceBase):
|
|||||||
super().__init__(parameters)
|
super().__init__(parameters)
|
||||||
|
|
||||||
def _setup(self) -> None:
|
def _setup(self) -> None:
|
||||||
assert self.parameters is not None
|
if self.parameters is None:
|
||||||
|
raise ValueError(f"Parameters not set: {self.parameters}")
|
||||||
if self.parameters.battery_id is None:
|
if self.parameters.battery_id is None:
|
||||||
# For the moment raise exception
|
# For the moment raise exception
|
||||||
# TODO: Make battery configurable by config
|
# TODO: Make battery configurable by config
|
||||||
@ -41,7 +42,8 @@ class Inverter(DeviceBase):
|
|||||||
) # Maximum power that the inverter can handle
|
) # Maximum power that the inverter can handle
|
||||||
|
|
||||||
def _post_setup(self) -> None:
|
def _post_setup(self) -> None:
|
||||||
assert self.parameters is not None
|
if self.parameters is None:
|
||||||
|
raise ValueError(f"Parameters not set: {self.parameters}")
|
||||||
self.battery = self.devices.get_device_by_id(self.parameters.battery_id)
|
self.battery = self.devices.get_device_by_id(self.parameters.battery_id)
|
||||||
|
|
||||||
def process_energy(
|
def process_energy(
|
||||||
|
@ -121,7 +121,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
if self.fix_seed is not None:
|
if self.fix_seed is not None:
|
||||||
random.seed(self.fix_seed)
|
random.seed(self.fix_seed)
|
||||||
elif logger.level == "DEBUG":
|
elif logger.level == "DEBUG":
|
||||||
self.fix_seed = random.randint(1, 100000000000)
|
self.fix_seed = random.randint(1, 100000000000) # noqa: S311
|
||||||
random.seed(self.fix_seed)
|
random.seed(self.fix_seed)
|
||||||
|
|
||||||
def decode_charge_discharge(
|
def decode_charge_discharge(
|
||||||
|
@ -104,12 +104,13 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
|||||||
- add the file cache again.
|
- add the file cache again.
|
||||||
"""
|
"""
|
||||||
source = "https://api.akkudoktor.net"
|
source = "https://api.akkudoktor.net"
|
||||||
assert self.start_datetime # mypy fix
|
if not self.start_datetime:
|
||||||
|
raise ValueError(f"Start DateTime not set: {self.start_datetime}")
|
||||||
# Try to take data from 5 weeks back for prediction
|
# Try to take data from 5 weeks back for prediction
|
||||||
date = to_datetime(self.start_datetime - to_duration("35 days"), as_string="YYYY-MM-DD")
|
date = to_datetime(self.start_datetime - to_duration("35 days"), as_string="YYYY-MM-DD")
|
||||||
last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
|
last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
|
||||||
url = f"{source}/prices?start={date}&end={last_date}&tz={self.config.general.timezone}"
|
url = f"{source}/prices?start={date}&end={last_date}&tz={self.config.general.timezone}"
|
||||||
response = requests.get(url)
|
response = requests.get(url, timeout=10)
|
||||||
logger.debug(f"Response from {url}: {response}")
|
logger.debug(f"Response from {url}: {response}")
|
||||||
response.raise_for_status() # Raise an error for bad responses
|
response.raise_for_status() # Raise an error for bad responses
|
||||||
akkudoktor_data = self._validate_data(response.content)
|
akkudoktor_data = self._validate_data(response.content)
|
||||||
@ -148,7 +149,8 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
|||||||
"""
|
"""
|
||||||
# Get Akkudoktor electricity price data
|
# Get Akkudoktor electricity price data
|
||||||
akkudoktor_data = self._request_forecast(force_update=force_update) # type: ignore
|
akkudoktor_data = self._request_forecast(force_update=force_update) # type: ignore
|
||||||
assert self.start_datetime # mypy fix
|
if not self.start_datetime:
|
||||||
|
raise ValueError(f"Start DateTime not set: {self.start_datetime}")
|
||||||
|
|
||||||
# Assumption that all lists are the same length and are ordered chronologically
|
# Assumption that all lists are the same length and are ordered chronologically
|
||||||
# in ascending order and have the same timestamps.
|
# in ascending order and have the same timestamps.
|
||||||
@ -178,7 +180,10 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
amount_datasets = len(self.records)
|
amount_datasets = len(self.records)
|
||||||
assert highest_orig_datetime # mypy fix
|
if not highest_orig_datetime: # mypy fix
|
||||||
|
error_msg = f"Highest original datetime not available: {highest_orig_datetime}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
# some of our data is already in the future, so we need to predict less. If we got less data we increase the prediction hours
|
# some of our data is already in the future, so we need to predict less. If we got less data we increase the prediction hours
|
||||||
needed_hours = int(
|
needed_hours = int(
|
||||||
|
@ -14,7 +14,7 @@ class SelfConsumptionProbabilityInterpolator:
|
|||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
# Load the RegularGridInterpolator
|
# Load the RegularGridInterpolator
|
||||||
with open(self.filepath, "rb") as file:
|
with open(self.filepath, "rb") as file:
|
||||||
self.interpolator: RegularGridInterpolator = pickle.load(file)
|
self.interpolator: RegularGridInterpolator = pickle.load(file) # noqa: S301
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
@lru_cache(maxsize=128)
|
||||||
def generate_points(
|
def generate_points(
|
||||||
|
@ -291,7 +291,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If the API response does not include expected `meta` data.
|
ValueError: If the API response does not include expected `meta` data.
|
||||||
"""
|
"""
|
||||||
response = requests.get(self._url())
|
response = requests.get(self._url(), timeout=10)
|
||||||
response.raise_for_status() # Raise an error for bad responses
|
response.raise_for_status() # Raise an error for bad responses
|
||||||
logger.debug(f"Response from {self._url()}: {response}")
|
logger.debug(f"Response from {self._url()}: {response}")
|
||||||
akkudoktor_data = self._validate_data(response.content)
|
akkudoktor_data = self._validate_data(response.content)
|
||||||
@ -332,7 +332,8 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
|||||||
logger.error(f"Akkudoktor schema change: {error_msg}")
|
logger.error(f"Akkudoktor schema change: {error_msg}")
|
||||||
raise ValueError(error_msg)
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
assert self.start_datetime # mypy fix
|
if not self.start_datetime:
|
||||||
|
raise ValueError(f"Start DateTime not set: {self.start_datetime}")
|
||||||
|
|
||||||
# Iterate over forecast data points
|
# Iterate over forecast data points
|
||||||
for forecast_values in zip(*akkudoktor_data.values):
|
for forecast_values in zip(*akkudoktor_data.values):
|
||||||
|
@ -100,7 +100,8 @@ class WeatherBrightSky(WeatherProvider):
|
|||||||
date = to_datetime(self.start_datetime, as_string=True)
|
date = to_datetime(self.start_datetime, as_string=True)
|
||||||
last_date = to_datetime(self.end_datetime, as_string=True)
|
last_date = to_datetime(self.end_datetime, as_string=True)
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{source}/weather?lat={self.config.general.latitude}&lon={self.config.general.longitude}&date={date}&last_date={last_date}&tz={self.config.general.timezone}"
|
f"{source}/weather?lat={self.config.general.latitude}&lon={self.config.general.longitude}&date={date}&last_date={last_date}&tz={self.config.general.timezone}",
|
||||||
|
timeout=10,
|
||||||
)
|
)
|
||||||
response.raise_for_status() # Raise an error for bad responses
|
response.raise_for_status() # Raise an error for bad responses
|
||||||
logger.debug(f"Response from {source}: {response}")
|
logger.debug(f"Response from {source}: {response}")
|
||||||
@ -222,7 +223,7 @@ class WeatherBrightSky(WeatherProvider):
|
|||||||
|
|
||||||
# Add Preciptable Water (PWAT) with a PVLib method.
|
# Add Preciptable Water (PWAT) with a PVLib method.
|
||||||
key = WeatherDataRecord.key_from_description("Temperature (°C)")
|
key = WeatherDataRecord.key_from_description("Temperature (°C)")
|
||||||
assert key
|
assert key # noqa: S101
|
||||||
temperature = self.key_to_array(
|
temperature = self.key_to_array(
|
||||||
key=key,
|
key=key,
|
||||||
start_datetime=self.start_datetime,
|
start_datetime=self.start_datetime,
|
||||||
@ -235,7 +236,7 @@ class WeatherBrightSky(WeatherProvider):
|
|||||||
logger.debug(debug_msg)
|
logger.debug(debug_msg)
|
||||||
return
|
return
|
||||||
key = WeatherDataRecord.key_from_description("Relative Humidity (%)")
|
key = WeatherDataRecord.key_from_description("Relative Humidity (%)")
|
||||||
assert key
|
assert key # noqa: S101
|
||||||
humidity = self.key_to_array(
|
humidity = self.key_to_array(
|
||||||
key=key,
|
key=key,
|
||||||
start_datetime=self.start_datetime,
|
start_datetime=self.start_datetime,
|
||||||
|
@ -93,7 +93,7 @@ class WeatherClearOutside(WeatherProvider):
|
|||||||
source = "https://clearoutside.com/forecast"
|
source = "https://clearoutside.com/forecast"
|
||||||
latitude = round(self.config.general.latitude, 2)
|
latitude = round(self.config.general.latitude, 2)
|
||||||
longitude = round(self.config.general.longitude, 2)
|
longitude = round(self.config.general.longitude, 2)
|
||||||
response = requests.get(f"{source}/{latitude}/{longitude}?desktop=true")
|
response = requests.get(f"{source}/{latitude}/{longitude}?desktop=true", timeout=10)
|
||||||
response.raise_for_status() # Raise an error for bad responses
|
response.raise_for_status() # Raise an error for bad responses
|
||||||
logger.debug(f"Response from {source}: {response}")
|
logger.debug(f"Response from {source}: {response}")
|
||||||
# We are working on fresh data (no cache), report update time
|
# We are working on fresh data (no cache), report update time
|
||||||
|
@ -82,8 +82,8 @@ def AdminConfig(
|
|||||||
try:
|
try:
|
||||||
if config:
|
if config:
|
||||||
config_file_path = get_nested_value(config, ["general", "config_file_path"])
|
config_file_path = get_nested_value(config, ["general", "config_file_path"])
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.debug(f"general.config_file_path: {e}")
|
||||||
# export config file
|
# export config file
|
||||||
export_to_file_next_tag = to_datetime(as_string="YYYYMMDDHHmmss")
|
export_to_file_next_tag = to_datetime(as_string="YYYYMMDDHHmmss")
|
||||||
export_to_file_status = (None,)
|
export_to_file_status = (None,)
|
||||||
@ -95,7 +95,7 @@ def AdminConfig(
|
|||||||
if data["action"] == "save_to_file":
|
if data["action"] == "save_to_file":
|
||||||
# Safe current configuration to file
|
# Safe current configuration to file
|
||||||
try:
|
try:
|
||||||
result = requests.put(f"{server}/v1/config/file")
|
result = requests.put(f"{server}/v1/config/file", timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
config_file_path = result.json()["general"]["config_file_path"]
|
config_file_path = result.json()["general"]["config_file_path"]
|
||||||
status = Success(f"Saved to '{config_file_path}' on '{eos_hostname}'")
|
status = Success(f"Saved to '{config_file_path}' on '{eos_hostname}'")
|
||||||
@ -143,7 +143,7 @@ def AdminConfig(
|
|||||||
try:
|
try:
|
||||||
with import_file_path.open("r", encoding="utf-8", newline=None) as fd:
|
with import_file_path.open("r", encoding="utf-8", newline=None) as fd:
|
||||||
import_config = json.load(fd)
|
import_config = json.load(fd)
|
||||||
result = requests.put(f"{server}/v1/config", json=import_config)
|
result = requests.put(f"{server}/v1/config", json=import_config, timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
import_from_file_status = Success(
|
import_from_file_status = Success(
|
||||||
f"Config imported from '{import_file_path}' on '{eosdash_hostname}'"
|
f"Config imported from '{import_file_path}' on '{eosdash_hostname}'"
|
||||||
@ -267,7 +267,7 @@ def Admin(eos_host: str, eos_port: Union[str, int], data: Optional[dict] = None)
|
|||||||
# Get current configuration from server
|
# Get current configuration from server
|
||||||
server = f"http://{eos_host}:{eos_port}"
|
server = f"http://{eos_host}:{eos_port}"
|
||||||
try:
|
try:
|
||||||
result = requests.get(f"{server}/v1/config")
|
result = requests.get(f"{server}/v1/config", timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
config = result.json()
|
config = result.json()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
|
@ -218,7 +218,7 @@ def get_configuration(eos_host: str, eos_port: Union[str, int]) -> list[dict]:
|
|||||||
|
|
||||||
# Get current configuration from server
|
# Get current configuration from server
|
||||||
try:
|
try:
|
||||||
result = requests.get(f"{server}/v1/config")
|
result = requests.get(f"{server}/v1/config", timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
config = result.json()
|
config = result.json()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
@ -303,9 +303,14 @@ def ConfigPlanesCard(
|
|||||||
planes_update_open = True
|
planes_update_open = True
|
||||||
plane_update_open = True
|
plane_update_open = True
|
||||||
# Make mypy happy - should never trigger
|
# Make mypy happy - should never trigger
|
||||||
assert isinstance(update_error, (str, type(None)))
|
if (
|
||||||
assert isinstance(update_value, (str, type(None)))
|
not isinstance(update_error, (str, type(None)))
|
||||||
assert isinstance(update_open, (bool, type(None)))
|
or not isinstance(update_value, (str, type(None)))
|
||||||
|
or not isinstance(update_open, (bool, type(None)))
|
||||||
|
):
|
||||||
|
error_msg = "update_error or update_value or update_open of wrong type."
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise TypeError(error_msg)
|
||||||
plane_rows.append(
|
plane_rows.append(
|
||||||
ConfigCard(
|
ConfigCard(
|
||||||
config["name"],
|
config["name"],
|
||||||
@ -441,9 +446,14 @@ def Configuration(
|
|||||||
update_value = config_update_latest.get(config["name"], {}).get("value")
|
update_value = config_update_latest.get(config["name"], {}).get("value")
|
||||||
update_open = config_update_latest.get(config["name"], {}).get("open")
|
update_open = config_update_latest.get(config["name"], {}).get("open")
|
||||||
# Make mypy happy - should never trigger
|
# Make mypy happy - should never trigger
|
||||||
assert isinstance(update_error, (str, type(None)))
|
if (
|
||||||
assert isinstance(update_value, (str, type(None)))
|
not isinstance(update_error, (str, type(None)))
|
||||||
assert isinstance(update_open, (bool, type(None)))
|
or not isinstance(update_value, (str, type(None)))
|
||||||
|
or not isinstance(update_open, (bool, type(None)))
|
||||||
|
):
|
||||||
|
error_msg = "update_error or update_value or update_open of wrong type."
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise TypeError(error_msg)
|
||||||
if (
|
if (
|
||||||
config["type"]
|
config["type"]
|
||||||
== "Optional[list[akkudoktoreos.prediction.pvforecast.PVForecastPlaneSetting]]"
|
== "Optional[list[akkudoktoreos.prediction.pvforecast.PVForecastPlaneSetting]]"
|
||||||
@ -505,7 +515,7 @@ def ConfigKeyUpdate(eos_host: str, eos_port: Union[str, int], key: str, value: s
|
|||||||
error = None
|
error = None
|
||||||
config = None
|
config = None
|
||||||
try:
|
try:
|
||||||
response = requests.put(f"{server}/v1/config/{path}", json=data)
|
response = requests.put(f"{server}/v1/config/{path}", json=data, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
config = response.json()
|
config = response.json()
|
||||||
except requests.exceptions.HTTPError as err:
|
except requests.exceptions.HTTPError as err:
|
||||||
|
@ -75,9 +75,9 @@
|
|||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"startup_eosdash": true,
|
"startup_eosdash": true,
|
||||||
"host": "0.0.0.0",
|
"host": "127.0.0.1",
|
||||||
"port": 8503,
|
"port": 8503,
|
||||||
"eosdash_host": "0.0.0.0",
|
"eosdash_host": "127.0.0.1",
|
||||||
"eosdash_port": 8504
|
"eosdash_port": 8504
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
|
@ -186,7 +186,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
|||||||
|
|
||||||
# Get current configuration from server
|
# Get current configuration from server
|
||||||
try:
|
try:
|
||||||
result = requests.get(f"{server}/v1/config")
|
result = requests.get(f"{server}/v1/config", timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as err:
|
except requests.exceptions.HTTPError as err:
|
||||||
detail = result.json()["detail"]
|
detail = result.json()["detail"]
|
||||||
@ -200,12 +200,12 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
|||||||
with FILE_DEMOCONFIG.open("r", encoding="utf-8") as fd:
|
with FILE_DEMOCONFIG.open("r", encoding="utf-8") as fd:
|
||||||
democonfig = json.load(fd)
|
democonfig = json.load(fd)
|
||||||
try:
|
try:
|
||||||
result = requests.put(f"{server}/v1/config", json=democonfig)
|
result = requests.put(f"{server}/v1/config", json=democonfig, timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as err:
|
except requests.exceptions.HTTPError as err:
|
||||||
detail = result.json()["detail"]
|
detail = result.json()["detail"]
|
||||||
# Try to reset to original config
|
# Try to reset to original config
|
||||||
requests.put(f"{server}/v1/config", json=config)
|
requests.put(f"{server}/v1/config", json=config, timeout=10)
|
||||||
return P(
|
return P(
|
||||||
f"Can not set demo configuration on {server}: {err}, {detail}",
|
f"Can not set demo configuration on {server}: {err}, {detail}",
|
||||||
cls="text-center",
|
cls="text-center",
|
||||||
@ -213,12 +213,12 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
|||||||
|
|
||||||
# Update all predictions
|
# Update all predictions
|
||||||
try:
|
try:
|
||||||
result = requests.post(f"{server}/v1/prediction/update")
|
result = requests.post(f"{server}/v1/prediction/update", timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as err:
|
except requests.exceptions.HTTPError as err:
|
||||||
detail = result.json()["detail"]
|
detail = result.json()["detail"]
|
||||||
# Try to reset to original config
|
# Try to reset to original config
|
||||||
requests.put(f"{server}/v1/config", json=config)
|
requests.put(f"{server}/v1/config", json=config, timeout=10)
|
||||||
return P(
|
return P(
|
||||||
f"Can not update predictions on {server}: {err}, {detail}",
|
f"Can not update predictions on {server}: {err}, {detail}",
|
||||||
cls="text-center",
|
cls="text-center",
|
||||||
@ -239,7 +239,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
|||||||
"load_mean_adjusted",
|
"load_mean_adjusted",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
result = requests.get(f"{server}/v1/prediction/dataframe", params=params)
|
result = requests.get(f"{server}/v1/prediction/dataframe", params=params, timeout=10)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
predictions = PydanticDateTimeDataFrame(**result.json()).to_dataframe()
|
predictions = PydanticDateTimeDataFrame(**result.json()).to_dataframe()
|
||||||
except requests.exceptions.HTTPError as err:
|
except requests.exceptions.HTTPError as err:
|
||||||
@ -255,7 +255,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Reset to original config
|
# Reset to original config
|
||||||
requests.put(f"{server}/v1/config", json=config)
|
requests.put(f"{server}/v1/config", json=config, timeout=10)
|
||||||
|
|
||||||
return Grid(
|
return Grid(
|
||||||
DemoPVForecast(predictions, democonfig),
|
DemoPVForecast(predictions, democonfig),
|
||||||
|
@ -24,7 +24,7 @@ def get_alive(eos_host: str, eos_port: Union[str, int]) -> str:
|
|||||||
"""
|
"""
|
||||||
result = requests.Response()
|
result = requests.Response()
|
||||||
try:
|
try:
|
||||||
result = requests.get(f"http://{eos_host}:{eos_port}/v1/health")
|
result = requests.get(f"http://{eos_host}:{eos_port}/v1/health", timeout=10)
|
||||||
if result.status_code == 200:
|
if result.status_code == 200:
|
||||||
alive = result.json()["status"]
|
alive = result.json()["status"]
|
||||||
else:
|
else:
|
||||||
|
@ -49,7 +49,11 @@ from akkudoktoreos.prediction.prediction import PredictionCommonSettings, get_pr
|
|||||||
from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings
|
from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings
|
||||||
from akkudoktoreos.server.rest.error import create_error_page
|
from akkudoktoreos.server.rest.error import create_error_page
|
||||||
from akkudoktoreos.server.rest.tasks import repeat_every
|
from akkudoktoreos.server.rest.tasks import repeat_every
|
||||||
from akkudoktoreos.server.server import get_default_host, wait_for_port_free
|
from akkudoktoreos.server.server import (
|
||||||
|
get_default_host,
|
||||||
|
is_valid_ip_or_hostname,
|
||||||
|
wait_for_port_free,
|
||||||
|
)
|
||||||
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
|
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@ -100,6 +104,11 @@ def start_eosdash(
|
|||||||
Raises:
|
Raises:
|
||||||
RuntimeError: If the EOSdash server fails to start.
|
RuntimeError: If the EOSdash server fails to start.
|
||||||
"""
|
"""
|
||||||
|
if not is_valid_ip_or_hostname(host):
|
||||||
|
raise ValueError(f"Invalid EOSdash host: {host}")
|
||||||
|
if not is_valid_ip_or_hostname(eos_host):
|
||||||
|
raise ValueError(f"Invalid EOS host: {eos_host}")
|
||||||
|
|
||||||
eosdash_path = Path(__file__).parent.resolve().joinpath("eosdash.py")
|
eosdash_path = Path(__file__).parent.resolve().joinpath("eosdash.py")
|
||||||
|
|
||||||
# Do a one time check for port free to generate warnings if not so
|
# Do a one time check for port free to generate warnings if not so
|
||||||
@ -130,7 +139,7 @@ def start_eosdash(
|
|||||||
env["EOS_CONFIG_DIR"] = eos_config_dir
|
env["EOS_CONFIG_DIR"] = eos_config_dir
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server_process = subprocess.Popen(
|
server_process = subprocess.Popen( # noqa: S603
|
||||||
cmd,
|
cmd,
|
||||||
env=env,
|
env=env,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@ -240,10 +249,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
access_log = args.access_log
|
access_log = args.access_log
|
||||||
reload = args.reload
|
reload = args.reload
|
||||||
|
|
||||||
host = host if host else get_default_host()
|
|
||||||
port = port if port else 8504
|
|
||||||
eos_host = eos_host if eos_host else get_default_host()
|
eos_host = eos_host if eos_host else get_default_host()
|
||||||
eos_port = eos_port if eos_port else 8503
|
eos_port = eos_port if eos_port else 8503
|
||||||
|
host = host if host else eos_host
|
||||||
|
port = port if port else 8504
|
||||||
|
|
||||||
eos_dir = str(config_eos.general.data_folder_path)
|
eos_dir = str(config_eos.general.data_folder_path)
|
||||||
eos_config_dir = str(config_eos.general.config_folder_path)
|
eos_config_dir = str(config_eos.general.config_folder_path)
|
||||||
@ -370,7 +379,7 @@ async def fastapi_admin_server_restart_post() -> dict:
|
|||||||
env["EOS_DIR"] = str(config_eos.general.data_folder_path)
|
env["EOS_DIR"] = str(config_eos.general.data_folder_path)
|
||||||
env["EOS_CONFIG_DIR"] = str(config_eos.general.config_folder_path)
|
env["EOS_CONFIG_DIR"] = str(config_eos.general.config_folder_path)
|
||||||
|
|
||||||
new_process = subprocess.Popen(
|
new_process = subprocess.Popen( # noqa: S603
|
||||||
[
|
[
|
||||||
sys.executable,
|
sys.executable,
|
||||||
]
|
]
|
||||||
@ -1208,7 +1217,7 @@ def redirect(request: Request, path: str) -> Union[HTMLResponse, RedirectRespons
|
|||||||
if port is None:
|
if port is None:
|
||||||
port = 8504
|
port = 8504
|
||||||
# Make hostname Windows friendly
|
# Make hostname Windows friendly
|
||||||
if host == "0.0.0.0" and os.name == "nt":
|
if host == "0.0.0.0" and os.name == "nt": # noqa: S104
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
url = f"http://{host}:{port}/"
|
url = f"http://{host}:{port}/"
|
||||||
error_page = create_error_page(
|
error_page = create_error_page(
|
||||||
@ -1225,7 +1234,7 @@ Did you want to connect to <a href="{url}" class="back-button">EOSdash</a>?
|
|||||||
|
|
||||||
# Make hostname Windows friendly
|
# Make hostname Windows friendly
|
||||||
host = str(config_eos.server.eosdash_host)
|
host = str(config_eos.server.eosdash_host)
|
||||||
if host == "0.0.0.0" and os.name == "nt":
|
if host == "0.0.0.0" and os.name == "nt": # noqa: S104
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
if host and config_eos.server.eosdash_port:
|
if host and config_eos.server.eosdash_port:
|
||||||
# Redirect to EOSdash server
|
# Redirect to EOSdash server
|
||||||
@ -1258,7 +1267,7 @@ def run_eos(host: str, port: int, log_level: str, access_log: bool, reload: bool
|
|||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
# Make hostname Windows friendly
|
# Make hostname Windows friendly
|
||||||
if host == "0.0.0.0" and os.name == "nt":
|
if host == "0.0.0.0" and os.name == "nt": # noqa: S104
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
|
|
||||||
# Wait for EOS port to be free - e.g. in case of restart
|
# Wait for EOS port to be free - e.g. in case of restart
|
||||||
|
@ -242,7 +242,7 @@ def run_eosdash() -> None:
|
|||||||
reload = False
|
reload = False
|
||||||
|
|
||||||
# Make hostname Windows friendly
|
# Make hostname Windows friendly
|
||||||
if eosdash_host == "0.0.0.0" and os.name == "nt":
|
if eosdash_host == "0.0.0.0" and os.name == "nt": # noqa: S104
|
||||||
eosdash_host = "localhost"
|
eosdash_host = "localhost"
|
||||||
|
|
||||||
# Wait for EOSdash port to be free - e.g. in case of restart
|
# Wait for EOSdash port to be free - e.g. in case of restart
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Server Module."""
|
"""Server Module."""
|
||||||
|
|
||||||
import os
|
import ipaddress
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
@ -14,9 +15,39 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def get_default_host() -> str:
|
def get_default_host() -> str:
|
||||||
if os.name == "nt":
|
"""Default host for EOS."""
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
return "0.0.0.0"
|
|
||||||
|
|
||||||
|
def is_valid_ip_or_hostname(value: str) -> bool:
|
||||||
|
"""Validate whether a string is a valid IP address (IPv4 or IPv6) or hostname.
|
||||||
|
|
||||||
|
This function first attempts to interpret the input as an IP address using the
|
||||||
|
standard library `ipaddress` module. If that fails, it checks whether the input
|
||||||
|
is a valid hostname according to RFC 1123, which allows domain names consisting
|
||||||
|
of alphanumeric characters and hyphens, with specific length and structure rules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (str): The input string to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the input is a valid IP address or hostname, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(value)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(value) > 253:
|
||||||
|
return False
|
||||||
|
|
||||||
|
hostname_regex = re.compile(
|
||||||
|
r"^(?=.{1,253}$)(?!-)[A-Z\d-]{1,63}(?<!-)"
|
||||||
|
r"(?:\.(?!-)[A-Z\d-]{1,63}(?<!-))*\.?$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
return bool(hostname_regex.fullmatch(value))
|
||||||
|
|
||||||
|
|
||||||
def wait_for_port_free(port: int, timeout: int = 0, waiting_app_name: str = "App") -> bool:
|
def wait_for_port_free(port: int, timeout: int = 0, waiting_app_name: str = "App") -> bool:
|
||||||
@ -110,6 +141,8 @@ class ServerCommonSettings(SettingsBaseModel):
|
|||||||
cls, value: Optional[Union[str, IPvAnyAddress]]
|
cls, value: Optional[Union[str, IPvAnyAddress]]
|
||||||
) -> Optional[Union[str, IPvAnyAddress]]:
|
) -> Optional[Union[str, IPvAnyAddress]]:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
if not is_valid_ip_or_hostname(value):
|
||||||
|
raise ValueError(f"Invalid host: {value}")
|
||||||
if value.lower() in ("localhost", "loopback"):
|
if value.lower() in ("localhost", "loopback"):
|
||||||
value = "127.0.0.1"
|
value = "127.0.0.1"
|
||||||
return value
|
return value
|
||||||
|
@ -429,7 +429,7 @@ def server(xprocess, config_eos, config_default_dirs) -> Generator[str, None, No
|
|||||||
Provides URL of the server.
|
Provides URL of the server.
|
||||||
"""
|
"""
|
||||||
# create url/port info to the server
|
# create url/port info to the server
|
||||||
url = "http://0.0.0.0:8503"
|
url = "http://127.0.0.1:8503"
|
||||||
|
|
||||||
class Starter(ProcessStarter):
|
class Starter(ProcessStarter):
|
||||||
# Set environment before any subprocess run, to keep custom config dir
|
# Set environment before any subprocess run, to keep custom config dir
|
||||||
|
@ -28,8 +28,6 @@ def test_openapi_spec_current(config_eos):
|
|||||||
spec = generate_openapi.generate_openapi()
|
spec = generate_openapi.generate_openapi()
|
||||||
spec_str = json.dumps(spec, indent=4, sort_keys=True)
|
spec_str = json.dumps(spec, indent=4, sort_keys=True)
|
||||||
|
|
||||||
if os.name == "nt":
|
|
||||||
spec_str = spec_str.replace("127.0.0.1", "0.0.0.0")
|
|
||||||
with new_spec_path.open("w", encoding="utf-8", newline="\n") as f_new:
|
with new_spec_path.open("w", encoding="utf-8", newline="\n") as f_new:
|
||||||
f_new.write(spec_str)
|
f_new.write(spec_str)
|
||||||
|
|
||||||
@ -62,8 +60,6 @@ def test_openapi_md_current(config_eos):
|
|||||||
|
|
||||||
spec_md = generate_openapi_md.generate_openapi_md()
|
spec_md = generate_openapi_md.generate_openapi_md()
|
||||||
|
|
||||||
if os.name == "nt":
|
|
||||||
spec_md = spec_md.replace("127.0.0.1", "0.0.0.0")
|
|
||||||
with new_spec_md_path.open("w", encoding="utf-8", newline="\n") as f_new:
|
with new_spec_md_path.open("w", encoding="utf-8", newline="\n") as f_new:
|
||||||
f_new.write(spec_md)
|
f_new.write(spec_md)
|
||||||
|
|
||||||
@ -94,7 +90,7 @@ def test_config_md_current(config_eos):
|
|||||||
config_md = generate_config_md.generate_config_md(config_eos)
|
config_md = generate_config_md.generate_config_md(config_eos)
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
config_md = config_md.replace("127.0.0.1", "0.0.0.0").replace("\\\\", "/")
|
config_md = config_md.replace("\\\\", "/")
|
||||||
with new_config_md_path.open("w", encoding="utf-8", newline="\n") as f_new:
|
with new_config_md_path.open("w", encoding="utf-8", newline="\n") as f_new:
|
||||||
f_new.write(config_md)
|
f_new.write(config_md)
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ class TestEOSdashConfig:
|
|||||||
item["name"] == "server.eosdash_port" and item["value"] == "8504" for item in config
|
item["name"] == "server.eosdash_port" and item["value"] == "8504" for item in config
|
||||||
)
|
)
|
||||||
assert any(
|
assert any(
|
||||||
item["name"] == "server.eosdash_host" and item["value"] == '"0.0.0.0"'
|
item["name"] == "server.eosdash_host" and item["value"] == '"127.0.0.1"'
|
||||||
for item in config
|
for item in config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ class TestPydanticBaseModel:
|
|||||||
assert model.datetime_field == dt
|
assert model.datetime_field == dt
|
||||||
|
|
||||||
def test_invalid_datetime_string(self):
|
def test_invalid_datetime_string(self):
|
||||||
with pytest.raises(ValidationError, match="Input should be an instance of DateTime"):
|
with pytest.raises(ValidationError, match="Cannot convert 'invalid_datetime' to datetime"):
|
||||||
PydanticTestModel(datetime_field="invalid_datetime")
|
PydanticTestModel(datetime_field="invalid_datetime")
|
||||||
|
|
||||||
def test_iso8601_serialization(self):
|
def test_iso8601_serialization(self):
|
||||||
|
4
tests/testdata/eosserver_config_1.json
vendored
4
tests/testdata/eosserver_config_1.json
vendored
@ -75,9 +75,9 @@
|
|||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"startup_eosdash": true,
|
"startup_eosdash": true,
|
||||||
"host": "0.0.0.0",
|
"host": "127.0.0.1",
|
||||||
"port": 8503,
|
"port": 8503,
|
||||||
"eosdash_host": "0.0.0.0",
|
"eosdash_host": "127.0.0.1",
|
||||||
"eosdash_port": 8504
|
"eosdash_port": 8504
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user