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.
Files changed (38) hide show
  1. elementary_runtime/__init__.py +5 -0
  2. elementary_runtime/common/__init__.py +0 -0
  3. elementary_runtime/common/validators.py +27 -0
  4. elementary_runtime/config/__init__.py +40 -0
  5. elementary_runtime/config/base.py +19 -0
  6. elementary_runtime/config/connections.py +55 -0
  7. elementary_runtime/config/env.py +66 -0
  8. elementary_runtime/config/errors.py +20 -0
  9. elementary_runtime/config/runtime.py +60 -0
  10. elementary_runtime/config/secrets.py +77 -0
  11. elementary_runtime/config/sections.py +98 -0
  12. elementary_runtime/coordinator/__init__.py +0 -0
  13. elementary_runtime/execution/__init__.py +0 -0
  14. elementary_runtime/execution/redaction/__init__.py +0 -0
  15. elementary_runtime/execution/sql/__init__.py +0 -0
  16. elementary_runtime/execution/worker/__init__.py +0 -0
  17. elementary_runtime/protocol/__init__.py +162 -0
  18. elementary_runtime/protocol/base.py +82 -0
  19. elementary_runtime/protocol/enums.py +89 -0
  20. elementary_runtime/protocol/errors.py +28 -0
  21. elementary_runtime/protocol/http.py +15 -0
  22. elementary_runtime/protocol/instances.py +51 -0
  23. elementary_runtime/protocol/limits.py +18 -0
  24. elementary_runtime/protocol/tasks/__init__.py +80 -0
  25. elementary_runtime/protocol/tasks/custom_sql_test.py +35 -0
  26. elementary_runtime/protocol/tasks/envelope.py +20 -0
  27. elementary_runtime/protocol/tasks/heartbeat.py +34 -0
  28. elementary_runtime/protocol/tasks/poll.py +20 -0
  29. elementary_runtime/protocol/tasks/registry.py +35 -0
  30. elementary_runtime/protocol/tasks/results.py +30 -0
  31. elementary_runtime/protocol/tasks/run_sql.py +29 -0
  32. elementary_runtime/protocol/tasks/sql_query.py +102 -0
  33. elementary_runtime/protocol/telemetry.py +99 -0
  34. elementary_runtime/py.typed +0 -0
  35. elementary_runtime/warehouses/__init__.py +0 -0
  36. elementary_runtime-0.1.0rc1.dist-info/METADATA +95 -0
  37. elementary_runtime-0.1.0rc1.dist-info/RECORD +38 -0
  38. 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