atlan-application-sdk-api 3.51.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.
- application_sdk/__init__.py +7 -0
- application_sdk/_build_info.py +62 -0
- application_sdk/_context_errors.py +65 -0
- application_sdk/_install.py +130 -0
- application_sdk/_logging.py +171 -0
- application_sdk/_runtime/__init__.py +29 -0
- application_sdk/_runtime/offload.py +1020 -0
- application_sdk/_runtime/progress.py +757 -0
- application_sdk/clients/__init__.py +63 -0
- application_sdk/clients/_interface.py +58 -0
- application_sdk/clients/_sql_failure_classifier.py +112 -0
- application_sdk/clients/base.py +565 -0
- application_sdk/clients/http_errors.py +28 -0
- application_sdk/clients/models.py +49 -0
- application_sdk/clients/sql.py +1218 -0
- application_sdk/clients/sql_errors.py +192 -0
- application_sdk/clients/sql_typecasters.py +431 -0
- application_sdk/clients/ssl_utils.py +247 -0
- application_sdk/common/__init__.py +53 -0
- application_sdk/common/_generated_tree.py +321 -0
- application_sdk/common/_listing.py +192 -0
- application_sdk/common/aws_utils.py +522 -0
- application_sdk/common/aws_utils_errors.py +88 -0
- application_sdk/common/build_identity.py +120 -0
- application_sdk/common/concurrency.py +45 -0
- application_sdk/common/errors.py +99 -0
- application_sdk/common/filter_matching.py +261 -0
- application_sdk/common/logger.py +11 -0
- application_sdk/common/models.py +31 -0
- application_sdk/common/sql_filters.py +719 -0
- application_sdk/common/sql_filters_errors.py +17 -0
- application_sdk/common/task_queue.py +431 -0
- application_sdk/common/types.py +51 -0
- application_sdk/common/utils.py +87 -0
- application_sdk/constants.py +1120 -0
- application_sdk/contracts/__init__.py +158 -0
- application_sdk/contracts/base.py +1178 -0
- application_sdk/contracts/events.py +251 -0
- application_sdk/contracts/types.py +801 -0
- application_sdk/contracts/types_errors.py +17 -0
- application_sdk/credentials/__init__.py +326 -0
- application_sdk/credentials/errors.py +226 -0
- application_sdk/credentials/extra.py +114 -0
- application_sdk/credentials/ingress.py +283 -0
- application_sdk/credentials/ref.py +376 -0
- application_sdk/credentials/spec.py +182 -0
- application_sdk/credentials/types.py +339 -0
- application_sdk/credentials/utils.py +257 -0
- application_sdk/errors/__init__.py +224 -0
- application_sdk/errors/base.py +504 -0
- application_sdk/errors/categories.py +118 -0
- application_sdk/errors/http.py +81 -0
- application_sdk/errors/leaves.py +526 -0
- application_sdk/errors/wire.py +124 -0
- application_sdk/handler/__init__.py +97 -0
- application_sdk/handler/_warmup.py +198 -0
- application_sdk/handler/asgi.py +252 -0
- application_sdk/handler/base.py +281 -0
- application_sdk/handler/context.py +212 -0
- application_sdk/handler/contracts.py +1166 -0
- application_sdk/handler/manifest.py +81 -0
- application_sdk/handler/metadata_routes.py +699 -0
- application_sdk/handler/object_routes.py +418 -0
- application_sdk/handler/request_contract.py +91 -0
- application_sdk/handler/routes.py +1118 -0
- application_sdk/handler/service_errors.py +37 -0
- application_sdk/server/__init__.py +15 -0
- application_sdk/server/fastapi/models.py +338 -0
- application_sdk/templates/__init__.py +52 -0
- application_sdk/templates/contracts/__init__.py +111 -0
- application_sdk/templates/contracts/base_metadata_extraction.py +32 -0
- application_sdk/templates/contracts/incremental_sql.py +563 -0
- application_sdk/templates/contracts/sql_metadata.py +824 -0
- application_sdk/templates/contracts/sql_query.py +105 -0
- application_sdk/version.py +40 -0
- atlan_application_sdk_api-3.51.0.dist-info/METADATA +230 -0
- atlan_application_sdk_api-3.51.0.dist-info/RECORD +78 -0
- atlan_application_sdk_api-3.51.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Typed contracts for incremental SQL metadata extraction.
|
|
2
|
+
|
|
3
|
+
Provides the full contract hierarchy for the v3 incremental extraction template,
|
|
4
|
+
including the ``IncrementalRunContext`` local accumulator and all per-task
|
|
5
|
+
input/output types.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from pydantic import Field, field_serializer, field_validator
|
|
14
|
+
|
|
15
|
+
from application_sdk.contracts.base import Input, Output, SerializableEnum
|
|
16
|
+
from application_sdk.templates.contracts.sql_metadata import (
|
|
17
|
+
ExtractionInput,
|
|
18
|
+
ExtractionOutput,
|
|
19
|
+
ExtractionTaskInput,
|
|
20
|
+
FetchColumnsOutput,
|
|
21
|
+
FetchDatabasesOutput,
|
|
22
|
+
FetchSchemasOutput,
|
|
23
|
+
FetchTablesOutput,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# BLDX-518: marker_timestamp is substituted into incremental SQL templates
|
|
27
|
+
# via ``str.replace``, so untrusted input could carry SQL escape sequences
|
|
28
|
+
# (the marker is sourced from a persistent file but can also be overridden
|
|
29
|
+
# from the workflow input — see ``FetchIncrementalMarkerInput.existing_marker``).
|
|
30
|
+
#
|
|
31
|
+
# Accept either:
|
|
32
|
+
# * Empty string — signals "no marker, perform a full extraction".
|
|
33
|
+
# * ISO-8601 / RFC-3339 timestamp — date + time, optional fractional
|
|
34
|
+
# seconds, optional timezone offset. ``T`` or space between date and
|
|
35
|
+
# time is allowed to match the variants different sources emit.
|
|
36
|
+
#
|
|
37
|
+
# Anything else (including SQL escape chars) is rejected by the validator
|
|
38
|
+
# regardless of whether the value came from object storage or a workflow
|
|
39
|
+
# override.
|
|
40
|
+
_ISO_TIMESTAMP_PATTERN = re.compile(
|
|
41
|
+
r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validate_marker_timestamp(value: str | None) -> str | None:
|
|
46
|
+
"""Reject marker timestamps that don't match ISO-8601 / empty.
|
|
47
|
+
|
|
48
|
+
Empty / None passes through (no marker = full-extraction signal).
|
|
49
|
+
Anything else must match :data:`_ISO_TIMESTAMP_PATTERN`. Raises
|
|
50
|
+
``ValueError`` so Pydantic surfaces it as ``ValidationError``.
|
|
51
|
+
|
|
52
|
+
Assumption: all SDK-produced markers are ISO-8601 timestamps or empty
|
|
53
|
+
strings, so adding this validator to Output contracts
|
|
54
|
+
(``FetchIncrementalMarkerOutput``, ``UpdateMarkerOutput``) is safe for
|
|
55
|
+
Temporal workflow replay — no well-formed history entry would be
|
|
56
|
+
rejected by this constraint.
|
|
57
|
+
"""
|
|
58
|
+
if value is None or value == "":
|
|
59
|
+
return value
|
|
60
|
+
if not _ISO_TIMESTAMP_PATTERN.match(value):
|
|
61
|
+
# conformance: ignore[E012] Pydantic field_validator requires ValueError to surface as ValidationError; replacing with AppError would break Pydantic validation contract
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"marker_timestamp must be empty or an ISO-8601 / RFC-3339 timestamp "
|
|
64
|
+
f"(got {value!r}). Reject reason: prevents SQL injection via "
|
|
65
|
+
"templates that substitute the marker into a quoted literal."
|
|
66
|
+
)
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_wire_marker(value: str) -> str:
|
|
71
|
+
"""Validate a wire-form marker field (a ``str``, ``""`` for none)."""
|
|
72
|
+
_validate_marker_timestamp(value)
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# Marker sentinel boundary
|
|
78
|
+
# =============================================================================
|
|
79
|
+
#
|
|
80
|
+
# "No marker" has one Python spelling, ``None`` (``IncrementalRunContext`` and
|
|
81
|
+
# ``MarkerPair``), and one wire spelling, ``""`` (every task contract field, so
|
|
82
|
+
# existing Temporal histories and app payloads keep their shape). These two
|
|
83
|
+
# functions are the only place one becomes the other.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def marker_to_wire(marker: str | None) -> str:
|
|
87
|
+
"""Encode a Python marker for a task contract: ``None`` becomes ``""``."""
|
|
88
|
+
return "" if marker is None else marker
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def marker_from_wire(value: str) -> str | None:
|
|
92
|
+
"""Decode a task contract's marker field: ``""`` becomes ``None``."""
|
|
93
|
+
return value or None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# IncrementalRunContext — local accumulator, NOT a Temporal payload
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclasses.dataclass
|
|
102
|
+
class IncrementalRunContext:
|
|
103
|
+
"""Incremental extraction state accumulated across tasks within a single run().
|
|
104
|
+
|
|
105
|
+
This context is a local variable in run() — NOT stored in app_state, NOT a
|
|
106
|
+
Temporal payload. It is built progressively as task outputs are collected.
|
|
107
|
+
Temporal's workflow replay reconstructs it deterministically from recorded
|
|
108
|
+
activity outputs, so it is crash-safe.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
workflow_id: str = ""
|
|
112
|
+
workflow_run_id: str = ""
|
|
113
|
+
output_prefix: str = ""
|
|
114
|
+
output_path: str = ""
|
|
115
|
+
connection_qualified_name: str = ""
|
|
116
|
+
connection_name: str = ""
|
|
117
|
+
application_name: str = ""
|
|
118
|
+
incremental_extraction: bool = False
|
|
119
|
+
column_batch_size: int = 25000
|
|
120
|
+
column_chunk_size: int = 100000
|
|
121
|
+
copy_workers: int = 3
|
|
122
|
+
upload_concurrency: int = 4
|
|
123
|
+
prepone_marker_timestamp: bool = True
|
|
124
|
+
prepone_marker_hours: int = 3
|
|
125
|
+
# Set by fetch_incremental_marker
|
|
126
|
+
marker_timestamp: str | None = None
|
|
127
|
+
next_marker_timestamp: str = ""
|
|
128
|
+
# Set by read_current_state
|
|
129
|
+
current_state_available: bool = False
|
|
130
|
+
current_state_path: str = ""
|
|
131
|
+
current_state_s3_prefix: str = ""
|
|
132
|
+
current_state_json_count: int = 0
|
|
133
|
+
# Set by write_current_state
|
|
134
|
+
current_state_files: int = 0
|
|
135
|
+
incremental_diff_path: str = ""
|
|
136
|
+
incremental_diff_s3_prefix: str = ""
|
|
137
|
+
incremental_diff_files: int = 0
|
|
138
|
+
# Set by prepare_column_extraction_queries
|
|
139
|
+
total_column_batches: int = 0
|
|
140
|
+
changed_tables: int = 0
|
|
141
|
+
backfill_tables: int = 0
|
|
142
|
+
|
|
143
|
+
def is_incremental_ready(self) -> bool:
|
|
144
|
+
"""Return True when all prerequisites for incremental mode are satisfied.
|
|
145
|
+
|
|
146
|
+
Requires: incremental_extraction=True AND a marker from a previous run AND
|
|
147
|
+
a current-state snapshot from that run. On the very first run, marker_timestamp
|
|
148
|
+
is None and current_state_available is False, so this returns False and the
|
|
149
|
+
run performs a full extraction — building the initial state for future runs.
|
|
150
|
+
"""
|
|
151
|
+
return bool(
|
|
152
|
+
self.incremental_extraction
|
|
153
|
+
and self.marker_timestamp
|
|
154
|
+
and self.current_state_available
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# =============================================================================
|
|
159
|
+
# Top-level input/output for IncrementalSqlMetadataExtractor.run()
|
|
160
|
+
# =============================================================================
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class IncrementalExtractionInput(ExtractionInput):
|
|
164
|
+
"""Top-level input for an incremental SQL metadata extraction run.
|
|
165
|
+
|
|
166
|
+
Extends :class:`ExtractionInput` with incremental-specific configuration.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
incremental_extraction: bool = False
|
|
170
|
+
"""Enable incremental extraction mode."""
|
|
171
|
+
|
|
172
|
+
column_batch_size: int = 25000
|
|
173
|
+
"""Number of tables per batch for incremental column extraction."""
|
|
174
|
+
|
|
175
|
+
column_chunk_size: int = 100000
|
|
176
|
+
"""Number of column records per output chunk file."""
|
|
177
|
+
|
|
178
|
+
copy_workers: int = 3
|
|
179
|
+
"""Parallel workers for file copy operations during state snapshot."""
|
|
180
|
+
|
|
181
|
+
upload_concurrency: int = Field(default=4, gt=0)
|
|
182
|
+
"""Max concurrent object-store requests for the current-state and
|
|
183
|
+
incremental-diff uploads. Connections with very large column counts
|
|
184
|
+
(100K+) can raise this to shrink wall-clock time on the upload step."""
|
|
185
|
+
|
|
186
|
+
prepone_marker_timestamp: bool = True
|
|
187
|
+
"""Whether to move the marker back by ``prepone_marker_hours``."""
|
|
188
|
+
|
|
189
|
+
prepone_marker_hours: int = 3
|
|
190
|
+
"""Hours to subtract from the marker when preponing is enabled."""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class IncrementalExtractionOutput(ExtractionOutput):
|
|
194
|
+
"""Top-level output from an incremental SQL metadata extraction run.
|
|
195
|
+
|
|
196
|
+
Extends :class:`ExtractionOutput` with incremental-specific statistics.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
current_state_files: int = 0
|
|
200
|
+
"""Number of files written to the current-state snapshot."""
|
|
201
|
+
|
|
202
|
+
incremental_diff_files: int = 0
|
|
203
|
+
"""Number of files in the incremental diff (0 on first run)."""
|
|
204
|
+
|
|
205
|
+
column_batches_executed: int = 0
|
|
206
|
+
"""Number of incremental column batches executed."""
|
|
207
|
+
|
|
208
|
+
changed_tables: int = 0
|
|
209
|
+
"""Tables detected as changed since last run."""
|
|
210
|
+
|
|
211
|
+
backfill_tables: int = 0
|
|
212
|
+
"""Tables detected as needing backfill (new tables)."""
|
|
213
|
+
|
|
214
|
+
marker_updated: bool = False
|
|
215
|
+
"""Whether the incremental marker was updated after this run."""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# =============================================================================
|
|
219
|
+
# Shared incremental task input base
|
|
220
|
+
# =============================================================================
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class IncrementalTaskInput(ExtractionTaskInput):
|
|
224
|
+
"""Base task input with incremental runtime state.
|
|
225
|
+
|
|
226
|
+
Extends :class:`ExtractionTaskInput` with the incremental fields that
|
|
227
|
+
every incremental task needs to decide between full and incremental paths.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
incremental_extraction: bool = False
|
|
231
|
+
"""Whether incremental extraction is enabled for this run."""
|
|
232
|
+
|
|
233
|
+
marker_timestamp: str = ""
|
|
234
|
+
"""Marker timestamp from previous run; empty string means full extraction."""
|
|
235
|
+
|
|
236
|
+
current_state_available: bool = False
|
|
237
|
+
"""Whether a current-state snapshot from a previous run is available."""
|
|
238
|
+
|
|
239
|
+
column_chunk_size: int = 100000
|
|
240
|
+
"""Number of column records per output chunk file."""
|
|
241
|
+
|
|
242
|
+
@field_validator("marker_timestamp", mode="after")
|
|
243
|
+
@classmethod
|
|
244
|
+
def _validate_marker(cls, v: str) -> str:
|
|
245
|
+
return _validate_wire_marker(v)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# =============================================================================
|
|
249
|
+
# Per-task incremental input types (inherit IncrementalTaskInput)
|
|
250
|
+
# =============================================================================
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class FetchTablesIncrementalInput(IncrementalTaskInput):
|
|
254
|
+
"""Input for the incremental fetch_tables task.
|
|
255
|
+
|
|
256
|
+
Carries all fields from :class:`IncrementalTaskInput`. The marker and
|
|
257
|
+
current_state_available fields together indicate whether to use the
|
|
258
|
+
incremental or full-extraction SQL path — see the ``fetch_tables``
|
|
259
|
+
docstring in :class:`IncrementalSqlMetadataExtractor` for details.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class FetchColumnsIncrementalInput(IncrementalTaskInput):
|
|
264
|
+
"""Input for the incremental fetch_columns task.
|
|
265
|
+
|
|
266
|
+
When both ``marker_timestamp`` is non-empty and ``current_state_available``
|
|
267
|
+
is True, the base ``fetch_columns`` implementation returns immediately with
|
|
268
|
+
zero counts — column extraction is delegated to batch tasks instead.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# =============================================================================
|
|
273
|
+
# fetch_incremental_marker task
|
|
274
|
+
# =============================================================================
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class FetchIncrementalMarkerInput(Input):
|
|
278
|
+
"""Input for the fetch_incremental_marker task."""
|
|
279
|
+
|
|
280
|
+
connection_qualified_name: str = ""
|
|
281
|
+
"""Connection qualified name used to locate the persistent marker file."""
|
|
282
|
+
|
|
283
|
+
application_name: str = ""
|
|
284
|
+
"""Application name for S3 path resolution."""
|
|
285
|
+
|
|
286
|
+
existing_marker: str | None = None
|
|
287
|
+
"""Pre-existing marker value (e.g., from a manual workflow override)."""
|
|
288
|
+
|
|
289
|
+
prepone_enabled: bool = True
|
|
290
|
+
"""Whether to move the marker back by ``prepone_hours``."""
|
|
291
|
+
|
|
292
|
+
prepone_hours: float = 3.0
|
|
293
|
+
"""Hours to subtract from the marker when preponing is enabled."""
|
|
294
|
+
|
|
295
|
+
@field_validator("existing_marker", mode="after")
|
|
296
|
+
@classmethod
|
|
297
|
+
def _validate_existing_marker(cls, v: str | None) -> str | None:
|
|
298
|
+
return _validate_marker_timestamp(v)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class FetchIncrementalMarkerOutput(Output):
|
|
302
|
+
"""Output from the fetch_incremental_marker task."""
|
|
303
|
+
|
|
304
|
+
marker_timestamp: str = ""
|
|
305
|
+
"""Processed marker from the previous run; empty on the first run."""
|
|
306
|
+
|
|
307
|
+
next_marker_timestamp: str = ""
|
|
308
|
+
"""New marker timestamp generated for the current run."""
|
|
309
|
+
|
|
310
|
+
@field_validator("marker_timestamp", "next_marker_timestamp", mode="after")
|
|
311
|
+
@classmethod
|
|
312
|
+
def _validate_marker(cls, v: str) -> str:
|
|
313
|
+
return _validate_wire_marker(v)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# =============================================================================
|
|
317
|
+
# read_current_state task
|
|
318
|
+
# =============================================================================
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ReadCurrentStateInput(Input):
|
|
322
|
+
"""Input for the read_current_state task."""
|
|
323
|
+
|
|
324
|
+
connection_qualified_name: str = ""
|
|
325
|
+
application_name: str = ""
|
|
326
|
+
output_path: str = ""
|
|
327
|
+
"""The run's output path. Only read when the snapshot is materialized on
|
|
328
|
+
read (an ``after_current_state_read`` or legacy ``read_current_state``
|
|
329
|
+
override), which puts it under ``{output_path}/incremental/previous-state``."""
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class ReadCurrentStateOutput(Output):
|
|
333
|
+
"""Output from the read_current_state task."""
|
|
334
|
+
|
|
335
|
+
current_state_path: str = ""
|
|
336
|
+
"""Local path the snapshot was materialized into, or ``""`` when the read
|
|
337
|
+
only probed (the default: nothing is downloaded until a later task needs
|
|
338
|
+
the files)."""
|
|
339
|
+
|
|
340
|
+
current_state_s3_prefix: str = ""
|
|
341
|
+
"""S3 prefix for the current-state folder."""
|
|
342
|
+
|
|
343
|
+
current_state_available: bool = False
|
|
344
|
+
"""Whether a non-empty current-state snapshot was found."""
|
|
345
|
+
|
|
346
|
+
current_state_json_count: int = 0
|
|
347
|
+
"""Number of JSON files in the committed current state."""
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# =============================================================================
|
|
351
|
+
# prepare_column_extraction_queries task
|
|
352
|
+
# =============================================================================
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class PrepareColumnQueriesInput(IncrementalTaskInput):
|
|
356
|
+
"""Input for the prepare_column_extraction_queries task."""
|
|
357
|
+
|
|
358
|
+
connection_qualified_name: str = ""
|
|
359
|
+
"""Connection qualified name for persistent artifact path resolution."""
|
|
360
|
+
|
|
361
|
+
current_state_s3_prefix: str = ""
|
|
362
|
+
"""S3 prefix of the current-state snapshot for backfill comparison."""
|
|
363
|
+
|
|
364
|
+
column_batch_size: int = 25000
|
|
365
|
+
"""Number of tables per batch file."""
|
|
366
|
+
|
|
367
|
+
application_name: str = ""
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class PrepareColumnQueriesOutput(Output):
|
|
371
|
+
"""Output from the prepare_column_extraction_queries task."""
|
|
372
|
+
|
|
373
|
+
total_batches: int = 0
|
|
374
|
+
changed_tables: int = 0
|
|
375
|
+
backfill_tables: int = 0
|
|
376
|
+
total_tables: int = 0
|
|
377
|
+
batches_s3_prefix: str = ""
|
|
378
|
+
batches_local_dir: str = ""
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# =============================================================================
|
|
382
|
+
# execute_single_column_batch task
|
|
383
|
+
# =============================================================================
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class ExecuteColumnBatchInput(IncrementalTaskInput):
|
|
387
|
+
"""Input for executing a single incremental column batch."""
|
|
388
|
+
|
|
389
|
+
batch_index: int = 0
|
|
390
|
+
"""Zero-based index of this batch within the total."""
|
|
391
|
+
|
|
392
|
+
total_batches: int = 1
|
|
393
|
+
"""Total number of batches."""
|
|
394
|
+
|
|
395
|
+
batches_s3_prefix: str = ""
|
|
396
|
+
"""S3 prefix where the batch JSON files are stored."""
|
|
397
|
+
|
|
398
|
+
application_name: str = ""
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class ColumnBatchStatus(SerializableEnum):
|
|
402
|
+
"""Outcome of one ``execute_single_column_batch`` task.
|
|
403
|
+
|
|
404
|
+
A ``StrEnum``: members compare equal to, and serialize as, the plain
|
|
405
|
+
strings the field carried before it was typed.
|
|
406
|
+
|
|
407
|
+
Members:
|
|
408
|
+
SUCCESS: The batch file was found and its SQL ran.
|
|
409
|
+
NOT_FOUND: The batch file was missing, so nothing ran.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
SUCCESS = "success"
|
|
413
|
+
NOT_FOUND = "not_found"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class ExecuteColumnBatchOutput(Output):
|
|
417
|
+
"""Output from executing a single incremental column batch."""
|
|
418
|
+
|
|
419
|
+
batch_index: int = 0
|
|
420
|
+
records: int = 0
|
|
421
|
+
# Pre-dates BLDX-1244's standard Output.status (``OutputStatus`` enum)
|
|
422
|
+
# and uses domain-specific values ("not_found", "success") that aren't
|
|
423
|
+
# part of that vocabulary, hence its own enum; the ignore acknowledges
|
|
424
|
+
# the deliberate field-type narrowing. ``None`` is "not set" in Python;
|
|
425
|
+
# on the wire it stays ``""``, the old default, in both directions.
|
|
426
|
+
status: ColumnBatchStatus | None = None # type: ignore[assignment]
|
|
427
|
+
|
|
428
|
+
@field_validator("status", mode="before")
|
|
429
|
+
@classmethod
|
|
430
|
+
def _legacy_unset_status(cls, v: object) -> object:
|
|
431
|
+
# A payload recorded before the field was typed carries "" for "not
|
|
432
|
+
# set"; it must still deserialize on replay.
|
|
433
|
+
return None if v == "" else v
|
|
434
|
+
|
|
435
|
+
@field_serializer("status")
|
|
436
|
+
def _unset_status_as_empty_string(self, v: ColumnBatchStatus | None) -> str:
|
|
437
|
+
# The field was a plain str defaulting to "": a consumer reading the
|
|
438
|
+
# payload expects a string, never null.
|
|
439
|
+
return "" if v is None else v.value
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# =============================================================================
|
|
443
|
+
# write_current_state task
|
|
444
|
+
# =============================================================================
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class WriteCurrentStateInput(IncrementalTaskInput):
|
|
448
|
+
"""Input for the write_current_state task."""
|
|
449
|
+
|
|
450
|
+
workflow_run_id: str = Field(
|
|
451
|
+
default="",
|
|
452
|
+
deprecated=(
|
|
453
|
+
"WriteCurrentStateInput.workflow_run_id is deprecated and ignored; "
|
|
454
|
+
"write_current_state reads the run ID from its own Temporal context, "
|
|
455
|
+
"so stop passing it — will be removed in v4.0.0."
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
"""Deprecated and ignored; the task uses its own Temporal run ID instead.
|
|
459
|
+
|
|
460
|
+
That run ID stamps the current-state snapshot and keys the incremental
|
|
461
|
+
diff. A value here that disagrees with it is logged at WARNING. Will be
|
|
462
|
+
removed in v4.0.0."""
|
|
463
|
+
|
|
464
|
+
current_state_s3_prefix: str = ""
|
|
465
|
+
"""S3 prefix for the existing current-state (for previous-state download)."""
|
|
466
|
+
|
|
467
|
+
copy_workers: int = 3
|
|
468
|
+
"""Parallel workers for file copy operations."""
|
|
469
|
+
|
|
470
|
+
upload_concurrency: int = Field(default=4, gt=0)
|
|
471
|
+
"""Max concurrent object-store requests when uploading the current-state
|
|
472
|
+
snapshot and incremental diff. Connections with very large column counts
|
|
473
|
+
(100K+) can raise this to shrink wall-clock time on the upload step,
|
|
474
|
+
which otherwise runs at this same fixed default regardless of scale."""
|
|
475
|
+
|
|
476
|
+
application_name: str = ""
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class WriteCurrentStateOutput(Output):
|
|
480
|
+
"""Output from the write_current_state task."""
|
|
481
|
+
|
|
482
|
+
current_state_path: str = ""
|
|
483
|
+
current_state_s3_prefix: str = ""
|
|
484
|
+
current_state_files: int = 0
|
|
485
|
+
incremental_diff_path: str = ""
|
|
486
|
+
incremental_diff_s3_prefix: str = ""
|
|
487
|
+
incremental_diff_files: int = 0
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# =============================================================================
|
|
491
|
+
# update_incremental_marker task
|
|
492
|
+
# =============================================================================
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class UpdateMarkerInput(Input):
|
|
496
|
+
"""Input for the update_incremental_marker task."""
|
|
497
|
+
|
|
498
|
+
connection_qualified_name: str = ""
|
|
499
|
+
next_marker_timestamp: str = ""
|
|
500
|
+
application_name: str = ""
|
|
501
|
+
|
|
502
|
+
@field_validator("next_marker_timestamp", mode="after")
|
|
503
|
+
@classmethod
|
|
504
|
+
def _validate_marker(cls, v: str) -> str:
|
|
505
|
+
return _validate_wire_marker(v)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class UpdateMarkerOutput(Output):
|
|
509
|
+
"""Output from the update_incremental_marker task."""
|
|
510
|
+
|
|
511
|
+
marker_written: bool = False
|
|
512
|
+
marker_timestamp: str = ""
|
|
513
|
+
s3_key: str = ""
|
|
514
|
+
|
|
515
|
+
@field_validator("marker_timestamp", mode="after")
|
|
516
|
+
@classmethod
|
|
517
|
+
def _validate_marker(cls, v: str) -> str:
|
|
518
|
+
return _validate_wire_marker(v)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# =============================================================================
|
|
522
|
+
# Re-export base types used in incremental contracts for convenience
|
|
523
|
+
# =============================================================================
|
|
524
|
+
|
|
525
|
+
__all__ = [
|
|
526
|
+
# Context (not a Temporal payload)
|
|
527
|
+
"IncrementalRunContext",
|
|
528
|
+
# Marker sentinel boundary
|
|
529
|
+
"marker_from_wire",
|
|
530
|
+
"marker_to_wire",
|
|
531
|
+
# Top-level run() contracts
|
|
532
|
+
"IncrementalExtractionInput",
|
|
533
|
+
"IncrementalExtractionOutput",
|
|
534
|
+
# Shared incremental task base
|
|
535
|
+
"IncrementalTaskInput",
|
|
536
|
+
# Per-task incremental inputs (override of base extraction tasks)
|
|
537
|
+
"FetchTablesIncrementalInput",
|
|
538
|
+
"FetchColumnsIncrementalInput",
|
|
539
|
+
# fetch_incremental_marker
|
|
540
|
+
"FetchIncrementalMarkerInput",
|
|
541
|
+
"FetchIncrementalMarkerOutput",
|
|
542
|
+
# read_current_state
|
|
543
|
+
"ReadCurrentStateInput",
|
|
544
|
+
"ReadCurrentStateOutput",
|
|
545
|
+
# prepare_column_extraction_queries
|
|
546
|
+
"PrepareColumnQueriesInput",
|
|
547
|
+
"PrepareColumnQueriesOutput",
|
|
548
|
+
# execute_single_column_batch
|
|
549
|
+
"ColumnBatchStatus",
|
|
550
|
+
"ExecuteColumnBatchInput",
|
|
551
|
+
"ExecuteColumnBatchOutput",
|
|
552
|
+
# write_current_state
|
|
553
|
+
"WriteCurrentStateInput",
|
|
554
|
+
"WriteCurrentStateOutput",
|
|
555
|
+
# update_incremental_marker
|
|
556
|
+
"UpdateMarkerInput",
|
|
557
|
+
"UpdateMarkerOutput",
|
|
558
|
+
# Re-exported base output types (used in incremental extractor signatures)
|
|
559
|
+
"FetchDatabasesOutput",
|
|
560
|
+
"FetchSchemasOutput",
|
|
561
|
+
"FetchTablesOutput",
|
|
562
|
+
"FetchColumnsOutput",
|
|
563
|
+
]
|