Core Module Documentation

Description

Base classes for MasterPiece - a light-weight and general purpose object-oriented toolkit for implementing modular plugin-aware applications.

class masterpiece.core.Application(name, payload=None)[source]

Masterpiece application base class. Implements startup argument parsing, plugin management and initialization of class attributes through class specific JSON configuration files.

Todo

Abstract the serialization API to support multiple formats (e.g., JSON, XML, etc.).

deserialize()[source]

Deserialize instances from the startup file specified by ‘serialization_file’ class attribute, or ‘–file’ startup argument.

TODO: or by optional method parameter

Return type:

None

install_plugins()[source]

Installs plugins into the application by invoking the install() method of each loaded plugin module. Note: This method is intended for testing and debugging purposes only. In a typical use case, the application should handle the instantiation of classes and manage their attributes as needed.

Return type:

None

instantiate_plugin_by_name(name)[source]

Installs the plugin by name, that is, instantiates the plugin class and inserts the instance as child to the application. :type name: str :param name: name of the plugin class :type name: str

Return type:

Optional[MasterPiece]

classmethod load_plugins()[source]

Loads and initializes all plugins for instantiation. This method corresponds to importing Python modules with import clauses.

Return type:

None

classmethod register_args(parser)[source]

Register startup arguments to be parsed.

Parameters:

parser (argparse.ArgumentParser) – parser to add the startup arguments.

classmethod register_plugin_group(name)[source]

Registers a new plugin group within the application. Only plugins that match the registered groups will be loaded. By default, all ‘masterpiece’ plugins are included. Frameworks built on the MasterPiece framework can define framework-specific group names, enabling plugins to be developed for any application built on those frameworks. Additionally, individual applications can introduce application-specific groups (typically named after the application), allowing plugins to be developed exclusively for that specific application.

Parameters:

name (str) – The name of the plugin group to be registered

Return type:

None

serialize()[source]

Serialize application state to the file specified by ‘serialization_file’ class attribute’.

TODO: or by optional method parameter

Return type:

None

class masterpiece.core.Args[source]

For typing ArgumentParser args. Sub-classes of MasterPiece can sub-class their startup arguments from this base class to define any additional args.

TODO: change the implementation due to Liskov’s warning

class masterpiece.core.Composite(name='group', payload=None)[source]

Class implementing hierarchy. Objects of this class can consist of children.

This class can be used for grouping masterpieces into larger entities.

Example:

motion_sensors = Composite("motionsensors")
motion_sensors.add(ShellyMotionSensor("downstairs"))
motion_sensors.add(ShellyMotionSensor("upstairs"))
add(h)[source]

Add new automation object as children. The object to be inserted must be derived from MasterPiece base class.

Parameters:

h (T) – object to be inserted.

Return type:

None

from_dict(data)[source]

Recursively deserialize the group from a dictionary, including its children.

Parameters:

data (dict) – data to deserialize from.

print(prefix='', is_last=True)[source]

Print the hierarchy of the node using ├─, └─, and │.

Example:

parent = Composite("parent")
child = MasterPiece("child")
parent.add(child)
parent.print()

# expected output:
# parent
#  └─ child
Parameters:
  • prefix (str, optional) – Defaults to “”.

  • is_last (bool, optional) – Defaults to True.

run_forever()[source]

Dispatches first the call to all children and then to the super class. It is up to the sub classes to implement the actual functionality for this method.

Return type:

None

shutdown()[source]

Dispatches first the call to all children and then to the super class. It is up to the sub classes to implement the actual functionality for this method.

Return type:

None

class masterpiece.core.Log(name, level=10)[source]

Default logger for logging events to application log and/or to console.

class masterpiece.core.MasterPiece(name='noname', payload=None)[source]

An object with a name. Base class of everything. Serves as the foundational class offering key features needed by any robust object-oriented software.

Logging

All objects have logging methods e.g. info() and error() at their fingertips, for centralized logging.

if (err := self.do_good()) < 0:
    self.error(f"Damn, did bad {err}")

Factory Method Pattern

Instantiation via class identifiers, adhering to the factory method pattern. This allows for the dynamic creation of instances based on class identifiers, promoting decoupled and extensible design required by plugin architecture.

# instead of fixed implementation car = Ferrari()
car = Object.instantiate(car_class_id)

Serialization

Serialization of both class and instance attributes serves as a means of configuration.

Class attributes should follow a consistent naming convention where an underscore prefix (‘_’ or ‘__’) implies the attribute is private and transient, meaning it is not serialized. Class attributes without an underscore prefix are initialized from configuration files named ‘~/.masterpiece/[appname]/[classname].json’, if present. If the class-specific configuration files do not already exist, they are automatically created upon the first run.

Instance attributes can be serialized and deserialized using the serialize() and deserialize() methods:

# serialize to json file
with open("foo.json", "w") as f:
    foo.serialize(f)

# deserialize
foo = F()
with open("foo.json", "r") as f:
    foo.deserialize(f)

Deserialization must restore the object’s state to what it was when it was serialized. As Python does not have ‘transient’ keyword to tag attributes that should be serialized, all classes must explicitely describe information for the serialization. This is done with to_dict() and from_dict() methods:

def to_dict(self):
    data = super().to_dict()
    data["_foo"] = {
        "topic": self.topic,
        "temperature": self.temperature,
    }
    return data

def from_dict(self, data):
    super().from_dict(data)
    for key, value in data["_foo"].items():
        setattr(self, key, value)

Copying Objects

Any object can be copied using the copy() method. This feature is based on serialization, so typically, subclasses don’t need to implement the copy() method; everything is taken care of by the base class.

foo2 = foo.copy()
classmethod app_name(name)[source]

Set application name

Parameters:

name (str) – application name

classmethod classattrs_from_dict(attributes)[source]

Set class attributes from a dictionary.

classmethod classattrs_to_dict()[source]

Convert class attributes to a dictionary.

classmethod configure_args(args)[source]

Register startup arguments to be parsed.

Parameters:

parser (argparse.ArgumentParser) – parser to add the startup arguments.

Return type:

None

copy()[source]

Create and return a copy of the current object.

This method serializes the current object to a dictionary using the to_dict method, creates a new instance of the object’s class, and populates it with the serialized data using the from_dict method.

This method uses class identifier based instantiation (see factory method pattern) to create a new instance of the object, and ‘to_dict’ and ‘from_dict’ methods to initialize object’s state.

Return type:

MasterPiece

Returns:

A new instance of the object’s class with the same state as the original object.

Example:

clone_of_john = john.copy()
debug(msg, details='')[source]

Logs the given debug message to the application log.

Parameters:
  • msg (str) – The information message to be logged.

  • details (str) – Additional detailed information for the message to be logged

Return type:

None

deserialize_from_json(f)[source]

Load attributes from the given JSON file.

elapsed_seconds_in_day(ts_utc)[source]

Fetch the elapsed seconds since the be given time stamp ‘ts_utc’.

Returns:

elapsed second today

Return type:

float

elapsed_seconds_in_hour(ts_utc)[source]

Given timestamp in UTC, Compute elapsed seconds within an hour

Parameters:

ts (float) – seconds since UTC epoch

Returns:

_description_

Return type:

float

epoc2utc(epoch)[source]

Converts the given epoch time to UTC time string. All time coordinates are represented in UTC time. This allows the time coordinate to be mapped to any local time representation without ambiguity.

Parameters:
  • epoch (float) – timestamp in UTC time

  • rc (str) – time string describing date, time and time zone e.g 2024-07-08T12:10:22Z

Returns:

UTC time

error(msg, details='')[source]

Logs the given error message to the application log.

Parameters:
  • msg (str) – The message to be logged.

  • details (str) – Additional detailed information for the message to be logged

Return type:

None

classmethod find_class(class_id)[source]

Given class identifier find the registered class. If no class with the give identifier exists return None.

Parameters:

class_id (int) – class identifier

Returns:

class or null if not registered

Return type:

obj (obj)

from_dict(data)[source]

Update instance attributes from a dictionary.

classmethod get_app_name()[source]

Get application name

Returns:

application name

Return type:

name (str)

classmethod get_class_id()[source]

Return the class id of the class. Each class has an unique identifier that can be used for instantiating the class via Object.instantiate() method.

Parameters:

cls (class) – class

Return type:

str

Returns:

id (int) unique class identifier through which the class can be instantiated by factory method pattern.

classmethod get_json_file()[source]

Generate the JSON file name based on the class name.

The file is created into users home folder.

classmethod get_registered_classes()[source]

Get the dictionary holding the registered class identifiers and the corresponding classes.

Returns:

dictionary of class identifier - class pairs

Return type:

dict

classmethod has_class_method_directly(method_name)[source]

Check if the method is in the class’s own dictionary

Return type:

bool

info(msg, details='')[source]

Logs the given information message to the application log.

Parameters:
  • msg (str) – The information message to be logged.

  • details (str) – Additional detailed information for the message to be logged

Return type:

None

classmethod initialize_class(load_class_attrs=True)[source]

Initialize the class for instantiation. Initializes the class identifier. Initialize public class attributes from the class-specific configuration files, unless disabled with the load_class_attrs parameter. Call the register() method of the class to let each class do custom initializations. Set up atexit callback to generate class-specific initialization files if they don’t exist already.

Parameters:

load_class_attrs (bool, optional) – If true, then attempts to initialize class attributes from the class-specific configuration files. Defaults to True.

Returns:

Returns true if the class was initialized. False implies the class is already initialized,

in which case the method call has no effect.

Return type:

bool

classmethod instantiate(class_id)[source]

Create an instance of the class corresponding to the given class identifier. This method implements the factory method pattern, which is essential for a plugin architecture.

Parameters:

class_id (int) – Identifier of the class to instantiate.

Returns:

An instance of the class corresponding to the given class identifier.

Return type:

obj

classmethod instantiate_with_param(class_id, param)[source]

Given class identifier and one constructor argument create the corresponding object.

Parameters:
  • class_id (str) – class identifier

  • param (Any) – class specific constructor parameter

Returns:

instance of the given class.

Return type:

obj

classmethod is_abstract()[source]

Check whether the class is abstract or real. Override in the derived sub-classes. The default is False.

Return type:

bool

Returns:

True (bool) if abstract

is_time_between(begin_time, end_time, check_time=None)[source]

Check if the given time is within the given time line. All timestamps must be in UTC time.

Parameters:
  • begin_time (timestamp) – beginning of the timeline

  • end_time (timestamp) – end of the timeline

  • check_time (timestamp) – time to be checked

Returns:

True if within the timeline

Return type:

rc (bool)

classmethod load_from_json()[source]

Load class attributes from a JSON file.

classmethod parse_args()[source]

Parse registered startup arguments.

Parameters:

parser (argparse.ArgumentParser) – parser to add the startup arguments.

Return type:

None

classmethod parse_initial_args()[source]

Parse application name and configuration name arguments. Note that argument parsing is two stage process. This method must be called at very early on to know where to load class initialization files. See register_args() method.

print(prefix='', is_last=True)[source]

Print the name of the object, decorated with prefix consisting of ├─, └─, and │.

quantize(quanta, value)[source]

Quantize the given value.

Parameters:
  • quanta (float) – resolution for quantization

  • value (float) – value to be quantized

Returns:

quantized value

Return type:

(float)

Example:

hour_of_a_day = self.quantize(3600, epoch_seconds)
classmethod register()[source]

General purpose class method for initializing classes for instantiation. Up to the sub clases to implement. This method is called right before the class attributes are loaded from the class specific configuration files.

Return type:

None

classmethod register_args(parser)[source]

Register startup arguments to be parsed.

Parameters:

parser (argparse.ArgumentParser) – parser to add the startup arguments.

Return type:

None

classmethod register_class(class_id, ctor)[source]

Register the given class identifier for identifier based instantiation . This, factory method pattern, as it is called, decouples the actual implementation from the interface. For more information see instantiate() method.

Parameters:
  • class_id (str) – class identifier

  • ctor (function) – constructor

run()[source]

Run the masterpiece. Dispatches the call to payload object and returns the control to the caller.

Return type:

None

run_forever()[source]

Run the masterpiece forever. This method will return only when violently terminated.

Return type:

None

classmethod save_to_json()[source]

Create class configuration file, if the file does not exist yet.

serialize_to_json(f)[source]

Serialize the object to given JSON file

classmethod set_log(l)[source]

Set logger.

Parameters:

l (logger) – logger object

Return type:

None

shutdown()[source]

Shutdown the masterpiece. It is up to the sub classes to implement this method. Dispatches the call to payload object.

Return type:

None

timestamp()[source]

Returns the current date-time in UTC.

Returns:

datetime in UTC.

Return type:

rc (datetime)

timestamp_hour(ts)[source]

Returns the hour in 24h format in UTC.

Parameters:

ts (float) – timestamp

Returns:

current hour in UTC 0 …23

Return type:

rc (int)

timestampstr(ts)[source]

Converts the given timestamp to human readable string of format ‘Y-m-d H:M:S’.

Parameters:

ts (timestamp) – time stamp to be converted

Returns:

human readable date-time string

Return type:

rc (string)

to_dict()[source]

Convert instance attributes to a dictionary.

warning(msg, details='')[source]

Logs the given warning message to the application log.

Parameters:
  • msg (str) – The message to be logged.

  • details (str) – Additional detailed information for the message to be logged

Return type:

None

class masterpiece.core.MetaMasterPiece(name, bases, dct)[source]

Meta class that implements per-class initialization procedure for the MasterPiece classes. Typically the application code should not need to to be aware of this class.

class masterpiece.core.PlugMaster(name)[source]

The Plugmaster class is responsible for managing and loading plugins into an application.

The Plugmaster is designed to work with plugins that are Masterpiece objects or subclasses thereof. Plugins can optionally be derived from the Plugin class.

If a plugin implements the Plugin interface, it is responsible for determining what objects should be added to the application.

If a plugin is not a Plugin class, it is simply loaded, and it is the responsibility of the application configuration file or the application code to determine how to utilize the plugin.

find_class_by_name(name)[source]

Find and return a plugin class by its name.

Return type:

Optional[Type[MasterPiece]]

Returns:

plugin class (MasterPiece) or none if not found

get()[source]

Fetch the directionary of plugins classes.

Returns:

List of plugins

Return type:

List[Type[MasterPiece]]

install(app)[source]

Instantiate and add all the registered plugins to the application.

Typically is up to the application or the configuration to define the instances to be added. This method is provided for testing purposes only.

Parameters:
  • app (Composite) – parent object (application), for hosting the instances

  • plugin. (created by the)

Return type:

None

instantiate_class_by_name(app, name)[source]

Instantiate and add the plugin into the application.

Parameters:
  • app (Composite) – parent object, for hosting the instances createdby the plugin.

  • name (str) – name of the plugin to be instantiated

Return type:

Optional[MasterPiece]

load(name)[source]

Fetch the entry points associated with the ‘name’, call their ‘load()’ methods and insert to the list of plugins. Note: Python’s ‘importlib.metadata’ API has been redesigned a couple of times in the past. The current implementation has been tested with Python 3.8, 3.9 and 3.12.

Parameters:

name (str) – Name determining the plugins to be loaded.

Return type:

None

class masterpiece.core.Plugin(name='noname', payload=None)[source]

Base class for plugins. TODO: make formally abstract

install(app)[source]

Instantiates and installs the classes in the plugin module into the application. This is an abstract method that the plugin classes must implement. Plugins may choose not to do anything here and instead leave it up to the higher level software layers.

Parameters:

app (Composite) – application to plug into