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 |
|
||||
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
:::
|
||||
|
||||
@ -895,11 +895,11 @@ Validators:
|
||||
|
||||
{
|
||||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8503,
|
||||
"verbose": false,
|
||||
"startup_eosdash": true,
|
||||
"eosdash_host": "0.0.0.0",
|
||||
"eosdash_host": "127.0.0.1",
|
||||
"eosdash_port": 8504
|
||||
}
|
||||
}
|
||||
@ -1054,11 +1054,11 @@ Validators:
|
||||
"provider_settings": null
|
||||
},
|
||||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8503,
|
||||
"verbose": false,
|
||||
"startup_eosdash": true,
|
||||
"eosdash_host": "0.0.0.0",
|
||||
"eosdash_host": "127.0.0.1",
|
||||
"eosdash_port": 8504
|
||||
},
|
||||
"utils": {}
|
||||
|
@ -220,9 +220,9 @@
|
||||
"server": {
|
||||
"$ref": "#/components/schemas/ServerCommonSettings",
|
||||
"default": {
|
||||
"eosdash_host": "0.0.0.0",
|
||||
"eosdash_host": "127.0.0.1",
|
||||
"eosdash_port": 8504,
|
||||
"host": "0.0.0.0",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8503,
|
||||
"startup_eosdash": true,
|
||||
"verbose": false
|
||||
@ -2279,7 +2279,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "0.0.0.0",
|
||||
"default": "127.0.0.1",
|
||||
"description": "EOSdash server IP address.",
|
||||
"title": "Eosdash Host"
|
||||
},
|
||||
@ -2306,7 +2306,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": "0.0.0.0",
|
||||
"default": "127.0.0.1",
|
||||
"description": "EOS server IP address.",
|
||||
"title": "Host"
|
||||
},
|
||||
|
@ -43,12 +43,18 @@ profile = "black"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
exclude = [
|
||||
"tests",
|
||||
"scripts",
|
||||
]
|
||||
output-format = "full"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"F", # Enable all `Pyflakes` rules.
|
||||
"D", # Enable all `pydocstyle` rules, limiting to those that adhere to the
|
||||
# Google convention via `convention = "google"`, below.
|
||||
"S", # Enable all `flake8-bandit` rules.
|
||||
]
|
||||
ignore = [
|
||||
# Prevent errors due to ruff false positives
|
||||
|
@ -298,7 +298,7 @@ def main():
|
||||
try:
|
||||
config_md = generate_config_md(config_eos)
|
||||
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:
|
||||
# Write to file
|
||||
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
||||
|
@ -58,8 +58,6 @@ def main():
|
||||
try:
|
||||
openapi_spec = generate_openapi()
|
||||
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:
|
||||
# Write to file
|
||||
with open(args.output_file, "w", encoding="utf-8", newline="\n") as f:
|
||||
|
@ -286,7 +286,7 @@ def main():
|
||||
try:
|
||||
openapi_md = generate_openapi_md()
|
||||
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:
|
||||
# Write to file
|
||||
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:
|
||||
"""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
|
||||
SettingsBaseModel.config = self
|
||||
# (Re-)load settings
|
||||
@ -394,7 +403,9 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
||||
ValueError: If the `settings` is not a `SettingsEOS` instance.
|
||||
"""
|
||||
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))
|
||||
|
||||
@ -471,10 +482,10 @@ class ConfigEOS(SingletonMixin, SettingsEOSDefaults):
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
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 = []
|
||||
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__)
|
||||
cache_file.seek(0)
|
||||
if "b" in mode:
|
||||
result = pickle.load(cache_file)
|
||||
result = pickle.load(cache_file) # noqa: S301
|
||||
else:
|
||||
result = cache_file.read()
|
||||
except Exception as e:
|
||||
|
@ -34,7 +34,7 @@ class classproperty:
|
||||
argument and returns a value.
|
||||
|
||||
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:
|
||||
@ -43,5 +43,6 @@ class classproperty:
|
||||
def __get__(self, _: Any, owner_cls: Optional[type[Any]] = None) -> Any:
|
||||
if owner_cls is None:
|
||||
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)
|
||||
|
@ -393,7 +393,8 @@ class EnergyManagement(SingletonMixin, ConfigMixin, PredictionMixin, PydanticBas
|
||||
|
||||
# Fetch objects
|
||||
battery = self.battery
|
||||
assert battery # to please mypy
|
||||
if battery is None:
|
||||
raise ValueError(f"battery not set: {battery}")
|
||||
ev = self.ev
|
||||
home_appliance = self.home_appliance
|
||||
inverter = self.inverter
|
||||
|
@ -450,8 +450,8 @@ class PydanticBaseModel(BaseModel, PydanticModelNestedValueMixin):
|
||||
if expected_type is pendulum.DateTime or expected_type is AwareDatetime:
|
||||
try:
|
||||
value = to_datetime(value)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise ValueError(f"Cannot convert {value!r} to datetime: {e}")
|
||||
return value
|
||||
|
||||
# Override Pydantic’s serialization for all DateTime fields
|
||||
|
@ -121,7 +121,8 @@ class Battery(DeviceBase):
|
||||
|
||||
def _setup(self) -> None:
|
||||
"""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.initial_soc_percentage = self.parameters.initial_soc_percentage
|
||||
self.charging_efficiency = self.parameters.charging_efficiency
|
||||
|
@ -170,7 +170,8 @@ class DevicesBase(DevicesStartEndMixin, PredictionMixin):
|
||||
def add_device(self, device: Optional["DeviceBase"]) -> None:
|
||||
if device is None:
|
||||
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
|
||||
|
||||
def remove_device(self, device: Type["DeviceBase"] | str) -> bool:
|
||||
|
@ -34,7 +34,8 @@ class HomeAppliance(DeviceBase):
|
||||
super().__init__(parameters)
|
||||
|
||||
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.duration_h = self.parameters.duration_h
|
||||
self.consumption_wh = self.parameters.consumption_wh
|
||||
|
@ -28,7 +28,8 @@ class Inverter(DeviceBase):
|
||||
super().__init__(parameters)
|
||||
|
||||
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:
|
||||
# For the moment raise exception
|
||||
# TODO: Make battery configurable by config
|
||||
@ -41,7 +42,8 @@ class Inverter(DeviceBase):
|
||||
) # Maximum power that the inverter can handle
|
||||
|
||||
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)
|
||||
|
||||
def process_energy(
|
||||
|
@ -121,7 +121,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
||||
if self.fix_seed is not None:
|
||||
random.seed(self.fix_seed)
|
||||
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)
|
||||
|
||||
def decode_charge_discharge(
|
||||
|
@ -104,12 +104,13 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
||||
- add the file cache again.
|
||||
"""
|
||||
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
|
||||
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")
|
||||
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}")
|
||||
response.raise_for_status() # Raise an error for bad responses
|
||||
akkudoktor_data = self._validate_data(response.content)
|
||||
@ -148,7 +149,8 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
||||
"""
|
||||
# Get Akkudoktor electricity price data
|
||||
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
|
||||
# in ascending order and have the same timestamps.
|
||||
@ -178,7 +180,10 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
|
||||
)
|
||||
|
||||
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
|
||||
needed_hours = int(
|
||||
|
@ -14,7 +14,7 @@ class SelfConsumptionProbabilityInterpolator:
|
||||
self.filepath = filepath
|
||||
# Load the RegularGridInterpolator
|
||||
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)
|
||||
def generate_points(
|
||||
|
@ -291,7 +291,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
Raises:
|
||||
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
|
||||
logger.debug(f"Response from {self._url()}: {response}")
|
||||
akkudoktor_data = self._validate_data(response.content)
|
||||
@ -332,7 +332,8 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
logger.error(f"Akkudoktor schema change: {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
|
||||
for forecast_values in zip(*akkudoktor_data.values):
|
||||
|
@ -100,7 +100,8 @@ class WeatherBrightSky(WeatherProvider):
|
||||
date = to_datetime(self.start_datetime, as_string=True)
|
||||
last_date = to_datetime(self.end_datetime, as_string=True)
|
||||
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
|
||||
logger.debug(f"Response from {source}: {response}")
|
||||
@ -222,7 +223,7 @@ class WeatherBrightSky(WeatherProvider):
|
||||
|
||||
# Add Preciptable Water (PWAT) with a PVLib method.
|
||||
key = WeatherDataRecord.key_from_description("Temperature (°C)")
|
||||
assert key
|
||||
assert key # noqa: S101
|
||||
temperature = self.key_to_array(
|
||||
key=key,
|
||||
start_datetime=self.start_datetime,
|
||||
@ -235,7 +236,7 @@ class WeatherBrightSky(WeatherProvider):
|
||||
logger.debug(debug_msg)
|
||||
return
|
||||
key = WeatherDataRecord.key_from_description("Relative Humidity (%)")
|
||||
assert key
|
||||
assert key # noqa: S101
|
||||
humidity = self.key_to_array(
|
||||
key=key,
|
||||
start_datetime=self.start_datetime,
|
||||
|
@ -93,7 +93,7 @@ class WeatherClearOutside(WeatherProvider):
|
||||
source = "https://clearoutside.com/forecast"
|
||||
latitude = round(self.config.general.latitude, 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
|
||||
logger.debug(f"Response from {source}: {response}")
|
||||
# We are working on fresh data (no cache), report update time
|
||||
|
@ -82,8 +82,8 @@ def AdminConfig(
|
||||
try:
|
||||
if config:
|
||||
config_file_path = get_nested_value(config, ["general", "config_file_path"])
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"general.config_file_path: {e}")
|
||||
# export config file
|
||||
export_to_file_next_tag = to_datetime(as_string="YYYYMMDDHHmmss")
|
||||
export_to_file_status = (None,)
|
||||
@ -95,7 +95,7 @@ def AdminConfig(
|
||||
if data["action"] == "save_to_file":
|
||||
# Safe current configuration to file
|
||||
try:
|
||||
result = requests.put(f"{server}/v1/config/file")
|
||||
result = requests.put(f"{server}/v1/config/file", timeout=10)
|
||||
result.raise_for_status()
|
||||
config_file_path = result.json()["general"]["config_file_path"]
|
||||
status = Success(f"Saved to '{config_file_path}' on '{eos_hostname}'")
|
||||
@ -143,7 +143,7 @@ def AdminConfig(
|
||||
try:
|
||||
with import_file_path.open("r", encoding="utf-8", newline=None) as 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()
|
||||
import_from_file_status = Success(
|
||||
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
|
||||
server = f"http://{eos_host}:{eos_port}"
|
||||
try:
|
||||
result = requests.get(f"{server}/v1/config")
|
||||
result = requests.get(f"{server}/v1/config", timeout=10)
|
||||
result.raise_for_status()
|
||||
config = result.json()
|
||||
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
|
||||
try:
|
||||
result = requests.get(f"{server}/v1/config")
|
||||
result = requests.get(f"{server}/v1/config", timeout=10)
|
||||
result.raise_for_status()
|
||||
config = result.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
@ -303,9 +303,14 @@ def ConfigPlanesCard(
|
||||
planes_update_open = True
|
||||
plane_update_open = True
|
||||
# Make mypy happy - should never trigger
|
||||
assert isinstance(update_error, (str, type(None)))
|
||||
assert isinstance(update_value, (str, type(None)))
|
||||
assert isinstance(update_open, (bool, type(None)))
|
||||
if (
|
||||
not isinstance(update_error, (str, 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(
|
||||
ConfigCard(
|
||||
config["name"],
|
||||
@ -441,9 +446,14 @@ def Configuration(
|
||||
update_value = config_update_latest.get(config["name"], {}).get("value")
|
||||
update_open = config_update_latest.get(config["name"], {}).get("open")
|
||||
# Make mypy happy - should never trigger
|
||||
assert isinstance(update_error, (str, type(None)))
|
||||
assert isinstance(update_value, (str, type(None)))
|
||||
assert isinstance(update_open, (bool, type(None)))
|
||||
if (
|
||||
not isinstance(update_error, (str, 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 (
|
||||
config["type"]
|
||||
== "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
|
||||
config = None
|
||||
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()
|
||||
config = response.json()
|
||||
except requests.exceptions.HTTPError as err:
|
||||
|
@ -75,9 +75,9 @@
|
||||
},
|
||||
"server": {
|
||||
"startup_eosdash": true,
|
||||
"host": "0.0.0.0",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8503,
|
||||
"eosdash_host": "0.0.0.0",
|
||||
"eosdash_host": "127.0.0.1",
|
||||
"eosdash_port": 8504
|
||||
},
|
||||
"weather": {
|
||||
|
@ -186,7 +186,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
||||
|
||||
# Get current configuration from server
|
||||
try:
|
||||
result = requests.get(f"{server}/v1/config")
|
||||
result = requests.get(f"{server}/v1/config", timeout=10)
|
||||
result.raise_for_status()
|
||||
except requests.exceptions.HTTPError as err:
|
||||
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:
|
||||
democonfig = json.load(fd)
|
||||
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()
|
||||
except requests.exceptions.HTTPError as err:
|
||||
detail = result.json()["detail"]
|
||||
# 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(
|
||||
f"Can not set demo configuration on {server}: {err}, {detail}",
|
||||
cls="text-center",
|
||||
@ -213,12 +213,12 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
||||
|
||||
# Update all predictions
|
||||
try:
|
||||
result = requests.post(f"{server}/v1/prediction/update")
|
||||
result = requests.post(f"{server}/v1/prediction/update", timeout=10)
|
||||
result.raise_for_status()
|
||||
except requests.exceptions.HTTPError as err:
|
||||
detail = result.json()["detail"]
|
||||
# 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(
|
||||
f"Can not update predictions on {server}: {err}, {detail}",
|
||||
cls="text-center",
|
||||
@ -239,7 +239,7 @@ def Demo(eos_host: str, eos_port: Union[str, int]) -> str:
|
||||
"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()
|
||||
predictions = PydanticDateTimeDataFrame(**result.json()).to_dataframe()
|
||||
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
|
||||
requests.put(f"{server}/v1/config", json=config)
|
||||
requests.put(f"{server}/v1/config", json=config, timeout=10)
|
||||
|
||||
return Grid(
|
||||
DemoPVForecast(predictions, democonfig),
|
||||
|
@ -24,7 +24,7 @@ def get_alive(eos_host: str, eos_port: Union[str, int]) -> str:
|
||||
"""
|
||||
result = requests.Response()
|
||||
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:
|
||||
alive = result.json()["status"]
|
||||
else:
|
||||
|
@ -49,7 +49,11 @@ from akkudoktoreos.prediction.prediction import PredictionCommonSettings, get_pr
|
||||
from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings
|
||||
from akkudoktoreos.server.rest.error import create_error_page
|
||||
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
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@ -100,6 +104,11 @@ def start_eosdash(
|
||||
Raises:
|
||||
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")
|
||||
|
||||
# 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
|
||||
|
||||
try:
|
||||
server_process = subprocess.Popen(
|
||||
server_process = subprocess.Popen( # noqa: S603
|
||||
cmd,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
@ -240,10 +249,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
access_log = args.access_log
|
||||
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_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_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_CONFIG_DIR"] = str(config_eos.general.config_folder_path)
|
||||
|
||||
new_process = subprocess.Popen(
|
||||
new_process = subprocess.Popen( # noqa: S603
|
||||
[
|
||||
sys.executable,
|
||||
]
|
||||
@ -1208,7 +1217,7 @@ def redirect(request: Request, path: str) -> Union[HTMLResponse, RedirectRespons
|
||||
if port is None:
|
||||
port = 8504
|
||||
# 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"
|
||||
url = f"http://{host}:{port}/"
|
||||
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
|
||||
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"
|
||||
if host and config_eos.server.eosdash_port:
|
||||
# Redirect to EOSdash server
|
||||
@ -1258,7 +1267,7 @@ def run_eos(host: str, port: int, log_level: str, access_log: bool, reload: bool
|
||||
None
|
||||
"""
|
||||
# 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"
|
||||
|
||||
# Wait for EOS port to be free - e.g. in case of restart
|
||||
|
@ -242,7 +242,7 @@ def run_eosdash() -> None:
|
||||
reload = False
|
||||
|
||||
# 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"
|
||||
|
||||
# Wait for EOSdash port to be free - e.g. in case of restart
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Server Module."""
|
||||
|
||||
import os
|
||||
import ipaddress
|
||||
import re
|
||||
import time
|
||||
from typing import Optional, Union
|
||||
|
||||
@ -14,9 +15,39 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_default_host() -> str:
|
||||
if os.name == "nt":
|
||||
return "127.0.0.1"
|
||||
return "0.0.0.0"
|
||||
"""Default host for EOS."""
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
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:
|
||||
@ -110,6 +141,8 @@ class ServerCommonSettings(SettingsBaseModel):
|
||||
cls, value: Optional[Union[str, IPvAnyAddress]]
|
||||
) -> Optional[Union[str, IPvAnyAddress]]:
|
||||
if isinstance(value, str):
|
||||
if not is_valid_ip_or_hostname(value):
|
||||
raise ValueError(f"Invalid host: {value}")
|
||||
if value.lower() in ("localhost", "loopback"):
|
||||
value = "127.0.0.1"
|
||||
return value
|
||||
|
@ -429,7 +429,7 @@ def server(xprocess, config_eos, config_default_dirs) -> Generator[str, None, No
|
||||
Provides URL of 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):
|
||||
# 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_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:
|
||||
f_new.write(spec_str)
|
||||
|
||||
@ -62,8 +60,6 @@ def test_openapi_md_current(config_eos):
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
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:
|
||||
f_new.write(config_md)
|
||||
|
||||
|
@ -83,7 +83,7 @@ class TestEOSdashConfig:
|
||||
item["name"] == "server.eosdash_port" and item["value"] == "8504" for item in config
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -152,7 +152,7 @@ class TestPydanticBaseModel:
|
||||
assert model.datetime_field == dt
|
||||
|
||||
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")
|
||||
|
||||
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": {
|
||||
"startup_eosdash": true,
|
||||
"host": "0.0.0.0",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8503,
|
||||
"eosdash_host": "0.0.0.0",
|
||||
"eosdash_host": "127.0.0.1",
|
||||
"eosdash_port": 8504
|
||||
},
|
||||
"weather": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user