diff --git a/.rio b/.rio new file mode 100644 index 00000000..06d4ef5d --- /dev/null +++ b/.rio @@ -0,0 +1 @@ +{"configtrees": {"project-da78q6sqp38c73f8ikhg": {"test1": {"rev_id": "rev-da78rjdugeis739lu3m0", "committed": true}}}} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index dfb88593..2df99277 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dependencies = [ "waiting>=1.4.1", "yaspin==2.5.0", "ansible-core>=2.13.13", - "rapyuta-io-sdk-v2 @ git+https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2@feat/managed-database-phase-3", + "rapyuta-io-sdk-v2 @ git+https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2@feat/managed-database-phase-4", "cryptography>=40.0.0", "typing-extensions>=4.15.0", "python-benedict==0.30", @@ -204,3 +204,5 @@ filterwarnings = [ "ignore::PendingDeprecationWarning", ] +[tool.uv.sources] +rapyuta-io-sdk-v2 = { git = "https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2", branch = "feat/managed-database-phase-4" } diff --git a/riocli/apply/manifests/restore.yaml b/riocli/apply/manifests/restore.yaml new file mode 100644 index 00000000..15a42d94 --- /dev/null +++ b/riocli/apply/manifests/restore.yaml @@ -0,0 +1,48 @@ +apiVersion: "api.rapyuta.io/v2" +kind: "Restore" +metadata: + name: "orders-db-restore" + labels: + app: orders +spec: + # Target database. It must already exist and be Running: a restore loads into + # the live instance, it never creates one. A second restore against the same + # database is refused while one is still in flight. + database: orders-db + source: + type: backup # Supported: backup, dataDirectory + # --- type: backup --- + # The archive to restore, by file-upload GUID or filename. A backup produces + # many archives over its life, so the one to load is named directly. + # `rio device uploads list ` shows both columns. + fileUpload: fileupload-d9upialugeis73e1to40 + # Both are derived from the upload's own metadata and can be omitted. Set + # them only to override: backupName selects the restore image's major + # version, and the pair is what lets the device find the backup in its own + # barman store instead of downloading the archive back. + # backupName: orders-nightly + # backupRunID: "20260101T020000" + # Point in time to roll forward to. Honoured only when the device still + # holds this backup in its barman store, which keeps the WAL archive + # alongside it; an uploaded archive is a fixed point and the device refuses + # the request rather than restoring to the wrong one. + # targetTime: "2026-01-01T02:00:00Z" + # + # --- type: dataDirectory --- + # A major-version migration is expressed as a restore from the old cluster's + # data directory, which is the database's dataDirectory with the major + # version appended. Must be absolute: Docker reads a relative path as a + # named volume, so the restore would run against an empty directory. The old + # cluster is copied, never opened in place. + # oldDataDirectory: /opt/rapyuta/volumes/orders-db/17 + # The old cluster's major version. It resolves the restore image, and the + # image cross-checks it against the directory's PG_VERSION. + # sourceVersion: "17" + # Logical databases to load. Omit to restore every one the source holds; + # anything outside this list is left untouched in the target. + databases: + - orders + options: + clean: true # Drop the objects being restored before recreating them + noOwner: true # Restore without the source's ownership + ifExists: true # Tolerate absent objects; only meaningful with clean diff --git a/riocli/apply/util.py b/riocli/apply/util.py index b4c2e80c..c58e3fa6 100644 --- a/riocli/apply/util.py +++ b/riocli/apply/util.py @@ -29,6 +29,7 @@ from riocli.constants import Colors from riocli.constants.symbols import Symbols from riocli.database.model import Database +from riocli.database.restore.model import Restore from riocli.deployment.model import Deployment from riocli.device.model import Device from riocli.disk.model import Disk @@ -46,6 +47,7 @@ KIND_TO_CLASS = { "database": Database, "backup": Backup, + "restore": Restore, "deployment": Deployment, "device": Device, "disk": Disk, diff --git a/riocli/backup/util.py b/riocli/backup/util.py index cfd78a8b..0b5a190e 100644 --- a/riocli/backup/util.py +++ b/riocli/backup/util.py @@ -43,11 +43,23 @@ def fetch_backups( def display_backup_list(backups: typing.Any, show_header: bool = True): headers = [] if show_header: - headers = ("GUID", "Name", "Type", "Database", "Schedule", "Phase") + headers = ( + "GUID", + "Name", + "Type", + "Database", + "Schedule", + "Phase", + "Step", + ) data = [] for backup in backups: - phase = getattr(backup.status, "phase", None) if backup.status else None + status = backup.status + phase = getattr(status, "phase", None) if status else None + # The recover dominates a run, so the phase alone cannot tell a slow + # backup from a stuck one. + step = getattr(status, "step", None) if status else None data.append( [ backup.metadata.guid, @@ -56,6 +68,7 @@ def display_backup_list(backups: typing.Any, show_header: bool = True): backup.spec.database, backup.spec.schedule or "-", phase or "Unknown", + step or "-", ] ) diff --git a/riocli/database/__init__.py b/riocli/database/__init__.py index 4aa28f28..043c0679 100644 --- a/riocli/database/__init__.py +++ b/riocli/database/__init__.py @@ -18,6 +18,8 @@ from riocli.database.delete import delete_database from riocli.database.inspect import inspect_database from riocli.database.list import list_databases +from riocli.database.restore import restore +from riocli.database.upload import upload from riocli.utils import AliasedGroup @@ -30,7 +32,8 @@ def database() -> None: """Manage PostgreSQL databases. - Create, list, inspect, and delete managed databases. + Create, list, inspect, and delete managed databases, and restore data + into a running one with ``rio database restore``. Use ``rio apply`` to create or update databases from a manifest file. """ pass @@ -39,3 +42,5 @@ def database() -> None: database.add_command(list_databases) database.add_command(inspect_database) database.add_command(delete_database) +database.add_command(restore) +database.add_command(upload) diff --git a/riocli/database/restore/__init__.py b/riocli/database/restore/__init__.py new file mode 100644 index 00000000..9c746567 --- /dev/null +++ b/riocli/database/restore/__init__.py @@ -0,0 +1,49 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from riocli.constants import Colors +from riocli.database.restore.create import create_restore +from riocli.database.restore.inspect import inspect_restore +from riocli.database.restore.list import list_restores +from riocli.utils import AliasedGroup + + +@click.group( + invoke_without_command=False, + cls=AliasedGroup, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +def restore() -> None: + """Restore data into a running database. + + A restore loads logical databases into a live, running database, from either + a backup or an old on-device data directory. The latter is how a + major-version migration is done: create the new database, then restore into + it from the old data directory. + + A restore never creates a database, and never touches logical databases + outside the ones it is given. Restores are sub-resources of a database, so + every command takes the target with --database. + + Use ``rio apply`` to run a restore from a manifest file. + """ + pass + + +restore.add_command(list_restores) +restore.add_command(inspect_restore) +restore.add_command(create_restore) diff --git a/riocli/database/restore/create.py b/riocli/database/restore/create.py new file mode 100644 index 00000000..ef125855 --- /dev/null +++ b/riocli/database/restore/create.py @@ -0,0 +1,217 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsCommand + +from riocli.config import new_v2_client +from riocli.constants import Colors, Symbols +from riocli.database.restore.util import display_restore_list + + +@click.command( + "create", + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option( + "--database", + "-d", + "database", + type=click.STRING, + required=True, + help="Target database. Must be running; the data is loaded into it", +) +@click.option( + "--source", + "-s", + "source_type", + type=click.Choice(["backup", "dataDirectory"], case_sensitive=False), + default="backup", + help="Where to restore from. Defaults to backup", +) +@click.option( + "--file-upload", + "-u", + "file_upload", + type=click.STRING, + default=None, + help="Uploaded archive to restore: its file-upload GUID or filename " + "(required when --source is backup). See `rio device uploads list`", +) +@click.option( + "--backup", + "-b", + "backup_name", + type=click.STRING, + default=None, + help="Source backup name, recorded as provenance", +) +@click.option( + "--backup-run", + "backup_run_id", + type=click.STRING, + default=None, + help="Barman backup ID to restore. Defaults to the backup's latest run", +) +@click.option( + "--old-data-directory", + "old_data_directory", + type=click.STRING, + default=None, + help="Absolute path of the old cluster (required when --source is dataDirectory)", +) +@click.option( + "--source-version", + "source_version", + type=click.Choice(["16", "17", "18"]), + default=None, + help="Major version of the old cluster (required when --source is dataDirectory)", +) +@click.option( + "--db", + "databases", + multiple=True, + type=click.STRING, + default=(), + help="Logical database to restore. Repeatable. Defaults to every one in the source", +) +@click.option( + "--clean", + is_flag=True, + default=False, + help="Drop the objects being restored before recreating them", +) +@click.option( + "--no-owner", + "no_owner", + is_flag=True, + default=False, + help="Skip restoring object ownership", +) +@click.option( + "--if-exists", + "if_exists", + is_flag=True, + default=False, + help="Tolerate objects that are already absent. Only meaningful with --clean", +) +@click.argument("restore-name", type=str) +def create_restore( + database: str, + source_type: str, + file_upload: str, + backup_name: str, + backup_run_id: str, + old_data_directory: str, + source_version: str, + databases: list[str], + clean: bool, + no_owner: bool, + if_exists: bool, + restore_name: str, +) -> None: + """Restore data into a running database. + + Only the logical databases you name are touched; everything else in the + target is left as it was. A restore is one-shot and is refused while another + is still running against the same database. + + Usage Examples: + + Restore one logical database from an uploaded archive + + $ rio database restore create orders-restore -d orders-db \\ + -u orders_20260101T020000.tar.gz --db orders + + Restore every logical database the archive holds + + $ rio database restore create orders-restore -d orders-db -u fileupload-abc123 + + Migrate a v17 cluster into a new v18 database + + $ rio database restore create orders-migrate -d orders-db-v18 \\ + --source dataDirectory \\ + --old-data-directory /opt/rapyuta/volumes/orders-db \\ + --source-version 17 + """ + source = {"type": source_type} + + if source_type == "backup": + if not file_upload: + click.secho( + "--file-upload is required when --source is backup", fg=Colors.RED + ) + raise SystemExit(1) + + source["fileUpload"] = file_upload + + # Provenance: neither resolves the archive, both make the record readable. + if backup_name: + source["backupName"] = backup_name + if backup_run_id: + source["backupRunID"] = backup_run_id + else: + if not old_data_directory or not source_version: + click.secho( + "--old-data-directory and --source-version are required when " + "--source is dataDirectory", + fg=Colors.RED, + ) + raise SystemExit(1) + + source["oldDataDirectory"] = old_data_directory + source["sourceVersion"] = source_version + + spec = {"database": database, "source": source} + + if databases: + spec["databases"] = list(databases) + + options = {} + if clean: + options["clean"] = True + if no_owner: + options["noOwner"] = True + # pg_restore errors on --if-exists without --clean, so the device drops it; + # say so here rather than letting it silently do nothing. + if if_exists: + if not clean: + click.secho("--if-exists has no effect without --clean", fg=Colors.YELLOW) + else: + options["ifExists"] = True + + if options: + spec["options"] = options + + body = { + "apiVersion": "api.rapyuta.io/v2", + "kind": "Restore", + "metadata": {"name": restore_name}, + "spec": spec, + } + + try: + client = new_v2_client() + restore = client.create_restore(body=body) + click.secho(f"{Symbols.SUCCESS} Restore started", fg=Colors.GREEN) + display_restore_list([restore], show_header=True) + click.secho( + f"\nFollow it with: rio database restore inspect {restore_name} -d {database}", + fg=Colors.YELLOW, + ) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e diff --git a/riocli/database/restore/inspect.py b/riocli/database/restore/inspect.py new file mode 100644 index 00000000..f4d49113 --- /dev/null +++ b/riocli/database/restore/inspect.py @@ -0,0 +1,66 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsCommand + +from riocli.config import new_v2_client +from riocli.constants import Colors +from riocli.utils import inspect_with_format + + +@click.command( + "inspect", + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option( + "--database", + "-d", + "database", + type=click.STRING, + required=True, + help="Database the restore belongs to", +) +@click.option( + "--format", + "-f", + "format_type", + default="yaml", + type=click.Choice(["json", "yaml"], case_sensitive=False), +) +@click.argument("restore-name", type=str) +def inspect_restore(database: str, format_type: str, restore_name: str) -> None: + """Inspect a restore by its name. + + The status carries the outcome the device reported: the phase, the failure + message with a log tail when it failed, and the logical databases that were + actually loaded. + + Usage Examples: + + $ rio database restore inspect orders-db-restore --database orders-db + + $ rio database restore inspect orders-db-restore -d orders-db --format json + """ + try: + client = new_v2_client() + restore = client.get_restore(database=database, name=restore_name) + inspect_with_format( + restore.model_dump(exclude_none=True, by_alias=True), format_type + ) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e diff --git a/riocli/database/restore/list.py b/riocli/database/restore/list.py new file mode 100644 index 00000000..d5dc96c8 --- /dev/null +++ b/riocli/database/restore/list.py @@ -0,0 +1,56 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsCommand +from rapyuta_io_sdk_v2 import walk_pages + +from riocli.config import new_v2_client +from riocli.constants import Colors +from riocli.database.restore.util import display_restore_list + + +@click.command( + "list", + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option( + "--database", + "-d", + "database", + type=click.STRING, + required=True, + help="Database whose restores to list", +) +def list_restores(database: str) -> None: + """List a database's restores. + + Restores are kept after they finish, so this is the database's restore + history: what was restored, from where, and whether it landed. + + Usage Examples: + + $ rio database restore list --database orders-db + """ + try: + client = new_v2_client(with_project=True) + restores = [] + for page in walk_pages(client.list_restores, database=database): + restores.extend(page) + display_restore_list(restores, show_header=True) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e diff --git a/riocli/database/restore/model.py b/riocli/database/restore/model.py new file mode 100644 index 00000000..003c6f95 --- /dev/null +++ b/riocli/database/restore/model.py @@ -0,0 +1,58 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rapyuta_io_sdk_v2 import Client as v2Client +from rapyuta_io_sdk_v2 import Restore as RestoreModel +from typing_extensions import override + +from riocli.model import Model + + +class Restore(Model): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.update(*args, **kwargs) + self._obj = RestoreModel.model_validate(self) + + @override + def create_object(self, v2_client: v2Client, *args, **kwargs) -> RestoreModel: + return v2_client.create_restore(body=self._obj) # pyright:ignore[reportArgumentType] + + @override + def update_object(self, v2_client: v2Client, *args, **kwargs) -> RestoreModel: + # A restore is a one-shot operation: its spec is immutable and the API + # exposes no update. Re-applying an existing restore does not re-run it. + raise NotImplementedError + + @override + def delete_object(self, v2_client: v2Client, *args, **kwargs) -> None: + # A no-op, not NotImplementedError. The API exposes no restore delete: a + # restore runs to a terminal phase and stays as an audit record, and + # deleting the target database is what stops one still in flight. Model + # .delete() only catches HttpNotFoundError, so raising here aborts + # `rio delete -f` for every other resource in the same bundle. + return None + + @override + def list_dependencies(self) -> list[str] | None: + # The target database must exist and be running before a restore can run. + deps = [f"database:{self._obj.spec.database}"] + + # A named backup is ordering information only — the archive is addressed + # by its own GUID or filename, and a dataDirectory source is a path on + # the device with no manifest to depend on. + if self._obj.spec.source.type == "backup" and self._obj.spec.source.backup_name: + deps.append(f"backup:{self._obj.spec.source.backup_name}") + + return deps diff --git a/riocli/database/restore/util.py b/riocli/database/restore/util.py new file mode 100644 index 00000000..7921eeac --- /dev/null +++ b/riocli/database/restore/util.py @@ -0,0 +1,91 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import typing + +from rapyuta_io_sdk_v2 import Client, walk_pages + +from riocli.utils import tabulate_data + + +def fetch_restores( + client: Client, + database: str, + restore_name_or_regex: str, + include_all: bool, +) -> list: + restores = [] + for page in walk_pages(client.list_restores, database=database): + restores.extend(page) + + if include_all: + return restores + + result = [] + for restore in restores: + if re.search(restore_name_or_regex, restore.metadata.name): + result.append(restore) + + return result + + +def display_restore_list(restores: typing.Any, show_header: bool = True): + headers = [] + if show_header: + headers = ( + "GUID", + "Name", + "Source", + "Databases", + "Phase", + "Step", + "Restored", + ) + + data = [] + for restore in restores: + status = restore.status + # The phase and the restored list come from the device's result pointer, + # which is the only record of what actually landed. + phase = getattr(status, "phase", None) if status else None + restored = getattr(status, "restored_databases", None) if status else None + # A restore holds one phase for minutes, so the step is what separates + # progress from a stall. + step = getattr(status, "step", None) if status else None + requested = restore.spec.databases + + data.append( + [ + restore.metadata.guid, + restore.metadata.name, + _source_summary(restore.spec.source), + ", ".join(requested) if requested else "all", + phase or "Unknown", + step or "-", + ", ".join(restored) if restored else "-", + ] + ) + + tabulate_data(data, headers) + + +def _source_summary(source: typing.Any) -> str: + """Describe where a restore reads from, in one column.""" + if source.type == "dataDirectory": + return f"dataDirectory: {source.old_data_directory}" + + # The archive is what the restore actually reads, so it is what the column + # shows; the backup name is provenance and may be absent. + return f"backup: {source.file_upload}" diff --git a/riocli/database/upload/__init__.py b/riocli/database/upload/__init__.py new file mode 100644 index 00000000..222c593e --- /dev/null +++ b/riocli/database/upload/__init__.py @@ -0,0 +1,40 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click + +from riocli.constants import Colors +from riocli.database.upload.delete import delete_upload +from riocli.database.upload.list import list_uploads +from riocli.utils import AliasedGroup + + +@click.group( + invoke_without_command=False, + cls=AliasedGroup, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +def upload() -> None: + """Uploaded backup archives of a database. + + An archive is in object storage, not on the device that wrote it, so these + are listed per database rather than per device. They outlive both the + uploading device and the Backup record that produced them. + """ + pass + + +upload.add_command(list_uploads) +upload.add_command(delete_upload) diff --git a/riocli/database/upload/delete.py b/riocli/database/upload/delete.py new file mode 100644 index 00000000..1bb456d8 --- /dev/null +++ b/riocli/database/upload/delete.py @@ -0,0 +1,63 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsCommand + +from riocli.config import new_v2_client +from riocli.constants import Colors, Symbols +from riocli.utils.spinner import with_spinner + + +@click.command( + "delete", + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.option( + "--force", + "-f", + "--silent", + "force", + is_flag=True, + default=False, + help="Skip confirmation", +) +@click.argument("database", type=click.STRING) +@click.argument("upload-guid", type=click.STRING) +@with_spinner(text="Deleting archive...") +def delete_upload(database: str, upload_guid: str, force: bool, spinner=None) -> None: + """Delete one uploaded backup archive of a database. + + Archives outlive their backup and their database, so this is the only thing + that removes one. The blob goes with it. + + Usage Examples: + + $ rio database upload delete orders-db fileupload-abc123 + """ + with spinner.hidden(): + if not force: + click.confirm(f"Delete archive {upload_guid} of {database}?", abort=True) + + try: + client = new_v2_client(with_project=True) + client.delete_database_upload(database=database, guid=upload_guid) + spinner.text = click.style("Archive deleted successfully.", fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) + except Exception as e: + spinner.text = click.style(str(e), fg=Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e diff --git a/riocli/database/upload/list.py b/riocli/database/upload/list.py new file mode 100644 index 00000000..6c2bd8d8 --- /dev/null +++ b/riocli/database/upload/list.py @@ -0,0 +1,50 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +from click_help_colors import HelpColorsCommand +from rapyuta_io_sdk_v2 import walk_pages + +from riocli.config import new_v2_client +from riocli.constants import Colors +from riocli.database.upload.util import display_archive_list + + +@click.command( + "list", + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, +) +@click.argument("database", type=click.STRING) +def list_uploads(database: str) -> None: + """List a database's uploaded backup archives. + + The Upload ID is what ``rio database restore create --file-upload`` takes. + Archives are found by the database they belong to, so they remain listed + after the uploading device is deleted. + + Usage Examples: + + $ rio database upload list orders-db + """ + try: + client = new_v2_client(with_project=True) + archives = [] + for page in walk_pages(client.list_database_uploads, database=database): + archives.extend(page) + display_archive_list(archives, show_header=True) + except Exception as e: + click.secho(str(e), fg=Colors.RED) + raise SystemExit(1) from e diff --git a/riocli/database/upload/util.py b/riocli/database/upload/util.py new file mode 100644 index 00000000..55370eed --- /dev/null +++ b/riocli/database/upload/util.py @@ -0,0 +1,39 @@ +# Copyright 2025 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from rapyuta_io_sdk_v2.models import BackupArchive + +from riocli.utils import tabulate_data + + +def display_archive_list(archives: list[BackupArchive], show_header: bool = True) -> None: + headers = [] + if show_header: + headers = ("Upload ID", "Filename", "Backup", "Run", "Status", "Size") + + data = [] + for a in archives: + data.append( + [ + a.guid, + a.filename or "-", + a.backup_name or "-", + a.backup_run_id or "-", + a.status or "-", + a.total_size if a.total_size is not None else "-", + ] + ) + + tabulate_data(data, headers) diff --git a/tests/unit/test_backup_list.py b/tests/unit/test_backup_list.py new file mode 100644 index 00000000..c5c590b9 --- /dev/null +++ b/tests/unit/test_backup_list.py @@ -0,0 +1,54 @@ +# Copyright 2026 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from rapyuta_io_sdk_v2 import Backup + +from riocli.backup.util import display_backup_list + + +def _backup(status: dict | None) -> Backup: + return Backup.model_validate( + { + "apiVersion": "api.rapyuta.io/v2", + "kind": "Backup", + "metadata": {"name": "orders-nightly", "guid": "backup-aaaaaaaaaaaaaaaaaa"}, + "spec": { + "type": "scheduled", + "database": "orders-db", + "schedule": "0 2 * * *", + }, + "status": status, + } + ) + + +def test_step_is_shown(capsys): + backup = _backup({"phase": "Ready", "step": "archiving base backup"}) + + display_backup_list([backup]) + out = capsys.readouterr().out + + # The recover dominates a run, so the step is what separates a slow backup + # from a stuck one. + assert "archiving base backup" in out + assert "Ready" in out + + +def test_missing_status_does_not_break_the_table(capsys): + display_backup_list([_backup(None)]) + out = capsys.readouterr().out + + assert "Unknown" in out diff --git a/tests/unit/test_restore_util.py b/tests/unit/test_restore_util.py new file mode 100644 index 00000000..2c744094 --- /dev/null +++ b/tests/unit/test_restore_util.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from rapyuta_io_sdk_v2 import Restore + +from riocli.database.restore.util import _source_summary, display_restore_list + + +def _restore(source: dict, status: dict | None = None) -> Restore: + return Restore.model_validate( + { + "apiVersion": "api.rapyuta.io/v2", + "kind": "Restore", + "metadata": {"name": "orders-restore", "guid": "restore-aaaaaaaaaaaaaaaa"}, + "spec": {"database": "orders-db", "source": source}, + "status": status, + } + ) + + +def test_source_summary_backup_names_the_archive(): + # The archive is what the restore actually reads, so it is what is shown — + # not the backup name, which is provenance and may be absent. + r = _restore( + { + "type": "backup", + "fileUpload": "orders_20260101T020000.tar.gz", + "backupName": "orders-nightly", + } + ) + assert _source_summary(r.spec.source) == "backup: orders_20260101T020000.tar.gz" + + +def test_source_summary_backup_by_guid(): + r = _restore({"type": "backup", "fileUpload": "fileupload-abc123"}) + assert _source_summary(r.spec.source) == "backup: fileupload-abc123" + + +def test_source_summary_data_directory(): + # A migration's source is a path on the device, not a platform resource. + r = _restore( + { + "type": "dataDirectory", + "oldDataDirectory": "/opt/rapyuta/volumes/orders-db/17", + "sourceVersion": "17", + } + ) + assert ( + _source_summary(r.spec.source) + == "dataDirectory: /opt/rapyuta/volumes/orders-db/17" + ) + + +def test_step_is_shown_alongside_the_phase(capsys): + # A restore sits in Running for minutes, so the phase alone cannot tell + # progress from a stall. + r = _restore( + {"type": "backup", "fileUpload": "fileupload-abc123"}, + {"phase": "Running", "step": "loading orders"}, + ) + + display_restore_list([r]) + out = capsys.readouterr().out + + assert "Running" in out + assert "loading orders" in out + + +def test_missing_status_does_not_break_the_table(capsys): + display_restore_list([_restore({"type": "backup", "fileUpload": "fileupload-x"})]) + assert "Unknown" in capsys.readouterr().out + + +def test_delete_object_is_a_noop_not_an_exception(): + # Model.delete() catches only HttpNotFoundError, so a NotImplementedError here + # aborts `rio delete -f` for every other resource in the same bundle. A + # restore is an audit record with no delete route: skipping it is correct. + from riocli.database.restore.model import Restore as RestoreResource + + r = RestoreResource( + { + "apiVersion": "api.rapyuta.io/v2", + "kind": "Restore", + "metadata": {"name": "orders-restore"}, + "spec": { + "database": "orders-db", + "source": {"type": "backup", "fileUpload": "fileupload-abc"}, + }, + } + ) + + assert r.delete_object(v2_client=None) is None diff --git a/uv.lock b/uv.lock index eac44bd1..9e181a58 100644 --- a/uv.lock +++ b/uv.lock @@ -660,59 +660,59 @@ toml = [ [[package]] name = "cryptography" -version = "50.0.0" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, - { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, - { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, - { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, - { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, - { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, - { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, - { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, ] [[package]] @@ -798,7 +798,7 @@ version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "futurist", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "futurist", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "futurist", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pbr" }, { name = "requests" }, ] @@ -875,7 +875,7 @@ wheels = [ [[package]] name = "futurist" -version = "3.4.0" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", @@ -884,9 +884,9 @@ resolution-markers = [ dependencies = [ { name = "debtcollector", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/c3/d6df974b3b488606a1064b02094e90ba8a35efa8072ab403cca2b9e3c51d/futurist-3.4.0.tar.gz", hash = "sha256:ed00c6f4c815cce9549157e8ec28624b2f5ec83f9577b0dbe54f7516137d43d3", size = 52282, upload-time = "2026-06-26T13:33:57.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/82/eb917bf6aa03197ac5bb3ce8a95bee2243a938426ec60747a52cbed9344e/futurist-3.5.0.tar.gz", hash = "sha256:97d01f6144bbe3abe1b2155cb2b42dab4ec33e09f4f7f7df7569f35d9a29aaf7", size = 61471, upload-time = "2026-08-20T15:20:26.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/51/96f2ef5038dbd9db345b26b5850c1ad52131e42d16ec6ae6ed8eb2fcada4/futurist-3.4.0-py3-none-any.whl", hash = "sha256:a4c16e5522c0d9726e1f8ee2129b91b5593d054949ea3799f256241bd241bed5", size = 43051, upload-time = "2026-06-26T13:33:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/14c24c98b7ab804bef1bdcbbe0b6f11510366b98faba34a9cedefa309582/futurist-3.5.0-py3-none-any.whl", hash = "sha256:77c37e074fb94e2c7cc748b1b711ab4b998bf30aa76c91fbaed0206d14dd8a96", size = 50730, upload-time = "2026-08-20T15:20:25.783Z" }, ] [[package]] @@ -946,20 +946,20 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] name = "imagesize" -version = "2.0.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, ] [[package]] @@ -1348,11 +1348,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] @@ -1727,7 +1727,7 @@ requires-dist = [ { name = "pytz" }, { name = "pyyaml", specifier = ">=5.4.1" }, { name = "rapyuta-io", specifier = ">=3.1.0" }, - { name = "rapyuta-io-sdk-v2", git = "https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2?rev=feat%2Fmanaged-database-phase-3" }, + { name = "rapyuta-io-sdk-v2", git = "https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2?branch=feat%2Fmanaged-database-phase-4" }, { name = "requests", specifier = ">=2.20.0" }, { name = "semver", specifier = ">=3.0.0" }, { name = "setuptools" }, @@ -1755,7 +1755,7 @@ dev = [ [[package]] name = "rapyuta-io-sdk-v2" version = "0.3.0" -source = { git = "https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2?rev=feat%2Fmanaged-database-phase-3#282dab07c27c15b5148bfa8a617c5db463f463e4" } +source = { git = "https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2?branch=feat%2Fmanaged-database-phase-4#667bdd4d12e470f452b14bbf42d8913e09d5dfda" } dependencies = [ { name = "httpx" }, { name = "pydantic-settings" },