ringo-task-queue 0.1.0.dev0__py3-none-win_amd64.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.
- ringo_task_queue/__init__.py +68 -0
- ringo_task_queue/_pb.py +154 -0
- ringo_task_queue/_proto/ringo/v1/queue_pb2.py +133 -0
- ringo_task_queue/_proto/ringo/v1/queue_pb2_grpc.py +574 -0
- ringo_task_queue/_version.py +13 -0
- ringo_task_queue/bin/manifest.json +20 -0
- ringo_task_queue/bin/ringo-task-queue-windows-amd64.exe +0 -0
- ringo_task_queue/binary.py +253 -0
- ringo_task_queue/client.py +863 -0
- ringo_task_queue/daemon.py +507 -0
- ringo_task_queue/errors.py +152 -0
- ringo_task_queue/models.py +364 -0
- ringo_task_queue/py.typed +0 -0
- ringo_task_queue/worker.py +785 -0
- ringo_task_queue-0.1.0.dev0.dist-info/METADATA +445 -0
- ringo_task_queue-0.1.0.dev0.dist-info/RECORD +18 -0
- ringo_task_queue-0.1.0.dev0.dist-info/WHEEL +4 -0
- ringo_task_queue-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Platform daemon discovery and integrity validation.
|
|
2
|
+
|
|
3
|
+
Platform wheels contain a manifest beside one or more daemon binaries. The
|
|
4
|
+
manifest is generated by ``tools/stage_binary.py`` and is deliberately not
|
|
5
|
+
checked into source control with a real executable. Explicit paths and the
|
|
6
|
+
``RINGO_DAEMON_PATH`` override remain useful for development and are resolved
|
|
7
|
+
before packaged resources.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import re
|
|
17
|
+
import stat
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from importlib import resources
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ._version import PROTOCOL_MAJOR, PROTOCOL_MINOR, SCHEMA_VERSION, daemon_version_for_sdk
|
|
26
|
+
from .errors import RingoError
|
|
27
|
+
|
|
28
|
+
MANIFEST_NAME = "manifest.json"
|
|
29
|
+
MANIFEST_SCHEMA_VERSION = 1
|
|
30
|
+
DAEMON_NAME = "ringo-task-queue"
|
|
31
|
+
DAEMON_SCHEMA_VERSION = SCHEMA_VERSION
|
|
32
|
+
SUPPORTED_PLATFORMS = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"windows-amd64",
|
|
35
|
+
"linux-amd64",
|
|
36
|
+
"linux-arm64",
|
|
37
|
+
"macos-amd64",
|
|
38
|
+
"macos-arm64",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BinaryError(RingoError):
|
|
44
|
+
"""Base error for a missing, unsupported, or invalid packaged daemon."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UnsupportedPlatformError(BinaryError):
|
|
48
|
+
"""The current OS/architecture has no daemon in the wheel."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BinaryIntegrityError(BinaryError):
|
|
52
|
+
"""A packaged daemon does not match its manifest checksum."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class BinaryMetadata:
|
|
57
|
+
platform: str
|
|
58
|
+
filename: str
|
|
59
|
+
sha256: str
|
|
60
|
+
version: str
|
|
61
|
+
protocol_major: int
|
|
62
|
+
protocol_minor: int
|
|
63
|
+
size: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def platform_key(*, system: str | None = None, machine: str | None = None) -> str:
|
|
67
|
+
"""Return the stable manifest key for an OS and CPU architecture."""
|
|
68
|
+
system = (system or sys.platform).lower()
|
|
69
|
+
machine = (machine or platform.machine()).lower().replace("-", "_")
|
|
70
|
+
if system.startswith("win"):
|
|
71
|
+
os_name = "windows"
|
|
72
|
+
elif system.startswith("linux"):
|
|
73
|
+
os_name = "linux"
|
|
74
|
+
elif system in {"darwin", "macos"}:
|
|
75
|
+
os_name = "macos"
|
|
76
|
+
else:
|
|
77
|
+
raise UnsupportedPlatformError(f"unsupported operating system: {system}")
|
|
78
|
+
|
|
79
|
+
if machine in {"amd64", "x86_64", "x64"}:
|
|
80
|
+
arch = "amd64"
|
|
81
|
+
elif machine in {"arm64", "aarch64"} and os_name in {"macos", "linux"}:
|
|
82
|
+
arch = "arm64"
|
|
83
|
+
else:
|
|
84
|
+
raise UnsupportedPlatformError(
|
|
85
|
+
f"unsupported architecture {machine!r} on {os_name}"
|
|
86
|
+
)
|
|
87
|
+
return f"{os_name}-{arch}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _manifest_from_object(raw: Any) -> dict[str, BinaryMetadata]:
|
|
91
|
+
"""Parse the shared release-artifact manifest contract."""
|
|
92
|
+
if (
|
|
93
|
+
not isinstance(raw, dict)
|
|
94
|
+
or raw.get("manifestVersion") != 1
|
|
95
|
+
or raw.get("name") != DAEMON_NAME
|
|
96
|
+
or raw.get("schemaVersion") != DAEMON_SCHEMA_VERSION
|
|
97
|
+
):
|
|
98
|
+
raise BinaryError("invalid daemon manifest schema")
|
|
99
|
+
artifacts = raw.get("artifacts")
|
|
100
|
+
if not isinstance(artifacts, list) or not artifacts:
|
|
101
|
+
raise BinaryError("daemon manifest contains no artifacts")
|
|
102
|
+
version = raw.get("version")
|
|
103
|
+
commit = raw.get("buildCommit")
|
|
104
|
+
build_date = raw.get("buildDate")
|
|
105
|
+
protocol_major = raw.get("protocolMajor")
|
|
106
|
+
protocol_minor = raw.get("protocolMinor")
|
|
107
|
+
if (
|
|
108
|
+
not isinstance(version, str)
|
|
109
|
+
or not version
|
|
110
|
+
or not isinstance(commit, str)
|
|
111
|
+
or not commit
|
|
112
|
+
or commit.lower() in {"unknown", "local"}
|
|
113
|
+
or not isinstance(build_date, str)
|
|
114
|
+
or not build_date
|
|
115
|
+
or version != daemon_version_for_sdk()
|
|
116
|
+
or not isinstance(protocol_major, int)
|
|
117
|
+
or protocol_major != PROTOCOL_MAJOR
|
|
118
|
+
or not isinstance(protocol_minor, int)
|
|
119
|
+
or protocol_minor != PROTOCOL_MINOR
|
|
120
|
+
or not re.fullmatch(
|
|
121
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})",
|
|
122
|
+
build_date,
|
|
123
|
+
)
|
|
124
|
+
):
|
|
125
|
+
raise BinaryError("daemon manifest has invalid compatibility metadata")
|
|
126
|
+
try:
|
|
127
|
+
parsed_date = datetime.fromisoformat(build_date.replace("Z", "+00:00"))
|
|
128
|
+
if parsed_date.tzinfo is None or parsed_date.utcoffset() != timezone.utc.utcoffset(None):
|
|
129
|
+
raise ValueError
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
raise BinaryError("daemon manifest has invalid buildDate") from exc
|
|
132
|
+
result: dict[str, BinaryMetadata] = {}
|
|
133
|
+
for item in artifacts:
|
|
134
|
+
if not isinstance(item, dict):
|
|
135
|
+
raise BinaryError("invalid daemon manifest artifact")
|
|
136
|
+
os_name = item.get("os")
|
|
137
|
+
arch = item.get("arch")
|
|
138
|
+
key = f"{os_name}-{'amd64' if arch == 'amd64' else arch}"
|
|
139
|
+
if os_name == "darwin":
|
|
140
|
+
key = f"macos-{arch}"
|
|
141
|
+
elif os_name in {"windows", "linux"}:
|
|
142
|
+
key = f"{os_name}-{arch}"
|
|
143
|
+
if key not in SUPPORTED_PLATFORMS:
|
|
144
|
+
raise BinaryError(f"invalid daemon manifest platform entry: {key!r}")
|
|
145
|
+
if key in result:
|
|
146
|
+
raise BinaryError(f"duplicate daemon manifest platform entry: {key!r}")
|
|
147
|
+
filename = item.get("filename")
|
|
148
|
+
expected_filename = f"ringo-task-queue-{os_name}-{arch}{'.exe' if os_name == 'windows' else ''}"
|
|
149
|
+
digest = item.get("sha256")
|
|
150
|
+
if (
|
|
151
|
+
not isinstance(filename, str)
|
|
152
|
+
or filename != expected_filename
|
|
153
|
+
or "/" in filename
|
|
154
|
+
or "\\" in filename
|
|
155
|
+
or Path(filename).name != filename
|
|
156
|
+
or not isinstance(digest, str)
|
|
157
|
+
or len(digest) != 64
|
|
158
|
+
or any(char not in "0123456789abcdef" for char in digest)
|
|
159
|
+
or not isinstance(item.get("size"), int)
|
|
160
|
+
or item["size"] <= 0
|
|
161
|
+
or item["size"] > 512 * 1024**2
|
|
162
|
+
):
|
|
163
|
+
raise BinaryError(f"invalid daemon manifest entry for {key!r}")
|
|
164
|
+
result[key] = BinaryMetadata(
|
|
165
|
+
platform=key,
|
|
166
|
+
filename=filename,
|
|
167
|
+
sha256=digest,
|
|
168
|
+
version=version,
|
|
169
|
+
protocol_major=protocol_major,
|
|
170
|
+
protocol_minor=protocol_minor,
|
|
171
|
+
size=item["size"],
|
|
172
|
+
)
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def load_manifest(directory: Path) -> dict[str, BinaryMetadata]:
|
|
177
|
+
"""Load and validate ``bin/manifest.json`` from an unpacked package."""
|
|
178
|
+
path = directory / MANIFEST_NAME
|
|
179
|
+
try:
|
|
180
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
181
|
+
except FileNotFoundError:
|
|
182
|
+
raise BinaryError(f"daemon manifest not found: {path}") from None
|
|
183
|
+
except (OSError, ValueError) as exc:
|
|
184
|
+
raise BinaryError(f"cannot read daemon manifest {path}: {exc}") from exc
|
|
185
|
+
return _manifest_from_object(raw)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def ensure_executable(path: Path) -> None:
|
|
189
|
+
"""Ensure a Unix-installed daemon retains an executable mode bit."""
|
|
190
|
+
if os.name == "nt":
|
|
191
|
+
return
|
|
192
|
+
try:
|
|
193
|
+
mode = path.stat().st_mode
|
|
194
|
+
if not mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
|
|
195
|
+
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
196
|
+
except OSError as exc:
|
|
197
|
+
raise BinaryError(f"daemon is not executable: {path}: {exc}") from exc
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def verify_binary(path: Path, metadata: BinaryMetadata) -> None:
|
|
201
|
+
"""Check a staged daemon's SHA-256 and make it executable on Unix."""
|
|
202
|
+
try:
|
|
203
|
+
link_info = path.lstat()
|
|
204
|
+
except OSError as exc:
|
|
205
|
+
raise BinaryIntegrityError(f"daemon binary missing: {path}") from exc
|
|
206
|
+
if path.is_symlink() or not stat.S_ISREG(link_info.st_mode):
|
|
207
|
+
raise BinaryIntegrityError(f"daemon binary is not a regular file: {path}")
|
|
208
|
+
digest_state = hashlib.sha256()
|
|
209
|
+
try:
|
|
210
|
+
with path.open("rb") as source:
|
|
211
|
+
opened_info = os.fstat(source.fileno())
|
|
212
|
+
if not stat.S_ISREG(opened_info.st_mode) or not os.path.samestat(link_info, opened_info):
|
|
213
|
+
raise BinaryIntegrityError(f"daemon binary changed while opening: {path}")
|
|
214
|
+
if opened_info.st_size != metadata.size:
|
|
215
|
+
raise BinaryIntegrityError(
|
|
216
|
+
f"daemon size mismatch for {path}: expected {metadata.size}, got {opened_info.st_size}"
|
|
217
|
+
)
|
|
218
|
+
read_size = 0
|
|
219
|
+
while chunk := source.read(min(1024 * 1024, metadata.size - read_size + 1)):
|
|
220
|
+
read_size += len(chunk)
|
|
221
|
+
if read_size > metadata.size:
|
|
222
|
+
raise BinaryIntegrityError(f"daemon binary grew while reading: {path}")
|
|
223
|
+
digest_state.update(chunk)
|
|
224
|
+
if read_size != metadata.size:
|
|
225
|
+
raise BinaryIntegrityError(f"daemon binary changed while reading: {path}")
|
|
226
|
+
except BinaryIntegrityError:
|
|
227
|
+
raise
|
|
228
|
+
except OSError as exc:
|
|
229
|
+
raise BinaryIntegrityError(f"cannot read daemon binary: {path}: {exc}") from exc
|
|
230
|
+
digest = digest_state.hexdigest()
|
|
231
|
+
if digest != metadata.sha256:
|
|
232
|
+
raise BinaryIntegrityError(
|
|
233
|
+
f"daemon SHA-256 mismatch for {path}: expected {metadata.sha256}, got {digest}"
|
|
234
|
+
)
|
|
235
|
+
ensure_executable(path)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def packaged_binary() -> tuple[Path, BinaryMetadata]:
|
|
239
|
+
"""Resolve and verify the daemon matching the running platform."""
|
|
240
|
+
key = platform_key()
|
|
241
|
+
try:
|
|
242
|
+
package_bin = Path(str(resources.files("ringo_task_queue") / "bin"))
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
raise BinaryError(f"cannot access packaged daemon resources: {exc}") from exc
|
|
245
|
+
entries = load_manifest(package_bin)
|
|
246
|
+
metadata = entries.get(key)
|
|
247
|
+
if metadata is None:
|
|
248
|
+
raise UnsupportedPlatformError(
|
|
249
|
+
f"wheel has no daemon for platform {key}; available: {', '.join(sorted(entries))}"
|
|
250
|
+
)
|
|
251
|
+
path = package_bin / metadata.filename
|
|
252
|
+
verify_binary(path, metadata)
|
|
253
|
+
return path, metadata
|