From a7f03a1af9d5d3106bfc00363c3bf48f25c9cc04 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 25 Sep 2024 23:55:32 +0200 Subject: [PATCH] [#344] Removed `marshmallow_dataclass` dependency from `system` plugin. --- platypush/plugins/system/__init__.py | 2 + platypush/schemas/dataclasses/__init__.py | 4 +- platypush/schemas/system/_base.py | 2 +- platypush/schemas/system/_battery/_base.py | 20 -- platypush/schemas/system/_battery/_model.py | 36 +- platypush/schemas/system/_battery/_schemas.py | 46 ++- platypush/schemas/system/_connection/_base.py | 39 --- .../schemas/system/_connection/_model.py | 96 +----- .../schemas/system/_connection/_schemas.py | 112 ++++++- platypush/schemas/system/_cpu/__init__.py | 9 +- platypush/schemas/system/_cpu/_base.py | 56 ---- platypush/schemas/system/_cpu/_model.py | 147 ++------ platypush/schemas/system/_cpu/_schemas.py | 315 +++++++++++++++++- platypush/schemas/system/_disk/_base.py | 24 -- platypush/schemas/system/_disk/_model.py | 136 +------- platypush/schemas/system/_disk/_schemas.py | 132 +++++++- platypush/schemas/system/_fan/_base.py | 15 - platypush/schemas/system/_fan/_model.py | 32 +- platypush/schemas/system/_fan/_schemas.py | 40 ++- platypush/schemas/system/_memory/_base.py | 18 - platypush/schemas/system/_memory/_model.py | 116 +------ platypush/schemas/system/_memory/_schemas.py | 135 +++++++- platypush/schemas/system/_network/_base.py | 43 --- platypush/schemas/system/_network/_model.py | 204 ++---------- platypush/schemas/system/_network/_schemas.py | 210 +++++++++++- platypush/schemas/system/_process/_base.py | 28 -- platypush/schemas/system/_process/_model.py | 110 +----- platypush/schemas/system/_process/_schemas.py | 125 ++++++- platypush/schemas/system/_schemas.py | 27 +- .../schemas/system/_temperature/_base.py | 15 - .../schemas/system/_temperature/_model.py | 53 +-- .../schemas/system/_temperature/_schemas.py | 55 ++- platypush/schemas/system/_user/_base.py | 22 -- platypush/schemas/system/_user/_model.py | 40 +-- platypush/schemas/system/_user/_schemas.py | 48 ++- 35 files changed, 1327 insertions(+), 1185 deletions(-) delete mode 100644 platypush/schemas/system/_battery/_base.py delete mode 100644 platypush/schemas/system/_connection/_base.py delete mode 100644 platypush/schemas/system/_cpu/_base.py delete mode 100644 platypush/schemas/system/_disk/_base.py delete mode 100644 platypush/schemas/system/_fan/_base.py delete mode 100644 platypush/schemas/system/_memory/_base.py delete mode 100644 platypush/schemas/system/_network/_base.py delete mode 100644 platypush/schemas/system/_process/_base.py delete mode 100644 platypush/schemas/system/_temperature/_base.py delete mode 100644 platypush/schemas/system/_user/_base.py diff --git a/platypush/plugins/system/__init__.py b/platypush/plugins/system/__init__.py index 5156681755..8933f486de 100644 --- a/platypush/plugins/system/__init__.py +++ b/platypush/plugins/system/__init__.py @@ -154,6 +154,8 @@ class SystemPlugin(SensorPlugin, EntityManager): @staticmethod def _cpu_frequency_avg() -> CpuFrequency: + # Dummy call to ensure the CPU frequency is updated + psutil.cpu_freq(percpu=False) return CpuFrequencySchema().load(psutil.cpu_freq(percpu=False)) # type: ignore @staticmethod diff --git a/platypush/schemas/dataclasses/__init__.py b/platypush/schemas/dataclasses/__init__.py index 3a4eb9a8f6..22df0920c5 100644 --- a/platypush/schemas/dataclasses/__init__.py +++ b/platypush/schemas/dataclasses/__init__.py @@ -1,4 +1,3 @@ -from dataclasses import field from datetime import date, datetime from uuid import UUID @@ -18,8 +17,7 @@ def percent_field(**kwargs): """ Field used to model percentage float fields between 0 and 1. """ - return field( - default_factory=float, + return fields.Float( metadata={ 'validate': Range(min=0, max=1), **kwargs, diff --git a/platypush/schemas/system/_base.py b/platypush/schemas/system/_base.py index 26c03a4de2..9befdde36e 100644 --- a/platypush/schemas/system/_base.py +++ b/platypush/schemas/system/_base.py @@ -5,7 +5,7 @@ from platypush.schemas.dataclasses import DataClassSchema class SystemBaseSchema(DataClassSchema): """ - Base schema for system info. + Base schema for system information. """ @pre_load diff --git a/platypush/schemas/system/_battery/_base.py b/platypush/schemas/system/_battery/_base.py deleted file mode 100644 index 33e1603cd0..0000000000 --- a/platypush/schemas/system/_battery/_base.py +++ /dev/null @@ -1,20 +0,0 @@ -from enum import Enum - -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class BatteryBaseSchema(SystemBaseSchema): - """ - Base schema for system battery sensors. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - percent = data.pop('percent', data.pop('value', None)) - seconds_left = data.pop('secsleft', data.pop('seconds_left', None)) - data['value'] = percent / 100 if percent is not None else None - data['seconds_left'] = None if isinstance(seconds_left, Enum) else seconds_left - return data diff --git a/platypush/schemas/system/_battery/_model.py b/platypush/schemas/system/_battery/_model.py index cd35e479c5..f329cc0016 100644 --- a/platypush/schemas/system/_battery/_model.py +++ b/platypush/schemas/system/_battery/_model.py @@ -1,39 +1,13 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional -from platypush.schemas.dataclasses import percent_field - @dataclass class Battery: """ - System battery sensor wrapper. + System battery sensor representation. """ - seconds_left: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'High threshold for the temperature sensor, in Celsius', - 'example': 75, - } - } - ) - - power_plugged: Optional[bool] = field( - metadata={ - 'metadata': { - 'description': 'Whether the battery is plugged in or not', - 'example': False, - } - } - ) - - value: Optional[float] = percent_field( - metadata={ - 'metadata': { - 'description': 'Current charge left, as a percentage value ' - 'between 0 and 1', - 'example': 0.5, - } - } - ) + seconds_left: Optional[float] = None + power_plugged: Optional[bool] = None + value: Optional[float] = None diff --git a/platypush/schemas/system/_battery/_schemas.py b/platypush/schemas/system/_battery/_schemas.py index 7a6e458972..65da669aa7 100644 --- a/platypush/schemas/system/_battery/_schemas.py +++ b/platypush/schemas/system/_battery/_schemas.py @@ -1,7 +1,45 @@ -from marshmallow_dataclass import class_schema +from enum import Enum -from ._base import BatteryBaseSchema -from ._model import Battery +from marshmallow import fields, pre_load + +from platypush.schemas.dataclasses import percent_field + +from .._base import SystemBaseSchema -BatterySchema = class_schema(Battery, base_schema=BatteryBaseSchema) +class BatterySchema(SystemBaseSchema): + """ + System battery sensor schema. + """ + + seconds_left = fields.Float( + allow_none=True, + metadata={ + 'description': 'Number of seconds left before the battery runs out', + 'example': 7200, + }, + ) + + power_plugged = fields.Boolean( + metadata={ + 'description': 'Whether the battery is currently plugged in', + 'example': False, + } + ) + + value = percent_field( + allow_none=True, + metadata={ + 'description': 'Current battery charge level, as a percentage between 0 and 1', + 'example': 0.5, + }, + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + percent = data.pop('percent', data.pop('value', None)) + seconds_left = data.pop('secsleft', data.pop('seconds_left', None)) + data['value'] = percent / 100 if percent is not None else None + data['seconds_left'] = None if isinstance(seconds_left, Enum) else seconds_left + return data diff --git a/platypush/schemas/system/_connection/_base.py b/platypush/schemas/system/_connection/_base.py deleted file mode 100644 index 97977c57ad..0000000000 --- a/platypush/schemas/system/_connection/_base.py +++ /dev/null @@ -1,39 +0,0 @@ -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class ConnectionBaseSchema(SystemBaseSchema): - """ - Base schema for connections. - """ - - @pre_load - def pre_load(self, data, **_) -> dict: - data = super().pre_load(data) - addr_mapping = { - 'laddr': ('local_address', 'local_port'), - 'raddr': ('remote_address', 'remote_port'), - } - - # Parse laddr/raddr attributes - for ext_attr, (addr_attr, port_attr) in addr_mapping.items(): - value = data.pop(ext_attr, None) - if not value: - data[addr_attr] = data[port_attr] = None - elif isinstance(value, tuple): - data[addr_attr], data[port_attr] = value - elif isinstance(value, str): - data[addr_attr] = value - data[port_attr] = None - - # Handle enum values - for attr in ['type', 'family']: - value = data.pop(attr, None) - if value is not None: - data[attr] = value.name - - if data.get('status') == 'NONE': - data['status'] = None - - return data diff --git a/platypush/schemas/system/_connection/_model.py b/platypush/schemas/system/_connection/_model.py index d4c4a760e3..1c7013da21 100644 --- a/platypush/schemas/system/_connection/_model.py +++ b/platypush/schemas/system/_connection/_model.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from socket import AddressFamily, SocketKind from typing import Optional @@ -9,88 +9,12 @@ class Connection: Network/UNIX socket data class. """ - fd: int = field( - metadata={ - 'metadata': { - 'description': 'File descriptor', - 'example': 3, - }, - } - ) - - family: AddressFamily = field( - metadata={ - 'metadata': { - 'description': 'Socket family', - 'example': AddressFamily.AF_INET.name, - } - } - ) - - type: SocketKind = field( - metadata={ - 'metadata': { - 'description': 'Socket type', - 'example': SocketKind.SOCK_STREAM.name, - } - } - ) - - local_address: str = field( - metadata={ - 'metadata': { - 'description': 'Local address, as an IP address for network ' - 'connections and a socket path for a UNIX socket', - 'example': '192.168.1.2', - } - } - ) - - local_port: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'Local port, if this is a TCP/UDP connection, ' - 'otherwise null', - 'example': 12345, - } - } - ) - - remote_address: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Remote address, if this is a network ' - 'connection, otherwise null', - 'example': '192.168.1.1', - } - } - ) - - remote_port: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'Local port, if this is a TCP/UDP connection, ' - 'otherwise null', - 'example': 443, - } - } - ) - - status: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Connection status, if this is a network ' - 'connection, otherise null', - 'example': 'ESTABLISHED', - } - } - ) - - pid: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'ID of the process that owns the connection', - 'example': 4321, - } - } - ) + fd: int + family: AddressFamily + type: SocketKind + local_address: str + local_port: Optional[int] = None + remote_address: Optional[str] = None + remote_port: Optional[int] = None + status: Optional[str] = None + pid: Optional[int] = None diff --git a/platypush/schemas/system/_connection/_schemas.py b/platypush/schemas/system/_connection/_schemas.py index 2ad33a7ac2..741cf40e03 100644 --- a/platypush/schemas/system/_connection/_schemas.py +++ b/platypush/schemas/system/_connection/_schemas.py @@ -1,7 +1,111 @@ -from marshmallow_dataclass import class_schema +from marshmallow import fields, pre_load -from ._base import ConnectionBaseSchema -from ._model import Connection +from .._base import SystemBaseSchema -ConnectionSchema = class_schema(Connection, base_schema=ConnectionBaseSchema) +class ConnectionSchema(SystemBaseSchema): + """ + Base schema for connections. + """ + + fd = fields.Int( + required=True, + metadata={ + 'description': 'File descriptor number.', + 'example': 3, + }, + ) + + family = fields.String( + allow_none=True, + metadata={ + 'description': 'Address family.', + 'example': 'AF_INET', + }, + ) + + type = fields.String( + allow_none=True, + metadata={ + 'description': 'Socket type.', + 'example': 'SOCK_STREAM', + }, + ) + + local_address = fields.String( + allow_none=True, + metadata={ + 'description': 'Local address.', + 'example': '192.168.1.3', + }, + ) + + local_port = fields.Int( + allow_none=True, + metadata={ + 'description': 'Local port.', + 'example': 1234, + }, + ) + + remote_address = fields.String( + allow_none=True, + metadata={ + 'description': 'Remote address.', + 'example': '192.168.1.4', + }, + ) + + remote_port = fields.Int( + allow_none=True, + metadata={ + 'description': 'Remote port.', + 'example': 5678, + }, + ) + + status = fields.String( + allow_none=True, + metadata={ + 'description': 'Connection status.', + 'example': 'ESTABLISHED', + }, + ) + + pid = fields.Int( + allow_none=True, + metadata={ + 'description': 'ID of the process that owns the connection.', + 'example': 1234, + }, + ) + + @pre_load + def pre_load(self, data, **_) -> dict: + data = super().pre_load(data) + addr_mapping = { + 'laddr': ('local_address', 'local_port'), + 'raddr': ('remote_address', 'remote_port'), + } + + # Parse laddr/raddr attributes + for ext_attr, (addr_attr, port_attr) in addr_mapping.items(): + value = data.pop(ext_attr, None) + if not value: + data[addr_attr] = data[port_attr] = None + elif isinstance(value, tuple): + data[addr_attr], data[port_attr] = value + elif isinstance(value, str): + data[addr_attr] = value + data[port_attr] = None + + # Handle enum values + for attr in ['type', 'family']: + value = data.pop(attr, None) + if value is not None: + data[attr] = value.name + + if data.get('status') == 'NONE': + data['status'] = None + + return data diff --git a/platypush/schemas/system/_cpu/__init__.py b/platypush/schemas/system/_cpu/__init__.py index e2c2b01e39..8b119e1af2 100644 --- a/platypush/schemas/system/_cpu/__init__.py +++ b/platypush/schemas/system/_cpu/__init__.py @@ -1,5 +1,11 @@ from ._model import Cpu, CpuFrequency, CpuInfo, CpuStats, CpuTimes -from ._schemas import CpuFrequencySchema, CpuInfoSchema, CpuStatsSchema, CpuTimesSchema +from ._schemas import ( + CpuFrequencySchema, + CpuInfoSchema, + CpuSchema, + CpuStatsSchema, + CpuTimesSchema, +) __all__ = [ @@ -8,6 +14,7 @@ __all__ = [ "CpuFrequencySchema", "CpuInfo", "CpuInfoSchema", + "CpuSchema", "CpuStats", "CpuStatsSchema", "CpuTimes", diff --git a/platypush/schemas/system/_cpu/_base.py b/platypush/schemas/system/_cpu/_base.py deleted file mode 100644 index e5ab02794f..0000000000 --- a/platypush/schemas/system/_cpu/_base.py +++ /dev/null @@ -1,56 +0,0 @@ -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class CpuInfoBaseSchema(SystemBaseSchema): - """ - Base schema for CPU info. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - if data.get('hz_advertised'): - data['frequency_advertised'] = data.pop('hz_advertised')[0] - if data.get('hz_actual'): - data['frequency_actual'] = data.pop('hz_actual')[0] - - for key, value in data.items(): - if key.endswith("_cache_size") and isinstance(value, str): - tokens = value.split(" ") - unit = None - if len(tokens) > 1: - unit = tokens[1] - - value = int(tokens[0]) - if unit == "KiB": - value *= 1024 - elif unit == "MiB": - value *= 1024 * 1024 - elif unit == "GiB": - value *= 1024 * 1024 * 1024 - - data[key] = value - - return data - - -class CpuTimesBaseSchema(SystemBaseSchema): - """ - Base schema for CPU times. - """ - - @pre_load - def pre_load(self, data, **_) -> dict: - """ - Convert the underlying object to dict and normalize all the percentage - values from [0, 100] to [0, 1]. - """ - data = super().pre_load(data) - return { - key: value / 100.0 - for key, value in ( - data if isinstance(data, dict) else data._asdict() - ).items() - } diff --git a/platypush/schemas/system/_cpu/_model.py b/platypush/schemas/system/_cpu/_model.py index 639652829a..f07a6ebe14 100644 --- a/platypush/schemas/system/_cpu/_model.py +++ b/platypush/schemas/system/_cpu/_model.py @@ -1,8 +1,6 @@ from dataclasses import dataclass, field from typing import List, Optional, Tuple -from platypush.schemas.dataclasses import percent_field - @dataclass class CpuInfo: @@ -10,117 +8,18 @@ class CpuInfo: CPU info data class. """ - architecture: Optional[str] = field( - metadata={ - 'data_key': 'arch_string_raw', - 'metadata': { - 'description': 'CPU architecture', - 'example': 'x86_64', - }, - } - ) - - bits: int = field( - metadata={ - 'metadata': { - 'description': 'CPU bits / register size', - 'example': 64, - } - } - ) - - cores: int = field( - metadata={ - 'data_key': 'count', - 'metadata': { - 'description': 'Number of cores', - 'example': 4, - }, - } - ) - - vendor: Optional[str] = field( - metadata={ - 'data_key': 'vendor_id_raw', - 'metadata': { - 'description': 'Vendor string', - 'example': 'GenuineIntel', - }, - } - ) - - brand: Optional[str] = field( - metadata={ - 'data_key': 'brand_raw', - 'metadata': { - 'description': 'CPU brand string', - 'example': 'Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz', - }, - } - ) - - frequency_advertised: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Advertised CPU frequency, in Hz', - 'example': 2400000000, - } - } - ) - - frequency_actual: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Actual CPU frequency, in Hz', - 'example': 2350000000, - } - } - ) - - flags: List[str] = field( - metadata={ - 'metadata': { - 'description': 'CPU flags', - 'example': ['acpi', 'aes', 'cpuid'], - } - } - ) - - l1_instruction_cache_size: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Size of the L1 instruction cache, in bytes', - 'example': 65536, - } - } - ) - - l1_data_cache_size: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Size of the L1 data cache, in bytes', - 'example': 65536, - } - } - ) - - l2_cache_size: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Size of the L2 cache, in bytes', - 'example': 524288, - } - } - ) - - l3_cache_size: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Size of the L2 cache, in bytes', - 'example': 4194304, - } - } - ) + bits: int + cores: int + architecture: Optional[str] = None + vendor: Optional[str] = None + brand: Optional[str] = None + frequency_advertised: Optional[float] = None + frequency_actual: Optional[float] = None + flags: List[str] = field(default_factory=list) + l1_instruction_cache_size: Optional[float] = None + l1_data_cache_size: Optional[float] = None + l2_cache_size: Optional[float] = None + l3_cache_size: Optional[float] = None @dataclass @@ -129,16 +28,16 @@ class CpuTimes: CPU times data class. """ - user: Optional[float] = percent_field() - nice: Optional[float] = percent_field() - system: Optional[float] = percent_field() - idle: Optional[float] = percent_field() - iowait: Optional[float] = percent_field() - irq: Optional[float] = percent_field() - softirq: Optional[float] = percent_field() - steal: Optional[float] = percent_field() - guest: Optional[float] = percent_field() - guest_nice: Optional[float] = percent_field() + user: Optional[float] = None + nice: Optional[float] = None + system: Optional[float] = None + idle: Optional[float] = None + iowait: Optional[float] = None + irq: Optional[float] = None + softirq: Optional[float] = None + steal: Optional[float] = None + guest: Optional[float] = None + guest_nice: Optional[float] = None @dataclass @@ -175,4 +74,4 @@ class Cpu: frequency: CpuFrequency stats: CpuStats load_avg: Tuple[float, float, float] - percent: float = percent_field() + percent: float diff --git a/platypush/schemas/system/_cpu/_schemas.py b/platypush/schemas/system/_cpu/_schemas.py index bdfda432a3..0a8e08e7b4 100644 --- a/platypush/schemas/system/_cpu/_schemas.py +++ b/platypush/schemas/system/_cpu/_schemas.py @@ -1,11 +1,312 @@ -from marshmallow_dataclass import class_schema +from marshmallow import fields, pre_load +from platypush.schemas.dataclasses import percent_field from .._base import SystemBaseSchema -from ._base import CpuInfoBaseSchema, CpuTimesBaseSchema -from ._model import CpuFrequency, CpuInfo, CpuStats, CpuTimes -CpuFrequencySchema = class_schema(CpuFrequency, base_schema=SystemBaseSchema) -CpuInfoSchema = class_schema(CpuInfo, base_schema=CpuInfoBaseSchema) -CpuTimesSchema = class_schema(CpuTimes, base_schema=CpuTimesBaseSchema) -CpuStatsSchema = class_schema(CpuStats, base_schema=SystemBaseSchema) +class CpuInfoSchema(SystemBaseSchema): + """ + Base schema for CPU info. + """ + + architecture = fields.String( + data_key='arch_string_raw', + metadata={ + 'description': 'CPU architecture.', + 'example': 'x86_64', + }, + ) + + bits = fields.Int( + metadata={ + 'description': 'CPU architecture bits.', + 'example': 64, + } + ) + + cores = fields.Int( + data_key='count', + metadata={ + 'description': 'Number of CPU cores.', + 'example': 4, + }, + ) + + vendor = fields.String( + data_key='vendor_id_raw', + metadata={ + 'description': 'CPU vendor.', + 'example': 'GenuineIntel', + }, + ) + + brand = fields.String( + data_key='brand_raw', + metadata={ + 'description': 'CPU brand.', + 'example': 'Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz', + }, + ) + + frequency_advertised = fields.Float( + metadata={ + 'description': 'CPU advertised frequency.', + 'example': 2800000000.0, + } + ) + + frequency_actual = fields.Float( + metadata={ + 'description': 'CPU actual frequency.', + 'example': 2650000000.0, + } + ) + + flags = fields.List( + fields.String(), + metadata={ + 'description': 'CPU flags.', + 'example': ['fpu', 'vme', 'de', 'pse', 'tsc', 'msr', 'pae'], + }, + ) + + l1_instruction_cache_size = fields.Int( + metadata={ + 'description': 'L1 instruction cache size.', + 'example': 65536, + } + ) + + l1_data_cache_size = fields.Int( + metadata={ + 'description': 'L1 data cache size.', + 'example': 65536, + } + ) + + l2_cache_size = fields.Int( + metadata={ + 'description': 'L2 cache size.', + 'example': 524288, + } + ) + + l3_cache_size = fields.Int( + metadata={ + 'description': 'L3 cache size.', + 'example': 6291456, + } + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + if data.get('hz_advertised'): + data['frequency_advertised'] = data.pop('hz_advertised')[0] + if data.get('hz_actual'): + data['frequency_actual'] = data.pop('hz_actual')[0] + + for key, value in data.items(): + if key.endswith("_cache_size") and isinstance(value, str): + tokens = value.split(" ") + unit = None + if len(tokens) > 1: + unit = tokens[1] + + value = int(tokens[0]) + if unit == "KiB": + value *= 1024 + elif unit == "MiB": + value *= 1024 * 1024 + elif unit == "GiB": + value *= 1024 * 1024 * 1024 + + data[key] = value + + return data + + +class CpuTimesSchema(SystemBaseSchema): + """ + Base schema for CPU times. + """ + + user = percent_field( + metadata={ + 'description': 'Time spent in user mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + nice = percent_field( + metadata={ + 'description': 'Time spent in nice mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + system = percent_field( + metadata={ + 'description': 'Time spent in system mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + idle = percent_field( + metadata={ + 'description': 'Time spent in idle mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + iowait = percent_field( + metadata={ + 'description': 'Time spent in I/O wait mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + irq = percent_field( + metadata={ + 'description': 'Time spent in IRQ mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + softirq = percent_field( + metadata={ + 'description': 'Time spent in soft IRQ mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + steal = percent_field( + metadata={ + 'description': 'Time spent in steal mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + guest = percent_field( + metadata={ + 'description': 'Time spent in guest mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + guest_nice = percent_field( + metadata={ + 'description': 'Time spent in guest nice mode, ' + 'as a percentage between 0 and 1 of the total CPU time.', + 'example': 0.0, + } + ) + + @pre_load + def pre_load(self, data, **_) -> dict: + """ + Convert the underlying object to dict and normalize all the percentage + values from [0, 100] to [0, 1]. + """ + data = super().pre_load(data) + return { + key: value / 100.0 + for key, value in ( + data if isinstance(data, dict) else data._asdict() + ).items() + } + + +class CpuFrequencySchema(SystemBaseSchema): + """ + Base schema for CPU frequency. + """ + + current = fields.Float( + metadata={ + 'description': 'Current CPU frequency.', + 'example': 2800000000.0, + } + ) + + min = fields.Float( + metadata={ + 'description': 'Minimum CPU frequency.', + 'example': 800000000.0, + } + ) + + max = fields.Float( + metadata={ + 'description': 'Maximum CPU frequency.', + 'example': 2800000000.0, + } + ) + + +class CpuStatsSchema(SystemBaseSchema): + """ + Base schema for CPU stats. + """ + + ctx_switches = fields.Int( + metadata={ + 'description': 'Number of context switches.', + 'example': 1234, + } + ) + + interrupts = fields.Int( + metadata={ + 'description': 'Number of interrupts.', + 'example': 1234, + } + ) + + soft_interrupts = fields.Int( + metadata={ + 'description': 'Number of soft interrupts.', + 'example': 1234, + } + ) + + syscalls = fields.Int( + metadata={ + 'description': 'Number of system calls.', + 'example': 1234, + } + ) + + +class CpuSchema(SystemBaseSchema): + """ + Base schema for CPU results. + """ + + info = fields.Nested(CpuInfoSchema) + times = fields.Nested(CpuTimesSchema) + frequency = fields.Nested(CpuFrequencySchema) + stats = fields.Nested(CpuStatsSchema) + load_avg = fields.Tuple( + [fields.Float(), fields.Float(), fields.Float()], + metadata={ + 'description': 'CPU load average.', + 'example': (0.0, 0.0, 0.0), + }, + ) + + percent = percent_field( + metadata={ + 'description': 'CPU usage percentage, as a value between 0 and 1.', + 'example': 0.0, + } + ) diff --git a/platypush/schemas/system/_disk/_base.py b/platypush/schemas/system/_disk/_base.py deleted file mode 100644 index 3f55603f73..0000000000 --- a/platypush/schemas/system/_disk/_base.py +++ /dev/null @@ -1,24 +0,0 @@ -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class DiskBaseSchema(SystemBaseSchema): - """ - Base schema for disk stats. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - - # Convert read/write/busy times from milliseconds to seconds - for attr in ['read_time', 'write_time', 'busy_time']: - if data.get(attr) is not None: - data[attr] /= 1000 - - # Normalize the percentage between 0 and 1 - if data.get('percent') is not None: - data['percent'] /= 100 - - return data diff --git a/platypush/schemas/system/_disk/_model.py b/platypush/schemas/system/_disk/_model.py index 822ec99837..e1fb88437d 100644 --- a/platypush/schemas/system/_disk/_model.py +++ b/platypush/schemas/system/_disk/_model.py @@ -1,8 +1,6 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional -from platypush.schemas.dataclasses import percent_field - @dataclass class Disk: @@ -10,120 +8,18 @@ class Disk: Disk data class. """ - device: str = field( - metadata={ - 'metadata': { - 'description': 'Path/identifier of the disk/partition', - 'example': '/dev/sda1', - } - } - ) - - mountpoint: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Where the disk is mounted', - 'example': '/home', - } - } - ) - - fstype: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Filesystem type', - 'example': 'ext4', - } - } - ) - - opts: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Extra mount options passed to the partition', - 'example': 'rw,relatime,fmask=0022,dmask=0022,utf8', - } - } - ) - - total: int = field( - metadata={ - 'metadata': { - 'description': 'Total available space, in bytes', - } - } - ) - - used: int = field( - metadata={ - 'metadata': { - 'description': 'Used disk space, in bytes', - } - } - ) - - free: int = field( - metadata={ - 'metadata': { - 'description': 'Free disk space, in bytes', - } - } - ) - - read_count: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'Number of recorded read operations', - } - } - ) - - write_count: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'Number of recorded write operations', - } - } - ) - - read_bytes: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'Number of read bytes', - } - } - ) - - write_bytes: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'Number of written bytes', - } - } - ) - - read_time: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Time spent reading, in seconds', - } - } - ) - - write_time: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Time spent writing, in seconds', - } - } - ) - - busy_time: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Total disk busy time, in seconds', - } - } - ) - - percent: float = percent_field() + device: str + mountpoint: Optional[str] = None + fstype: Optional[str] = None + opts: Optional[str] = None + total: Optional[int] = None + used: Optional[int] = None + free: Optional[int] = None + read_count: Optional[int] = None + write_count: Optional[int] = None + read_bytes: Optional[int] = None + write_bytes: Optional[int] = None + read_time: Optional[float] = None + write_time: Optional[float] = None + busy_time: Optional[float] = None + percent: Optional[float] = None diff --git a/platypush/schemas/system/_disk/_schemas.py b/platypush/schemas/system/_disk/_schemas.py index 564910ae4d..b74a4d9dec 100644 --- a/platypush/schemas/system/_disk/_schemas.py +++ b/platypush/schemas/system/_disk/_schemas.py @@ -1,7 +1,131 @@ -from marshmallow_dataclass import class_schema +from marshmallow import fields, pre_load -from ._base import DiskBaseSchema -from ._model import Disk +from platypush.schemas.dataclasses import percent_field +from .._base import SystemBaseSchema -DiskSchema = class_schema(Disk, base_schema=DiskBaseSchema) +class DiskSchema(SystemBaseSchema): + """ + Base schema for disk stats. + """ + + device = fields.String( + required=True, + metadata={ + 'description': 'Path/identifier of the disk/partition', + 'example': '/dev/sda1', + }, + ) + + mountpoint = fields.String( + metadata={ + 'description': 'Mountpoint of the disk/partition', + 'example': '/mnt/data', + } + ) + + fstype = fields.String( + metadata={ + 'description': 'Filesystem type', + 'example': 'ext4', + } + ) + + opts = fields.String( # type: ignore + metadata={ + 'description': 'Mount options', + 'example': 'rw,relatime', + } + ) + + total = fields.Integer( + metadata={ + 'description': 'Total disk space in bytes', + 'example': 1024**3, + } + ) + + used = fields.Integer( + metadata={ + 'description': 'Used disk space in bytes', + 'example': 1024**2, + } + ) + + free = fields.Integer( + metadata={ + 'description': 'Free disk space in bytes', + 'example': (1024**3) - (1024**2), + } + ) + + read_count = fields.Integer( + metadata={ + 'description': 'Number of read operations', + 'example': 100, + } + ) + + write_count = fields.Integer( + metadata={ + 'description': 'Number of write operations', + 'example': 50, + } + ) + + read_bytes = fields.Integer( + metadata={ + 'description': 'Number of bytes read', + 'example': 1024**3, + } + ) + + write_bytes = fields.Integer( + metadata={ + 'description': 'Number of bytes written', + 'example': 1024**2, + } + ) + + read_time = fields.Float( + metadata={ + 'description': 'Time spent reading in seconds', + 'example': 10.5, + } + ) + + write_time = fields.Float( + metadata={ + 'description': 'Time spent writing in seconds', + 'example': 5.5, + } + ) + + busy_time = fields.Float( + metadata={ + 'description': 'Time spent doing I/Os in seconds', + 'example': 20.5, + } + ) + + percent = percent_field( + metadata={ + 'description': 'Percentage of disk space used, normalized between 0 and 1', + 'example': 0.1, + } + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + + # Convert read/write/busy times from milliseconds to seconds + for attr in ['read_time', 'write_time', 'busy_time']: + if data.get(attr) is not None: + data[attr] /= 1000 + + # Normalize the percentage between 0 and 1 + if data.get('percent') is not None: + data['percent'] /= 100 + + return data diff --git a/platypush/schemas/system/_fan/_base.py b/platypush/schemas/system/_fan/_base.py deleted file mode 100644 index 2a3fc7497c..0000000000 --- a/platypush/schemas/system/_fan/_base.py +++ /dev/null @@ -1,15 +0,0 @@ -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class FanBaseSchema(SystemBaseSchema): - """ - Base schema for system fan sensors. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - data['value'] = data.pop('current', data.pop('value', None)) - return data diff --git a/platypush/schemas/system/_fan/_model.py b/platypush/schemas/system/_fan/_model.py index 17a6ff9367..7550662a48 100644 --- a/platypush/schemas/system/_fan/_model.py +++ b/platypush/schemas/system/_fan/_model.py @@ -1,4 +1,5 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass +from typing import Optional @dataclass @@ -7,29 +8,6 @@ class Fan: System fan sensor data class. """ - id: str = field( - metadata={ - 'metadata': { - 'description': 'Unique ID for the sensor', - 'example': 'acpi_1', - } - } - ) - - label: str = field( - metadata={ - 'metadata': { - 'description': 'Name of the sensor', - 'example': 'CPU', - } - } - ) - - value: float = field( - metadata={ - 'metadata': { - 'description': 'Current fan speed, in RPM', - 'example': 3000, - } - } - ) + id: str + label: Optional[str] = None + value: Optional[float] = None diff --git a/platypush/schemas/system/_fan/_schemas.py b/platypush/schemas/system/_fan/_schemas.py index 1c82d8b25e..273022f31e 100644 --- a/platypush/schemas/system/_fan/_schemas.py +++ b/platypush/schemas/system/_fan/_schemas.py @@ -1,7 +1,39 @@ -from marshmallow_dataclass import class_schema +from marshmallow import fields, pre_load -from ._base import FanBaseSchema -from ._model import Fan +from .._base import SystemBaseSchema -FanSchema = class_schema(Fan, base_schema=FanBaseSchema) +class FanSchema(SystemBaseSchema): + """ + Base schema for system fan sensors. + """ + + id = fields.String( + required=True, + metadata={ + 'description': 'Unique ID for the sensor', + 'example': 'acpi_1', + }, + ) + + label = fields.String( + allow_none=True, + metadata={ + 'description': 'Label for the sensor', + 'example': 'CPU Fan', + }, + ) + + value = fields.Float( + allow_none=True, + metadata={ + 'description': 'Current fan speed in RPM', + 'example': 1200, + }, + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + data['value'] = data.pop('current', data.pop('value', None)) + return data diff --git a/platypush/schemas/system/_memory/_base.py b/platypush/schemas/system/_memory/_base.py deleted file mode 100644 index 85e10d7d8c..0000000000 --- a/platypush/schemas/system/_memory/_base.py +++ /dev/null @@ -1,18 +0,0 @@ -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class MemoryStatsBaseSchema(SystemBaseSchema): - """ - Base schema for memory stats. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - - # Normalize the percentage between 0 and 1 - if data.get('percent') is not None: - data['percent'] /= 100 - return data diff --git a/platypush/schemas/system/_memory/_model.py b/platypush/schemas/system/_memory/_model.py index ab9ea68ed2..5fdcb4d710 100644 --- a/platypush/schemas/system/_memory/_model.py +++ b/platypush/schemas/system/_memory/_model.py @@ -1,6 +1,4 @@ -from dataclasses import dataclass, field - -from platypush.schemas.dataclasses import percent_field +from dataclasses import dataclass @dataclass @@ -9,79 +7,16 @@ class MemoryStats: Memory stats data class. """ - total: int = field( - metadata={ - 'metadata': { - 'description': 'Total available memory, in bytes', - } - } - ) - - available: int = field( - metadata={ - 'metadata': { - 'description': 'Available memory, in bytes', - } - } - ) - - used: int = field( - metadata={ - 'metadata': { - 'description': 'Used memory, in bytes', - } - } - ) - - free: int = field( - metadata={ - 'metadata': { - 'description': 'Free memory, in bytes', - } - } - ) - - active: int = field( - metadata={ - 'metadata': { - 'description': 'Size of the active memory, in bytes', - } - } - ) - - inactive: int = field( - metadata={ - 'metadata': { - 'description': 'Size of the inactive memory, in bytes', - } - } - ) - - buffers: int = field( - metadata={ - 'metadata': { - 'description': 'Size of the buffered memory, in bytes', - } - } - ) - - cached: int = field( - metadata={ - 'metadata': { - 'description': 'Size of the cached memory, in bytes', - } - } - ) - - shared: int = field( - metadata={ - 'metadata': { - 'description': 'Size of the shared memory, in bytes', - } - } - ) - - percent: float = percent_field() + total: int + available: int + used: int + free: int + active: int + inactive: int + buffers: int + cached: int + shared: int + percent: float @dataclass @@ -90,28 +25,7 @@ class SwapStats: Swap memory stats data class. """ - total: int = field( - metadata={ - 'metadata': { - 'description': 'Total available memory, in bytes', - } - } - ) - - used: int = field( - metadata={ - 'metadata': { - 'description': 'Used memory, in bytes', - } - } - ) - - free: int = field( - metadata={ - 'metadata': { - 'description': 'Free memory, in bytes', - } - } - ) - - percent: float = percent_field() + total: int + used: int + free: int + percent: float diff --git a/platypush/schemas/system/_memory/_schemas.py b/platypush/schemas/system/_memory/_schemas.py index 9d7bc21df8..15f828dd52 100644 --- a/platypush/schemas/system/_memory/_schemas.py +++ b/platypush/schemas/system/_memory/_schemas.py @@ -1,8 +1,133 @@ -from marshmallow_dataclass import class_schema +from marshmallow import fields, pre_load -from ._base import MemoryStatsBaseSchema -from ._model import MemoryStats, SwapStats +from platypush.schemas.dataclasses import percent_field + +from .._base import SystemBaseSchema -MemoryStatsSchema = class_schema(MemoryStats, base_schema=MemoryStatsBaseSchema) -SwapStatsSchema = class_schema(SwapStats, base_schema=MemoryStatsBaseSchema) +class MemoryStatsSchema(SystemBaseSchema): + """ + Base schema for memory stats. + """ + + total = fields.Integer( + metadata={ + 'description': 'Total memory available in bytes.', + 'example': 8589934592, + } + ) + + available = fields.Integer( + metadata={ + 'description': 'Memory available in bytes.', + 'example': 2147483648, + } + ) + + used = fields.Integer( + metadata={ + 'description': 'Memory used in bytes.', + 'example': 6442450944, + } + ) + + free = fields.Integer( + metadata={ + 'description': 'Memory free in bytes.', + 'example': 2147483648, + } + ) + + active = fields.Integer( + metadata={ + 'description': 'Memory active in bytes.', + 'example': 4294967296, + } + ) + + inactive = fields.Integer( + metadata={ + 'description': 'Memory inactive in bytes.', + 'example': 2147483648, + } + ) + + buffers = fields.Integer( + metadata={ + 'description': 'Memory buffers in bytes.', + 'example': 2147483648, + } + ) + + cached = fields.Integer( + metadata={ + 'description': 'Memory cached in bytes.', + 'example': 2147483648, + } + ) + + shared = fields.Integer( + metadata={ + 'description': 'Memory shared in bytes.', + 'example': 3221225472, + } + ) + + percent = percent_field( + metadata={ + 'description': 'Memory usage percentage between 0 and 1.', + 'example': 0.75, + } + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + + # Normalize the percentage between 0 and 1 + if data.get('percent') is not None: + data['percent'] /= 100 + return data + + +class SwapStatsSchema(SystemBaseSchema): + """ + Base schema for swap stats. + """ + + total = fields.Integer( + metadata={ + 'description': 'Total memory available in bytes.', + 'example': 8589934592, + } + ) + + used = fields.Integer( + metadata={ + 'description': 'Memory used in bytes.', + 'example': 6442450944, + } + ) + + free = fields.Integer( + metadata={ + 'description': 'Memory free in bytes.', + 'example': 2147483648, + } + ) + + percent = percent_field( + metadata={ + 'description': 'Memory usage percentage between 0 and 1.', + 'example': 0.75, + } + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + + # Normalize the percentage between 0 and 1 + if data.get('percent') is not None: + data['percent'] /= 100 + return data diff --git a/platypush/schemas/system/_network/_base.py b/platypush/schemas/system/_network/_base.py deleted file mode 100644 index 97f3affc2c..0000000000 --- a/platypush/schemas/system/_network/_base.py +++ /dev/null @@ -1,43 +0,0 @@ -from enum import Enum -from socket import AddressFamily - -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class NetworkInterfaceBaseSchema(SystemBaseSchema): - """ - Base schema for network interface stats. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - - # Custom attribute mappings - for in_attr, out_attr in { - 'errin': 'errors_in', - 'errout': 'errors_out', - 'dropin': 'drop_in', - 'dropout': 'drop_out', - 'isup': 'is_up', - }.items(): - if in_attr in data: - data[out_attr] = data.pop(in_attr) - - # Serialize enum values - for i, addr in enumerate(data.get('addresses', [])): - if hasattr(addr, '_asdict'): - addr = addr._asdict() - if isinstance(addr.get('family'), AddressFamily): - addr['family'] = addr['family'].name - data['addresses'][i] = addr - - if isinstance(data.get('duplex'), Enum): - data['duplex'] = data['duplex'].name.split('_')[-1] - - # Split the flags string - data['flags'] = data.get('flags', '').split(',') - - return data diff --git a/platypush/schemas/system/_network/_model.py b/platypush/schemas/system/_network/_model.py index 68b1f03e70..080aa9e4b4 100644 --- a/platypush/schemas/system/_network/_model.py +++ b/platypush/schemas/system/_network/_model.py @@ -2,154 +2,6 @@ from dataclasses import dataclass, field from socket import AddressFamily from typing import List, Optional -from marshmallow.validate import OneOf - - -@dataclass -class NetworkInterface: - """ - Network interface statistics data class. - """ - - interface: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Network interface identifier', - 'example': 'eth0', - } - } - ) - - bytes_sent: int = field( - metadata={ - 'metadata': { - 'description': 'Number of bytes sent', - } - } - ) - - bytes_recv: int = field( - metadata={ - 'metadata': { - 'description': 'Number of bytes received', - } - } - ) - - packets_sent: int = field( - metadata={ - 'metadata': { - 'description': 'Number of packets sent', - } - } - ) - - packets_recv: int = field( - metadata={ - 'metadata': { - 'description': 'Number of packets received', - } - } - ) - - errors_in: int = field( - metadata={ - 'metadata': { - 'description': 'Number of errors on incoming traffic', - }, - } - ) - - errors_out: int = field( - metadata={ - 'metadata': { - 'description': 'Number of errors on outgoing traffic', - }, - } - ) - - drop_in: int = field( - metadata={ - 'metadata': { - 'description': 'Number of packets dropped on incoming traffic', - }, - } - ) - - drop_out: int = field( - metadata={ - 'metadata': { - 'description': 'Number of packets dropped on outgoing traffic', - }, - } - ) - - is_up: bool = field( - metadata={ - 'metadata': { - 'description': 'Whether the interface is active', - 'example': True, - }, - } - ) - - speed: int = field( - metadata={ - 'metadata': { - 'description': 'Interface reported speed in Mbps', - 'example': 10000, - }, - } - ) - - mtu: int = field( - metadata={ - 'metadata': { - 'description': 'Interface maximum transmission unit expressed ' - 'in bytes', - 'example': 65535, - }, - } - ) - - duplex: str = field( - metadata={ - 'validate': OneOf(['FULL', 'HALF', 'UNKNOWN']), - 'metadata': { - 'description': 'Interface duplex configuration. Can be FULL, ' - 'HALF or UNKNOWN', - 'example': 'FULL', - }, - } - ) - - flags: List[str] = field( - default_factory=list, - metadata={ - 'metadata': { - 'description': 'List of flags associated to the interface', - 'example': ['up', 'broadcast', 'running'], - } - }, - ) - - addresses: List['NetworkInterfaceAddress'] = field( - default_factory=list, - metadata={ - 'metadata': { - 'description': 'List of addresses associated to the interface', - 'example': [ - { - 'family': AddressFamily.AF_INET.name, - 'address': '192.168.1.2', - 'netmask': '255.255.255.0', - 'broadcast': '192.168.1.255', - } - ], - }, - }, - ) - @dataclass class NetworkInterfaceAddress: @@ -157,38 +9,30 @@ class NetworkInterfaceAddress: Network interface address data class. """ - family: AddressFamily = field( - metadata={ - 'metadata': { - 'description': 'Address family', - 'example': AddressFamily.AF_INET.name, - } - } - ) + family: Optional[AddressFamily] = None + address: Optional[str] = None + netmask: Optional[str] = None + broadcast: Optional[str] = None - address: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'IPv4 or IPv6 address of the interface', - 'example': '192.168.1.2', - } - } - ) - netmask: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Netmask for the interface address', - 'example': '255.255.255.0', - } - } - ) +@dataclass +class NetworkInterface: + """ + Network interface statistics data class. + """ - broadcast: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Broadcast address for the interface', - 'example': '192.168.1.255', - } - } - ) + interface: Optional[str] = None + bytes_sent: int = 0 + bytes_recv: int = 0 + packets_sent: int = 0 + packets_recv: int = 0 + errors_in: int = 0 + errors_out: int = 0 + drop_in: int = 0 + drop_out: int = 0 + is_up: bool = False + speed: int = 0 + mtu: int = 0 + duplex: Optional[str] = None + flags: List[str] = field(default_factory=list) + addresses: List[NetworkInterfaceAddress] = field(default_factory=list) diff --git a/platypush/schemas/system/_network/_schemas.py b/platypush/schemas/system/_network/_schemas.py index e960b111b3..341f24243a 100644 --- a/platypush/schemas/system/_network/_schemas.py +++ b/platypush/schemas/system/_network/_schemas.py @@ -1,9 +1,207 @@ -from marshmallow_dataclass import class_schema +from enum import Enum +from socket import AddressFamily -from ._base import NetworkInterfaceBaseSchema -from ._model import NetworkInterface +from marshmallow import fields, pre_load +from marshmallow.validate import OneOf + +from .._base import SystemBaseSchema -NetworkInterfaceSchema = class_schema( - NetworkInterface, base_schema=NetworkInterfaceBaseSchema -) +class NetworkInterfaceAddressSchema(SystemBaseSchema): + """ + Schema for network interface address. + """ + + family = fields.String( + allow_none=True, + metadata={ + 'description': 'The address family.', + 'example': 'AF_INET', + }, + ) + + address = fields.String( + allow_none=True, + metadata={ + 'description': 'The IP address associated with the interface.', + 'example': '192.168.1.4', + }, + ) + + netmask = fields.String( + allow_none=True, + metadata={ + 'description': 'The netmask associated with the interface.', + 'example': '255.255.255.0', + }, + ) + + broadcast = fields.String( + allow_none=True, + metadata={ + 'description': 'The broadcast address associated with the interface.', + 'example': '192.168.1.255', + }, + ) + + +class NetworkInterfaceSchema(SystemBaseSchema): + """ + Schema for network interface stats. + """ + + interface = fields.String( + allow_none=True, + metadata={ + 'description': 'The name of the network interface.', + 'example': 'eth0', + }, + ) + + bytes_sent = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of bytes sent.', + 'example': 123456, + }, + ) + + bytes_recv = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of bytes received.', + 'example': 654321, + }, + ) + + packets_sent = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of packets sent.', + 'example': 123, + }, + ) + + packets_recv = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of packets received.', + 'example': 321, + }, + ) + + errors_in = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of errors on the input side.', + 'example': 10, + }, + ) + + errors_out = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of errors on the output side.', + 'example': 5, + }, + ) + + drop_in = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of dropped packets on the input side.', + 'example': 1, + }, + ) + + drop_out = fields.Integer( + missing=0, + metadata={ + 'description': 'The number of dropped packets on the output side.', + 'example': 2, + }, + ) + + is_up = fields.Boolean( + metadata={ + 'description': 'Whether the interface is up.', + 'example': True, + }, + ) + + speed = fields.Integer( + metadata={ + 'description': 'The advertised speed of the interface in Mbps.', + 'example': 1000, + }, + ) + + mtu = fields.Integer( + metadata={ + 'description': 'The maximum transmission unit of the interface in bytes.', + 'example': 1500, + }, + ) + + duplex = fields.String( + validate=OneOf(['FULL', 'HALF', 'UNKNOWN']), + metadata={ + 'description': 'Interface duplex configuration. Can be FULL, ' + 'HALF or UNKNOWN', + 'example': 'FULL', + }, + ) + + flags = fields.List( + fields.String(), + metadata={ + 'description': 'A list of flags associated with the interface.', + 'example': ['up', 'broadcast', 'running', 'multicast'], + }, + ) + + addresses = fields.List( + fields.Nested(NetworkInterfaceAddressSchema), + metadata={ + 'description': 'A list of addresses associated with the interface.', + 'example': [ + { + 'family': 'AF_INET', + 'address': '192.168.1.4', + 'netmask': '255.255.255.0', + 'broadcast': '192.168.1.255', + }, + ], + }, + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + + # Custom attribute mappings + for in_attr, out_attr in { + 'errin': 'errors_in', + 'errout': 'errors_out', + 'dropin': 'drop_in', + 'dropout': 'drop_out', + 'isup': 'is_up', + }.items(): + if in_attr in data: + data[out_attr] = data.pop(in_attr) + + # Serialize enum values + for i, addr in enumerate(data.get('addresses', [])): + if hasattr(addr, '_asdict'): + addr = addr._asdict() + if isinstance(addr.get('family'), AddressFamily): + addr['family'] = addr['family'].name + data['addresses'][i] = addr + + if isinstance(data.get('duplex'), Enum): + data['duplex'] = data['duplex'].name.split('_')[-1] + + # Split the flags string + data['flags'] = data.get('flags', '').split(',') + + return data diff --git a/platypush/schemas/system/_process/_base.py b/platypush/schemas/system/_process/_base.py deleted file mode 100644 index 72b505e065..0000000000 --- a/platypush/schemas/system/_process/_base.py +++ /dev/null @@ -1,28 +0,0 @@ -from datetime import datetime - -from dateutil.tz import gettz -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class ProcessBaseSchema(SystemBaseSchema): - """ - Base schema for system processes. - """ - - @pre_load - def pre_load(self, data, **_) -> dict: - if hasattr(data, 'as_dict'): - data = data.as_dict() - - started_ts = data.pop('create_time', None) - if started_ts is not None: - data['started'] = datetime.fromtimestamp(started_ts).replace(tzinfo=gettz()) - - data['command_line'] = data.pop('cmdline', None) - data['cpu_percent'] = data.pop('cpu_percent') / 100 - data['current_directory'] = data.pop('cwd', None) - data['memory_percent'] = data.pop('memory_percent') / 100 - data['parent_pid'] = data.pop('ppid', None) - return data diff --git a/platypush/schemas/system/_process/_model.py b/platypush/schemas/system/_process/_model.py index 30513b5463..5a32a3ea97 100644 --- a/platypush/schemas/system/_process/_model.py +++ b/platypush/schemas/system/_process/_model.py @@ -2,8 +2,6 @@ from dataclasses import dataclass, field from datetime import datetime from typing import List, Optional -from platypush.schemas.dataclasses import percent_field - @dataclass class Process: @@ -11,100 +9,14 @@ class Process: System process data class. """ - pid: int = field( - metadata={ - 'metadata': { - 'description': 'Process PID', - 'example': 12345, - } - } - ) - - name: str = field( - metadata={ - 'metadata': { - 'description': 'Process name', - 'example': 'python', - } - } - ) - - parent_pid: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'PID of the parent process', - 'example': 1000, - } - } - ) - - username: str = field( - metadata={ - 'metadata': { - 'description': 'Username that owns the process', - 'example': 'root', - } - } - ) - - command_line: List[str] = field( - metadata={ - 'metadata': { - 'description': 'Command line of the process', - 'example': ['/usr/bin/python', '-m', 'platypush'], - } - } - ) - - status: str = field( - metadata={ - 'metadata': { - 'description': 'Process status', - 'example': 'running', - } - } - ) - - terminal: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Terminal the process is running on', - 'example': 'pts/1', - } - } - ) - - current_directory: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Current directory of the process', - 'example': '/root', - } - } - ) - - started: Optional[datetime] = field( - metadata={ - 'metadata': { - 'description': 'When the process started', - } - } - ) - - cpu_percent: float = percent_field( - metadata={ - 'metadata': { - 'description': 'Percentage of CPU used by the process, between 0 and 1', - 'example': 0.1, - } - } - ) - - memory_percent: float = percent_field( - metadata={ - 'metadata': { - 'description': 'Percentage of memory used by the process, between 0 and 1', - 'example': 0.05, - } - } - ) + pid: int + name: str + parent_pid: Optional[int] = None + username: Optional[str] = None + command_line: List[str] = field(default_factory=list) + status: Optional[str] = None + terminal: Optional[str] = None + current_directory: Optional[str] = None + started: Optional[datetime] = None + cpu_percent: float = 0 + memory_percent: float = 0 diff --git a/platypush/schemas/system/_process/_schemas.py b/platypush/schemas/system/_process/_schemas.py index c4cca1ab49..4d2f7c80f7 100644 --- a/platypush/schemas/system/_process/_schemas.py +++ b/platypush/schemas/system/_process/_schemas.py @@ -1,7 +1,124 @@ -from marshmallow_dataclass import class_schema +from datetime import datetime -from ._base import ProcessBaseSchema -from ._model import Process +from dateutil.tz import gettz +from marshmallow import fields, pre_load + +from platypush.schemas import DateTime +from platypush.schemas.dataclasses import percent_field +from .._base import SystemBaseSchema -ProcessSchema = class_schema(Process, base_schema=ProcessBaseSchema) +class ProcessSchema(SystemBaseSchema): + """ + Schema for system processes. + """ + + pid = fields.Int( + missing=-1, + metadata={ + 'description': 'The process ID.', + 'example': 1234, + }, + ) + + name = fields.String( + allow_none=True, + metadata={ + 'description': 'The name of the process.', + 'example': 'python', + }, + ) + + parent_pid = fields.Int( + allow_none=True, + metadata={ + 'description': 'The parent process ID.', + 'example': 1000, + }, + ) + + username = fields.String( + allow_none=True, + metadata={ + 'description': 'The username of the process owner.', + 'example': 'root', + }, + ) + + command_line = fields.List( + fields.String(), + allow_none=True, + metadata={ + 'description': 'The command line arguments of the process.', + 'example': ['python', 'script.py'], + }, + ) + + status = fields.String( + allow_none=True, + metadata={ + 'description': 'The status of the process.', + 'example': 'running', + }, + ) + + terminal = fields.String( + allow_none=True, + metadata={ + 'description': 'The terminal of the process.', + 'example': 'tty1', + }, + ) + + current_directory = fields.String( + allow_none=True, + metadata={ + 'description': 'The current working directory of the process.', + 'example': '/home/user', + }, + ) + + started = DateTime( + allow_none=True, + metadata={ + 'description': 'The timestamp when the process was started.', + 'example': '2021-01-01T00:00:00+00:00', + }, + ) + + cpu_percent = percent_field( + missing=0, + metadata={ + 'description': 'The CPU usage percentage of the process, in the range [0, 1].', + 'example': 0.5, + }, + ) + + memory_percent = percent_field( + missing=0, + metadata={ + 'description': 'The memory usage percentage of the process, in the range [0, 1].', + 'example': 0.5, + }, + ) + + @pre_load + def pre_load(self, data, **_) -> dict: + import psutil + + if hasattr(data, 'as_dict'): + try: + data = data.as_dict() + except psutil.NoSuchProcess: + return {} + + started_ts = data.pop('create_time', None) + if started_ts is not None: + data['started'] = datetime.fromtimestamp(started_ts).replace(tzinfo=gettz()) + + data['command_line'] = data.pop('cmdline', None) + data['cpu_percent'] = data.pop('cpu_percent') / 100 + data['current_directory'] = data.pop('cwd', None) + data['memory_percent'] = data.pop('memory_percent') / 100 + data['parent_pid'] = data.pop('ppid', None) + return data diff --git a/platypush/schemas/system/_schemas.py b/platypush/schemas/system/_schemas.py index 0bd2992fc4..19e4defef1 100644 --- a/platypush/schemas/system/_schemas.py +++ b/platypush/schemas/system/_schemas.py @@ -1,8 +1,25 @@ -from marshmallow_dataclass import class_schema +from marshmallow import fields -from platypush.schemas.dataclasses import DataClassSchema - -from ._model import SystemInfo +from ._base import SystemBaseSchema +from ._battery import BatterySchema +from ._cpu import CpuSchema +from ._disk import DiskSchema +from ._fan import FanSchema +from ._memory import MemoryStatsSchema, SwapStatsSchema +from ._network import NetworkInterfaceSchema +from ._temperature import TemperatureSchema -SystemInfoSchema = class_schema(SystemInfo, base_schema=DataClassSchema) +class SystemInfoSchema(SystemBaseSchema): + """ + Schema for system info. + """ + + cpu = fields.Nested(CpuSchema) + memory = fields.Nested(MemoryStatsSchema) + swap = fields.Nested(SwapStatsSchema) + disks = fields.List(fields.Nested(DiskSchema)) + network = fields.List(fields.Nested(NetworkInterfaceSchema)) + temperature = fields.List(fields.Nested(TemperatureSchema)) + fans = fields.List(fields.Nested(FanSchema)) + battery = fields.Nested(BatterySchema) diff --git a/platypush/schemas/system/_temperature/_base.py b/platypush/schemas/system/_temperature/_base.py deleted file mode 100644 index a5ada9dc6c..0000000000 --- a/platypush/schemas/system/_temperature/_base.py +++ /dev/null @@ -1,15 +0,0 @@ -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class TemperatureBaseSchema(SystemBaseSchema): - """ - Base schema for system temperature sensors. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - data['value'] = data.pop('current', data.pop('value', None)) - return data diff --git a/platypush/schemas/system/_temperature/_model.py b/platypush/schemas/system/_temperature/_model.py index d53b405c72..470e4a3f25 100644 --- a/platypush/schemas/system/_temperature/_model.py +++ b/platypush/schemas/system/_temperature/_model.py @@ -1,54 +1,15 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional @dataclass class Temperature: """ - System temperature sensor wrapper. + System temperature sensor data class. """ - id: str = field( - metadata={ - 'metadata': { - 'description': 'Unique ID for the sensor', - 'example': 'acpi_1', - } - } - ) - - label: str = field( - metadata={ - 'metadata': { - 'description': 'Name of the sensor', - 'example': 'CPU', - } - } - ) - - value: float = field( - metadata={ - 'metadata': { - 'description': 'Current temperature value, in Celsius', - 'example': 55, - } - } - ) - - high: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'High threshold for the temperature sensor, in Celsius', - 'example': 75, - } - } - ) - - critical: Optional[float] = field( - metadata={ - 'metadata': { - 'description': 'Critical threshold for the temperature sensor, in Celsius', - 'example': 95, - } - } - ) + id: str + label: Optional[str] = None + value: Optional[float] = None + high: Optional[float] = None + critical: Optional[float] = None diff --git a/platypush/schemas/system/_temperature/_schemas.py b/platypush/schemas/system/_temperature/_schemas.py index 09bc1463fd..b6aa4f2899 100644 --- a/platypush/schemas/system/_temperature/_schemas.py +++ b/platypush/schemas/system/_temperature/_schemas.py @@ -1,7 +1,54 @@ -from marshmallow_dataclass import class_schema +from marshmallow import fields, pre_load -from ._base import TemperatureBaseSchema -from ._model import Temperature +from .._base import SystemBaseSchema -TemperatureSchema = class_schema(Temperature, base_schema=TemperatureBaseSchema) +class TemperatureSchema(SystemBaseSchema): + """ + Schema for system temperature sensors. + """ + + id = fields.String( + metadata={ + 'description': 'The unique identifier of the temperature sensor.', + 'example': 'acpi_1', + } + ) + + label = fields.String( + allow_none=True, + metadata={ + 'description': 'The label of the temperature sensor.', + 'example': 'CPU Temperature', + }, + ) + + value = fields.Float( + allow_none=True, + metadata={ + 'description': 'The current temperature value of the sensor, in degrees Celsius.', + 'example': 56.0, + }, + ) + + high = fields.Float( + allow_none=True, + metadata={ + 'description': 'The high temperature threshold of the sensor, in degrees Celsius.', + 'example': 90.0, + }, + ) + + critical = fields.Float( + allow_none=True, + metadata={ + 'description': 'The critical temperature threshold of the sensor, in degrees Celsius.', + 'example': 100.0, + }, + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + data['value'] = data.pop('current', data.pop('value', None)) + return data diff --git a/platypush/schemas/system/_user/_base.py b/platypush/schemas/system/_user/_base.py deleted file mode 100644 index be32bb4f63..0000000000 --- a/platypush/schemas/system/_user/_base.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import datetime - -from dateutil.tz import gettz -from marshmallow import pre_load - -from .._base import SystemBaseSchema - - -class UserBaseSchema(SystemBaseSchema): - """ - Base schema for system users. - """ - - @pre_load - def pre_load(self, data: dict, **_) -> dict: - data = super().pre_load(data) - started_ts = data.pop('started', None) - if started_ts is not None: - data['started'] = datetime.fromtimestamp(started_ts).replace(tzinfo=gettz()) - - data['username'] = data.pop('name', data.pop('username', None)) - return data diff --git a/platypush/schemas/system/_user/_model.py b/platypush/schemas/system/_user/_model.py index 2fc9e974e6..b023bf26cb 100644 --- a/platypush/schemas/system/_user/_model.py +++ b/platypush/schemas/system/_user/_model.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime from typing import Optional @@ -9,37 +9,7 @@ class User: System user wrapper. """ - username: str = field( - metadata={ - 'metadata': { - 'description': 'Username', - 'example': 'root', - } - } - ) - - terminal: Optional[str] = field( - metadata={ - 'metadata': { - 'description': 'Identifier of the terminal the user is connected to', - 'example': 'pts/1', - } - } - ) - - started: Optional[datetime] = field( - metadata={ - 'metadata': { - 'description': 'When the user session started', - } - } - ) - - pid: Optional[int] = field( - metadata={ - 'metadata': { - 'description': 'PID of the process that holds the session', - 'example': 12345, - } - } - ) + username: str + terminal: Optional[str] = None + started: Optional[datetime] = None + pid: Optional[int] = None diff --git a/platypush/schemas/system/_user/_schemas.py b/platypush/schemas/system/_user/_schemas.py index ae3395db75..2ba9c9f2ce 100644 --- a/platypush/schemas/system/_user/_schemas.py +++ b/platypush/schemas/system/_user/_schemas.py @@ -1,7 +1,47 @@ -from marshmallow_dataclass import class_schema +from datetime import datetime -from ._base import UserBaseSchema -from ._model import User +from dateutil.tz import gettz +from marshmallow import fields, pre_load + +from platypush.schemas import DateTime + +from .._base import SystemBaseSchema -UserSchema = class_schema(User, base_schema=UserBaseSchema) +class UserSchema(SystemBaseSchema): + """ + Schema for system users. + """ + + username = fields.String( + required=True, + metadata={'description': 'The username of the user.', 'example': 'johndoe'}, + ) + + terminal = fields.String( + metadata={ + 'description': 'The terminal the user is currently using.', + 'example': 'tty1', + } + ) + + started = DateTime( + metadata={ + 'description': 'The timestamp when the user session started.', + 'example': '2021-01-01T00:00:00+00:00', + } + ) + + pid = fields.Integer( + metadata={'description': 'The PID of the user session.', 'example': 1234} + ) + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + data = super().pre_load(data) + started_ts = data.pop('started', None) + if started_ts is not None: + data['started'] = datetime.fromtimestamp(started_ts).replace(tzinfo=gettz()) + + data['username'] = data.pop('name', data.pop('username', None)) + return data