"""ApiKeys namespace — list, create, describe, update, and delete operations."""
from __future__ import annotations
import logging
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any
from urllib.parse import quote
from pinecone._internal.adapters.admin_adapter import AdminAdapter
from pinecone._internal.validation import require_max_length, require_non_empty
from pinecone.errors.exceptions import ValidationError
from pinecone.models.admin.api_key import APIKeyList, APIKeyModel, APIKeyRole, APIKeyWithSecret
if TYPE_CHECKING:
from pinecone._internal.http_client import HTTPClient
logger = logging.getLogger(__name__)
_VALID_ROLES = {r.value for r in APIKeyRole}
def _validate_roles(roles: Sequence[APIKeyRole | str]) -> list[APIKeyRole]:
"""Validate each role and return typed enum values."""
result: list[APIKeyRole] = []
for index, role in enumerate(roles):
role_str = role.value if isinstance(role, APIKeyRole) else role
if role_str not in _VALID_ROLES:
opts = ", ".join(repr(v) for v in sorted(_VALID_ROLES))
raise ValidationError(
f"roles[{index}]: Invalid role {role_str!r}. Must be one of {opts}"
)
result.append(APIKeyRole(role_str))
return result
[docs]
class ApiKeys:
"""Operations on Pinecone API keys.
An API key is a project-scoped credential: it is the thing you pass to
:class:`~pinecone.Pinecone` to read and write indexes in one project. Where a service
account authenticates an :class:`~pinecone.Admin` client against the whole
organization, an API key reaches exactly one project. Not constructed directly — reach
it as ``admin.api_keys``.
:meth:`create` is the only call that returns a key's secret, and it returns it once.
Examples:
>>> from pinecone import Admin
>>> admin = Admin(client_id="your-client-id", client_secret="your-client-secret")
>>> for key in admin.api_keys.list(project_id="proj-abc123"):
... print(key.name, key.roles)
.. seealso::
:class:`~pinecone.admin.service_accounts.ServiceAccounts` — the organization-scoped
OAuth credentials an :class:`~pinecone.Admin` client itself uses.
:doc:`/guides/error-handling` — what each exception these calls raise means.
"""
[docs]
def __init__(self, *, http: HTTPClient) -> None:
self._http = http
self._adapter = AdminAdapter()
def __repr__(self) -> str:
"""Return developer-friendly representation."""
return "ApiKeys()"
[docs]
def list(self, *, project_id: str) -> APIKeyList:
"""List the API keys belonging to a project.
Secrets are never returned here; only :meth:`create` carries one.
Args:
project_id (str): The project's identifier, e.g. ``"proj-abc123"``.
Returns:
An :class:`APIKeyList` of every key in the project, supporting iteration,
``len()``, and index access. Returned whole — there is no paging.
Raises:
:exc:`~pinecone.errors.exceptions.PineconeValueError`: If *project_id* is empty
or whitespace-only. Checked before the request is sent.
Examples:
>>> for key in admin.api_keys.list(project_id="proj-abc123"):
... print(key.name, key.roles)
"""
require_non_empty("project_id", project_id)
logger.info("Listing API keys for project %r", project_id)
response = self._http.get(f"/admin/projects/{quote(project_id, safe='')}/api-keys")
result = self._adapter.to_api_key_list(response.content)
logger.debug("Listed %d API keys", len(result))
return result
[docs]
def create(
self,
*,
project_id: str,
name: str,
roles: Sequence[APIKeyRole | str] | None = None,
) -> APIKeyWithSecret:
"""Create an API key scoped to one project.
The response is the only place the key's secret ever appears — ``value`` is
returned here and nowhere else, and no call recovers it later. Store it before
doing anything else; if you lose it, delete the key and create another.
Args:
project_id (str): The project the key will reach, e.g. ``"proj-abc123"``.
name (str): Label for the key, e.g. ``"prod-search-key"``; 1-80 characters,
checked client-side.
roles (list[APIKeyRole | str] | None): Roles the key holds. Valid values are
``"ProjectEditor"``, ``"ProjectViewer"``, ``"ControlPlaneEditor"``,
``"ControlPlaneViewer"``, ``"DataPlaneEditor"``, and
``"DataPlaneViewer"``, either as strings or as
:class:`~pinecone.APIKeyRole` members. Defaults to ``["ProjectEditor"]``.
A role the organization is not entitled to grant is refused even though
the name is valid; see *Raises*.
Returns:
An :class:`APIKeyWithSecret` with ``value`` (the secret, this once only) and
``key`` (an :class:`APIKeyModel` carrying the key's ``id``, ``name``, and
``roles``). Pass ``value`` to :class:`~pinecone.Pinecone`; keep ``key.id`` to
reach the key again through :meth:`describe`, :meth:`update`, or
:meth:`delete`.
Raises:
:exc:`~pinecone.errors.exceptions.PineconeValueError`: If *project_id* or
*name* is empty, if *name* is longer than 80 characters, or if *roles*
contains a value that is not one of the six role names. All checked
before the request is sent.
:exc:`~pinecone.errors.exceptions.PaymentRequiredError`: If the organization's
billing state does not permit creating an API key.
:exc:`~pinecone.errors.exceptions.ForbiddenError`: Either the project has
reached its API-key quota or *roles* names a role the organization cannot
grant — the error message distinguishes the two. A full quota surfaces
here rather than as :exc:`~pinecone.errors.exceptions.RateLimitError`, so
do not retry it.
Examples:
>>> from pinecone import APIKeyRole
>>> result = admin.api_keys.create(
... project_id="proj-abc123", name="prod-search-key",
... roles=[APIKeyRole.DATA_PLANE_EDITOR]
... )
>>> result.value
'pcsk_abc123_secretvalue'
>>> result.key.roles
[<APIKeyRole.DATA_PLANE_EDITOR: 'DataPlaneEditor'>]
The secret is what the data-plane client authenticates with, so this is where
an admin workflow hands off to :class:`~pinecone.Pinecone`:
>>> from pinecone import Pinecone
>>> pc = Pinecone(api_key=result.value)
>>> for index in pc.indexes.list():
... print(index.name)
"""
require_non_empty("project_id", project_id)
require_non_empty("name", name)
require_max_length("name", name, 80)
body: dict[str, Any] = {"name": name}
if roles is not None:
body["roles"] = _validate_roles(roles)
logger.info("Creating API key %r in project %r", name, project_id)
response = self._http.post(
f"/admin/projects/{quote(project_id, safe='')}/api-keys", json=body
)
result = self._adapter.to_api_key_with_secret(response.content)
logger.debug("Created API key %r", result.key.id)
return result
[docs]
def describe(self, *, api_key_id: str) -> APIKeyModel:
"""Get one API key's metadata.
The secret is not part of it; only :meth:`create` ever returns that.
Args:
api_key_id (str): The key's identifier — ``key.id`` from :meth:`create` or
:meth:`list`, e.g. ``"key-abc123"``. This is not the secret.
Returns:
An :class:`APIKeyModel` with the key's ``id``, ``name``, ``project_id``, and
``roles``.
Raises:
:exc:`~pinecone.errors.exceptions.PineconeValueError`: If *api_key_id* is empty
or whitespace-only. Checked before the request is sent.
Examples:
>>> key = admin.api_keys.describe(api_key_id="key-abc123")
>>> key.name
'prod-search-key'
>>> key.roles
[<APIKeyRole.DATA_PLANE_EDITOR: 'DataPlaneEditor'>]
"""
require_non_empty("api_key_id", api_key_id)
logger.info("Describing API key %r", api_key_id)
response = self._http.get(f"/admin/api-keys/{quote(api_key_id, safe='')}")
result = self._adapter.to_api_key(response.content)
logger.debug("Described API key %r", api_key_id)
return result
[docs]
def update(
self,
*,
api_key_id: str,
name: str | None = None,
roles: Sequence[APIKeyRole | str] | None = None,
) -> APIKeyModel:
"""Change an API key's name or roles.
Omitted arguments are left alone, but *roles* is not merged: passing it replaces
the whole role set, so include every role the key should keep. The secret does not
change, so callers holding it keep working under the new roles.
Args:
api_key_id (str): The key's identifier, e.g. ``"key-abc123"``. Left unchanged
by this call.
name (str | None): New label for the key, e.g. ``"prod-search-key-v2"``. Left
unchanged if omitted. Unlike :meth:`create`, the length limit is not
checked client-side — an over-long name is rejected by the server.
roles (list[APIKeyRole | str] | None): The key's complete new role set, from
the same six values :meth:`create` accepts. Left unchanged if omitted, and
subject to the same entitlement restriction.
Returns:
An :class:`APIKeyModel` reflecting the stored state after the change.
Raises:
:exc:`~pinecone.errors.exceptions.PineconeValueError`: If *api_key_id* is empty
or whitespace-only, or if *roles* contains a value that is not one of the
six role names. Both checked before the request is sent.
:exc:`~pinecone.errors.exceptions.ForbiddenError`: If *roles* names a role the
organization cannot grant. Unlike :meth:`create`, no API-key quota applies
here — the key already exists.
Examples:
>>> key = admin.api_keys.update(
... api_key_id="key-abc123", roles=["DataPlaneEditor", "DataPlaneViewer"]
... )
"""
require_non_empty("api_key_id", api_key_id)
body: dict[str, Any] = {}
if name is not None:
body["name"] = name
if roles is not None:
body["roles"] = _validate_roles(roles)
logger.info("Updating API key %r", api_key_id)
response = self._http.patch(f"/admin/api-keys/{quote(api_key_id, safe='')}", json=body)
result = self._adapter.to_api_key(response.content)
logger.debug("Updated API key %r", api_key_id)
return result
[docs]
def delete(self, *, api_key_id: str) -> None:
"""Delete an API key permanently.
Anything still authenticating with the key's secret starts failing, and there is
no way to restore it — a replacement is a new :meth:`create` with a new secret.
Args:
api_key_id (str): The key's identifier, e.g. ``"key-abc123"``, not the secret.
Raises:
:exc:`~pinecone.errors.exceptions.PineconeValueError`: If *api_key_id* is empty
or whitespace-only. Checked before the request is sent.
Examples:
>>> admin.api_keys.delete(api_key_id="key-abc123")
"""
require_non_empty("api_key_id", api_key_id)
logger.info("Deleting API key %r", api_key_id)
self._http.delete(f"/admin/api-keys/{quote(api_key_id, safe='')}")
logger.debug("Deleted API key %r", api_key_id)