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.
Files changed (105) hide show
  1. {splight_lib-4.3.7 → splight_lib-4.4.0}/PKG-INFO +24 -7
  2. splight_lib-4.4.0/README.md +33 -0
  3. {splight_lib-4.3.7 → splight_lib-4.4.0}/pyproject.toml +2 -1
  4. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/abstract.py +14 -137
  5. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/spec.py +1 -0
  6. splight_lib-4.4.0/splight_lib/execution/__init__.py +12 -0
  7. splight_lib-4.4.0/splight_lib/execution/engine.py +101 -0
  8. splight_lib-4.4.0/splight_lib/execution/exceptions.py +9 -0
  9. splight_lib-4.4.0/splight_lib/execution/scheduling.py +92 -0
  10. splight_lib-4.4.0/splight_lib/execution/task.py +80 -0
  11. splight_lib-4.4.0/splight_lib/execution/tests/test_execution.py +41 -0
  12. splight_lib-4.4.0/splight_lib/execution/tests/test_scheduling.py +85 -0
  13. splight_lib-4.4.0/splight_lib/execution/trigger.py +44 -0
  14. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/component.py +8 -6
  15. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/function.py +78 -16
  16. splight_lib-4.3.7/README.md +0 -17
  17. splight_lib-4.3.7/splight_lib/execution.py +0 -407
  18. splight_lib-4.3.7/splight_lib/tests/test_execution.py +0 -114
  19. {splight_lib-4.3.7 → splight_lib-4.4.0}/LICENSE.txt +0 -0
  20. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/__init__.py +0 -0
  21. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/abstract/__init__.py +0 -0
  22. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/abstract/client.py +0 -0
  23. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/__init__.py +0 -0
  24. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/exceptions.py +0 -0
  25. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/mac_auth.py +0 -0
  26. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/auth/token.py +0 -0
  27. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/__init__.py +0 -0
  28. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/__init__.py +0 -0
  29. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/abstract.py +0 -0
  30. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/classmap.py +0 -0
  31. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/exceptions.py +0 -0
  32. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/local_client.py +0 -0
  33. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/communication/remote_client.py +0 -0
  34. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/__init__.py +0 -0
  35. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/abstract.py +0 -0
  36. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/builder.py +0 -0
  37. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/classmap.py +0 -0
  38. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/local_client.py +0 -0
  39. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/database/remote_client.py +0 -0
  40. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/__init__.py +0 -0
  41. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/abstract.py +0 -0
  42. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/builder.py +0 -0
  43. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/local_client.py +0 -0
  44. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/datalake/remote_client.py +0 -0
  45. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/exceptions.py +0 -0
  46. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/file_handler.py +0 -0
  47. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/filter.py +0 -0
  48. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/hub/__init__.py +0 -0
  49. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/hub/abstract.py +0 -0
  50. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/hub/client.py +0 -0
  51. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_communication.py +0 -0
  52. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_database.py +0 -0
  53. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_datalake.py +0 -0
  54. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/client/tests/test_hub.py +0 -0
  55. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/communication/__init__.py +0 -0
  56. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/communication/event_handler.py +0 -0
  57. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/communication/tests/test_event_handler.py +0 -0
  58. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/__init__.py +0 -0
  59. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/exceptions.py +0 -0
  60. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/tests/test_abstract.py +0 -0
  61. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/component/tests/test_spec.py +0 -0
  62. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/constants.py +0 -0
  63. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/encryption.py +0 -0
  64. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/__init__.py +0 -0
  65. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/_internal.py +0 -0
  66. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/component.py +0 -0
  67. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/constants.py +0 -0
  68. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/logging.py +0 -0
  69. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/logging/tests/test_logging.py +0 -0
  70. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/__init__.py +0 -0
  71. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/alert.py +0 -0
  72. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/asset.py +0 -0
  73. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/attribute.py +0 -0
  74. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/base.py +0 -0
  75. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/communication.py +0 -0
  76. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/dashboard.py +0 -0
  77. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/data_address.py +0 -0
  78. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/event.py +0 -0
  79. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/exceptions.py +0 -0
  80. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/file.py +0 -0
  81. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/generic.py +0 -0
  82. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/hub.py +0 -0
  83. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/metadata.py +0 -0
  84. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/native.py +0 -0
  85. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/pipeline.py +0 -0
  86. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/secret.py +0 -0
  87. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/models.json +0 -0
  88. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_component_object_instance.py +0 -0
  89. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_database_model.py +0 -0
  90. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_metadata.py +0 -0
  91. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/models/tests/test_models.py +0 -0
  92. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/__init__.py +0 -0
  93. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/client.py +0 -0
  94. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/exceptions.py +0 -0
  95. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/tests/test_restclient.py +0 -0
  96. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/restclient/types.py +0 -0
  97. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/settings.py +0 -0
  98. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/testing/__init__.py +0 -0
  99. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/tests/FakeProc.py +0 -0
  100. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/tests/test_encryption.py +0 -0
  101. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/utils/__init__.py +0 -0
  102. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/utils/custom_model.py +0 -0
  103. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/utils/hub.py +0 -0
  104. {splight_lib-4.3.7 → splight_lib-4.4.0}/splight_lib/version.py +0 -0
  105. {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.7
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
- [![Lib upload](https://github.com/splightplatform/splight-lib/actions/workflows/libupload.yml/badge.svg)](https://github.com/splightplatform/splight-lib/actions/workflows/libupload.yml)
41
+ # Splight Python SDK
41
42
 
42
- ## How to install
43
+ ## Installation
43
44
 
44
- For development
45
+ A release version can be installed using `pip` with
45
46
 
46
- - `make install`
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 productive envs.
56
+ ### For Development
49
57
 
50
- - `python setup.py install`
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.7"
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, List, Optional, Type
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: ExecutionClient):
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
- is_alive, status = self._engine.healthcheck()
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, args=(), daemon=False
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) -> ExecutionClient:
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.get_last_exception():
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.terminate_all()
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.terminate_all()
194
+ self._execution_engine.stop()
318
195
  sys.exit(1)
@@ -31,6 +31,7 @@ VALID_PARAMETER_VALUES = {
31
31
  "bool": bool,
32
32
  "str": str,
33
33
  "float": float,
34
+ "crontab": str,
34
35
  "url": AnyUrl,
35
36
  "datetime": None,
36
37
  "File": None, # UUID
@@ -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,9 @@
1
+ class InvalidCronString(Exception):
2
+ def __init__(self, cron_str: str):
3
+ self._msg = (
4
+ f"Invalid cron string: {cron_str}. "
5
+ f"Expected format: 'minute hour day_of_month month day_of_week'"
6
+ )
7
+
8
+ def __str__(self):
9
+ return self._msg
@@ -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