elementary-runtime 0.1.0rc1__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.
- elementary_runtime/__init__.py +5 -0
- elementary_runtime/common/__init__.py +0 -0
- elementary_runtime/common/validators.py +27 -0
- elementary_runtime/config/__init__.py +40 -0
- elementary_runtime/config/base.py +19 -0
- elementary_runtime/config/connections.py +55 -0
- elementary_runtime/config/env.py +66 -0
- elementary_runtime/config/errors.py +20 -0
- elementary_runtime/config/runtime.py +60 -0
- elementary_runtime/config/secrets.py +77 -0
- elementary_runtime/config/sections.py +98 -0
- elementary_runtime/coordinator/__init__.py +0 -0
- elementary_runtime/execution/__init__.py +0 -0
- elementary_runtime/execution/redaction/__init__.py +0 -0
- elementary_runtime/execution/sql/__init__.py +0 -0
- elementary_runtime/execution/worker/__init__.py +0 -0
- elementary_runtime/protocol/__init__.py +162 -0
- elementary_runtime/protocol/base.py +82 -0
- elementary_runtime/protocol/enums.py +89 -0
- elementary_runtime/protocol/errors.py +28 -0
- elementary_runtime/protocol/http.py +15 -0
- elementary_runtime/protocol/instances.py +51 -0
- elementary_runtime/protocol/limits.py +18 -0
- elementary_runtime/protocol/tasks/__init__.py +80 -0
- elementary_runtime/protocol/tasks/custom_sql_test.py +35 -0
- elementary_runtime/protocol/tasks/envelope.py +20 -0
- elementary_runtime/protocol/tasks/heartbeat.py +34 -0
- elementary_runtime/protocol/tasks/poll.py +20 -0
- elementary_runtime/protocol/tasks/registry.py +35 -0
- elementary_runtime/protocol/tasks/results.py +30 -0
- elementary_runtime/protocol/tasks/run_sql.py +29 -0
- elementary_runtime/protocol/tasks/sql_query.py +102 -0
- elementary_runtime/protocol/telemetry.py +99 -0
- elementary_runtime/py.typed +0 -0
- elementary_runtime/warehouses/__init__.py +0 -0
- elementary_runtime-0.1.0rc1.dist-info/METADATA +95 -0
- elementary_runtime-0.1.0rc1.dist-info/RECORD +38 -0
- elementary_runtime-0.1.0rc1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import (
|
|
6
|
+
AfterValidator,
|
|
7
|
+
AwareDatetime,
|
|
8
|
+
BaseModel,
|
|
9
|
+
ConfigDict,
|
|
10
|
+
PlainSerializer,
|
|
11
|
+
)
|
|
12
|
+
from pydantic.types import StringConstraints
|
|
13
|
+
|
|
14
|
+
from elementary_runtime.protocol.limits import (
|
|
15
|
+
MAX_DISPLAY_NAME_LENGTH,
|
|
16
|
+
MAX_ERROR_MESSAGE_LENGTH,
|
|
17
|
+
MAX_ID_LENGTH,
|
|
18
|
+
MAX_INSTANCE_ID_LENGTH,
|
|
19
|
+
MAX_INSTANCE_ID_PREFIX_LENGTH,
|
|
20
|
+
MAX_NON_EMPTY_STR_LENGTH,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
INSTANCE_ID_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._:/-]*$"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProtocolModel(BaseModel):
|
|
27
|
+
model_config = ConfigDict(extra="forbid", frozen=True, allow_inf_nan=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProtocolEnum(str, Enum):
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return str(self.value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_utc(value: datetime) -> datetime:
|
|
36
|
+
return value.astimezone(timezone.utc)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def utc_isoformat(value: datetime) -> str:
|
|
40
|
+
return value.isoformat().replace("+00:00", "Z")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
UtcDatetime = Annotated[
|
|
44
|
+
AwareDatetime,
|
|
45
|
+
AfterValidator(to_utc),
|
|
46
|
+
PlainSerializer(utc_isoformat, when_used="json"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
InstanceId = Annotated[
|
|
50
|
+
str,
|
|
51
|
+
StringConstraints(
|
|
52
|
+
min_length=1,
|
|
53
|
+
max_length=MAX_INSTANCE_ID_LENGTH,
|
|
54
|
+
pattern=INSTANCE_ID_PATTERN,
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
InstanceIdPrefix = Annotated[
|
|
59
|
+
str,
|
|
60
|
+
StringConstraints(
|
|
61
|
+
min_length=1,
|
|
62
|
+
max_length=MAX_INSTANCE_ID_PREFIX_LENGTH,
|
|
63
|
+
pattern=INSTANCE_ID_PATTERN,
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
IdStr = Annotated[str, StringConstraints(min_length=1, max_length=MAX_ID_LENGTH)]
|
|
68
|
+
|
|
69
|
+
NonEmptyStr = Annotated[
|
|
70
|
+
str,
|
|
71
|
+
StringConstraints(min_length=1, max_length=MAX_NON_EMPTY_STR_LENGTH),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
DisplayName = Annotated[
|
|
75
|
+
str,
|
|
76
|
+
StringConstraints(min_length=1, max_length=MAX_DISPLAY_NAME_LENGTH),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
ErrorMessage = Annotated[
|
|
80
|
+
str,
|
|
81
|
+
StringConstraints(min_length=1, max_length=MAX_ERROR_MESSAGE_LENGTH),
|
|
82
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from elementary_runtime.protocol.base import ProtocolEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TaskType(ProtocolEnum):
|
|
5
|
+
CUSTOM_SQL_TEST_V1 = "custom_sql_test.v1"
|
|
6
|
+
RUN_SQL_V1 = "run_sql.v1"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConnectionHealth(ProtocolEnum):
|
|
10
|
+
HEALTHY = "healthy"
|
|
11
|
+
UNHEALTHY = "unhealthy"
|
|
12
|
+
NOT_CHECKED = "not_checked"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConnectionErrorCode(ProtocolEnum):
|
|
16
|
+
AUTHENTICATION_FAILED = "authentication_failed"
|
|
17
|
+
NETWORK_UNREACHABLE = "network_unreachable"
|
|
18
|
+
PERMISSION_DENIED = "permission_denied"
|
|
19
|
+
QUERY_TIMEOUT = "query_timeout"
|
|
20
|
+
QUERY_FAILED = "query_failed"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HeartbeatStatus(ProtocolEnum):
|
|
24
|
+
EXTENDED = "extended"
|
|
25
|
+
STALE = "stale"
|
|
26
|
+
COMPLETED = "completed"
|
|
27
|
+
NOT_FOUND = "not_found"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SubmittedResultStatus(ProtocolEnum):
|
|
31
|
+
ACCEPTED = "accepted"
|
|
32
|
+
ALREADY_COMPLETED = "already_completed"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ErrorCode(ProtocolEnum):
|
|
36
|
+
INVALID_REQUEST = "invalid_request"
|
|
37
|
+
INVALID_TOKEN = "invalid_token"
|
|
38
|
+
FORBIDDEN_CONNECTION = "forbidden_connection"
|
|
39
|
+
UNKNOWN_INSTANCE = "unknown_instance"
|
|
40
|
+
UNKNOWN_RESOURCE = "unknown_resource"
|
|
41
|
+
INCOMPATIBLE_VERSION = "incompatible_version"
|
|
42
|
+
INVALID_CONNECTION_STATE = "invalid_connection_state"
|
|
43
|
+
PAYLOAD_TOO_LARGE = "payload_too_large"
|
|
44
|
+
UNSUPPORTED_TASK_TYPE = "unsupported_task_type"
|
|
45
|
+
INVALID_RESULT = "invalid_result"
|
|
46
|
+
INVALID_TELEMETRY_EVENT = "invalid_telemetry_event"
|
|
47
|
+
RATE_LIMITED = "rate_limited"
|
|
48
|
+
PROCESSING_FAILED = "processing_failed"
|
|
49
|
+
SERVICE_UNAVAILABLE = "service_unavailable"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TelemetryEventType(ProtocolEnum):
|
|
53
|
+
RUNTIME_STARTED = "runtime_started"
|
|
54
|
+
RUNTIME_STOPPED = "runtime_stopped"
|
|
55
|
+
TASK_EXECUTION_SUMMARY = "task_execution_summary"
|
|
56
|
+
RUNTIME_EXCEPTION = "runtime_exception"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RuntimeStopReason(ProtocolEnum):
|
|
60
|
+
GRACEFUL = "graceful"
|
|
61
|
+
DRAIN_TIMEOUT = "drain_timeout"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TaskExecutionOutcome(ProtocolEnum):
|
|
65
|
+
COMPLETED = "completed"
|
|
66
|
+
QUERY_ERROR = "query_error"
|
|
67
|
+
TIMEOUT = "timeout"
|
|
68
|
+
PERMISSION_ERROR = "permission_error"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ExceptionComponent(ProtocolEnum):
|
|
72
|
+
COORDINATOR = "coordinator"
|
|
73
|
+
SQL_VALIDATOR = "sql_validator"
|
|
74
|
+
WAREHOUSE_CLIENT = "warehouse_client"
|
|
75
|
+
RESULT_ENCODER = "result_encoder"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ExceptionOperation(ProtocolEnum):
|
|
79
|
+
REGISTER = "register"
|
|
80
|
+
POLL = "poll"
|
|
81
|
+
HEARTBEAT = "heartbeat"
|
|
82
|
+
EXECUTE = "execute"
|
|
83
|
+
SUBMIT_RESULTS = "submit_results"
|
|
84
|
+
SEND_TELEMETRY = "send_telemetry"
|
|
85
|
+
SHUTDOWN = "shutdown"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestSourceType(ProtocolEnum):
|
|
89
|
+
CLOUD_TEST = "cloud_test"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from elementary_runtime.protocol.base import (
|
|
2
|
+
ErrorMessage,
|
|
3
|
+
IdStr,
|
|
4
|
+
NonEmptyStr,
|
|
5
|
+
ProtocolModel,
|
|
6
|
+
)
|
|
7
|
+
from elementary_runtime.protocol.enums import ErrorCode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskErrorDetail(ProtocolModel):
|
|
11
|
+
message: ErrorMessage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErrorDetails(ProtocolModel):
|
|
15
|
+
field: NonEmptyStr | None = None
|
|
16
|
+
task_id: IdStr | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ErrorBody(ProtocolModel):
|
|
20
|
+
code: ErrorCode
|
|
21
|
+
message: ErrorMessage
|
|
22
|
+
request_id: IdStr
|
|
23
|
+
retryable: bool
|
|
24
|
+
details: ErrorDetails | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ErrorResponse(ProtocolModel):
|
|
28
|
+
error: ErrorBody
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Paths:
|
|
5
|
+
REGISTER_INSTANCE: Final = "/instances/register"
|
|
6
|
+
POLL_TASKS: Final = "/tasks/poll"
|
|
7
|
+
HEARTBEAT_TASKS: Final = "/tasks/heartbeat"
|
|
8
|
+
SUBMIT_RESULTS: Final = "/tasks/results"
|
|
9
|
+
TELEMETRY: Final = "/telemetry"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Headers:
|
|
13
|
+
INSTANCE_ID: Final = "X-Elementary-Runtime-Instance-ID"
|
|
14
|
+
REQUEST_ID: Final = "X-Request-ID"
|
|
15
|
+
RETRY_AFTER: Final = "Retry-After"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import AfterValidator, Field, model_validator
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from elementary_runtime.common.validators import require_unique, require_unique_attr
|
|
7
|
+
from elementary_runtime.protocol.base import (
|
|
8
|
+
DisplayName,
|
|
9
|
+
IdStr,
|
|
10
|
+
NonEmptyStr,
|
|
11
|
+
ProtocolModel,
|
|
12
|
+
)
|
|
13
|
+
from elementary_runtime.protocol.enums import (
|
|
14
|
+
ConnectionErrorCode,
|
|
15
|
+
ConnectionHealth,
|
|
16
|
+
TaskType,
|
|
17
|
+
)
|
|
18
|
+
from elementary_runtime.protocol.limits import MAX_CONNECTIONS_PER_REGISTER
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ObservedWarehouseIdentity(ProtocolModel):
|
|
22
|
+
warehouse_type: NonEmptyStr
|
|
23
|
+
logical_identifier: NonEmptyStr | None = None
|
|
24
|
+
database: NonEmptyStr | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConnectionReport(ProtocolModel):
|
|
28
|
+
warehouse_connection_id: IdStr
|
|
29
|
+
health: ConnectionHealth
|
|
30
|
+
observed_warehouse_identity: ObservedWarehouseIdentity | None
|
|
31
|
+
error_code: ConnectionErrorCode | None
|
|
32
|
+
|
|
33
|
+
@model_validator(mode="after")
|
|
34
|
+
def error_code_matches_health(self) -> Self:
|
|
35
|
+
if self.health is ConnectionHealth.UNHEALTHY:
|
|
36
|
+
if self.error_code is None:
|
|
37
|
+
raise ValueError("error_code is required when health is unhealthy")
|
|
38
|
+
elif self.error_code is not None:
|
|
39
|
+
raise ValueError("error_code must be null when health is not unhealthy")
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RegisterInstanceRequest(ProtocolModel):
|
|
44
|
+
display_name: DisplayName | None
|
|
45
|
+
agent_version: NonEmptyStr
|
|
46
|
+
supported_task_types: Annotated[list[TaskType], AfterValidator(require_unique)]
|
|
47
|
+
connections: Annotated[
|
|
48
|
+
list[ConnectionReport],
|
|
49
|
+
Field(max_length=MAX_CONNECTIONS_PER_REGISTER),
|
|
50
|
+
AfterValidator(require_unique_attr("warehouse_connection_id")),
|
|
51
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MAX_POLL_TASKS = 256
|
|
2
|
+
MAX_HEARTBEAT_TASKS = 256
|
|
3
|
+
MAX_RESULTS_PER_BATCH = 64
|
|
4
|
+
MAX_TELEMETRY_EVENTS = 256
|
|
5
|
+
MAX_CONNECTIONS_PER_REGISTER = 256
|
|
6
|
+
|
|
7
|
+
MAX_QUERY_TIMEOUT_SECONDS = 86_400
|
|
8
|
+
MAX_SAMPLE_ROW_LIMIT = 100_000
|
|
9
|
+
MAX_SAMPLE_ROWS = 100_000
|
|
10
|
+
MAX_SAMPLE_COLUMNS = 1_000
|
|
11
|
+
|
|
12
|
+
MAX_ERROR_MESSAGE_LENGTH = 4_096
|
|
13
|
+
MAX_EXCEPTION_FRAMES = 50
|
|
14
|
+
MAX_INSTANCE_ID_LENGTH = 255
|
|
15
|
+
MAX_INSTANCE_ID_PREFIX_LENGTH = 128
|
|
16
|
+
MAX_DISPLAY_NAME_LENGTH = 255
|
|
17
|
+
MAX_ID_LENGTH = 255
|
|
18
|
+
MAX_NON_EMPTY_STR_LENGTH = 255
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from elementary_runtime.protocol.errors import TaskErrorDetail
|
|
2
|
+
from elementary_runtime.protocol.tasks.custom_sql_test import (
|
|
3
|
+
CloudTestSource,
|
|
4
|
+
CustomSqlTestResult,
|
|
5
|
+
CustomSqlTestResultPayload,
|
|
6
|
+
CustomSqlTestTask,
|
|
7
|
+
CustomSqlTestTaskPayload,
|
|
8
|
+
)
|
|
9
|
+
from elementary_runtime.protocol.tasks.envelope import (
|
|
10
|
+
TaskEnvelope,
|
|
11
|
+
TaskResultEnvelope,
|
|
12
|
+
WarehouseTaskEnvelope,
|
|
13
|
+
)
|
|
14
|
+
from elementary_runtime.protocol.tasks.heartbeat import (
|
|
15
|
+
HeartbeatTasksRequest,
|
|
16
|
+
HeartbeatTasksResponse,
|
|
17
|
+
TaskHeartbeat,
|
|
18
|
+
TaskHeartbeatStatus,
|
|
19
|
+
)
|
|
20
|
+
from elementary_runtime.protocol.tasks.poll import PollTasksRequest, PollTasksResponse
|
|
21
|
+
from elementary_runtime.protocol.tasks.registry import (
|
|
22
|
+
TASK_MODELS,
|
|
23
|
+
Task,
|
|
24
|
+
TaskModels,
|
|
25
|
+
TaskResult,
|
|
26
|
+
)
|
|
27
|
+
from elementary_runtime.protocol.tasks.results import (
|
|
28
|
+
SubmitResultsRequest,
|
|
29
|
+
SubmitResultsResponse,
|
|
30
|
+
SubmittedResult,
|
|
31
|
+
)
|
|
32
|
+
from elementary_runtime.protocol.tasks.run_sql import (
|
|
33
|
+
RunSqlResult,
|
|
34
|
+
RunSqlResultPayload,
|
|
35
|
+
RunSqlTask,
|
|
36
|
+
RunSqlTaskPayload,
|
|
37
|
+
)
|
|
38
|
+
from elementary_runtime.protocol.tasks.sql_query import (
|
|
39
|
+
ResultSample,
|
|
40
|
+
SampleColumnType,
|
|
41
|
+
SampleHeader,
|
|
42
|
+
SqlExecutionStatus,
|
|
43
|
+
SqlQueryResultPayload,
|
|
44
|
+
SqlQueryTaskPayload,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"TASK_MODELS",
|
|
49
|
+
"CloudTestSource",
|
|
50
|
+
"CustomSqlTestResult",
|
|
51
|
+
"CustomSqlTestResultPayload",
|
|
52
|
+
"CustomSqlTestTask",
|
|
53
|
+
"CustomSqlTestTaskPayload",
|
|
54
|
+
"HeartbeatTasksRequest",
|
|
55
|
+
"HeartbeatTasksResponse",
|
|
56
|
+
"PollTasksRequest",
|
|
57
|
+
"PollTasksResponse",
|
|
58
|
+
"ResultSample",
|
|
59
|
+
"RunSqlResult",
|
|
60
|
+
"RunSqlResultPayload",
|
|
61
|
+
"RunSqlTask",
|
|
62
|
+
"RunSqlTaskPayload",
|
|
63
|
+
"SampleColumnType",
|
|
64
|
+
"SampleHeader",
|
|
65
|
+
"SqlExecutionStatus",
|
|
66
|
+
"SqlQueryResultPayload",
|
|
67
|
+
"SqlQueryTaskPayload",
|
|
68
|
+
"SubmitResultsRequest",
|
|
69
|
+
"SubmitResultsResponse",
|
|
70
|
+
"SubmittedResult",
|
|
71
|
+
"Task",
|
|
72
|
+
"TaskEnvelope",
|
|
73
|
+
"TaskErrorDetail",
|
|
74
|
+
"TaskHeartbeat",
|
|
75
|
+
"TaskHeartbeatStatus",
|
|
76
|
+
"TaskModels",
|
|
77
|
+
"TaskResult",
|
|
78
|
+
"TaskResultEnvelope",
|
|
79
|
+
"WarehouseTaskEnvelope",
|
|
80
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from elementary_runtime.protocol.base import IdStr, ProtocolModel
|
|
4
|
+
from elementary_runtime.protocol.enums import TaskType, TestSourceType
|
|
5
|
+
from elementary_runtime.protocol.tasks.envelope import (
|
|
6
|
+
TaskResultEnvelope,
|
|
7
|
+
WarehouseTaskEnvelope,
|
|
8
|
+
)
|
|
9
|
+
from elementary_runtime.protocol.tasks.sql_query import (
|
|
10
|
+
SqlQueryResultPayload,
|
|
11
|
+
SqlQueryTaskPayload,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CloudTestSource(ProtocolModel):
|
|
16
|
+
type: Literal[TestSourceType.CLOUD_TEST]
|
|
17
|
+
id: IdStr
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CustomSqlTestTaskPayload(SqlQueryTaskPayload):
|
|
21
|
+
source: CloudTestSource
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CustomSqlTestTask(WarehouseTaskEnvelope):
|
|
25
|
+
task_type: Literal[TaskType.CUSTOM_SQL_TEST_V1]
|
|
26
|
+
payload: CustomSqlTestTaskPayload
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CustomSqlTestResultPayload(SqlQueryResultPayload):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CustomSqlTestResult(TaskResultEnvelope):
|
|
34
|
+
task_type: Literal[TaskType.CUSTOM_SQL_TEST_V1]
|
|
35
|
+
payload: CustomSqlTestResultPayload
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from elementary_runtime.protocol.base import IdStr, ProtocolModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskEnvelope(ProtocolModel):
|
|
9
|
+
task_id: IdStr
|
|
10
|
+
delivery_id: IdStr
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WarehouseTaskEnvelope(TaskEnvelope):
|
|
14
|
+
warehouse_connection_id: IdStr
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskResultEnvelope(ProtocolModel):
|
|
18
|
+
task_id: IdStr
|
|
19
|
+
delivery_id: IdStr
|
|
20
|
+
duration_seconds: Annotated[float, Field(ge=0.0)]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import AfterValidator, Field
|
|
4
|
+
|
|
5
|
+
from elementary_runtime.common.validators import require_unique_attr
|
|
6
|
+
from elementary_runtime.protocol.base import IdStr, ProtocolModel
|
|
7
|
+
from elementary_runtime.protocol.enums import HeartbeatStatus
|
|
8
|
+
from elementary_runtime.protocol.limits import MAX_HEARTBEAT_TASKS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskHeartbeat(ProtocolModel):
|
|
12
|
+
task_id: IdStr
|
|
13
|
+
delivery_id: IdStr
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HeartbeatTasksRequest(ProtocolModel):
|
|
17
|
+
tasks: Annotated[
|
|
18
|
+
list[TaskHeartbeat],
|
|
19
|
+
Field(max_length=MAX_HEARTBEAT_TASKS),
|
|
20
|
+
AfterValidator(require_unique_attr("task_id")),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaskHeartbeatStatus(ProtocolModel):
|
|
25
|
+
task_id: IdStr
|
|
26
|
+
status: HeartbeatStatus
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HeartbeatTasksResponse(ProtocolModel):
|
|
30
|
+
tasks: Annotated[
|
|
31
|
+
list[TaskHeartbeatStatus],
|
|
32
|
+
Field(max_length=MAX_HEARTBEAT_TASKS),
|
|
33
|
+
AfterValidator(require_unique_attr("task_id")),
|
|
34
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import AfterValidator, Field
|
|
4
|
+
|
|
5
|
+
from elementary_runtime.common.validators import require_unique_attr
|
|
6
|
+
from elementary_runtime.protocol.base import ProtocolModel
|
|
7
|
+
from elementary_runtime.protocol.limits import MAX_POLL_TASKS
|
|
8
|
+
from elementary_runtime.protocol.tasks.registry import Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PollTasksRequest(ProtocolModel):
|
|
12
|
+
available_slots: Annotated[int, Field(ge=0, le=MAX_POLL_TASKS)]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PollTasksResponse(ProtocolModel):
|
|
16
|
+
tasks: Annotated[
|
|
17
|
+
list[Task],
|
|
18
|
+
Field(max_length=MAX_POLL_TASKS),
|
|
19
|
+
AfterValidator(require_unique_attr("task_id")),
|
|
20
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Annotated, NamedTuple
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from elementary_runtime.protocol.enums import TaskType
|
|
7
|
+
from elementary_runtime.protocol.tasks.custom_sql_test import (
|
|
8
|
+
CustomSqlTestResult,
|
|
9
|
+
CustomSqlTestTask,
|
|
10
|
+
)
|
|
11
|
+
from elementary_runtime.protocol.tasks.envelope import TaskEnvelope, TaskResultEnvelope
|
|
12
|
+
from elementary_runtime.protocol.tasks.run_sql import RunSqlResult, RunSqlTask
|
|
13
|
+
|
|
14
|
+
Task = Annotated[CustomSqlTestTask | RunSqlTask, Field(discriminator="task_type")]
|
|
15
|
+
TaskResult = Annotated[
|
|
16
|
+
CustomSqlTestResult | RunSqlResult,
|
|
17
|
+
Field(discriminator="task_type"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TaskModels(NamedTuple):
|
|
22
|
+
task: type[TaskEnvelope]
|
|
23
|
+
result: type[TaskResultEnvelope]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
TASK_MODELS: Mapping[TaskType, TaskModels] = {
|
|
27
|
+
TaskType.CUSTOM_SQL_TEST_V1: TaskModels(
|
|
28
|
+
task=CustomSqlTestTask,
|
|
29
|
+
result=CustomSqlTestResult,
|
|
30
|
+
),
|
|
31
|
+
TaskType.RUN_SQL_V1: TaskModels(
|
|
32
|
+
task=RunSqlTask,
|
|
33
|
+
result=RunSqlResult,
|
|
34
|
+
),
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import AfterValidator, Field
|
|
4
|
+
|
|
5
|
+
from elementary_runtime.common.validators import require_unique_attr
|
|
6
|
+
from elementary_runtime.protocol.base import IdStr, ProtocolModel
|
|
7
|
+
from elementary_runtime.protocol.enums import SubmittedResultStatus
|
|
8
|
+
from elementary_runtime.protocol.limits import MAX_RESULTS_PER_BATCH
|
|
9
|
+
from elementary_runtime.protocol.tasks.registry import TaskResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SubmitResultsRequest(ProtocolModel):
|
|
13
|
+
results: Annotated[
|
|
14
|
+
list[TaskResult],
|
|
15
|
+
Field(max_length=MAX_RESULTS_PER_BATCH),
|
|
16
|
+
AfterValidator(require_unique_attr("task_id")),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SubmittedResult(ProtocolModel):
|
|
21
|
+
task_id: IdStr
|
|
22
|
+
status: SubmittedResultStatus
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SubmitResultsResponse(ProtocolModel):
|
|
26
|
+
results: Annotated[
|
|
27
|
+
list[SubmittedResult],
|
|
28
|
+
Field(max_length=MAX_RESULTS_PER_BATCH),
|
|
29
|
+
AfterValidator(require_unique_attr("task_id")),
|
|
30
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from elementary_runtime.protocol.enums import TaskType
|
|
4
|
+
from elementary_runtime.protocol.tasks.envelope import (
|
|
5
|
+
TaskResultEnvelope,
|
|
6
|
+
WarehouseTaskEnvelope,
|
|
7
|
+
)
|
|
8
|
+
from elementary_runtime.protocol.tasks.sql_query import (
|
|
9
|
+
SqlQueryResultPayload,
|
|
10
|
+
SqlQueryTaskPayload,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RunSqlTaskPayload(SqlQueryTaskPayload):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RunSqlTask(WarehouseTaskEnvelope):
|
|
19
|
+
task_type: Literal[TaskType.RUN_SQL_V1]
|
|
20
|
+
payload: RunSqlTaskPayload
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RunSqlResultPayload(SqlQueryResultPayload):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RunSqlResult(TaskResultEnvelope):
|
|
28
|
+
task_type: Literal[TaskType.RUN_SQL_V1]
|
|
29
|
+
payload: RunSqlResultPayload
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import AfterValidator, Field, JsonValue, model_validator
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from elementary_runtime.common.validators import require_unique_attr
|
|
7
|
+
from elementary_runtime.protocol.base import (
|
|
8
|
+
IdStr,
|
|
9
|
+
NonEmptyStr,
|
|
10
|
+
ProtocolEnum,
|
|
11
|
+
ProtocolModel,
|
|
12
|
+
)
|
|
13
|
+
from elementary_runtime.protocol.errors import TaskErrorDetail
|
|
14
|
+
from elementary_runtime.protocol.limits import (
|
|
15
|
+
MAX_QUERY_TIMEOUT_SECONDS,
|
|
16
|
+
MAX_SAMPLE_COLUMNS,
|
|
17
|
+
MAX_SAMPLE_ROW_LIMIT,
|
|
18
|
+
MAX_SAMPLE_ROWS,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SqlExecutionStatus(ProtocolEnum):
|
|
23
|
+
COMPLETED = "completed"
|
|
24
|
+
SQL_VALIDATION_FAILED = "sql_validation_failed"
|
|
25
|
+
QUERY_ERROR = "query_error"
|
|
26
|
+
QUERY_TIMEOUT = "query_timeout"
|
|
27
|
+
PERMISSION_DENIED = "permission_denied"
|
|
28
|
+
TIMEOUT_NOT_SUPPORTED = "timeout_not_supported"
|
|
29
|
+
RESULT_LIMIT_EXCEEDED = "result_limit_exceeded"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SampleColumnType(ProtocolEnum):
|
|
33
|
+
STR = "str"
|
|
34
|
+
INT = "int"
|
|
35
|
+
FLOAT = "float"
|
|
36
|
+
BOOL = "bool"
|
|
37
|
+
DATE = "date"
|
|
38
|
+
TIMESTAMP = "timestamp"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SampleHeader(ProtocolModel):
|
|
42
|
+
id: IdStr
|
|
43
|
+
display_name: NonEmptyStr
|
|
44
|
+
type: SampleColumnType
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ResultSample(ProtocolModel):
|
|
48
|
+
headers: Annotated[
|
|
49
|
+
list[SampleHeader],
|
|
50
|
+
Field(max_length=MAX_SAMPLE_COLUMNS),
|
|
51
|
+
AfterValidator(require_unique_attr("id")),
|
|
52
|
+
]
|
|
53
|
+
rows: Annotated[list[dict[str, JsonValue]], Field(max_length=MAX_SAMPLE_ROWS)]
|
|
54
|
+
|
|
55
|
+
@model_validator(mode="after")
|
|
56
|
+
def rows_match_headers(self) -> Self:
|
|
57
|
+
expected = {header.id for header in self.headers}
|
|
58
|
+
for index, row in enumerate(self.rows):
|
|
59
|
+
if set(row) != expected:
|
|
60
|
+
raise ValueError(f"row {index} keys must equal the header ids")
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SqlQueryTaskPayload(ProtocolModel):
|
|
65
|
+
sql: Annotated[str, Field(min_length=1)]
|
|
66
|
+
query_timeout_seconds: Annotated[int, Field(ge=1, le=MAX_QUERY_TIMEOUT_SECONDS)]
|
|
67
|
+
sample_row_limit: Annotated[int, Field(ge=0, le=MAX_SAMPLE_ROW_LIMIT)]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SqlQueryResultPayload(ProtocolModel):
|
|
71
|
+
execution_status: SqlExecutionStatus
|
|
72
|
+
row_count: Annotated[int, Field(ge=0)] | None
|
|
73
|
+
sample: ResultSample | None
|
|
74
|
+
error: TaskErrorDetail | None
|
|
75
|
+
|
|
76
|
+
@model_validator(mode="after")
|
|
77
|
+
def status_fields_are_consistent(self) -> Self:
|
|
78
|
+
if self.execution_status is SqlExecutionStatus.COMPLETED:
|
|
79
|
+
if self.row_count is None:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"row_count is required when execution_status is completed"
|
|
82
|
+
)
|
|
83
|
+
if self.error is not None:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"error must be null when execution_status is completed"
|
|
86
|
+
)
|
|
87
|
+
if self.sample is not None and len(self.sample.rows) > self.row_count:
|
|
88
|
+
raise ValueError("sample cannot contain more rows than row_count")
|
|
89
|
+
else:
|
|
90
|
+
if self.error is None:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"error is required when execution_status is not completed"
|
|
93
|
+
)
|
|
94
|
+
if self.row_count is not None:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"row_count must be null when execution_status is not completed"
|
|
97
|
+
)
|
|
98
|
+
if self.sample is not None:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"sample must be null when execution_status is not completed"
|
|
101
|
+
)
|
|
102
|
+
return self
|