larex-action-sdk 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,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: astral-sh/setup-uv@v7
14
+ with:
15
+ enable-cache: true
16
+ - run: uv sync --all-extras
17
+ - run: uv run ruff format --check .
18
+ - run: uv run ruff check .
19
+ - run: uv run pyright
20
+ - run: uv run pytest
21
+ - run: uv build
@@ -0,0 +1,59 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*rc*"
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ publish:
16
+ if: ${{ github.event_name == 'release' && !github.event.release.prerelease && !contains(github.event.release.tag_name, 'rc') }}
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ id-token: write
21
+ environment:
22
+ name: pypi
23
+ url: https://pypi.org/p/larex-action-sdk
24
+ steps:
25
+ - uses: actions/checkout@v5
26
+ - uses: astral-sh/setup-uv@v7
27
+ with:
28
+ enable-cache: true
29
+ - run: uv sync --locked --all-extras
30
+ - run: uv run ruff format --check .
31
+ - run: uv run ruff check .
32
+ - run: uv run pyright
33
+ - run: uv run pytest
34
+ - run: uv build
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
36
+
37
+ publish-testpypi:
38
+ if: ${{ (github.event_name == 'push' && contains(github.ref_name, 'rc')) || github.event_name == 'workflow_dispatch' }}
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ contents: read
42
+ id-token: write
43
+ environment:
44
+ name: testpypi
45
+ url: https://test.pypi.org/p/larex-action-sdk
46
+ steps:
47
+ - uses: actions/checkout@v5
48
+ - uses: astral-sh/setup-uv@v7
49
+ with:
50
+ enable-cache: true
51
+ - run: uv sync --locked --all-extras
52
+ - run: uv run ruff format --check .
53
+ - run: uv run ruff check .
54
+ - run: uv run pyright
55
+ - run: uv run pytest
56
+ - run: uv build
57
+ - uses: pypa/gh-action-pypi-publish@release/v1
58
+ with:
59
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ .pyright/
5
+ dist/
6
+ htmlcov/
7
+ coverage.xml
8
+ __pycache__/
9
+ *.py[cod]
10
+ .idea
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OCR4all
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: larex-action-sdk
3
+ Version: 0.1.0
4
+ Summary: Framework-neutral Python SDK for building LAREX Action processors.
5
+ Project-URL: Homepage, https://github.com/OCR4all/larex-action-sdk
6
+ Project-URL: Repository, https://github.com/OCR4all/larex-action-sdk
7
+ Project-URL: Issues, https://github.com/OCR4all/larex-action-sdk/issues
8
+ Author: OCR4all
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: actions,larex,ocr,processors
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx<1,>=0.28
23
+ Requires-Dist: pydantic<3,>=2.10
24
+ Provides-Extra: fastapi
25
+ Requires-Dist: fastapi<1,>=0.115; extra == 'fastapi'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # LAREX Action SDK
29
+
30
+ > This SDK is work in progress. The public API can still change before LAREX
31
+ > Actions and the SDK are considered stable.
32
+
33
+ Framework-neutral Python SDK for building [LAREX](https://github.com/OCR4all/larex)
34
+ Action processors.
35
+
36
+ The core package verifies LAREX dispatch requests, parses typed run/input payloads,
37
+ sends heartbeats, downloads selected files, and uploads result manifests. FastAPI
38
+ support is available as an optional convenience extra.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ uv add "larex-action-sdk[fastapi]"
44
+ ```
45
+
46
+ For framework-neutral usage only:
47
+
48
+ ```bash
49
+ uv add larex-action-sdk
50
+ ```
51
+
52
+ ## FastAPI Processor
53
+
54
+ ```python
55
+ import os
56
+
57
+ from larex_actions import ActionContext
58
+ from larex_actions.fastapi import create_larex_action_app
59
+
60
+
61
+ async def process(ctx: ActionContext) -> None:
62
+ action_input = await ctx.pull_input()
63
+ results = ctx.result_builder()
64
+
65
+ for page in action_input.pages:
66
+ await ctx.heartbeat(25, f"Processing {page.name}", raise_on_cancel=True)
67
+ if page.xml:
68
+ xml_bytes = await ctx.download_bytes(page.xml[0])
69
+ results.add_xml_bytes(
70
+ page_id=page.id,
71
+ content=xml_bytes,
72
+ file_name=f"{page.name}-processed.xml",
73
+ variant="processed",
74
+ )
75
+
76
+ await ctx.complete(results, "Done")
77
+
78
+
79
+ app = create_larex_action_app(
80
+ processor_id="my-processor",
81
+ dispatch_secret=os.environ["LAREX_DISPATCH_HMAC_SECRET"],
82
+ handler=process,
83
+ )
84
+ ```
85
+
86
+ ## Framework-Neutral Dispatch Verification
87
+
88
+ ```python
89
+ from larex_actions import DispatchVerifier
90
+
91
+ payload = DispatchVerifier(
92
+ processor_id="my-processor",
93
+ dispatch_secret=secret,
94
+ ).verify(
95
+ method=request_method,
96
+ path_and_query=request_path_and_query,
97
+ headers=request_headers,
98
+ body=request_body,
99
+ )
100
+ ```
101
+
102
+ You can then pass `payload.model_dump(mode="json", by_alias=True)` to your own
103
+ queue/worker system and use `ActionClient.from_dispatch(payload)` in async workers.
104
+
105
+ ## Security
106
+
107
+ - Dispatch requests are verified with the `X-LAREX-Action-*` HMAC headers.
108
+ - Timestamps and nonces are checked to reduce replay risk.
109
+ - Per-run bearer secrets and dispatch HMAC secrets are never included in model reprs.
110
+ - Processor YAML must still declare the inputs and outputs LAREX should expose or accept.
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ uv sync --all-extras
116
+ uv run ruff format .
117
+ uv run ruff check .
118
+ uv run pyright
119
+ uv run pytest
120
+ uv build
121
+ ```
122
+
123
+ Releases are published with PyPI Trusted Publishing from GitHub Actions. Release
124
+ candidate tags containing `rc` publish to TestPyPI; published GitHub releases
125
+ publish to PyPI.
@@ -0,0 +1,98 @@
1
+ # LAREX Action SDK
2
+
3
+ > This SDK is work in progress. The public API can still change before LAREX
4
+ > Actions and the SDK are considered stable.
5
+
6
+ Framework-neutral Python SDK for building [LAREX](https://github.com/OCR4all/larex)
7
+ Action processors.
8
+
9
+ The core package verifies LAREX dispatch requests, parses typed run/input payloads,
10
+ sends heartbeats, downloads selected files, and uploads result manifests. FastAPI
11
+ support is available as an optional convenience extra.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ uv add "larex-action-sdk[fastapi]"
17
+ ```
18
+
19
+ For framework-neutral usage only:
20
+
21
+ ```bash
22
+ uv add larex-action-sdk
23
+ ```
24
+
25
+ ## FastAPI Processor
26
+
27
+ ```python
28
+ import os
29
+
30
+ from larex_actions import ActionContext
31
+ from larex_actions.fastapi import create_larex_action_app
32
+
33
+
34
+ async def process(ctx: ActionContext) -> None:
35
+ action_input = await ctx.pull_input()
36
+ results = ctx.result_builder()
37
+
38
+ for page in action_input.pages:
39
+ await ctx.heartbeat(25, f"Processing {page.name}", raise_on_cancel=True)
40
+ if page.xml:
41
+ xml_bytes = await ctx.download_bytes(page.xml[0])
42
+ results.add_xml_bytes(
43
+ page_id=page.id,
44
+ content=xml_bytes,
45
+ file_name=f"{page.name}-processed.xml",
46
+ variant="processed",
47
+ )
48
+
49
+ await ctx.complete(results, "Done")
50
+
51
+
52
+ app = create_larex_action_app(
53
+ processor_id="my-processor",
54
+ dispatch_secret=os.environ["LAREX_DISPATCH_HMAC_SECRET"],
55
+ handler=process,
56
+ )
57
+ ```
58
+
59
+ ## Framework-Neutral Dispatch Verification
60
+
61
+ ```python
62
+ from larex_actions import DispatchVerifier
63
+
64
+ payload = DispatchVerifier(
65
+ processor_id="my-processor",
66
+ dispatch_secret=secret,
67
+ ).verify(
68
+ method=request_method,
69
+ path_and_query=request_path_and_query,
70
+ headers=request_headers,
71
+ body=request_body,
72
+ )
73
+ ```
74
+
75
+ You can then pass `payload.model_dump(mode="json", by_alias=True)` to your own
76
+ queue/worker system and use `ActionClient.from_dispatch(payload)` in async workers.
77
+
78
+ ## Security
79
+
80
+ - Dispatch requests are verified with the `X-LAREX-Action-*` HMAC headers.
81
+ - Timestamps and nonces are checked to reduce replay risk.
82
+ - Per-run bearer secrets and dispatch HMAC secrets are never included in model reprs.
83
+ - Processor YAML must still declare the inputs and outputs LAREX should expose or accept.
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ uv sync --all-extras
89
+ uv run ruff format .
90
+ uv run ruff check .
91
+ uv run pyright
92
+ uv run pytest
93
+ uv build
94
+ ```
95
+
96
+ Releases are published with PyPI Trusted Publishing from GitHub Actions. Release
97
+ candidate tags containing `rc` publish to TestPyPI; published GitHub releases
98
+ publish to PyPI.
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "larex-action-sdk"
7
+ version = "0.1.0"
8
+ description = "Framework-neutral Python SDK for building LAREX Action processors."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "OCR4all" }
14
+ ]
15
+ keywords = ["larex", "ocr", "actions", "processors"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Framework :: FastAPI",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Typing :: Typed"
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.28,<1",
29
+ "pydantic>=2.10,<3"
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ fastapi = [
34
+ "fastapi>=0.115,<1"
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/OCR4all/larex-action-sdk"
39
+ Repository = "https://github.com/OCR4all/larex-action-sdk"
40
+ Issues = "https://github.com/OCR4all/larex-action-sdk/issues"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "fastapi>=0.115,<1",
45
+ "pyright>=1.1.407",
46
+ "pytest>=8.3,<9",
47
+ "pytest-asyncio>=0.25,<1",
48
+ "ruff>=0.9,<1"
49
+ ]
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/larex_actions"]
53
+
54
+ [tool.pytest.ini_options]
55
+ addopts = "-q"
56
+ asyncio_mode = "auto"
57
+ testpaths = ["tests"]
58
+
59
+ [tool.ruff]
60
+ line-length = 100
61
+ target-version = "py311"
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "UP", "B", "C4", "SIM", "PTH", "RUF"]
65
+
66
+ [tool.pyright]
67
+ include = ["src", "tests"]
68
+ pythonVersion = "3.11"
69
+ typeCheckingMode = "strict"
70
+ reportUnknownMemberType = false
71
+ reportUnknownVariableType = false
72
+ reportUnknownArgumentType = false
@@ -0,0 +1,38 @@
1
+ from .client import ActionClient, ActionContext
2
+ from .exceptions import ActionCancelled, DispatchVerificationError, LarexActionError
3
+ from .models import (
4
+ ActionDispatchPayload,
5
+ ActionFile,
6
+ ActionInput,
7
+ ActionPage,
8
+ FileType,
9
+ HeartbeatResponse,
10
+ ResultFile,
11
+ ResultManifest,
12
+ ResultStatus,
13
+ RunStatus,
14
+ )
15
+ from .nonce import NonceStore
16
+ from .results import ResultBuilder
17
+ from .verifier import DispatchVerifier
18
+
19
+ __all__ = [
20
+ "ActionCancelled",
21
+ "ActionClient",
22
+ "ActionContext",
23
+ "ActionDispatchPayload",
24
+ "ActionFile",
25
+ "ActionInput",
26
+ "ActionPage",
27
+ "DispatchVerificationError",
28
+ "DispatchVerifier",
29
+ "FileType",
30
+ "HeartbeatResponse",
31
+ "LarexActionError",
32
+ "NonceStore",
33
+ "ResultBuilder",
34
+ "ResultFile",
35
+ "ResultManifest",
36
+ "ResultStatus",
37
+ "RunStatus",
38
+ ]
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from contextlib import ExitStack
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from .exceptions import ActionCancelled
11
+ from .models import (
12
+ ActionDispatchPayload,
13
+ ActionFile,
14
+ ActionInput,
15
+ HeartbeatResponse,
16
+ ResultStatus,
17
+ RunStatus,
18
+ )
19
+ from .results import ResultBuilder
20
+
21
+
22
+ class ActionClient:
23
+ def __init__(
24
+ self,
25
+ *,
26
+ pull_url: str,
27
+ heartbeat_url: str,
28
+ result_url: str,
29
+ secret: str,
30
+ client: httpx.AsyncClient | None = None,
31
+ timeout: float | httpx.Timeout = 120.0,
32
+ ) -> None:
33
+ self.pull_url = pull_url
34
+ self.heartbeat_url = heartbeat_url
35
+ self.result_url = result_url
36
+ self._secret = secret
37
+ self._client = client or httpx.AsyncClient(timeout=timeout)
38
+ self._owns_client = client is None
39
+
40
+ @classmethod
41
+ def from_dispatch(
42
+ cls,
43
+ payload: ActionDispatchPayload,
44
+ *,
45
+ client: httpx.AsyncClient | None = None,
46
+ timeout: float | httpx.Timeout = 120.0,
47
+ ) -> ActionClient:
48
+ return cls(
49
+ pull_url=payload.pull_url,
50
+ heartbeat_url=payload.heartbeat_url,
51
+ result_url=payload.result_url,
52
+ secret=payload.secret.get_secret_value(),
53
+ client=client,
54
+ timeout=timeout,
55
+ )
56
+
57
+ async def __aenter__(self) -> ActionClient:
58
+ return self
59
+
60
+ async def __aexit__(self, *_exc_info: object) -> None:
61
+ await self.aclose()
62
+
63
+ async def aclose(self) -> None:
64
+ if self._owns_client:
65
+ await self._client.aclose()
66
+
67
+ async def pull_input(self) -> ActionInput:
68
+ response = await self._client.get(self.pull_url, headers=self._auth_headers())
69
+ response.raise_for_status()
70
+ return ActionInput.model_validate(response.json())
71
+
72
+ async def heartbeat(
73
+ self,
74
+ progress_percent: int | None = None,
75
+ status_message: str | None = None,
76
+ *,
77
+ log: str | None = None,
78
+ status: RunStatus = "running",
79
+ error_message: str | None = None,
80
+ raise_on_cancel: bool = False,
81
+ ) -> HeartbeatResponse:
82
+ payload: dict[str, Any] = {
83
+ "status": status,
84
+ "progressPercent": progress_percent,
85
+ "statusMessage": status_message,
86
+ "log": log,
87
+ "errorMessage": error_message,
88
+ }
89
+ response = await self._client.post(
90
+ self.heartbeat_url,
91
+ headers=self._auth_headers(),
92
+ json={key: value for key, value in payload.items() if value is not None},
93
+ )
94
+ response.raise_for_status()
95
+ heartbeat = HeartbeatResponse.model_validate(response.json())
96
+ if raise_on_cancel and heartbeat.cancel_requested:
97
+ raise ActionCancelled("LAREX requested cancellation")
98
+ return heartbeat
99
+
100
+ async def download_bytes(self, file: ActionFile) -> bytes:
101
+ response = await self._client.get(file.download_url, headers=self._auth_headers())
102
+ response.raise_for_status()
103
+ return response.content
104
+
105
+ async def download_to_path(self, file: ActionFile, path: str | Path) -> Path:
106
+ target = Path(path)
107
+ target.parent.mkdir(parents=True, exist_ok=True)
108
+ async with self._client.stream(
109
+ "GET",
110
+ file.download_url,
111
+ headers=self._auth_headers(),
112
+ ) as response:
113
+ response.raise_for_status()
114
+ with target.open("wb") as output:
115
+ async for chunk in response.aiter_bytes():
116
+ output.write(chunk)
117
+ return target
118
+
119
+ async def complete(
120
+ self,
121
+ results: ResultBuilder,
122
+ message: str | None = None,
123
+ ) -> Mapping[str, Any]:
124
+ return await self._post_results(results, status="completed", message=message)
125
+
126
+ async def upload_results(
127
+ self,
128
+ results: ResultBuilder,
129
+ *,
130
+ status: ResultStatus = "completed",
131
+ message: str | None = None,
132
+ ) -> Mapping[str, Any]:
133
+ return await self._post_results(results, status=status, message=message)
134
+
135
+ async def fail(
136
+ self,
137
+ message: str,
138
+ *,
139
+ log: str | None = None,
140
+ progress_percent: int | None = None,
141
+ ) -> HeartbeatResponse:
142
+ return await self.heartbeat(
143
+ progress_percent=progress_percent,
144
+ status_message=message,
145
+ log=log,
146
+ status="failed",
147
+ error_message=message,
148
+ )
149
+
150
+ async def _post_results(
151
+ self,
152
+ results: ResultBuilder,
153
+ *,
154
+ status: ResultStatus,
155
+ message: str | None,
156
+ ) -> Mapping[str, Any]:
157
+ with ExitStack() as exit_stack:
158
+ response = await self._client.post(
159
+ self.result_url,
160
+ headers=self._auth_headers(),
161
+ files=results.httpx_files(status=status, message=message, exit_stack=exit_stack),
162
+ )
163
+ response.raise_for_status()
164
+ data = response.json()
165
+ return data if isinstance(data, Mapping) else {"response": data}
166
+
167
+ def _auth_headers(self) -> dict[str, str]:
168
+ return {"Authorization": f"Bearer {self._secret}"}
169
+
170
+
171
+ class ActionContext:
172
+ def __init__(self, *, payload: ActionDispatchPayload, client: ActionClient) -> None:
173
+ self.payload = payload
174
+ self.client = client
175
+
176
+ @property
177
+ def run_id(self) -> str:
178
+ return self.payload.run_id
179
+
180
+ @property
181
+ def processor_id(self) -> str:
182
+ return self.payload.processor_id
183
+
184
+ @property
185
+ def parameters(self) -> dict[str, Any]:
186
+ return self.payload.parameters
187
+
188
+ async def pull_input(self) -> ActionInput:
189
+ return await self.client.pull_input()
190
+
191
+ async def heartbeat(
192
+ self,
193
+ progress_percent: int | None = None,
194
+ status_message: str | None = None,
195
+ *,
196
+ log: str | None = None,
197
+ raise_on_cancel: bool = False,
198
+ ) -> HeartbeatResponse:
199
+ return await self.client.heartbeat(
200
+ progress_percent=progress_percent,
201
+ status_message=status_message,
202
+ log=log,
203
+ raise_on_cancel=raise_on_cancel,
204
+ )
205
+
206
+ async def raise_if_cancelled(self) -> None:
207
+ await self.heartbeat(raise_on_cancel=True)
208
+
209
+ async def download_bytes(self, file: ActionFile) -> bytes:
210
+ return await self.client.download_bytes(file)
211
+
212
+ async def download_to_path(self, file: ActionFile, path: str | Path) -> Path:
213
+ return await self.client.download_to_path(file, path)
214
+
215
+ def result_builder(self) -> ResultBuilder:
216
+ return ResultBuilder()
217
+
218
+ async def complete(
219
+ self,
220
+ results: ResultBuilder,
221
+ message: str | None = None,
222
+ ) -> Mapping[str, Any]:
223
+ return await self.client.complete(results, message)
224
+
225
+ async def upload_results(
226
+ self,
227
+ results: ResultBuilder,
228
+ *,
229
+ status: ResultStatus = "completed",
230
+ message: str | None = None,
231
+ ) -> Mapping[str, Any]:
232
+ return await self.client.upload_results(results, status=status, message=message)
233
+
234
+ async def fail(self, message: str, *, log: str | None = None) -> HeartbeatResponse:
235
+ return await self.client.fail(message, log=log)
@@ -0,0 +1,14 @@
1
+ class LarexActionError(Exception):
2
+ """Base exception raised by the LAREX Action SDK."""
3
+
4
+
5
+ class DispatchVerificationError(LarexActionError):
6
+ """Raised when an incoming dispatch request cannot be trusted."""
7
+
8
+ def __init__(self, message: str, *, status_code: int = 401) -> None:
9
+ super().__init__(message)
10
+ self.status_code = status_code
11
+
12
+
13
+ class ActionCancelled(LarexActionError):
14
+ """Raised when LAREX asks the processor to stop cooperatively."""