splight-lib 4.0.0.dev5__tar.gz → 4.1.0.dev1__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 (87) hide show
  1. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/PKG-INFO +1 -1
  2. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/setup.py +1 -1
  3. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/datalake/abstract.py +25 -0
  4. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/datalake/remote_client.py +50 -0
  5. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/component/abstract.py +42 -21
  6. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/execution.py +46 -40
  7. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/asset.py +2 -0
  8. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/base.py +21 -0
  9. splight-lib-4.1.0.dev1/splight_lib/models/metadata.py +20 -0
  10. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/native.py +5 -0
  11. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/restclient/client.py +84 -0
  12. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib.egg-info/PKG-INFO +1 -1
  13. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib.egg-info/SOURCES.txt +1 -0
  14. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/LICENSE.txt +0 -0
  15. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/README.md +0 -0
  16. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/pyproject.toml +0 -0
  17. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/setup.cfg +0 -0
  18. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/__init__.py +0 -0
  19. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/abstract/__init__.py +0 -0
  20. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/abstract/client.py +0 -0
  21. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/auth/__init__.py +0 -0
  22. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/auth/exceptions.py +0 -0
  23. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/auth/mac_auth.py +0 -0
  24. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/auth/token.py +0 -0
  25. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/__init__.py +0 -0
  26. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/communication/__init__.py +0 -0
  27. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/communication/abstract.py +0 -0
  28. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/communication/classmap.py +0 -0
  29. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/communication/exceptions.py +0 -0
  30. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/communication/local_client.py +0 -0
  31. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/communication/remote_client.py +0 -0
  32. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/database/__init__.py +0 -0
  33. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/database/abstract.py +0 -0
  34. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/database/builder.py +0 -0
  35. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/database/classmap.py +0 -0
  36. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/database/local_client.py +0 -0
  37. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/database/remote_client.py +0 -0
  38. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/datalake/__init__.py +0 -0
  39. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/datalake/builder.py +0 -0
  40. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/datalake/local_client.py +0 -0
  41. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/exceptions.py +0 -0
  42. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/file_handler.py +0 -0
  43. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/filter.py +0 -0
  44. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/hub/__init__.py +0 -0
  45. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/hub/abstract.py +0 -0
  46. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/client/hub/client.py +0 -0
  47. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/communication/__init__.py +0 -0
  48. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/communication/event_handler.py +0 -0
  49. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/component/__init__.py +0 -0
  50. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/component/exceptions.py +0 -0
  51. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/component/spec.py +0 -0
  52. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/constants.py +0 -0
  53. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/encryption.py +0 -0
  54. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/logging/__init__.py +0 -0
  55. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/logging/_internal.py +0 -0
  56. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/logging/component.py +0 -0
  57. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/logging/constants.py +0 -0
  58. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/logging/logging.py +0 -0
  59. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/__init__.py +0 -0
  60. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/alert.py +0 -0
  61. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/attribute.py +0 -0
  62. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/communication.py +0 -0
  63. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/component.py +0 -0
  64. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/dashboard.py +0 -0
  65. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/data_address.py +0 -0
  66. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/event.py +0 -0
  67. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/exceptions.py +0 -0
  68. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/file.py +0 -0
  69. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/hub.py +0 -0
  70. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/pipeline.py +0 -0
  71. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/query.py +0 -0
  72. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/secret.py +0 -0
  73. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/models/setpoint.py +0 -0
  74. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/restclient/__init__.py +0 -0
  75. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/restclient/exceptions.py +0 -0
  76. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/restclient/types.py +0 -0
  77. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/settings.py +0 -0
  78. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/testing/__init__.py +0 -0
  79. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/utils/__init__.py +0 -0
  80. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/utils/custom_model.py +0 -0
  81. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/utils/hub.py +0 -0
  82. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/version.py +0 -0
  83. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib/webhook.py +0 -0
  84. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib.egg-info/dependency_links.txt +0 -0
  85. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib.egg-info/not-zip-safe +0 -0
  86. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib.egg-info/requires.txt +0 -0
  87. {splight-lib-4.0.0.dev5 → splight-lib-4.1.0.dev1}/splight_lib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: splight-lib
3
- Version: 4.0.0.dev5
3
+ Version: 4.1.0.dev1
4
4
  Summary: Library for public use. Splight
5
5
  Home-page: UNKNOWN
6
6
  Author: Splight
@@ -14,7 +14,7 @@ test_requires = [
14
14
 
15
15
  setup(
16
16
  name="splight-lib",
17
- version="4.0.0.dev5",
17
+ version="4.1.0.dev1",
18
18
  author="Splight",
19
19
  author_email="factory@splight-ae.com",
20
20
  packages=find_packages(),
@@ -13,12 +13,22 @@ class AbstractDatalakeClient(AbstractClient):
13
13
  kwargs["count_func"] = "None"
14
14
  return QuerySet(self, *args, **kwargs)
15
15
 
16
+ async def async_get(self, *args, **kwargs):
17
+ # TODO: consider using an async QuerySet
18
+ return await self._async_raw_get(*args, **kwargs)
19
+
16
20
  @abstractmethod
17
21
  def save(
18
22
  self, collection: str, instances: Union[List[Dict], Dict]
19
23
  ) -> List[dict]:
20
24
  pass
21
25
 
26
+ @abstractmethod
27
+ async def async_save(
28
+ self, collection: str, instances: Union[List[Dict], Dict]
29
+ ) -> List[dict]:
30
+ pass
31
+
22
32
  @abstractmethod
23
33
  def delete(self, collection: str, **kwargs) -> None:
24
34
  pass
@@ -65,6 +75,21 @@ class AbstractDatalakeClient(AbstractClient):
65
75
  ) -> List[Dict]:
66
76
  pass
67
77
 
78
+ @abstractmethod
79
+ async def _async_raw_get(
80
+ self,
81
+ resource_name: str,
82
+ collection: str,
83
+ limit_: int = 50,
84
+ skip_: int = 0,
85
+ sort: Union[List, str] = ["timestamp__desc"],
86
+ group_id: Optional[Union[List, str]] = None,
87
+ group_fields: Optional[Union[List, str]] = None,
88
+ tzinfo: timezone = timezone(timedelta()),
89
+ **filters,
90
+ ) -> List[Dict]:
91
+ pass
92
+
68
93
  @abstractmethod
69
94
  def execute_query(
70
95
  self,
@@ -49,6 +49,20 @@ class RemoteDatalakeClient(AbstractDatalakeClient, AbstractRemoteClient):
49
49
  response.raise_for_status()
50
50
  return instances
51
51
 
52
+ @retry(SPLIGHT_REQUEST_EXCEPTIONS, tries=3, delay=2, jitter=1)
53
+ async def async_save(
54
+ self,
55
+ collection: str,
56
+ instances: Union[List[Dict], Dict],
57
+ ) -> List[dict]:
58
+ instances = instances if isinstance(instances, list) else [instances]
59
+ url = self._base_url / f"{self._PREFIX}/save/"
60
+ response = await self._restclient.async_post(
61
+ url, params={"source": collection}, json=instances
62
+ )
63
+ response.raise_for_status()
64
+ return instances
65
+
52
66
  @retry(SPLIGHT_REQUEST_EXCEPTIONS, tries=3, delay=2, jitter=1)
53
67
  def _raw_get(
54
68
  self,
@@ -85,6 +99,42 @@ class RemoteDatalakeClient(AbstractDatalakeClient, AbstractRemoteClient):
85
99
 
86
100
  return output
87
101
 
102
+ @retry(SPLIGHT_REQUEST_EXCEPTIONS, tries=3, delay=2, jitter=1)
103
+ async def _async_raw_get(
104
+ self,
105
+ resource_name: str,
106
+ collection: str,
107
+ limit_: int = 50,
108
+ skip_: int = 0,
109
+ sort: Union[List, str] = ["timestamp__desc"],
110
+ group_id: Optional[Union[List, str]] = None,
111
+ group_fields: Optional[Union[List, str]] = None,
112
+ tzinfo: timezone = timezone(timedelta()),
113
+ **filters,
114
+ ) -> List[Dict]:
115
+ # GET /datalake/data/
116
+ url = self._base_url / f"{self._PREFIX}/data/"
117
+
118
+ filters.update(
119
+ {
120
+ "source": collection,
121
+ "output_format": resource_name,
122
+ "sort": sort,
123
+ "limit_": limit_,
124
+ "skip_": skip_,
125
+ "group_id": group_id,
126
+ "group_fields": group_fields,
127
+ # "tzinfo": tzinfo
128
+ }
129
+ )
130
+
131
+ params = self._parse_params(**filters)
132
+ response = await self._restclient.async_get(url, params=params)
133
+ response.raise_for_status()
134
+ output = response.json()["results"]
135
+
136
+ return output
137
+
88
138
  @retry(SPLIGHT_REQUEST_EXCEPTIONS, tries=3, delay=2, jitter=1)
89
139
  def delete(self, collection: str, **kwargs) -> None:
90
140
  # DELETE /datalake/delete/
@@ -1,16 +1,13 @@
1
1
  import os
2
2
  import sys
3
- import traceback
3
+ from abc import ABC, abstractmethod
4
4
  from functools import partial
5
5
  from tempfile import NamedTemporaryFile
6
6
  from time import sleep
7
- from typing import Dict, List, Optional, Type
7
+ from typing import Callable, Dict, List, Optional, Type
8
8
 
9
- from furl import furl
10
9
  from pydantic import BaseModel, create_model
11
- from retry import retry
12
10
 
13
- from splight_lib.auth import SplightAuthToken
14
11
  from splight_lib.client.communication import RemoteCommunicationClient
15
12
  from splight_lib.communication.event_handler import (
16
13
  command_event_handler,
@@ -18,7 +15,6 @@ from splight_lib.communication.event_handler import (
18
15
  setpoint_event_handler,
19
16
  )
20
17
  from splight_lib.component.exceptions import (
21
- DuplicatedComponentException,
22
18
  InvalidBidingObject,
23
19
  MissingBindingCallback,
24
20
  MissingCommandCallback,
@@ -41,12 +37,7 @@ from splight_lib.models.component import (
41
37
  )
42
38
  from splight_lib.models.event import EventNames
43
39
  from splight_lib.models.setpoint import SetPoint
44
- from splight_lib.restclient import (
45
- ConnectError,
46
- HTTPError,
47
- SplightRestClient,
48
- Timeout,
49
- )
40
+ from splight_lib.restclient import ConnectError, HTTPError, Timeout
50
41
  from splight_lib.settings import settings
51
42
 
52
43
  REQUEST_EXCEPTIONS = (ConnectError, HTTPError, Timeout)
@@ -83,16 +74,14 @@ class HealthCheckProcessor:
83
74
  if not is_alive:
84
75
  exc = self._engine.get_last_exception()
85
76
  self._log_exception(exc)
86
- self._logger.error(
87
- "Healthcheck task failed.", tags=LogTags.RUNTIME
88
- )
77
+ self._logger.info("Healthcheck finished", tags=LogTags.RUNTIME)
89
78
  self._health_file.close()
90
- self._logger.error(
79
+ self._logger.info(
91
80
  "Healthcheck file removed: %s",
92
81
  self._health_file,
93
82
  tags=LogTags.RUNTIME,
94
83
  )
95
- sys.exit(1)
84
+ break
96
85
  sleep(self._HEALTHCHECK_INTERVAL)
97
86
 
98
87
  def stop(self):
@@ -106,7 +95,7 @@ class HealthCheckProcessor:
106
95
  self._logger.exception(exc, exc_info=(exc_type, exc, stack))
107
96
 
108
97
 
109
- class SplightBaseComponent:
98
+ class SplightBaseComponent(ABC):
110
99
  def __init__(
111
100
  self,
112
101
  component_id: Optional[str] = None,
@@ -121,9 +110,9 @@ class SplightBaseComponent:
121
110
  instance_id=component_id,
122
111
  )
123
112
  self._execution_engine = ExecutionClient()
124
- health_check = HealthCheckProcessor(self._execution_engine)
113
+ self._health_check = HealthCheckProcessor(self._execution_engine)
125
114
  self._health_check_thread = Thread(
126
- target=health_check.start, args=(), daemon=False
115
+ target=self._health_check.start, args=(), daemon=False
127
116
  )
128
117
  # We can't add the healthcheck thread into the execution engine
129
118
  # because that thread should stop if any of the registered threads
@@ -164,6 +153,8 @@ class SplightBaseComponent:
164
153
  self._custom_types = self._get_custom_type_model(component_objects)
165
154
  self._routines = self._get_routine_model(routines_objects)
166
155
 
156
+ self.start = self._wrap_start(self.start)
157
+
167
158
  @property
168
159
  def input(self) -> BaseModel:
169
160
  return self._input
@@ -184,6 +175,36 @@ class SplightBaseComponent:
184
175
  def execution_engine(self) -> ExecutionClient:
185
176
  return self._execution_engine
186
177
 
178
+ def _register_exit(self):
179
+ if self._execution_engine.get_last_exception():
180
+ sys.exit(1)
181
+ else:
182
+ sys.exit(0)
183
+
184
+ def _wrap_start(self, original_start: Callable) -> Callable:
185
+ """Wraps the start method to wait for all threads to finish.
186
+
187
+ Parameters
188
+ ----------
189
+ original_start: Callable
190
+ The implemented start method from the component
191
+
192
+ Returns
193
+ -------
194
+ Callable
195
+ The start method wrapped
196
+ """
197
+
198
+ def wrapper():
199
+ original_start()
200
+ for thread in self._execution_engine.threads:
201
+ thread.join()
202
+
203
+ self._health_check_thread.join()
204
+ self._register_exit()
205
+
206
+ return wrapper
207
+
187
208
  def _get_custom_type_model(
188
209
  self, component_object: Dict[str, Type[ComponentObjectInstance]]
189
210
  ) -> BaseModel:
@@ -299,10 +320,10 @@ class SplightBaseComponent:
299
320
  ),
300
321
  )
301
322
 
323
+ @abstractmethod
302
324
  def start(self):
303
325
  raise NotImplementedError()
304
326
 
305
327
  def stop(self):
306
- self._execution_engine.stop(self._health_check_thread)
307
328
  self._execution_engine.terminate_all()
308
329
  sys.exit(1)
@@ -1,4 +1,3 @@
1
- import atexit
2
1
  import sys
3
2
  import threading
4
3
  import time
@@ -153,17 +152,20 @@ class TaskMap:
153
152
 
154
153
 
155
154
  class Scheduler:
156
- def __init__(self, *args, **kwargs) -> None:
155
+ def __init__(self, event: Event) -> None:
156
+ # The _event attr is used for controling the scheduler loop meanwhile
157
+ # the _tasks_event is used to notify the scheduler that a task should
158
+ # be executed
159
+ self._event = event
157
160
  self._tasks = TaskMap()
158
- self._event = Event()
161
+ self._tasks_event = Event()
159
162
  self._mutex = Lock()
160
163
  self._to_add: List[Task] = []
161
164
  self._to_remove: List[Task] = []
162
165
 
163
166
  def start(self) -> None:
164
167
  """Scheduler infinite loop."""
165
- self._stop = False
166
- while not self._stop:
168
+ while self._event.is_set():
167
169
  # Update the task list
168
170
  self._update_task_list()
169
171
  # Run the scheduler task
@@ -172,11 +174,12 @@ class Scheduler:
172
174
  next_event_time = near_event - time.time()
173
175
 
174
176
  # Wait until next event or someone trigger the event
175
- self._event.wait(timeout=next_event_time)
176
- self._event.clear()
177
+ self._tasks_event.wait(timeout=next_event_time)
178
+ self._tasks_event.clear()
177
179
 
178
180
  def stop(self) -> None:
179
- self._stop = True
181
+ if self._event.is_set():
182
+ self._event.clear()
180
183
 
181
184
  def _schedule(self) -> float:
182
185
  """Scheduler main task."""
@@ -197,7 +200,7 @@ class Scheduler:
197
200
  Unlock the scheduler if is locked.
198
201
  This will run the scheduler main task.
199
202
  """
200
- self._event.set()
203
+ self._tasks_event.set()
201
204
 
202
205
  def schedule(self, task: Task) -> None:
203
206
  """
@@ -242,20 +245,8 @@ class Thread(DefaultThread):
242
245
  def __init__(self, target: Callable, args: Tuple = (), **kwargs) -> None:
243
246
  target = self.store_result(target)
244
247
  self.result = Empty()
245
- self._exc = None
246
248
  super().__init__(target=target, args=args, name=target, **kwargs)
247
249
 
248
- @property
249
- def exc(self):
250
- return self._exc
251
-
252
- def run(self):
253
- try:
254
- super().run()
255
- except Exception as exc:
256
- self._exc = exc
257
- raise exc
258
-
259
250
  def store_result(self, func: Callable) -> Callable:
260
251
  @wraps(func)
261
252
  def wrapper(*args, **kwargs):
@@ -288,31 +279,56 @@ class Popen(DefaultPopen):
288
279
 
289
280
  class ExecutionClient(AbstractClient):
290
281
  def __init__(self, *args, **kwargs):
282
+ self._event = Event()
291
283
  self.processes: List[Popen] = []
292
284
  self.threads: List[Thread] = []
293
285
 
294
286
  self._register_exit_functions()
295
287
  super(ExecutionClient, self).__init__(*args, **kwargs)
296
288
  logger.info("Execution client initialized.", tags=LogTags.RUNTIME)
289
+ self._exc = None
290
+ self._thread_exc = None
291
+
292
+ @property
293
+ def event(self) -> Event:
294
+ return self._event
297
295
 
298
296
  def _register_exit_functions(self) -> None:
299
297
  excepthook = sys.excepthook
298
+ thread_exchook = threading.excepthook
300
299
 
301
300
  def wrap_excepthook(type, value, traceback):
301
+ self._exc = (type, value, traceback)
302
302
  self.terminate_all()
303
303
  excepthook(type, value, traceback)
304
304
 
305
- atexit.register(self.terminate_all)
305
+ def wrap_thread_excepthook(args):
306
+ self._thread_exc = (
307
+ args.exc_type,
308
+ args.exc_value,
309
+ args.exc_traceback,
310
+ )
311
+ self.terminate_all()
312
+ thread_exchook(args)
313
+
306
314
  sys.excepthook = wrap_excepthook
315
+ threading.excepthook = wrap_thread_excepthook
307
316
 
308
317
  def __del__(self) -> None:
309
318
  self.terminate_all()
310
319
 
311
320
  def terminate_all(self) -> None:
321
+ # Set the event to False to stop all threads
322
+ # This will also stop the scheduler
323
+ self._event.clear()
324
+
312
325
  for p in self.processes:
313
326
  p.terminate()
314
327
 
315
328
  def start(self, job=Union[Popen, Thread, Task]):
329
+ # Set the event in true so the threads can run
330
+ if not self._event.is_set():
331
+ self._event.set()
316
332
  logger.info("Executing new job.", tags=LogTags.RUNTIME)
317
333
  if isinstance(job, Popen):
318
334
  return self._start_process(job)
@@ -344,7 +360,7 @@ class ExecutionClient(AbstractClient):
344
360
  logger.debug("Starting Task", tags=LogTags.RUNTIME)
345
361
  if not getattr(self, "_scheduler", None):
346
362
  # Instantiate and start Scheduler thread
347
- self._scheduler = Scheduler()
363
+ self._scheduler = Scheduler(self._event)
348
364
  self._start_thread(
349
365
  Thread(target=self._scheduler.start, daemon=False)
350
366
  )
@@ -356,19 +372,8 @@ class ExecutionClient(AbstractClient):
356
372
  return self._scheduler.unschedule(job)
357
373
 
358
374
  def is_alive(self) -> bool:
359
- # We need to also check the main thread just in case some component is
360
- # not using the execution client explicitly and is doing some
361
- # processing directly in the main thread so the healtcheck should
362
- # say the component is healthy.
363
- main_thread = threading.main_thread()
364
- threads_status = [
365
- p.is_alive() or p.exit_ok() for p in self.processes + self.threads
366
- ]
367
- return (
368
- main_thread.is_alive() or all(threads_status)
369
- if threads_status
370
- else main_thread.is_alive()
371
- )
375
+ threads_status = [p.is_alive() for p in self.processes + self.threads]
376
+ return all(threads_status)
372
377
 
373
378
  def healthcheck(self) -> Tuple[bool, ComponentStatus]:
374
379
  """Check if the component is alive and return the status.
@@ -393,9 +398,10 @@ class ExecutionClient(AbstractClient):
393
398
  def get_last_exception(self) -> Optional[Exception]:
394
399
  """Get the last exception thrown in one of the threads.
395
400
  It assumes that there is only one thread that crashed
396
- Also, only works for the thread not the processes.
401
+ It only works for the thread not the processes.
397
402
  """
398
- broken_thread = [x.exc for x in self.threads if x.exc]
399
- if broken_thread:
400
- return broken_thread[0]
403
+ if self._exc:
404
+ return self._exc[1]
405
+ if self._thread_exc:
406
+ return self._thread_exc[1]
401
407
  return None
@@ -4,6 +4,7 @@ from geojson_pydantic import GeometryCollection
4
4
 
5
5
  from splight_lib.models.attribute import Attribute
6
6
  from splight_lib.models.base import SplightDatabaseBaseModel
7
+ from splight_lib.models.metadata import Metadata
7
8
 
8
9
 
9
10
  class Asset(SplightDatabaseBaseModel):
@@ -12,6 +13,7 @@ class Asset(SplightDatabaseBaseModel):
12
13
  description: Optional[str] = None
13
14
  tags: List[str] = []
14
15
  attributes: List[Attribute] = []
16
+ metadata: List[Metadata] = []
15
17
  verified: bool = False
16
18
  geometry: Optional[GeometryCollection]
17
19
  centroid_coordinates: Optional[Tuple[float, float]]
@@ -94,6 +94,19 @@ class SplightDatalakeBaseModel(BaseModel):
94
94
  instances = [cls.parse_obj(item) for item in instances]
95
95
  return instances
96
96
 
97
+ @classmethod
98
+ async def async_get(
99
+ cls, **params: Dict
100
+ ) -> List["SplightDatalakeBaseModel"]:
101
+ dl_client = cls.__get_datalake_client()
102
+ instances = await dl_client.async_get(
103
+ resource_name=cls.__name__,
104
+ collection=cls._collection_name,
105
+ **params,
106
+ )
107
+ instances = [cls.parse_obj(item) for item in instances]
108
+ return instances
109
+
97
110
  @classmethod
98
111
  def get_dataframe(cls, **params: Dict) -> pd.DataFrame:
99
112
  dl_client = cls.__get_datalake_client()
@@ -111,6 +124,14 @@ class SplightDatalakeBaseModel(BaseModel):
111
124
  instances=json.loads(self.json()),
112
125
  )
113
126
 
127
+ async def async_save(self):
128
+ dl_client = self.__get_datalake_client()
129
+
130
+ await dl_client.async_save(
131
+ collection=self._collection_name,
132
+ instances=json.loads(self.json()),
133
+ )
134
+
114
135
  @classmethod
115
136
  def save_dataframe(cls, dataframe: pd.DataFrame):
116
137
  dl_client = cls.__get_datalake_client()
@@ -0,0 +1,20 @@
1
+ from enum import auto
2
+ from typing import Optional
3
+
4
+ from strenum import PascalCaseStrEnum
5
+
6
+ from splight_lib.models.base import SplightDatabaseBaseModel
7
+
8
+
9
+ class MetadataType(PascalCaseStrEnum):
10
+ NUMBER = auto()
11
+ BOOLEAN = auto()
12
+ STRING = auto()
13
+
14
+
15
+ class Metadata(SplightDatabaseBaseModel):
16
+ id: Optional[str]
17
+ name: Optional[str]
18
+ asset: Optional[str]
19
+ type: MetadataType = MetadataType.NUMBER
20
+ value: Optional[str]
@@ -24,6 +24,11 @@ class NativeOutput(SplightDatalakeBaseModel):
24
24
  params["output_format"] = cls._output_format
25
25
  return super().get(**params)
26
26
 
27
+ @classmethod
28
+ async def async_get(cls, **params: Dict) -> List["NativeOutput"]:
29
+ params["output_format"] = cls._output_format
30
+ return await super().async_get(**params)
31
+
27
32
  @classmethod
28
33
  def get_dataframe(cls, **params: Dict) -> pd.DataFrame:
29
34
  params["output_format"] = cls._output_format
@@ -183,9 +183,33 @@ class SplightRestClient:
183
183
  default_encoding=default_encoding,
184
184
  event_hooks=event_hooks,
185
185
  )
186
+ self._async_client = httpx.AsyncClient(
187
+ auth=auth,
188
+ params=params,
189
+ headers=headers,
190
+ cookies=cookies,
191
+ timeout=timeout,
192
+ verify=verify,
193
+ cert=cert,
194
+ http1=http1,
195
+ http2=http2,
196
+ proxies=proxies,
197
+ follow_redirects=allow_redirects,
198
+ base_url=base_url,
199
+ limits=limits,
200
+ max_redirects=max_redirects,
201
+ transport=transport,
202
+ app=app,
203
+ trust_env=trust_env,
204
+ default_encoding=default_encoding,
205
+ event_hooks=event_hooks,
206
+ )
186
207
 
187
208
  def update_headers(self, new_headers: HeaderTypes):
188
209
  self._client.headers = self._client._merge_headers(new_headers)
210
+ self._async_client.headers = self._async_client._merge_headers(
211
+ new_headers
212
+ )
189
213
 
190
214
  def get(
191
215
  self,
@@ -214,6 +238,33 @@ class SplightRestClient:
214
238
  )
215
239
  return SplightResponse.from_response(raw_response)
216
240
 
241
+ async def async_get(
242
+ self,
243
+ url: URLTypes,
244
+ *,
245
+ params: Optional[QueryParamTypes] = None,
246
+ headers: Optional[HeaderTypes] = None,
247
+ cookies: Optional[CookieTypes] = None,
248
+ auth: Union[AuthTypes, DefaultClient] = DEFAULT_CLIENT,
249
+ allow_redirects: Union[bool, DefaultClient] = DEFAULT_CLIENT,
250
+ timeout: Union[TimeoutTypes, DefaultClient] = DEFAULT_CLIENT,
251
+ ) -> SplightResponse:
252
+ """Send a GET request to the specified URL.
253
+
254
+ Parameters: See class docstring.
255
+ """
256
+ raw_response = await self._async_client.request(
257
+ self._GET_METHOD,
258
+ str(url),
259
+ params=params,
260
+ headers=headers,
261
+ cookies=cookies,
262
+ auth=auth,
263
+ follow_redirects=allow_redirects,
264
+ timeout=timeout,
265
+ )
266
+ return SplightResponse.from_response(raw_response)
267
+
217
268
  def options(
218
269
  self,
219
270
  url: URLTypes,
@@ -301,6 +352,39 @@ class SplightRestClient:
301
352
  )
302
353
  return SplightResponse.from_response(raw_response)
303
354
 
355
+ async def async_post(
356
+ self,
357
+ url: URLTypes,
358
+ *,
359
+ data: Optional[RequestData] = None,
360
+ files: Optional[RequestFiles] = None,
361
+ json: Optional[Any] = None,
362
+ params: Optional[QueryParamTypes] = None,
363
+ headers: Optional[HeaderTypes] = None,
364
+ cookies: Optional[CookieTypes] = None,
365
+ auth: Union[AuthTypes, DefaultClient] = DEFAULT_CLIENT,
366
+ allow_redirects: Union[bool, DefaultClient] = DEFAULT_CLIENT,
367
+ timeout: Union[TimeoutTypes, DefaultClient] = DEFAULT_CLIENT,
368
+ ) -> SplightResponse:
369
+ """Send a POST request to the specified URL.
370
+
371
+ Parameters: See class docstring.
372
+ """
373
+ raw_response = await self._async_client.request(
374
+ self._POST_METHOD,
375
+ str(url),
376
+ data=data,
377
+ files=files,
378
+ json=json,
379
+ params=params,
380
+ headers=headers,
381
+ cookies=cookies,
382
+ auth=auth,
383
+ follow_redirects=allow_redirects,
384
+ timeout=timeout,
385
+ )
386
+ return SplightResponse.from_response(raw_response)
387
+
304
388
  def put(
305
389
  self,
306
390
  url: URLTypes,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: splight-lib
3
- Version: 4.0.0.dev5
3
+ Version: 4.1.0.dev1
4
4
  Summary: Library for public use. Splight
5
5
  Home-page: UNKNOWN
6
6
  Author: Splight
@@ -69,6 +69,7 @@ splight_lib/models/event.py
69
69
  splight_lib/models/exceptions.py
70
70
  splight_lib/models/file.py
71
71
  splight_lib/models/hub.py
72
+ splight_lib/models/metadata.py
72
73
  splight_lib/models/native.py
73
74
  splight_lib/models/pipeline.py
74
75
  splight_lib/models/query.py