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 ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Description
3
+ ===========
4
+
5
+ Classes comprising the MasterPiece software framework.
6
+
7
+ """
8
+
9
+ from .masterpiece import MasterPiece
10
+ from .composite import Composite
11
+
12
+
13
+ __all__ = ["MasterPiece", "Composite"]
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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (71.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ __init__
2
+ composite
3
+ masterpiece
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)