cognite-extractor-utils 7.6.0__py3-none-any.whl → 7.8.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.
Potentially problematic release.
This version of cognite-extractor-utils might be problematic. Click here for more details.
- cognite/examples/unstable/extractors/simple_extractor/config/config.yaml +3 -0
- cognite/examples/unstable/extractors/simple_extractor/config/connection_config.yaml +10 -0
- cognite/examples/unstable/extractors/simple_extractor/main.py +81 -0
- cognite/extractorutils/__init__.py +1 -1
- cognite/extractorutils/_inner_util.py +2 -2
- cognite/extractorutils/base.py +1 -1
- cognite/extractorutils/configtools/elements.py +4 -2
- cognite/extractorutils/configtools/loaders.py +3 -3
- cognite/extractorutils/exceptions.py +1 -1
- cognite/extractorutils/metrics.py +8 -6
- cognite/extractorutils/statestore/watermark.py +6 -3
- cognite/extractorutils/threading.py +2 -2
- cognite/extractorutils/unstable/configuration/exceptions.py +28 -1
- cognite/extractorutils/unstable/configuration/models.py +157 -32
- cognite/extractorutils/unstable/core/_dto.py +80 -7
- cognite/extractorutils/unstable/core/base.py +175 -106
- cognite/extractorutils/unstable/core/checkin_worker.py +428 -0
- cognite/extractorutils/unstable/core/errors.py +2 -2
- cognite/extractorutils/unstable/core/logger.py +49 -0
- cognite/extractorutils/unstable/core/runtime.py +200 -31
- cognite/extractorutils/unstable/core/tasks.py +2 -2
- cognite/extractorutils/uploader/__init__.py +2 -0
- cognite/extractorutils/uploader/_base.py +1 -1
- cognite/extractorutils/uploader/assets.py +1 -1
- cognite/extractorutils/uploader/data_modeling.py +1 -1
- cognite/extractorutils/uploader/events.py +1 -1
- cognite/extractorutils/uploader/files.py +4 -4
- cognite/extractorutils/uploader/raw.py +1 -1
- cognite/extractorutils/uploader/time_series.py +319 -52
- cognite/extractorutils/uploader_extractor.py +20 -5
- cognite/extractorutils/uploader_types.py +13 -2
- cognite/extractorutils/util.py +8 -6
- {cognite_extractor_utils-7.6.0.dist-info → cognite_extractor_utils-7.8.0.dist-info}/METADATA +3 -2
- cognite_extractor_utils-7.8.0.dist-info/RECORD +55 -0
- cognite_extractor_utils-7.8.0.dist-info/entry_points.txt +2 -0
- cognite_extractor_utils-7.6.0.dist-info/RECORD +0 -50
- {cognite_extractor_utils-7.6.0.dist-info → cognite_extractor_utils-7.8.0.dist-info}/WHEEL +0 -0
- {cognite_extractor_utils-7.6.0.dist-info → cognite_extractor_utils-7.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
Temporary holding place for DTOs against Extraction Pipelines 2.0 until it's in the SDK.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Annotated, Any, Literal, Optional
|
|
6
7
|
|
|
8
|
+
from annotated_types import Len
|
|
7
9
|
from humps import camelize
|
|
8
|
-
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, StringConstraints
|
|
11
|
+
from typing_extensions import TypeAliasType
|
|
12
|
+
|
|
13
|
+
from cognite.extractorutils.unstable.core.errors import Error as InternalError
|
|
14
|
+
from cognite.extractorutils.unstable.core.errors import ErrorLevel
|
|
9
15
|
|
|
10
16
|
|
|
11
17
|
class CogniteModel(BaseModel):
|
|
@@ -17,30 +23,97 @@ class CogniteModel(BaseModel):
|
|
|
17
23
|
* exclude Nones from serialized JSON instead of having nulls in the response text.
|
|
18
24
|
"""
|
|
19
25
|
|
|
20
|
-
def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
26
|
+
def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: # noqa: ANN401
|
|
21
27
|
if kwargs:
|
|
22
28
|
kwargs["exclude_none"] = True
|
|
23
29
|
else:
|
|
24
30
|
kwargs = {"exclude_none": True}
|
|
25
31
|
return BaseModel.model_dump(self, *args, **kwargs)
|
|
26
32
|
|
|
27
|
-
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
33
|
+
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: # noqa: ANN401
|
|
28
34
|
return self.model_dump(*args, **kwargs)
|
|
29
35
|
|
|
30
36
|
model_config = ConfigDict(alias_generator=camelize, populate_by_name=True, extra="forbid")
|
|
31
37
|
|
|
32
38
|
|
|
39
|
+
class WithExternalId(CogniteModel):
|
|
40
|
+
external_id: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MessageType = Annotated[str, StringConstraints(min_length=0, max_length=1000)]
|
|
44
|
+
|
|
45
|
+
|
|
33
46
|
class TaskUpdate(CogniteModel):
|
|
34
47
|
type: Literal["started"] | Literal["ended"]
|
|
35
48
|
name: str
|
|
36
49
|
timestamp: int
|
|
50
|
+
message: MessageType | None = None
|
|
37
51
|
|
|
38
52
|
|
|
39
|
-
class Error(
|
|
40
|
-
|
|
41
|
-
level: str
|
|
53
|
+
class Error(WithExternalId):
|
|
54
|
+
level: ErrorLevel
|
|
42
55
|
description: str
|
|
43
56
|
details: str | None
|
|
44
57
|
start_time: int
|
|
45
58
|
end_time: int | None
|
|
46
59
|
task: str | None
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_internal(cls, error: InternalError) -> "Error":
|
|
63
|
+
"""Convert the error into a DTO (Data Transfer Object) for reporting."""
|
|
64
|
+
return Error(
|
|
65
|
+
external_id=error.external_id,
|
|
66
|
+
level=error.level,
|
|
67
|
+
description=error.description,
|
|
68
|
+
details=error.details,
|
|
69
|
+
start_time=error.start_time,
|
|
70
|
+
end_time=error.end_time,
|
|
71
|
+
task=error._task_name,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
TaskUpdateList = Annotated[list[TaskUpdate], Len(min_length=1, max_length=1000)]
|
|
76
|
+
ErrorList = Annotated[list[Error], Len(min_length=0, max_length=1000)]
|
|
77
|
+
VersionType = Annotated[str, StringConstraints(min_length=1, max_length=32)]
|
|
78
|
+
DescriptionType = Annotated[str, StringConstraints(min_length=0, max_length=500)]
|
|
79
|
+
TaskList = Annotated[list["Task"], Len(min_length=1, max_length=1000)]
|
|
80
|
+
JSONType = TypeAliasType( # type: ignore[misc]
|
|
81
|
+
"JSONType",
|
|
82
|
+
bool | int | float | str | None | list[Optional["JSONType"]] | dict[str, Optional["JSONType"]], # type: ignore[misc]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class WithVersion(CogniteModel):
|
|
87
|
+
version: VersionType | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ExtractorInfo(WithExternalId, WithVersion):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TaskType(Enum):
|
|
95
|
+
continuous = "continuous"
|
|
96
|
+
batch = "batch"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Task(CogniteModel):
|
|
100
|
+
type: TaskType
|
|
101
|
+
name: str
|
|
102
|
+
action: bool = False
|
|
103
|
+
description: DescriptionType | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class StartupRequest(WithExternalId):
|
|
107
|
+
extractor: ExtractorInfo
|
|
108
|
+
tasks: TaskList | None = None
|
|
109
|
+
active_config_revision: int | Literal["local"] | None = None
|
|
110
|
+
timestamp: int | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CheckinRequest(WithExternalId):
|
|
114
|
+
task_events: TaskUpdateList | None = None
|
|
115
|
+
errors: ErrorList | None = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CheckinResponse(WithExternalId):
|
|
119
|
+
last_config_revision: int | None = None
|
|
@@ -47,38 +47,56 @@ The subclass should also define several class attributes:
|
|
|
47
47
|
import logging
|
|
48
48
|
import time
|
|
49
49
|
from concurrent.futures import ThreadPoolExecutor
|
|
50
|
+
from datetime import datetime, timezone
|
|
50
51
|
from functools import partial
|
|
51
|
-
from logging.handlers import TimedRotatingFileHandler
|
|
52
52
|
from multiprocessing import Queue
|
|
53
|
+
from multiprocessing.synchronize import Event as MpEvent
|
|
53
54
|
from threading import RLock, Thread
|
|
54
55
|
from types import TracebackType
|
|
55
|
-
from typing import Generic,
|
|
56
|
+
from typing import Generic, TypeVar
|
|
56
57
|
|
|
57
58
|
from humps import pascalize
|
|
58
59
|
from typing_extensions import Self, assert_never
|
|
59
60
|
|
|
60
61
|
from cognite.extractorutils._inner_util import _resolve_log_level
|
|
62
|
+
from cognite.extractorutils.statestore import (
|
|
63
|
+
AbstractStateStore,
|
|
64
|
+
LocalStateStore,
|
|
65
|
+
NoStateStore,
|
|
66
|
+
)
|
|
61
67
|
from cognite.extractorutils.threading import CancellationToken
|
|
62
68
|
from cognite.extractorutils.unstable.configuration.models import (
|
|
69
|
+
ConfigRevision,
|
|
70
|
+
ConfigType,
|
|
63
71
|
ConnectionConfig,
|
|
64
72
|
ExtractorConfig,
|
|
65
73
|
LogConsoleHandlerConfig,
|
|
66
74
|
LogFileHandlerConfig,
|
|
67
75
|
)
|
|
68
|
-
from cognite.extractorutils.unstable.core._dto import
|
|
69
|
-
|
|
76
|
+
from cognite.extractorutils.unstable.core._dto import (
|
|
77
|
+
CogniteModel,
|
|
78
|
+
ExtractorInfo,
|
|
79
|
+
StartupRequest,
|
|
80
|
+
TaskType,
|
|
81
|
+
)
|
|
82
|
+
from cognite.extractorutils.unstable.core._dto import (
|
|
83
|
+
Task as DtoTask,
|
|
84
|
+
)
|
|
70
85
|
from cognite.extractorutils.unstable.core._messaging import RuntimeMessage
|
|
86
|
+
from cognite.extractorutils.unstable.core.checkin_worker import CheckinWorker
|
|
71
87
|
from cognite.extractorutils.unstable.core.errors import Error, ErrorLevel
|
|
72
|
-
from cognite.extractorutils.unstable.core.logger import CogniteLogger
|
|
88
|
+
from cognite.extractorutils.unstable.core.logger import CogniteLogger, RobustFileHandler
|
|
73
89
|
from cognite.extractorutils.unstable.core.restart_policy import WHEN_CONTINUOUS_TASKS_CRASHES, RestartPolicy
|
|
74
90
|
from cognite.extractorutils.unstable.core.tasks import ContinuousTask, ScheduledTask, StartupTask, Task, TaskContext
|
|
75
91
|
from cognite.extractorutils.unstable.scheduling import TaskScheduler
|
|
76
92
|
from cognite.extractorutils.util import now
|
|
77
93
|
|
|
78
|
-
__all__ = [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
__all__ = [
|
|
95
|
+
"CogniteModel",
|
|
96
|
+
"ConfigRevision",
|
|
97
|
+
"ConfigType",
|
|
98
|
+
"Extractor",
|
|
99
|
+
]
|
|
82
100
|
|
|
83
101
|
|
|
84
102
|
_T = TypeVar("_T", bound=ExtractorConfig)
|
|
@@ -97,10 +115,12 @@ class FullConfig(Generic[_T]):
|
|
|
97
115
|
connection_config: ConnectionConfig,
|
|
98
116
|
application_config: _T,
|
|
99
117
|
current_config_revision: ConfigRevision,
|
|
118
|
+
log_level_override: str | None = None,
|
|
100
119
|
) -> None:
|
|
101
120
|
self.connection_config = connection_config
|
|
102
121
|
self.application_config = application_config
|
|
103
|
-
self.current_config_revision = current_config_revision
|
|
122
|
+
self.current_config_revision: ConfigRevision = current_config_revision
|
|
123
|
+
self.log_level_override = log_level_override
|
|
104
124
|
|
|
105
125
|
|
|
106
126
|
class Extractor(Generic[ConfigType], CogniteLogger):
|
|
@@ -122,33 +142,60 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
122
142
|
CONFIG_TYPE: type[ConfigType]
|
|
123
143
|
|
|
124
144
|
RESTART_POLICY: RestartPolicy = WHEN_CONTINUOUS_TASKS_CRASHES
|
|
145
|
+
USE_DEFAULT_STATE_STORE: bool = True
|
|
146
|
+
_statestore_singleton: AbstractStateStore | None = None
|
|
147
|
+
|
|
148
|
+
cancellation_token: CancellationToken
|
|
125
149
|
|
|
126
|
-
def __init__(self, config: FullConfig[ConfigType]) -> None:
|
|
150
|
+
def __init__(self, config: FullConfig[ConfigType], checkin_worker: CheckinWorker) -> None:
|
|
127
151
|
self._logger = logging.getLogger(f"{self.EXTERNAL_ID}.main")
|
|
152
|
+
self._checkin_worker = checkin_worker
|
|
128
153
|
|
|
129
154
|
self.cancellation_token = CancellationToken()
|
|
130
155
|
self.cancellation_token.cancel_on_interrupt()
|
|
131
156
|
|
|
132
157
|
self.connection_config = config.connection_config
|
|
133
158
|
self.application_config = config.application_config
|
|
134
|
-
self.current_config_revision = config.current_config_revision
|
|
159
|
+
self.current_config_revision: ConfigRevision = config.current_config_revision
|
|
160
|
+
self.log_level_override = config.log_level_override
|
|
135
161
|
|
|
136
162
|
self.cognite_client = self.connection_config.get_cognite_client(f"{self.EXTERNAL_ID}-{self.VERSION}")
|
|
137
163
|
|
|
164
|
+
self.state_store: AbstractStateStore
|
|
165
|
+
|
|
138
166
|
self._checkin_lock = RLock()
|
|
139
167
|
self._runtime_messages: Queue[RuntimeMessage] | None = None
|
|
140
168
|
|
|
141
169
|
self._scheduler = TaskScheduler(self.cancellation_token.create_child_token())
|
|
142
170
|
|
|
143
171
|
self._tasks: list[Task] = []
|
|
144
|
-
self.
|
|
145
|
-
self._errors: dict[str, Error] = {}
|
|
172
|
+
self._start_time: datetime
|
|
146
173
|
|
|
147
174
|
self.__init_tasks__()
|
|
148
175
|
|
|
176
|
+
def _setup_cancellation_watcher(self, cancel_event: MpEvent) -> None:
|
|
177
|
+
"""Starts a daemon thread to watch the inter-process event."""
|
|
178
|
+
|
|
179
|
+
def watcher() -> None:
|
|
180
|
+
"""Blocks until the event is set, then cancels the local token."""
|
|
181
|
+
cancel_event.wait()
|
|
182
|
+
if not self.cancellation_token.is_cancelled:
|
|
183
|
+
self._logger.info("Cancellation signal received from runtime. Shutting down gracefully.")
|
|
184
|
+
self.cancellation_token.cancel()
|
|
185
|
+
|
|
186
|
+
thread = Thread(target=watcher, name="RuntimeCancellationWatcher", daemon=True)
|
|
187
|
+
thread.start()
|
|
188
|
+
|
|
149
189
|
def _setup_logging(self) -> None:
|
|
150
|
-
|
|
151
|
-
|
|
190
|
+
if self.log_level_override:
|
|
191
|
+
level_to_set = _resolve_log_level(self.log_level_override)
|
|
192
|
+
# Use the override level if provided
|
|
193
|
+
min_level = level_to_set
|
|
194
|
+
max_level = level_to_set
|
|
195
|
+
else:
|
|
196
|
+
# Otherwise, use the levels from the config file
|
|
197
|
+
min_level = min([_resolve_log_level(h.level.value) for h in self.application_config.log_handlers])
|
|
198
|
+
max_level = max([_resolve_log_level(h.level.value) for h in self.application_config.log_handlers])
|
|
152
199
|
|
|
153
200
|
root = logging.getLogger()
|
|
154
201
|
root.setLevel(min_level)
|
|
@@ -173,21 +220,85 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
173
220
|
case LogConsoleHandlerConfig() as console_handler:
|
|
174
221
|
sh = logging.StreamHandler()
|
|
175
222
|
sh.setFormatter(fmt)
|
|
176
|
-
|
|
223
|
+
level_for_handler = _resolve_log_level(
|
|
224
|
+
self.log_level_override if self.log_level_override else console_handler.level.value
|
|
225
|
+
)
|
|
226
|
+
sh.setLevel(level_for_handler)
|
|
177
227
|
|
|
178
228
|
root.addHandler(sh)
|
|
179
229
|
|
|
180
230
|
case LogFileHandlerConfig() as file_handler:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
231
|
+
try:
|
|
232
|
+
fh = RobustFileHandler(
|
|
233
|
+
filename=file_handler.path,
|
|
234
|
+
when="midnight",
|
|
235
|
+
utc=True,
|
|
236
|
+
backupCount=file_handler.retention,
|
|
237
|
+
create_dirs=True,
|
|
238
|
+
)
|
|
239
|
+
level_for_handler = _resolve_log_level(
|
|
240
|
+
self.log_level_override if self.log_level_override else file_handler.level.value
|
|
241
|
+
)
|
|
242
|
+
fh.setLevel(level_for_handler)
|
|
243
|
+
fh.setFormatter(fmt)
|
|
244
|
+
|
|
245
|
+
root.addHandler(fh)
|
|
246
|
+
except (OSError, PermissionError) as e:
|
|
247
|
+
self._logger.warning(
|
|
248
|
+
f"Could not create or write to log file {file_handler.path}: {e}. "
|
|
249
|
+
"Falling back to console logging."
|
|
250
|
+
)
|
|
251
|
+
if not any(isinstance(h, logging.StreamHandler) for h in root.handlers):
|
|
252
|
+
sh = logging.StreamHandler()
|
|
253
|
+
sh.setFormatter(fmt)
|
|
254
|
+
sh.setLevel(level_for_handler)
|
|
255
|
+
root.addHandler(sh)
|
|
256
|
+
|
|
257
|
+
def _load_state_store(self) -> None:
|
|
258
|
+
"""
|
|
259
|
+
Searches through the config object for a StateStoreConfig.
|
|
260
|
+
|
|
261
|
+
If found, it will use that configuration to generate a state store, if no such config is found it will either
|
|
262
|
+
create a LocalStateStore or a NoStateStore depending on whether the ``use_default_state_store`` argument to the
|
|
263
|
+
constructor was true or false.
|
|
264
|
+
|
|
265
|
+
Either way, the state_store attribute is guaranteed to be set after calling this method.
|
|
266
|
+
"""
|
|
267
|
+
state_store_config = self.application_config.state_store
|
|
268
|
+
|
|
269
|
+
if state_store_config:
|
|
270
|
+
self.state_store = state_store_config.create_state_store(
|
|
271
|
+
cdf_client=self.cognite_client,
|
|
272
|
+
default_to_local=self.USE_DEFAULT_STATE_STORE,
|
|
273
|
+
cancellation_token=self.cancellation_token,
|
|
274
|
+
)
|
|
275
|
+
elif self.USE_DEFAULT_STATE_STORE:
|
|
276
|
+
self.state_store = LocalStateStore("states.json", cancellation_token=self.cancellation_token)
|
|
277
|
+
else:
|
|
278
|
+
self.state_store = NoStateStore()
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
self.state_store.initialize()
|
|
282
|
+
except ValueError:
|
|
283
|
+
self._logger.exception("Could not load state store, using an empty state store as default")
|
|
284
|
+
self.state_store = NoStateStore()
|
|
189
285
|
|
|
190
|
-
|
|
286
|
+
Extractor._statestore_singleton = self.state_store
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def get_current_statestore(cls) -> AbstractStateStore:
|
|
290
|
+
"""
|
|
291
|
+
Get the current state store singleton.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
The current state store singleton
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ValueError: If no state store singleton has been created, meaning no state store has been loaded.
|
|
298
|
+
"""
|
|
299
|
+
if Extractor._statestore_singleton is None:
|
|
300
|
+
raise ValueError("No state store singleton created. Have a state store been loaded?")
|
|
301
|
+
return Extractor._statestore_singleton
|
|
191
302
|
|
|
192
303
|
def __init_tasks__(self) -> None:
|
|
193
304
|
"""
|
|
@@ -202,55 +313,37 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
202
313
|
def _set_runtime_message_queue(self, queue: Queue) -> None:
|
|
203
314
|
self._runtime_messages = queue
|
|
204
315
|
|
|
205
|
-
def
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
316
|
+
def _attach_runtime_controls(self, *, cancel_event: MpEvent, message_queue: Queue) -> None:
|
|
317
|
+
self._set_runtime_message_queue(message_queue)
|
|
318
|
+
self._setup_cancellation_watcher(cancel_event)
|
|
319
|
+
|
|
320
|
+
def _get_startup_request(self) -> StartupRequest:
|
|
321
|
+
return StartupRequest(
|
|
322
|
+
external_id=self.connection_config.integration.external_id,
|
|
323
|
+
active_config_revision=self.current_config_revision,
|
|
324
|
+
extractor=ExtractorInfo(version=self.VERSION, external_id=self.EXTERNAL_ID),
|
|
325
|
+
tasks=[
|
|
326
|
+
DtoTask(
|
|
327
|
+
type=TaskType.continuous if isinstance(t, ContinuousTask) else TaskType.batch,
|
|
328
|
+
action=isinstance(t, ScheduledTask),
|
|
329
|
+
description=t.description,
|
|
330
|
+
name=t.name,
|
|
331
|
+
)
|
|
332
|
+
for t in self._tasks
|
|
221
333
|
]
|
|
222
|
-
self.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
f"/api/v1/projects/{self.cognite_client.config.project}/integrations/checkin",
|
|
226
|
-
json={
|
|
227
|
-
"externalId": self.connection_config.integration,
|
|
228
|
-
"taskEvents": task_updates,
|
|
229
|
-
"errors": error_updates,
|
|
230
|
-
},
|
|
231
|
-
headers={"cdf-version": "alpha"},
|
|
334
|
+
if len(self._tasks) > 0
|
|
335
|
+
else None,
|
|
336
|
+
timestamp=int(self._start_time.timestamp() * 1000),
|
|
232
337
|
)
|
|
233
|
-
new_config_revision = res.json().get("lastConfigRevision")
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
new_config_revision
|
|
237
|
-
and self.current_config_revision != "local"
|
|
238
|
-
and new_config_revision > self.current_config_revision
|
|
239
|
-
):
|
|
240
|
-
self.restart()
|
|
241
338
|
|
|
242
339
|
def _run_checkin(self) -> None:
|
|
243
|
-
|
|
244
|
-
try:
|
|
245
|
-
self._logger.debug("Running checkin")
|
|
246
|
-
self._checkin()
|
|
247
|
-
except Exception:
|
|
248
|
-
self._logger.exception("Error during checkin")
|
|
249
|
-
self.cancellation_token.wait(10)
|
|
340
|
+
self._checkin_worker.run_periodic_checkin(self.cancellation_token, self._get_startup_request())
|
|
250
341
|
|
|
251
342
|
def _report_error(self, error: Error) -> None:
|
|
252
|
-
|
|
253
|
-
|
|
343
|
+
self._checkin_worker.report_error(error)
|
|
344
|
+
|
|
345
|
+
def _try_report_error(self, error: Error) -> None:
|
|
346
|
+
self._checkin_worker.try_report_error(error)
|
|
254
347
|
|
|
255
348
|
def _new_error(
|
|
256
349
|
self,
|
|
@@ -278,8 +371,8 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
278
371
|
self.cancellation_token.cancel()
|
|
279
372
|
|
|
280
373
|
@classmethod
|
|
281
|
-
def _init_from_runtime(cls, config: FullConfig[ConfigType]) -> Self:
|
|
282
|
-
return cls(config)
|
|
374
|
+
def _init_from_runtime(cls, config: FullConfig[ConfigType], checkin_worker: CheckinWorker) -> Self:
|
|
375
|
+
return cls(config, checkin_worker)
|
|
283
376
|
|
|
284
377
|
def add_task(self, task: Task) -> None:
|
|
285
378
|
"""
|
|
@@ -298,10 +391,7 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
298
391
|
A wrapped version of the task's target, with tracking and error handling.
|
|
299
392
|
"""
|
|
300
393
|
# Record a task start
|
|
301
|
-
|
|
302
|
-
self._task_updates.append(
|
|
303
|
-
TaskUpdate(type="started", name=task.name, timestamp=now()),
|
|
304
|
-
)
|
|
394
|
+
self._checkin_worker.report_task_start(name=task.name, timestamp=now())
|
|
305
395
|
|
|
306
396
|
try:
|
|
307
397
|
# Run task
|
|
@@ -320,10 +410,7 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
320
410
|
|
|
321
411
|
finally:
|
|
322
412
|
# Record task end
|
|
323
|
-
|
|
324
|
-
self._task_updates.append(
|
|
325
|
-
TaskUpdate(type="ended", name=task.name, timestamp=now()),
|
|
326
|
-
)
|
|
413
|
+
self._checkin_worker.report_task_end(name=task.name, timestamp=now())
|
|
327
414
|
|
|
328
415
|
task.target = run_task
|
|
329
416
|
self._tasks.append(task)
|
|
@@ -341,29 +428,6 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
341
428
|
),
|
|
342
429
|
)
|
|
343
430
|
|
|
344
|
-
def _report_extractor_info(self) -> None:
|
|
345
|
-
self.cognite_client.post(
|
|
346
|
-
f"/api/v1/projects/{self.cognite_client.config.project}/integrations/extractorinfo",
|
|
347
|
-
json={
|
|
348
|
-
"externalId": self.connection_config.integration,
|
|
349
|
-
"activeConfigRevision": self.current_config_revision,
|
|
350
|
-
"extractor": {
|
|
351
|
-
"version": self.VERSION,
|
|
352
|
-
"externalId": self.EXTERNAL_ID,
|
|
353
|
-
},
|
|
354
|
-
"tasks": [
|
|
355
|
-
{
|
|
356
|
-
"name": t.name,
|
|
357
|
-
"type": "continuous" if isinstance(t, ContinuousTask) else "batch",
|
|
358
|
-
"action": bool(isinstance(t, ScheduledTask)),
|
|
359
|
-
"description": t.description,
|
|
360
|
-
}
|
|
361
|
-
for t in self._tasks
|
|
362
|
-
],
|
|
363
|
-
},
|
|
364
|
-
headers={"cdf-version": "alpha"},
|
|
365
|
-
)
|
|
366
|
-
|
|
367
431
|
def start(self) -> None:
|
|
368
432
|
"""
|
|
369
433
|
Start the extractor.
|
|
@@ -372,7 +436,11 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
372
436
|
``with`` statement, which ensures proper cleanup on exit.
|
|
373
437
|
"""
|
|
374
438
|
self._setup_logging()
|
|
375
|
-
self.
|
|
439
|
+
self._start_time = datetime.now(tz=timezone.utc)
|
|
440
|
+
|
|
441
|
+
self._load_state_store()
|
|
442
|
+
self.state_store.start()
|
|
443
|
+
|
|
376
444
|
Thread(target=self._run_checkin, name="ExtractorCheckin", daemon=True).start()
|
|
377
445
|
|
|
378
446
|
def stop(self) -> None:
|
|
@@ -401,10 +469,11 @@ class Extractor(Generic[ConfigType], CogniteLogger):
|
|
|
401
469
|
Stop the extractor when exiting the context manager.
|
|
402
470
|
"""
|
|
403
471
|
self.stop()
|
|
404
|
-
with self._checkin_lock:
|
|
405
|
-
self._checkin()
|
|
406
472
|
|
|
407
|
-
self.
|
|
473
|
+
if self.state_store:
|
|
474
|
+
self.state_store.synchronize()
|
|
475
|
+
|
|
476
|
+
self._checkin_worker.flush(self.cancellation_token)
|
|
408
477
|
return exc_val is None
|
|
409
478
|
|
|
410
479
|
def run(self) -> None:
|