execution-plane-client 0.1.5__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.
- execution_plane_client/__init__.py +1 -0
- execution_plane_client/_version.py +24 -0
- execution_plane_client/catalog.py +61 -0
- execution_plane_client/commands/__init__.py +37 -0
- execution_plane_client/commands/base.py +64 -0
- execution_plane_client/commands/clusters.py +96 -0
- execution_plane_client/commands/control.py +35 -0
- execution_plane_client/commands/credentials.py +25 -0
- execution_plane_client/commands/targets.py +151 -0
- execution_plane_client/commands/work.py +39 -0
- execution_plane_client/errors.py +11 -0
- execution_plane_client/handlers.py +22 -0
- execution_plane_client/protocol.py +133 -0
- execution_plane_client/py.typed +0 -0
- execution_plane_client/registry.py +35 -0
- execution_plane_client-0.1.5.dist-info/METADATA +79 -0
- execution_plane_client-0.1.5.dist-info/RECORD +19 -0
- execution_plane_client-0.1.5.dist-info/WHEEL +4 -0
- execution_plane_client-0.1.5.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared command contracts and participant-owned handler registration."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.5'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 5)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Supported command revisions and their wire payload contracts."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from execution_plane_client.commands.base import CommandPayload
|
|
6
|
+
from execution_plane_client.commands.clusters import ClusterDelete, ClusterProvision, ClusterRegister, ClusterUpdate
|
|
7
|
+
from execution_plane_client.commands.control import CancelCommand, Completion
|
|
8
|
+
from execution_plane_client.commands.credentials import CredentialResolve
|
|
9
|
+
from execution_plane_client.commands.targets import TargetDelete, TargetRegister, TargetUpdate
|
|
10
|
+
from execution_plane_client.commands.work import WorkExecute
|
|
11
|
+
from execution_plane_client.errors import UnsupportedCommand
|
|
12
|
+
from execution_plane_client.protocol import MessageEnvelope
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CommandDefinition:
|
|
17
|
+
"""One command type, revision and payload schema."""
|
|
18
|
+
|
|
19
|
+
command_type: str
|
|
20
|
+
revision: int
|
|
21
|
+
payload_schema: type[CommandPayload]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CommandCatalog:
|
|
25
|
+
"""Validate known commands independently of installed handlers."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Install version-one work, resource and completion contracts."""
|
|
29
|
+
payloads = (
|
|
30
|
+
WorkExecute,
|
|
31
|
+
Completion,
|
|
32
|
+
CredentialResolve,
|
|
33
|
+
ClusterRegister,
|
|
34
|
+
ClusterProvision,
|
|
35
|
+
ClusterUpdate,
|
|
36
|
+
ClusterDelete,
|
|
37
|
+
TargetRegister,
|
|
38
|
+
TargetUpdate,
|
|
39
|
+
TargetDelete,
|
|
40
|
+
CancelCommand,
|
|
41
|
+
)
|
|
42
|
+
self._definitions = {
|
|
43
|
+
(schema.command_type, schema.command_revision): CommandDefinition(
|
|
44
|
+
schema.command_type, schema.command_revision, schema
|
|
45
|
+
)
|
|
46
|
+
for schema in payloads
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def definition(self, command_type: str, revision: int) -> CommandDefinition:
|
|
50
|
+
"""Return a supported revision or an explicit rejection."""
|
|
51
|
+
try:
|
|
52
|
+
return self._definitions[command_type, revision]
|
|
53
|
+
except KeyError:
|
|
54
|
+
raise UnsupportedCommand from None
|
|
55
|
+
|
|
56
|
+
def validate(self, message: MessageEnvelope) -> CommandPayload:
|
|
57
|
+
"""Validate the payload and completion parent, returning the typed payload."""
|
|
58
|
+
definition = self.definition(message.command.command_type, message.command.command_revision)
|
|
59
|
+
payload = definition.payload_schema.model_validate(message.command.payload)
|
|
60
|
+
definition.payload_schema.validate_parent(message.command.parent_command_id)
|
|
61
|
+
return payload
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Shared resource identities and HTTPS configuration validation."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlsplit
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from execution_plane_client.protocol import PositiveInt, WireModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResourceIdentity(WireModel):
|
|
10
|
+
"""Caller-supplied physical resource identity."""
|
|
11
|
+
|
|
12
|
+
resource_id: UUID
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResourceRevision(WireModel):
|
|
16
|
+
"""Stable cluster integration identity and desired-state revision."""
|
|
17
|
+
|
|
18
|
+
resource_key: UUID
|
|
19
|
+
desired_revision: PositiveInt
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_endpoint(value: str) -> str:
|
|
23
|
+
"""Require HTTPS without embedded credentials, queries or fragments."""
|
|
24
|
+
parsed = urlsplit(value)
|
|
25
|
+
if (
|
|
26
|
+
parsed.scheme != "https"
|
|
27
|
+
or not parsed.hostname
|
|
28
|
+
or parsed.username is not None
|
|
29
|
+
or parsed.password is not None
|
|
30
|
+
or parsed.query
|
|
31
|
+
or parsed.fragment
|
|
32
|
+
or any(char.isspace() for char in value)
|
|
33
|
+
):
|
|
34
|
+
message = "Endpoint must be an HTTPS URL without userinfo, query or fragment"
|
|
35
|
+
raise ValueError(message)
|
|
36
|
+
_ = parsed.port
|
|
37
|
+
return value
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Construct transport-neutral messages from typed command payloads."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
from execution_plane_client.errors import ProtocolConflict
|
|
8
|
+
from execution_plane_client.protocol import Command, CommandScope, MessageEnvelope, WireModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandPayload(WireModel):
|
|
12
|
+
"""A typed command knows its wire identity and builds its delivery envelope."""
|
|
13
|
+
|
|
14
|
+
command_type: ClassVar[str]
|
|
15
|
+
command_revision: ClassVar[int] = 1
|
|
16
|
+
requires_parent: ClassVar[bool] = False
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def validate_parent(cls, parent_command_id: UUID | None) -> None:
|
|
20
|
+
"""Require correlation for commands that report another command's outcome."""
|
|
21
|
+
if cls.requires_parent and parent_command_id is None:
|
|
22
|
+
raise ProtocolConflict
|
|
23
|
+
|
|
24
|
+
def _message_payload(self) -> dict[str, object]:
|
|
25
|
+
return self.model_dump(mode="json")
|
|
26
|
+
|
|
27
|
+
def to_message(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
source_id: str,
|
|
31
|
+
destination_id: str,
|
|
32
|
+
client_id: str,
|
|
33
|
+
command_id: UUID | None = None,
|
|
34
|
+
parent_command_id: UUID | None = None,
|
|
35
|
+
occurred_at: datetime | None = None,
|
|
36
|
+
message_id: UUID | None = None,
|
|
37
|
+
sequence: int = 0,
|
|
38
|
+
) -> MessageEnvelope:
|
|
39
|
+
"""Build a validated message, generating a command UUID and UTC time if omitted.
|
|
40
|
+
|
|
41
|
+
The message ID defaults to the command ID. Retain the returned message for
|
|
42
|
+
retries, or supply the original IDs and timestamp when reconstructing it.
|
|
43
|
+
Routing, authentication, HTTP delivery and persistence belong to the caller.
|
|
44
|
+
"""
|
|
45
|
+
self.validate_parent(parent_command_id)
|
|
46
|
+
payload = self._message_payload()
|
|
47
|
+
type(self).model_validate(payload)
|
|
48
|
+
identity = uuid4() if command_id is None else command_id
|
|
49
|
+
return MessageEnvelope(
|
|
50
|
+
message_id=identity if message_id is None else message_id,
|
|
51
|
+
command_id=identity,
|
|
52
|
+
source_id=source_id,
|
|
53
|
+
destination_id=destination_id,
|
|
54
|
+
scope=CommandScope(client_id=client_id),
|
|
55
|
+
sequence=sequence,
|
|
56
|
+
command=Command(
|
|
57
|
+
command_id=identity,
|
|
58
|
+
parent_command_id=parent_command_id,
|
|
59
|
+
command_type=self.command_type,
|
|
60
|
+
command_revision=self.command_revision,
|
|
61
|
+
payload=payload,
|
|
62
|
+
occurred_at=datetime.now(UTC) if occurred_at is None else occurred_at,
|
|
63
|
+
),
|
|
64
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Cluster-binding desired state, independent of EP persistence models."""
|
|
2
|
+
|
|
3
|
+
import ssl
|
|
4
|
+
from typing import Annotated, ClassVar, Self
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, StrictBool, StrictStr, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from execution_plane_client.commands import ResourceRevision, validate_endpoint
|
|
10
|
+
from execution_plane_client.commands.base import CommandPayload
|
|
11
|
+
from execution_plane_client.protocol import WireModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClusterConfigBase(WireModel):
|
|
15
|
+
"""Registration configuration fields used by handlers to copy and compare bindings."""
|
|
16
|
+
|
|
17
|
+
name: Annotated[StrictStr, Field(min_length=1, max_length=255)]
|
|
18
|
+
endpoint: Annotated[StrictStr, Field(max_length=2048)]
|
|
19
|
+
namespace: Annotated[StrictStr, Field(min_length=1, max_length=63, pattern=r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")]
|
|
20
|
+
credential_ref: UUID
|
|
21
|
+
ca_certificate: Annotated[StrictStr, Field(max_length=65536)] | None = Field(default=None, repr=False)
|
|
22
|
+
project_ids: list[UUID] | None = None
|
|
23
|
+
labels: dict[str, str] = Field(default_factory=dict)
|
|
24
|
+
enabled: StrictBool = True
|
|
25
|
+
is_default: StrictBool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClusterRegister(ClusterConfigBase, ResourceRevision, CommandPayload):
|
|
29
|
+
"""Create or replace the complete desired configuration of a binding."""
|
|
30
|
+
|
|
31
|
+
command_type: ClassVar[str] = "cluster.register"
|
|
32
|
+
|
|
33
|
+
@field_validator("endpoint")
|
|
34
|
+
@classmethod
|
|
35
|
+
def check_endpoint(cls, value: str) -> str:
|
|
36
|
+
"""Protect the management credential in transit."""
|
|
37
|
+
return validate_endpoint(value)
|
|
38
|
+
|
|
39
|
+
@field_validator("ca_certificate")
|
|
40
|
+
@classmethod
|
|
41
|
+
def check_ca(cls, value: str | None) -> str | None:
|
|
42
|
+
"""Reject invalid PEM before accepting desired state."""
|
|
43
|
+
if value is not None:
|
|
44
|
+
try:
|
|
45
|
+
ssl.create_default_context().load_verify_locations(cadata=value)
|
|
46
|
+
except ssl.SSLError as exc:
|
|
47
|
+
message = "CA certificate must contain valid PEM"
|
|
48
|
+
raise ValueError(message) from exc
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def check_credential_ref(self) -> Self:
|
|
53
|
+
"""Require a resolvable credential reference for enabled bindings."""
|
|
54
|
+
if self.enabled and self.credential_ref == UUID(int=0):
|
|
55
|
+
message = "Enabled cluster bindings require a credential_ref"
|
|
56
|
+
raise ValueError(message)
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ClusterProvision(ClusterRegister):
|
|
61
|
+
"""Provision through the same binding and default-target reconciler."""
|
|
62
|
+
|
|
63
|
+
command_type: ClassVar[str] = "cluster.provision"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ClusterDelete(ResourceRevision, CommandPayload):
|
|
67
|
+
"""Disable an existing binding at the requested desired-state revision."""
|
|
68
|
+
|
|
69
|
+
command_type: ClassVar[str] = "cluster.delete"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ClusterUpdate(ResourceRevision, CommandPayload):
|
|
73
|
+
"""Patch a binding; only CA and project grants support explicit null."""
|
|
74
|
+
|
|
75
|
+
command_type: ClassVar[str] = "cluster.update"
|
|
76
|
+
|
|
77
|
+
name: StrictStr | None = None
|
|
78
|
+
endpoint: StrictStr | None = None
|
|
79
|
+
namespace: StrictStr | None = None
|
|
80
|
+
credential_ref: UUID | None = None
|
|
81
|
+
ca_certificate: StrictStr | None = Field(default=None, repr=False)
|
|
82
|
+
project_ids: list[UUID] | None = None
|
|
83
|
+
labels: dict[str, str] | None = None
|
|
84
|
+
enabled: StrictBool | None = None
|
|
85
|
+
is_default: StrictBool | None = None
|
|
86
|
+
|
|
87
|
+
def _message_payload(self) -> dict[str, object]:
|
|
88
|
+
return self.model_dump(mode="json", exclude_unset=True)
|
|
89
|
+
|
|
90
|
+
@model_validator(mode="after")
|
|
91
|
+
def check_nulls(self) -> Self:
|
|
92
|
+
"""Preserve omitted values but reject clearing required fields."""
|
|
93
|
+
if any(getattr(self, key) is None for key in self.model_fields_set - {"ca_certificate", "project_ids"}):
|
|
94
|
+
message = "Required binding fields cannot be null"
|
|
95
|
+
raise ValueError(message)
|
|
96
|
+
return self
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Cancellation requests and work completion commands."""
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, Literal
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import Field, field_validator
|
|
7
|
+
|
|
8
|
+
from execution_plane_client.commands.base import CommandPayload
|
|
9
|
+
from execution_plane_client.protocol import ResourceReference, validate_json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CancelCommand(CommandPayload):
|
|
13
|
+
"""Cancel an existing work command, whose UUID is its work-item UUID."""
|
|
14
|
+
|
|
15
|
+
command_type: ClassVar[str] = "command.cancel"
|
|
16
|
+
|
|
17
|
+
target_command_id: UUID
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Completion(CommandPayload):
|
|
21
|
+
"""Terminal or uncertain work outcome delivered to the initiating client."""
|
|
22
|
+
|
|
23
|
+
command_type: ClassVar[str] = "work.execute.completed"
|
|
24
|
+
requires_parent: ClassVar[bool] = True
|
|
25
|
+
|
|
26
|
+
status: Literal["succeeded", "failed", "cancelled", "outcome_unknown"]
|
|
27
|
+
result: dict[str, object] = Field(default_factory=dict, repr=False)
|
|
28
|
+
resource: ResourceReference | None = None
|
|
29
|
+
error_code: str | None = None
|
|
30
|
+
|
|
31
|
+
@field_validator("result", mode="before")
|
|
32
|
+
@classmethod
|
|
33
|
+
def check_result(cls, value: object) -> object:
|
|
34
|
+
"""Require finite JSON; work output limits are set by the invocation."""
|
|
35
|
+
return validate_json(value, max_bytes=None)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Credential-reference resolution commands and responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, ClassVar
|
|
6
|
+
from uuid import UUID # noqa: TC003 - Pydantic evaluates fields.
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, StrictStr
|
|
9
|
+
|
|
10
|
+
from execution_plane_client.commands.base import CommandPayload
|
|
11
|
+
from execution_plane_client.protocol import WireModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CredentialResolve(CommandPayload):
|
|
15
|
+
"""Resolve an opaque credential reference into a short-lived value."""
|
|
16
|
+
|
|
17
|
+
command_type: ClassVar[str] = "credential.resolve"
|
|
18
|
+
credential_ref: UUID
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CredentialResolution(WireModel):
|
|
22
|
+
"""The resolved credential value for one submitted command."""
|
|
23
|
+
|
|
24
|
+
message_id: UUID
|
|
25
|
+
credential: Annotated[StrictStr, Field(min_length=1, max_length=65536, repr=False)]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Execution-target configuration and independent registration commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Annotated, ClassVar, Literal, Self
|
|
7
|
+
from uuid import UUID # noqa: TC003 - Pydantic evaluates fields.
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, StrictBool, StrictStr, field_validator, model_validator
|
|
10
|
+
|
|
11
|
+
from execution_plane_client.commands import ResourceIdentity, validate_endpoint
|
|
12
|
+
from execution_plane_client.commands.base import CommandPayload
|
|
13
|
+
from execution_plane_client.protocol import WireModel
|
|
14
|
+
|
|
15
|
+
_LABEL_NAME = re.compile(r"^[A-Za-z0-9]([A-Za-z0-9_.-]*[A-Za-z0-9])?$")
|
|
16
|
+
_DNS_LABEL = re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
|
|
17
|
+
_LABEL_LIMIT = 63
|
|
18
|
+
_DOMAIN_LIMIT = 253
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _label_key(value: str) -> bool:
|
|
22
|
+
prefix, separator, name = value.partition("/")
|
|
23
|
+
if separator:
|
|
24
|
+
if (
|
|
25
|
+
not prefix
|
|
26
|
+
or len(prefix) > _DOMAIN_LIMIT
|
|
27
|
+
or any(len(part) > _LABEL_LIMIT or not _DNS_LABEL.fullmatch(part) for part in prefix.split("."))
|
|
28
|
+
):
|
|
29
|
+
return False
|
|
30
|
+
else:
|
|
31
|
+
name = prefix
|
|
32
|
+
return len(name) <= _LABEL_LIMIT and bool(_LABEL_NAME.fullmatch(name))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _label_value(value: str) -> bool:
|
|
36
|
+
return len(value) <= _LABEL_LIMIT and (value == "" or bool(_LABEL_NAME.fullmatch(value)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class KubernetesPlacement(WireModel):
|
|
40
|
+
"""Kubernetes placement contract, independent of Kubernetes libraries."""
|
|
41
|
+
|
|
42
|
+
type: Literal["kubernetes"] = "kubernetes"
|
|
43
|
+
namespace: Annotated[StrictStr, Field(min_length=1, max_length=63, pattern=r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")]
|
|
44
|
+
node_selectors: list[str] = Field(default_factory=list)
|
|
45
|
+
tolerations: list[str] = Field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
@field_validator("node_selectors")
|
|
48
|
+
@classmethod
|
|
49
|
+
def check_selectors(cls, values: list[str]) -> list[str]:
|
|
50
|
+
"""Accept only Kubernetes key=value selectors."""
|
|
51
|
+
for value in values:
|
|
52
|
+
key, separator, label = value.partition("=")
|
|
53
|
+
if not separator or not _label_key(key) or not _label_value(label):
|
|
54
|
+
message = "Invalid Kubernetes node selector"
|
|
55
|
+
raise ValueError(message)
|
|
56
|
+
return values
|
|
57
|
+
|
|
58
|
+
@field_validator("tolerations")
|
|
59
|
+
@classmethod
|
|
60
|
+
def check_tolerations(cls, values: list[str]) -> list[str]:
|
|
61
|
+
"""Accept compact tolerations with supported scheduling effects."""
|
|
62
|
+
for value in values:
|
|
63
|
+
key_value, separator, effect = value.rpartition(":")
|
|
64
|
+
if not separator:
|
|
65
|
+
key_value, effect = value, ""
|
|
66
|
+
key, equals, label = key_value.partition("=")
|
|
67
|
+
valid_key = bool(key) and _label_key(key) if equals else not key or _label_key(key)
|
|
68
|
+
if (
|
|
69
|
+
not valid_key
|
|
70
|
+
or not _label_value(label)
|
|
71
|
+
or effect not in {"", "NoSchedule", "PreferNoSchedule", "NoExecute"}
|
|
72
|
+
):
|
|
73
|
+
message = "Invalid Kubernetes toleration"
|
|
74
|
+
raise ValueError(message)
|
|
75
|
+
return values
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RHELPlacement(WireModel):
|
|
79
|
+
"""Placement for a supported RHEL backend."""
|
|
80
|
+
|
|
81
|
+
type: Literal["rhel"] = "rhel"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
Placement = Annotated[KubernetesPlacement | RHELPlacement, Field(discriminator="type")]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TargetDefinition(WireModel):
|
|
88
|
+
"""Desired execution-target configuration."""
|
|
89
|
+
|
|
90
|
+
name: Annotated[StrictStr, Field(min_length=1, max_length=255)]
|
|
91
|
+
backend_type: Literal["vanilla_k8s", "openshell"]
|
|
92
|
+
placement: Placement
|
|
93
|
+
endpoint: Annotated[StrictStr, Field(min_length=1, max_length=2048)]
|
|
94
|
+
credential_ref: UUID
|
|
95
|
+
labels: dict[str, str] = Field(default_factory=dict)
|
|
96
|
+
|
|
97
|
+
@field_validator("endpoint")
|
|
98
|
+
@classmethod
|
|
99
|
+
def check_endpoint(cls, value: str) -> str:
|
|
100
|
+
"""Preserve verified HTTPS for provider management credentials."""
|
|
101
|
+
return validate_endpoint(value)
|
|
102
|
+
|
|
103
|
+
@model_validator(mode="after")
|
|
104
|
+
def check_backend(self) -> Self:
|
|
105
|
+
"""Reject backend/placement pairs unsupported by the provider adapters."""
|
|
106
|
+
if (self.backend_type == "vanilla_k8s") != isinstance(self.placement, KubernetesPlacement):
|
|
107
|
+
message = "Backend does not support this placement"
|
|
108
|
+
raise ValueError(message)
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TargetRegister(TargetDefinition, ResourceIdentity, CommandPayload):
|
|
113
|
+
"""Register an independently identified target on an existing cluster."""
|
|
114
|
+
|
|
115
|
+
command_type: ClassVar[str] = "execution_target.register"
|
|
116
|
+
|
|
117
|
+
cluster_id: UUID
|
|
118
|
+
is_default: StrictBool = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TargetDelete(ResourceIdentity, CommandPayload):
|
|
122
|
+
"""Request draining of an independently managed execution target."""
|
|
123
|
+
|
|
124
|
+
command_type: ClassVar[str] = "execution_target.delete"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TargetUpdate(ResourceIdentity, CommandPayload):
|
|
128
|
+
"""Mutable target fields; `is_default=true` promotes this target."""
|
|
129
|
+
|
|
130
|
+
command_type: ClassVar[str] = "execution_target.update"
|
|
131
|
+
|
|
132
|
+
name: Annotated[StrictStr, Field(min_length=1, max_length=255)] | None = None
|
|
133
|
+
endpoint: Annotated[StrictStr, Field(min_length=1, max_length=2048)] | None = None
|
|
134
|
+
placement: Placement | None = None
|
|
135
|
+
credential_ref: UUID | None = None
|
|
136
|
+
labels: dict[str, str] | None = None
|
|
137
|
+
is_default: StrictBool | None = None
|
|
138
|
+
|
|
139
|
+
def _message_payload(self) -> dict[str, object]:
|
|
140
|
+
payload = self.model_dump(mode="json", exclude_unset=True)
|
|
141
|
+
if self.placement is not None:
|
|
142
|
+
payload["placement"] = self.placement.model_dump(
|
|
143
|
+
mode="json", include=self.placement.model_fields_set | {"type"}
|
|
144
|
+
)
|
|
145
|
+
return payload
|
|
146
|
+
|
|
147
|
+
@field_validator("endpoint")
|
|
148
|
+
@classmethod
|
|
149
|
+
def check_endpoint(cls, value: str | None) -> str | None:
|
|
150
|
+
"""Validate explicit endpoint updates before applying them."""
|
|
151
|
+
return None if value is None else validate_endpoint(value)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Versioned work invocation values without node-provider imports."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, ClassVar, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, StrictStr, field_validator
|
|
6
|
+
|
|
7
|
+
from execution_plane_client.commands.base import CommandPayload
|
|
8
|
+
from execution_plane_client.protocol import PositiveInt, WireModel, validate_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NodeInvocation(WireModel):
|
|
12
|
+
"""Version-one invocation consumed by the existing node protocol."""
|
|
13
|
+
|
|
14
|
+
version: Literal[1] = 1
|
|
15
|
+
operation: Literal["execute"] = "execute"
|
|
16
|
+
inputs: dict[str, object]
|
|
17
|
+
credentials: dict[str, object] = Field(repr=False)
|
|
18
|
+
workflow_context: dict[str, object]
|
|
19
|
+
settings: dict[str, object]
|
|
20
|
+
timeout_seconds: PositiveInt
|
|
21
|
+
max_output_bytes: PositiveInt
|
|
22
|
+
|
|
23
|
+
@field_validator("inputs", "credentials", "workflow_context", "settings", mode="before")
|
|
24
|
+
@classmethod
|
|
25
|
+
def check_json(cls, value: object) -> object:
|
|
26
|
+
"""Keep all nested invocation data portable and bounded."""
|
|
27
|
+
return validate_json(value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WorkExecute(CommandPayload):
|
|
31
|
+
"""Script command with stable caller correlation and provider-neutral input."""
|
|
32
|
+
|
|
33
|
+
command_type: ClassVar[str] = "work.execute"
|
|
34
|
+
|
|
35
|
+
image: Annotated[StrictStr, Field(min_length=1, max_length=2048)]
|
|
36
|
+
selectors: dict[StrictStr, StrictStr] = Field(default_factory=dict)
|
|
37
|
+
invocation: NodeInvocation = Field(repr=False)
|
|
38
|
+
output_config: dict[str, str] | None = None
|
|
39
|
+
workload_type: Literal["script"] = "script"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Safe protocol errors, without request bodies or infrastructure details."""
|
|
2
|
+
|
|
3
|
+
# ruff: noqa: N818
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProtocolConflict(Exception):
|
|
7
|
+
"""Duplicate registration or conflicting resource state."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnsupportedCommand(Exception):
|
|
11
|
+
"""The command type or revision is unsupported by this participant."""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Participant-owned command handling, without database or HTTP dependencies."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from execution_plane_client.commands.base import CommandPayload
|
|
7
|
+
from execution_plane_client.protocol import MessageEnvelope, ResourceReference
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class HandlingResult:
|
|
12
|
+
"""Optional resource created or affected by a command."""
|
|
13
|
+
|
|
14
|
+
resource: ResourceReference | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommandHandler[ContextT](Protocol):
|
|
18
|
+
"""A participant supplies its own authenticated execution context."""
|
|
19
|
+
|
|
20
|
+
async def handle(self, message: MessageEnvelope, payload: CommandPayload, context: ContextT) -> HandlingResult:
|
|
21
|
+
"""Apply domain effects using the catalog's validated payload and original envelope."""
|
|
22
|
+
...
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Strict, versioned command envelopes and receipt contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Annotated, Literal, Self
|
|
8
|
+
from uuid import UUID # noqa: TC003 - Pydantic resolves these model fields at runtime.
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, StrictInt, StrictStr, ValidationInfo, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
MAX_PAYLOAD_BYTES = 512 * 1024
|
|
13
|
+
Identifier = Annotated[StrictStr, Field(min_length=1, max_length=255, pattern=r"^[A-Za-z0-9_.:@/-]+$")]
|
|
14
|
+
PositiveInt = Annotated[StrictInt, Field(gt=0)]
|
|
15
|
+
Sequence = Annotated[StrictInt, Field(ge=0)]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WireModel(BaseModel):
|
|
19
|
+
"""Forbid silent field loss and freeze top-level wire values."""
|
|
20
|
+
|
|
21
|
+
model_config = {"extra": "forbid", "frozen": True, "validate_default": True}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_json(value: object, *, max_bytes: int | None = MAX_PAYLOAD_BYTES) -> object:
|
|
25
|
+
"""Reject coercions and non-finite JSON, optionally bounding its encoded size."""
|
|
26
|
+
try:
|
|
27
|
+
encoded = json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode("utf-8")
|
|
28
|
+
except (TypeError, ValueError, UnicodeError, RecursionError) as exc:
|
|
29
|
+
message = "Payload is not finite UTF-8 JSON"
|
|
30
|
+
raise ValueError(message) from exc
|
|
31
|
+
if max_bytes is not None and len(encoded) > max_bytes:
|
|
32
|
+
message = f"Payload exceeds {max_bytes // 1024} KiB"
|
|
33
|
+
raise ValueError(message)
|
|
34
|
+
pending = [value]
|
|
35
|
+
while pending:
|
|
36
|
+
item = pending.pop()
|
|
37
|
+
if type(item) is dict:
|
|
38
|
+
if any(not isinstance(key, str) for key in item):
|
|
39
|
+
message = "JSON object keys must be strings"
|
|
40
|
+
raise ValueError(message)
|
|
41
|
+
pending.extend(item.values())
|
|
42
|
+
elif type(item) is list:
|
|
43
|
+
pending.extend(item)
|
|
44
|
+
elif item is not None and type(item) not in (str, bool, int, float):
|
|
45
|
+
message = "Payload must contain JSON values only"
|
|
46
|
+
raise ValueError(message)
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CommandScope(WireModel):
|
|
51
|
+
"""Initiating service client; project authorization belongs to the caller."""
|
|
52
|
+
|
|
53
|
+
client_id: Identifier
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Command(WireModel):
|
|
57
|
+
"""One versioned operation; participants persist only its domain effects."""
|
|
58
|
+
|
|
59
|
+
command_id: UUID
|
|
60
|
+
parent_command_id: UUID | None = None
|
|
61
|
+
command_type: Annotated[StrictStr, Field(pattern=r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$")]
|
|
62
|
+
command_revision: PositiveInt
|
|
63
|
+
payload: dict[str, object] = Field(repr=False)
|
|
64
|
+
occurred_at: datetime
|
|
65
|
+
|
|
66
|
+
@field_validator("payload", mode="before")
|
|
67
|
+
@classmethod
|
|
68
|
+
def check_payload(cls, value: object, info: ValidationInfo) -> object:
|
|
69
|
+
"""Preserve the payload's JSON types and enforce its size limit."""
|
|
70
|
+
limit = None if info.data.get("command_type") == "work.execute.completed" else MAX_PAYLOAD_BYTES
|
|
71
|
+
return validate_json(value, max_bytes=limit)
|
|
72
|
+
|
|
73
|
+
@field_validator("occurred_at")
|
|
74
|
+
@classmethod
|
|
75
|
+
def check_time(cls, value: datetime) -> datetime:
|
|
76
|
+
"""Require an explicit timezone and serialize instants in UTC."""
|
|
77
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
78
|
+
message = "occurred_at requires a timezone"
|
|
79
|
+
raise ValueError(message)
|
|
80
|
+
return value.astimezone(UTC)
|
|
81
|
+
|
|
82
|
+
@model_validator(mode="after")
|
|
83
|
+
def check_parent(self) -> Self:
|
|
84
|
+
"""Reject a command naming itself as its parent."""
|
|
85
|
+
if self.parent_command_id == self.command_id:
|
|
86
|
+
message = "A command cannot be its own parent"
|
|
87
|
+
raise ValueError(message)
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class MessageEnvelope(WireModel):
|
|
92
|
+
"""Participant identity around one logical command."""
|
|
93
|
+
|
|
94
|
+
message_id: UUID
|
|
95
|
+
message_type: Literal["command.submit"] = "command.submit"
|
|
96
|
+
command_id: UUID
|
|
97
|
+
sequence: Sequence = 0
|
|
98
|
+
source_id: Identifier
|
|
99
|
+
destination_id: Identifier
|
|
100
|
+
scope: CommandScope
|
|
101
|
+
command: Command
|
|
102
|
+
|
|
103
|
+
@model_validator(mode="after")
|
|
104
|
+
def check_identity(self) -> Self:
|
|
105
|
+
"""Keep the envelope and nested command identity consistent."""
|
|
106
|
+
if self.command_id != self.command.command_id:
|
|
107
|
+
message = "Envelope command_id must match command.command_id"
|
|
108
|
+
raise ValueError(message)
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ResourceReference(WireModel):
|
|
113
|
+
"""Safe reference from a command to a participant-owned resource."""
|
|
114
|
+
|
|
115
|
+
kind: Identifier
|
|
116
|
+
resource_id: UUID
|
|
117
|
+
resource_key: UUID | None = None
|
|
118
|
+
revision: PositiveInt | None = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class CapabilitiesRead(WireModel):
|
|
122
|
+
"""Command revisions installed by this participant."""
|
|
123
|
+
|
|
124
|
+
protocol_revision: Literal[1] = 1
|
|
125
|
+
commands: dict[str, list[int]]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TransportReceipt(WireModel):
|
|
129
|
+
"""Receipt of a handled request, without a persisted command record."""
|
|
130
|
+
|
|
131
|
+
received: Literal[True] = True
|
|
132
|
+
message_id: UUID
|
|
133
|
+
resource: ResourceReference | None = None
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""In-memory registration of participant-owned handlers."""
|
|
2
|
+
|
|
3
|
+
from execution_plane_client.catalog import CommandDefinition
|
|
4
|
+
from execution_plane_client.errors import ProtocolConflict, UnsupportedCommand
|
|
5
|
+
from execution_plane_client.handlers import CommandHandler
|
|
6
|
+
from execution_plane_client.protocol import CapabilitiesRead
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CommandRegistry[ContextT]:
|
|
10
|
+
"""Map installed command revisions to exactly one handler."""
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
"""Start without handlers or advertised capabilities."""
|
|
14
|
+
self._handlers: dict[tuple[str, int], CommandHandler[ContextT]] = {}
|
|
15
|
+
|
|
16
|
+
def register(self, definition: CommandDefinition, handler: CommandHandler[ContextT]) -> None:
|
|
17
|
+
"""Reject duplicate registrations."""
|
|
18
|
+
key = (definition.command_type, definition.revision)
|
|
19
|
+
if key in self._handlers:
|
|
20
|
+
raise ProtocolConflict
|
|
21
|
+
self._handlers[key] = handler
|
|
22
|
+
|
|
23
|
+
def get(self, command_type: str, revision: int) -> CommandHandler[ContextT]:
|
|
24
|
+
"""Reject commands the participant cannot handle."""
|
|
25
|
+
try:
|
|
26
|
+
return self._handlers[command_type, revision]
|
|
27
|
+
except KeyError:
|
|
28
|
+
raise UnsupportedCommand from None
|
|
29
|
+
|
|
30
|
+
def capabilities(self) -> CapabilitiesRead:
|
|
31
|
+
"""Advertise only installed revisions."""
|
|
32
|
+
commands: dict[str, list[int]] = {}
|
|
33
|
+
for name, revision in sorted(self._handlers):
|
|
34
|
+
commands.setdefault(name, []).append(revision)
|
|
35
|
+
return CapabilitiesRead(commands=commands)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: execution-plane-client
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Shared execution-plane command contracts and handler registry
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: <3.15,>=3.12
|
|
8
|
+
Requires-Dist: pydantic>=2.0.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Execution Plane Client
|
|
12
|
+
|
|
13
|
+
Shared Pydantic command contracts, command catalog, in-memory handler registry
|
|
14
|
+
and a generic handler protocol. No HTTP router, network client, persistence,
|
|
15
|
+
authentication implementation or EP imports are included.
|
|
16
|
+
|
|
17
|
+
Participants register their own handlers. EP implements command submission and
|
|
18
|
+
resource queries; the caller handles `work.execute.completed` using this same
|
|
19
|
+
protocol. Commands have no persisted status or child history. A receipt confirms
|
|
20
|
+
that the domain handler returned, not that asynchronous resource work is complete.
|
|
21
|
+
|
|
22
|
+
## Constructing messages
|
|
23
|
+
|
|
24
|
+
Instantiate a typed command and call `to_message()`; the command supplies its
|
|
25
|
+
wire type and revision and builds the envelope:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from uuid import uuid4
|
|
29
|
+
|
|
30
|
+
from execution_plane_client.commands.clusters import ClusterRegister
|
|
31
|
+
|
|
32
|
+
message = ClusterRegister(
|
|
33
|
+
resource_key=uuid4(),
|
|
34
|
+
desired_revision=1,
|
|
35
|
+
name="workers",
|
|
36
|
+
endpoint="https://cluster.example",
|
|
37
|
+
namespace="execution-plane",
|
|
38
|
+
credential_ref=uuid4(),
|
|
39
|
+
).to_message(
|
|
40
|
+
source_id="syntara-orchestration",
|
|
41
|
+
destination_id="execution-plane",
|
|
42
|
+
client_id="syntara-orchestration",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
body = message.model_dump(mode="json") # Send using your application's HTTP client.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Every concrete command supports this method, including `ClusterDelete` and
|
|
49
|
+
`TargetDelete`. Partial updates serialize only supplied fields, retaining explicit
|
|
50
|
+
nulls and placement discriminators without resetting omitted configuration.
|
|
51
|
+
|
|
52
|
+
By default, each call generates a command UUID and UTC timestamp; `message_id`
|
|
53
|
+
defaults to that command UUID. Keep the returned message for retries. To rebuild
|
|
54
|
+
it from existing domain state, pass `command_id` and `occurred_at` explicitly,
|
|
55
|
+
plus `message_id` if it differs. `parent_command_id` and `sequence` are optional;
|
|
56
|
+
`Completion` requires a parent work command UUID. Source, destination and client
|
|
57
|
+
scope are always supplied by the caller; the helper does not authenticate them.
|
|
58
|
+
|
|
59
|
+
## Handling messages
|
|
60
|
+
|
|
61
|
+
After parsing a `MessageEnvelope`, validate it through the catalog and pass the
|
|
62
|
+
returned typed payload to the registered handler:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
payload = catalog.validate(message)
|
|
66
|
+
handler = registry.get(message.command.command_type, message.command.command_revision)
|
|
67
|
+
result = await handler.handle(message, payload, context)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Handlers implement `handle(message, payload, context)`. The catalog selects the
|
|
71
|
+
payload model by command type and revision, validates it and checks required
|
|
72
|
+
parent correlation. Handlers narrow the payload to their supported command
|
|
73
|
+
types without parsing the raw dictionary again. The original envelope remains
|
|
74
|
+
available for identity and correlation; partial-update payloads retain which
|
|
75
|
+
fields were supplied. Authentication, authorization and domain-state validation
|
|
76
|
+
(including validation after merging a patch) remain participant responsibilities.
|
|
77
|
+
|
|
78
|
+
Build with `uv build src/execution_plane_client`. See the service's integration
|
|
79
|
+
documentation for command examples and the current HTTP authentication contract.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
execution_plane_client/__init__.py,sha256=B_1B86MgamC9arJ1IjoZ_pdMg6oRU-1dyqmOtlQUrGY,75
|
|
2
|
+
execution_plane_client/_version.py,sha256=MYViowQy3gBKIlctXQ6CgExkQCBPcL8ycJEVEexVzdI,520
|
|
3
|
+
execution_plane_client/catalog.py,sha256=xVWRTDHtykjqWfDvhA-cDZgxPZQPBA9doynn0G0Wazg,2420
|
|
4
|
+
execution_plane_client/errors.py,sha256=FQrlqkbWuWUYyIYM6x3Y-PPOuuVW8nq-v6ZQ6tBSils,313
|
|
5
|
+
execution_plane_client/handlers.py,sha256=mHSzEdbDzy8JnUM4SqM0zYR9cen1pWEwFSJ_MKOHAOI,788
|
|
6
|
+
execution_plane_client/protocol.py,sha256=XQf_REdMj_V2tRt4Q_lJ40vCkjz_Apc_pW30tVZ27Nw,4847
|
|
7
|
+
execution_plane_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
execution_plane_client/registry.py,sha256=qJbHm1Y9bdCAsc_XODHXmJahHX-X6SlFzW-LjBcIdDw,1503
|
|
9
|
+
execution_plane_client/commands/__init__.py,sha256=NgNbTP525RcOy7WKS0CBXNc3Ym1zpbJ3U2ooxYev0vY,1041
|
|
10
|
+
execution_plane_client/commands/base.py,sha256=daf83pxxWWuFz7mRwj0xodldALaqBRJVvWe54Los6yw,2486
|
|
11
|
+
execution_plane_client/commands/clusters.py,sha256=B_wSi6vG2pco_gx9vhXsZjl-nQxZaE-Q2vNQYD9qOJU,3784
|
|
12
|
+
execution_plane_client/commands/control.py,sha256=oO5d6TCCXoZs6xRq9GA6IDr8wYbwL8auMXslQu2laYg,1199
|
|
13
|
+
execution_plane_client/commands/credentials.py,sha256=KsBkOKgRndZ-E7fZ5FQzHhp0ulwzJeoesgvtJ1EjTzs,779
|
|
14
|
+
execution_plane_client/commands/targets.py,sha256=Q2zoQWwkDsERxsUM4i6WrIlJXFaCLm3WJFRNMjQ1j8c,5733
|
|
15
|
+
execution_plane_client/commands/work.py,sha256=21939Oe2jCuQ60JgrGZcg6S-GMZ_ipFc_mmx5PQN7ds,1455
|
|
16
|
+
execution_plane_client-0.1.5.dist-info/METADATA,sha256=JrmtdfOWrGYUrA34XmTKu5LHWPzAtEeYOJ0InhuzAvE,3310
|
|
17
|
+
execution_plane_client-0.1.5.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
18
|
+
execution_plane_client-0.1.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
19
|
+
execution_plane_client-0.1.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|