daggerml.contrib.adapters

View source
  1from __future__ import annotations
  2
  3import argparse
  4import json
  5import sys
  6import time
  7from pathlib import Path
  8from threading import Lock
  9from typing import Any
 10from urllib.parse import urlparse
 11from warnings import warn
 12
 13from daggerml import Dml, Ref, Runnable
 14from daggerml._core import AdapterCancelResponse, AdapterCleanupResponse, AdapterInvokeResponse
 15from daggerml.api import DmlRepoError, _entry_points
 16from daggerml.contrib.s3 import S3Store, is_s3_uri
 17from daggerml.util import get_client
 18
 19
 20class AdapterBase:
 21    name = ""
 22
 23    @classmethod
 24    def resolve_runnable(cls, uri, kwargs, sub) -> Runnable:
 25        from daggerml.contrib.executors._base import get_executor
 26
 27        return get_executor(cls.name, uri).resolve_runnable(uri, kwargs, sub)
 28
 29    @classmethod
 30    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
 31        raise NotImplementedError("Adapter send method is not implemented")
 32
 33    @classmethod
 34    def _read_input(cls, input_path: str) -> str:
 35        if input_path == "-":
 36            return sys.stdin.read()
 37        if is_s3_uri(input_path):
 38            return S3Store().get(input_path).decode("utf-8")
 39        return Path(input_path).read_text()
 40
 41    @classmethod
 42    def _write_output(cls, output_path: str, data: str) -> None:
 43        if output_path == "-":
 44            sys.stdout.write(data)
 45            if not data.endswith("\n"):
 46                sys.stdout.write("\n")
 47            sys.stdout.flush()
 48            return
 49        if is_s3_uri(output_path):
 50            parsed = urlparse(output_path)
 51            bucket = parsed.netloc
 52            key = parsed.path.lstrip("/")
 53            get_client("s3").put_object(
 54                Bucket=bucket,
 55                Key=key,
 56                Body=data.encode("utf-8"),
 57                ContentType="application/json",
 58            )
 59            return
 60        Path(output_path).write_text(data)
 61
 62    @classmethod
 63    def cli(cls, argv: list[str] | None = None) -> int:
 64        parser = argparse.ArgumentParser(description=f"{cls.__name__} CLI")
 65        parser.add_argument("-i", "--input", default="-")
 66        parser.add_argument("-o", "--output", default="-")
 67        parser.add_argument("--poll", action="store_true")
 68        # FIXME: `--poll` make `cancel` difficult. We should allow for coordination between caller and this loop.
 69        args = parser.parse_args(argv)
 70        raw = cls._read_input(args.input)
 71        payload = json.loads(raw)
 72        result = cls.send(**payload)
 73        while args.poll and payload.get("operation") == "invoke" and result.get("status") == "retry":
 74            state = result.get("adapter_state")
 75            if not isinstance(state, dict):
 76                raise DmlRepoError("Retry adapter response requires object adapter_state")
 77            payload["adapter_state"] = state
 78            time.sleep(0.1)
 79            result = cls.send(**payload)
 80        if args.poll and payload.get("operation") == "invoke" and result.get("status") == "success":
 81            payload["adapter_state"] = result.get("adapter_state", payload["adapter_state"])
 82            record = Dml(remote_root=payload["remote"]["root"]).runtime.read_execution_record(
 83                Ref(f"index:{payload['execution_id']}")
 84            )
 85            result_ref = record["state"]["result_ref"]
 86            if result_ref is None:
 87                raise DmlRepoError("Successful nested invoke did not publish a result")
 88            cleanup_payload = {**payload, "operation": "cleanup", "result_ref": result_ref}
 89            try:
 90                cleanup_result = cls.send(**cleanup_payload)
 91                while cleanup_result.get("status") == "retry":
 92                    state = cleanup_result.get("adapter_state")
 93                    if not isinstance(state, dict):
 94                        raise DmlRepoError("Retry adapter response requires object adapter_state")
 95                    cleanup_payload["adapter_state"] = state
 96                    time.sleep(0.1)
 97                    cleanup_result = cls.send(**cleanup_payload)
 98                if cleanup_result.get("status") != "success":
 99                    print(
100                        f"Nested cleanup failed for {payload['execution_id']}: {cleanup_result.get('error')}",
101                        file=sys.stderr,
102                    )
103            except Exception as exc:
104                print(f"Nested cleanup failed for {payload['execution_id']}: {exc}", file=sys.stderr)
105        if args.poll and payload.get("operation") == "invoke":
106            result = {key: value for key, value in result.items() if key != "adapter_state"}
107        cls._write_output(args.output, json.dumps(result))
108        return 0
109
110
111class LocalAdapter(AdapterBase):
112    name = "local"
113    executable = "dml-local-adapter"
114
115    @classmethod
116    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
117        from daggerml.contrib.executors._base import get_executor
118
119        return get_executor("local", kw["runnable"]["target"]["uri"]).handle(**kw)
120
121
122class LambdaAdapter(AdapterBase):
123    name = "lambda"
124    executable = "dml-lambda-adapter"
125
126    @classmethod
127    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
128        client = get_client("lambda")
129        try:
130            response = client.invoke(
131                FunctionName=kw["runnable"]["target"]["uri"],
132                InvocationType="RequestResponse",
133                Payload=json.dumps(kw, separators=(",", ":"), sort_keys=True).encode("utf-8"),
134            )
135        except Exception as exc:
136            code = getattr(exc, "response", {}).get("Error", {}).get("Code")
137            if code not in {"TooManyRequestsException", "ThrottlingException"}:
138                raise
139            headers = getattr(exc, "response", {}).get("ResponseMetadata", {}).get("HTTPHeaders", {})
140            try:
141                retry_after_ms = max(0, int(float(headers.get("retry-after")) * 1000))
142            except (TypeError, ValueError):
143                retry_after_ms = None
144            result: AdapterInvokeResponse = {
145                "status": "retry",
146                "error": None,
147                "adapter_state": kw.get("adapter_state") if isinstance(kw.get("adapter_state"), dict) else {},
148            }
149            if retry_after_ms is not None:
150                result["retry_after_ms"] = retry_after_ms
151            return result
152        stream = response.get("Payload")
153        if stream is None:
154            raise DmlRepoError("Lambda adapter invoke response missing Payload")
155        return json.loads(stream.read().decode("utf-8"))
156
157
158################################################################################
159############################### Adapter registry ###############################
160################################################################################
161ADAPTER_ENTRYPOINT_GROUP = "daggerml.contrib.adapters"
162
163_LOCK = Lock()
164_ADAPTER_SPECS: dict[str, str] = {}
165_PLUGINS_LOADED = False
166
167
168def load_adapter_plugins() -> None:
169    global _PLUGINS_LOADED
170    if _PLUGINS_LOADED:
171        return
172    with _LOCK:
173        if _PLUGINS_LOADED:
174            return
175        for ep in _entry_points(ADAPTER_ENTRYPOINT_GROUP):
176            try:
177                loaded = ep.load()
178                if loaded.name in _ADAPTER_SPECS:
179                    # warn about duplicate adapter registration but allow the last one to win
180                    warn(
181                        f"Adapter: '{loaded.name}' is overwriting existing '{ep.name} ({ep.value})'",
182                        stacklevel=2,
183                    )
184                _ADAPTER_SPECS[loaded.name] = loaded
185            except Exception as e:
186                raise DmlRepoError(f"Adapter plugin '{ep.name} ({ep.value})' failed: {e}") from e
187        _PLUGINS_LOADED = True
188
189
190def get_adapter(name: str) -> Any:
191    load_adapter_plugins()
192    spec = _ADAPTER_SPECS.get(name)
193    if spec is None:
194        raise DmlRepoError(f"Adapter '{name}' is not registered")
195    return spec
196
197
198def list_adapters() -> list[str]:
199    load_adapter_plugins()
200    return sorted(_ADAPTER_SPECS.keys())

AdapterBase

class AdapterBase:
View source
 21class AdapterBase:
 22    name = ""
 23
 24    @classmethod
 25    def resolve_runnable(cls, uri, kwargs, sub) -> Runnable:
 26        from daggerml.contrib.executors._base import get_executor
 27
 28        return get_executor(cls.name, uri).resolve_runnable(uri, kwargs, sub)
 29
 30    @classmethod
 31    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
 32        raise NotImplementedError("Adapter send method is not implemented")
 33
 34    @classmethod
 35    def _read_input(cls, input_path: str) -> str:
 36        if input_path == "-":
 37            return sys.stdin.read()
 38        if is_s3_uri(input_path):
 39            return S3Store().get(input_path).decode("utf-8")
 40        return Path(input_path).read_text()
 41
 42    @classmethod
 43    def _write_output(cls, output_path: str, data: str) -> None:
 44        if output_path == "-":
 45            sys.stdout.write(data)
 46            if not data.endswith("\n"):
 47                sys.stdout.write("\n")
 48            sys.stdout.flush()
 49            return
 50        if is_s3_uri(output_path):
 51            parsed = urlparse(output_path)
 52            bucket = parsed.netloc
 53            key = parsed.path.lstrip("/")
 54            get_client("s3").put_object(
 55                Bucket=bucket,
 56                Key=key,
 57                Body=data.encode("utf-8"),
 58                ContentType="application/json",
 59            )
 60            return
 61        Path(output_path).write_text(data)
 62
 63    @classmethod
 64    def cli(cls, argv: list[str] | None = None) -> int:
 65        parser = argparse.ArgumentParser(description=f"{cls.__name__} CLI")
 66        parser.add_argument("-i", "--input", default="-")
 67        parser.add_argument("-o", "--output", default="-")
 68        parser.add_argument("--poll", action="store_true")
 69        # FIXME: `--poll` make `cancel` difficult. We should allow for coordination between caller and this loop.
 70        args = parser.parse_args(argv)
 71        raw = cls._read_input(args.input)
 72        payload = json.loads(raw)
 73        result = cls.send(**payload)
 74        while args.poll and payload.get("operation") == "invoke" and result.get("status") == "retry":
 75            state = result.get("adapter_state")
 76            if not isinstance(state, dict):
 77                raise DmlRepoError("Retry adapter response requires object adapter_state")
 78            payload["adapter_state"] = state
 79            time.sleep(0.1)
 80            result = cls.send(**payload)
 81        if args.poll and payload.get("operation") == "invoke" and result.get("status") == "success":
 82            payload["adapter_state"] = result.get("adapter_state", payload["adapter_state"])
 83            record = Dml(remote_root=payload["remote"]["root"]).runtime.read_execution_record(
 84                Ref(f"index:{payload['execution_id']}")
 85            )
 86            result_ref = record["state"]["result_ref"]
 87            if result_ref is None:
 88                raise DmlRepoError("Successful nested invoke did not publish a result")
 89            cleanup_payload = {**payload, "operation": "cleanup", "result_ref": result_ref}
 90            try:
 91                cleanup_result = cls.send(**cleanup_payload)
 92                while cleanup_result.get("status") == "retry":
 93                    state = cleanup_result.get("adapter_state")
 94                    if not isinstance(state, dict):
 95                        raise DmlRepoError("Retry adapter response requires object adapter_state")
 96                    cleanup_payload["adapter_state"] = state
 97                    time.sleep(0.1)
 98                    cleanup_result = cls.send(**cleanup_payload)
 99                if cleanup_result.get("status") != "success":
100                    print(
101                        f"Nested cleanup failed for {payload['execution_id']}: {cleanup_result.get('error')}",
102                        file=sys.stderr,
103                    )
104            except Exception as exc:
105                print(f"Nested cleanup failed for {payload['execution_id']}: {exc}", file=sys.stderr)
106        if args.poll and payload.get("operation") == "invoke":
107            result = {key: value for key, value in result.items() if key != "adapter_state"}
108        cls._write_output(args.output, json.dumps(result))
109        return 0

AdapterBase.name

name= ''

AdapterBase.resolve_runnable

@classmethod
def resolve_runnable(cls, uri, kwargs, sub) -> daggerml.Runnable:
View source
24    @classmethod
25    def resolve_runnable(cls, uri, kwargs, sub) -> Runnable:
26        from daggerml.contrib.executors._base import get_executor
27
28        return get_executor(cls.name, uri).resolve_runnable(uri, kwargs, sub)

AdapterBase.send

@classmethod
def send(cls, **kw) -> dict[str, typing.Any]:
View source
30    @classmethod
31    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
32        raise NotImplementedError("Adapter send method is not implemented")

AdapterBase.cli

@classmethod
def cli(cls, argv: list[str] | None = None) -> int:
View source
 63    @classmethod
 64    def cli(cls, argv: list[str] | None = None) -> int:
 65        parser = argparse.ArgumentParser(description=f"{cls.__name__} CLI")
 66        parser.add_argument("-i", "--input", default="-")
 67        parser.add_argument("-o", "--output", default="-")
 68        parser.add_argument("--poll", action="store_true")
 69        # FIXME: `--poll` make `cancel` difficult. We should allow for coordination between caller and this loop.
 70        args = parser.parse_args(argv)
 71        raw = cls._read_input(args.input)
 72        payload = json.loads(raw)
 73        result = cls.send(**payload)
 74        while args.poll and payload.get("operation") == "invoke" and result.get("status") == "retry":
 75            state = result.get("adapter_state")
 76            if not isinstance(state, dict):
 77                raise DmlRepoError("Retry adapter response requires object adapter_state")
 78            payload["adapter_state"] = state
 79            time.sleep(0.1)
 80            result = cls.send(**payload)
 81        if args.poll and payload.get("operation") == "invoke" and result.get("status") == "success":
 82            payload["adapter_state"] = result.get("adapter_state", payload["adapter_state"])
 83            record = Dml(remote_root=payload["remote"]["root"]).runtime.read_execution_record(
 84                Ref(f"index:{payload['execution_id']}")
 85            )
 86            result_ref = record["state"]["result_ref"]
 87            if result_ref is None:
 88                raise DmlRepoError("Successful nested invoke did not publish a result")
 89            cleanup_payload = {**payload, "operation": "cleanup", "result_ref": result_ref}
 90            try:
 91                cleanup_result = cls.send(**cleanup_payload)
 92                while cleanup_result.get("status") == "retry":
 93                    state = cleanup_result.get("adapter_state")
 94                    if not isinstance(state, dict):
 95                        raise DmlRepoError("Retry adapter response requires object adapter_state")
 96                    cleanup_payload["adapter_state"] = state
 97                    time.sleep(0.1)
 98                    cleanup_result = cls.send(**cleanup_payload)
 99                if cleanup_result.get("status") != "success":
100                    print(
101                        f"Nested cleanup failed for {payload['execution_id']}: {cleanup_result.get('error')}",
102                        file=sys.stderr,
103                    )
104            except Exception as exc:
105                print(f"Nested cleanup failed for {payload['execution_id']}: {exc}", file=sys.stderr)
106        if args.poll and payload.get("operation") == "invoke":
107            result = {key: value for key, value in result.items() if key != "adapter_state"}
108        cls._write_output(args.output, json.dumps(result))
109        return 0

LocalAdapter

class LocalAdapter(AdapterBase):
View source
112class LocalAdapter(AdapterBase):
113    name = "local"
114    executable = "dml-local-adapter"
115
116    @classmethod
117    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
118        from daggerml.contrib.executors._base import get_executor
119
120        return get_executor("local", kw["runnable"]["target"]["uri"]).handle(**kw)

LocalAdapter.name

name= 'local'

LocalAdapter.executable

executable= 'dml-local-adapter'

LocalAdapter.send

@classmethod
def send(cls, **kw) -> dict[str, typing.Any]:
View source
116    @classmethod
117    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
118        from daggerml.contrib.executors._base import get_executor
119
120        return get_executor("local", kw["runnable"]["target"]["uri"]).handle(**kw)

Inherited members

AdapterBase
resolve_runnable
cli

LambdaAdapter

class LambdaAdapter(AdapterBase):
View source
123class LambdaAdapter(AdapterBase):
124    name = "lambda"
125    executable = "dml-lambda-adapter"
126
127    @classmethod
128    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
129        client = get_client("lambda")
130        try:
131            response = client.invoke(
132                FunctionName=kw["runnable"]["target"]["uri"],
133                InvocationType="RequestResponse",
134                Payload=json.dumps(kw, separators=(",", ":"), sort_keys=True).encode("utf-8"),
135            )
136        except Exception as exc:
137            code = getattr(exc, "response", {}).get("Error", {}).get("Code")
138            if code not in {"TooManyRequestsException", "ThrottlingException"}:
139                raise
140            headers = getattr(exc, "response", {}).get("ResponseMetadata", {}).get("HTTPHeaders", {})
141            try:
142                retry_after_ms = max(0, int(float(headers.get("retry-after")) * 1000))
143            except (TypeError, ValueError):
144                retry_after_ms = None
145            result: AdapterInvokeResponse = {
146                "status": "retry",
147                "error": None,
148                "adapter_state": kw.get("adapter_state") if isinstance(kw.get("adapter_state"), dict) else {},
149            }
150            if retry_after_ms is not None:
151                result["retry_after_ms"] = retry_after_ms
152            return result
153        stream = response.get("Payload")
154        if stream is None:
155            raise DmlRepoError("Lambda adapter invoke response missing Payload")
156        return json.loads(stream.read().decode("utf-8"))

LambdaAdapter.name

name= 'lambda'

LambdaAdapter.executable

executable= 'dml-lambda-adapter'

LambdaAdapter.send

@classmethod
def send(cls, **kw) -> dict[str, typing.Any]:
View source
127    @classmethod
128    def send(cls, **kw) -> AdapterInvokeResponse | AdapterCleanupResponse | AdapterCancelResponse:
129        client = get_client("lambda")
130        try:
131            response = client.invoke(
132                FunctionName=kw["runnable"]["target"]["uri"],
133                InvocationType="RequestResponse",
134                Payload=json.dumps(kw, separators=(",", ":"), sort_keys=True).encode("utf-8"),
135            )
136        except Exception as exc:
137            code = getattr(exc, "response", {}).get("Error", {}).get("Code")
138            if code not in {"TooManyRequestsException", "ThrottlingException"}:
139                raise
140            headers = getattr(exc, "response", {}).get("ResponseMetadata", {}).get("HTTPHeaders", {})
141            try:
142                retry_after_ms = max(0, int(float(headers.get("retry-after")) * 1000))
143            except (TypeError, ValueError):
144                retry_after_ms = None
145            result: AdapterInvokeResponse = {
146                "status": "retry",
147                "error": None,
148                "adapter_state": kw.get("adapter_state") if isinstance(kw.get("adapter_state"), dict) else {},
149            }
150            if retry_after_ms is not None:
151                result["retry_after_ms"] = retry_after_ms
152            return result
153        stream = response.get("Payload")
154        if stream is None:
155            raise DmlRepoError("Lambda adapter invoke response missing Payload")
156        return json.loads(stream.read().decode("utf-8"))

Inherited members

AdapterBase
resolve_runnable
cli

ADAPTER_ENTRYPOINT_GROUP

ADAPTER_ENTRYPOINT_GROUP= 'daggerml.contrib.adapters'

load_adapter_plugins

def load_adapter_plugins() -> None:
View source
169def load_adapter_plugins() -> None:
170    global _PLUGINS_LOADED
171    if _PLUGINS_LOADED:
172        return
173    with _LOCK:
174        if _PLUGINS_LOADED:
175            return
176        for ep in _entry_points(ADAPTER_ENTRYPOINT_GROUP):
177            try:
178                loaded = ep.load()
179                if loaded.name in _ADAPTER_SPECS:
180                    # warn about duplicate adapter registration but allow the last one to win
181                    warn(
182                        f"Adapter: '{loaded.name}' is overwriting existing '{ep.name} ({ep.value})'",
183                        stacklevel=2,
184                    )
185                _ADAPTER_SPECS[loaded.name] = loaded
186            except Exception as e:
187                raise DmlRepoError(f"Adapter plugin '{ep.name} ({ep.value})' failed: {e}") from e
188        _PLUGINS_LOADED = True

get_adapter

def get_adapter(name: str) -> Any:
View source
191def get_adapter(name: str) -> Any:
192    load_adapter_plugins()
193    spec = _ADAPTER_SPECS.get(name)
194    if spec is None:
195        raise DmlRepoError(f"Adapter '{name}' is not registered")
196    return spec

list_adapters

def list_adapters() -> list[str]:
View source
199def list_adapters() -> list[str]:
200    load_adapter_plugins()
201    return sorted(_ADAPTER_SPECS.keys())