Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/bentoml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"use_arguments": "._internal.utils.args:use_arguments",
"Bento": "._internal.bento:Bento",
"BentoCloudClient": "._internal.cloud:BentoCloudClient",
"ApiToken": "._internal.cloud.api_token:ApiToken",
"ApiTokenAPI": "._internal.cloud.api_token:ApiTokenAPI",
"Context": "._internal.context:ServiceContext",
"server_context": "._internal.context:server_context",
"Model": "._internal.models:Model",
Expand Down Expand Up @@ -82,6 +84,8 @@
# BentoML built-in types
from ._internal.bento import Bento
from ._internal.cloud import BentoCloudClient
from ._internal.cloud.api_token import ApiToken
from ._internal.cloud.api_token import ApiTokenAPI
from ._internal.configuration import load_config
from ._internal.configuration import save_config
from ._internal.configuration import set_serialization_strategy
Expand Down Expand Up @@ -139,6 +143,7 @@
from . import monitoring # Monitoring API
from . import cloud # Cloud API
from . import deployment # deployment API
from . import api_token # API token management
from . import validators # validators

# isort: on
Expand Down Expand Up @@ -290,6 +295,7 @@
monitoring = _LazyLoader("bentoml.monitoring", globals(), "bentoml.monitoring")
cloud = _LazyLoader("bentoml.cloud", globals(), "bentoml.cloud")
deployment = _LazyLoader("bentoml.deployment", globals(), "bentoml.deployment")
api_token = _LazyLoader("bentoml.api_token", globals(), "bentoml.api_token")
validators = _LazyLoader("bentoml.validators", globals(), "bentoml.validators")
del _LazyLoader, FrameworkImporter

Expand Down Expand Up @@ -332,6 +338,8 @@ def __getattr__(name: str) -> Any:
"Model",
"monitoring",
"BentoCloudClient", # BentoCloud REST API Client
"ApiToken",
"ApiTokenAPI",
# bento APIs
"list",
"get",
Expand Down Expand Up @@ -371,6 +379,7 @@ def __getattr__(name: str) -> Any:
"gradio",
"cloud",
"deployment",
"api_token",
"triton",
"monitor",
"load_config",
Expand Down
5 changes: 5 additions & 0 deletions src/bentoml/_internal/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from bentoml._internal.cloud.client import RestApiClient

from .api_token import ApiTokenAPI
from .base import Spinner
from .bento import BentoAPI
from .config import DEFAULT_ENDPOINT
Expand All @@ -29,13 +30,15 @@ class BentoCloudClient:
model: Model API
deployment: Deployment API
secret: Secret API
api_token: API Token API
"""

client: RestApiClient
bento: BentoAPI
model: ModelAPI
deployment: DeploymentAPI
secret: SecretAPI
api_token: ApiTokenAPI

def __init__(
self,
Expand All @@ -57,13 +60,15 @@ def __init__(
model = ModelAPI(client, spinner=spinner)
deployment = DeploymentAPI(client)
secret = SecretAPI(client)
api_token = ApiTokenAPI(client)

self.__attrs_init__(
client=client,
bento=bento,
model=model,
deployment=deployment,
secret=secret,
api_token=api_token,
)

@classmethod
Expand Down
120 changes: 120 additions & 0 deletions src/bentoml/_internal/cloud/api_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from __future__ import annotations

import typing as t
from datetime import datetime

import attr
import yaml

from ..utils.cattr import bentoml_cattr
from .schemas.schemasv1 import ApiTokenSchema
from .schemas.schemasv1 import CreateApiTokenSchema

if t.TYPE_CHECKING:
from .client import RestApiClient


@attr.define(kw_only=True)
class ApiToken(ApiTokenSchema):
created_by: str = attr.field(default="")

def to_dict(self) -> dict[str, t.Any]:
return bentoml_cattr.unstructure(self)

def to_yaml(self) -> str:
dt = self.to_dict()
return yaml.dump(dt, sort_keys=False)

@classmethod
def from_schema(cls, schema: ApiTokenSchema) -> ApiToken:
return cls(
name=schema.name,
uid=schema.uid,
resource_type=schema.resource_type,
labels=schema.labels,
created_at=schema.created_at,
updated_at=schema.updated_at,
deleted_at=schema.deleted_at,
description=schema.description,
scopes=schema.scopes,
user=schema.user,
organization=schema.organization,
expired_at=schema.expired_at,
last_used_at=schema.last_used_at,
is_expired=schema.is_expired,
is_api_token=schema.is_api_token,
is_organization_token=schema.is_organization_token,
is_global_access=schema.is_global_access,
token=schema.token,
created_by=schema.user.name,
)


@attr.define
class ApiTokenAPI:
_client: RestApiClient

def list(self, search: str | None = None) -> t.List[ApiToken]:
"""
List all API tokens.

Args:
search (str | None): Optional search string to filter tokens.

Returns:
List[ApiToken]: A list of ApiToken objects.
"""
tokens = self._client.v1.list_api_tokens(search=search)
return [ApiToken.from_schema(token) for token in tokens.items]

def create(
self,
name: str,
description: str | None = None,
scopes: t.List[str] | None = None,
expired_at: datetime | None = None,
) -> ApiToken:
"""
Create a new API token.

Args:
name (str): Name of the token.
description (str | None): Description of the token.
scopes (List[str] | None): List of scopes for the token.
expired_at (datetime | None): Expiration datetime for the token.

Returns:
ApiToken: The created API token (includes the token value).
"""
schema = CreateApiTokenSchema(
name=name,
description=description,
scopes=scopes,
expired_at=expired_at,
)
token = self._client.v1.create_api_token(schema)
return ApiToken.from_schema(token)

def get(self, token_uid: str) -> ApiToken | None:
"""
Get an API token by UID.

Args:
token_uid (str): The UID of the token to get.

Returns:
ApiToken | None: The API token if found, None otherwise.
"""
token = self._client.v1.get_api_token(token_uid)
if token is None:
return None
return ApiToken.from_schema(token)

def delete(self, token_uid: str) -> None:
"""
Delete an API token.

Args:
token_uid (str): The UID of the token to delete.
"""
self._client.v1.delete_api_token(token_uid)
36 changes: 36 additions & 0 deletions src/bentoml/_internal/cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
from ...exceptions import CloudRESTApiClientError
from ...exceptions import NotFound
from ..configuration import BENTOML_VERSION
from .schemas.schemasv1 import ApiTokenListSchema
from .schemas.schemasv1 import ApiTokenSchema
from .schemas.schemasv1 import BentoListSchema
from .schemas.schemasv1 import BentoRepositorySchema
from .schemas.schemasv1 import BentoSchema
from .schemas.schemasv1 import BentoWithRepositoryListSchema
from .schemas.schemasv1 import ClusterFullSchema
from .schemas.schemasv1 import ClusterListSchema
from .schemas.schemasv1 import CompleteMultipartUploadSchema
from .schemas.schemasv1 import CreateApiTokenSchema
from .schemas.schemasv1 import CreateBentoRepositorySchema
from .schemas.schemasv1 import CreateBentoSchema
from .schemas.schemasv1 import CreateDeploymentSchema as CreateDeploymentSchemaV1
Expand Down Expand Up @@ -564,6 +567,39 @@ def update_secret(
self._check_resp(resp)
return schema_from_object(resp.json(), SecretSchema)

def list_api_tokens(
self,
count: int = 100,
search: str | None = None,
start: int = 0,
) -> ApiTokenListSchema:
url = "/api/v1/api_tokens"
params: dict[str, t.Any] = {"start": start, "count": count}
if search:
params["search"] = search
resp = self.session.get(url, params=params)
self._check_resp(resp)
return schema_from_object(resp.json(), ApiTokenListSchema)

def create_api_token(self, req: CreateApiTokenSchema) -> ApiTokenSchema:
url = "/api/v1/api_tokens"
resp = self.session.post(url, json=schema_to_object(req))
self._check_resp(resp)
return schema_from_object(resp.json(), ApiTokenSchema)

def get_api_token(self, token_uid: str) -> ApiTokenSchema | None:
url = f"/api/v1/api_tokens/{token_uid}"
resp = self.session.get(url)
if self._is_not_found(resp):
return None
self._check_resp(resp)
return schema_from_object(resp.json(), ApiTokenSchema)

def delete_api_token(self, token_uid: str) -> None:
url = f"/api/v1/api_tokens/{token_uid}"
resp = self.session.delete(url)
self._check_resp(resp)


class RestApiClientV2(BaseRestApiClient):
def create_deployment(
Expand Down
34 changes: 34 additions & 0 deletions src/bentoml/_internal/cloud/schemas/schemasv1.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,37 @@ class UpdateSecretSchema:
__forbid_extra_keys__ = False
content: SecretContentSchema
description: t.Optional[str] = attr.field(default=None)


@attr.define
class ApiTokenSchema(ResourceSchema):
__omit_if_default__ = True
__forbid_extra_keys__ = False
description: str
scopes: t.List[str]
user: UserSchema
organization: OrganizationSchema
expired_at: t.Optional[datetime] = attr.field(default=None)
last_used_at: t.Optional[datetime] = attr.field(default=None)
is_expired: bool = attr.field(default=False)
is_api_token: bool = attr.field(default=True)
is_organization_token: bool = attr.field(default=False)
is_global_access: bool = attr.field(default=False)
token: t.Optional[str] = attr.field(default=None) # Only returned on create


@attr.define
class ApiTokenListSchema(BaseListSchema):
__omit_if_default__ = True
__forbid_extra_keys__ = False
items: t.List[ApiTokenSchema]


@attr.define
class CreateApiTokenSchema:
__omit_if_default__ = True
__forbid_extra_keys__ = False
name: str
description: t.Optional[str] = attr.field(default=None)
scopes: t.Optional[t.List[str]] = attr.field(default=None)
expired_at: t.Optional[datetime] = attr.field(default=None)
Loading
Loading