commit c11a7b5b5bea12f5e7dfea2a9c5d99826e9675a0 Author: Cabbagec Date: Wed Jul 22 14:55:02 2026 +0000 feat: add archive client foundation diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5a8b5d3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +__pycache__ +*.pyc +state +backups diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df3674f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.py[cod] +.venv/ +dist/ +build/ +*.egg-info/ +.pytest_cache/ +state/ +backups/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ddd99f3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-slim AS builder +WORKDIR /build +COPY pyproject.toml ./ +COPY src ./src +RUN pip wheel --no-cache-dir --wheel-dir /wheels . + +FROM builder AS test +RUN pip install --no-cache-dir /wheels/*.whl +COPY tests ./tests +CMD ["python", "-m", "unittest", "discover", "-s", "tests", "-v"] + +FROM python:3.11-slim +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +RUN groupadd --gid 1001 archive-control \ + && useradd --uid 1001 --gid 1001 --no-create-home archive-control +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir /wheels/*.whl && rm -rf /wheels +USER 1001:1001 +ENTRYPOINT ["archive-client"] +CMD ["--config", "/etc/archive-control/client.toml"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f41c2e0 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Archive Clients + +One Python daemon runs as either an `archive` or `cache` node for Archive +Control. Both roles use the same code and `sodium/archive-clients` image. + +The current foundation provides strict TOML configuration, file-backed secrets, +API/local root mapping, fail-fast permission and sparse-file probes, a durable +SQLite command inbox, bounded one-writer WebSocket output, registration-first +authentication, heartbeat handling, duplicate-command acknowledgements, and +indefinite capped exponential reconnect with optional jitter. + +```bash +archive-client --config /etc/archive-control/client.toml --check-config +archive-client --config /etc/archive-control/client.toml --mode archive +``` + +`--mode` accepts only `archive` or `cache` and overrides the configured role. +Secrets must be regular files without group/world permissions. The daemon never +stores them in SQLite or sends the shared token after registration. + +This foundation currently executes heartbeat and state-snapshot commands. +Other mutation commands are durably rejected as unsupported until their +service and file-operation executors are added; they are never falsely +acknowledged as accepted. + +Run tests and build using containers: + +```bash +docker build --target test -t archive-clients-test . +docker run --rm archive-clients-test +docker build -t sodium/archive-clients:dev . +``` + +Generated bindings are pinned to archive-control-proto commit +`4ec852014dad74606d4078b3ae1aa208c814b033`. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..65f270f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,13 @@ +services: + client: + image: sodium/archive-clients:dev + build: . + user: "1001:1001" + command: ["--config", "/etc/archive-control/client.toml", "--check-config"] + volumes: + - ./examples/client.toml:/etc/archive-control/client.toml:ro + - ./state:/var/lib/archive-control + - ./backups:/var/backups/archive-control + - ./secrets:/run/secrets:ro + - ./data/qb:/data/qb + - ./data/sync:/data/sync diff --git a/examples/client.toml b/examples/client.toml new file mode 100644 index 0000000..c7a5093 --- /dev/null +++ b/examples/client.toml @@ -0,0 +1,29 @@ +client_id = "cache-1" +display_name = "Cache 1" +role = "cache" +control_endpoint = "ws://control:8765/archive_control" +shared_token_file = "/run/secrets/archive_control_token" +state_db = "/var/lib/archive-control/client.db" +backup_dir = "/var/backups/archive-control" + +[connection] +registration_timeout = "10s" +reconnect_initial = "1s" +reconnect_max = "60s" +reconnect_reset_after = "60s" +reconnect_jitter = true + +[qbittorrent] +endpoint = "http://qbittorrent:8080" +username = "admin" +password_file = "/run/secrets/qb_password" +api_root = "/downloads" +local_root = "/data/qb" + +[syncthing] +endpoint = "http://syncthing:8384" +api_key_file = "/run/secrets/syncthing_api_key" +api_root = "/sync" +local_root = "/data/sync" +advertised_addresses = ["dynamic"] + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3e84cfc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools==80.9.0", "wheel==0.46.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "archive-clients" +version = "0.1.0.dev0" +requires-python = ">=3.11" +dependencies = ["protobuf==7.35.1", "websockets==16.0"] + +[project.scripts] +archive-client = "archive_clients.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + diff --git a/src/archive_clients/__init__.py b/src/archive_clients/__init__.py new file mode 100644 index 0000000..0458cb7 --- /dev/null +++ b/src/archive_clients/__init__.py @@ -0,0 +1,4 @@ +"""Archive Control data-node daemon.""" + +PROTO_COMMIT = "4ec852014dad74606d4078b3ae1aa208c814b033" + diff --git a/src/archive_clients/cli.py b/src/archive_clients/cli.py new file mode 100644 index 0000000..868afaa --- /dev/null +++ b/src/archive_clients/cli.py @@ -0,0 +1,40 @@ +"""Archive client command-line entry point.""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import logging +from pathlib import Path +from typing import Sequence + +from archive_clients.config import ClientConfig +from archive_clients.daemon import ArchiveClientDaemon +from archive_clients.probes import probe_root + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="archive-client") + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--mode", choices=("archive", "cache")) + parser.add_argument("--check-config", action="store_true") + arguments = parser.parse_args(argv) + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + config = ClientConfig.load(arguments.config, arguments.mode) + probes = [ + probe_root(config.qbittorrent.local_root), + probe_root(config.syncthing.local_root), + ] + config.read_shared_token() + config.qbittorrent.read_password() + config.syncthing.read_api_key() + if arguments.check_config: + print(json.dumps({ + "client_id": config.client_id, + "role": config.role, + "filesystems": [probe.__dict__ | {"root": str(probe.root)} for probe in probes], + }, sort_keys=True)) + return 0 + asyncio.run(ArchiveClientDaemon(config, probes).run()) + return 0 diff --git a/src/archive_clients/config.py b/src/archive_clients/config.py new file mode 100644 index 0000000..540b0f2 --- /dev/null +++ b/src/archive_clients/config.py @@ -0,0 +1,259 @@ +"""Strict TOML configuration with file-backed secrets.""" + +from __future__ import annotations + +import os +import re +import stat +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import Any +from urllib.parse import urlsplit + +import tomllib + + +class ConfigError(ValueError): + pass + + +_CLIENT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") +_DURATION = re.compile(r"^([1-9][0-9]*)(ms|s|m|h)$") +_FACTORS = {"ms": 0.001, "s": 1, "m": 60, "h": 3600} +_ENV = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") + + +@dataclass(frozen=True) +class RootMapping: + api_root: PurePosixPath + local_root: Path + + def api_to_local(self, api_path: str) -> Path: + candidate = PurePosixPath(api_path) + try: + relative = candidate.relative_to(self.api_root) + except ValueError as exc: + raise ConfigError("API path is outside its configured root") from exc + if any(part in {"", ".", ".."} for part in relative.parts): + raise ConfigError("API path contains an unsafe component") + return self.local_root.joinpath(*relative.parts) + + +@dataclass(frozen=True) +class ServiceConfig: + endpoint: str + api_root: PurePosixPath + local_root: Path + username: str | None = None + password_file: Path | None = None + api_key_file: Path | None = None + advertised_addresses: tuple[str, ...] = () + + @property + def roots(self) -> RootMapping: + return RootMapping(self.api_root, self.local_root) + + def read_password(self) -> str | None: + return _secret(self.password_file, "service password") if self.password_file else None + + def read_api_key(self) -> str | None: + return _secret(self.api_key_file, "service API key") if self.api_key_file else None + + +@dataclass(frozen=True) +class ConnectionConfig: + registration_timeout: float = 10 + heartbeat_interval: float = 15 + offline_timeout: float = 45 + reconnect_initial: float = 1 + reconnect_max: float = 60 + reconnect_reset_after: float = 60 + reconnect_jitter: bool = True + + +@dataclass(frozen=True) +class JobsConfig: + stall_after: float = 30 * 60 + + +@dataclass(frozen=True) +class ClientConfig: + client_id: str + display_name: str + role: str + control_endpoint: str + shared_token_file: Path + state_db: Path + backup_dir: Path + qbittorrent: ServiceConfig + syncthing: ServiceConfig + connection: ConnectionConfig = ConnectionConfig() + jobs: JobsConfig = JobsConfig() + + @classmethod + def load(cls, path: Path, mode_override: str | None = None) -> "ClientConfig": + try: + with path.open("rb") as source: + raw = tomllib.load(source) + except (OSError, tomllib.TOMLDecodeError) as exc: + raise ConfigError("configuration cannot be read") from exc + _keys(raw, { + "client_id", "display_name", "role", "control_endpoint", + "shared_token_file", "state_db", "backup_dir", "connection", + "jobs", "qbittorrent", "syncthing", + }, "root") + role = mode_override or raw.get("role") + if role not in {"archive", "cache"}: + raise ConfigError("role/--mode must be archive or cache") + client_id = raw.get("client_id") + if not isinstance(client_id, str) or not _CLIENT_ID.fullmatch(client_id): + raise ConfigError("client_id is invalid") + display_name = raw.get("display_name") + if not isinstance(display_name, str) or not 1 <= len(display_name) <= 128: + raise ConfigError("display_name is invalid") + connection = _connection(raw.get("connection", {})) + jobs = _jobs(raw.get("jobs", {})) + return cls( + client_id, display_name, role, + _endpoint(raw, "control_endpoint", {"ws", "wss"}), + _absolute_path(raw, "shared_token_file"), + _absolute_path(raw, "state_db"), + _absolute_path(raw, "backup_dir"), + _service(raw.get("qbittorrent"), "qbittorrent"), + _service(raw.get("syncthing"), "syncthing"), connection, jobs, + ) + + def read_shared_token(self) -> str: + return _secret(self.shared_token_file, "shared token") + + +def _service(value: Any, name: str) -> ServiceConfig: + if not isinstance(value, dict): + raise ConfigError(f"{name} must be a table") + allowed = { + "endpoint", "api_root", "local_root", "username", "password_file", + "api_key_file", "advertised_addresses", + } + _keys(value, allowed, name) + api_root = PurePosixPath(_string(value, "api_root")) + if not api_root.is_absolute() or ".." in api_root.parts: + raise ConfigError(f"{name}.api_root must be absolute and normalized") + username = value.get("username") + if username is not None and (not isinstance(username, str) or not username): + raise ConfigError(f"{name}.username must be a non-empty string") + addresses = value.get("advertised_addresses", []) + if not isinstance(addresses, list) or any( + not isinstance(address, str) or not address for address in addresses + ): + raise ConfigError(f"{name}.advertised_addresses must be a string array") + return ServiceConfig( + _endpoint(value, "endpoint", {"http", "https"}), api_root, + _absolute_path(value, "local_root"), username, + _absolute_path(value, "password_file") + if "password_file" in value else None, + _absolute_path(value, "api_key_file") + if "api_key_file" in value else None, + tuple(addresses), + ) + + +def _connection(value: Any) -> ConnectionConfig: + if not isinstance(value, dict): + raise ConfigError("connection must be a table") + _keys(value, {"registration_timeout", "heartbeat_interval", "offline_timeout", "reconnect_initial", "reconnect_max", "reconnect_reset_after", "reconnect_jitter"}, "connection") + result = ConnectionConfig( + registration_timeout=_duration(value.get("registration_timeout", "10s")), + heartbeat_interval=_duration(value.get("heartbeat_interval", "15s")), + offline_timeout=_duration(value.get("offline_timeout", "45s")), + reconnect_initial=_duration(value.get("reconnect_initial", "1s")), + reconnect_max=_duration(value.get("reconnect_max", "60s")), + reconnect_reset_after=_duration(value.get("reconnect_reset_after", "60s")), + reconnect_jitter=value.get("reconnect_jitter", True), + ) + if result.reconnect_initial > result.reconnect_max: + raise ConfigError("reconnect_initial cannot exceed reconnect_max") + if result.offline_timeout <= result.heartbeat_interval: + raise ConfigError("offline_timeout must exceed heartbeat_interval") + if not isinstance(result.reconnect_jitter, bool): + raise ConfigError("reconnect_jitter must be boolean") + return result + + +def _jobs(value: Any) -> JobsConfig: + if not isinstance(value, dict): + raise ConfigError("jobs must be a table") + _keys(value, {"stall_after"}, "jobs") + return JobsConfig(stall_after=_duration(value.get("stall_after", "30m"))) + + +def _duration(value: Any) -> float: + if not isinstance(value, str) or not (match := _DURATION.fullmatch(value)): + raise ConfigError("duration must look like 15s or 30m") + return int(match.group(1)) * _FACTORS[match.group(2)] + + +def _string(value: dict[str, Any], key: str) -> str: + item = value.get(key) + if not isinstance(item, str) or not item: + raise ConfigError(f"{key} must be a non-empty string") + return _expand(item) + + +def _path(value: dict[str, Any], key: str) -> Path: + return Path(_string(value, key)) + + +def _absolute_path(value: dict[str, Any], key: str) -> Path: + path = _path(value, key) + if not path.is_absolute(): + raise ConfigError(f"{key} must be an absolute path") + return path + + +def _endpoint( + value: dict[str, Any], key: str, allowed_schemes: set[str] +) -> str: + endpoint = _string(value, key) + parsed = urlsplit(endpoint) + if ( + parsed.scheme not in allowed_schemes + or not parsed.hostname + or parsed.username is not None + or parsed.password is not None + or parsed.fragment + ): + schemes = "/".join(sorted(allowed_schemes)) + raise ConfigError(f"{key} must be a credential-free {schemes} endpoint") + return endpoint + + +def _expand(value: str) -> str: + def replace(match: re.Match[str]) -> str: + name = match.group(1) + if name not in os.environ: + raise ConfigError(f"environment variable {name} is not set") + return os.environ[name] + expanded = _ENV.sub(replace, value) + if "$" in expanded: + raise ConfigError("only ${NAME} environment interpolation is supported") + return expanded + + +def _secret(path: Path, name: str) -> str: + try: + metadata = path.stat() + exposed = metadata.st_mode & (stat.S_IRWXG | stat.S_IRWXO) + if not stat.S_ISREG(metadata.st_mode) or exposed: + raise ConfigError(f"{name} file permissions are unsafe") + value = path.read_text(encoding="utf-8").strip() + except OSError as exc: + raise ConfigError(f"{name} file cannot be read") from exc + if not value: + raise ConfigError(f"{name} is empty") + return value + + +def _keys(value: dict[str, Any], allowed: set[str], name: str) -> None: + unknown = sorted(set(value) - allowed) + if unknown: + raise ConfigError(f"unknown {name} keys: {', '.join(unknown)}") diff --git a/src/archive_clients/daemon.py b/src/archive_clients/daemon.py new file mode 100644 index 0000000..445e529 --- /dev/null +++ b/src/archive_clients/daemon.py @@ -0,0 +1,198 @@ +"""Reconnectable Archive Control client transport.""" + +from __future__ import annotations + +import asyncio +import logging +import random +import time +import uuid +from typing import Any + +from websockets.asyncio.client import connect + +from archive_clients.config import ClientConfig +from archive_clients.probes import FilesystemProbe +from archive_clients.protocol import ( + decode, + decode_message, + encode, + encode_message, + new_envelope, +) +from archive_clients.state import ClientStore, CommandConflict +from archive_control.v1 import client_pb2, common_pb2, control_pb2, job_pb2 + + +logger = logging.getLogger(__name__) + + +class ArchiveClientDaemon: + def __init__(self, config: ClientConfig, probes: list[FilesystemProbe]): + self.config = config + self.probes = probes + self.store = ClientStore(config.state_db) + + async def run(self) -> None: + await asyncio.to_thread(self.store.initialize) + delay = self.config.connection.reconnect_initial + while True: + started = time.monotonic() + try: + await self._connection() + except asyncio.CancelledError: + raise + except Exception as exc: + logger.warning("control connection ended: %s", type(exc).__name__) + if time.monotonic() - started >= self.config.connection.reconnect_reset_after: + delay = self.config.connection.reconnect_initial + wait = random.uniform(0, delay) if self.config.connection.reconnect_jitter else delay + await asyncio.sleep(wait) + delay = min(delay * 2, self.config.connection.reconnect_max) + + async def _connection(self) -> None: + async with connect( + self.config.control_endpoint, ping_interval=None, compression=None, + max_size=1024 * 1024, + ) as websocket: + registration = self._registration() + await websocket.send(encode(registration)) + registration.register_request.shared_token = "" + response = decode(await asyncio.wait_for( + websocket.recv(), self.config.connection.registration_timeout + )) + if response.WhichOneof("payload") != "register_response": + raise RuntimeError("control did not answer registration") + if response.correlation_id != registration.message_id: + raise RuntimeError("registration response correlation mismatch") + if response.register_response.status != client_pb2.REGISTRATION_STATUS_ACCEPTED: + raise RuntimeError("control rejected registration") + if response.register_response.negotiated_version.major != 1: + raise RuntimeError("control negotiated an unsupported protocol version") + outbound: asyncio.Queue[str] = asyncio.Queue(maxsize=100) + writer = asyncio.create_task(self._writer(websocket, outbound)) + try: + async for frame in websocket: + await self._handle(decode(frame), outbound) + finally: + writer.cancel() + await asyncio.gather(writer, return_exceptions=True) + + def _registration(self): + envelope = new_envelope() + request = envelope.register_request + request.protocol_version.CopyFrom(envelope.protocol_version) + request.client.client_id = self.config.client_id + request.client.display_name = self.config.display_name + request.client.role = ( + common_pb2.CLIENT_ROLE_ARCHIVE + if self.config.role == "archive" else common_pb2.CLIENT_ROLE_CACHE + ) + request.connection_instance_id = str(uuid.uuid4()) + request.shared_token = self.config.read_shared_token() + request.capabilities.max_envelope_bytes = 1024 * 1024 + request.capabilities.syncthing_advertised_addresses.extend( + self.config.syncthing.advertised_addresses + ) + for root_name, probe in zip( + ("qbittorrent", "syncthing"), self.probes, strict=True + ): + filesystem = request.capabilities.filesystems.add() + filesystem.root_name = root_name + filesystem.readable = probe.readable + filesystem.writable = probe.writable + filesystem.hard_link = probe.hard_link + filesystem.sparse_files = probe.sparse_files + if all(probe.sparse_files for probe in self.probes): + request.capabilities.features.append(client_pb2.CLIENT_FEATURE_SPARSE_FILES) + for cursor in self.store.list_active_job_cursors(): + active = request.active_jobs.add() + active.job_id = str(cursor["job_id"]) + active.job_revision = int(cursor["revision"]) + active.last_event_sequence = int(cursor["last_event_sequence"]) + active.state = job_pb2.JobState.Value(str(cursor["state"])) + active.committed = bool(cursor["committed"]) + return envelope + + async def _writer( + self, websocket: Any, outbound: asyncio.Queue[str] + ) -> None: + while True: + await websocket.send(await outbound.get()) + + async def _handle( + self, envelope: Any, outbound: asyncio.Queue[str] + ) -> None: + payload = envelope.WhichOneof("payload") + if payload == "heartbeat": + response = new_envelope() + response.correlation_id = envelope.message_id + response.heartbeat_ack.sequence = envelope.heartbeat.sequence + await outbound.put(encode(response)) + elif payload == "command": + await self._accept_command(envelope, outbound) + elif payload == "protocol_error": + logger.warning("control reported protocol error code=%s", envelope.protocol_error.error.code) + + async def _accept_command( + self, envelope: Any, outbound: asyncio.Queue[str] + ) -> None: + command = envelope.command + acknowledgement = self._initial_acknowledgement(command) + accepted = None + try: + accepted = await asyncio.to_thread( + self.store.accept_command, + command.command_id, + encode_message(command), + encode_message(acknowledgement), + ) + if accepted.duplicate: + acknowledgement = decode_message( + accepted.acknowledgement_json, control_pb2.CommandAck() + ) + if ( + acknowledgement.status + == control_pb2.COMMAND_ACK_STATUS_ACCEPTED + ): + acknowledgement.status = ( + control_pb2.COMMAND_ACK_STATUS_DUPLICATE + ) + except CommandConflict: + acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_REJECTED + acknowledgement.error.code = common_pb2.ERROR_CODE_CONFLICT + acknowledgement.error.message = "command ID content conflict" + response = new_envelope() + response.correlation_id = envelope.message_id + response.command_ack.CopyFrom(acknowledgement) + await outbound.put(encode(response)) + + if ( + accepted is not None + and not accepted.duplicate + and acknowledgement.status + == control_pb2.COMMAND_ACK_STATUS_ACCEPTED + and command.WhichOneof("payload") == "request_job_snapshot" + ): + snapshot = new_envelope() + snapshot.correlation_id = envelope.message_id + snapshot.client_state_snapshot.snapshot_id = str(uuid.uuid4()) + snapshot.client_state_snapshot.observed_at.CopyFrom(snapshot.sent_at) + await outbound.put(encode(snapshot)) + + @staticmethod + def _initial_acknowledgement(command: Any) -> control_pb2.CommandAck: + acknowledgement = control_pb2.CommandAck(command_id=command.command_id) + if not command.command_id or command.WhichOneof("payload") is None: + acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_REJECTED + acknowledgement.error.code = common_pb2.ERROR_CODE_INVALID_ARGUMENT + acknowledgement.error.message = "command ID and payload are required" + elif command.WhichOneof("payload") == "request_job_snapshot": + acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_ACCEPTED + else: + acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_REJECTED + acknowledgement.error.code = common_pb2.ERROR_CODE_UNSUPPORTED + acknowledgement.error.message = ( + "command is not supported by this client build" + ) + return acknowledgement diff --git a/src/archive_clients/probes.py b/src/archive_clients/probes.py new file mode 100644 index 0000000..2cbdadc --- /dev/null +++ b/src/archive_clients/probes.py @@ -0,0 +1,54 @@ +"""Fail-fast root and filesystem capability probes.""" + +from __future__ import annotations + +import os +import tempfile +from dataclasses import dataclass +from pathlib import Path + + +class ProbeError(RuntimeError): + pass + + +@dataclass(frozen=True) +class FilesystemProbe: + root: Path + readable: bool + writable: bool + hard_link: bool + sparse_files: bool + + +def probe_root(root: Path) -> FilesystemProbe: + if not root.is_absolute() or not root.is_dir(): + raise ProbeError(f"configured root is not an existing absolute directory: {root}") + if not os.access(root, os.R_OK | os.X_OK | os.W_OK): + raise ProbeError(f"configured root permissions are insufficient: {root}") + source: Path | None = None + linked: Path | None = None + try: + descriptor, raw_path = tempfile.mkstemp(prefix=".archive-control-probe-", dir=root) + source = Path(raw_path) + with os.fdopen(descriptor, "wb") as probe: + probe.seek(1024 * 1024) + probe.write(b"x") + probe.flush() + os.fsync(probe.fileno()) + allocated = source.stat().st_blocks * 512 + sparse = allocated < source.stat().st_size + linked = source.with_name(f"{source.name}.link") + try: + os.link(source, linked) + hard_link = linked.stat().st_ino == source.stat().st_ino + except OSError: + hard_link = False + return FilesystemProbe(root, True, True, hard_link, sparse) + except OSError as exc: + raise ProbeError(f"filesystem capability probe failed for {root}") from exc + finally: + if linked is not None: + linked.unlink(missing_ok=True) + if source is not None: + source.unlink(missing_ok=True) diff --git a/src/archive_clients/protocol.py b/src/archive_clients/protocol.py new file mode 100644 index 0000000..0b2619b --- /dev/null +++ b/src/archive_clients/protocol.py @@ -0,0 +1,93 @@ +"""Strict protobuf-JSON envelope helpers.""" + +from __future__ import annotations + +import json +import uuid +from datetime import datetime, timezone +from typing import Any + +from google.protobuf import json_format + +from archive_control.v1 import envelope_pb2 + + +class ProtocolError(ValueError): + pass + + +def new_envelope() -> envelope_pb2.Envelope: + envelope = envelope_pb2.Envelope() + envelope.protocol_version.major = 1 + envelope.message_id = str(uuid.uuid4()) + envelope.sent_at.FromDatetime(datetime.now(timezone.utc)) + return envelope + + +def encode(envelope: envelope_pb2.Envelope) -> str: + return json_format.MessageToJson( + envelope, preserving_proto_field_name=False, indent=None, + sort_keys=True, ensure_ascii=False, + ) + + +def encode_message(message: Any) -> str: + return json_format.MessageToJson( + message, preserving_proto_field_name=False, indent=None, + sort_keys=True, ensure_ascii=False, + ) + + +def decode_message(data: str, message: Any) -> Any: + try: + raw = json.loads(data, object_pairs_hook=_unique) + return json_format.ParseDict(raw, message) + except (ValueError, json.JSONDecodeError, json_format.ParseError) as exc: + raise ProtocolError("invalid protobuf JSON message") from exc + + +def decode(data: str | bytes, max_bytes: int = 1024 * 1024) -> envelope_pb2.Envelope: + if not isinstance(data, str) or len(data.encode()) > max_bytes: + raise ProtocolError("control envelope must be bounded text") + try: + raw = json.loads(data, object_pairs_hook=_unique) + envelope = json_format.ParseDict(raw, envelope_pb2.Envelope()) + except ( + TypeError, + ValueError, + json.JSONDecodeError, + json_format.ParseError, + ) as exc: + raise ProtocolError("invalid control envelope") from exc + if envelope.protocol_version.major != 1: + raise ProtocolError("unsupported protocol major version") + _canonical_uuid(envelope.message_id, "message_id") + if envelope.correlation_id: + _canonical_uuid(envelope.correlation_id, "correlation_id") + if not envelope.HasField("sent_at"): + raise ProtocolError("sent_at is required") + try: + envelope.sent_at.ToDatetime(tzinfo=timezone.utc) + except (ValueError, OverflowError) as exc: + raise ProtocolError("sent_at is invalid") from exc + if envelope.WhichOneof("payload") is None: + raise ProtocolError("unsupported or empty control envelope") + return envelope + + +def _unique(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate JSON key") + result[key] = value + return result + + +def _canonical_uuid(value: str, name: str) -> None: + try: + parsed = uuid.UUID(value) + except (ValueError, AttributeError) as exc: + raise ProtocolError(f"{name} must be a canonical UUID") from exc + if str(parsed) != value: + raise ProtocolError(f"{name} must be a lowercase canonical UUID") diff --git a/src/archive_clients/state.py b/src/archive_clients/state.py new file mode 100644 index 0000000..81011e4 --- /dev/null +++ b/src/archive_clients/state.py @@ -0,0 +1,122 @@ +"""Durable command inbox and client recovery cursors.""" + +from __future__ import annotations + +import hashlib +import json +import sqlite3 +from dataclasses import dataclass +from pathlib import Path + + +class CommandConflict(RuntimeError): + pass + + +@dataclass(frozen=True) +class CommandAcceptance: + duplicate: bool + acknowledgement_json: str + + +class ClientStore: + def __init__(self, database: Path): + self.database = database + + def initialize(self) -> None: + self.database.parent.mkdir(parents=True, exist_ok=True) + with self._connect() as connection: + version = connection.execute("PRAGMA user_version").fetchone()[0] + if version > 1: + raise RuntimeError( + f"client database schema {version} is newer than supported" + ) + connection.executescript(_SCHEMA) + connection.execute("PRAGMA user_version = 1") + if connection.execute("PRAGMA foreign_key_check").fetchall(): + raise RuntimeError("client database foreign-key check failed") + + def accept_command( + self, command_id: str, command_json: str, acknowledgement_json: str + ) -> CommandAcceptance: + payload = _canonical(json.loads(command_json)) + acknowledgement = _canonical(json.loads(acknowledgement_json)) + digest = hashlib.sha256(payload.encode()).hexdigest() + with self._connect() as connection: + connection.execute("BEGIN IMMEDIATE") + existing = connection.execute( + "SELECT payload_sha256, command_json, acknowledgement_json FROM commands WHERE command_id = ?", + (command_id,), + ).fetchone() + if existing: + if existing["payload_sha256"] != digest or existing["command_json"] != payload: + raise CommandConflict("command ID was reused with different content") + return CommandAcceptance(True, existing["acknowledgement_json"]) + connection.execute( + """ + INSERT INTO commands ( + command_id, payload_sha256, command_json, + acknowledgement_json, state + ) VALUES (?, ?, ?, ?, 'received') + """, + (command_id, digest, payload, acknowledgement), + ) + return CommandAcceptance(False, acknowledgement) + + def list_active_job_cursors(self) -> list[dict[str, object]]: + with self._connect() as connection: + rows = connection.execute( + """ + SELECT job_id, revision, last_event_sequence, state, committed + FROM jobs WHERE state NOT IN ('JOB_STATE_SUCCEEDED', + 'JOB_STATE_FAILED', 'JOB_STATE_CANCELLED') + ORDER BY job_id + """ + ).fetchall() + return [dict(row) for row in rows] + + def _connect(self) -> sqlite3.Connection: + connection = sqlite3.connect(self.database, isolation_level=None, timeout=5) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + connection.execute("PRAGMA journal_mode = WAL") + connection.execute("PRAGMA synchronous = FULL") + connection.execute("PRAGMA busy_timeout = 5000") + return connection + + +def _canonical(value: object) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS commands ( + command_id TEXT PRIMARY KEY, + payload_sha256 TEXT NOT NULL, + command_json TEXT NOT NULL, + acknowledgement_json TEXT NOT NULL, + state TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS jobs ( + job_id TEXT PRIMARY KEY, + definition_json TEXT NOT NULL, + state TEXT NOT NULL, + revision INTEGER NOT NULL, + last_event_sequence INTEGER NOT NULL, + committed INTEGER NOT NULL CHECK(committed IN (0, 1)) +); +CREATE TABLE IF NOT EXISTS events ( + event_id TEXT PRIMARY KEY, + job_id TEXT NOT NULL REFERENCES jobs(job_id), + sequence INTEGER NOT NULL, + event_json TEXT NOT NULL, + UNIQUE(job_id, sequence) +); +CREATE TABLE IF NOT EXISTS file_journal ( + operation_id TEXT PRIMARY KEY, + job_id TEXT NOT NULL, + intent_json TEXT NOT NULL, + result_json TEXT, + state TEXT NOT NULL +); +""" diff --git a/src/archive_control/__init__.py b/src/archive_control/__init__.py new file mode 100644 index 0000000..4576864 --- /dev/null +++ b/src/archive_control/__init__.py @@ -0,0 +1,2 @@ +"""Generated Archive Control protocol package.""" + diff --git a/src/archive_control/v1/__init__.py b/src/archive_control/v1/__init__.py new file mode 100644 index 0000000..6c01800 --- /dev/null +++ b/src/archive_control/v1/__init__.py @@ -0,0 +1,2 @@ +"""Generated archive_control.v1 bindings.""" + diff --git a/src/archive_control/v1/client_pb2.py b/src/archive_control/v1/client_pb2.py new file mode 100644 index 0000000..f421b6e --- /dev/null +++ b/src/archive_control/v1/client_pb2.py @@ -0,0 +1,57 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/client.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/client.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2 +from archive_control.v1 import job_pb2 as archive__control_dot_v1_dot_job__pb2 +from archive_control.v1 import route_pb2 as archive__control_dot_v1_dot_route__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61rchive_control/v1/client.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/common.proto\x1a\x1c\x61rchive_control/v1/job.proto\x1a\x1e\x61rchive_control/v1/route.proto\x1a\x1egoogle/protobuf/duration.proto\"\xdf\x01\n\x16\x46ilesystemCapabilities\x12\x1b\n\troot_name\x18\x01 \x01(\tR\x08rootName\x12\x1a\n\x08readable\x18\x02 \x01(\x08R\x08readable\x12\x1a\n\x08writable\x18\x03 \x01(\x08R\x08writable\x12\x1b\n\thard_link\x18\x04 \x01(\x08R\x08hardLink\x12\x18\n\x07reflink\x18\x05 \x01(\x08R\x07reflink\x12!\n\x0csparse_files\x18\x06 \x01(\x08R\x0bsparseFiles\x12\x16\n\x06\x64\x65tail\x18\x07 \x01(\tR\x06\x64\x65tail\"\xc6\x05\n\x12\x43lientCapabilities\x12=\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0e\x32!.archive_control.v1.ClientFeatureR\x08\x66\x65\x61tures\x12=\n\x08services\x18\x02 \x03(\x0b\x32!.archive_control.v1.ServiceHealthR\x08services\x12L\n\x0b\x66ilesystems\x18\x03 \x03(\x0b\x32*.archive_control.v1.FilesystemCapabilitiesR\x0b\x66ilesystems\x12\x36\n\x06routes\x18\x04 \x03(\x0b\x32\x1e.archive_control.v1.LocalRouteR\x06routes\x12.\n\x13syncthing_device_id\x18\x05 \x01(\tR\x11syncthingDeviceId\x12\x44\n\x1esyncthing_advertised_addresses\x18\x06 \x03(\tR\x1csyncthingAdvertisedAddresses\x12/\n\x13qbittorrent_version\x18\x07 \x01(\tR\x12qbittorrentVersion\x12=\n\x1bqbittorrent_web_api_version\x18\x08 \x01(\tR\x18qbittorrentWebApiVersion\x12-\n\x12libtorrent_version\x18\t \x01(\tR\x11libtorrentVersion\x12+\n\x11syncthing_version\x18\n \x01(\tR\x10syncthingVersion\x12<\n\x1asupported_partfile_formats\x18\x0b \x03(\tR\x18supportedPartfileFormats\x12,\n\x12max_envelope_bytes\x18\x0c \x01(\rR\x10maxEnvelopeBytes\"\xcd\x01\n\x0f\x41\x63tiveJobCursor\x12\x15\n\x06job_id\x18\x01 \x01(\tR\x05jobId\x12!\n\x0cjob_revision\x18\x02 \x01(\x04R\x0bjobRevision\x12.\n\x13last_event_sequence\x18\x03 \x01(\x04R\x11lastEventSequence\x12\x32\n\x05state\x18\x04 \x01(\x0e\x32\x1c.archive_control.v1.JobStateR\x05state\x12\x1c\n\tcommitted\x18\x05 \x01(\x08R\tcommitted\"\x88\x03\n\x0fRegisterRequest\x12N\n\x10protocol_version\x18\x01 \x01(\x0b\x32#.archive_control.v1.ProtocolVersionR\x0fprotocolVersion\x12:\n\x06\x63lient\x18\x02 \x01(\x0b\x32\".archive_control.v1.ClientIdentityR\x06\x63lient\x12\x34\n\x16\x63onnection_instance_id\x18\x03 \x01(\tR\x14\x63onnectionInstanceId\x12!\n\x0cshared_token\x18\x04 \x01(\tR\x0bsharedToken\x12J\n\x0c\x63\x61pabilities\x18\x05 \x01(\x0b\x32&.archive_control.v1.ClientCapabilitiesR\x0c\x63\x61pabilities\x12\x44\n\x0b\x61\x63tive_jobs\x18\x06 \x03(\x0b\x32#.archive_control.v1.ActiveJobCursorR\nactiveJobs\"\x8e\x03\n\x10RegisterResponse\x12>\n\x06status\x18\x01 \x01(\x0e\x32&.archive_control.v1.RegistrationStatusR\x06status\x12R\n\x12negotiated_version\x18\x02 \x01(\x0b\x32#.archive_control.v1.ProtocolVersionR\x11negotiatedVersion\x12#\n\rconnection_id\x18\x03 \x01(\tR\x0c\x63onnectionId\x12H\n\x12heartbeat_interval\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationR\x11heartbeatInterval\x12\x46\n\x11heartbeat_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.DurationR\x10heartbeatTimeout\x12/\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\"\'\n\tHeartbeat\x12\x1a\n\x08sequence\x18\x01 \x01(\x04R\x08sequence\"*\n\x0cHeartbeatAck\x12\x1a\n\x08sequence\x18\x01 \x01(\x04R\x08sequence*\x9f\x03\n\rClientFeature\x12\x1e\n\x1a\x43LIENT_FEATURE_UNSPECIFIED\x10\x00\x12#\n\x1f\x43LIENT_FEATURE_INVENTORY_CHUNKS\x10\x01\x12\x1f\n\x1b\x43LIENT_FEATURE_CONTENT_TREE\x10\x02\x12%\n!CLIENT_FEATURE_ROUTE_PROVISIONING\x10\x03\x12\x1c\n\x18\x43LIENT_FEATURE_HARD_LINK\x10\x04\x12\x1a\n\x16\x43LIENT_FEATURE_REFLINK\x10\x05\x12\x1f\n\x1b\x43LIENT_FEATURE_SPARSE_FILES\x10\x06\x12%\n!CLIENT_FEATURE_PARTFILE_MIGRATION\x10\x07\x12!\n\x1d\x43LIENT_FEATURE_PARTFILE_MERGE\x10\x08\x12!\n\x1d\x43LIENT_FEATURE_QB_STOPPED_ADD\x10\t\x12\x1b\n\x17\x43LIENT_FEATURE_EVICTION\x10\n\x12\x1c\n\x18\x43LIENT_FEATURE_DB_BACKUP\x10\x0b*}\n\x12RegistrationStatus\x12#\n\x1fREGISTRATION_STATUS_UNSPECIFIED\x10\x00\x12 \n\x1cREGISTRATION_STATUS_ACCEPTED\x10\x01\x12 \n\x1cREGISTRATION_STATUS_REJECTED\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.client_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_CLIENTFEATURE']._serialized_start=2211 + _globals['_CLIENTFEATURE']._serialized_end=2626 + _globals['_REGISTRATIONSTATUS']._serialized_start=2628 + _globals['_REGISTRATIONSTATUS']._serialized_end=2753 + _globals['_FILESYSTEMCAPABILITIES']._serialized_start=183 + _globals['_FILESYSTEMCAPABILITIES']._serialized_end=406 + _globals['_CLIENTCAPABILITIES']._serialized_start=409 + _globals['_CLIENTCAPABILITIES']._serialized_end=1119 + _globals['_ACTIVEJOBCURSOR']._serialized_start=1122 + _globals['_ACTIVEJOBCURSOR']._serialized_end=1327 + _globals['_REGISTERREQUEST']._serialized_start=1330 + _globals['_REGISTERREQUEST']._serialized_end=1722 + _globals['_REGISTERRESPONSE']._serialized_start=1725 + _globals['_REGISTERRESPONSE']._serialized_end=2123 + _globals['_HEARTBEAT']._serialized_start=2125 + _globals['_HEARTBEAT']._serialized_end=2164 + _globals['_HEARTBEATACK']._serialized_start=2166 + _globals['_HEARTBEATACK']._serialized_end=2208 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/client_pb2.pyi b/src/archive_control/v1/client_pb2.pyi new file mode 100644 index 0000000..2f1b138 --- /dev/null +++ b/src/archive_control/v1/client_pb2.pyi @@ -0,0 +1,155 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from archive_control.v1 import common_pb2 as _common_pb2 +from archive_control.v1 import job_pb2 as _job_pb2 +from archive_control.v1 import route_pb2 as _route_pb2 +from google.protobuf import duration_pb2 as _duration_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClientFeature(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CLIENT_FEATURE_UNSPECIFIED: _ClassVar[ClientFeature] + CLIENT_FEATURE_INVENTORY_CHUNKS: _ClassVar[ClientFeature] + CLIENT_FEATURE_CONTENT_TREE: _ClassVar[ClientFeature] + CLIENT_FEATURE_ROUTE_PROVISIONING: _ClassVar[ClientFeature] + CLIENT_FEATURE_HARD_LINK: _ClassVar[ClientFeature] + CLIENT_FEATURE_REFLINK: _ClassVar[ClientFeature] + CLIENT_FEATURE_SPARSE_FILES: _ClassVar[ClientFeature] + CLIENT_FEATURE_PARTFILE_MIGRATION: _ClassVar[ClientFeature] + CLIENT_FEATURE_PARTFILE_MERGE: _ClassVar[ClientFeature] + CLIENT_FEATURE_QB_STOPPED_ADD: _ClassVar[ClientFeature] + CLIENT_FEATURE_EVICTION: _ClassVar[ClientFeature] + CLIENT_FEATURE_DB_BACKUP: _ClassVar[ClientFeature] + +class RegistrationStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + REGISTRATION_STATUS_UNSPECIFIED: _ClassVar[RegistrationStatus] + REGISTRATION_STATUS_ACCEPTED: _ClassVar[RegistrationStatus] + REGISTRATION_STATUS_REJECTED: _ClassVar[RegistrationStatus] +CLIENT_FEATURE_UNSPECIFIED: ClientFeature +CLIENT_FEATURE_INVENTORY_CHUNKS: ClientFeature +CLIENT_FEATURE_CONTENT_TREE: ClientFeature +CLIENT_FEATURE_ROUTE_PROVISIONING: ClientFeature +CLIENT_FEATURE_HARD_LINK: ClientFeature +CLIENT_FEATURE_REFLINK: ClientFeature +CLIENT_FEATURE_SPARSE_FILES: ClientFeature +CLIENT_FEATURE_PARTFILE_MIGRATION: ClientFeature +CLIENT_FEATURE_PARTFILE_MERGE: ClientFeature +CLIENT_FEATURE_QB_STOPPED_ADD: ClientFeature +CLIENT_FEATURE_EVICTION: ClientFeature +CLIENT_FEATURE_DB_BACKUP: ClientFeature +REGISTRATION_STATUS_UNSPECIFIED: RegistrationStatus +REGISTRATION_STATUS_ACCEPTED: RegistrationStatus +REGISTRATION_STATUS_REJECTED: RegistrationStatus + +class FilesystemCapabilities(_message.Message): + __slots__ = ("root_name", "readable", "writable", "hard_link", "reflink", "sparse_files", "detail") + ROOT_NAME_FIELD_NUMBER: _ClassVar[int] + READABLE_FIELD_NUMBER: _ClassVar[int] + WRITABLE_FIELD_NUMBER: _ClassVar[int] + HARD_LINK_FIELD_NUMBER: _ClassVar[int] + REFLINK_FIELD_NUMBER: _ClassVar[int] + SPARSE_FILES_FIELD_NUMBER: _ClassVar[int] + DETAIL_FIELD_NUMBER: _ClassVar[int] + root_name: str + readable: bool + writable: bool + hard_link: bool + reflink: bool + sparse_files: bool + detail: str + def __init__(self, root_name: _Optional[str] = ..., readable: _Optional[bool] = ..., writable: _Optional[bool] = ..., hard_link: _Optional[bool] = ..., reflink: _Optional[bool] = ..., sparse_files: _Optional[bool] = ..., detail: _Optional[str] = ...) -> None: ... + +class ClientCapabilities(_message.Message): + __slots__ = ("features", "services", "filesystems", "routes", "syncthing_device_id", "syncthing_advertised_addresses", "qbittorrent_version", "qbittorrent_web_api_version", "libtorrent_version", "syncthing_version", "supported_partfile_formats", "max_envelope_bytes") + FEATURES_FIELD_NUMBER: _ClassVar[int] + SERVICES_FIELD_NUMBER: _ClassVar[int] + FILESYSTEMS_FIELD_NUMBER: _ClassVar[int] + ROUTES_FIELD_NUMBER: _ClassVar[int] + SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int] + SYNCTHING_ADVERTISED_ADDRESSES_FIELD_NUMBER: _ClassVar[int] + QBITTORRENT_VERSION_FIELD_NUMBER: _ClassVar[int] + QBITTORRENT_WEB_API_VERSION_FIELD_NUMBER: _ClassVar[int] + LIBTORRENT_VERSION_FIELD_NUMBER: _ClassVar[int] + SYNCTHING_VERSION_FIELD_NUMBER: _ClassVar[int] + SUPPORTED_PARTFILE_FORMATS_FIELD_NUMBER: _ClassVar[int] + MAX_ENVELOPE_BYTES_FIELD_NUMBER: _ClassVar[int] + features: _containers.RepeatedScalarFieldContainer[ClientFeature] + services: _containers.RepeatedCompositeFieldContainer[_common_pb2.ServiceHealth] + filesystems: _containers.RepeatedCompositeFieldContainer[FilesystemCapabilities] + routes: _containers.RepeatedCompositeFieldContainer[_route_pb2.LocalRoute] + syncthing_device_id: str + syncthing_advertised_addresses: _containers.RepeatedScalarFieldContainer[str] + qbittorrent_version: str + qbittorrent_web_api_version: str + libtorrent_version: str + syncthing_version: str + supported_partfile_formats: _containers.RepeatedScalarFieldContainer[str] + max_envelope_bytes: int + def __init__(self, features: _Optional[_Iterable[_Union[ClientFeature, str]]] = ..., services: _Optional[_Iterable[_Union[_common_pb2.ServiceHealth, _Mapping]]] = ..., filesystems: _Optional[_Iterable[_Union[FilesystemCapabilities, _Mapping]]] = ..., routes: _Optional[_Iterable[_Union[_route_pb2.LocalRoute, _Mapping]]] = ..., syncthing_device_id: _Optional[str] = ..., syncthing_advertised_addresses: _Optional[_Iterable[str]] = ..., qbittorrent_version: _Optional[str] = ..., qbittorrent_web_api_version: _Optional[str] = ..., libtorrent_version: _Optional[str] = ..., syncthing_version: _Optional[str] = ..., supported_partfile_formats: _Optional[_Iterable[str]] = ..., max_envelope_bytes: _Optional[int] = ...) -> None: ... + +class ActiveJobCursor(_message.Message): + __slots__ = ("job_id", "job_revision", "last_event_sequence", "state", "committed") + JOB_ID_FIELD_NUMBER: _ClassVar[int] + JOB_REVISION_FIELD_NUMBER: _ClassVar[int] + LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + COMMITTED_FIELD_NUMBER: _ClassVar[int] + job_id: str + job_revision: int + last_event_sequence: int + state: _job_pb2.JobState + committed: bool + def __init__(self, job_id: _Optional[str] = ..., job_revision: _Optional[int] = ..., last_event_sequence: _Optional[int] = ..., state: _Optional[_Union[_job_pb2.JobState, str]] = ..., committed: _Optional[bool] = ...) -> None: ... + +class RegisterRequest(_message.Message): + __slots__ = ("protocol_version", "client", "connection_instance_id", "shared_token", "capabilities", "active_jobs") + PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int] + CLIENT_FIELD_NUMBER: _ClassVar[int] + CONNECTION_INSTANCE_ID_FIELD_NUMBER: _ClassVar[int] + SHARED_TOKEN_FIELD_NUMBER: _ClassVar[int] + CAPABILITIES_FIELD_NUMBER: _ClassVar[int] + ACTIVE_JOBS_FIELD_NUMBER: _ClassVar[int] + protocol_version: _common_pb2.ProtocolVersion + client: _common_pb2.ClientIdentity + connection_instance_id: str + shared_token: str + capabilities: ClientCapabilities + active_jobs: _containers.RepeatedCompositeFieldContainer[ActiveJobCursor] + def __init__(self, protocol_version: _Optional[_Union[_common_pb2.ProtocolVersion, _Mapping]] = ..., client: _Optional[_Union[_common_pb2.ClientIdentity, _Mapping]] = ..., connection_instance_id: _Optional[str] = ..., shared_token: _Optional[str] = ..., capabilities: _Optional[_Union[ClientCapabilities, _Mapping]] = ..., active_jobs: _Optional[_Iterable[_Union[ActiveJobCursor, _Mapping]]] = ...) -> None: ... + +class RegisterResponse(_message.Message): + __slots__ = ("status", "negotiated_version", "connection_id", "heartbeat_interval", "heartbeat_timeout", "error") + STATUS_FIELD_NUMBER: _ClassVar[int] + NEGOTIATED_VERSION_FIELD_NUMBER: _ClassVar[int] + CONNECTION_ID_FIELD_NUMBER: _ClassVar[int] + HEARTBEAT_INTERVAL_FIELD_NUMBER: _ClassVar[int] + HEARTBEAT_TIMEOUT_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + status: RegistrationStatus + negotiated_version: _common_pb2.ProtocolVersion + connection_id: str + heartbeat_interval: _duration_pb2.Duration + heartbeat_timeout: _duration_pb2.Duration + error: _common_pb2.Error + def __init__(self, status: _Optional[_Union[RegistrationStatus, str]] = ..., negotiated_version: _Optional[_Union[_common_pb2.ProtocolVersion, _Mapping]] = ..., connection_id: _Optional[str] = ..., heartbeat_interval: _Optional[_Union[datetime.timedelta, _duration_pb2.Duration, _Mapping]] = ..., heartbeat_timeout: _Optional[_Union[datetime.timedelta, _duration_pb2.Duration, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ...) -> None: ... + +class Heartbeat(_message.Message): + __slots__ = ("sequence",) + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + sequence: int + def __init__(self, sequence: _Optional[int] = ...) -> None: ... + +class HeartbeatAck(_message.Message): + __slots__ = ("sequence",) + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + sequence: int + def __init__(self, sequence: _Optional[int] = ...) -> None: ... diff --git a/src/archive_control/v1/common_pb2.py b/src/archive_control/v1/common_pb2.py new file mode 100644 index 0000000..563dd81 --- /dev/null +++ b/src/archive_control/v1/common_pb2.py @@ -0,0 +1,56 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/common.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/common.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61rchive_control/v1/common.proto\x12\x12\x61rchive_control.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"=\n\x0fProtocolVersion\x12\x14\n\x05major\x18\x01 \x01(\rR\x05major\x12\x14\n\x05minor\x18\x02 \x01(\rR\x05minor\"\xf0\x01\n\x05\x45rror\x12\x31\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1d.archive_control.v1.ErrorCodeR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\x12\x1c\n\tretryable\x18\x03 \x01(\x08R\tretryable\x12@\n\x07\x64\x65tails\x18\x04 \x03(\x0b\x32&.archive_control.v1.Error.DetailsEntryR\x07\x64\x65tails\x1a:\n\x0c\x44\x65tailsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xee\x01\n\rServiceHealth\x12\x18\n\x07service\x18\x01 \x01(\tR\x07service\x12\x35\n\x05state\x18\x02 \x01(\x0e\x32\x1f.archive_control.v1.HealthStateR\x05state\x12\x18\n\x07version\x18\x03 \x01(\tR\x07version\x12\x1f\n\x0b\x61pi_version\x18\x04 \x01(\tR\napiVersion\x12\x16\n\x06\x64\x65tail\x18\x05 \x01(\tR\x06\x64\x65tail\x12\x39\n\nchecked_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcheckedAt\"\x84\x01\n\x0e\x43lientIdentity\x12\x1b\n\tclient_id\x18\x01 \x01(\tR\x08\x63lientId\x12!\n\x0c\x64isplay_name\x18\x02 \x01(\tR\x0b\x64isplayName\x12\x32\n\x04role\x18\x03 \x01(\x0e\x32\x1e.archive_control.v1.ClientRoleR\x04role\"I\n\x0bPageRequest\x12\x1b\n\tpage_size\x18\x01 \x01(\rR\x08pageSize\x12\x1d\n\npage_token\x18\x02 \x01(\tR\tpageToken*Y\n\nClientRole\x12\x1b\n\x17\x43LIENT_ROLE_UNSPECIFIED\x10\x00\x12\x17\n\x13\x43LIENT_ROLE_ARCHIVE\x10\x01\x12\x15\n\x11\x43LIENT_ROLE_CACHE\x10\x02*\x96\x01\n\x0bHealthState\x12\x1c\n\x18HEALTH_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14HEALTH_STATE_HEALTHY\x10\x01\x12\x19\n\x15HEALTH_STATE_DEGRADED\x10\x02\x12\x1a\n\x16HEALTH_STATE_UNHEALTHY\x10\x03\x12\x18\n\x14HEALTH_STATE_UNKNOWN\x10\x04*\xe2\x04\n\tErrorCode\x12\x1a\n\x16\x45RROR_CODE_UNSPECIFIED\x10\x00\x12\x1f\n\x1b\x45RROR_CODE_INVALID_ARGUMENT\x10\x01\x12\x1e\n\x1a\x45RROR_CODE_UNAUTHENTICATED\x10\x02\x12(\n$ERROR_CODE_PROTOCOL_VERSION_MISMATCH\x10\x03\x12!\n\x1d\x45RROR_CODE_CLIENT_ID_CONFLICT\x10\x04\x12\x18\n\x14\x45RROR_CODE_NOT_FOUND\x10\x05\x12\x17\n\x13\x45RROR_CODE_CONFLICT\x10\x06\x12\x1a\n\x16\x45RROR_CODE_STALE_STATE\x10\x07\x12\x1a\n\x16\x45RROR_CODE_UNSUPPORTED\x10\x08\x12\"\n\x1e\x45RROR_CODE_PRECONDITION_FAILED\x10\t\x12!\n\x1d\x45RROR_CODE_RESOURCE_EXHAUSTED\x10\n\x12\x1a\n\x16\x45RROR_CODE_UNAVAILABLE\x10\x0b\x12\x16\n\x12\x45RROR_CODE_TIMEOUT\x10\x0c\x12\x18\n\x14\x45RROR_CODE_CANCELLED\x10\r\x12%\n!ERROR_CODE_INTEGRITY_CHECK_FAILED\x10\x0e\x12\x1c\n\x18\x45RROR_CODE_PATH_CONFLICT\x10\x0f\x12 \n\x1c\x45RROR_CODE_PERMISSION_DENIED\x10\x10\x12+\n\'ERROR_CODE_MANUAL_INTERVENTION_REQUIRED\x10\x11\x12\x17\n\x13\x45RROR_CODE_INTERNAL\x10\x12\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.common_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_ERROR_DETAILSENTRY']._loaded_options = None + _globals['_ERROR_DETAILSENTRY']._serialized_options = b'8\001' + _globals['_CLIENTROLE']._serialized_start=845 + _globals['_CLIENTROLE']._serialized_end=934 + _globals['_HEALTHSTATE']._serialized_start=937 + _globals['_HEALTHSTATE']._serialized_end=1087 + _globals['_ERRORCODE']._serialized_start=1090 + _globals['_ERRORCODE']._serialized_end=1700 + _globals['_PROTOCOLVERSION']._serialized_start=88 + _globals['_PROTOCOLVERSION']._serialized_end=149 + _globals['_ERROR']._serialized_start=152 + _globals['_ERROR']._serialized_end=392 + _globals['_ERROR_DETAILSENTRY']._serialized_start=334 + _globals['_ERROR_DETAILSENTRY']._serialized_end=392 + _globals['_SERVICEHEALTH']._serialized_start=395 + _globals['_SERVICEHEALTH']._serialized_end=633 + _globals['_CLIENTIDENTITY']._serialized_start=636 + _globals['_CLIENTIDENTITY']._serialized_end=768 + _globals['_PAGEREQUEST']._serialized_start=770 + _globals['_PAGEREQUEST']._serialized_end=843 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/common_pb2.pyi b/src/archive_control/v1/common_pb2.pyi new file mode 100644 index 0000000..9f2d749 --- /dev/null +++ b/src/archive_control/v1/common_pb2.pyi @@ -0,0 +1,136 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClientRole(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CLIENT_ROLE_UNSPECIFIED: _ClassVar[ClientRole] + CLIENT_ROLE_ARCHIVE: _ClassVar[ClientRole] + CLIENT_ROLE_CACHE: _ClassVar[ClientRole] + +class HealthState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + HEALTH_STATE_UNSPECIFIED: _ClassVar[HealthState] + HEALTH_STATE_HEALTHY: _ClassVar[HealthState] + HEALTH_STATE_DEGRADED: _ClassVar[HealthState] + HEALTH_STATE_UNHEALTHY: _ClassVar[HealthState] + HEALTH_STATE_UNKNOWN: _ClassVar[HealthState] + +class ErrorCode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + ERROR_CODE_UNSPECIFIED: _ClassVar[ErrorCode] + ERROR_CODE_INVALID_ARGUMENT: _ClassVar[ErrorCode] + ERROR_CODE_UNAUTHENTICATED: _ClassVar[ErrorCode] + ERROR_CODE_PROTOCOL_VERSION_MISMATCH: _ClassVar[ErrorCode] + ERROR_CODE_CLIENT_ID_CONFLICT: _ClassVar[ErrorCode] + ERROR_CODE_NOT_FOUND: _ClassVar[ErrorCode] + ERROR_CODE_CONFLICT: _ClassVar[ErrorCode] + ERROR_CODE_STALE_STATE: _ClassVar[ErrorCode] + ERROR_CODE_UNSUPPORTED: _ClassVar[ErrorCode] + ERROR_CODE_PRECONDITION_FAILED: _ClassVar[ErrorCode] + ERROR_CODE_RESOURCE_EXHAUSTED: _ClassVar[ErrorCode] + ERROR_CODE_UNAVAILABLE: _ClassVar[ErrorCode] + ERROR_CODE_TIMEOUT: _ClassVar[ErrorCode] + ERROR_CODE_CANCELLED: _ClassVar[ErrorCode] + ERROR_CODE_INTEGRITY_CHECK_FAILED: _ClassVar[ErrorCode] + ERROR_CODE_PATH_CONFLICT: _ClassVar[ErrorCode] + ERROR_CODE_PERMISSION_DENIED: _ClassVar[ErrorCode] + ERROR_CODE_MANUAL_INTERVENTION_REQUIRED: _ClassVar[ErrorCode] + ERROR_CODE_INTERNAL: _ClassVar[ErrorCode] +CLIENT_ROLE_UNSPECIFIED: ClientRole +CLIENT_ROLE_ARCHIVE: ClientRole +CLIENT_ROLE_CACHE: ClientRole +HEALTH_STATE_UNSPECIFIED: HealthState +HEALTH_STATE_HEALTHY: HealthState +HEALTH_STATE_DEGRADED: HealthState +HEALTH_STATE_UNHEALTHY: HealthState +HEALTH_STATE_UNKNOWN: HealthState +ERROR_CODE_UNSPECIFIED: ErrorCode +ERROR_CODE_INVALID_ARGUMENT: ErrorCode +ERROR_CODE_UNAUTHENTICATED: ErrorCode +ERROR_CODE_PROTOCOL_VERSION_MISMATCH: ErrorCode +ERROR_CODE_CLIENT_ID_CONFLICT: ErrorCode +ERROR_CODE_NOT_FOUND: ErrorCode +ERROR_CODE_CONFLICT: ErrorCode +ERROR_CODE_STALE_STATE: ErrorCode +ERROR_CODE_UNSUPPORTED: ErrorCode +ERROR_CODE_PRECONDITION_FAILED: ErrorCode +ERROR_CODE_RESOURCE_EXHAUSTED: ErrorCode +ERROR_CODE_UNAVAILABLE: ErrorCode +ERROR_CODE_TIMEOUT: ErrorCode +ERROR_CODE_CANCELLED: ErrorCode +ERROR_CODE_INTEGRITY_CHECK_FAILED: ErrorCode +ERROR_CODE_PATH_CONFLICT: ErrorCode +ERROR_CODE_PERMISSION_DENIED: ErrorCode +ERROR_CODE_MANUAL_INTERVENTION_REQUIRED: ErrorCode +ERROR_CODE_INTERNAL: ErrorCode + +class ProtocolVersion(_message.Message): + __slots__ = ("major", "minor") + MAJOR_FIELD_NUMBER: _ClassVar[int] + MINOR_FIELD_NUMBER: _ClassVar[int] + major: int + minor: int + def __init__(self, major: _Optional[int] = ..., minor: _Optional[int] = ...) -> None: ... + +class Error(_message.Message): + __slots__ = ("code", "message", "retryable", "details") + class DetailsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + CODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + RETRYABLE_FIELD_NUMBER: _ClassVar[int] + DETAILS_FIELD_NUMBER: _ClassVar[int] + code: ErrorCode + message: str + retryable: bool + details: _containers.ScalarMap[str, str] + def __init__(self, code: _Optional[_Union[ErrorCode, str]] = ..., message: _Optional[str] = ..., retryable: _Optional[bool] = ..., details: _Optional[_Mapping[str, str]] = ...) -> None: ... + +class ServiceHealth(_message.Message): + __slots__ = ("service", "state", "version", "api_version", "detail", "checked_at") + SERVICE_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + VERSION_FIELD_NUMBER: _ClassVar[int] + API_VERSION_FIELD_NUMBER: _ClassVar[int] + DETAIL_FIELD_NUMBER: _ClassVar[int] + CHECKED_AT_FIELD_NUMBER: _ClassVar[int] + service: str + state: HealthState + version: str + api_version: str + detail: str + checked_at: _timestamp_pb2.Timestamp + def __init__(self, service: _Optional[str] = ..., state: _Optional[_Union[HealthState, str]] = ..., version: _Optional[str] = ..., api_version: _Optional[str] = ..., detail: _Optional[str] = ..., checked_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class ClientIdentity(_message.Message): + __slots__ = ("client_id", "display_name", "role") + CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + client_id: str + display_name: str + role: ClientRole + def __init__(self, client_id: _Optional[str] = ..., display_name: _Optional[str] = ..., role: _Optional[_Union[ClientRole, str]] = ...) -> None: ... + +class PageRequest(_message.Message): + __slots__ = ("page_size", "page_token") + PAGE_SIZE_FIELD_NUMBER: _ClassVar[int] + PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int] + page_size: int + page_token: str + def __init__(self, page_size: _Optional[int] = ..., page_token: _Optional[str] = ...) -> None: ... diff --git a/src/archive_control/v1/control_pb2.py b/src/archive_control/v1/control_pb2.py new file mode 100644 index 0000000..2eac574 --- /dev/null +++ b/src/archive_control/v1/control_pb2.py @@ -0,0 +1,69 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/control.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/control.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2 +from archive_control.v1 import inventory_pb2 as archive__control_dot_v1_dot_inventory__pb2 +from archive_control.v1 import job_pb2 as archive__control_dot_v1_dot_job__pb2 +from archive_control.v1 import resource_pb2 as archive__control_dot_v1_dot_resource__pb2 +from archive_control.v1 import route_pb2 as archive__control_dot_v1_dot_route__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n archive_control/v1/control.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/common.proto\x1a\"archive_control/v1/inventory.proto\x1a\x1c\x61rchive_control/v1/job.proto\x1a!archive_control/v1/resource.proto\x1a\x1e\x61rchive_control/v1/route.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xbc\x01\n\x10\x41ssignJobCommand\x12\x33\n\x03job\x18\x01 \x01(\x0b\x32!.archive_control.v1.JobDefinitionR\x03job\x12\x32\n\x15\x65xpected_job_revision\x18\x02 \x01(\x04R\x13\x65xpectedJobRevision\x12?\n\x1c\x65xpected_last_event_sequence\x18\x03 \x01(\x04R\x19\x65xpectedLastEventSequence\"\xef\x01\n\x12\x45xecuteStepCommand\x12\x15\n\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x32\n\x15\x65xpected_job_revision\x18\x02 \x01(\x04R\x13\x65xpectedJobRevision\x12\x33\n\x04step\x18\x03 \x01(\x0e\x32\x1f.archive_control.v1.JobStepKindR\x04step\x12\x18\n\x07\x61ttempt\x18\x04 \x01(\rR\x07\x61ttempt\x12?\n\x1c\x65xpected_last_event_sequence\x18\x05 \x01(\x04R\x19\x65xpectedLastEventSequence\"\xb6\x01\n\x10\x43\x61ncelJobCommand\x12\x15\n\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x32\n\x15\x65xpected_job_revision\x18\x02 \x01(\x04R\x13\x65xpectedJobRevision\x12\x16\n\x06reason\x18\x03 \x01(\tR\x06reason\x12?\n\x1c\x65xpected_last_event_sequence\x18\x04 \x01(\x04R\x19\x65xpectedLastEventSequence\"O\n\x12\x45nsureRouteCommand\x12\x39\n\x05route\x18\x01 \x01(\x0b\x32#.archive_control.v1.EnsureRouteSpecR\x05route\"4\n\x19RequestJobSnapshotCommand\x12\x17\n\x07job_ids\x18\x01 \x03(\tR\x06jobIds\"\xc8\x04\n\x07\x43ommand\x12\x1d\n\ncommand_id\x18\x01 \x01(\tR\tcommandId\x12\x39\n\ncreated_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x45\n\nassign_job\x18\n \x01(\x0b\x32$.archive_control.v1.AssignJobCommandH\x00R\tassignJob\x12K\n\x0c\x65xecute_step\x18\x0b \x01(\x0b\x32&.archive_control.v1.ExecuteStepCommandH\x00R\x0b\x65xecuteStep\x12\x45\n\ncancel_job\x18\x0c \x01(\x0b\x32$.archive_control.v1.CancelJobCommandH\x00R\tcancelJob\x12K\n\x0c\x65nsure_route\x18\r \x01(\x0b\x32&.archive_control.v1.EnsureRouteCommandH\x00R\x0b\x65nsureRoute\x12M\n\x0finventory_query\x18\x0e \x01(\x0b\x32\".archive_control.v1.InventoryQueryH\x00R\x0einventoryQuery\x12\x61\n\x14request_job_snapshot\x18\x0f \x01(\x0b\x32-.archive_control.v1.RequestJobSnapshotCommandH\x00R\x12requestJobSnapshotB\t\n\x07payload\"\x9a\x01\n\nCommandAck\x12\x1d\n\ncommand_id\x18\x01 \x01(\tR\tcommandId\x12<\n\x06status\x18\x02 \x01(\x0e\x32$.archive_control.v1.CommandAckStatusR\x06status\x12/\n\x05\x65rror\x18\x03 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\"\xd7\x04\n\x08JobEvent\x12\x19\n\x08\x65vent_id\x18\x01 \x01(\tR\x07\x65ventId\x12\x15\n\x06job_id\x18\x02 \x01(\tR\x05jobId\x12\x1a\n\x08sequence\x18\x03 \x01(\x04R\x08sequence\x12!\n\x0cjob_revision\x18\x04 \x01(\x04R\x0bjobRevision\x12\x34\n\x04type\x18\x05 \x01(\x0e\x32 .archive_control.v1.JobEventTypeR\x04type\x12\x32\n\x05state\x18\x06 \x01(\x0e\x32\x1c.archive_control.v1.JobStateR\x05state\x12\x1c\n\tcommitted\x18\x07 \x01(\x08R\tcommitted\x12;\n\x08progress\x18\x08 \x01(\x0b\x32\x1f.archive_control.v1.JobProgressR\x08progress\x12/\n\x05\x65rror\x18\t \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\x12Y\n\x11observed_resource\x18\n \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x10observedResource\x12L\n\x12observed_placement\x18\x0b \x01(\x0b\x32\x1d.archive_control.v1.PlacementR\x11observedPlacement\x12;\n\x0boccurred_at\x18\x0c \x01(\x0b\x32\x1a.google.protobuf.TimestampR\noccurredAt\"\xa1\x01\n\x0bJobSnapshot\x12/\n\x03job\x18\x01 \x01(\x0b\x32\x1d.archive_control.v1.JobRecordR\x03job\x12.\n\x13last_event_sequence\x18\x02 \x01(\x04R\x11lastEventSequence\x12\x31\n\x15in_flight_command_ids\x18\x03 \x03(\tR\x12inFlightCommandIds\"\xac\x02\n\x13\x43lientStateSnapshot\x12\x1f\n\x0bsnapshot_id\x18\x01 \x01(\tR\nsnapshotId\x12@\n\x0b\x61\x63tive_jobs\x18\x02 \x03(\x0b\x32\x1f.archive_control.v1.JobSnapshotR\nactiveJobs\x12\x36\n\x06routes\x18\x03 \x03(\x0b\x32\x1e.archive_control.v1.LocalRouteR\x06routes\x12=\n\x08services\x18\x04 \x03(\x0b\x32!.archive_control.v1.ServiceHealthR\x08services\x12;\n\x0bobserved_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nobservedAt\"\x97\x02\n\x0bRouteUpdate\x12\x1d\n\ncommand_id\x18\x01 \x01(\tR\tcommandId\x12\x34\n\x05route\x18\x02 \x01(\x0b\x32\x1e.archive_control.v1.LocalRouteR\x05route\x12I\n\x0cverification\x18\x03 \x01(\x0b\x32%.archive_control.v1.RouteVerificationR\x0cverification\x12/\n\x05\x65rror\x18\x04 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\x12\x1b\n\tupdate_id\x18\x05 \x01(\tR\x08updateId\x12\x1a\n\x08sequence\x18\x06 \x01(\x04R\x08sequence\"r\n\rProtocolError\x12/\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\x12\x30\n\x14offending_message_id\x18\x02 \x01(\tR\x12offendingMessageId*\x9a\x01\n\x10\x43ommandAckStatus\x12\"\n\x1e\x43OMMAND_ACK_STATUS_UNSPECIFIED\x10\x00\x12\x1f\n\x1b\x43OMMAND_ACK_STATUS_ACCEPTED\x10\x01\x12 \n\x1c\x43OMMAND_ACK_STATUS_DUPLICATE\x10\x02\x12\x1f\n\x1b\x43OMMAND_ACK_STATUS_REJECTED\x10\x03*\xe9\x03\n\x0cJobEventType\x12\x1e\n\x1aJOB_EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17JOB_EVENT_TYPE_ASSIGNED\x10\x01\x12\x1f\n\x1bJOB_EVENT_TYPE_STEP_STARTED\x10\x02\x12\x1b\n\x17JOB_EVENT_TYPE_PROGRESS\x10\x03\x12\x1a\n\x16JOB_EVENT_TYPE_WAITING\x10\x04\x12\x1a\n\x16JOB_EVENT_TYPE_STALLED\x10\x05\x12!\n\x1dJOB_EVENT_TYPE_STEP_SUCCEEDED\x10\x06\x12\x1c\n\x18JOB_EVENT_TYPE_COMMITTED\x10\x07\x12\x1d\n\x19JOB_EVENT_TYPE_CANCELLING\x10\x08\x12#\n\x1fJOB_EVENT_TYPE_ROLLBACK_STARTED\x10\t\x12%\n!JOB_EVENT_TYPE_ROLLBACK_SUCCEEDED\x10\n\x12#\n\x1fJOB_EVENT_TYPE_CLEANUP_REQUIRED\x10\x0b\x12\x1c\n\x18JOB_EVENT_TYPE_SUCCEEDED\x10\x0c\x12\x19\n\x15JOB_EVENT_TYPE_FAILED\x10\r\x12\x1c\n\x18JOB_EVENT_TYPE_CANCELLED\x10\x0e\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.control_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_COMMANDACKSTATUS']._serialized_start=3220 + _globals['_COMMANDACKSTATUS']._serialized_end=3374 + _globals['_JOBEVENTTYPE']._serialized_start=3377 + _globals['_JOBEVENTTYPE']._serialized_end=3866 + _globals['_ASSIGNJOBCOMMAND']._serialized_start=256 + _globals['_ASSIGNJOBCOMMAND']._serialized_end=444 + _globals['_EXECUTESTEPCOMMAND']._serialized_start=447 + _globals['_EXECUTESTEPCOMMAND']._serialized_end=686 + _globals['_CANCELJOBCOMMAND']._serialized_start=689 + _globals['_CANCELJOBCOMMAND']._serialized_end=871 + _globals['_ENSUREROUTECOMMAND']._serialized_start=873 + _globals['_ENSUREROUTECOMMAND']._serialized_end=952 + _globals['_REQUESTJOBSNAPSHOTCOMMAND']._serialized_start=954 + _globals['_REQUESTJOBSNAPSHOTCOMMAND']._serialized_end=1006 + _globals['_COMMAND']._serialized_start=1009 + _globals['_COMMAND']._serialized_end=1593 + _globals['_COMMANDACK']._serialized_start=1596 + _globals['_COMMANDACK']._serialized_end=1750 + _globals['_JOBEVENT']._serialized_start=1753 + _globals['_JOBEVENT']._serialized_end=2352 + _globals['_JOBSNAPSHOT']._serialized_start=2355 + _globals['_JOBSNAPSHOT']._serialized_end=2516 + _globals['_CLIENTSTATESNAPSHOT']._serialized_start=2519 + _globals['_CLIENTSTATESNAPSHOT']._serialized_end=2819 + _globals['_ROUTEUPDATE']._serialized_start=2822 + _globals['_ROUTEUPDATE']._serialized_end=3101 + _globals['_PROTOCOLERROR']._serialized_start=3103 + _globals['_PROTOCOLERROR']._serialized_end=3217 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/control_pb2.pyi b/src/archive_control/v1/control_pb2.pyi new file mode 100644 index 0000000..21e4da8 --- /dev/null +++ b/src/archive_control/v1/control_pb2.pyi @@ -0,0 +1,215 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from archive_control.v1 import common_pb2 as _common_pb2 +from archive_control.v1 import inventory_pb2 as _inventory_pb2 +from archive_control.v1 import job_pb2 as _job_pb2 +from archive_control.v1 import resource_pb2 as _resource_pb2 +from archive_control.v1 import route_pb2 as _route_pb2 +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class CommandAckStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + COMMAND_ACK_STATUS_UNSPECIFIED: _ClassVar[CommandAckStatus] + COMMAND_ACK_STATUS_ACCEPTED: _ClassVar[CommandAckStatus] + COMMAND_ACK_STATUS_DUPLICATE: _ClassVar[CommandAckStatus] + COMMAND_ACK_STATUS_REJECTED: _ClassVar[CommandAckStatus] + +class JobEventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + JOB_EVENT_TYPE_UNSPECIFIED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_ASSIGNED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_STEP_STARTED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_PROGRESS: _ClassVar[JobEventType] + JOB_EVENT_TYPE_WAITING: _ClassVar[JobEventType] + JOB_EVENT_TYPE_STALLED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_STEP_SUCCEEDED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_COMMITTED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_CANCELLING: _ClassVar[JobEventType] + JOB_EVENT_TYPE_ROLLBACK_STARTED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_ROLLBACK_SUCCEEDED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_CLEANUP_REQUIRED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_SUCCEEDED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_FAILED: _ClassVar[JobEventType] + JOB_EVENT_TYPE_CANCELLED: _ClassVar[JobEventType] +COMMAND_ACK_STATUS_UNSPECIFIED: CommandAckStatus +COMMAND_ACK_STATUS_ACCEPTED: CommandAckStatus +COMMAND_ACK_STATUS_DUPLICATE: CommandAckStatus +COMMAND_ACK_STATUS_REJECTED: CommandAckStatus +JOB_EVENT_TYPE_UNSPECIFIED: JobEventType +JOB_EVENT_TYPE_ASSIGNED: JobEventType +JOB_EVENT_TYPE_STEP_STARTED: JobEventType +JOB_EVENT_TYPE_PROGRESS: JobEventType +JOB_EVENT_TYPE_WAITING: JobEventType +JOB_EVENT_TYPE_STALLED: JobEventType +JOB_EVENT_TYPE_STEP_SUCCEEDED: JobEventType +JOB_EVENT_TYPE_COMMITTED: JobEventType +JOB_EVENT_TYPE_CANCELLING: JobEventType +JOB_EVENT_TYPE_ROLLBACK_STARTED: JobEventType +JOB_EVENT_TYPE_ROLLBACK_SUCCEEDED: JobEventType +JOB_EVENT_TYPE_CLEANUP_REQUIRED: JobEventType +JOB_EVENT_TYPE_SUCCEEDED: JobEventType +JOB_EVENT_TYPE_FAILED: JobEventType +JOB_EVENT_TYPE_CANCELLED: JobEventType + +class AssignJobCommand(_message.Message): + __slots__ = ("job", "expected_job_revision", "expected_last_event_sequence") + JOB_FIELD_NUMBER: _ClassVar[int] + EXPECTED_JOB_REVISION_FIELD_NUMBER: _ClassVar[int] + EXPECTED_LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int] + job: _job_pb2.JobDefinition + expected_job_revision: int + expected_last_event_sequence: int + def __init__(self, job: _Optional[_Union[_job_pb2.JobDefinition, _Mapping]] = ..., expected_job_revision: _Optional[int] = ..., expected_last_event_sequence: _Optional[int] = ...) -> None: ... + +class ExecuteStepCommand(_message.Message): + __slots__ = ("job_id", "expected_job_revision", "step", "attempt", "expected_last_event_sequence") + JOB_ID_FIELD_NUMBER: _ClassVar[int] + EXPECTED_JOB_REVISION_FIELD_NUMBER: _ClassVar[int] + STEP_FIELD_NUMBER: _ClassVar[int] + ATTEMPT_FIELD_NUMBER: _ClassVar[int] + EXPECTED_LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int] + job_id: str + expected_job_revision: int + step: _job_pb2.JobStepKind + attempt: int + expected_last_event_sequence: int + def __init__(self, job_id: _Optional[str] = ..., expected_job_revision: _Optional[int] = ..., step: _Optional[_Union[_job_pb2.JobStepKind, str]] = ..., attempt: _Optional[int] = ..., expected_last_event_sequence: _Optional[int] = ...) -> None: ... + +class CancelJobCommand(_message.Message): + __slots__ = ("job_id", "expected_job_revision", "reason", "expected_last_event_sequence") + JOB_ID_FIELD_NUMBER: _ClassVar[int] + EXPECTED_JOB_REVISION_FIELD_NUMBER: _ClassVar[int] + REASON_FIELD_NUMBER: _ClassVar[int] + EXPECTED_LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int] + job_id: str + expected_job_revision: int + reason: str + expected_last_event_sequence: int + def __init__(self, job_id: _Optional[str] = ..., expected_job_revision: _Optional[int] = ..., reason: _Optional[str] = ..., expected_last_event_sequence: _Optional[int] = ...) -> None: ... + +class EnsureRouteCommand(_message.Message): + __slots__ = ("route",) + ROUTE_FIELD_NUMBER: _ClassVar[int] + route: _route_pb2.EnsureRouteSpec + def __init__(self, route: _Optional[_Union[_route_pb2.EnsureRouteSpec, _Mapping]] = ...) -> None: ... + +class RequestJobSnapshotCommand(_message.Message): + __slots__ = ("job_ids",) + JOB_IDS_FIELD_NUMBER: _ClassVar[int] + job_ids: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, job_ids: _Optional[_Iterable[str]] = ...) -> None: ... + +class Command(_message.Message): + __slots__ = ("command_id", "created_at", "assign_job", "execute_step", "cancel_job", "ensure_route", "inventory_query", "request_job_snapshot") + COMMAND_ID_FIELD_NUMBER: _ClassVar[int] + CREATED_AT_FIELD_NUMBER: _ClassVar[int] + ASSIGN_JOB_FIELD_NUMBER: _ClassVar[int] + EXECUTE_STEP_FIELD_NUMBER: _ClassVar[int] + CANCEL_JOB_FIELD_NUMBER: _ClassVar[int] + ENSURE_ROUTE_FIELD_NUMBER: _ClassVar[int] + INVENTORY_QUERY_FIELD_NUMBER: _ClassVar[int] + REQUEST_JOB_SNAPSHOT_FIELD_NUMBER: _ClassVar[int] + command_id: str + created_at: _timestamp_pb2.Timestamp + assign_job: AssignJobCommand + execute_step: ExecuteStepCommand + cancel_job: CancelJobCommand + ensure_route: EnsureRouteCommand + inventory_query: _inventory_pb2.InventoryQuery + request_job_snapshot: RequestJobSnapshotCommand + def __init__(self, command_id: _Optional[str] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., assign_job: _Optional[_Union[AssignJobCommand, _Mapping]] = ..., execute_step: _Optional[_Union[ExecuteStepCommand, _Mapping]] = ..., cancel_job: _Optional[_Union[CancelJobCommand, _Mapping]] = ..., ensure_route: _Optional[_Union[EnsureRouteCommand, _Mapping]] = ..., inventory_query: _Optional[_Union[_inventory_pb2.InventoryQuery, _Mapping]] = ..., request_job_snapshot: _Optional[_Union[RequestJobSnapshotCommand, _Mapping]] = ...) -> None: ... + +class CommandAck(_message.Message): + __slots__ = ("command_id", "status", "error") + COMMAND_ID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + command_id: str + status: CommandAckStatus + error: _common_pb2.Error + def __init__(self, command_id: _Optional[str] = ..., status: _Optional[_Union[CommandAckStatus, str]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ...) -> None: ... + +class JobEvent(_message.Message): + __slots__ = ("event_id", "job_id", "sequence", "job_revision", "type", "state", "committed", "progress", "error", "observed_resource", "observed_placement", "occurred_at") + EVENT_ID_FIELD_NUMBER: _ClassVar[int] + JOB_ID_FIELD_NUMBER: _ClassVar[int] + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + JOB_REVISION_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + COMMITTED_FIELD_NUMBER: _ClassVar[int] + PROGRESS_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + OBSERVED_RESOURCE_FIELD_NUMBER: _ClassVar[int] + OBSERVED_PLACEMENT_FIELD_NUMBER: _ClassVar[int] + OCCURRED_AT_FIELD_NUMBER: _ClassVar[int] + event_id: str + job_id: str + sequence: int + job_revision: int + type: JobEventType + state: _job_pb2.JobState + committed: bool + progress: _job_pb2.JobProgress + error: _common_pb2.Error + observed_resource: _resource_pb2.ResourceStateFingerprint + observed_placement: _resource_pb2.Placement + occurred_at: _timestamp_pb2.Timestamp + def __init__(self, event_id: _Optional[str] = ..., job_id: _Optional[str] = ..., sequence: _Optional[int] = ..., job_revision: _Optional[int] = ..., type: _Optional[_Union[JobEventType, str]] = ..., state: _Optional[_Union[_job_pb2.JobState, str]] = ..., committed: _Optional[bool] = ..., progress: _Optional[_Union[_job_pb2.JobProgress, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., observed_resource: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., observed_placement: _Optional[_Union[_resource_pb2.Placement, _Mapping]] = ..., occurred_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class JobSnapshot(_message.Message): + __slots__ = ("job", "last_event_sequence", "in_flight_command_ids") + JOB_FIELD_NUMBER: _ClassVar[int] + LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int] + IN_FLIGHT_COMMAND_IDS_FIELD_NUMBER: _ClassVar[int] + job: _job_pb2.JobRecord + last_event_sequence: int + in_flight_command_ids: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, job: _Optional[_Union[_job_pb2.JobRecord, _Mapping]] = ..., last_event_sequence: _Optional[int] = ..., in_flight_command_ids: _Optional[_Iterable[str]] = ...) -> None: ... + +class ClientStateSnapshot(_message.Message): + __slots__ = ("snapshot_id", "active_jobs", "routes", "services", "observed_at") + SNAPSHOT_ID_FIELD_NUMBER: _ClassVar[int] + ACTIVE_JOBS_FIELD_NUMBER: _ClassVar[int] + ROUTES_FIELD_NUMBER: _ClassVar[int] + SERVICES_FIELD_NUMBER: _ClassVar[int] + OBSERVED_AT_FIELD_NUMBER: _ClassVar[int] + snapshot_id: str + active_jobs: _containers.RepeatedCompositeFieldContainer[JobSnapshot] + routes: _containers.RepeatedCompositeFieldContainer[_route_pb2.LocalRoute] + services: _containers.RepeatedCompositeFieldContainer[_common_pb2.ServiceHealth] + observed_at: _timestamp_pb2.Timestamp + def __init__(self, snapshot_id: _Optional[str] = ..., active_jobs: _Optional[_Iterable[_Union[JobSnapshot, _Mapping]]] = ..., routes: _Optional[_Iterable[_Union[_route_pb2.LocalRoute, _Mapping]]] = ..., services: _Optional[_Iterable[_Union[_common_pb2.ServiceHealth, _Mapping]]] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class RouteUpdate(_message.Message): + __slots__ = ("command_id", "route", "verification", "error", "update_id", "sequence") + COMMAND_ID_FIELD_NUMBER: _ClassVar[int] + ROUTE_FIELD_NUMBER: _ClassVar[int] + VERIFICATION_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + UPDATE_ID_FIELD_NUMBER: _ClassVar[int] + SEQUENCE_FIELD_NUMBER: _ClassVar[int] + command_id: str + route: _route_pb2.LocalRoute + verification: _route_pb2.RouteVerification + error: _common_pb2.Error + update_id: str + sequence: int + def __init__(self, command_id: _Optional[str] = ..., route: _Optional[_Union[_route_pb2.LocalRoute, _Mapping]] = ..., verification: _Optional[_Union[_route_pb2.RouteVerification, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., update_id: _Optional[str] = ..., sequence: _Optional[int] = ...) -> None: ... + +class ProtocolError(_message.Message): + __slots__ = ("error", "offending_message_id") + ERROR_FIELD_NUMBER: _ClassVar[int] + OFFENDING_MESSAGE_ID_FIELD_NUMBER: _ClassVar[int] + error: _common_pb2.Error + offending_message_id: str + def __init__(self, error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., offending_message_id: _Optional[str] = ...) -> None: ... diff --git a/src/archive_control/v1/envelope_pb2.py b/src/archive_control/v1/envelope_pb2.py new file mode 100644 index 0000000..54efcca --- /dev/null +++ b/src/archive_control/v1/envelope_pb2.py @@ -0,0 +1,42 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/envelope.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/envelope.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from archive_control.v1 import client_pb2 as archive__control_dot_v1_dot_client__pb2 +from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2 +from archive_control.v1 import control_pb2 as archive__control_dot_v1_dot_control__pb2 +from archive_control.v1 import inventory_pb2 as archive__control_dot_v1_dot_inventory__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!archive_control/v1/envelope.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/client.proto\x1a\x1f\x61rchive_control/v1/common.proto\x1a archive_control/v1/control.proto\x1a\"archive_control/v1/inventory.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xce\x08\n\x08\x45nvelope\x12N\n\x10protocol_version\x18\x01 \x01(\x0b\x32#.archive_control.v1.ProtocolVersionR\x0fprotocolVersion\x12\x1d\n\nmessage_id\x18\x02 \x01(\tR\tmessageId\x12%\n\x0e\x63orrelation_id\x18\x03 \x01(\tR\rcorrelationId\x12\x33\n\x07sent_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x06sentAt\x12P\n\x10register_request\x18\n \x01(\x0b\x32#.archive_control.v1.RegisterRequestH\x00R\x0fregisterRequest\x12S\n\x11register_response\x18\x0b \x01(\x0b\x32$.archive_control.v1.RegisterResponseH\x00R\x10registerResponse\x12=\n\theartbeat\x18\x0c \x01(\x0b\x32\x1d.archive_control.v1.HeartbeatH\x00R\theartbeat\x12G\n\rheartbeat_ack\x18\r \x01(\x0b\x32 .archive_control.v1.HeartbeatAckH\x00R\x0cheartbeatAck\x12\x37\n\x07\x63ommand\x18\x0e \x01(\x0b\x32\x1b.archive_control.v1.CommandH\x00R\x07\x63ommand\x12\x41\n\x0b\x63ommand_ack\x18\x0f \x01(\x0b\x32\x1e.archive_control.v1.CommandAckH\x00R\ncommandAck\x12M\n\x0finventory_chunk\x18\x10 \x01(\x0b\x32\".archive_control.v1.InventoryChunkH\x00R\x0einventoryChunk\x12;\n\tjob_event\x18\x11 \x01(\x0b\x32\x1c.archive_control.v1.JobEventH\x00R\x08jobEvent\x12\x44\n\x0cjob_snapshot\x18\x12 \x01(\x0b\x32\x1f.archive_control.v1.JobSnapshotH\x00R\x0bjobSnapshot\x12]\n\x15\x63lient_state_snapshot\x18\x13 \x01(\x0b\x32\'.archive_control.v1.ClientStateSnapshotH\x00R\x13\x63lientStateSnapshot\x12\x44\n\x0croute_update\x18\x14 \x01(\x0b\x32\x1f.archive_control.v1.RouteUpdateH\x00R\x0brouteUpdate\x12J\n\x0eprotocol_error\x18\x15 \x01(\x0b\x32!.archive_control.v1.ProtocolErrorH\x00R\rprotocolErrorB\t\n\x07payloadb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.envelope_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_ENVELOPE']._serialized_start=227 + _globals['_ENVELOPE']._serialized_end=1329 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/envelope_pb2.pyi b/src/archive_control/v1/envelope_pb2.pyi new file mode 100644 index 0000000..2c400c1 --- /dev/null +++ b/src/archive_control/v1/envelope_pb2.pyi @@ -0,0 +1,50 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from archive_control.v1 import client_pb2 as _client_pb2 +from archive_control.v1 import common_pb2 as _common_pb2 +from archive_control.v1 import control_pb2 as _control_pb2 +from archive_control.v1 import inventory_pb2 as _inventory_pb2 +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class Envelope(_message.Message): + __slots__ = ("protocol_version", "message_id", "correlation_id", "sent_at", "register_request", "register_response", "heartbeat", "heartbeat_ack", "command", "command_ack", "inventory_chunk", "job_event", "job_snapshot", "client_state_snapshot", "route_update", "protocol_error") + PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int] + MESSAGE_ID_FIELD_NUMBER: _ClassVar[int] + CORRELATION_ID_FIELD_NUMBER: _ClassVar[int] + SENT_AT_FIELD_NUMBER: _ClassVar[int] + REGISTER_REQUEST_FIELD_NUMBER: _ClassVar[int] + REGISTER_RESPONSE_FIELD_NUMBER: _ClassVar[int] + HEARTBEAT_FIELD_NUMBER: _ClassVar[int] + HEARTBEAT_ACK_FIELD_NUMBER: _ClassVar[int] + COMMAND_FIELD_NUMBER: _ClassVar[int] + COMMAND_ACK_FIELD_NUMBER: _ClassVar[int] + INVENTORY_CHUNK_FIELD_NUMBER: _ClassVar[int] + JOB_EVENT_FIELD_NUMBER: _ClassVar[int] + JOB_SNAPSHOT_FIELD_NUMBER: _ClassVar[int] + CLIENT_STATE_SNAPSHOT_FIELD_NUMBER: _ClassVar[int] + ROUTE_UPDATE_FIELD_NUMBER: _ClassVar[int] + PROTOCOL_ERROR_FIELD_NUMBER: _ClassVar[int] + protocol_version: _common_pb2.ProtocolVersion + message_id: str + correlation_id: str + sent_at: _timestamp_pb2.Timestamp + register_request: _client_pb2.RegisterRequest + register_response: _client_pb2.RegisterResponse + heartbeat: _client_pb2.Heartbeat + heartbeat_ack: _client_pb2.HeartbeatAck + command: _control_pb2.Command + command_ack: _control_pb2.CommandAck + inventory_chunk: _inventory_pb2.InventoryChunk + job_event: _control_pb2.JobEvent + job_snapshot: _control_pb2.JobSnapshot + client_state_snapshot: _control_pb2.ClientStateSnapshot + route_update: _control_pb2.RouteUpdate + protocol_error: _control_pb2.ProtocolError + def __init__(self, protocol_version: _Optional[_Union[_common_pb2.ProtocolVersion, _Mapping]] = ..., message_id: _Optional[str] = ..., correlation_id: _Optional[str] = ..., sent_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., register_request: _Optional[_Union[_client_pb2.RegisterRequest, _Mapping]] = ..., register_response: _Optional[_Union[_client_pb2.RegisterResponse, _Mapping]] = ..., heartbeat: _Optional[_Union[_client_pb2.Heartbeat, _Mapping]] = ..., heartbeat_ack: _Optional[_Union[_client_pb2.HeartbeatAck, _Mapping]] = ..., command: _Optional[_Union[_control_pb2.Command, _Mapping]] = ..., command_ack: _Optional[_Union[_control_pb2.CommandAck, _Mapping]] = ..., inventory_chunk: _Optional[_Union[_inventory_pb2.InventoryChunk, _Mapping]] = ..., job_event: _Optional[_Union[_control_pb2.JobEvent, _Mapping]] = ..., job_snapshot: _Optional[_Union[_control_pb2.JobSnapshot, _Mapping]] = ..., client_state_snapshot: _Optional[_Union[_control_pb2.ClientStateSnapshot, _Mapping]] = ..., route_update: _Optional[_Union[_control_pb2.RouteUpdate, _Mapping]] = ..., protocol_error: _Optional[_Union[_control_pb2.ProtocolError, _Mapping]] = ...) -> None: ... diff --git a/src/archive_control/v1/inventory_pb2.py b/src/archive_control/v1/inventory_pb2.py new file mode 100644 index 0000000..165af54 --- /dev/null +++ b/src/archive_control/v1/inventory_pb2.py @@ -0,0 +1,49 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/inventory.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/inventory.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2 +from archive_control.v1 import resource_pb2 as archive__control_dot_v1_dot_resource__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"archive_control/v1/inventory.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/common.proto\x1a!archive_control/v1/resource.proto\"\xe1\x02\n\x0eInventoryQuery\x12\x19\n\x08query_id\x18\x01 \x01(\tR\x07queryId\x12\x38\n\x05scope\x18\x02 \x01(\x0e\x32\".archive_control.v1.InventoryScopeR\x05scope\x12\x41\n\x0cresource_ids\x18\x03 \x03(\x0b\x32\x1e.archive_control.v1.ResourceIdR\x0bresourceIds\x12\x33\n\x04page\x18\x04 \x01(\x0b\x32\x1f.archive_control.v1.PageRequestR\x04page\x12\x1f\n\x0bpage_filter\x18\x05 \x01(\tR\npageFilter\x12\x34\n\x16selected_complete_only\x18\x06 \x01(\x08R\x14selectedCompleteOnly\x12+\n\x11\x65xpected_revision\x18\x07 \x01(\tR\x10\x65xpectedRevision\"Y\n\x14ResourceSummaryChunk\x12\x41\n\tresources\x18\x01 \x03(\x0b\x32#.archive_control.v1.ResourceSummaryR\tresources\"\xcf\x01\n\x10\x43ontentTreeChunk\x12?\n\x0bresource_id\x18\x01 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12>\n\x07\x65ntries\x18\x02 \x03(\x0b\x32$.archive_control.v1.ContentTreeEntryR\x07\x65ntries\x12:\n\x19resource_content_revision\x18\x03 \x01(\tR\x17resourceContentRevision\"O\n\x0ePlacementChunk\x12=\n\nplacements\x18\x01 \x03(\x0b\x32\x1d.archive_control.v1.PlacementR\nplacements\"\xf8\x03\n\x0eInventoryChunk\x12\x19\n\x08query_id\x18\x01 \x01(\tR\x07queryId\x12\x1f\n\x0bsnapshot_id\x18\x02 \x01(\tR\nsnapshotId\x12\x1a\n\x08revision\x18\x03 \x01(\tR\x08revision\x12\x1f\n\x0b\x63hunk_index\x18\x04 \x01(\rR\nchunkIndex\x12\x1d\n\nlast_chunk\x18\x05 \x01(\x08R\tlastChunk\x12&\n\x0fnext_page_token\x18\x06 \x01(\tR\rnextPageToken\x12/\n\x05\x65rror\x18\x07 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\x12Y\n\x12resource_summaries\x18\n \x01(\x0b\x32(.archive_control.v1.ResourceSummaryChunkH\x00R\x11resourceSummaries\x12I\n\x0c\x63ontent_tree\x18\x0b \x01(\x0b\x32$.archive_control.v1.ContentTreeChunkH\x00R\x0b\x63ontentTree\x12\x44\n\nplacements\x18\x0c \x01(\x0b\x32\".archive_control.v1.PlacementChunkH\x00R\nplacementsB\t\n\x07payload*\xc0\x01\n\x0eInventoryScope\x12\x1f\n\x1bINVENTORY_SCOPE_UNSPECIFIED\x10\x00\x12&\n\"INVENTORY_SCOPE_RESOURCE_SUMMARIES\x10\x01\x12#\n\x1fINVENTORY_SCOPE_RESOURCE_LOOKUP\x10\x02\x12 \n\x1cINVENTORY_SCOPE_CONTENT_TREE\x10\x03\x12\x1e\n\x1aINVENTORY_SCOPE_PLACEMENTS\x10\x04\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.inventory_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_INVENTORYSCOPE']._serialized_start=1372 + _globals['_INVENTORYSCOPE']._serialized_end=1564 + _globals['_INVENTORYQUERY']._serialized_start=127 + _globals['_INVENTORYQUERY']._serialized_end=480 + _globals['_RESOURCESUMMARYCHUNK']._serialized_start=482 + _globals['_RESOURCESUMMARYCHUNK']._serialized_end=571 + _globals['_CONTENTTREECHUNK']._serialized_start=574 + _globals['_CONTENTTREECHUNK']._serialized_end=781 + _globals['_PLACEMENTCHUNK']._serialized_start=783 + _globals['_PLACEMENTCHUNK']._serialized_end=862 + _globals['_INVENTORYCHUNK']._serialized_start=865 + _globals['_INVENTORYCHUNK']._serialized_end=1369 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/inventory_pb2.pyi b/src/archive_control/v1/inventory_pb2.pyi new file mode 100644 index 0000000..b25fba9 --- /dev/null +++ b/src/archive_control/v1/inventory_pb2.pyi @@ -0,0 +1,88 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +from archive_control.v1 import common_pb2 as _common_pb2 +from archive_control.v1 import resource_pb2 as _resource_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class InventoryScope(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + INVENTORY_SCOPE_UNSPECIFIED: _ClassVar[InventoryScope] + INVENTORY_SCOPE_RESOURCE_SUMMARIES: _ClassVar[InventoryScope] + INVENTORY_SCOPE_RESOURCE_LOOKUP: _ClassVar[InventoryScope] + INVENTORY_SCOPE_CONTENT_TREE: _ClassVar[InventoryScope] + INVENTORY_SCOPE_PLACEMENTS: _ClassVar[InventoryScope] +INVENTORY_SCOPE_UNSPECIFIED: InventoryScope +INVENTORY_SCOPE_RESOURCE_SUMMARIES: InventoryScope +INVENTORY_SCOPE_RESOURCE_LOOKUP: InventoryScope +INVENTORY_SCOPE_CONTENT_TREE: InventoryScope +INVENTORY_SCOPE_PLACEMENTS: InventoryScope + +class InventoryQuery(_message.Message): + __slots__ = ("query_id", "scope", "resource_ids", "page", "page_filter", "selected_complete_only", "expected_revision") + QUERY_ID_FIELD_NUMBER: _ClassVar[int] + SCOPE_FIELD_NUMBER: _ClassVar[int] + RESOURCE_IDS_FIELD_NUMBER: _ClassVar[int] + PAGE_FIELD_NUMBER: _ClassVar[int] + PAGE_FILTER_FIELD_NUMBER: _ClassVar[int] + SELECTED_COMPLETE_ONLY_FIELD_NUMBER: _ClassVar[int] + EXPECTED_REVISION_FIELD_NUMBER: _ClassVar[int] + query_id: str + scope: InventoryScope + resource_ids: _containers.RepeatedCompositeFieldContainer[_resource_pb2.ResourceId] + page: _common_pb2.PageRequest + page_filter: str + selected_complete_only: bool + expected_revision: str + def __init__(self, query_id: _Optional[str] = ..., scope: _Optional[_Union[InventoryScope, str]] = ..., resource_ids: _Optional[_Iterable[_Union[_resource_pb2.ResourceId, _Mapping]]] = ..., page: _Optional[_Union[_common_pb2.PageRequest, _Mapping]] = ..., page_filter: _Optional[str] = ..., selected_complete_only: _Optional[bool] = ..., expected_revision: _Optional[str] = ...) -> None: ... + +class ResourceSummaryChunk(_message.Message): + __slots__ = ("resources",) + RESOURCES_FIELD_NUMBER: _ClassVar[int] + resources: _containers.RepeatedCompositeFieldContainer[_resource_pb2.ResourceSummary] + def __init__(self, resources: _Optional[_Iterable[_Union[_resource_pb2.ResourceSummary, _Mapping]]] = ...) -> None: ... + +class ContentTreeChunk(_message.Message): + __slots__ = ("resource_id", "entries", "resource_content_revision") + RESOURCE_ID_FIELD_NUMBER: _ClassVar[int] + ENTRIES_FIELD_NUMBER: _ClassVar[int] + RESOURCE_CONTENT_REVISION_FIELD_NUMBER: _ClassVar[int] + resource_id: _resource_pb2.ResourceId + entries: _containers.RepeatedCompositeFieldContainer[_resource_pb2.ContentTreeEntry] + resource_content_revision: str + def __init__(self, resource_id: _Optional[_Union[_resource_pb2.ResourceId, _Mapping]] = ..., entries: _Optional[_Iterable[_Union[_resource_pb2.ContentTreeEntry, _Mapping]]] = ..., resource_content_revision: _Optional[str] = ...) -> None: ... + +class PlacementChunk(_message.Message): + __slots__ = ("placements",) + PLACEMENTS_FIELD_NUMBER: _ClassVar[int] + placements: _containers.RepeatedCompositeFieldContainer[_resource_pb2.Placement] + def __init__(self, placements: _Optional[_Iterable[_Union[_resource_pb2.Placement, _Mapping]]] = ...) -> None: ... + +class InventoryChunk(_message.Message): + __slots__ = ("query_id", "snapshot_id", "revision", "chunk_index", "last_chunk", "next_page_token", "error", "resource_summaries", "content_tree", "placements") + QUERY_ID_FIELD_NUMBER: _ClassVar[int] + SNAPSHOT_ID_FIELD_NUMBER: _ClassVar[int] + REVISION_FIELD_NUMBER: _ClassVar[int] + CHUNK_INDEX_FIELD_NUMBER: _ClassVar[int] + LAST_CHUNK_FIELD_NUMBER: _ClassVar[int] + NEXT_PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + RESOURCE_SUMMARIES_FIELD_NUMBER: _ClassVar[int] + CONTENT_TREE_FIELD_NUMBER: _ClassVar[int] + PLACEMENTS_FIELD_NUMBER: _ClassVar[int] + query_id: str + snapshot_id: str + revision: str + chunk_index: int + last_chunk: bool + next_page_token: str + error: _common_pb2.Error + resource_summaries: ResourceSummaryChunk + content_tree: ContentTreeChunk + placements: PlacementChunk + def __init__(self, query_id: _Optional[str] = ..., snapshot_id: _Optional[str] = ..., revision: _Optional[str] = ..., chunk_index: _Optional[int] = ..., last_chunk: _Optional[bool] = ..., next_page_token: _Optional[str] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., resource_summaries: _Optional[_Union[ResourceSummaryChunk, _Mapping]] = ..., content_tree: _Optional[_Union[ContentTreeChunk, _Mapping]] = ..., placements: _Optional[_Union[PlacementChunk, _Mapping]] = ...) -> None: ... diff --git a/src/archive_control/v1/job_pb2.py b/src/archive_control/v1/job_pb2.py new file mode 100644 index 0000000..d3b595f --- /dev/null +++ b/src/archive_control/v1/job_pb2.py @@ -0,0 +1,58 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/job.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/job.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2 +from archive_control.v1 import resource_pb2 as archive__control_dot_v1_dot_resource__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x61rchive_control/v1/job.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/common.proto\x1a!archive_control/v1/resource.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb8\x06\n\x0fTransferJobSpec\x12(\n\x10source_client_id\x18\x01 \x01(\tR\x0esourceClientId\x12(\n\x10target_client_id\x18\x02 \x01(\tR\x0etargetClientId\x12\x19\n\x08route_id\x18\x03 \x01(\tR\x07routeId\x12I\n\x0frequested_files\x18\x04 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x0erequestedFiles\x12[\n\x12source_fingerprint\x18\x05 \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x11sourceFingerprint\x12l\n\x1btarget_baseline_fingerprint\x18\x06 \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x19targetBaselineFingerprint\x12T\n$expected_target_placement_generation\x18\x07 \x01(\x04H\x00R!expectedTargetPlacementGeneration\x88\x01\x01\x12T\n\x15target_baseline_files\x18\x08 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x13targetBaselineFiles\x12R\n\x14transfer_delta_files\x18\t \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x12transferDeltaFiles\x12\x36\n\x17requested_logical_bytes\x18\n \x01(\x04R\x15requestedLogicalBytes\x12?\n\x1ctransfer_delta_logical_bytes\x18\x0b \x01(\x04R\x19transferDeltaLogicalBytesB\'\n%_expected_target_placement_generation\"\x87\x02\n\x0f\x41rchiveCoverage\x12*\n\x11\x61rchive_client_id\x18\x01 \x01(\tR\x0f\x61rchiveClientId\x12\x45\n\rcovered_files\x18\x02 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x0c\x63overedFiles\x12\x31\n\x14placement_generation\x18\x03 \x01(\x04R\x13placementGeneration\x12N\n\x0b\x66ingerprint\x18\x04 \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x0b\x66ingerprint\"\xea\x02\n\x0f\x45victionJobSpec\x12&\n\x0f\x63\x61\x63he_client_id\x18\x01 \x01(\tR\rcacheClientId\x12\x46\n\x0e\x66iles_to_evict\x18\x02 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x0c\x66ilesToEvict\x12<\n\x1a\x63\x61\x63he_placement_generation\x18\x03 \x01(\x04R\x18\x63\x61\x63hePlacementGeneration\x12Y\n\x11\x63\x61\x63he_fingerprint\x18\x04 \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x10\x63\x61\x63heFingerprint\x12N\n\x10\x61rchive_coverage\x18\x05 \x03(\x0b\x32#.archive_control.v1.ArchiveCoverageR\x0f\x61rchiveCoverage\"\xcd\x03\n\rJobDefinition\x12\x15\n\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\'\n\x0fidempotency_key\x18\x02 \x01(\tR\x0eidempotencyKey\x12>\n\toperation\x18\x03 \x01(\x0e\x32 .archive_control.v1.JobOperationR\toperation\x12?\n\x0bresource_id\x18\x04 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12\x32\n\x15resource_display_name\x18\x05 \x01(\tR\x13resourceDisplayName\x12\x39\n\ncreated_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x41\n\x08transfer\x18\n \x01(\x0b\x32#.archive_control.v1.TransferJobSpecH\x00R\x08transfer\x12\x41\n\x08\x65viction\x18\x0b \x01(\x0b\x32#.archive_control.v1.EvictionJobSpecH\x00R\x08\x65victionB\x06\n\x04spec\"\xa5\x04\n\x0bJobProgress\x12\x33\n\x04step\x18\x01 \x01(\x0e\x32\x1f.archive_control.v1.JobStepKindR\x04step\x12.\n\x13\x64isplay_step_number\x18\x02 \x01(\rR\x11\x64isplayStepNumber\x12,\n\x12\x64isplay_step_total\x18\x03 \x01(\rR\x10\x64isplayStepTotal\x12\x33\n\x05state\x18\x04 \x01(\x0e\x32\x1d.archive_control.v1.StepStateR\x05state\x12+\n\x11\x66raction_complete\x18\x05 \x01(\x01R\x10\x66ractionComplete\x12%\n\x0e\x62ytes_complete\x18\x06 \x01(\x04R\rbytesComplete\x12\x1f\n\x0b\x62ytes_total\x18\x07 \x01(\x04R\nbytesTotal\x12?\n\x1c\x61pproximate_bytes_per_second\x18\x08 \x01(\x01R\x19\x61pproximateBytesPerSecond\x12\x16\n\x06\x64\x65tail\x18\t \x01(\tR\x06\x64\x65tail\x12\x44\n\x10last_progress_at\x18\n \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x0elastProgressAt\x12:\n\x19overall_fraction_complete\x18\x0b \x01(\x01R\x17overallFractionComplete\"\xe1\x03\n\tJobRecord\x12\x41\n\ndefinition\x18\x01 \x01(\x0b\x32!.archive_control.v1.JobDefinitionR\ndefinition\x12\x32\n\x05state\x18\x02 \x01(\x0e\x32\x1c.archive_control.v1.JobStateR\x05state\x12\x1c\n\tcommitted\x18\x03 \x01(\x08R\tcommitted\x12\x1a\n\x08revision\x18\x04 \x01(\x04R\x08revision\x12;\n\x08progress\x18\x05 \x01(\x0b\x32\x1f.archive_control.v1.JobProgressR\x08progress\x12/\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\x12\x39\n\nupdated_at\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tupdatedAt\x12=\n\x0c\x63ommitted_at\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x0b\x63ommittedAt\x12;\n\x0b\x66inished_at\x18\t \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nfinishedAt*\x84\x01\n\x0cJobOperation\x12\x1d\n\x19JOB_OPERATION_UNSPECIFIED\x10\x00\x12\x19\n\x15JOB_OPERATION_ARCHIVE\x10\x01\x12\x1b\n\x17JOB_OPERATION_UNARCHIVE\x10\x02\x12\x1d\n\x19JOB_OPERATION_EVICT_CACHE\x10\x03*\xb7\x02\n\x08JobState\x12\x19\n\x15JOB_STATE_UNSPECIFIED\x10\x00\x12\x14\n\x10JOB_STATE_QUEUED\x10\x01\x12\x17\n\x13JOB_STATE_PREPARING\x10\x02\x12\x15\n\x11JOB_STATE_RUNNING\x10\x03\x12\x15\n\x11JOB_STATE_WAITING\x10\x04\x12\x15\n\x11JOB_STATE_STALLED\x10\x05\x12\x18\n\x14JOB_STATE_CANCELLING\x10\x06\x12\x1a\n\x16JOB_STATE_ROLLING_BACK\x10\x07\x12\x1e\n\x1aJOB_STATE_CLEANUP_REQUIRED\x10\x08\x12\x17\n\x13JOB_STATE_SUCCEEDED\x10\t\x12\x14\n\x10JOB_STATE_FAILED\x10\n\x12\x17\n\x13JOB_STATE_CANCELLED\x10\x0b*\xa2\x03\n\x0bJobStepKind\x12\x1d\n\x19JOB_STEP_KIND_UNSPECIFIED\x10\x00\x12\x1d\n\x19JOB_STEP_KIND_ROUTE_SETUP\x10\x01\x12\x1b\n\x17JOB_STEP_KIND_PREFLIGHT\x10\x02\x12\x1e\n\x1aJOB_STEP_KIND_SOURCE_STAGE\x10\x03\x12$\n JOB_STEP_KIND_SYNCTHING_TRANSFER\x10\x04\x12$\n JOB_STEP_KIND_TARGET_MATERIALIZE\x10\x05\x12\x1b\n\x17JOB_STEP_KIND_QB_VERIFY\x10\x06\x12!\n\x1dJOB_STEP_KIND_STAGING_CLEANUP\x10\x07\x12)\n%JOB_STEP_KIND_VERIFY_ARCHIVE_COVERAGE\x10\x08\x12!\n\x1dJOB_STEP_KIND_QB_REMOVE_ENTRY\x10\t\x12\"\n\x1eJOB_STEP_KIND_SAFE_FILE_UNLINK\x10\n\x12\x1a\n\x16JOB_STEP_KIND_ROLLBACK\x10\x0b*\xed\x01\n\tStepState\x12\x1a\n\x16STEP_STATE_UNSPECIFIED\x10\x00\x12\x1a\n\x16STEP_STATE_NOT_STARTED\x10\x01\x12\x15\n\x11STEP_STATE_QUEUED\x10\x02\x12\x16\n\x12STEP_STATE_RUNNING\x10\x03\x12\x16\n\x12STEP_STATE_WAITING\x10\x04\x12\x16\n\x12STEP_STATE_STALLED\x10\x05\x12\x18\n\x14STEP_STATE_SUCCEEDED\x10\x06\x12\x15\n\x11STEP_STATE_FAILED\x10\x07\x12\x18\n\x14STEP_STATE_CANCELLED\x10\x08\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.job_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_JOBOPERATION']._serialized_start=3112 + _globals['_JOBOPERATION']._serialized_end=3244 + _globals['_JOBSTATE']._serialized_start=3247 + _globals['_JOBSTATE']._serialized_end=3558 + _globals['_JOBSTEPKIND']._serialized_start=3561 + _globals['_JOBSTEPKIND']._serialized_end=3979 + _globals['_STEPSTATE']._serialized_start=3982 + _globals['_STEPSTATE']._serialized_end=4219 + _globals['_TRANSFERJOBSPEC']._serialized_start=154 + _globals['_TRANSFERJOBSPEC']._serialized_end=978 + _globals['_ARCHIVECOVERAGE']._serialized_start=981 + _globals['_ARCHIVECOVERAGE']._serialized_end=1244 + _globals['_EVICTIONJOBSPEC']._serialized_start=1247 + _globals['_EVICTIONJOBSPEC']._serialized_end=1609 + _globals['_JOBDEFINITION']._serialized_start=1612 + _globals['_JOBDEFINITION']._serialized_end=2073 + _globals['_JOBPROGRESS']._serialized_start=2076 + _globals['_JOBPROGRESS']._serialized_end=2625 + _globals['_JOBRECORD']._serialized_start=2628 + _globals['_JOBRECORD']._serialized_end=3109 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/job_pb2.pyi b/src/archive_control/v1/job_pb2.pyi new file mode 100644 index 0000000..d5b13a0 --- /dev/null +++ b/src/archive_control/v1/job_pb2.pyi @@ -0,0 +1,220 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from archive_control.v1 import common_pb2 as _common_pb2 +from archive_control.v1 import resource_pb2 as _resource_pb2 +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class JobOperation(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + JOB_OPERATION_UNSPECIFIED: _ClassVar[JobOperation] + JOB_OPERATION_ARCHIVE: _ClassVar[JobOperation] + JOB_OPERATION_UNARCHIVE: _ClassVar[JobOperation] + JOB_OPERATION_EVICT_CACHE: _ClassVar[JobOperation] + +class JobState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + JOB_STATE_UNSPECIFIED: _ClassVar[JobState] + JOB_STATE_QUEUED: _ClassVar[JobState] + JOB_STATE_PREPARING: _ClassVar[JobState] + JOB_STATE_RUNNING: _ClassVar[JobState] + JOB_STATE_WAITING: _ClassVar[JobState] + JOB_STATE_STALLED: _ClassVar[JobState] + JOB_STATE_CANCELLING: _ClassVar[JobState] + JOB_STATE_ROLLING_BACK: _ClassVar[JobState] + JOB_STATE_CLEANUP_REQUIRED: _ClassVar[JobState] + JOB_STATE_SUCCEEDED: _ClassVar[JobState] + JOB_STATE_FAILED: _ClassVar[JobState] + JOB_STATE_CANCELLED: _ClassVar[JobState] + +class JobStepKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + JOB_STEP_KIND_UNSPECIFIED: _ClassVar[JobStepKind] + JOB_STEP_KIND_ROUTE_SETUP: _ClassVar[JobStepKind] + JOB_STEP_KIND_PREFLIGHT: _ClassVar[JobStepKind] + JOB_STEP_KIND_SOURCE_STAGE: _ClassVar[JobStepKind] + JOB_STEP_KIND_SYNCTHING_TRANSFER: _ClassVar[JobStepKind] + JOB_STEP_KIND_TARGET_MATERIALIZE: _ClassVar[JobStepKind] + JOB_STEP_KIND_QB_VERIFY: _ClassVar[JobStepKind] + JOB_STEP_KIND_STAGING_CLEANUP: _ClassVar[JobStepKind] + JOB_STEP_KIND_VERIFY_ARCHIVE_COVERAGE: _ClassVar[JobStepKind] + JOB_STEP_KIND_QB_REMOVE_ENTRY: _ClassVar[JobStepKind] + JOB_STEP_KIND_SAFE_FILE_UNLINK: _ClassVar[JobStepKind] + JOB_STEP_KIND_ROLLBACK: _ClassVar[JobStepKind] + +class StepState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + STEP_STATE_UNSPECIFIED: _ClassVar[StepState] + STEP_STATE_NOT_STARTED: _ClassVar[StepState] + STEP_STATE_QUEUED: _ClassVar[StepState] + STEP_STATE_RUNNING: _ClassVar[StepState] + STEP_STATE_WAITING: _ClassVar[StepState] + STEP_STATE_STALLED: _ClassVar[StepState] + STEP_STATE_SUCCEEDED: _ClassVar[StepState] + STEP_STATE_FAILED: _ClassVar[StepState] + STEP_STATE_CANCELLED: _ClassVar[StepState] +JOB_OPERATION_UNSPECIFIED: JobOperation +JOB_OPERATION_ARCHIVE: JobOperation +JOB_OPERATION_UNARCHIVE: JobOperation +JOB_OPERATION_EVICT_CACHE: JobOperation +JOB_STATE_UNSPECIFIED: JobState +JOB_STATE_QUEUED: JobState +JOB_STATE_PREPARING: JobState +JOB_STATE_RUNNING: JobState +JOB_STATE_WAITING: JobState +JOB_STATE_STALLED: JobState +JOB_STATE_CANCELLING: JobState +JOB_STATE_ROLLING_BACK: JobState +JOB_STATE_CLEANUP_REQUIRED: JobState +JOB_STATE_SUCCEEDED: JobState +JOB_STATE_FAILED: JobState +JOB_STATE_CANCELLED: JobState +JOB_STEP_KIND_UNSPECIFIED: JobStepKind +JOB_STEP_KIND_ROUTE_SETUP: JobStepKind +JOB_STEP_KIND_PREFLIGHT: JobStepKind +JOB_STEP_KIND_SOURCE_STAGE: JobStepKind +JOB_STEP_KIND_SYNCTHING_TRANSFER: JobStepKind +JOB_STEP_KIND_TARGET_MATERIALIZE: JobStepKind +JOB_STEP_KIND_QB_VERIFY: JobStepKind +JOB_STEP_KIND_STAGING_CLEANUP: JobStepKind +JOB_STEP_KIND_VERIFY_ARCHIVE_COVERAGE: JobStepKind +JOB_STEP_KIND_QB_REMOVE_ENTRY: JobStepKind +JOB_STEP_KIND_SAFE_FILE_UNLINK: JobStepKind +JOB_STEP_KIND_ROLLBACK: JobStepKind +STEP_STATE_UNSPECIFIED: StepState +STEP_STATE_NOT_STARTED: StepState +STEP_STATE_QUEUED: StepState +STEP_STATE_RUNNING: StepState +STEP_STATE_WAITING: StepState +STEP_STATE_STALLED: StepState +STEP_STATE_SUCCEEDED: StepState +STEP_STATE_FAILED: StepState +STEP_STATE_CANCELLED: StepState + +class TransferJobSpec(_message.Message): + __slots__ = ("source_client_id", "target_client_id", "route_id", "requested_files", "source_fingerprint", "target_baseline_fingerprint", "expected_target_placement_generation", "target_baseline_files", "transfer_delta_files", "requested_logical_bytes", "transfer_delta_logical_bytes") + SOURCE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + TARGET_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + ROUTE_ID_FIELD_NUMBER: _ClassVar[int] + REQUESTED_FILES_FIELD_NUMBER: _ClassVar[int] + SOURCE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + TARGET_BASELINE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + EXPECTED_TARGET_PLACEMENT_GENERATION_FIELD_NUMBER: _ClassVar[int] + TARGET_BASELINE_FILES_FIELD_NUMBER: _ClassVar[int] + TRANSFER_DELTA_FILES_FIELD_NUMBER: _ClassVar[int] + REQUESTED_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + TRANSFER_DELTA_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + source_client_id: str + target_client_id: str + route_id: str + requested_files: _resource_pb2.SelectionSet + source_fingerprint: _resource_pb2.ResourceStateFingerprint + target_baseline_fingerprint: _resource_pb2.ResourceStateFingerprint + expected_target_placement_generation: int + target_baseline_files: _resource_pb2.SelectionSet + transfer_delta_files: _resource_pb2.SelectionSet + requested_logical_bytes: int + transfer_delta_logical_bytes: int + def __init__(self, source_client_id: _Optional[str] = ..., target_client_id: _Optional[str] = ..., route_id: _Optional[str] = ..., requested_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., source_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., target_baseline_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., expected_target_placement_generation: _Optional[int] = ..., target_baseline_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., transfer_delta_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., requested_logical_bytes: _Optional[int] = ..., transfer_delta_logical_bytes: _Optional[int] = ...) -> None: ... + +class ArchiveCoverage(_message.Message): + __slots__ = ("archive_client_id", "covered_files", "placement_generation", "fingerprint") + ARCHIVE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + COVERED_FILES_FIELD_NUMBER: _ClassVar[int] + PLACEMENT_GENERATION_FIELD_NUMBER: _ClassVar[int] + FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + archive_client_id: str + covered_files: _resource_pb2.SelectionSet + placement_generation: int + fingerprint: _resource_pb2.ResourceStateFingerprint + def __init__(self, archive_client_id: _Optional[str] = ..., covered_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., placement_generation: _Optional[int] = ..., fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ...) -> None: ... + +class EvictionJobSpec(_message.Message): + __slots__ = ("cache_client_id", "files_to_evict", "cache_placement_generation", "cache_fingerprint", "archive_coverage") + CACHE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + FILES_TO_EVICT_FIELD_NUMBER: _ClassVar[int] + CACHE_PLACEMENT_GENERATION_FIELD_NUMBER: _ClassVar[int] + CACHE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + ARCHIVE_COVERAGE_FIELD_NUMBER: _ClassVar[int] + cache_client_id: str + files_to_evict: _resource_pb2.SelectionSet + cache_placement_generation: int + cache_fingerprint: _resource_pb2.ResourceStateFingerprint + archive_coverage: _containers.RepeatedCompositeFieldContainer[ArchiveCoverage] + def __init__(self, cache_client_id: _Optional[str] = ..., files_to_evict: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., cache_placement_generation: _Optional[int] = ..., cache_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., archive_coverage: _Optional[_Iterable[_Union[ArchiveCoverage, _Mapping]]] = ...) -> None: ... + +class JobDefinition(_message.Message): + __slots__ = ("job_id", "idempotency_key", "operation", "resource_id", "resource_display_name", "created_at", "transfer", "eviction") + JOB_ID_FIELD_NUMBER: _ClassVar[int] + IDEMPOTENCY_KEY_FIELD_NUMBER: _ClassVar[int] + OPERATION_FIELD_NUMBER: _ClassVar[int] + RESOURCE_ID_FIELD_NUMBER: _ClassVar[int] + RESOURCE_DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int] + CREATED_AT_FIELD_NUMBER: _ClassVar[int] + TRANSFER_FIELD_NUMBER: _ClassVar[int] + EVICTION_FIELD_NUMBER: _ClassVar[int] + job_id: str + idempotency_key: str + operation: JobOperation + resource_id: _resource_pb2.ResourceId + resource_display_name: str + created_at: _timestamp_pb2.Timestamp + transfer: TransferJobSpec + eviction: EvictionJobSpec + def __init__(self, job_id: _Optional[str] = ..., idempotency_key: _Optional[str] = ..., operation: _Optional[_Union[JobOperation, str]] = ..., resource_id: _Optional[_Union[_resource_pb2.ResourceId, _Mapping]] = ..., resource_display_name: _Optional[str] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., transfer: _Optional[_Union[TransferJobSpec, _Mapping]] = ..., eviction: _Optional[_Union[EvictionJobSpec, _Mapping]] = ...) -> None: ... + +class JobProgress(_message.Message): + __slots__ = ("step", "display_step_number", "display_step_total", "state", "fraction_complete", "bytes_complete", "bytes_total", "approximate_bytes_per_second", "detail", "last_progress_at", "overall_fraction_complete") + STEP_FIELD_NUMBER: _ClassVar[int] + DISPLAY_STEP_NUMBER_FIELD_NUMBER: _ClassVar[int] + DISPLAY_STEP_TOTAL_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + FRACTION_COMPLETE_FIELD_NUMBER: _ClassVar[int] + BYTES_COMPLETE_FIELD_NUMBER: _ClassVar[int] + BYTES_TOTAL_FIELD_NUMBER: _ClassVar[int] + APPROXIMATE_BYTES_PER_SECOND_FIELD_NUMBER: _ClassVar[int] + DETAIL_FIELD_NUMBER: _ClassVar[int] + LAST_PROGRESS_AT_FIELD_NUMBER: _ClassVar[int] + OVERALL_FRACTION_COMPLETE_FIELD_NUMBER: _ClassVar[int] + step: JobStepKind + display_step_number: int + display_step_total: int + state: StepState + fraction_complete: float + bytes_complete: int + bytes_total: int + approximate_bytes_per_second: float + detail: str + last_progress_at: _timestamp_pb2.Timestamp + overall_fraction_complete: float + def __init__(self, step: _Optional[_Union[JobStepKind, str]] = ..., display_step_number: _Optional[int] = ..., display_step_total: _Optional[int] = ..., state: _Optional[_Union[StepState, str]] = ..., fraction_complete: _Optional[float] = ..., bytes_complete: _Optional[int] = ..., bytes_total: _Optional[int] = ..., approximate_bytes_per_second: _Optional[float] = ..., detail: _Optional[str] = ..., last_progress_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., overall_fraction_complete: _Optional[float] = ...) -> None: ... + +class JobRecord(_message.Message): + __slots__ = ("definition", "state", "committed", "revision", "progress", "error", "updated_at", "committed_at", "finished_at") + DEFINITION_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + COMMITTED_FIELD_NUMBER: _ClassVar[int] + REVISION_FIELD_NUMBER: _ClassVar[int] + PROGRESS_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + UPDATED_AT_FIELD_NUMBER: _ClassVar[int] + COMMITTED_AT_FIELD_NUMBER: _ClassVar[int] + FINISHED_AT_FIELD_NUMBER: _ClassVar[int] + definition: JobDefinition + state: JobState + committed: bool + revision: int + progress: JobProgress + error: _common_pb2.Error + updated_at: _timestamp_pb2.Timestamp + committed_at: _timestamp_pb2.Timestamp + finished_at: _timestamp_pb2.Timestamp + def __init__(self, definition: _Optional[_Union[JobDefinition, _Mapping]] = ..., state: _Optional[_Union[JobState, str]] = ..., committed: _Optional[bool] = ..., revision: _Optional[int] = ..., progress: _Optional[_Union[JobProgress, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., updated_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., committed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., finished_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... diff --git a/src/archive_control/v1/resource_pb2.py b/src/archive_control/v1/resource_pb2.py new file mode 100644 index 0000000..b42874d --- /dev/null +++ b/src/archive_control/v1/resource_pb2.py @@ -0,0 +1,58 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/resource.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/resource.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!archive_control/v1/resource.proto\x12\x12\x61rchive_control.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"^\n\nResourceId\x12\'\n\x10info_hash_v1_hex\x18\x01 \x01(\tR\rinfoHashV1Hex\x12\'\n\x10info_hash_v2_hex\x18\x02 \x01(\tR\rinfoHashV2Hex\":\n\x0e\x46ileIndexRange\x12\x14\n\x05\x66irst\x18\x01 \x01(\rR\x05\x66irst\x12\x12\n\x04last\x18\x02 \x01(\rR\x04last\"J\n\x0cSelectionSet\x12:\n\x06ranges\x18\x01 \x03(\x0b\x32\".archive_control.v1.FileIndexRangeR\x06ranges\"\x98\x02\n\x0bTorrentFile\x12\x1d\n\nfile_index\x18\x01 \x01(\rR\tfileIndex\x12%\n\x0e\x63\x61nonical_path\x18\x02 \x01(\tR\rcanonicalPath\x12#\n\rlogical_bytes\x18\x03 \x01(\x04R\x0clogicalBytes\x12\'\n\x0f\x61llocated_bytes\x18\x04 \x01(\x04R\x0e\x61llocatedBytes\x12\'\n\x0f\x63ompleted_bytes\x18\x05 \x01(\x04R\x0e\x63ompletedBytes\x12\x1a\n\x08selected\x18\x06 \x01(\x08R\x08selected\x12\x16\n\x06sparse\x18\x07 \x01(\x08R\x06sparse\x12\x18\n\x07padding\x18\x08 \x01(\x08R\x07padding\"\xe1\x03\n\x10\x43ontentTreeEntry\x12%\n\x0e\x63\x61nonical_path\x18\x01 \x01(\tR\rcanonicalPath\x12\x1f\n\x0bparent_path\x18\x02 \x01(\tR\nparentPath\x12!\n\x0c\x64isplay_name\x18\x03 \x01(\tR\x0b\x64isplayName\x12\x38\n\x04type\x18\x04 \x01(\x0e\x32$.archive_control.v1.ContentEntryTypeR\x04type\x12\"\n\nfile_index\x18\x05 \x01(\rH\x00R\tfileIndex\x88\x01\x01\x12V\n\x16\x61vailable_file_indices\x18\x06 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x14\x61vailableFileIndices\x12\x36\n\x17\x61vailable_logical_bytes\x18\x07 \x01(\x04R\x15\x61vailableLogicalBytes\x12\x30\n\x14\x61vailable_file_count\x18\x08 \x01(\rR\x12\x61vailableFileCount\x12\x33\n\x04\x66ile\x18\t \x01(\x0b\x32\x1f.archive_control.v1.TorrentFileR\x04\x66ileB\r\n\x0b_file_index\"\xe3\x05\n\x0fResourceSummary\x12?\n\x0bresource_id\x18\x01 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12\"\n\rqb_torrent_id\x18\x02 \x01(\tR\x0bqbTorrentId\x12!\n\x0c\x64isplay_name\x18\x03 \x01(\tR\x0b\x64isplayName\x12L\n\rruntime_state\x18\x04 \x01(\x0e\x32\'.archive_control.v1.TorrentRuntimeStateR\x0cruntimeState\x12G\n\x0eselected_files\x18\x05 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\rselectedFiles\x12X\n\x17selected_complete_files\x18\x06 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x15selectedCompleteFiles\x12\x34\n\x16selected_logical_bytes\x18\x07 \x01(\x04R\x14selectedLogicalBytes\x12\x36\n\x17selected_complete_bytes\x18\x08 \x01(\x04R\x15selectedCompleteBytes\x12.\n\x13total_logical_bytes\x18\t \x01(\x04R\x11totalLogicalBytes\x12(\n\x10total_file_count\x18\n \x01(\rR\x0etotalFileCount\x12)\n\x10\x63ontent_revision\x18\x0b \x01(\tR\x0f\x63ontentRevision\x12\'\n\x0f\x63\x61nonical_paths\x18\x0c \x01(\x08R\x0e\x63\x61nonicalPaths\x12;\n\x0bobserved_at\x18\r \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nobservedAt\"\xa9\x04\n\x18ResourceStateFingerprint\x12?\n\x0bresource_id\x18\x01 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12\x1b\n\tclient_id\x18\x02 \x01(\tR\x08\x63lientId\x12\"\n\rqb_torrent_id\x18\x03 \x01(\tR\x0bqbTorrentId\x12)\n\x10\x63ontent_revision\x18\x04 \x01(\tR\x0f\x63ontentRevision\x12G\n\x0eselected_files\x18\x05 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\rselectedFiles\x12X\n\x17selected_complete_files\x18\x06 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x15selectedCompleteFiles\x12L\n\rruntime_state\x18\x07 \x01(\x0e\x32\'.archive_control.v1.TorrentRuntimeStateR\x0cruntimeState\x12\x32\n\x15save_path_fingerprint\x18\x08 \x01(\tR\x13savePathFingerprint\x12;\n\x0bobserved_at\x18\t \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nobservedAt\"\xfa\x03\n\tPlacement\x12?\n\x0bresource_id\x18\x01 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12\x1b\n\tclient_id\x18\x02 \x01(\tR\x08\x63lientId\x12\x38\n\x05state\x18\x03 \x01(\x0e\x32\".archive_control.v1.PlacementStateR\x05state\x12G\n\x0everified_files\x18\x04 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\rverifiedFiles\x12\x34\n\x16verified_logical_bytes\x18\x05 \x01(\x04R\x14verifiedLogicalBytes\x12\x1e\n\ngeneration\x18\x06 \x01(\x04R\ngeneration\x12N\n\x0b\x66ingerprint\x18\x07 \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x0b\x66ingerprint\x12;\n\x0bverified_at\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nverifiedAt\x12)\n\x11\x63reated_by_job_id\x18\t \x01(\tR\x0e\x63reatedByJobId*\xd6\x02\n\x13TorrentRuntimeState\x12%\n!TORRENT_RUNTIME_STATE_UNSPECIFIED\x10\x00\x12!\n\x1dTORRENT_RUNTIME_STATE_STOPPED\x10\x01\x12 \n\x1cTORRENT_RUNTIME_STATE_QUEUED\x10\x02\x12\"\n\x1eTORRENT_RUNTIME_STATE_CHECKING\x10\x03\x12%\n!TORRENT_RUNTIME_STATE_DOWNLOADING\x10\x04\x12!\n\x1dTORRENT_RUNTIME_STATE_SEEDING\x10\x05\x12!\n\x1dTORRENT_RUNTIME_STATE_STALLED\x10\x06\x12\x1f\n\x1bTORRENT_RUNTIME_STATE_ERROR\x10\x07\x12!\n\x1dTORRENT_RUNTIME_STATE_MISSING\x10\x08*\x9a\x01\n\x10\x43ontentEntryType\x12\"\n\x1e\x43ONTENT_ENTRY_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x43ONTENT_ENTRY_TYPE_FILE\x10\x01\x12 \n\x1c\x43ONTENT_ENTRY_TYPE_DIRECTORY\x10\x02\x12#\n\x1f\x43ONTENT_ENTRY_TYPE_PADDING_FILE\x10\x03*\xdd\x01\n\x0ePlacementState\x12\x1f\n\x1bPLACEMENT_STATE_UNSPECIFIED\x10\x00\x12\x1b\n\x17PLACEMENT_STATE_PRESENT\x10\x01\x12\x1b\n\x17PLACEMENT_STATE_MERGING\x10\x02\x12\x1c\n\x18PLACEMENT_STATE_EVICTING\x10\x03\x12\x1a\n\x16PLACEMENT_STATE_ABSENT\x10\x04\x12\x19\n\x15PLACEMENT_STATE_ERROR\x10\x05\x12\x1b\n\x17PLACEMENT_STATE_UNKNOWN\x10\x06\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.resource_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_TORRENTRUNTIMESTATE']._serialized_start=2897 + _globals['_TORRENTRUNTIMESTATE']._serialized_end=3239 + _globals['_CONTENTENTRYTYPE']._serialized_start=3242 + _globals['_CONTENTENTRYTYPE']._serialized_end=3396 + _globals['_PLACEMENTSTATE']._serialized_start=3399 + _globals['_PLACEMENTSTATE']._serialized_end=3620 + _globals['_RESOURCEID']._serialized_start=90 + _globals['_RESOURCEID']._serialized_end=184 + _globals['_FILEINDEXRANGE']._serialized_start=186 + _globals['_FILEINDEXRANGE']._serialized_end=244 + _globals['_SELECTIONSET']._serialized_start=246 + _globals['_SELECTIONSET']._serialized_end=320 + _globals['_TORRENTFILE']._serialized_start=323 + _globals['_TORRENTFILE']._serialized_end=603 + _globals['_CONTENTTREEENTRY']._serialized_start=606 + _globals['_CONTENTTREEENTRY']._serialized_end=1087 + _globals['_RESOURCESUMMARY']._serialized_start=1090 + _globals['_RESOURCESUMMARY']._serialized_end=1829 + _globals['_RESOURCESTATEFINGERPRINT']._serialized_start=1832 + _globals['_RESOURCESTATEFINGERPRINT']._serialized_end=2385 + _globals['_PLACEMENT']._serialized_start=2388 + _globals['_PLACEMENT']._serialized_end=2894 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/resource_pb2.pyi b/src/archive_control/v1/resource_pb2.pyi new file mode 100644 index 0000000..2474c21 --- /dev/null +++ b/src/archive_control/v1/resource_pb2.pyi @@ -0,0 +1,199 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class TorrentRuntimeState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + TORRENT_RUNTIME_STATE_UNSPECIFIED: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_STOPPED: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_QUEUED: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_CHECKING: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_DOWNLOADING: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_SEEDING: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_STALLED: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_ERROR: _ClassVar[TorrentRuntimeState] + TORRENT_RUNTIME_STATE_MISSING: _ClassVar[TorrentRuntimeState] + +class ContentEntryType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CONTENT_ENTRY_TYPE_UNSPECIFIED: _ClassVar[ContentEntryType] + CONTENT_ENTRY_TYPE_FILE: _ClassVar[ContentEntryType] + CONTENT_ENTRY_TYPE_DIRECTORY: _ClassVar[ContentEntryType] + CONTENT_ENTRY_TYPE_PADDING_FILE: _ClassVar[ContentEntryType] + +class PlacementState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PLACEMENT_STATE_UNSPECIFIED: _ClassVar[PlacementState] + PLACEMENT_STATE_PRESENT: _ClassVar[PlacementState] + PLACEMENT_STATE_MERGING: _ClassVar[PlacementState] + PLACEMENT_STATE_EVICTING: _ClassVar[PlacementState] + PLACEMENT_STATE_ABSENT: _ClassVar[PlacementState] + PLACEMENT_STATE_ERROR: _ClassVar[PlacementState] + PLACEMENT_STATE_UNKNOWN: _ClassVar[PlacementState] +TORRENT_RUNTIME_STATE_UNSPECIFIED: TorrentRuntimeState +TORRENT_RUNTIME_STATE_STOPPED: TorrentRuntimeState +TORRENT_RUNTIME_STATE_QUEUED: TorrentRuntimeState +TORRENT_RUNTIME_STATE_CHECKING: TorrentRuntimeState +TORRENT_RUNTIME_STATE_DOWNLOADING: TorrentRuntimeState +TORRENT_RUNTIME_STATE_SEEDING: TorrentRuntimeState +TORRENT_RUNTIME_STATE_STALLED: TorrentRuntimeState +TORRENT_RUNTIME_STATE_ERROR: TorrentRuntimeState +TORRENT_RUNTIME_STATE_MISSING: TorrentRuntimeState +CONTENT_ENTRY_TYPE_UNSPECIFIED: ContentEntryType +CONTENT_ENTRY_TYPE_FILE: ContentEntryType +CONTENT_ENTRY_TYPE_DIRECTORY: ContentEntryType +CONTENT_ENTRY_TYPE_PADDING_FILE: ContentEntryType +PLACEMENT_STATE_UNSPECIFIED: PlacementState +PLACEMENT_STATE_PRESENT: PlacementState +PLACEMENT_STATE_MERGING: PlacementState +PLACEMENT_STATE_EVICTING: PlacementState +PLACEMENT_STATE_ABSENT: PlacementState +PLACEMENT_STATE_ERROR: PlacementState +PLACEMENT_STATE_UNKNOWN: PlacementState + +class ResourceId(_message.Message): + __slots__ = ("info_hash_v1_hex", "info_hash_v2_hex") + INFO_HASH_V1_HEX_FIELD_NUMBER: _ClassVar[int] + INFO_HASH_V2_HEX_FIELD_NUMBER: _ClassVar[int] + info_hash_v1_hex: str + info_hash_v2_hex: str + def __init__(self, info_hash_v1_hex: _Optional[str] = ..., info_hash_v2_hex: _Optional[str] = ...) -> None: ... + +class FileIndexRange(_message.Message): + __slots__ = ("first", "last") + FIRST_FIELD_NUMBER: _ClassVar[int] + LAST_FIELD_NUMBER: _ClassVar[int] + first: int + last: int + def __init__(self, first: _Optional[int] = ..., last: _Optional[int] = ...) -> None: ... + +class SelectionSet(_message.Message): + __slots__ = ("ranges",) + RANGES_FIELD_NUMBER: _ClassVar[int] + ranges: _containers.RepeatedCompositeFieldContainer[FileIndexRange] + def __init__(self, ranges: _Optional[_Iterable[_Union[FileIndexRange, _Mapping]]] = ...) -> None: ... + +class TorrentFile(_message.Message): + __slots__ = ("file_index", "canonical_path", "logical_bytes", "allocated_bytes", "completed_bytes", "selected", "sparse", "padding") + FILE_INDEX_FIELD_NUMBER: _ClassVar[int] + CANONICAL_PATH_FIELD_NUMBER: _ClassVar[int] + LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + ALLOCATED_BYTES_FIELD_NUMBER: _ClassVar[int] + COMPLETED_BYTES_FIELD_NUMBER: _ClassVar[int] + SELECTED_FIELD_NUMBER: _ClassVar[int] + SPARSE_FIELD_NUMBER: _ClassVar[int] + PADDING_FIELD_NUMBER: _ClassVar[int] + file_index: int + canonical_path: str + logical_bytes: int + allocated_bytes: int + completed_bytes: int + selected: bool + sparse: bool + padding: bool + def __init__(self, file_index: _Optional[int] = ..., canonical_path: _Optional[str] = ..., logical_bytes: _Optional[int] = ..., allocated_bytes: _Optional[int] = ..., completed_bytes: _Optional[int] = ..., selected: _Optional[bool] = ..., sparse: _Optional[bool] = ..., padding: _Optional[bool] = ...) -> None: ... + +class ContentTreeEntry(_message.Message): + __slots__ = ("canonical_path", "parent_path", "display_name", "type", "file_index", "available_file_indices", "available_logical_bytes", "available_file_count", "file") + CANONICAL_PATH_FIELD_NUMBER: _ClassVar[int] + PARENT_PATH_FIELD_NUMBER: _ClassVar[int] + DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + FILE_INDEX_FIELD_NUMBER: _ClassVar[int] + AVAILABLE_FILE_INDICES_FIELD_NUMBER: _ClassVar[int] + AVAILABLE_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + AVAILABLE_FILE_COUNT_FIELD_NUMBER: _ClassVar[int] + FILE_FIELD_NUMBER: _ClassVar[int] + canonical_path: str + parent_path: str + display_name: str + type: ContentEntryType + file_index: int + available_file_indices: SelectionSet + available_logical_bytes: int + available_file_count: int + file: TorrentFile + def __init__(self, canonical_path: _Optional[str] = ..., parent_path: _Optional[str] = ..., display_name: _Optional[str] = ..., type: _Optional[_Union[ContentEntryType, str]] = ..., file_index: _Optional[int] = ..., available_file_indices: _Optional[_Union[SelectionSet, _Mapping]] = ..., available_logical_bytes: _Optional[int] = ..., available_file_count: _Optional[int] = ..., file: _Optional[_Union[TorrentFile, _Mapping]] = ...) -> None: ... + +class ResourceSummary(_message.Message): + __slots__ = ("resource_id", "qb_torrent_id", "display_name", "runtime_state", "selected_files", "selected_complete_files", "selected_logical_bytes", "selected_complete_bytes", "total_logical_bytes", "total_file_count", "content_revision", "canonical_paths", "observed_at") + RESOURCE_ID_FIELD_NUMBER: _ClassVar[int] + QB_TORRENT_ID_FIELD_NUMBER: _ClassVar[int] + DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int] + RUNTIME_STATE_FIELD_NUMBER: _ClassVar[int] + SELECTED_FILES_FIELD_NUMBER: _ClassVar[int] + SELECTED_COMPLETE_FILES_FIELD_NUMBER: _ClassVar[int] + SELECTED_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + SELECTED_COMPLETE_BYTES_FIELD_NUMBER: _ClassVar[int] + TOTAL_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + TOTAL_FILE_COUNT_FIELD_NUMBER: _ClassVar[int] + CONTENT_REVISION_FIELD_NUMBER: _ClassVar[int] + CANONICAL_PATHS_FIELD_NUMBER: _ClassVar[int] + OBSERVED_AT_FIELD_NUMBER: _ClassVar[int] + resource_id: ResourceId + qb_torrent_id: str + display_name: str + runtime_state: TorrentRuntimeState + selected_files: SelectionSet + selected_complete_files: SelectionSet + selected_logical_bytes: int + selected_complete_bytes: int + total_logical_bytes: int + total_file_count: int + content_revision: str + canonical_paths: bool + observed_at: _timestamp_pb2.Timestamp + def __init__(self, resource_id: _Optional[_Union[ResourceId, _Mapping]] = ..., qb_torrent_id: _Optional[str] = ..., display_name: _Optional[str] = ..., runtime_state: _Optional[_Union[TorrentRuntimeState, str]] = ..., selected_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., selected_complete_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., selected_logical_bytes: _Optional[int] = ..., selected_complete_bytes: _Optional[int] = ..., total_logical_bytes: _Optional[int] = ..., total_file_count: _Optional[int] = ..., content_revision: _Optional[str] = ..., canonical_paths: _Optional[bool] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class ResourceStateFingerprint(_message.Message): + __slots__ = ("resource_id", "client_id", "qb_torrent_id", "content_revision", "selected_files", "selected_complete_files", "runtime_state", "save_path_fingerprint", "observed_at") + RESOURCE_ID_FIELD_NUMBER: _ClassVar[int] + CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + QB_TORRENT_ID_FIELD_NUMBER: _ClassVar[int] + CONTENT_REVISION_FIELD_NUMBER: _ClassVar[int] + SELECTED_FILES_FIELD_NUMBER: _ClassVar[int] + SELECTED_COMPLETE_FILES_FIELD_NUMBER: _ClassVar[int] + RUNTIME_STATE_FIELD_NUMBER: _ClassVar[int] + SAVE_PATH_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + OBSERVED_AT_FIELD_NUMBER: _ClassVar[int] + resource_id: ResourceId + client_id: str + qb_torrent_id: str + content_revision: str + selected_files: SelectionSet + selected_complete_files: SelectionSet + runtime_state: TorrentRuntimeState + save_path_fingerprint: str + observed_at: _timestamp_pb2.Timestamp + def __init__(self, resource_id: _Optional[_Union[ResourceId, _Mapping]] = ..., client_id: _Optional[str] = ..., qb_torrent_id: _Optional[str] = ..., content_revision: _Optional[str] = ..., selected_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., selected_complete_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., runtime_state: _Optional[_Union[TorrentRuntimeState, str]] = ..., save_path_fingerprint: _Optional[str] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class Placement(_message.Message): + __slots__ = ("resource_id", "client_id", "state", "verified_files", "verified_logical_bytes", "generation", "fingerprint", "verified_at", "created_by_job_id") + RESOURCE_ID_FIELD_NUMBER: _ClassVar[int] + CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + VERIFIED_FILES_FIELD_NUMBER: _ClassVar[int] + VERIFIED_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + GENERATION_FIELD_NUMBER: _ClassVar[int] + FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + VERIFIED_AT_FIELD_NUMBER: _ClassVar[int] + CREATED_BY_JOB_ID_FIELD_NUMBER: _ClassVar[int] + resource_id: ResourceId + client_id: str + state: PlacementState + verified_files: SelectionSet + verified_logical_bytes: int + generation: int + fingerprint: ResourceStateFingerprint + verified_at: _timestamp_pb2.Timestamp + created_by_job_id: str + def __init__(self, resource_id: _Optional[_Union[ResourceId, _Mapping]] = ..., client_id: _Optional[str] = ..., state: _Optional[_Union[PlacementState, str]] = ..., verified_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., verified_logical_bytes: _Optional[int] = ..., generation: _Optional[int] = ..., fingerprint: _Optional[_Union[ResourceStateFingerprint, _Mapping]] = ..., verified_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., created_by_job_id: _Optional[str] = ...) -> None: ... diff --git a/src/archive_control/v1/route_pb2.py b/src/archive_control/v1/route_pb2.py new file mode 100644 index 0000000..df7957b --- /dev/null +++ b/src/archive_control/v1/route_pb2.py @@ -0,0 +1,48 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/route.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/route.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x61rchive_control/v1/route.proto\x12\x12\x61rchive_control.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa1\x04\n\nLocalRoute\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12.\n\x13local_relative_path\x18\x02 \x01(\tR\x11localRelativePath\x12H\n\x0b\x66older_type\x18\x03 \x01(\x0e\x32\'.archive_control.v1.SyncthingFolderTypeR\nfolderType\x12\x39\n\x19local_syncthing_device_id\x18\x04 \x01(\tR\x16localSyncthingDeviceId\x12\x39\n\x19peer_syncthing_device_ids\x18\x05 \x03(\tR\x16peerSyncthingDeviceIds\x12\x34\n\x05state\x18\x06 \x01(\x0e\x32\x1e.archive_control.v1.RouteStateR\x05state\x12\x1a\n\x08writable\x18\x07 \x01(\x08R\x08writable\x12)\n\x10sparse_supported\x18\x08 \x01(\x08R\x0fsparseSupported\x12\x36\n\x17\x61rchive_control_created\x18\t \x01(\x08R\x15\x61rchiveControlCreated\x12\x16\n\x06\x64\x65tail\x18\n \x01(\tR\x06\x64\x65tail\x12;\n\x0bobserved_at\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nobservedAt\"\x85\x03\n\x0fRouteDescriptor\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12*\n\x11\x61rchive_client_id\x18\x02 \x01(\tR\x0f\x61rchiveClientId\x12&\n\x0f\x63\x61\x63he_client_id\x18\x03 \x01(\tR\rcacheClientId\x12=\n\x1b\x61rchive_syncthing_device_id\x18\x04 \x01(\tR\x18\x61rchiveSyncthingDeviceId\x12\x39\n\x19\x63\x61\x63he_syncthing_device_id\x18\x05 \x01(\tR\x16\x63\x61\x63heSyncthingDeviceId\x12\x34\n\x05state\x18\x06 \x01(\x0e\x32\x1e.archive_control.v1.RouteStateR\x05state\x12;\n\x0bverified_at\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nverifiedAt\x12\x16\n\x06\x64\x65tail\x18\x08 \x01(\tR\x06\x64\x65tail\"\x96\x02\n\x0f\x45nsureRouteSpec\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12$\n\x0epeer_client_id\x18\x02 \x01(\tR\x0cpeerClientId\x12\x37\n\x18peer_syncthing_device_id\x18\x03 \x01(\tR\x15peerSyncthingDeviceId\x12%\n\x0epeer_addresses\x18\x04 \x03(\tR\rpeerAddresses\x12.\n\x13local_relative_path\x18\x05 \x01(\tR\x11localRelativePath\x12\x32\n\x15setup_timeout_seconds\x18\x06 \x01(\rR\x13setupTimeoutSeconds\"\xbe\x02\n\x11RouteVerification\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12\x14\n\x05nonce\x18\x02 \x01(\tR\x05nonce\x12\x36\n\x18local_nonce_seen_by_peer\x18\x03 \x01(\x08R\x14localNonceSeenByPeer\x12\x35\n\x17peer_nonce_seen_locally\x18\x04 \x01(\x08R\x14peerNonceSeenLocally\x12\x34\n\x05state\x18\x05 \x01(\x0e\x32\x1e.archive_control.v1.RouteStateR\x05state\x12\x16\n\x06\x64\x65tail\x18\x06 \x01(\tR\x06\x64\x65tail\x12;\n\x0bobserved_at\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nobservedAt*\xd2\x01\n\x13SyncthingFolderType\x12%\n!SYNCTHING_FOLDER_TYPE_UNSPECIFIED\x10\x00\x12&\n\"SYNCTHING_FOLDER_TYPE_SEND_RECEIVE\x10\x01\x12#\n\x1fSYNCTHING_FOLDER_TYPE_SEND_ONLY\x10\x02\x12&\n\"SYNCTHING_FOLDER_TYPE_RECEIVE_ONLY\x10\x03\x12\x1f\n\x1bSYNCTHING_FOLDER_TYPE_OTHER\x10\x04*\xe5\x01\n\nRouteState\x12\x1b\n\x17ROUTE_STATE_UNSPECIFIED\x10\x00\x12\x1a\n\x16ROUTE_STATE_DISCOVERED\x10\x01\x12\x1c\n\x18ROUTE_STATE_PROVISIONING\x10\x02\x12\x19\n\x15ROUTE_STATE_VERIFYING\x10\x03\x12\x15\n\x11ROUTE_STATE_READY\x10\x04\x12\x16\n\x12ROUTE_STATE_PAUSED\x10\x05\x12\x19\n\x15ROUTE_STATE_UNHEALTHY\x10\x06\x12\x1b\n\x17ROUTE_STATE_UNSUPPORTED\x10\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.route_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_SYNCTHINGFOLDERTYPE']._serialized_start=1630 + _globals['_SYNCTHINGFOLDERTYPE']._serialized_end=1840 + _globals['_ROUTESTATE']._serialized_start=1843 + _globals['_ROUTESTATE']._serialized_end=2072 + _globals['_LOCALROUTE']._serialized_start=88 + _globals['_LOCALROUTE']._serialized_end=633 + _globals['_ROUTEDESCRIPTOR']._serialized_start=636 + _globals['_ROUTEDESCRIPTOR']._serialized_end=1025 + _globals['_ENSUREROUTESPEC']._serialized_start=1028 + _globals['_ENSUREROUTESPEC']._serialized_end=1306 + _globals['_ROUTEVERIFICATION']._serialized_start=1309 + _globals['_ROUTEVERIFICATION']._serialized_end=1627 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/route_pb2.pyi b/src/archive_control/v1/route_pb2.pyi new file mode 100644 index 0000000..abe25c7 --- /dev/null +++ b/src/archive_control/v1/route_pb2.pyi @@ -0,0 +1,124 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class SyncthingFolderType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + SYNCTHING_FOLDER_TYPE_UNSPECIFIED: _ClassVar[SyncthingFolderType] + SYNCTHING_FOLDER_TYPE_SEND_RECEIVE: _ClassVar[SyncthingFolderType] + SYNCTHING_FOLDER_TYPE_SEND_ONLY: _ClassVar[SyncthingFolderType] + SYNCTHING_FOLDER_TYPE_RECEIVE_ONLY: _ClassVar[SyncthingFolderType] + SYNCTHING_FOLDER_TYPE_OTHER: _ClassVar[SyncthingFolderType] + +class RouteState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + ROUTE_STATE_UNSPECIFIED: _ClassVar[RouteState] + ROUTE_STATE_DISCOVERED: _ClassVar[RouteState] + ROUTE_STATE_PROVISIONING: _ClassVar[RouteState] + ROUTE_STATE_VERIFYING: _ClassVar[RouteState] + ROUTE_STATE_READY: _ClassVar[RouteState] + ROUTE_STATE_PAUSED: _ClassVar[RouteState] + ROUTE_STATE_UNHEALTHY: _ClassVar[RouteState] + ROUTE_STATE_UNSUPPORTED: _ClassVar[RouteState] +SYNCTHING_FOLDER_TYPE_UNSPECIFIED: SyncthingFolderType +SYNCTHING_FOLDER_TYPE_SEND_RECEIVE: SyncthingFolderType +SYNCTHING_FOLDER_TYPE_SEND_ONLY: SyncthingFolderType +SYNCTHING_FOLDER_TYPE_RECEIVE_ONLY: SyncthingFolderType +SYNCTHING_FOLDER_TYPE_OTHER: SyncthingFolderType +ROUTE_STATE_UNSPECIFIED: RouteState +ROUTE_STATE_DISCOVERED: RouteState +ROUTE_STATE_PROVISIONING: RouteState +ROUTE_STATE_VERIFYING: RouteState +ROUTE_STATE_READY: RouteState +ROUTE_STATE_PAUSED: RouteState +ROUTE_STATE_UNHEALTHY: RouteState +ROUTE_STATE_UNSUPPORTED: RouteState + +class LocalRoute(_message.Message): + __slots__ = ("route_id", "local_relative_path", "folder_type", "local_syncthing_device_id", "peer_syncthing_device_ids", "state", "writable", "sparse_supported", "archive_control_created", "detail", "observed_at") + ROUTE_ID_FIELD_NUMBER: _ClassVar[int] + LOCAL_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int] + FOLDER_TYPE_FIELD_NUMBER: _ClassVar[int] + LOCAL_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int] + PEER_SYNCTHING_DEVICE_IDS_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + WRITABLE_FIELD_NUMBER: _ClassVar[int] + SPARSE_SUPPORTED_FIELD_NUMBER: _ClassVar[int] + ARCHIVE_CONTROL_CREATED_FIELD_NUMBER: _ClassVar[int] + DETAIL_FIELD_NUMBER: _ClassVar[int] + OBSERVED_AT_FIELD_NUMBER: _ClassVar[int] + route_id: str + local_relative_path: str + folder_type: SyncthingFolderType + local_syncthing_device_id: str + peer_syncthing_device_ids: _containers.RepeatedScalarFieldContainer[str] + state: RouteState + writable: bool + sparse_supported: bool + archive_control_created: bool + detail: str + observed_at: _timestamp_pb2.Timestamp + def __init__(self, route_id: _Optional[str] = ..., local_relative_path: _Optional[str] = ..., folder_type: _Optional[_Union[SyncthingFolderType, str]] = ..., local_syncthing_device_id: _Optional[str] = ..., peer_syncthing_device_ids: _Optional[_Iterable[str]] = ..., state: _Optional[_Union[RouteState, str]] = ..., writable: _Optional[bool] = ..., sparse_supported: _Optional[bool] = ..., archive_control_created: _Optional[bool] = ..., detail: _Optional[str] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class RouteDescriptor(_message.Message): + __slots__ = ("route_id", "archive_client_id", "cache_client_id", "archive_syncthing_device_id", "cache_syncthing_device_id", "state", "verified_at", "detail") + ROUTE_ID_FIELD_NUMBER: _ClassVar[int] + ARCHIVE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + CACHE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + ARCHIVE_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int] + CACHE_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + VERIFIED_AT_FIELD_NUMBER: _ClassVar[int] + DETAIL_FIELD_NUMBER: _ClassVar[int] + route_id: str + archive_client_id: str + cache_client_id: str + archive_syncthing_device_id: str + cache_syncthing_device_id: str + state: RouteState + verified_at: _timestamp_pb2.Timestamp + detail: str + def __init__(self, route_id: _Optional[str] = ..., archive_client_id: _Optional[str] = ..., cache_client_id: _Optional[str] = ..., archive_syncthing_device_id: _Optional[str] = ..., cache_syncthing_device_id: _Optional[str] = ..., state: _Optional[_Union[RouteState, str]] = ..., verified_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., detail: _Optional[str] = ...) -> None: ... + +class EnsureRouteSpec(_message.Message): + __slots__ = ("route_id", "peer_client_id", "peer_syncthing_device_id", "peer_addresses", "local_relative_path", "setup_timeout_seconds") + ROUTE_ID_FIELD_NUMBER: _ClassVar[int] + PEER_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + PEER_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int] + PEER_ADDRESSES_FIELD_NUMBER: _ClassVar[int] + LOCAL_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int] + SETUP_TIMEOUT_SECONDS_FIELD_NUMBER: _ClassVar[int] + route_id: str + peer_client_id: str + peer_syncthing_device_id: str + peer_addresses: _containers.RepeatedScalarFieldContainer[str] + local_relative_path: str + setup_timeout_seconds: int + def __init__(self, route_id: _Optional[str] = ..., peer_client_id: _Optional[str] = ..., peer_syncthing_device_id: _Optional[str] = ..., peer_addresses: _Optional[_Iterable[str]] = ..., local_relative_path: _Optional[str] = ..., setup_timeout_seconds: _Optional[int] = ...) -> None: ... + +class RouteVerification(_message.Message): + __slots__ = ("route_id", "nonce", "local_nonce_seen_by_peer", "peer_nonce_seen_locally", "state", "detail", "observed_at") + ROUTE_ID_FIELD_NUMBER: _ClassVar[int] + NONCE_FIELD_NUMBER: _ClassVar[int] + LOCAL_NONCE_SEEN_BY_PEER_FIELD_NUMBER: _ClassVar[int] + PEER_NONCE_SEEN_LOCALLY_FIELD_NUMBER: _ClassVar[int] + STATE_FIELD_NUMBER: _ClassVar[int] + DETAIL_FIELD_NUMBER: _ClassVar[int] + OBSERVED_AT_FIELD_NUMBER: _ClassVar[int] + route_id: str + nonce: str + local_nonce_seen_by_peer: bool + peer_nonce_seen_locally: bool + state: RouteState + detail: str + observed_at: _timestamp_pb2.Timestamp + def __init__(self, route_id: _Optional[str] = ..., nonce: _Optional[str] = ..., local_nonce_seen_by_peer: _Optional[bool] = ..., peer_nonce_seen_locally: _Optional[bool] = ..., state: _Optional[_Union[RouteState, str]] = ..., detail: _Optional[str] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... diff --git a/src/archive_control/v1/transfer_pb2.py b/src/archive_control/v1/transfer_pb2.py new file mode 100644 index 0000000..e31a5ad --- /dev/null +++ b/src/archive_control/v1/transfer_pb2.py @@ -0,0 +1,49 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: archive_control/v1/transfer.proto +# Protobuf Python Version: 7.35.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 1, + '', + 'archive_control/v1/transfer.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from archive_control.v1 import resource_pb2 as archive__control_dot_v1_dot_resource__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!archive_control/v1/transfer.proto\x12\x12\x61rchive_control.v1\x1a!archive_control/v1/resource.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd7\x03\n\x0cManifestFile\x12\x1d\n\nfile_index\x18\x01 \x01(\rR\tfileIndex\x12\x32\n\x15payload_relative_path\x18\x02 \x01(\tR\x13payloadRelativePath\x12\x32\n\x15target_canonical_path\x18\x03 \x01(\tR\x13targetCanonicalPath\x12#\n\rlogical_bytes\x18\x04 \x01(\x04R\x0clogicalBytes\x12\'\n\x0f\x61llocated_bytes\x18\x05 \x01(\x04R\x0e\x61llocatedBytes\x12\x16\n\x06sparse\x18\x06 \x01(\x08R\x06sparse\x12]\n\x15source_staging_method\x18\x07 \x01(\x0e\x32).archive_control.v1.MaterializationMethodR\x13sourceStagingMethod\x12N\n\rtarget_method\x18\x08 \x01(\x0e\x32).archive_control.v1.MaterializationMethodR\x0ctargetMethod\x12+\n\x11target_preexisted\x18\t \x01(\x08R\x10targetPreexisted\"\x99\x02\n\x10ManifestArtifact\x12\x34\n\x04kind\x18\x01 \x01(\x0e\x32 .archive_control.v1.ArtifactKindR\x04kind\x12\x32\n\x15payload_relative_path\x18\x02 \x01(\tR\x13payloadRelativePath\x12#\n\rlogical_bytes\x18\x03 \x01(\x04R\x0clogicalBytes\x12\'\n\x0f\x61llocated_bytes\x18\x04 \x01(\x04R\x0e\x61llocatedBytes\x12\x16\n\x06sparse\x18\x05 \x01(\x08R\x06sparse\x12\x16\n\x06\x66ormat\x18\x06 \x01(\tR\x06\x66ormat\x12\x1d\n\nsha256_hex\x18\x07 \x01(\tR\tsha256Hex\"\xfb\x06\n\x10TransferManifest\x12)\n\x10manifest_version\x18\x01 \x01(\rR\x0fmanifestVersion\x12\x15\n\x06job_id\x18\x02 \x01(\tR\x05jobId\x12?\n\x0bresource_id\x18\x03 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12(\n\x10source_client_id\x18\x04 \x01(\tR\x0esourceClientId\x12(\n\x10target_client_id\x18\x05 \x01(\tR\x0etargetClientId\x12\x19\n\x08route_id\x18\x06 \x01(\tR\x07routeId\x12I\n\x0frequested_files\x18\x07 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x0erequestedFiles\x12T\n\x15target_baseline_files\x18\x08 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x13targetBaselineFiles\x12R\n\x14transfer_delta_files\x18\t \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x12transferDeltaFiles\x12\x36\n\x05\x66iles\x18\n \x03(\x0b\x32 .archive_control.v1.ManifestFileR\x05\x66iles\x12\x42\n\tartifacts\x18\x0b \x03(\x0b\x32$.archive_control.v1.ManifestArtifactR\tartifacts\x12[\n\x12source_fingerprint\x18\x0c \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x11sourceFingerprint\x12l\n\x1btarget_baseline_fingerprint\x18\r \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x19targetBaselineFingerprint\x12\x39\n\ncreated_at\x18\x0e \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\"\x8b\x01\n\x0bReadyMarker\x12\x15\n\x06job_id\x18\x01 \x01(\tR\x05jobId\x12.\n\x13manifest_sha256_hex\x18\x02 \x01(\tR\x11manifestSha256Hex\x12\x35\n\x08ready_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x07readyAt*\xd9\x01\n\x15MaterializationMethod\x12&\n\"MATERIALIZATION_METHOD_UNSPECIFIED\x10\x00\x12$\n MATERIALIZATION_METHOD_HARD_LINK\x10\x01\x12\"\n\x1eMATERIALIZATION_METHOD_REFLINK\x10\x02\x12\x1f\n\x1bMATERIALIZATION_METHOD_COPY\x10\x03\x12-\n)MATERIALIZATION_METHOD_PREEXISTING_REUSED\x10\x04*\x97\x01\n\x0c\x41rtifactKind\x12\x1d\n\x19\x41RTIFACT_KIND_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x41RTIFACT_KIND_TORRENT_FILE\x10\x01\x12%\n!ARTIFACT_KIND_LIBTORRENT_PARTFILE\x10\x02\x12!\n\x1d\x41RTIFACT_KIND_OTHER_AUXILIARY\x10\x03\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.transfer_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_MATERIALIZATIONMETHOD']._serialized_start=1920 + _globals['_MATERIALIZATIONMETHOD']._serialized_end=2137 + _globals['_ARTIFACTKIND']._serialized_start=2140 + _globals['_ARTIFACTKIND']._serialized_end=2291 + _globals['_MANIFESTFILE']._serialized_start=126 + _globals['_MANIFESTFILE']._serialized_end=597 + _globals['_MANIFESTARTIFACT']._serialized_start=600 + _globals['_MANIFESTARTIFACT']._serialized_end=881 + _globals['_TRANSFERMANIFEST']._serialized_start=884 + _globals['_TRANSFERMANIFEST']._serialized_end=1775 + _globals['_READYMARKER']._serialized_start=1778 + _globals['_READYMARKER']._serialized_end=1917 +# @@protoc_insertion_point(module_scope) diff --git a/src/archive_control/v1/transfer_pb2.pyi b/src/archive_control/v1/transfer_pb2.pyi new file mode 100644 index 0000000..0f65722 --- /dev/null +++ b/src/archive_control/v1/transfer_pb2.pyi @@ -0,0 +1,119 @@ +# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033 +import datetime + +from archive_control.v1 import resource_pb2 as _resource_pb2 +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class MaterializationMethod(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + MATERIALIZATION_METHOD_UNSPECIFIED: _ClassVar[MaterializationMethod] + MATERIALIZATION_METHOD_HARD_LINK: _ClassVar[MaterializationMethod] + MATERIALIZATION_METHOD_REFLINK: _ClassVar[MaterializationMethod] + MATERIALIZATION_METHOD_COPY: _ClassVar[MaterializationMethod] + MATERIALIZATION_METHOD_PREEXISTING_REUSED: _ClassVar[MaterializationMethod] + +class ArtifactKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + ARTIFACT_KIND_UNSPECIFIED: _ClassVar[ArtifactKind] + ARTIFACT_KIND_TORRENT_FILE: _ClassVar[ArtifactKind] + ARTIFACT_KIND_LIBTORRENT_PARTFILE: _ClassVar[ArtifactKind] + ARTIFACT_KIND_OTHER_AUXILIARY: _ClassVar[ArtifactKind] +MATERIALIZATION_METHOD_UNSPECIFIED: MaterializationMethod +MATERIALIZATION_METHOD_HARD_LINK: MaterializationMethod +MATERIALIZATION_METHOD_REFLINK: MaterializationMethod +MATERIALIZATION_METHOD_COPY: MaterializationMethod +MATERIALIZATION_METHOD_PREEXISTING_REUSED: MaterializationMethod +ARTIFACT_KIND_UNSPECIFIED: ArtifactKind +ARTIFACT_KIND_TORRENT_FILE: ArtifactKind +ARTIFACT_KIND_LIBTORRENT_PARTFILE: ArtifactKind +ARTIFACT_KIND_OTHER_AUXILIARY: ArtifactKind + +class ManifestFile(_message.Message): + __slots__ = ("file_index", "payload_relative_path", "target_canonical_path", "logical_bytes", "allocated_bytes", "sparse", "source_staging_method", "target_method", "target_preexisted") + FILE_INDEX_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int] + TARGET_CANONICAL_PATH_FIELD_NUMBER: _ClassVar[int] + LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + ALLOCATED_BYTES_FIELD_NUMBER: _ClassVar[int] + SPARSE_FIELD_NUMBER: _ClassVar[int] + SOURCE_STAGING_METHOD_FIELD_NUMBER: _ClassVar[int] + TARGET_METHOD_FIELD_NUMBER: _ClassVar[int] + TARGET_PREEXISTED_FIELD_NUMBER: _ClassVar[int] + file_index: int + payload_relative_path: str + target_canonical_path: str + logical_bytes: int + allocated_bytes: int + sparse: bool + source_staging_method: MaterializationMethod + target_method: MaterializationMethod + target_preexisted: bool + def __init__(self, file_index: _Optional[int] = ..., payload_relative_path: _Optional[str] = ..., target_canonical_path: _Optional[str] = ..., logical_bytes: _Optional[int] = ..., allocated_bytes: _Optional[int] = ..., sparse: _Optional[bool] = ..., source_staging_method: _Optional[_Union[MaterializationMethod, str]] = ..., target_method: _Optional[_Union[MaterializationMethod, str]] = ..., target_preexisted: _Optional[bool] = ...) -> None: ... + +class ManifestArtifact(_message.Message): + __slots__ = ("kind", "payload_relative_path", "logical_bytes", "allocated_bytes", "sparse", "format", "sha256_hex") + KIND_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int] + LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int] + ALLOCATED_BYTES_FIELD_NUMBER: _ClassVar[int] + SPARSE_FIELD_NUMBER: _ClassVar[int] + FORMAT_FIELD_NUMBER: _ClassVar[int] + SHA256_HEX_FIELD_NUMBER: _ClassVar[int] + kind: ArtifactKind + payload_relative_path: str + logical_bytes: int + allocated_bytes: int + sparse: bool + format: str + sha256_hex: str + def __init__(self, kind: _Optional[_Union[ArtifactKind, str]] = ..., payload_relative_path: _Optional[str] = ..., logical_bytes: _Optional[int] = ..., allocated_bytes: _Optional[int] = ..., sparse: _Optional[bool] = ..., format: _Optional[str] = ..., sha256_hex: _Optional[str] = ...) -> None: ... + +class TransferManifest(_message.Message): + __slots__ = ("manifest_version", "job_id", "resource_id", "source_client_id", "target_client_id", "route_id", "requested_files", "target_baseline_files", "transfer_delta_files", "files", "artifacts", "source_fingerprint", "target_baseline_fingerprint", "created_at") + MANIFEST_VERSION_FIELD_NUMBER: _ClassVar[int] + JOB_ID_FIELD_NUMBER: _ClassVar[int] + RESOURCE_ID_FIELD_NUMBER: _ClassVar[int] + SOURCE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + TARGET_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + ROUTE_ID_FIELD_NUMBER: _ClassVar[int] + REQUESTED_FILES_FIELD_NUMBER: _ClassVar[int] + TARGET_BASELINE_FILES_FIELD_NUMBER: _ClassVar[int] + TRANSFER_DELTA_FILES_FIELD_NUMBER: _ClassVar[int] + FILES_FIELD_NUMBER: _ClassVar[int] + ARTIFACTS_FIELD_NUMBER: _ClassVar[int] + SOURCE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + TARGET_BASELINE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + CREATED_AT_FIELD_NUMBER: _ClassVar[int] + manifest_version: int + job_id: str + resource_id: _resource_pb2.ResourceId + source_client_id: str + target_client_id: str + route_id: str + requested_files: _resource_pb2.SelectionSet + target_baseline_files: _resource_pb2.SelectionSet + transfer_delta_files: _resource_pb2.SelectionSet + files: _containers.RepeatedCompositeFieldContainer[ManifestFile] + artifacts: _containers.RepeatedCompositeFieldContainer[ManifestArtifact] + source_fingerprint: _resource_pb2.ResourceStateFingerprint + target_baseline_fingerprint: _resource_pb2.ResourceStateFingerprint + created_at: _timestamp_pb2.Timestamp + def __init__(self, manifest_version: _Optional[int] = ..., job_id: _Optional[str] = ..., resource_id: _Optional[_Union[_resource_pb2.ResourceId, _Mapping]] = ..., source_client_id: _Optional[str] = ..., target_client_id: _Optional[str] = ..., route_id: _Optional[str] = ..., requested_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., target_baseline_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., transfer_delta_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., files: _Optional[_Iterable[_Union[ManifestFile, _Mapping]]] = ..., artifacts: _Optional[_Iterable[_Union[ManifestArtifact, _Mapping]]] = ..., source_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., target_baseline_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class ReadyMarker(_message.Message): + __slots__ = ("job_id", "manifest_sha256_hex", "ready_at") + JOB_ID_FIELD_NUMBER: _ClassVar[int] + MANIFEST_SHA256_HEX_FIELD_NUMBER: _ClassVar[int] + READY_AT_FIELD_NUMBER: _ClassVar[int] + job_id: str + manifest_sha256_hex: str + ready_at: _timestamp_pb2.Timestamp + def __init__(self, job_id: _Optional[str] = ..., manifest_sha256_hex: _Optional[str] = ..., ready_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..7e94240 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,101 @@ +import os +import tempfile +import unittest +from pathlib import Path + +from archive_clients.config import ClientConfig, ConfigError, RootMapping + + +class ConfigTests(unittest.TestCase): + def test_strict_config_mode_override_secrets_and_mapping(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in ("token", "qb-password", "syncthing-key"): + path = root / name + path.write_text(name, encoding="utf-8") + os.chmod(path, 0o600) + (root / "qb").mkdir() + (root / "sync").mkdir() + config_path = root / "client.toml" + config_path.write_text( + _config(root, role="cache"), encoding="utf-8" + ) + config = ClientConfig.load(config_path, "archive") + self.assertEqual(config.role, "archive") + self.assertEqual(config.read_shared_token(), "token") + self.assertEqual( + config.qbittorrent.roots.api_to_local("/downloads/a/b"), + root / "qb/a/b", + ) + self.assertEqual(config.jobs.stall_after, 30 * 60) + self.assertEqual( + config.syncthing.advertised_addresses, ("dynamic",) + ) + + def test_unknown_key_and_unsafe_secret_fail(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in ("token", "qb-password", "syncthing-key"): + path = root / name + path.write_text(name, encoding="utf-8") + os.chmod(path, 0o600) + config_path = root / "client.toml" + invalid = _config(root).replace( + "[qbittorrent]", "typo = true\n[qbittorrent]" + ) + config_path.write_text(invalid, encoding="utf-8") + with self.assertRaisesRegex(ConfigError, "unknown root"): + ClientConfig.load(config_path) + os.chmod(root / "token", 0o640) + config_path.write_text(_config(root), encoding="utf-8") + with self.assertRaisesRegex(ConfigError, "permissions"): + ClientConfig.load(config_path).read_shared_token() + + def test_mapping_rejects_escape(self): + mapping = RootMapping(Path("/api"), Path("/local")) + with self.assertRaises(ConfigError): + mapping.api_to_local("/elsewhere/file") + + def test_endpoint_scheme_and_job_keys_are_strict(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + invalid_endpoint = _config(root).replace( + 'control_endpoint = "ws://control/archive_control"', + 'control_endpoint = "http://control/archive_control"', + ) + path = root / "client.toml" + path.write_text(invalid_endpoint, encoding="utf-8") + with self.assertRaisesRegex(ConfigError, "ws/wss"): + ClientConfig.load(path) + path.write_text( + _config(root) + "\n[jobs]\nunknown = true\n", encoding="utf-8" + ) + with self.assertRaisesRegex(ConfigError, "unknown jobs"): + ClientConfig.load(path) + + +def _config(root: Path, role: str = "cache") -> str: + return f'''client_id = "cache-1" +display_name = "Cache 1" +role = "{role}" +control_endpoint = "ws://control/archive_control" +shared_token_file = "{root / 'token'}" +state_db = "{root / 'state.db'}" +backup_dir = "{root / 'backups'}" +[qbittorrent] +endpoint = "http://qb" +username = "admin" +password_file = "{root / 'qb-password'}" +api_root = "/downloads" +local_root = "{root / 'qb'}" +[syncthing] +endpoint = "http://syncthing" +api_key_file = "{root / 'syncthing-key'}" +api_root = "/sync" +local_root = "{root / 'sync'}" +advertised_addresses = ["dynamic"] +''' + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_daemon.py b/tests/test_daemon.py new file mode 100644 index 0000000..e1efc66 --- /dev/null +++ b/tests/test_daemon.py @@ -0,0 +1,112 @@ +import asyncio +import os +import tempfile +import unittest +from pathlib import Path, PurePosixPath +from uuid import uuid4 + +from websockets.asyncio.server import serve + +from archive_clients.config import ClientConfig, ConnectionConfig, ServiceConfig +from archive_clients.daemon import ArchiveClientDaemon +from archive_clients.probes import FilesystemProbe +from archive_clients.protocol import decode, encode, new_envelope +from archive_control.v1 import client_pb2, common_pb2, control_pb2 + + +class DaemonTransportTests(unittest.IsolatedAsyncioTestCase): + async def test_registration_heartbeat_and_duplicate_command(self): + observed = {} + + async def control(websocket): + registration = decode(await websocket.recv()) + observed["token"] = registration.register_request.shared_token + observed["root_names"] = [ + item.root_name + for item in registration.register_request.capabilities.filesystems + ] + observed["addresses"] = list( + registration.register_request.capabilities + .syncthing_advertised_addresses + ) + response = new_envelope() + response.correlation_id = registration.message_id + response.register_response.status = client_pb2.REGISTRATION_STATUS_ACCEPTED + response.register_response.negotiated_version.major = 1 + await websocket.send(encode(response)) + heartbeat = new_envelope() + heartbeat.heartbeat.sequence = 7 + await websocket.send(encode(heartbeat)) + observed["heartbeat"] = decode(await websocket.recv()).heartbeat_ack.sequence + command_id = str(uuid4()) + command = new_envelope() + command.command.command_id = command_id + command.command.created_at.CopyFrom(command.sent_at) + command.command.request_job_snapshot.job_ids.append(str(uuid4())) + await websocket.send(encode(command)) + observed["first"] = decode(await websocket.recv()).command_ack.status + observed["snapshot"] = ( + decode(await websocket.recv()).WhichOneof("payload") + ) + duplicate = new_envelope() + duplicate.command.CopyFrom(command.command) + await websocket.send(encode(duplicate)) + observed["second"] = decode(await websocket.recv()).command_ack.status + unsupported = new_envelope() + unsupported.command.command_id = str(uuid4()) + unsupported.command.created_at.CopyFrom(unsupported.sent_at) + unsupported.command.assign_job.SetInParent() + await websocket.send(encode(unsupported)) + rejected = decode(await websocket.recv()).command_ack + observed["rejected"] = (rejected.status, rejected.error.code) + unsupported_duplicate = new_envelope() + unsupported_duplicate.command.CopyFrom(unsupported.command) + await websocket.send(encode(unsupported_duplicate)) + observed["rejected_duplicate"] = ( + decode(await websocket.recv()).command_ack.status + ) + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + token = root / "token" + token.write_text("shared-secret", encoding="utf-8") + os.chmod(token, 0o600) + async with serve(control, "127.0.0.1", 0, ping_interval=None) as server: + port = server.sockets[0].getsockname()[1] + service = ServiceConfig( + "http://local", PurePosixPath("/api"), root, + advertised_addresses=("dynamic",), + ) + config = ClientConfig( + "cache-1", "Cache 1", "cache", + f"ws://127.0.0.1:{port}", token, + root / "state.db", root / "backups", service, service, + ConnectionConfig(registration_timeout=2), + ) + probe = FilesystemProbe(root, True, True, True, True) + daemon = ArchiveClientDaemon(config, [probe, probe]) + await asyncio.to_thread(daemon.store.initialize) + await daemon._connection() + + self.assertEqual(observed["token"], "shared-secret") + self.assertEqual(observed["root_names"], ["qbittorrent", "syncthing"]) + self.assertEqual(observed["addresses"], ["dynamic"]) + self.assertEqual(observed["heartbeat"], 7) + self.assertEqual(observed["first"], control_pb2.COMMAND_ACK_STATUS_ACCEPTED) + self.assertEqual(observed["second"], control_pb2.COMMAND_ACK_STATUS_DUPLICATE) + self.assertEqual(observed["snapshot"], "client_state_snapshot") + self.assertEqual( + observed["rejected"], + ( + control_pb2.COMMAND_ACK_STATUS_REJECTED, + common_pb2.ERROR_CODE_UNSUPPORTED, + ), + ) + self.assertEqual( + observed["rejected_duplicate"], + control_pb2.COMMAND_ACK_STATUS_REJECTED, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_generated_contract.py b/tests/test_generated_contract.py new file mode 100644 index 0000000..fbcc567 --- /dev/null +++ b/tests/test_generated_contract.py @@ -0,0 +1,17 @@ +import unittest +from pathlib import Path + +from archive_clients import PROTO_COMMIT + + +class GeneratedContractTests(unittest.TestCase): + def test_all_generated_files_record_proto_commit(self): + files = list(Path("src/archive_control/v1").glob("*_pb2.py")) + files += list(Path("src/archive_control/v1").glob("*_pb2.pyi")) + self.assertEqual(len(files), 18) + for path in files: + self.assertIn(PROTO_COMMIT, path.read_text(encoding="utf-8")[:256]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_protocol_and_probes.py b/tests/test_protocol_and_probes.py new file mode 100644 index 0000000..d726a46 --- /dev/null +++ b/tests/test_protocol_and_probes.py @@ -0,0 +1,33 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from archive_clients.probes import probe_root +from archive_clients.protocol import ProtocolError, decode, encode, new_envelope + + +class ProtocolAndProbeTests(unittest.TestCase): + def test_protocol_round_trip_and_duplicate_key_rejection(self): + envelope = new_envelope() + envelope.heartbeat.sequence = 1 + self.assertEqual(decode(encode(envelope)).heartbeat.sequence, 1) + raw = json.loads(encode(envelope)) + duplicate = json.dumps(raw).replace("{", '{"messageId":"duplicate",', 1) + with self.assertRaises(ProtocolError): + decode(duplicate) + raw["messageId"] = "not-a-uuid" + with self.assertRaisesRegex(ProtocolError, "canonical UUID"): + decode(json.dumps(raw)) + + def test_filesystem_probe_cleans_up(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + result = probe_root(root) + self.assertTrue(result.readable) + self.assertTrue(result.writable) + self.assertEqual(list(root.iterdir()), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 0000000..2b7ee52 --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,28 @@ +import tempfile +import unittest +from pathlib import Path +from uuid import uuid4 + +from archive_clients.state import ClientStore, CommandConflict + + +class ClientStoreTests(unittest.TestCase): + def test_command_acceptance_is_durable_and_content_addressed(self): + with tempfile.TemporaryDirectory() as directory: + database = Path(directory) / "state.db" + store = ClientStore(database) + store.initialize() + command_id = str(uuid4()) + first = store.accept_command(command_id, '{"b":2,"a":1}', '{"ok":true}') + duplicate = ClientStore(database).accept_command( + command_id, '{"a":1,"b":2}', '{"ok":false}' + ) + self.assertFalse(first.duplicate) + self.assertTrue(duplicate.duplicate) + self.assertEqual(duplicate.acknowledgement_json, '{"ok":true}') + with self.assertRaises(CommandConflict): + store.accept_command(command_id, '{"a":2}', '{"ok":true}') + + +if __name__ == "__main__": + unittest.main()