Module sipy.config

All quantities and units in sipy are generated from a config file.

The main config file is included in the package, but the SIPY_CFG_FILE environment variable can be used to specify additional quantities and units.

There key sections of the config file are outlined below.

Sipy

The version on the config file must be specified to ensure backwards compatability.

Constants

Constants are defined with a key value pair for each constant. The key is the name of the constant, the value an array that contains the value followed by it's units.

newton = ["6.67430e-11", "m^3.kg.s"]

Prefixes

Prefixes are defined with a key value pair, the key is the name of the prefix and the value is the multiplication factor applied by the prefix

kilo = 1e3

Quantity

This is an example entry for the Length quantity

  [Quantity.Length]
  miles = ["Multiplier", "1609.34"]
    [Quantity.Length.config]
    si_units = "m"
    unit_names = ["meter", "meters"]

The section name must contain the name of the quantity: Length.

Following the section name are any custom units, in this case we've defined miles as being a Multiplier that takes a single arg of 1069.34. Multiplier defines the unit class, it is a key into sipy.units.PUBLIC_UNITS.

The config subsection defines the SI unit of the Length quantity and any names used to refer to that quantity.

Expand source code
# -*- coding: utf-8 -*-
"""All quantities and units in `sipy` are generated from a config file.

The main config file is included in the package, but the `SIPY_CFG_FILE`
environment variable can be used to specify additional quantities and units.

There key sections of the config file are outlined below.

Sipy
====
The version on the config file must be specified to ensure backwards
compatability.

Constants
=========
Constants are defined with a key value pair for each constant. The key is the
name of the constant, the value an array that contains the value followed by
it's units.

```
newton = ["6.67430e-11", "m^3.kg.s"]
```

Prefixes
========
Prefixes are defined with a key value pair, the key is the name of the prefix
and the value is the multiplication factor applied by the prefix

```
kilo = 1e3
```

Quantity
========
This is an example entry for the `Length` quantity

```
  [Quantity.Length]
  miles = ["Multiplier", "1609.34"]
    [Quantity.Length.config]
    si_units = "m"
    unit_names = ["meter", "meters"]
```

The section name *must* contain the name of the quantity: `Length`.

Following the section name are any custom units, in this case we've defined `
miles` as being a `Multiplier` that takes a single arg of `1069.34`.
`Multiplier` defines the unit class, it is a key into `sipy.units.PUBLIC_UNITS`.

The config subsection defines the SI unit of the `Length` quantity and
any names used to refer to that quantity.
"""
# Standard Library
import os
import pathlib
from enum import Enum
from typing import Any, Counter, Dict, List, MutableMapping

# Third Party
import toml

# SIpy
from sipy.definitions import unit_counter_from_string

CONFIG_FILE = pathlib.Path(__file__).parent / "sipy.toml"

# Top level section names in the config file
KEYS = Enum(
    "Keys",
    {
        "prefixes": "Prefixes",
        "quantity": "Quantity",
        "sipy": "Sipy",
        "constants": "Constants",
    },
)

# Supported keys in the Quantity.<quantity>.config section
CONFIG_KEYS = Enum(
    "QuantityKey",
    {"si_units": "si_units", "unit_names": "unit_names", "prefix": "prefix"},
)
CONFIG_KEY = "config"

CUSTOM_FILE_ENV_VAR = "SIPY_CFG_FILE"


class SipyConfigError(Exception):
    """An error hit parsing the sipy config file"""


class Quantity:
    """A object representing a quantity from the config file."""

    def __init__(
        self,
        name: str,
        config: MutableMapping[str, Any],
        extra_units: MutableMapping[str, Any],
    ):
        """
        Args:
            name: The name of the quantity.
            config: Config relating to the quantity.
            extra_units: Extra units the quantity has.
        """
        self.name = name
        self.extra_units = extra_units

        self.unit_names = config.get(CONFIG_KEYS.unit_names.value, [])
        self.prefixes = config.get(CONFIG_KEYS.prefix.value, "Prefix")

        if CONFIG_KEYS.si_units.value not in config:
            raise SipyConfigError(
                "Quantities must define 'si_units' in [Quantity.*.config]"
            )
        self.si_units: str = config[CONFIG_KEYS.si_units.value]

        if self.prefixes and not self.unit_names:
            raise SipyConfigError(
                f"Must specify 'unit_names' for {self.name} if prefix=true"
            )

    @property
    def unit_counter(self) -> Counter[str]:
        """Create a counter from unit string in the config."""
        return unit_counter_from_string(self.si_units)

    @classmethod
    def from_toml_dict(
        cls, name: str, toml_dict: MutableMapping[str, Any]
    ) -> "Quantity":
        """Create a Quantity using a dictionary represntation of the toml file

        Args:
            name: The name of the Quantity.
            toml_dict: A dictionary representing the toml config in the
                quantities sub-section.
        """
        config = toml_dict.pop(CONFIG_KEY)
        return cls(name, config, toml_dict)


def _load_config(file_name=CONFIG_FILE) -> MutableMapping[str, Any]:
    """Parse the toml config file and return result as a dictionary."""
    cfg = toml.load(file_name)
    validate_config(cfg)
    return cfg


def validate_config(cfg: MutableMapping[str, Any]):
    """Verify that the toml file is in the expected format.

    Raises a `sipy.config.SipyConfigError` if the config is invalid.
    """
    if KEYS.sipy.value not in cfg:
        raise SipyConfigError(f"[{KEYS.sipy.value}] must be present in config file")

    version = cfg[KEYS.sipy.value].get("version")
    if version != "1.0.0":
        raise SipyConfigError(f"Unsupported config file version: {version}")

    all_keys = [k.value for k in KEYS]
    for key in cfg.keys():
        if key not in all_keys:
            raise SipyConfigError(f"[{key}] is not a valid section name")


def prefixes() -> Dict[str, float]:
    """Return prefix names along with their modifiers"""
    app_config = _load_config().get(KEYS.prefixes.value, {})

    env_file = os.getenv(CUSTOM_FILE_ENV_VAR)
    if env_file:
        env_config = _load_config(env_file).get(KEYS.prefixes.value, {})
        app_config.update(env_config)

    return dict(app_config)


def _extract_quantities(quantities_dict) -> List[Quantity]:
    return [
        Quantity.from_toml_dict(name, toml_dict)
        for name, toml_dict in quantities_dict.items()
    ]


def quantity_info() -> List[Quantity]:
    """Create `sipy.config.Quantities` from the config file"""
    app_config = _load_config()[KEYS.quantity.value]
    my_quantities = _extract_quantities(app_config)

    env_file = os.getenv(CUSTOM_FILE_ENV_VAR)
    if env_file:
        env_config = _load_config(env_file).get(KEYS.quantity.value, {})
        my_quantities += _extract_quantities(env_config)

    return my_quantities


def constants() -> Dict[str, List[str]]:
    """The constants defined in the config file

    Returns: A dictionary containing constant information in the format
        {<name>: [<value>, <units>], ...}
    """
    app_config = _load_config()[KEYS.constants.value]

    env_file = os.getenv(CUSTOM_FILE_ENV_VAR)
    if env_file:
        env_config = _load_config(env_file).get(KEYS.constants.value, {})
        app_config.update(env_config)

    return dict(app_config)

Functions

def constants()

The constants defined in the config file

Returns: A dictionary containing constant information in the format

Expand source code
def constants() -> Dict[str, List[str]]:
    """The constants defined in the config file

    Returns: A dictionary containing constant information in the format
        {<name>: [<value>, <units>], ...}
    """
    app_config = _load_config()[KEYS.constants.value]

    env_file = os.getenv(CUSTOM_FILE_ENV_VAR)
    if env_file:
        env_config = _load_config(env_file).get(KEYS.constants.value, {})
        app_config.update(env_config)

    return dict(app_config)
def prefixes()

Return prefix names along with their modifiers

Expand source code
def prefixes() -> Dict[str, float]:
    """Return prefix names along with their modifiers"""
    app_config = _load_config().get(KEYS.prefixes.value, {})

    env_file = os.getenv(CUSTOM_FILE_ENV_VAR)
    if env_file:
        env_config = _load_config(env_file).get(KEYS.prefixes.value, {})
        app_config.update(env_config)

    return dict(app_config)
def quantity_info()

Create sipy.config.Quantities from the config file

Expand source code
def quantity_info() -> List[Quantity]:
    """Create `sipy.config.Quantities` from the config file"""
    app_config = _load_config()[KEYS.quantity.value]
    my_quantities = _extract_quantities(app_config)

    env_file = os.getenv(CUSTOM_FILE_ENV_VAR)
    if env_file:
        env_config = _load_config(env_file).get(KEYS.quantity.value, {})
        my_quantities += _extract_quantities(env_config)

    return my_quantities
def validate_config(cfg)

Verify that the toml file is in the expected format.

Raises a SipyConfigError if the config is invalid.

Expand source code
def validate_config(cfg: MutableMapping[str, Any]):
    """Verify that the toml file is in the expected format.

    Raises a `sipy.config.SipyConfigError` if the config is invalid.
    """
    if KEYS.sipy.value not in cfg:
        raise SipyConfigError(f"[{KEYS.sipy.value}] must be present in config file")

    version = cfg[KEYS.sipy.value].get("version")
    if version != "1.0.0":
        raise SipyConfigError(f"Unsupported config file version: {version}")

    all_keys = [k.value for k in KEYS]
    for key in cfg.keys():
        if key not in all_keys:
            raise SipyConfigError(f"[{key}] is not a valid section name")

Classes

class CONFIG_KEYS (*args, **kwargs)

An enumeration.

Ancestors

  • enum.Enum

Class variables

var prefix

An enumeration.

var si_units

An enumeration.

var unit_names

An enumeration.

class KEYS (*args, **kwargs)

An enumeration.

Ancestors

  • enum.Enum

Class variables

var constants

An enumeration.

var prefixes

An enumeration.

var quantity

An enumeration.

var sipy

An enumeration.

class Quantity (name, config, extra_units)

A object representing a quantity from the config file.

Args

name
The name of the quantity.
config
Config relating to the quantity.
extra_units
Extra units the quantity has.
Expand source code
class Quantity:
    """A object representing a quantity from the config file."""

    def __init__(
        self,
        name: str,
        config: MutableMapping[str, Any],
        extra_units: MutableMapping[str, Any],
    ):
        """
        Args:
            name: The name of the quantity.
            config: Config relating to the quantity.
            extra_units: Extra units the quantity has.
        """
        self.name = name
        self.extra_units = extra_units

        self.unit_names = config.get(CONFIG_KEYS.unit_names.value, [])
        self.prefixes = config.get(CONFIG_KEYS.prefix.value, "Prefix")

        if CONFIG_KEYS.si_units.value not in config:
            raise SipyConfigError(
                "Quantities must define 'si_units' in [Quantity.*.config]"
            )
        self.si_units: str = config[CONFIG_KEYS.si_units.value]

        if self.prefixes and not self.unit_names:
            raise SipyConfigError(
                f"Must specify 'unit_names' for {self.name} if prefix=true"
            )

    @property
    def unit_counter(self) -> Counter[str]:
        """Create a counter from unit string in the config."""
        return unit_counter_from_string(self.si_units)

    @classmethod
    def from_toml_dict(
        cls, name: str, toml_dict: MutableMapping[str, Any]
    ) -> "Quantity":
        """Create a Quantity using a dictionary represntation of the toml file

        Args:
            name: The name of the Quantity.
            toml_dict: A dictionary representing the toml config in the
                quantities sub-section.
        """
        config = toml_dict.pop(CONFIG_KEY)
        return cls(name, config, toml_dict)

Static methods

def from_toml_dict(name, toml_dict)

Create a Quantity using a dictionary represntation of the toml file

Args

name
The name of the Quantity.
toml_dict
A dictionary representing the toml config in the quantities sub-section.
Expand source code
@classmethod
def from_toml_dict(
    cls, name: str, toml_dict: MutableMapping[str, Any]
) -> "Quantity":
    """Create a Quantity using a dictionary represntation of the toml file

    Args:
        name: The name of the Quantity.
        toml_dict: A dictionary representing the toml config in the
            quantities sub-section.
    """
    config = toml_dict.pop(CONFIG_KEY)
    return cls(name, config, toml_dict)

Instance variables

var unit_counter

Create a counter from unit string in the config.

Expand source code
@property
def unit_counter(self) -> Counter[str]:
    """Create a counter from unit string in the config."""
    return unit_counter_from_string(self.si_units)
class SipyConfigError (*args, **kwargs)

An error hit parsing the sipy config file

Expand source code
class SipyConfigError(Exception):
    """An error hit parsing the sipy config file"""

Ancestors

  • builtins.Exception
  • builtins.BaseException