masterpiece 0.0.0__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.
- __init__.py +13 -0
- composite.py +66 -0
- masterpiece-0.0.0.dist-info/METADATA +134 -0
- masterpiece-0.0.0.dist-info/RECORD +7 -0
- masterpiece-0.0.0.dist-info/WHEEL +5 -0
- masterpiece-0.0.0.dist-info/top_level.txt +3 -0
- masterpiece.py +493 -0
__init__.py
ADDED
composite.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List
|
|
3
|
+
from masterpiece import MasterPiece
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Composite(MasterPiece):
|
|
7
|
+
"""Group base class that can consist of `MasterPiece` and `Group` objects as
|
|
8
|
+
children.
|
|
9
|
+
|
|
10
|
+
This class can be used for grouping masterpieces into larger logical entities.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
::
|
|
14
|
+
|
|
15
|
+
motion_sensors = Group("motionsensors")
|
|
16
|
+
motion_sensors.add(ShellyMotionSensor("downstairs"))
|
|
17
|
+
motion_sensors.add(ShellyMotionSensor("upstairs"))
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_class_id = ""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str = "group") -> None:
|
|
23
|
+
super().__init__(name)
|
|
24
|
+
self.children: List = []
|
|
25
|
+
self.role: str = "union"
|
|
26
|
+
|
|
27
|
+
def add(self, h: MasterPiece) -> None:
|
|
28
|
+
"""Add new automation object as children. The object to be inserted
|
|
29
|
+
must be derived from Object base class.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
h (Object): object to be inserted.
|
|
33
|
+
"""
|
|
34
|
+
self.children.append(h)
|
|
35
|
+
|
|
36
|
+
def to_dict(self):
|
|
37
|
+
data = super().to_dict()
|
|
38
|
+
data["_group"] = {
|
|
39
|
+
"role": self.role,
|
|
40
|
+
"children": [child.to_dict() for child in self.children],
|
|
41
|
+
}
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
def from_dict(self, data):
|
|
45
|
+
"""Recursively deserialize the group from a dictionary, including it
|
|
46
|
+
children.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
data (dict): data to deserialize from.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
super().from_dict(data)
|
|
53
|
+
for key, value in data.get("_group", {}).items():
|
|
54
|
+
if key == "children":
|
|
55
|
+
for child_dict in value:
|
|
56
|
+
child = MasterPiece.instantiate(child_dict["_class"])
|
|
57
|
+
self.add(child)
|
|
58
|
+
child.from_dict(child_dict)
|
|
59
|
+
else:
|
|
60
|
+
setattr(self, key, value)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def register(cls):
|
|
64
|
+
if cls._class_id == "":
|
|
65
|
+
MasterPiece.register()
|
|
66
|
+
cls.initialize_class()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: masterpiece
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Masterpiece framework
|
|
5
|
+
Author-email: J Meskanen <juham.api@gmail.com>
|
|
6
|
+
Maintainer-email: "J. Meskanen" <juham.api@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
===========
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2024, Juha Meskanen
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Project-URL: Homepage, https://meskanen.com
|
|
31
|
+
Project-URL: Bug Reports, https://meskanen.com
|
|
32
|
+
Project-URL: Funding, https://meskanen.com
|
|
33
|
+
Project-URL: Say Thanks!, http://meskanen.com
|
|
34
|
+
Project-URL: Source, https://meskanen.com
|
|
35
|
+
Keywords: object-oriented,plugin,framework
|
|
36
|
+
Classifier: Development Status :: 1 - Planning
|
|
37
|
+
Classifier: Intended Audience :: Developers
|
|
38
|
+
Classifier: Topic :: Software Development
|
|
39
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
41
|
+
Requires-Python: >=3.8
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
Requires-Dist: paho-mqtt >=1
|
|
44
|
+
Requires-Dist: influxdb3-python >=0.3.0
|
|
45
|
+
Requires-Dist: requests >=2.31
|
|
46
|
+
Requires-Dist: pytz >=2024.1
|
|
47
|
+
Requires-Dist: importlib-metadata
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: check-manifest ; extra == 'dev'
|
|
50
|
+
|
|
51
|
+
Welcome to MasterPiece Framework
|
|
52
|
+
================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Project Status and Current State
|
|
56
|
+
--------------------------------
|
|
57
|
+
|
|
58
|
+
This is the initial commit of the project. At this stage, the framework is in its early development phase.
|
|
59
|
+
|
|
60
|
+
Here's what is currently available:
|
|
61
|
+
|
|
62
|
+
* Package Infrastructure: The basic Python package setup is in place, configured with pyproject.toml.
|
|
63
|
+
* Early Drafts: Initial versions of the two core base classes have been implemented 'MasterPiece' and 'Group'.
|
|
64
|
+
|
|
65
|
+
Insights and suggestions are invaluable as we continue to develop and refine the framework.
|
|
66
|
+
|
|
67
|
+
In this current state, you might call it merely a mission, rather than masterpiece, but I'm
|
|
68
|
+
working hard to turn it into a masterpiece!
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Goals
|
|
73
|
+
-----
|
|
74
|
+
|
|
75
|
+
The primary goal of this framework is to provide a minimal yet robust set of general-purpose base classes designed
|
|
76
|
+
to streamline the development of new software in Python. The key objectives of this framework include:
|
|
77
|
+
|
|
78
|
+
* Robusness: Minimal yet robust API providing the developer with 100% control.
|
|
79
|
+
* First-Time Excellence: The aim is to build a robust and reliable framework that is correct and efficient from the start,
|
|
80
|
+
eliminating the need for disruptive changes or backward compatibility issues in future releases.
|
|
81
|
+
* Abstraction: Provide a layer of abstraction to shield the API from the impacts of external code, including
|
|
82
|
+
third-party libraries and APIs.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
Design
|
|
87
|
+
------
|
|
88
|
+
|
|
89
|
+
The design patterns employed to achieve our goals include:
|
|
90
|
+
|
|
91
|
+
* Object-Oriented Paradigm: Employing object-oriented principles to promote code reuse, encapsulation, and modularity.
|
|
92
|
+
* Factory Method Pattern: Decoupling implementations from their interfaces to simplify object creation and enhance flexibility.
|
|
93
|
+
* Layered Design Pattern: Promoting separation of concerns and reusability by organizing code into distinct layers.
|
|
94
|
+
* Plugin API: Enabling extensibility and customization through a well-defined plugin interface for easy integration of additional features.
|
|
95
|
+
* Serialization: Facilitating seamless conversion between data formats and object representations.
|
|
96
|
+
* Easy Configuration: Simplifying setup and management through startup arguments and configuration files.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
If you appreciate these design concepts, you've come to the right place!
|
|
100
|
+
|
|
101
|
+
A framework designed with these principles deserves more than just "objects" — let's call
|
|
102
|
+
them "masterpieces". This term reflects commitment to fine-grained modular design ("pieces") and
|
|
103
|
+
adds a touch of humor with "Master".
|
|
104
|
+
|
|
105
|
+
Just as all creatures on Earth share a common ancestor, all components in this framework trace their lineage
|
|
106
|
+
back to this foundational anchestor named "masterpiece" ... (okay, perhaps a bit dramatic).
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
Install
|
|
110
|
+
-------
|
|
111
|
+
|
|
112
|
+
1. To install:
|
|
113
|
+
|
|
114
|
+
`pip install masterpiece`.
|
|
115
|
+
|
|
116
|
+
This installs all the dependencies as well, I hope.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
Developer Documentation
|
|
122
|
+
-----------------------
|
|
123
|
+
|
|
124
|
+
After several hours (okay, days), Sphinx finally generates something. It will require a
|
|
125
|
+
few more hours (okay, days) of effort to produce something really usable.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Special Thanks
|
|
130
|
+
--------------
|
|
131
|
+
|
|
132
|
+
My ability to translate my architecture ideas into Python is greatly due to the generous support of one
|
|
133
|
+
extraordinary gentleman: [Mahi.fi](https://mahi.fi). His support and encouragement have been
|
|
134
|
+
invaluable in bringing this project to life. Thank you!
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
__init__.py,sha256=EkD1bsmI5Px7FwqPoslFybexm-MR1uLeo3dZ5TcYALw,214
|
|
2
|
+
composite.py,sha256=0e0df2ZN7xvoXF-wcEpHG9pJB6_SHpUDyybeRgWH124,1939
|
|
3
|
+
masterpiece.py,sha256=1k17HtUvEk5Hv3Fnl8UbFnp0hrmLVVwVYwE0nLxjvhw,17135
|
|
4
|
+
masterpiece-0.0.0.dist-info/METADATA,sha256=Tz5CDW-RSDE0OPjJowctfrV90v456N8lHPWHky_rBK4,5694
|
|
5
|
+
masterpiece-0.0.0.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
|
|
6
|
+
masterpiece-0.0.0.dist-info/top_level.txt,sha256=pQ5lF-QNtWkpRgmQZIhWdA6GtQzEDjWm91fMaUx8vCA,31
|
|
7
|
+
masterpiece-0.0.0.dist-info/RECORD,,
|
masterpiece.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
6
|
+
import atexit
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MasterPiece:
|
|
11
|
+
"""An object with a name. Base class of everything. Serves as the foundational class offering key
|
|
12
|
+
features needed by any robust object-oriented software.
|
|
13
|
+
|
|
14
|
+
Logging
|
|
15
|
+
-------
|
|
16
|
+
|
|
17
|
+
All objects have logging methods e.g. info() and error() at their fingertips, for
|
|
18
|
+
centralized logging.
|
|
19
|
+
::
|
|
20
|
+
|
|
21
|
+
if (err := self.do_good()) < 0:
|
|
22
|
+
self.error(f"Damn, did bad {err}")
|
|
23
|
+
|
|
24
|
+
Factory Method Pattern
|
|
25
|
+
----------------------
|
|
26
|
+
|
|
27
|
+
Instantiation via class identifiers, adhering to the factory method pattern. This allows for the dynamic creation of
|
|
28
|
+
instances based on class identifiers, promoting decoupled and extensible design required by plugin architecture.
|
|
29
|
+
::
|
|
30
|
+
|
|
31
|
+
# instead of fixed implementation car = Ferrari()
|
|
32
|
+
car = Object.instantiate(car_class_id)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Serialization
|
|
36
|
+
-------------
|
|
37
|
+
|
|
38
|
+
Serialization of both class and instance attributes serves as a means of configuration.
|
|
39
|
+
|
|
40
|
+
Class attributes should follow a consistent naming convention where an underscore prefix
|
|
41
|
+
('_' or '__') implies the attribute is private and transient, meaning it is not serialized.
|
|
42
|
+
Class attributes without an underscore prefix are initialized from configuration files named
|
|
43
|
+
'~/.masterpiece/[appname]/[classname].json', if present. If the class-specific configuration
|
|
44
|
+
files do not already exist, they are automatically created upon the first run.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
Instance attributes can be serialized and deserialized using the `serialize()` and `deserialize()` methods:
|
|
48
|
+
::
|
|
49
|
+
|
|
50
|
+
# serialize to json file
|
|
51
|
+
with open("foo.json", "w") as f:
|
|
52
|
+
foo.serialize(f)
|
|
53
|
+
|
|
54
|
+
# deserialize
|
|
55
|
+
foo = F()
|
|
56
|
+
with open("foo.json", "r") as f:
|
|
57
|
+
foo.deserialize(f)
|
|
58
|
+
|
|
59
|
+
Deserialization must restore the object's state to what it was when it was serialized.
|
|
60
|
+
As Python does not have 'transient' keyword to tag attributes that should be serialized, all
|
|
61
|
+
classes must explicitely describe information for the serialization. This is done with
|
|
62
|
+
`to_dict()` and `from_dict()` methods:
|
|
63
|
+
::
|
|
64
|
+
|
|
65
|
+
def to_dict(self):
|
|
66
|
+
data = super().to_dict()
|
|
67
|
+
data["_foo"] = {
|
|
68
|
+
"topic": self.topic,
|
|
69
|
+
"temperature": self.temperature,
|
|
70
|
+
}
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
def from_dict(self, data):
|
|
74
|
+
super().from_dict(data)
|
|
75
|
+
for key, value in data["_foo"].items():
|
|
76
|
+
setattr(self, key, value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Copying Objects
|
|
80
|
+
---------------
|
|
81
|
+
|
|
82
|
+
Any object can be copied using the `copy()` method. This feature is based on serialization, so
|
|
83
|
+
typically, subclasses don't need to implement the `copy()` method; everything is taken care of
|
|
84
|
+
by the base class.
|
|
85
|
+
::
|
|
86
|
+
|
|
87
|
+
foo2 = foo.copy()
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# public serializable class attributes
|
|
92
|
+
app_name = "masterpiece"
|
|
93
|
+
config_folder = "config"
|
|
94
|
+
|
|
95
|
+
# non-serializable private class attributes
|
|
96
|
+
_log: Optional[logging.Logger] = None
|
|
97
|
+
_factory: dict = {}
|
|
98
|
+
_class_id: str = ""
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def initialize_class(cls, load_class_attrs: bool = True) -> bool:
|
|
102
|
+
"""Initialize the class for instantiation, if not initialized already.
|
|
103
|
+
This method initializes the class identifier and deserializes the
|
|
104
|
+
public attributes from the specified configuration folder.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
load_class_attrs (bool, optional): If true then attempts to initialize class attributes
|
|
108
|
+
from the class specific configuration files. Defaults to True.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
bool: returns true if the class was initialized, false implies the class is already initialized
|
|
112
|
+
in which case the method call has no effect.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
if cls._class_id == "":
|
|
116
|
+
cls._class_id = cls.__name__
|
|
117
|
+
MasterPiece.register_class(cls._class_id, cls)
|
|
118
|
+
if load_class_attrs:
|
|
119
|
+
cls.load_from_json()
|
|
120
|
+
atexit.register(cls.save_to_json)
|
|
121
|
+
return False
|
|
122
|
+
else:
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def is_abstract(cls) -> bool:
|
|
127
|
+
"""Check whether the class is abstract or real. Override in the derived
|
|
128
|
+
sub-classes. The default is False.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True (bool) if abstract
|
|
132
|
+
"""
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def set_log(cls, l: logging.Logger) -> None:
|
|
137
|
+
"""Set logger.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
l (logger): logger object
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
cls._log = l
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def get_class_id(cls) -> str:
|
|
147
|
+
"""Return the class id of the class. Each class has an unique
|
|
148
|
+
identifier that can be used for instantiating the class via
|
|
149
|
+
:meth:`Object.instantiate` method.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
cls (class): class
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
id (int) unique class identifier through which the class can be instantiated by factory method pattern.
|
|
156
|
+
"""
|
|
157
|
+
return cls.__name__
|
|
158
|
+
|
|
159
|
+
def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None:
|
|
160
|
+
"""Called when new sub-class is created.
|
|
161
|
+
|
|
162
|
+
Automatically registers the sub class by calling its register()
|
|
163
|
+
method. For more information on this method consult Python
|
|
164
|
+
documentation.
|
|
165
|
+
"""
|
|
166
|
+
super().__init_subclass__(**kwargs)
|
|
167
|
+
cls.register()
|
|
168
|
+
|
|
169
|
+
def __init__(self, name: str = "noname") -> None:
|
|
170
|
+
"""Creates object with the given name. Initializes logger for the newly
|
|
171
|
+
created object.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
```python
|
|
175
|
+
obj = Object('foo')
|
|
176
|
+
obj.info('Yippee, object created')
|
|
177
|
+
```
|
|
178
|
+
"""
|
|
179
|
+
self.name = name
|
|
180
|
+
|
|
181
|
+
def debug(self, msg: str, details: str = "") -> None:
|
|
182
|
+
"""Logs the given debug message to the application log.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
msg (str): The information message to be logged.
|
|
186
|
+
details (str): Additional detailed information for the message to be logged
|
|
187
|
+
"""
|
|
188
|
+
if self._log is not None:
|
|
189
|
+
self._log.debug(f"{self.name} : {msg} - {details}")
|
|
190
|
+
|
|
191
|
+
def info(self, msg: str, details: str = "") -> None:
|
|
192
|
+
"""Logs the given information message to the application log.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
msg (str): The information message to be logged.
|
|
196
|
+
details (str): Additional detailed information for the message to be logged
|
|
197
|
+
"""
|
|
198
|
+
if self._log is not None:
|
|
199
|
+
self._log.info(f"{self.name} : {msg} - {details}")
|
|
200
|
+
|
|
201
|
+
def warning(self, msg: str, details: str = "") -> None:
|
|
202
|
+
"""Logs the given warning message to the application log.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
msg (str): The message to be logged.
|
|
206
|
+
details (str): Additional detailed information for the message to be logged
|
|
207
|
+
"""
|
|
208
|
+
if self._log is not None:
|
|
209
|
+
self._log.warn(f"{self.name} : {msg} - {details}")
|
|
210
|
+
|
|
211
|
+
def error(self, msg: str, details: str = "") -> None:
|
|
212
|
+
"""Logs the given error message to the application log.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
msg (str): The message to be logged.
|
|
216
|
+
details (str): Additional detailed information for the message to be logged
|
|
217
|
+
"""
|
|
218
|
+
if self._log is not None:
|
|
219
|
+
self._log.error(f"{self.name} : {msg} - {details}")
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def get_json_file(cls):
|
|
223
|
+
"""Generate the JSON file name based on the class name.
|
|
224
|
+
|
|
225
|
+
The file is created into users home folder.
|
|
226
|
+
"""
|
|
227
|
+
return os.path.join(
|
|
228
|
+
os.path.expanduser("~"),
|
|
229
|
+
"." + cls.app_name,
|
|
230
|
+
cls.config_folder,
|
|
231
|
+
cls.__name__ + ".json",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def to_dict(self):
|
|
235
|
+
"""Convert instance attributes to a dictionary."""
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"_class": self.get_class_id(), # the real class
|
|
239
|
+
"_version:": 0,
|
|
240
|
+
"_object": {
|
|
241
|
+
"name": self.name,
|
|
242
|
+
# Add more attributes here as needed
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
def from_dict(self, data):
|
|
247
|
+
"""Update instance attributes from a dictionary."""
|
|
248
|
+
|
|
249
|
+
if self.get_class_id() != data["_class"]:
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"Class mismatch, expected:{self.get_class_id()}, actual:{data['_class']}"
|
|
252
|
+
)
|
|
253
|
+
for key, value in data["_object"].items():
|
|
254
|
+
setattr(self, key, value)
|
|
255
|
+
|
|
256
|
+
def serialize_to_json(self, f):
|
|
257
|
+
"""Serialize."""
|
|
258
|
+
json.dump(self.to_dict(), f, indent=4)
|
|
259
|
+
|
|
260
|
+
def deserialize_from_json(self, f):
|
|
261
|
+
"""Load attributes from a JSON file."""
|
|
262
|
+
attributes = json.load(f)
|
|
263
|
+
self.from_dict(attributes)
|
|
264
|
+
|
|
265
|
+
def copy(self):
|
|
266
|
+
"""Create and return a copy of the current object.
|
|
267
|
+
|
|
268
|
+
This method serializes the current object to a dictionary using the `to_dict` method,
|
|
269
|
+
creates a new instance of the object's class, and populates it with the serialized data
|
|
270
|
+
using the `from_dict` method.
|
|
271
|
+
|
|
272
|
+
This method uses class identifier based instantiation (see factory method pattern) to create a new instance
|
|
273
|
+
of the object, and 'to_dict' and 'from_dict' methods to initialize object's state.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
A new instance of the object's class with the same state as the original object.
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
::
|
|
280
|
+
|
|
281
|
+
clone_of_john = john.copy()
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
data = self.to_dict()
|
|
285
|
+
copy_of_self = MasterPiece.instantiate(self.get_class_id())
|
|
286
|
+
copy_of_self.from_dict(data)
|
|
287
|
+
return copy_of_self
|
|
288
|
+
|
|
289
|
+
def quantize(self, quanta: float, value: float):
|
|
290
|
+
"""Quantize the given value.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
quanta (float): resolution for quantization
|
|
294
|
+
value (float): value to be quantized
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
(float): quantized value
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
::
|
|
301
|
+
|
|
302
|
+
hour_of_a_day = self.quantize(3600, epoch_seconds)
|
|
303
|
+
"""
|
|
304
|
+
return (value // quanta) * quanta
|
|
305
|
+
|
|
306
|
+
def epoc2utc(self, epoch):
|
|
307
|
+
"""Converts the given epoch time to UTC time string. All time
|
|
308
|
+
coordinates are represented in UTC time. This allows the time
|
|
309
|
+
coordinate to be mapped to any local time representation without
|
|
310
|
+
ambiguity.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
epoch (float) : timestamp in UTC time
|
|
314
|
+
rc (str): time string describing date, time and time zone e.g 2024-07-08T12:10:22Z
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
UTC time
|
|
318
|
+
"""
|
|
319
|
+
utc_time = datetime.fromtimestamp(epoch, timezone.utc)
|
|
320
|
+
utc_timestr = utc_time.strftime("%Y-%m-%dT%H:%M:%S") + "Z"
|
|
321
|
+
return utc_timestr
|
|
322
|
+
|
|
323
|
+
def timestampstr(self, ts):
|
|
324
|
+
"""Converts the given timestamp to human readable string of format 'Y-m-d
|
|
325
|
+
H:M:S'.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
ts (timestamp): time stamp to be converted
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
rc (string): human readable date-time string
|
|
332
|
+
"""
|
|
333
|
+
return str(datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S"))
|
|
334
|
+
|
|
335
|
+
def timestamp(self):
|
|
336
|
+
"""Returns the current date-time in UTC.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
rc (datetime): datetime in UTC.
|
|
340
|
+
"""
|
|
341
|
+
return datetime.now(timezone.utc).timestamp()
|
|
342
|
+
|
|
343
|
+
def is_time_between(self, begin_time, end_time, check_time=None):
|
|
344
|
+
"""Check if the given time is within the given time line. All
|
|
345
|
+
timestamps must be in UTC time.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
begin_time (timestamp): beginning of the timeline
|
|
349
|
+
end_time (timestamp): end of the timeline
|
|
350
|
+
check_time (timestamp): time to be checked
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
rc (bool): True if within the timeline
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
check_time = check_time or datetime.utcnow().time()
|
|
357
|
+
if begin_time < end_time:
|
|
358
|
+
return check_time >= begin_time and check_time <= end_time
|
|
359
|
+
else: # crosses midnight
|
|
360
|
+
return check_time >= begin_time or check_time <= end_time
|
|
361
|
+
|
|
362
|
+
@classmethod
|
|
363
|
+
def classattrs_to_dict(cls):
|
|
364
|
+
"""Convert class attributes to a dictionary."""
|
|
365
|
+
return {
|
|
366
|
+
attr: getattr(cls, attr)
|
|
367
|
+
for attr in cls.__dict__
|
|
368
|
+
if not callable(getattr(cls, attr))
|
|
369
|
+
and not attr.startswith("__")
|
|
370
|
+
and not attr.startswith(("_"))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def classattrs_from_dict(cls, attributes):
|
|
375
|
+
"""Set class attributes from a dictionary."""
|
|
376
|
+
for key, value in attributes.items():
|
|
377
|
+
setattr(cls, key, value)
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def save_to_json(cls):
|
|
381
|
+
"""Create class configuration file, if the file does not exist yet."""
|
|
382
|
+
filename = cls.get_json_file()
|
|
383
|
+
if not os.path.exists(filename):
|
|
384
|
+
with open(cls.get_json_file(), "w") as f:
|
|
385
|
+
json.dump(cls.classattrs_to_dict(), f)
|
|
386
|
+
if cls._log is not None:
|
|
387
|
+
cls._log.info(f"Configuration file {filename} created")
|
|
388
|
+
|
|
389
|
+
@classmethod
|
|
390
|
+
def load_from_json(cls):
|
|
391
|
+
"""Load class attributes from a JSON file."""
|
|
392
|
+
try:
|
|
393
|
+
filename = cls.get_json_file()
|
|
394
|
+
with open(filename, "r") as f:
|
|
395
|
+
attributes = json.load(f)
|
|
396
|
+
cls.classattrs_from_dict(attributes)
|
|
397
|
+
except FileNotFoundError:
|
|
398
|
+
if cls._log is not None:
|
|
399
|
+
cls._log.info(f"No configuration file {filename} found")
|
|
400
|
+
|
|
401
|
+
@classmethod
|
|
402
|
+
def register_class(cls, class_id: str, ctor: Callable):
|
|
403
|
+
"""Register the given class identifier for identifier based
|
|
404
|
+
instantiation . This, factory method pattern, as it is called,
|
|
405
|
+
decouples the actual implementation from the interface. For more
|
|
406
|
+
information see :meth:`instantiate` method.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
class_id (str): class identifier
|
|
410
|
+
ctor (function): constructor
|
|
411
|
+
"""
|
|
412
|
+
cls._factory[class_id] = ctor
|
|
413
|
+
|
|
414
|
+
@classmethod
|
|
415
|
+
def instantiate(cls, class_id: str) -> object:
|
|
416
|
+
"""Create an instance of the class corresponding to the given class identifier.
|
|
417
|
+
This method implements the factory method pattern, which is essential for a plugin architecture.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
class_id (int): Identifier of the class to instantiate.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
obj: An instance of the class corresponding to the given class identifier.
|
|
424
|
+
"""
|
|
425
|
+
if class_id in cls._factory:
|
|
426
|
+
return cls._factory[class_id]()
|
|
427
|
+
else:
|
|
428
|
+
raise ValueError(f"Attempting to instantiate unregistered class {class_id}")
|
|
429
|
+
|
|
430
|
+
@classmethod
|
|
431
|
+
def find_class(cls, class_id: str) -> object:
|
|
432
|
+
"""Given class identifier find the registered class. If no class with
|
|
433
|
+
the give identifier exists return None.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
class_id (int): class identifier
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
obj (obj): class or null if not registered
|
|
440
|
+
"""
|
|
441
|
+
if class_id in cls._factory:
|
|
442
|
+
return cls._factory[class_id]
|
|
443
|
+
else:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
@classmethod
|
|
447
|
+
def instantiate_with_param(cls, class_id: str, param: Any):
|
|
448
|
+
"""Given class identifier and one constructor argument create the
|
|
449
|
+
corresponding object.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
class_id : class identifier
|
|
453
|
+
param : class specific constructor parameter
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
obj : instance of the given class.
|
|
457
|
+
"""
|
|
458
|
+
return cls._factory[class_id](param)
|
|
459
|
+
|
|
460
|
+
@classmethod
|
|
461
|
+
def parse_args(cls) -> None:
|
|
462
|
+
"""Parse the startup arguments defined by this class."""
|
|
463
|
+
parser = argparse.ArgumentParser(description=cls.get_class_id())
|
|
464
|
+
parser.add_argument(
|
|
465
|
+
"--config-folder",
|
|
466
|
+
type=str,
|
|
467
|
+
help="The folder from which to load configuration files",
|
|
468
|
+
)
|
|
469
|
+
args = parser.parse_args()
|
|
470
|
+
|
|
471
|
+
if args is not None and args.config_folder:
|
|
472
|
+
cls.config_folder = args.config_folder
|
|
473
|
+
|
|
474
|
+
@classmethod
|
|
475
|
+
def register(cls) -> None:
|
|
476
|
+
"""Register the class to the class database.
|
|
477
|
+
|
|
478
|
+
Once registered the class can be instantiated by its class
|
|
479
|
+
identifier. Note that this method is called automatically by the
|
|
480
|
+
system when the python loads the class. In this method sub
|
|
481
|
+
classes should prepare themselves for instantiation by
|
|
482
|
+
initializing their class attributes, for example.
|
|
483
|
+
"""
|
|
484
|
+
if cls._class_id == "":
|
|
485
|
+
cls._class_id = cls.__name__
|
|
486
|
+
cls.parse_args()
|
|
487
|
+
if not cls.is_abstract():
|
|
488
|
+
cls.register_class(cls.get_class_id(), cls)
|
|
489
|
+
|
|
490
|
+
# load class attributes from the configuration file
|
|
491
|
+
cls.load_from_json()
|
|
492
|
+
# automatically create configuration file, if not created already
|
|
493
|
+
atexit.register(cls.save_to_json)
|