"""RestoreJobs namespace — list and describe restore job operations."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from urllib.parse import quote
from pinecone._internal.adapters.restore_jobs_adapter import RestoreJobsAdapter
from pinecone._internal.backups_helpers import restore_job_list_params
from pinecone._internal.validation import require_non_empty
from pinecone.models.backups.list import RestoreJobList
from pinecone.models.backups.model import RestoreJobModel
if TYPE_CHECKING:
from pinecone._internal.http_client import HTTPClient
logger = logging.getLogger(__name__)
[docs]
class RestoreJobs:
"""Progress reports for restores of a backup into a new index.
:meth:`~pinecone.Pinecone.create_index_from_backup` hands back a
``restore_job_id`` and leaves the restore running in the background; this
namespace is how you follow it to completion. Reached as
``pc.restore_jobs``; not constructed directly.
A restore job is not a backup: :class:`~pinecone.client.backups.Backups`
manages the snapshots themselves, while a job here is a read-only record
of one attempt at turning a snapshot back into an index.
Examples:
>>> from pinecone import Pinecone
>>> pc = Pinecone(api_key="your-api-key")
>>> job = pc.restore_jobs.describe(job_id="rj-abc123")
>>> job.status, job.target_index_name
('Completed', 'product-search-restored')
.. seealso::
:doc:`/guides/error-handling` — the exceptions any of these methods
can raise, and which ones are worth retrying.
"""
[docs]
def __init__(self, http: HTTPClient) -> None:
self._http = http
self._adapter = RestoreJobsAdapter()
def __repr__(self) -> str:
"""Return developer-friendly representation."""
return "RestoreJobs()"
[docs]
def list(
self,
*,
limit: int | None = None,
pagination_token: str | None = None,
) -> RestoreJobList:
"""List one page of the project's restore jobs.
One call returns one page: :class:`RestoreJobList` carries a
``pagination`` token but never follows it, so iterating the return
value sees at most one page. Drive the token yourself to walk the
rest — see :doc:`/guides/pagination`. The result is a best-effort
sample rather than an inventory; the warning below says why that
matters.
Args:
limit (int | None): Maximum number of results per page. When ``None``,
the parameter is omitted and the server applies its own
default. Omitted too when *pagination_token* is given: the
token already carries the page size it was minted with, and a
different one sent alongside it would skip or repeat rows.
pagination_token (str | None): Offset token naming the next page,
taken from ``RestoreJobList.pagination.next``. A malformed or
truncated token is rejected with ``400`` (:exc:`ApiError`)
rather than restarting the listing.
Returns:
A :class:`RestoreJobList` supporting iteration, len(), and index access.
Its ``pagination`` attribute is ``None`` on the final page.
Examples:
Walk every page the server will hand out. Because pages can overlap,
the loop collects into a dict keyed by ``restore_job_id`` rather than
a list — that is the de-duplication the warning below calls for, and
it costs nothing on a listing that happens not to repeat:
.. code-block:: python
from pinecone import Pinecone
pc = Pinecone(api_key="your-api-key")
by_id = {}
page = pc.restore_jobs.list(limit=100)
while True:
for job in page:
by_id[job.restore_job_id] = job
if not (page.pagination and page.pagination.next):
break
page = pc.restore_jobs.list(pagination_token=page.pagination.next)
for job in by_id.values():
print(job.restore_job_id, job.target_index_name, job.status)
When one page is all you want:
.. code-block:: python
page = pc.restore_jobs.list(limit=5)
print(len(page))
.. warning::
**This listing can silently drop restore jobs, stop paginating
early, and repeat rows across pages.** The token stream can end
while restore jobs remain, and successive pages can overlap, so
pages are neither exhaustive nor disjoint; a restore job whose
target index has been deleted is dropped from the listing
entirely. Treat the result as a best-effort sample rather than an
inventory, never conclude a restore job does not exist from its
absence here, and de-duplicate by ``restore_job_id`` while walking
pages.
.. seealso::
:meth:`describe` — the authoritative read for a single job, by id.
"""
params: dict[str, Any] = restore_job_list_params(
limit=limit, pagination_token=pagination_token
)
logger.info("Listing restore jobs")
response = self._http.get("/restore-jobs", params=params)
result = self._adapter.to_restore_job_list(response.content)
logger.debug("Listed %d restore jobs", len(result))
return result
[docs]
def describe(self, *, job_id: str) -> RestoreJobModel:
"""Get the current state of one restore job.
Args:
job_id (str): The identifier of the restore job to describe.
Returns:
A :class:`RestoreJobModel` naming the ``backup_id`` restored and
the ``target_index_name`` it lands in. ``status`` is one of
``"Pending"``, ``"Completed"``, ``"Failed"``, or ``"Cancelled"``:
there is **no in-progress state**, so a restore that is actively
running reports ``"Pending"`` and polling for a ``"Running"``-style
value never succeeds. ``percent_complete`` and ``completed_at``
are populated only once ``status`` is ``"Completed"``, so
``percent_complete`` reports completion rather than progress and
cannot drive a progress bar.
Raises:
:exc:`PineconeValueError`: If *job_id* is empty.
:exc:`NotFoundError`: If the API answers ``404`` — which is **not**
the same as "the restore job does not exist"; see the warning
below.
Examples:
>>> from pinecone import Pinecone
>>> pc = Pinecone(api_key="your-api-key")
>>> job = pc.restore_jobs.describe(job_id="rj-abc123")
>>> job.status
'Completed'
>>> job.target_index_name
'product-search-restored'
To wait for a restore, poll until ``status`` *leaves* ``"Pending"``
rather than waiting for it to reach a running state — there is no
running state to reach. Bound the wait with a deadline so a job that
never lands stops the loop instead of spinning forever; ten minutes
below is illustrative, not a service guarantee:
.. code-block:: python
import time
deadline = time.monotonic() + 600
job = pc.restore_jobs.describe(job_id="rj-abc123")
while job.status == "Pending" and time.monotonic() < deadline:
time.sleep(5)
job = pc.restore_jobs.describe(job_id="rj-abc123")
print(job.status, job.completed_at)
.. warning::
**A ``404`` from this endpoint cannot be trusted to mean "no such
restore job".** Any failure to read the restore-job store, an
outage included, is answered with ``404``: what you see is
:exc:`NotFoundError`, and what it actually means is "could not read
this job", not "this job does not exist". Control flow keyed on it
— giving up, deleting local state, reporting the job as gone — can
each be wrong about what was really a transient failure, so treat
it as possibly transient unless you have independent evidence the
id is bad. A restore job whose target index has been deleted also
answers ``404``, under a different message, so do not match on
message text either; such a job is dropped from :meth:`list`
entirely rather than reported.
"""
require_non_empty("job_id", job_id)
logger.info("Describing restore job %r", job_id)
response = self._http.get(f"/restore-jobs/{quote(job_id, safe='')}")
result = self._adapter.to_restore_job(response.content)
logger.debug("Described restore job %r", job_id)
return result