londonaicentre-mesa-runtime 0.1.0__tar.gz

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.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.3
2
+ Name: londonaicentre-mesa-runtime
3
+ Version: 0.1.0
4
+ Author: Martin Chapman
5
+ Author-email: Martin Chapman <contact@martinchapman.co.uk>
6
+ Requires-Dist: boto3>=1.35
7
+ Requires-Dist: httpx>=0.28
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
12
+ # mesa-runtime/lib
13
+
14
+ A thin, programmatic wrapper around CI in this repo comprising a simple [backend](../frontend/deploy/) and a library that communicates with it.
15
+
16
+ ## Usage
17
+
18
+ ```python
19
+ from mesa_runtime import MesaRuntime
20
+
21
+ client = MesaRuntime("https://example.cloudfront.net", "username", "password")
22
+
23
+ run_id = client.dispatch("batch-2026-01", "foo", "onco")
24
+ print(client.get_run(run_id))
25
+ ```
26
+
27
+ Batch inference uploads the batch file with the caller's AWS credentials:
28
+
29
+ ```python
30
+ run_id = client.run_batch_inference("foo/bar", "baz", "qux.jsonl", "quux")
31
+ print(client.get_inference_run(run_id))
32
+ ```
33
+
34
+ ## API
35
+
36
+ | Method | Description |
37
+ |--------|-------------|
38
+ | `list_training_data()` | List available training data objects from S3 |
39
+ | `dispatch(training_batch_name, model_name, schema, ...)` | Trigger a workflow run, returns its ID |
40
+ | `get_run(run_id)` | Fetch a single run by ID |
41
+ | `list_runs()` | List the ten most recent runs |
42
+ | `list_queued_runs()` | List all pending runs |
43
+ | `cancel(run_id)` | Cancel a run |
44
+ | `get_logs(run_id, ...)` | Fetch step logs for a run |
45
+ | `get_step_inputs(run_id)` | Extract dispatch inputs from step env vars |
46
+ | `get_runner_status()` | Get runner online/busy status |
47
+ | `run_batch_inference(job_id, model_id, batch_file, bucket, ...)` | Upload a Bedrock batch file and trigger an inference run, returns its ID |
48
+ | `get_inference_run(run_id)` | Fetch a single inference run by ID |
49
+ | `list_inference_runs()` | List the ten most recent inference runs |
50
+ | `cancel_inference(run_id)` | Cancel an inference run |
51
+ | `get_inference_logs(run_id, ...)` | Fetch step logs for an inference run |
@@ -0,0 +1,40 @@
1
+ # mesa-runtime/lib
2
+
3
+ A thin, programmatic wrapper around CI in this repo comprising a simple [backend](../frontend/deploy/) and a library that communicates with it.
4
+
5
+ ## Usage
6
+
7
+ ```python
8
+ from mesa_runtime import MesaRuntime
9
+
10
+ client = MesaRuntime("https://example.cloudfront.net", "username", "password")
11
+
12
+ run_id = client.dispatch("batch-2026-01", "foo", "onco")
13
+ print(client.get_run(run_id))
14
+ ```
15
+
16
+ Batch inference uploads the batch file with the caller's AWS credentials:
17
+
18
+ ```python
19
+ run_id = client.run_batch_inference("foo/bar", "baz", "qux.jsonl", "quux")
20
+ print(client.get_inference_run(run_id))
21
+ ```
22
+
23
+ ## API
24
+
25
+ | Method | Description |
26
+ |--------|-------------|
27
+ | `list_training_data()` | List available training data objects from S3 |
28
+ | `dispatch(training_batch_name, model_name, schema, ...)` | Trigger a workflow run, returns its ID |
29
+ | `get_run(run_id)` | Fetch a single run by ID |
30
+ | `list_runs()` | List the ten most recent runs |
31
+ | `list_queued_runs()` | List all pending runs |
32
+ | `cancel(run_id)` | Cancel a run |
33
+ | `get_logs(run_id, ...)` | Fetch step logs for a run |
34
+ | `get_step_inputs(run_id)` | Extract dispatch inputs from step env vars |
35
+ | `get_runner_status()` | Get runner online/busy status |
36
+ | `run_batch_inference(job_id, model_id, batch_file, bucket, ...)` | Upload a Bedrock batch file and trigger an inference run, returns its ID |
37
+ | `get_inference_run(run_id)` | Fetch a single inference run by ID |
38
+ | `list_inference_runs()` | List the ten most recent inference runs |
39
+ | `cancel_inference(run_id)` | Cancel an inference run |
40
+ | `get_inference_logs(run_id, ...)` | Fetch step logs for an inference run |
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "londonaicentre-mesa-runtime"
3
+ version = "0.1.0"
4
+ readme = "README.md"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "boto3>=1.35",
8
+ "httpx>=0.28",
9
+ "pydantic>=2.0",
10
+ ]
11
+
12
+ [[project.authors]]
13
+ name = "Martin Chapman"
14
+ email = "contact@martinchapman.co.uk"
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.11.19,<0.12.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "boto3-stubs[s3]>=1.35",
23
+ "pytest>=9.1.0",
24
+ "pytest-cov>=6.0",
25
+ "pytest-mock>=3.0",
26
+ "ruff>=0.15.17",
27
+ "ty>=0.0.49",
28
+ ]
29
+
30
+ [tool.uv.build-backend]
31
+ module-name = "mesa_runtime"
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "londonaicentre-mesa-runtime"
3
+ version = "0.1.0"
4
+ readme = "README.md"
5
+ authors = [
6
+ { name = "Martin Chapman", email = "contact@martinchapman.co.uk" }
7
+ ]
8
+ requires-python = ">=3.12"
9
+ dependencies = [
10
+ "boto3>=1.35",
11
+ "httpx>=0.28",
12
+ "pydantic>=2.0",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.11.19,<0.12.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "boto3-stubs[s3]>=1.35",
22
+ "pytest>=9.1.0",
23
+ "pytest-cov>=6.0",
24
+ "pytest-mock>=3.0",
25
+ "ruff>=0.15.17",
26
+ "ty>=0.0.49",
27
+ ]
28
+
29
+ [tool.uv.build-backend]
30
+ module-name = "mesa_runtime"
@@ -0,0 +1,19 @@
1
+ from mesa_runtime.client import MesaRuntime
2
+ from mesa_runtime.types import (
3
+ BucketObject,
4
+ RunnerStatus,
5
+ RunnerStatusResponse,
6
+ WorkflowConclusion,
7
+ WorkflowRun,
8
+ WorkflowStatus,
9
+ )
10
+
11
+ __all__ = [
12
+ "BucketObject",
13
+ "MesaRuntime",
14
+ "RunnerStatus",
15
+ "RunnerStatusResponse",
16
+ "WorkflowConclusion",
17
+ "WorkflowRun",
18
+ "WorkflowStatus",
19
+ ]
@@ -0,0 +1,331 @@
1
+ import hashlib
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import boto3
7
+ from httpx import BasicAuth, Client, Response
8
+
9
+ from mesa_runtime.types import (
10
+ BucketObject,
11
+ InferenceInputs,
12
+ RunnerStatusResponse,
13
+ TrainingInputs,
14
+ WorkflowRun,
15
+ )
16
+
17
+
18
+ class MesaRuntime:
19
+ """A synchronous Python client for the MESA Runtime Lambda API.
20
+
21
+ Args:
22
+ base_url (str): Base URL of the Lambda/CloudFront endpoint
23
+ (e.g. ``"https://example.cloudfront.net"``).
24
+ username (str): Basic auth username.
25
+ password (str): Basic auth password.
26
+
27
+ """
28
+
29
+ __TRAINING: str = "/api/workflows"
30
+ __INFERENCE: str = "/api/inference"
31
+
32
+ def __init__(self, base_url: str, username: str, password: str) -> None:
33
+ self.__base_url: str = base_url.rstrip("/")
34
+ self.__auth: BasicAuth = BasicAuth(username, password)
35
+
36
+ @staticmethod
37
+ def _upload(batch_file: str, bucket: str, job_id: str) -> None:
38
+ boto3.client("s3").upload_file(
39
+ batch_file, bucket, f"{job_id}/input/{Path(batch_file).name}"
40
+ )
41
+
42
+ def _list_runs(self, prefix: str) -> list[WorkflowRun]:
43
+ return [
44
+ WorkflowRun.model_validate(run)
45
+ for run in self._request("GET", prefix).json()
46
+ ]
47
+
48
+ def _get_run(self, prefix: str, run_id: int) -> WorkflowRun:
49
+ return WorkflowRun.model_validate(
50
+ self._request("GET", f"{prefix}/{run_id}").json()
51
+ )
52
+
53
+ def _cancel(self, prefix: str, run_id: int) -> None:
54
+ self._request("DELETE", f"{prefix}/{run_id}")
55
+
56
+ def _get_logs(self, prefix: str, run_id: int, job: str, step: str) -> str:
57
+ return self._request(
58
+ "GET", f"{prefix}/{run_id}/logs", None, {"job": job, "step": step}
59
+ ).text
60
+
61
+ def _request(
62
+ self,
63
+ method: str,
64
+ path: str,
65
+ body: dict[str, Any] | None = None,
66
+ params: dict[str, str] | None = None,
67
+ timeout: float | None = None,
68
+ ) -> Response:
69
+ headers: dict[str, str] = {"x-requested-with": "fetch"}
70
+ content: bytes | None = None
71
+ if body is not None:
72
+ content = json.dumps(body).encode()
73
+ headers["content-type"] = "application/json"
74
+ headers["x-amz-content-sha256"] = hashlib.sha256(content).hexdigest()
75
+ with Client(auth=self.__auth, follow_redirects=True, timeout=timeout) as client:
76
+ response: Response = client.request(
77
+ method,
78
+ self.__base_url + path,
79
+ content=content,
80
+ headers=headers,
81
+ params=params,
82
+ )
83
+ response.raise_for_status()
84
+ return response
85
+
86
+ def list_training_data(self) -> list[BucketObject]:
87
+ """Lists available training data objects from S3.
88
+
89
+ Returns:
90
+ list[BucketObject]: Training data objects with key,
91
+ last_modified, and size.
92
+
93
+ """
94
+ return [
95
+ BucketObject.model_validate(obj)
96
+ for obj in self._request("GET", "/api/trainingdata").json()
97
+ ]
98
+
99
+ def list_runs(self) -> list[WorkflowRun]:
100
+ """Lists the ten most recent workflow runs.
101
+
102
+ Returns:
103
+ list[WorkflowRun]: Most recent runs, newest first.
104
+
105
+ """
106
+ return self._list_runs(MesaRuntime.__TRAINING)
107
+
108
+ def list_queued_runs(self) -> list[WorkflowRun]:
109
+ """Lists all pending workflow runs.
110
+
111
+ Returns:
112
+ list[WorkflowRun]: All queued/pending runs.
113
+
114
+ """
115
+ return [
116
+ WorkflowRun.model_validate(run)
117
+ for run in self._request("GET", f"{MesaRuntime.__TRAINING}/queued").json()
118
+ ]
119
+
120
+ def dispatch(
121
+ self,
122
+ training_batch_name: str,
123
+ model_name: str,
124
+ schema: str,
125
+ description: str = "",
126
+ version: str = "1.0.0",
127
+ config: str = "../../../mesa-runtime/config.build.finetune.yaml",
128
+ config_from_file: bool = False,
129
+ push_public: bool = False,
130
+ resume: bool = False,
131
+ post_process_only: bool = False,
132
+ ) -> int:
133
+ """Dispatches a workflow run and returns its run ID.
134
+
135
+ Args:
136
+ training_batch_name (str): Name of the training data batch.
137
+ model_name (str): Model name (normalised to lowercase-hyphenated).
138
+ schema (str): Schema type (e.g. ``"onco"``).
139
+ description (str): Optional run description. Defaults to ``""``.
140
+ version (str): Model version string. Defaults to ``"1.0.0"``.
141
+ config (str): Path to a YAML config file. Interpreted by the
142
+ runner unless ``config_from_file`` is set, in which case it
143
+ is instead read locally and sent as ``config_yaml``.
144
+ Defaults to ``"../../../mesa-runtime/config.build.finetune.yaml"``.
145
+ config_from_file (bool): Read ``config`` as a local file and
146
+ send its contents as ``config_yaml`` instead of sending
147
+ ``config`` as a path. Defaults to ``False``.
148
+ push_public (bool): Push the trained model to the public
149
+ registry after post-processing. Defaults to ``False``.
150
+ resume (bool): Resume the runner's currently crashed run from
151
+ its on-disk checkpoint instead of starting a new one.
152
+ Defaults to ``False``.
153
+ post_process_only (bool): Skip training; fuse and publish the
154
+ existing trainer.json's already-trained adapter. Mutually
155
+ exclusive with ``resume``. Defaults to ``False``.
156
+
157
+ Returns:
158
+ int: The ID of the dispatched run.
159
+
160
+ """
161
+ return self._request(
162
+ "POST",
163
+ MesaRuntime.__TRAINING,
164
+ TrainingInputs(
165
+ training_batch_name=training_batch_name,
166
+ model_name=model_name,
167
+ schema=schema,
168
+ description=description,
169
+ version=version,
170
+ config="" if config_from_file else config,
171
+ config_yaml=Path(config).read_text() if config_from_file else "",
172
+ push_public=push_public,
173
+ resume=resume,
174
+ post_process_only=post_process_only,
175
+ ).model_dump(by_alias=True),
176
+ timeout=30.0,
177
+ ).json()["run_id"]
178
+
179
+ def get_run(self, run_id: int) -> WorkflowRun:
180
+ """Gets a single workflow run by ID.
181
+
182
+ Args:
183
+ run_id (int): The run ID.
184
+
185
+ Returns:
186
+ WorkflowRun: The run's current state.
187
+
188
+ """
189
+ return self._get_run(MesaRuntime.__TRAINING, run_id)
190
+
191
+ def cancel(self, run_id: int) -> None:
192
+ """Cancels a workflow run.
193
+
194
+ Args:
195
+ run_id (int): The run ID.
196
+
197
+ """
198
+ self._cancel(MesaRuntime.__TRAINING, run_id)
199
+
200
+ def get_logs(
201
+ self,
202
+ run_id: int,
203
+ job: str = "run",
204
+ step: str = "Run uv run mesa-build-mlx-finetune",
205
+ ) -> str:
206
+ """Gets the log output for a named step within a job.
207
+
208
+ Args:
209
+ run_id (int): The run ID.
210
+ job (str): Name of the job to target. Defaults to ``"run"``.
211
+ step (str): Name of the step within the job. Defaults to
212
+ ``"Run uv run mesa-build-mlx-finetune"``.
213
+
214
+ Returns:
215
+ str: The step's log output.
216
+
217
+ """
218
+ return self._get_logs(MesaRuntime.__TRAINING, run_id, job, step)
219
+
220
+ def get_step_inputs(self, run_id: int) -> dict[str, str]:
221
+ """Gets the dispatch inputs for a run by parsing its step logs.
222
+
223
+ Args:
224
+ run_id (int): The run ID.
225
+
226
+ Returns:
227
+ dict[str, str]: Env vars from the step setup block.
228
+
229
+ """
230
+ return self._request("GET", f"{MesaRuntime.__TRAINING}/{run_id}/inputs").json()
231
+
232
+ def get_runner_status(self) -> RunnerStatusResponse:
233
+ """Gets the current runner online/busy status.
234
+
235
+ Returns:
236
+ RunnerStatusResponse: Runner status and busy flag.
237
+
238
+ """
239
+ return RunnerStatusResponse.model_validate(
240
+ self._request("GET", "/api/workflows/runner/status").json()
241
+ )
242
+
243
+ def run_batch_inference(
244
+ self,
245
+ job_id: str,
246
+ model_id: str,
247
+ batch_file: str,
248
+ bucket: str,
249
+ config: str = "../mesa-runtime/config.build.inference.yml",
250
+ config_from_file: bool = False,
251
+ ) -> int:
252
+ """Uploads a Bedrock batch file to S3 and dispatches an inference run
253
+ over it, in place of a Bedrock batch inference job.
254
+
255
+ Args:
256
+ job_id (str): The id of the batch job, used as its S3 prefix.
257
+ model_id (str): Name of the model served by the runner's
258
+ OpenAI-compatible endpoint.
259
+ batch_file (str): Path to the local Bedrock batch input file.
260
+ bucket (str): The name of the bucket to which the batch file
261
+ should be uploaded and the runner should write output.
262
+ config (str): Path to a YAML config file. Interpreted by the
263
+ runner unless ``config_from_file`` is set, in which case it
264
+ is instead read locally and sent as ``config_yaml``.
265
+ Defaults to ``"../mesa-runtime/config.build.inference.yml"``.
266
+ config_from_file (bool): Read ``config`` as a local file and
267
+ send its contents as ``config_yaml`` instead of sending
268
+ ``config`` as a path. Defaults to ``False``.
269
+
270
+ Returns:
271
+ int: The ID of the dispatched run.
272
+
273
+ """
274
+ inputs: InferenceInputs = InferenceInputs(
275
+ job_id=job_id,
276
+ model_name=model_id,
277
+ bucket=bucket,
278
+ config="" if config_from_file else config,
279
+ config_yaml=Path(config).read_text() if config_from_file else "",
280
+ )
281
+ MesaRuntime._upload(batch_file, inputs.bucket, inputs.job_id)
282
+ return self._request(
283
+ "POST", MesaRuntime.__INFERENCE, inputs.model_dump(), timeout=30.0
284
+ ).json()["run_id"]
285
+
286
+ def list_inference_runs(self) -> list[WorkflowRun]:
287
+ """Lists the ten most recent inference runs.
288
+
289
+ Returns:
290
+ list[WorkflowRun]: Most recent runs, newest first.
291
+
292
+ """
293
+ return self._list_runs(MesaRuntime.__INFERENCE)
294
+
295
+ def get_inference_run(self, run_id: int) -> WorkflowRun:
296
+ """Gets a single inference run by ID.
297
+
298
+ Args:
299
+ run_id (int): The run ID.
300
+
301
+ Returns:
302
+ WorkflowRun: The run's current state.
303
+
304
+ """
305
+ return self._get_run(MesaRuntime.__INFERENCE, run_id)
306
+
307
+ def cancel_inference(self, run_id: int) -> None:
308
+ """Cancels an inference run.
309
+
310
+ Args:
311
+ run_id (int): The run ID.
312
+
313
+ """
314
+ self._cancel(MesaRuntime.__INFERENCE, run_id)
315
+
316
+ def get_inference_logs(
317
+ self, run_id: int, job: str = "run", step: str = "Infer"
318
+ ) -> str:
319
+ """Gets the log output for a named step within an inference job.
320
+
321
+ Args:
322
+ run_id (int): The run ID.
323
+ job (str): Name of the job to target. Defaults to ``"run"``.
324
+ step (str): Name of the step within the job. Defaults to
325
+ ``"Infer"``.
326
+
327
+ Returns:
328
+ str: The step's log output.
329
+
330
+ """
331
+ return self._get_logs(MesaRuntime.__INFERENCE, run_id, job, step)
@@ -0,0 +1,97 @@
1
+ from datetime import datetime
2
+ from enum import StrEnum
3
+
4
+ from pydantic import (
5
+ BaseModel,
6
+ ConfigDict,
7
+ Field,
8
+ HttpUrl,
9
+ field_validator,
10
+ model_validator,
11
+ )
12
+
13
+
14
+ class BucketObject(BaseModel):
15
+ key: str
16
+ last_modified: str
17
+ size: int
18
+
19
+
20
+ class WorkflowStatus(StrEnum):
21
+ QUEUED = "queued"
22
+ IN_PROGRESS = "in_progress"
23
+ COMPLETED = "completed"
24
+ WAITING = "waiting"
25
+ REQUESTED = "requested"
26
+ PENDING = "pending"
27
+
28
+
29
+ class WorkflowConclusion(StrEnum):
30
+ ACTION_REQUIRED = "action_required"
31
+ CANCELLED = "cancelled"
32
+ FAILURE = "failure"
33
+ NEUTRAL = "neutral"
34
+ SUCCESS = "success"
35
+ SKIPPED = "skipped"
36
+ STALE = "stale"
37
+ TIMED_OUT = "timed_out"
38
+ STARTUP_FAILURE = "startup_failure"
39
+
40
+
41
+ class WorkflowRun(BaseModel):
42
+ id: int
43
+ status: WorkflowStatus
44
+ conclusion: WorkflowConclusion | None
45
+ created_at: datetime
46
+ html_url: HttpUrl
47
+
48
+
49
+ class DispatchInputs(BaseModel):
50
+ config: str = Field(default="", max_length=256)
51
+ config_yaml: str = Field(default="", max_length=65536)
52
+
53
+ @model_validator(mode="after")
54
+ def _validate_config_exclusive(self) -> "DispatchInputs":
55
+ if self.config and self.config_yaml:
56
+ raise ValueError("cannot supply both config and config_yaml")
57
+ return self
58
+
59
+
60
+ class TrainingInputs(DispatchInputs):
61
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
62
+
63
+ training_batch_name: str = Field(default="", max_length=256)
64
+ model_name: str = Field(default="", max_length=256)
65
+ schema_: str = Field(default="", alias="schema", max_length=256)
66
+ description: str = Field(default="", max_length=256)
67
+ version: str = Field(default="", max_length=64)
68
+ push_public: bool = Field(default=False)
69
+ resume: bool = Field(default=False)
70
+ post_process_only: bool = Field(default=False)
71
+
72
+ @field_validator("model_name", mode="before")
73
+ @classmethod
74
+ def _normalise_model_name(cls, value: str) -> str:
75
+ return value.replace(" ", "-").lower()
76
+
77
+ @model_validator(mode="after")
78
+ def _validate_resume_exclusive(self) -> "TrainingInputs":
79
+ if self.resume and self.post_process_only:
80
+ raise ValueError("cannot supply both resume and post_process_only")
81
+ return self
82
+
83
+
84
+ class InferenceInputs(DispatchInputs):
85
+ job_id: str = Field(max_length=256, pattern=r"^[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+)*$")
86
+ model_name: str = Field(min_length=1, max_length=256)
87
+ bucket: str = Field(default="", pattern=r"^([a-z0-9][a-z0-9.-]{1,61}[a-z0-9])?$")
88
+
89
+
90
+ class RunnerStatus(StrEnum):
91
+ ONLINE = "online"
92
+ OFFLINE = "offline"
93
+
94
+
95
+ class RunnerStatusResponse(BaseModel):
96
+ status: RunnerStatus
97
+ busy: bool