ekfsm 1.1.0a15.post1__py3-none-any.whl → 1.3.0a26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ekfsm might be problematic. Click here for more details.

@@ -2,7 +2,7 @@ id: HDRC300S.*
2
2
  name: "Hitron HDRC-300S"
3
3
  slot_type: CPCI_S0_PSU
4
4
  children:
5
- - device_type: PmBus
5
+ - device_type: PMBus
6
6
  name: "PMBUS"
7
7
  addr: 0x18
8
8
  slot_coding_mask: 0x07
ekfsm/cli.py CHANGED
@@ -1,15 +1,15 @@
1
1
  #! /usr/bin/env python3
2
2
  # import os
3
3
  # import sys
4
- import click
4
+ # pyright: reportOptionalMemberAccess = false
5
5
  import logging
6
6
  from pathlib import Path
7
7
 
8
- from ekfsm.system import System
9
- from ekfsm.log import ekfsm_logger
10
- from ekfsm.simctrl import enable_simulation
11
- from ekfsm.simctrl import register_gpio_simulations
8
+ import click
12
9
 
10
+ from ekfsm.log import ekfsm_logger
11
+ from ekfsm.simctrl import enable_simulation, register_gpio_simulations
12
+ from ekfsm.system import System
13
13
 
14
14
  logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
15
15
  logger = ekfsm_logger(__name__)
@@ -87,9 +87,7 @@ def cli(verbose, debug, sysfs, config):
87
87
  type=int,
88
88
  help="Write chassis serial number",
89
89
  )
90
- @click.option(
91
- "--unit", "-u", is_flag=False, prompt=True, type=int, help="Write chassis unit"
92
- )
90
+ @click.option("--unit", "-u", is_flag=False, prompt=True, type=int, help="Write chassis unit")
93
91
  @click.option(
94
92
  "--vendor",
95
93
  "-n",
ekfsm/config.py CHANGED
@@ -1,8 +1,10 @@
1
+ from typing import Any, List, Tuple
2
+
3
+ import munch
1
4
  import yamale # type: ignore
2
5
  import yaml
3
- import munch
4
6
 
5
- from typing import Any, List, Tuple
7
+ from ekfsm.exceptions import ConfigError
6
8
 
7
9
  schema_str = """
8
10
  system_config:
@@ -22,13 +24,19 @@ slot:
22
24
  def _validate_config(config_file: str) -> None:
23
25
  schema = yamale.make_schema(content=schema_str)
24
26
  data = yamale.make_data(config_file)
25
- yamale.validate(schema, data)
27
+ try:
28
+ yamale.validate(schema, data)
29
+ except yamale.YamaleError:
30
+ raise ConfigError("Error in configuration file")
26
31
 
27
32
 
28
33
  def _parse_config(config_file: str) -> Any | munch.Munch | List | Tuple:
29
- with open(config_file) as file:
30
- config = yaml.safe_load(file)
31
- munchified_config = munch.munchify(config)
34
+ try:
35
+ with open(config_file) as file:
36
+ config = yaml.safe_load(file)
37
+ munchified_config = munch.munchify(config)
38
+ except OSError:
39
+ raise ConfigError("Failed to open configuration file: {config_file}")
32
40
  return munchified_config
33
41
 
34
42
 
ekfsm/core/__init__.py CHANGED
@@ -1,14 +1,14 @@
1
- from .slots import Slot, SlotType, Slots # noqa: F401
2
- from .sysfs import SysFSDevice, SysFSAttribute # noqa: F401
3
- from .components import HwModule # noqa: F401
1
+ from .components import HWModule # noqa: F401
4
2
  from .probe import ProbeableDevice # noqa: F401
3
+ from .slots import Slot, Slots, SlotType # noqa: F401
4
+ from .sysfs import SysFSAttribute, SysfsDevice # noqa: F401
5
5
 
6
6
  __all__ = [
7
7
  "Slot",
8
8
  "SlotType",
9
9
  "Slots",
10
- "SysFSDevice",
10
+ "SysfsDevice",
11
11
  "SysFSAttribute",
12
- "HwModule",
12
+ "HWModule",
13
13
  "ProbeableDevice",
14
14
  ]
ekfsm/core/components.py CHANGED
@@ -36,9 +36,9 @@ class SysTree(NodeMixin):
36
36
  print(self)
37
37
 
38
38
 
39
- class HwModule(SysTree):
39
+ class HWModule(SysTree):
40
40
  """
41
- A HwModule represents an instantiation of a specifc hw board type,
41
+ A HWModule represents an instantiation of a specifc hw board type,
42
42
  for example an instance of an EKF SC9 board.
43
43
  """
44
44
 
@@ -119,13 +119,13 @@ class HwModule(SysTree):
119
119
  @property
120
120
  def instance_name(self) -> str:
121
121
  if self.name is None:
122
- raise RuntimeError("instance name not set")
122
+ raise RuntimeError("Instance name not set")
123
123
  return self.name
124
124
 
125
125
  @property
126
126
  def slot(self) -> Slot:
127
127
  if self._slot is None:
128
- raise RuntimeError("slot not set")
128
+ raise RuntimeError("Slot not set")
129
129
  return self._slot
130
130
 
131
131
  def __repr__(self) -> str:
ekfsm/core/slots.py CHANGED
@@ -1,11 +1,9 @@
1
1
  from enum import Enum
2
- from ekfsm.core.components import HwModule
3
-
2
+ from typing import Any
4
3
 
5
4
  from munch import Munch
6
5
 
7
-
8
- from typing import Any
6
+ from ekfsm.core.components import HWModule
9
7
 
10
8
 
11
9
  class SlotType(Enum):
@@ -66,8 +64,8 @@ class Slot:
66
64
  desired_hwmodule_type: str,
67
65
  desired_hwmodule_name: str,
68
66
  number: int,
69
- hwmodule: HwModule | None = None,
70
- master: HwModule | None = None,
67
+ hwmodule: HWModule | None = None,
68
+ master: HWModule | None = None,
71
69
  attributes: Munch | None = None,
72
70
  ) -> None:
73
71
  self._name = name
@@ -104,9 +102,7 @@ class Slot:
104
102
  }
105
103
 
106
104
  def __repr__(self) -> str:
107
- return (
108
- f"{self.__class__.__name__}(name={self._name}, slot_type={self.slot_type})"
109
- )
105
+ return f"{self.__class__.__name__}(name={self._name}, slot_type={self.slot_type})"
110
106
 
111
107
  @property
112
108
  def name(self) -> str:
@@ -127,10 +123,7 @@ class Slot:
127
123
  """
128
124
  Return True if the slot is populated with the desired hardware module type, False otherwise.
129
125
  """
130
- return (
131
- self.hwmodule is not None
132
- and self.hwmodule.board_type.lower() == self._desired_hwmodule_type.lower()
133
- )
126
+ return self.hwmodule is not None and self.hwmodule.board_type.lower() == self._desired_hwmodule_type.lower()
134
127
 
135
128
 
136
129
  class Slots(Munch):
ekfsm/core/sysfs.py CHANGED
@@ -1,4 +1,10 @@
1
+ from collections.abc import MutableMapping
1
2
  from pathlib import Path
3
+ from typing import Callable
4
+
5
+ from more_itertools import first_true
6
+
7
+ from ekfsm.exceptions import ConversionError, DriverError, SysFSError
2
8
 
3
9
  SYSFS_ROOT = Path("/sys")
4
10
 
@@ -20,31 +26,41 @@ class SysFSAttribute:
20
26
  """
21
27
  A SysFSAttribute is a singular sysfs attribute located somewhere in */sys*.
22
28
 
23
- Attributes
29
+ Parameters
24
30
  ----------
25
- path
31
+ path: Path
26
32
  Path to the underlying file for the SysFSAttribute instance.
27
33
  """
28
34
 
29
35
  def __init__(self, path: Path):
30
- self.path = path
31
36
  if not path.exists() or not path.is_file():
32
37
  raise FileNotFoundError("Invalid sysfs attribute path")
33
38
 
39
+ self.path = path
34
40
  self.name: str = path.name
35
41
 
36
42
  def read_utf8(self) -> str:
37
- return self.path.read_text()
43
+ try:
44
+ return self.path.read_text()
45
+ except OSError as e:
46
+ raise SysFSError("Error accessing SysFS attribute") from e
38
47
 
39
48
  def read_bytes(self) -> bytes:
40
- return self.path.read_bytes()
49
+ try:
50
+ return self.path.read_bytes()
51
+ except OSError as e:
52
+ raise SysFSError("Error accessing SysFS attribute") from e
41
53
 
54
+ # FIXME: This cannot work due to sysfs attributes not supporting seek().
42
55
  def write(self, data: str | bytes | None, offset: int = 0) -> None:
43
56
  if self.is_sysfs_attr() and data is not None:
44
57
  mode = "r+" if isinstance(data, str) else "rb+"
45
- with open(self.path, mode) as f:
46
- f.seek(offset)
47
- f.write(data)
58
+ try:
59
+ with open(self.path, mode) as f:
60
+ f.seek(offset)
61
+ f.write(data)
62
+ except OSError as e:
63
+ raise SysFSError("Error accessing SysFS attribute") from e
48
64
 
49
65
  def is_sysfs_attr(self) -> bool:
50
66
  return file_is_sysfs_attr(self.path)
@@ -60,10 +76,45 @@ def list_sysfs_attributes(path: Path) -> list[SysFSAttribute]:
60
76
  return [SysFSAttribute(item) for item in path.iterdir() if file_is_sysfs_attr(item)]
61
77
 
62
78
 
63
- class SysFSDevice:
64
- def __init__(self, base_dir: Path):
79
+ class SysfsDevice(MutableMapping):
80
+ def __init__(
81
+ self, base_dir: Path, driver_required=True, find_driver: Callable | None = None
82
+ ):
65
83
  self.path: Path = base_dir
66
- self.attributes: list[SysFSAttribute] = list_sysfs_attributes(self.path)
84
+ self.driver_required = driver_required
85
+
86
+ try:
87
+ self.driver = self.get_driver()
88
+ except Exception:
89
+ self.driver = None
90
+
91
+ if driver_required:
92
+ raise DriverError(f"No driver found for device at {base_dir}")
93
+
94
+ try:
95
+ self.attributes: list[SysFSAttribute] = list_sysfs_attributes(self.path)
96
+ except FileNotFoundError as e:
97
+ raise SysFSError(f"SysFS entry for {base_dir} does not exist") from e
98
+
99
+ def __getitem__(self, key):
100
+ if (
101
+ attr := first_true(self.attributes, pred=lambda a: a.name == key)
102
+ ) is not None:
103
+ return attr
104
+
105
+ raise KeyError(f"'{key}' is not a valid sysfs attribute in {self.path}")
106
+
107
+ def __setitem__(self, key, value):
108
+ self[key].write(value)
109
+
110
+ def __delitem__(self, key):
111
+ del self.attributes[key]
112
+
113
+ def __iter__(self):
114
+ return iter(self.attributes)
115
+
116
+ def __len__(self):
117
+ return len(self.attributes)
67
118
 
68
119
  def pre(self) -> None:
69
120
  pass
@@ -71,11 +122,130 @@ class SysFSDevice:
71
122
  def post(self) -> None:
72
123
  pass
73
124
 
74
- def write_attr(self, attr: str, data: str | bytes, offset: int = 0) -> None:
75
- next(x for x in self.attributes if x.name == attr).write(data, offset)
125
+ def write_attr(self, attr: str, data: str | bytes) -> None:
126
+ next(x for x in self.attributes if x.name == attr).write(data)
127
+
128
+ def write_attr_bytes(self, attr: str, data: str) -> None:
129
+ # TODO: This
130
+ pass
76
131
 
77
132
  def read_attr_utf8(self, attr: str) -> str:
78
133
  return next(x for x in self.attributes if x.name == attr).read_utf8()
79
134
 
135
+ def read_float(self, attr: str) -> float:
136
+ """
137
+ Read a sysfs attribute as a floating-point number
138
+
139
+ Parameters
140
+ ----------
141
+ attr: str
142
+ The sysfs attribute to read
143
+
144
+ Returns
145
+ -------
146
+ The sysfs attribute as a floating-point number
147
+
148
+ Raises
149
+ ------
150
+ SysFSError
151
+ If the sysfs attribute does not exist
152
+ ConversionError
153
+ If the sysfs attribute could not be converted to a floating-point number
154
+ """
155
+ try:
156
+ value = self.read_attr_utf8(attr)
157
+ return float(value)
158
+ except StopIteration as e:
159
+ raise SysFSError(f"'{attr}' sysfs attribute does not exist") from e
160
+ except SysFSError:
161
+ raise
162
+ except ValueError as e:
163
+ raise ConversionError(
164
+ "Failed to convert sysfs value to floating-point value"
165
+ ) from e
166
+
167
+ def read_int(self, attr) -> int:
168
+ """
169
+ Read a sysfs attribute as an integer
170
+
171
+ Parameters
172
+ ----------
173
+ attr: str
174
+ The sysfs attribute to read
175
+
176
+ Returns
177
+ -------
178
+ The sysfs attribute as an integer
179
+
180
+ Raises
181
+ ------
182
+ SysFSError
183
+ If the sysfs attribute does not exist
184
+ ConversionError
185
+ If the sysfs attribute could not be converted to an integer
186
+ """
187
+ try:
188
+ value = self.read_attr_utf8(attr).strip()
189
+ return int(value, 16)
190
+ except StopIteration as e:
191
+ raise SysFSError(f"'{attr}' sysfs attribute does not exist") from e
192
+ except SysFSError:
193
+ raise
194
+ except ValueError as e:
195
+ raise ConversionError("Failed to convert sysfs value to int") from e
196
+
197
+ def read_utf8(self, attr, strip=True) -> str:
198
+ """
199
+ Read a sysfs attribute as a UTF-8 encoded string
200
+
201
+ Parameters
202
+ ----------
203
+ attr: str
204
+ The sysfs attribute to read
205
+ strip: bool
206
+ Strip whitespace, defaults to true
207
+
208
+ Returns
209
+ -------
210
+ The sysfs attribute as a UTF-8 encoded string
211
+
212
+ Raises
213
+ ------
214
+ SysFSError
215
+ If the sysfs attribute does not exist
216
+ """
217
+ try:
218
+ value = self.read_attr_utf8(attr)
219
+ if strip:
220
+ value = value.strip()
221
+
222
+ return value
223
+ except StopIteration as e:
224
+ raise SysFSError(f"'{attr}' sysfs attribute does not exist") from e
225
+ except SysFSError:
226
+ raise
227
+
80
228
  def read_attr_bytes(self, attr: str) -> bytes:
81
229
  return next(x for x in self.attributes if x.name == attr).read_bytes()
230
+
231
+ def read_bytes(self, attr) -> bytes:
232
+ try:
233
+ value = self.read_attr_bytes(attr)
234
+ return value
235
+ except StopIteration as e:
236
+ raise SysFSError(f"'{attr}' sysfs attribute does not exist") from e
237
+ except SysFSError:
238
+ raise
239
+
240
+ def extend_attributes(self, attributes: list[SysFSAttribute]):
241
+ self.attributes.extend(attributes)
242
+
243
+ def get_driver(self) -> str | None:
244
+ path = self.path
245
+
246
+ if self.path.joinpath("device").exists():
247
+ path = self.path.joinpath("device")
248
+ elif not path.joinpath("driver").exists():
249
+ raise DriverError("Failed to retrieve driver info")
250
+
251
+ return path.joinpath("driver").readlink().name
ekfsm/core/utils.py CHANGED
@@ -1,92 +1,132 @@
1
1
  from __future__ import annotations
2
+
2
3
  import logging
3
- from typing import TYPE_CHECKING
4
- from anytree import AnyNode
4
+ from pprint import pformat
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from schema import Optional, Or, Schema, SchemaError, Use
8
+ from termcolor import colored
5
9
 
6
10
  from ekfsm.devices import CLASS_MAP
7
11
  from ekfsm.exceptions import ConfigError
8
12
 
9
13
  if TYPE_CHECKING:
10
- from .components import HwModule
11
14
  from ekfsm.devices.generic import Device
12
15
 
16
+ from .components import HWModule
13
17
 
14
- class BoardDictImporter:
15
18
 
16
- def __init__(self, nodecls=AnyNode):
17
- self.nodecls = nodecls
19
+ def import_board(logger: logging.Logger, data, parent=None, abort: bool = False):
20
+ from .components import HWModule
18
21
 
19
- def import_(self, logger: logging.Logger, data, parent=None, abort: bool = False):
20
- """Import tree from `data`."""
21
- return self.__import(logger, data, parent=parent, abort=abort)
22
+ device_type = data.get("device_type")
23
+ nodecls = CLASS_MAP.get(device_type)
24
+ if nodecls is None:
25
+ raise ConfigError(f"Unknown device type: {device_type}")
22
26
 
23
- def __import(self, logger: logging.Logger, data, parent=None, abort: bool = False):
24
- from .components import HwModule
27
+ children = data.pop("children", [])
28
+ if parent is not None and isinstance(parent, HWModule):
29
+ # ???
30
+ pass
25
31
 
26
- device_type = data.get("device_type")
27
- nodecls = CLASS_MAP.get(device_type)
28
- if nodecls is None:
29
- raise ConfigError(f"Unknown device type: {device_type}")
32
+ if provides := data.get("provides"):
33
+ for p in provides:
34
+ interfaces = data["provides"][p]
30
35
 
31
- children = data.pop("children", [])
32
- if parent is not None and isinstance(parent, HwModule):
33
- # ???
34
- pass
36
+ for interface in interfaces:
37
+ if isinstance(interface, str):
38
+ if attr := getattr(nodecls, interface, None):
39
+ if not callable(attr) and not abort:
40
+ raise ConfigError("No such method")
41
+ elif isinstance(interface, dict):
42
+ for key, value in interface.items():
43
+ if attr := getattr(nodecls, value, None):
44
+ if not callable(attr) and not abort:
45
+ raise ConfigError("No such method")
46
+ else:
47
+ raise ConfigError("Error in board configuration")
35
48
 
36
- node = nodecls(parent=parent, **data)
49
+ node = nodecls(parent=parent, abort=abort, **data)
37
50
 
38
- if children is not None:
39
- for child in children:
40
- try:
41
- logger.debug(f"Importing sub device {child}")
42
- self.__import(logger, child, parent=node, abort=abort)
43
- except Exception as e:
44
- if abort:
45
- logger.error(
46
- f"Failed to import sub device {child}: {e}. aborting"
47
- )
48
- raise e
49
- else:
50
- logger.error(
51
- f"Failed to import sub device {child}: {e}. continue anyway"
52
- )
53
- return node
51
+ if children is not None:
52
+ for child in children:
53
+ try:
54
+ logger.debug(f"Importing sub device {pformat(child)}")
55
+ import_board(logger, data=child, parent=node, abort=abort)
56
+ except Exception as e:
57
+ if abort:
58
+ logger.error(f"Failed to import sub device {pformat(child)}: {e}. Aborting.")
59
+ raise e
60
+ else:
61
+ logger.error(f"Failed to import sub device {pformat(child)}: {e}. Continuing anyway.")
62
+ return node
63
+
64
+
65
+ def provides_validator(x: Any) -> Any:
66
+ if isinstance(x, str):
67
+ return x
68
+ elif isinstance(x, dict) and len(x) == 1:
69
+ key, value = next(iter(x.items()))
70
+ if isinstance(key, str) and isinstance(value, str):
71
+ return x
72
+ raise SchemaError("Each provides item must be either a string or a dictionary with one string key/value pair")
73
+
74
+
75
+ device_schema = Schema({})
76
+
77
+ _device_structure = Schema(
78
+ {
79
+ "device_type": str,
80
+ "name": str,
81
+ Optional("addr"): int,
82
+ Optional("slot_coding_mask"): int,
83
+ Optional("channel_id"): int,
84
+ Optional("provides"): {
85
+ Optional(str): [Use(provides_validator)],
86
+ },
87
+ Optional("children"): Or(None, [device_schema]),
88
+ }
89
+ )
90
+
91
+ device_schema._schema = _device_structure
92
+
93
+ module_schema = Schema(
94
+ {
95
+ "id": Or(int, str),
96
+ "name": str,
97
+ "slot_type": str,
98
+ Optional("children"): [device_schema],
99
+ Optional("bus_masters"): {
100
+ Optional("i2c"): dict,
101
+ },
102
+ }
103
+ )
54
104
 
55
105
 
56
106
  def deserialize_hardware_tree(
57
- logger: logging.Logger, data: dict, parent: "HwModule"
107
+ logger: logging.Logger, data: dict, parent: "HWModule"
58
108
  ) -> tuple[str, str, str, list["Device"]]:
59
- importer = BoardDictImporter()
60
109
  abort = parent.abort
61
110
 
62
- # better use schema extension for this
63
- id = data.pop("id", None)
64
- if id is None:
65
- raise ConfigError("Board configuration must contain `id`")
66
- name = data.pop("name", None)
67
- if name is None:
68
- raise ConfigError("Board configuration must contain `name`")
69
- slot_type = data.pop("slot_type")
70
- if slot_type is None:
71
- raise ConfigError("Board configuration must contain `board_type`")
111
+ module_schema.validate(data)
112
+
113
+ id, name, slot_type = (data.pop(key) for key in ["id", "name", "slot_type"])
72
114
 
73
115
  children = data.pop("children", None)
116
+ if not children:
117
+ return id, name, slot_type, []
118
+
74
119
  devices = []
75
- if children:
76
- for child in children:
77
- try:
78
- logger.debug(f"Importing top level device {child}")
79
- node = importer.import_(logger, child, parent=parent, abort=abort)
80
- devices.append(node)
81
- except Exception as e:
82
- if abort:
83
- logger.error(
84
- f"Failed to import top level device {child}: {e}. aborting"
85
- )
86
- raise e
87
- else:
88
- logger.error(
89
- f"Failed to import top level device {child}: {e}. continue anyway"
90
- )
120
+ for child in children:
121
+ try:
122
+ logger.debug(colored(f"Importing top level device {pformat(child)}", "green"))
123
+
124
+ node = import_board(logger, child, parent=parent, abort=abort)
125
+ devices.append(node)
126
+ except Exception as e:
127
+ logger.error(colored(f"Failed to import top-level device {pformat(child)}: {e}", "red"))
128
+ logger.error(colored("Aborting." if abort else "Continuing anyway", "red"))
129
+ if abort:
130
+ raise
91
131
 
92
132
  return id, name, slot_type, devices
ekfsm/devices/__init__.py CHANGED
@@ -1,13 +1,14 @@
1
- from ekfsm.devices.generic import Device
2
1
  from ekfsm.devices.coretemp import CoreTemp
2
+ from ekfsm.devices.generic import Device
3
3
  from ekfsm.devices.smbios import SMBIOS
4
- from .eeprom import EEPROM, EKF_EEPROM, EKF_CCU_EEPROM
5
- from .pmbus import PmBus, PsuStatus
6
- from .gpio import GPIO, EKFIdentificationIOExpander, GPIOExpander
7
- from .ekf_sur_led import EKFSurLed
4
+
5
+ from .eeprom import EEPROM, EKF_CCU_EEPROM, EKF_EEPROM
8
6
  from .ekf_ccu_uc import EKFCcuUc
7
+ from .ekf_sur_led import EKFSurLed
8
+ from .gpio import GPIO, EKFIdentificationIOExpander, GPIOExpander
9
9
  from .iio_thermal_humidity import IIOThermalHumidity
10
10
  from .mux import I2CMux, MuxChannel
11
+ from .pmbus import PMBus, PSUStatus
11
12
 
12
13
  CLASS_MAP = {
13
14
  "GenericDevice": Device,
@@ -20,8 +21,8 @@ CLASS_MAP = {
20
21
  "EKF_EEPROM": EKF_EEPROM,
21
22
  "EKF_CCU_EEPROM": EKF_CCU_EEPROM,
22
23
  "EKFCcuUc": EKFCcuUc,
23
- "PmBus": PmBus,
24
- "PsuStatus": PsuStatus,
24
+ "PMBus": PMBus,
25
+ "PSUStatus": PSUStatus,
25
26
  "SMBIOS": SMBIOS,
26
27
  "HWMON": CoreTemp,
27
28
  "EKFSurLed": EKFSurLed,