splight-lib 4.3.7__tar.gz → 4.4.0__tar.gz
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.
- {splight_lib-4.3.7 → splight_lib-4.4.0}/PKG-INFO +24 -7
- splight_lib-4.4.0/README.md +33 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/pyproject.toml +2 -1
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/abstract.py +14 -137
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/spec.py +1 -0
- splight_lib-4.4.0/splight_lib/execution/__init__.py +12 -0
- splight_lib-4.4.0/splight_lib/execution/engine.py +101 -0
- splight_lib-4.4.0/splight_lib/execution/exceptions.py +9 -0
- splight_lib-4.4.0/splight_lib/execution/scheduling.py +92 -0
- splight_lib-4.4.0/splight_lib/execution/task.py +80 -0
- splight_lib-4.4.0/splight_lib/execution/tests/test_execution.py +41 -0
- splight_lib-4.4.0/splight_lib/execution/tests/test_scheduling.py +85 -0
- splight_lib-4.4.0/splight_lib/execution/trigger.py +44 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/component.py +8 -6
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/function.py +78 -16
- splight_lib-4.3.7/README.md +0 -17
- splight_lib-4.3.7/splight_lib/execution.py +0 -407
- splight_lib-4.3.7/splight_lib/tests/test_execution.py +0 -114
- {splight_lib-4.3.7 → splight_lib-4.4.0}/LICENSE.txt +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/abstract/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/abstract/client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/exceptions.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/mac_auth.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/token.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/abstract.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/classmap.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/exceptions.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/local_client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/remote_client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/abstract.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/builder.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/classmap.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/local_client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/remote_client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/abstract.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/builder.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/local_client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/remote_client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/exceptions.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/file_handler.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/filter.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/hub/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/hub/abstract.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/hub/client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_communication.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_database.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_datalake.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_hub.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/communication/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/communication/event_handler.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/communication/tests/test_event_handler.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/exceptions.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/tests/test_abstract.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/tests/test_spec.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/constants.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/encryption.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/_internal.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/component.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/constants.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/logging.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/tests/test_logging.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/alert.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/asset.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/attribute.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/base.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/communication.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/dashboard.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/data_address.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/event.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/exceptions.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/file.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/generic.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/hub.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/metadata.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/native.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/pipeline.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/secret.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/models.json +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_component_object_instance.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_database_model.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_metadata.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_models.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/client.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/exceptions.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/tests/test_restclient.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/types.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/settings.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/testing/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/tests/FakeProc.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/tests/test_encryption.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/utils/__init__.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/utils/custom_model.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/utils/hub.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/version.py +0 -0
- {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: splight-lib
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: Splight Library
|
|
5
5
|
Author: Splight Dev
|
|
6
6
|
Author-email: dev@splight-ae.com
|
|
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: apscheduler (==3.10.4)
|
|
13
14
|
Requires-Dist: concurrent-log-handler (==0.9.21)
|
|
14
15
|
Requires-Dist: cryptography (==35.0.0)
|
|
15
16
|
Requires-Dist: email-validator (>=2.1.0.post1,<3.0.0)
|
|
@@ -37,17 +38,33 @@ Requires-Dist: stringcase (==1.2.0)
|
|
|
37
38
|
Requires-Dist: typing-extensions (>=4.6.1,<5.0.0)
|
|
38
39
|
Description-Content-Type: text/markdown
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
# Splight Python SDK
|
|
41
42
|
|
|
42
|
-
##
|
|
43
|
+
## Installation
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
A release version can be installed using `pip` with
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
```bash
|
|
48
|
+
pip install --upgrade splight-lib
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
or if you want one particular version you can use
|
|
52
|
+
```bash
|
|
53
|
+
pip install splight-lib==x.y.z
|
|
54
|
+
```
|
|
47
55
|
|
|
48
|
-
For
|
|
56
|
+
### For Development
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
To install the library in development mode you need to have `poetry` installed
|
|
59
|
+
```bash
|
|
60
|
+
pip install poetry==1.7.0
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then to insall the library you can use the command
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
poetry install
|
|
67
|
+
```
|
|
51
68
|
|
|
52
69
|
## Tests
|
|
53
70
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Splight Python SDK
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
A release version can be installed using `pip` with
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install --upgrade splight-lib
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
or if you want one particular version you can use
|
|
12
|
+
```bash
|
|
13
|
+
pip install splight-lib==x.y.z
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### For Development
|
|
17
|
+
|
|
18
|
+
To install the library in development mode you need to have `poetry` installed
|
|
19
|
+
```bash
|
|
20
|
+
pip install poetry==1.7.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then to insall the library you can use the command
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
poetry install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tests
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
make test
|
|
33
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "splight-lib"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.4.0"
|
|
4
4
|
description = "Splight Library"
|
|
5
5
|
authors = ["Splight Dev <dev@splight-ae.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -32,6 +32,7 @@ stringcase = "1.2.0"
|
|
|
32
32
|
typing-extensions = "^4.6.1"
|
|
33
33
|
pydantic-settings = "2.0.3"
|
|
34
34
|
email-validator = "^2.1.0.post1"
|
|
35
|
+
apscheduler = "3.10.4"
|
|
35
36
|
|
|
36
37
|
[tool.poetry.group.dev.dependencies]
|
|
37
38
|
black = "23.3.0"
|
|
@@ -2,41 +2,21 @@ import os
|
|
|
2
2
|
import sys
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from collections import namedtuple
|
|
5
|
-
from functools import partial
|
|
6
5
|
from tempfile import NamedTemporaryFile
|
|
6
|
+
from threading import Thread
|
|
7
7
|
from time import sleep
|
|
8
|
-
from typing import Callable, Dict,
|
|
8
|
+
from typing import Callable, Dict, Optional, Type
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
|
-
from splight_lib.client.communication import RemoteCommunicationClient
|
|
13
|
-
from splight_lib.communication.event_handler import (
|
|
14
|
-
command_event_handler,
|
|
15
|
-
database_object_event_handler,
|
|
16
|
-
)
|
|
17
|
-
from splight_lib.component.exceptions import (
|
|
18
|
-
InvalidBidingObject,
|
|
19
|
-
MissingBindingCallback,
|
|
20
|
-
MissingCommandCallback,
|
|
21
|
-
MissingRoutineCallback,
|
|
22
|
-
)
|
|
23
12
|
from splight_lib.component.spec import Spec
|
|
24
|
-
|
|
25
|
-
# TODO: Use builder pattern
|
|
26
|
-
from splight_lib.execution import ExecutionClient, Thread
|
|
13
|
+
from splight_lib.execution.engine import EngineStatus, ExecutionEngine
|
|
27
14
|
from splight_lib.logging._internal import LogTags, get_splight_logger
|
|
28
15
|
from splight_lib.models.component import (
|
|
29
|
-
DB_MODEL_TYPE_MAPPING,
|
|
30
|
-
Binding,
|
|
31
|
-
Command,
|
|
32
16
|
ComponentObjectInstance,
|
|
33
|
-
Routine,
|
|
34
|
-
RoutineObject,
|
|
35
17
|
RoutineObjectInstance,
|
|
36
18
|
)
|
|
37
|
-
from splight_lib.models.event import EventNames
|
|
38
19
|
from splight_lib.restclient import ConnectError, HTTPError, Timeout
|
|
39
|
-
from splight_lib.settings import settings
|
|
40
20
|
|
|
41
21
|
REQUEST_EXCEPTIONS = (ConnectError, HTTPError, Timeout)
|
|
42
22
|
logger = get_splight_logger("Base Component")
|
|
@@ -47,7 +27,7 @@ class HealthCheckProcessor:
|
|
|
47
27
|
_HEALTH_FILE_PREFIX = "healthy_"
|
|
48
28
|
_STARTUP_FILE_PREFIX = "ready_"
|
|
49
29
|
|
|
50
|
-
def __init__(self, engine:
|
|
30
|
+
def __init__(self, engine: ExecutionEngine):
|
|
51
31
|
self._logger = get_splight_logger("HealthCheckProcessor")
|
|
52
32
|
self._engine = engine
|
|
53
33
|
self._health_file = NamedTemporaryFile(prefix=self._HEALTH_FILE_PREFIX)
|
|
@@ -68,11 +48,9 @@ class HealthCheckProcessor:
|
|
|
68
48
|
|
|
69
49
|
def start(self):
|
|
70
50
|
self._running = True
|
|
51
|
+
# Check when there is no task in engine
|
|
71
52
|
while self._running:
|
|
72
|
-
|
|
73
|
-
if not is_alive:
|
|
74
|
-
exc = self._engine.get_last_exception()
|
|
75
|
-
self._log_exception(exc)
|
|
53
|
+
if not self._engine.running:
|
|
76
54
|
self._logger.info("Healthcheck finished", tags=LogTags.RUNTIME)
|
|
77
55
|
self._health_file.close()
|
|
78
56
|
self._logger.info(
|
|
@@ -86,13 +64,6 @@ class HealthCheckProcessor:
|
|
|
86
64
|
def stop(self):
|
|
87
65
|
self._running = False
|
|
88
66
|
|
|
89
|
-
def _log_exception(self, exc: Optional[Exception]) -> None:
|
|
90
|
-
"""Logs the exception and the traceback."""
|
|
91
|
-
if exc:
|
|
92
|
-
stack = exc.__traceback__
|
|
93
|
-
exc_type = type(exc)
|
|
94
|
-
self._logger.exception(exc, exc_info=(exc_type, exc, stack))
|
|
95
|
-
|
|
96
67
|
|
|
97
68
|
class SplightBaseComponent(ABC):
|
|
98
69
|
def __init__(
|
|
@@ -100,18 +71,12 @@ class SplightBaseComponent(ABC):
|
|
|
100
71
|
component_id: Optional[str] = None,
|
|
101
72
|
):
|
|
102
73
|
self._component_id = component_id
|
|
103
|
-
|
|
104
|
-
# TODO: Change to use builder patter
|
|
105
|
-
self._comm_client = RemoteCommunicationClient(
|
|
106
|
-
url=settings.SPLIGHT_PLATFORM_API_HOST,
|
|
107
|
-
access_id=settings.SPLIGHT_ACCESS_ID,
|
|
108
|
-
secret_key=settings.SPLIGHT_SECRET_KEY,
|
|
109
|
-
instance_id=component_id,
|
|
110
|
-
)
|
|
111
|
-
self._execution_engine = ExecutionClient()
|
|
74
|
+
self._execution_engine = ExecutionEngine()
|
|
112
75
|
self._health_check = HealthCheckProcessor(self._execution_engine)
|
|
113
76
|
self._health_check_thread = Thread(
|
|
114
|
-
target=self._health_check.start,
|
|
77
|
+
target=self._health_check.start,
|
|
78
|
+
args=(),
|
|
79
|
+
daemon=False,
|
|
115
80
|
)
|
|
116
81
|
|
|
117
82
|
self._spec = self._load_spec()
|
|
@@ -130,16 +95,6 @@ class SplightBaseComponent(ABC):
|
|
|
130
95
|
)
|
|
131
96
|
for routine in self._spec.routines
|
|
132
97
|
}
|
|
133
|
-
|
|
134
|
-
# TODO: check if we are going to still use binding
|
|
135
|
-
bindings = [
|
|
136
|
-
binding
|
|
137
|
-
for binding in self._spec.bindings
|
|
138
|
-
if binding.object_type != "SetPoint"
|
|
139
|
-
]
|
|
140
|
-
self._load_bindings(bindings, component_objects)
|
|
141
|
-
self._load_routines(self._spec.routines, routine_objects)
|
|
142
|
-
self._load_commands(self._spec.commands)
|
|
143
98
|
self._custom_types = self._get_custom_type_model(component_objects)
|
|
144
99
|
self._routines = self._get_routine_model(routine_objects)
|
|
145
100
|
|
|
@@ -162,11 +117,11 @@ class SplightBaseComponent(ABC):
|
|
|
162
117
|
return self._custom_types
|
|
163
118
|
|
|
164
119
|
@property
|
|
165
|
-
def execution_engine(self) ->
|
|
120
|
+
def execution_engine(self) -> ExecutionEngine:
|
|
166
121
|
return self._execution_engine
|
|
167
122
|
|
|
168
123
|
def _register_exit(self):
|
|
169
|
-
if self._execution_engine.
|
|
124
|
+
if self._execution_engine.state == EngineStatus.FAILED:
|
|
170
125
|
sys.exit(1)
|
|
171
126
|
else:
|
|
172
127
|
sys.exit(0)
|
|
@@ -198,12 +153,9 @@ class SplightBaseComponent(ABC):
|
|
|
198
153
|
except Exception as exc:
|
|
199
154
|
logger.exception(exc, tags=LogTags.COMPONENT)
|
|
200
155
|
self._health_check.stop()
|
|
201
|
-
self._execution_engine.
|
|
156
|
+
self._execution_engine.stop()
|
|
202
157
|
sys.exit(1)
|
|
203
158
|
|
|
204
|
-
for thread in self._execution_engine.threads:
|
|
205
|
-
thread.join()
|
|
206
|
-
|
|
207
159
|
self._health_check.stop()
|
|
208
160
|
self._health_check_thread.join()
|
|
209
161
|
self._register_exit()
|
|
@@ -234,85 +186,10 @@ class SplightBaseComponent(ABC):
|
|
|
234
186
|
spec = Spec.from_file(os.path.join(base_path, "spec.json"))
|
|
235
187
|
return spec
|
|
236
188
|
|
|
237
|
-
def _load_bindings(
|
|
238
|
-
self,
|
|
239
|
-
bindings: List[Binding],
|
|
240
|
-
component_objects: Dict[str, Type[ComponentObjectInstance]],
|
|
241
|
-
):
|
|
242
|
-
"""Loads and assigns callbacks for the bindings."""
|
|
243
|
-
for binding in bindings:
|
|
244
|
-
type_ = binding.object_type
|
|
245
|
-
model_class = DB_MODEL_TYPE_MAPPING.get(
|
|
246
|
-
type_, component_objects.get(type_)
|
|
247
|
-
)
|
|
248
|
-
if not model_class:
|
|
249
|
-
raise InvalidBidingObject(model_class.__name__)
|
|
250
|
-
event_name = model_class.get_event_name(
|
|
251
|
-
model_class.__name__, binding.object_action
|
|
252
|
-
)
|
|
253
|
-
callback_func = getattr(self, binding.name, None)
|
|
254
|
-
if not callback_func:
|
|
255
|
-
raise MissingBindingCallback(binding.name)
|
|
256
|
-
|
|
257
|
-
self._comm_client.bind(
|
|
258
|
-
event_name,
|
|
259
|
-
partial(
|
|
260
|
-
database_object_event_handler,
|
|
261
|
-
callback_func,
|
|
262
|
-
model_class,
|
|
263
|
-
),
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
def _load_routines(
|
|
267
|
-
self,
|
|
268
|
-
routines: List[Routine],
|
|
269
|
-
routines_objects: Dict[str, Type[RoutineObjectInstance]],
|
|
270
|
-
) -> None:
|
|
271
|
-
"""Loads and assigns callbacks to the routines."""
|
|
272
|
-
|
|
273
|
-
actions = ["create", "update", "delete"]
|
|
274
|
-
for routine in routines:
|
|
275
|
-
model_calss = routines_objects.get(routine.name)
|
|
276
|
-
|
|
277
|
-
for action in actions:
|
|
278
|
-
event_name = RoutineObject.get_event_name(
|
|
279
|
-
model_calss.__name__, action
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
callback_func = getattr(
|
|
283
|
-
self, getattr(routine, f"{action}_handler"), None
|
|
284
|
-
)
|
|
285
|
-
if not callback_func:
|
|
286
|
-
raise MissingRoutineCallback(routine.name, action)
|
|
287
|
-
self._comm_client.bind(
|
|
288
|
-
event_name,
|
|
289
|
-
partial(
|
|
290
|
-
database_object_event_handler,
|
|
291
|
-
callback_func,
|
|
292
|
-
model_calss,
|
|
293
|
-
),
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
def _load_commands(self, commands: List[Command]):
|
|
297
|
-
"""Assigns callbacks function to each of the defined commands."""
|
|
298
|
-
for command in commands:
|
|
299
|
-
callback_func = getattr(self, command.name.lower(), None)
|
|
300
|
-
if not callback_func:
|
|
301
|
-
raise MissingCommandCallback(command.name)
|
|
302
|
-
event_name = (EventNames.COMPONENTCOMMAND_TRIGGER.value,)
|
|
303
|
-
self._comm_client.bind(
|
|
304
|
-
event_name,
|
|
305
|
-
partial(
|
|
306
|
-
command_event_handler,
|
|
307
|
-
callback_func,
|
|
308
|
-
self._comm_client,
|
|
309
|
-
),
|
|
310
|
-
)
|
|
311
|
-
|
|
312
189
|
@abstractmethod
|
|
313
190
|
def start(self):
|
|
314
191
|
raise NotImplementedError()
|
|
315
192
|
|
|
316
193
|
def stop(self):
|
|
317
|
-
self._execution_engine.
|
|
194
|
+
self._execution_engine.stop()
|
|
318
195
|
sys.exit(1)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from splight_lib.execution.engine import ExecutionEngine
|
|
2
|
+
from splight_lib.execution.scheduling import Crontab, TaskPeriod
|
|
3
|
+
from splight_lib.execution.task import CronnedTask, PeriodicTask, Task
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
ExecutionEngine,
|
|
7
|
+
Crontab,
|
|
8
|
+
TaskPeriod,
|
|
9
|
+
CronnedTask,
|
|
10
|
+
PeriodicTask,
|
|
11
|
+
Task,
|
|
12
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from typing import Set
|
|
3
|
+
|
|
4
|
+
import pytz
|
|
5
|
+
from apscheduler.events import EVENT_JOB_ERROR, JobExecutionEvent
|
|
6
|
+
from apscheduler.schedulers.background import BackgroundScheduler
|
|
7
|
+
from apscheduler.schedulers.blocking import BlockingScheduler
|
|
8
|
+
|
|
9
|
+
from splight_lib.execution.task import BaseTask
|
|
10
|
+
from splight_lib.logging._internal import LogTags, get_splight_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EngineStatus(Enum):
|
|
14
|
+
RUNNING = auto()
|
|
15
|
+
STOPPED = auto()
|
|
16
|
+
FINISHED = auto()
|
|
17
|
+
FAILED = auto()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExecutionEngine:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._logger = get_splight_logger("ExecutionEngine")
|
|
23
|
+
self._logger.info(
|
|
24
|
+
"Execution client initialized.", tags=LogTags.RUNTIME
|
|
25
|
+
)
|
|
26
|
+
self._blocking_sch = BlockingScheduler(timezone=pytz.UTC)
|
|
27
|
+
self._background_sch = BackgroundScheduler(
|
|
28
|
+
timezone=pytz.UTC, daemon=True
|
|
29
|
+
)
|
|
30
|
+
self._critical_jobs: Set[str] = set()
|
|
31
|
+
self._blocking_sch.add_listener(
|
|
32
|
+
self._task_fail_callbak, EVENT_JOB_ERROR
|
|
33
|
+
)
|
|
34
|
+
self._background_sch.add_listener(
|
|
35
|
+
self._task_fail_callbak, EVENT_JOB_ERROR
|
|
36
|
+
)
|
|
37
|
+
self._running = False
|
|
38
|
+
self._state = EngineStatus.STOPPED
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def running(self) -> bool:
|
|
42
|
+
return self._running
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def state(self) -> EngineStatus:
|
|
46
|
+
return self._state
|
|
47
|
+
|
|
48
|
+
def start(self):
|
|
49
|
+
"""Starts the the schedulers."""
|
|
50
|
+
self._running = True
|
|
51
|
+
self._state = EngineStatus.RUNNING
|
|
52
|
+
if self._background_sch.get_jobs():
|
|
53
|
+
self._background_sch.start()
|
|
54
|
+
if self._blocking_sch.get_jobs():
|
|
55
|
+
self._blocking_sch.start()
|
|
56
|
+
self._logger.info("Execution Engine started", tags=LogTags.RUNTIME)
|
|
57
|
+
|
|
58
|
+
def stop(self):
|
|
59
|
+
"""Stops all the schedulers and its task without waiting to finish."""
|
|
60
|
+
if self._blocking_sch.running:
|
|
61
|
+
self._blocking_sch.shutdown(wait=False)
|
|
62
|
+
if self._background_sch.running:
|
|
63
|
+
self._background_sch.shutdown(wait=False)
|
|
64
|
+
self._running = False
|
|
65
|
+
self._state = EngineStatus.STOPPED
|
|
66
|
+
self._logger.info("Execution Engine stopped", tags=LogTags.RUNTIME)
|
|
67
|
+
|
|
68
|
+
def add_task(
|
|
69
|
+
self,
|
|
70
|
+
task: BaseTask,
|
|
71
|
+
in_background: bool = True,
|
|
72
|
+
exit_on_fail: bool = False,
|
|
73
|
+
):
|
|
74
|
+
"""Adds new task to the corresponding scheduler.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
task: BaseTask
|
|
79
|
+
Instance of Task to be scheduled.
|
|
80
|
+
in_background: bool
|
|
81
|
+
Wheter to run the task using the BackgroundScheduler or the
|
|
82
|
+
BlockingScheduler
|
|
83
|
+
exit_on_fail: bool
|
|
84
|
+
Used to stop the engine if the task execution failed. This
|
|
85
|
+
parameter is usefull to declare critical tasks.
|
|
86
|
+
"""
|
|
87
|
+
if in_background:
|
|
88
|
+
job = self._background_sch.add_job(**task.as_job())
|
|
89
|
+
else:
|
|
90
|
+
job = self._blocking_sch.add_job(**task.as_job())
|
|
91
|
+
if exit_on_fail:
|
|
92
|
+
self._critical_jobs.add(job.id)
|
|
93
|
+
self._logger.info(f"Job {job.id} added", tags=LogTags.RUNTIME)
|
|
94
|
+
|
|
95
|
+
def _task_fail_callbak(self, event: JobExecutionEvent):
|
|
96
|
+
if event.job_id in self._critical_jobs:
|
|
97
|
+
self._logger.error(
|
|
98
|
+
"An error ocurred in job execution. Stopping engine"
|
|
99
|
+
)
|
|
100
|
+
self.stop()
|
|
101
|
+
self._state = EngineStatus.FAILED
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ValidationInfo, field_validator
|
|
5
|
+
|
|
6
|
+
from splight_lib.execution.exceptions import InvalidCronString
|
|
7
|
+
|
|
8
|
+
FIELD_RANGE = {
|
|
9
|
+
"month": (1, 12),
|
|
10
|
+
"day": (1, 31),
|
|
11
|
+
"week": (1, 53),
|
|
12
|
+
"day_of_week": (0, 6),
|
|
13
|
+
"hour": (0, 23),
|
|
14
|
+
"minute": (0, 59),
|
|
15
|
+
"second": (0, 59),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_value_in_range(
|
|
20
|
+
value: Union[int, str], min_value: int, max_value: int, name: str
|
|
21
|
+
):
|
|
22
|
+
if isinstance(value, str):
|
|
23
|
+
if "*" in value:
|
|
24
|
+
return value
|
|
25
|
+
elif value.isdigit():
|
|
26
|
+
value = int(value)
|
|
27
|
+
if not min_value <= value <= max_value:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"{name}'s value must be in range {min_value}-{max_value}"
|
|
30
|
+
)
|
|
31
|
+
elif isinstance(value, int):
|
|
32
|
+
if not min_value <= value <= max_value:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"{name}'s Value must be in range {min_value}-{max_value}"
|
|
35
|
+
)
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TaskPeriod(BaseModel):
|
|
40
|
+
weeks: int = 0
|
|
41
|
+
days: int = 0
|
|
42
|
+
hours: int = 0
|
|
43
|
+
minutes: int = 0
|
|
44
|
+
seconds: int = 0
|
|
45
|
+
start_date: datetime = datetime.now(timezone.utc)
|
|
46
|
+
end_date: Optional[datetime] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Crontab(BaseModel):
|
|
50
|
+
year: Optional[Union[str, int]] = None
|
|
51
|
+
month: Optional[Union[str, int]] = None
|
|
52
|
+
day: Optional[Union[str, int]] = None
|
|
53
|
+
week: Optional[Union[str, int]] = None
|
|
54
|
+
day_of_week: Optional[Union[str, int]] = None
|
|
55
|
+
hour: Optional[Union[str, int]] = None
|
|
56
|
+
minute: Optional[Union[str, int]] = None
|
|
57
|
+
second: Optional[Union[str, int]] = None
|
|
58
|
+
|
|
59
|
+
@field_validator(
|
|
60
|
+
"month", "day", "week", "day_of_week", "hour", "minute", "second"
|
|
61
|
+
)
|
|
62
|
+
def validate_minute(cls, value: str, info: ValidationInfo):
|
|
63
|
+
min_value, max_value = FIELD_RANGE[info.field_name]
|
|
64
|
+
return validate_value_in_range(
|
|
65
|
+
value, min_value, max_value, info.field_name
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_string(cls, cron_str: str) -> "Crontab":
|
|
70
|
+
"""Converts a crontab string into a Crontab instance.
|
|
71
|
+
Since crontab by default uses 5 fields, seconds and years are set to
|
|
72
|
+
None
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
cron_str : str
|
|
77
|
+
The crontab string to convert
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
Crontab instance
|
|
82
|
+
"""
|
|
83
|
+
elements = cron_str.split(" ")
|
|
84
|
+
if len(elements) != 5:
|
|
85
|
+
raise InvalidCronString(cron_str)
|
|
86
|
+
return cls(
|
|
87
|
+
minute=elements[0],
|
|
88
|
+
hour=elements[1],
|
|
89
|
+
day=elements[2],
|
|
90
|
+
month=elements[3],
|
|
91
|
+
day_of_week=elements[4],
|
|
92
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Callable, Dict, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import pytz
|
|
5
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
6
|
+
|
|
7
|
+
from splight_lib.execution.scheduling import Crontab, TaskPeriod
|
|
8
|
+
from splight_lib.execution.trigger import IntervalTrigger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseTask(ABC):
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def as_job(self) -> Dict:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PeriodicTask(BaseTask):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
target: Callable,
|
|
21
|
+
period: Union[TaskPeriod, int],
|
|
22
|
+
target_args: Optional[Tuple] = None,
|
|
23
|
+
):
|
|
24
|
+
self._target = target
|
|
25
|
+
self._args = target_args
|
|
26
|
+
if isinstance(period, TaskPeriod):
|
|
27
|
+
self._trigger = IntervalTrigger(
|
|
28
|
+
**period.model_dump(), timezone=pytz.UTC
|
|
29
|
+
)
|
|
30
|
+
elif isinstance(period, int):
|
|
31
|
+
self._trigger = IntervalTrigger(seconds=period, timezone=pytz.UTC)
|
|
32
|
+
|
|
33
|
+
def as_job(self) -> dict:
|
|
34
|
+
job_dict = {
|
|
35
|
+
"func": self._target,
|
|
36
|
+
"trigger": self._trigger,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if self._args:
|
|
40
|
+
job_dict.update({"args": self._args})
|
|
41
|
+
|
|
42
|
+
return job_dict
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Task = PeriodicTask
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CronnedTask(BaseTask):
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
target: Callable,
|
|
52
|
+
crontab: Crontab,
|
|
53
|
+
target_args: Optional[Tuple] = None,
|
|
54
|
+
):
|
|
55
|
+
self._target = target
|
|
56
|
+
self._trigger = CronTrigger(
|
|
57
|
+
**crontab.model_dump(exclude_none=True), timezone=pytz.UTC
|
|
58
|
+
)
|
|
59
|
+
self._args = target_args
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_cron_string(
|
|
63
|
+
cls,
|
|
64
|
+
target: Callable,
|
|
65
|
+
cron_str: str,
|
|
66
|
+
target_args: Optional[Tuple] = None,
|
|
67
|
+
):
|
|
68
|
+
crontab = Crontab.from_string(cron_str)
|
|
69
|
+
return cls(target, crontab, target_args)
|
|
70
|
+
|
|
71
|
+
def as_job(self) -> dict:
|
|
72
|
+
job_dict = {
|
|
73
|
+
"func": self._target,
|
|
74
|
+
"trigger": self._trigger,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if self._args:
|
|
78
|
+
job_dict.update({"args": self._args})
|
|
79
|
+
|
|
80
|
+
return job_dict
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from time import sleep
|
|
2
|
+
|
|
3
|
+
from splight_lib.execution.engine import EngineStatus, ExecutionEngine
|
|
4
|
+
from splight_lib.execution.task import PeriodicTask
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_task_with_error():
|
|
8
|
+
def task_function():
|
|
9
|
+
sleep(1)
|
|
10
|
+
print("This is a function")
|
|
11
|
+
raise ValueError()
|
|
12
|
+
|
|
13
|
+
task = PeriodicTask(target=task_function, period=1)
|
|
14
|
+
|
|
15
|
+
engine = ExecutionEngine()
|
|
16
|
+
assert engine.state == EngineStatus.STOPPED
|
|
17
|
+
|
|
18
|
+
engine.add_task(task, in_background=False, exit_on_fail=True)
|
|
19
|
+
|
|
20
|
+
engine.start()
|
|
21
|
+
assert engine.state == EngineStatus.FAILED
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_task_with_error_on_background():
|
|
25
|
+
def task_function():
|
|
26
|
+
sleep(1)
|
|
27
|
+
print("This is a function")
|
|
28
|
+
raise ValueError()
|
|
29
|
+
|
|
30
|
+
task = PeriodicTask(target=task_function, period=1)
|
|
31
|
+
|
|
32
|
+
engine = ExecutionEngine()
|
|
33
|
+
assert engine.state == EngineStatus.STOPPED
|
|
34
|
+
|
|
35
|
+
engine.add_task(task, in_background=True, exit_on_fail=True)
|
|
36
|
+
|
|
37
|
+
engine.start()
|
|
38
|
+
assert engine.state == EngineStatus.RUNNING
|
|
39
|
+
|
|
40
|
+
sleep(2)
|
|
41
|
+
assert engine.state == EngineStatus.FAILED
|