diff --git a/centml/cli/cserve_recipe.py b/centml/cli/cserve_recipe.py new file mode 100644 index 0000000..fd94f60 --- /dev/null +++ b/centml/cli/cserve_recipe.py @@ -0,0 +1,289 @@ +import json +import sys +from functools import wraps + +import click + +from centml.sdk.ops import get_centml_ops_client + + +def handle_exception(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except ImportError as e: + click.echo(f"Error: {e}") + click.echo("Please install platform-api-ops-client to use this feature.") + return None + except Exception as e: + click.echo(f"Error: {e}") + return None + + return wrapper + + +@click.command(help="Update CServe recipes from platform_db.json file") +@click.argument("platform_db_file", type=click.Path(exists=True)) +@click.option( + "--cluster-id", + type=int, + required=True, + help="The cluster ID to associate with hardware instances", +) +@handle_exception +def update(platform_db_file, cluster_id): + """ + Update CServe recipes from platform_db.json performance data. + + This command reads a platform_db.json file containing performance test results + and updates the CServe recipe configurations in the database. + + Example: + centml cserve-recipe update platform_db.json --cluster-id 1001 + """ + # Load platform_db.json file + try: + with open(platform_db_file, "r") as f: + platform_data = json.load(f) + except json.JSONDecodeError: + click.echo(f"Error: Invalid JSON file: {platform_db_file}") + sys.exit(1) + except Exception as e: + click.echo(f"Error reading file: {e}") + sys.exit(1) + + # Validate platform_data structure + if not isinstance(platform_data, dict): + click.echo("Error: platform_db.json should contain a dictionary of models") + sys.exit(1) + + click.echo(f"Processing {len(platform_data)} models from {platform_db_file}...") + click.echo(f"Target cluster ID: {cluster_id}") + + with get_centml_ops_client() as ops_client: + response = ops_client.update_cserve_recipes( + cluster_id=cluster_id, platform_data=platform_data + ) + + # Display results + click.echo("\n" + "=" * 60) + click.echo(click.style("✓ Update Complete", fg="green", bold=True)) + click.echo("=" * 60 + "\n") + + click.echo(click.style(response.message, fg="green")) + + if response.processed_models: + click.echo(f"\nProcessed Models ({len(response.processed_models)}):") + for model in response.processed_models: + click.echo(f" ✓ {model}") + + if response.errors: + click.echo( + click.style(f"\nErrors ({len(response.errors)}):", fg="red", bold=True) + ) + for error in response.errors: + click.echo(click.style(f" ✗ {error}", fg="red")) + sys.exit(1) + + +@click.command(help="List available clusters") +@click.option( + "--show-hardware", + is_flag=True, + help="Show hardware instances for each cluster", +) +@handle_exception +def list_clusters(show_hardware): + """ + List available clusters for the organization. + + Example: + centml cserve-recipe list-clusters + centml cserve-recipe list-clusters --show-hardware + """ + with get_centml_ops_client() as ops_client: + clusters_data = ops_client.get_clusters( + include_hardware_instances=show_hardware + ) + + if not clusters_data: + click.echo("No clusters found.") + return + + # Handle different return types + if show_hardware: + clusters = [item["cluster"] for item in clusters_data] + else: + clusters = clusters_data + + click.echo( + f"\n{click.style('Available Clusters', bold=True, fg='cyan')} ({len(clusters)} found)\n" + ) + + for i, cluster in enumerate(clusters): + click.echo(f"{click.style('Cluster ID:', bold=True)} {cluster.id}") + click.echo(f" Display Name: {cluster.display_name}") + if cluster.region: + click.echo(f" Region: {cluster.region}") + + if show_hardware: + hw_instances = clusters_data[i]["hardware_instances"] + if hw_instances: + click.echo( + f" {click.style('Hardware Instances:', fg='yellow')} ({len(hw_instances)} available)" + ) + for hw in hw_instances: + gpu_info = ( + f"{hw.num_accelerators}x{hw.gpu_type}" + if hw.num_accelerators + else hw.gpu_type + ) + click.echo( + f" • {click.style(hw.name, fg='green')} (ID: {hw.id})" + ) + click.echo(f" GPU: {gpu_info}") + click.echo(f" CPU: {hw.cpu} cores, Memory: {hw.memory} GB") + if hw.cost_per_hr: + click.echo(f" Cost: ${hw.cost_per_hr/100:.2f}/hr") + else: + click.echo( + f" {click.style('Hardware Instances:', fg='yellow')} None" + ) + + click.echo("") + + +@click.command(help="List CServe recipes") +@click.option( + "--model", help="Filter by model name (e.g., 'meta-llama/Llama-3.3-70B-Instruct')" +) +@click.option("--hf-token", help="HuggingFace token for private models") +@handle_exception +def list_recipes(model, hf_token): + """ + List CServe recipe configurations. + + Example: + # List all recipes + centml cserve-recipe list + + # List recipes for a specific model + centml cserve-recipe list --model "meta-llama/Llama-3.3-70B-Instruct" + """ + with get_centml_ops_client() as ops_client: + recipes = ops_client.get_cserve_recipes(model=model, hf_token=hf_token) + + if not recipes: + click.echo("No recipes found.") + return + + # Get all hardware instances to map IDs to names + try: + hardware_instances = ops_client.get_hardware_instances() + hw_map = {hw.id: hw for hw in hardware_instances} + except Exception as e: + click.echo( + click.style( + f"Warning: Could not fetch hardware instance details: {e}", + fg="yellow", + ) + ) + hw_map = {} + + click.echo( + f"\n{click.style('CServe Recipes', bold=True, fg='cyan')} ({len(recipes)} found)\n" + ) + + def format_hw_info(hw_id): + """Format hardware instance information""" + if hw_id in hw_map: + hw = hw_map[hw_id] + gpu_info = ( + f"{hw.num_accelerators}x{hw.gpu_type}" + if hw.num_accelerators + else hw.gpu_type + ) + return f"{hw.name} (ID: {hw_id}, {gpu_info})" + else: + return f"ID: {hw_id} {click.style('(details not found)', fg='yellow')}" + + for recipe in recipes: + click.echo(f"{click.style('Model:', bold=True)} {recipe.model}") + + # Display fastest configuration + if recipe.fastest: + click.echo(f" {click.style('Fastest:', fg='green')}") + click.echo( + f" Hardware: {format_hw_info(recipe.fastest.hardware_instance_id)}" + ) + click.echo(f" Recipe: {recipe.fastest.recipe.model}") + if hasattr(recipe.fastest.recipe, "additional_properties"): + tp_size = recipe.fastest.recipe.additional_properties.get( + "tensor_parallel_size", "N/A" + ) + pp_size = recipe.fastest.recipe.additional_properties.get( + "pipeline_parallel_size", "N/A" + ) + click.echo(f" Parallelism: TP={tp_size}, PP={pp_size}") + + # Display cheapest configuration + if recipe.cheapest: + if ( + recipe.cheapest.hardware_instance_id + != recipe.fastest.hardware_instance_id + ): + click.echo(f" {click.style('Cheapest:', fg='yellow')}") + click.echo( + f" Hardware: {format_hw_info(recipe.cheapest.hardware_instance_id)}" + ) + else: + click.echo( + f" {click.style('Cheapest:', fg='yellow')} Same as Fastest" + ) + + # Display best_value configuration + if recipe.best_value: + if ( + recipe.best_value.hardware_instance_id + != recipe.fastest.hardware_instance_id + ): + click.echo(f" {click.style('Best Value:', fg='blue')}") + click.echo( + f" Hardware: {format_hw_info(recipe.best_value.hardware_instance_id)}" + ) + else: + click.echo( + f" {click.style('Best Value:', fg='blue')} Same as Fastest" + ) + + click.echo("") # Empty line between recipes + + +@click.command(help="Delete CServe recipe for a specific model") +@click.argument("model") +@click.option("--confirm", is_flag=True, help="Skip confirmation prompt") +@handle_exception +def delete(model, confirm): + """ + Delete CServe recipe configurations for a specific model. + + This will remove all recipe configurations (fastest, cheapest, best_value) + for the specified model. + + Example: + centml cserve-recipe delete "meta-llama/Llama-3.3-70B-Instruct" + centml cserve-recipe delete "Qwen/Qwen3-0.6B" --confirm + """ + if not confirm: + if not click.confirm( + f"Are you sure you want to delete recipe for model '{model}'?" + ): + click.echo("Cancelled.") + return + + with get_centml_ops_client() as ops_client: + ops_client.delete_cserve_recipe(model=model) + click.echo( + click.style(f"✓ Successfully deleted recipe for model: {model}", fg="green") + ) diff --git a/centml/cli/main.py b/centml/cli/main.py index b1ecc73..24ec986 100644 --- a/centml/cli/main.py +++ b/centml/cli/main.py @@ -2,6 +2,12 @@ from centml.cli.login import login, logout from centml.cli.cluster import ls, get, delete, pause, resume +from centml.cli.cserve_recipe import ( + update as recipe_update, + delete as recipe_delete, + list_recipes, + list_clusters, +) @click.group() @@ -50,3 +56,17 @@ def ccluster(): cli.add_command(ccluster, name="cluster") + + +@click.group(help="CentML CServe recipe management CLI tool") +def cserve_recipe(): + pass + + +cserve_recipe.add_command(list_clusters, name="list-clusters") +cserve_recipe.add_command(list_recipes, name="list") +cserve_recipe.add_command(recipe_update, name="update") +cserve_recipe.add_command(recipe_delete, name="delete") + + +cli.add_command(cserve_recipe, name="cserve-recipe") diff --git a/centml/sdk/__init__.py b/centml/sdk/__init__.py index 2bed9e7..a5b566d 100644 --- a/centml/sdk/__init__.py +++ b/centml/sdk/__init__.py @@ -1,2 +1,7 @@ from platform_api_python_client import * -from . import api, auth +from . import api, auth, ops + +# Export OPS client classes and functions +from .ops import CentMLOpsClient, get_centml_ops_client + +__all__ = ['CentMLOpsClient', 'get_centml_ops_client', 'api', 'auth', 'ops'] diff --git a/centml/sdk/config.py b/centml/sdk/config.py index 3e8b6ea..d732249 100644 --- a/centml/sdk/config.py +++ b/centml/sdk/config.py @@ -8,20 +8,31 @@ class Config(BaseSettings): # It is possible to override the default values by setting the environment variables model_config = SettingsConfigDict(env_file=Path(".env")) - CENTML_WEB_URL: str = os.getenv("CENTML_WEB_URL", default="https://app.centml.com/") - CENTML_CONFIG_PATH: str = os.getenv("CENTML_CONFIG_PATH", default=os.path.expanduser("~/.centml")) + CENTML_WEB_URL: str = os.getenv("CENTML_WEB_URL", default="https://app.centml.org/") + CENTML_CONFIG_PATH: str = os.getenv( + "CENTML_CONFIG_PATH", default=os.path.expanduser("~/.centml") + ) CENTML_CRED_FILE: str = os.getenv("CENTML_CRED_FILE", default="credentials.json") CENTML_CRED_FILE_PATH: str = os.path.join(CENTML_CONFIG_PATH, CENTML_CRED_FILE) - CENTML_PLATFORM_API_URL: str = os.getenv("CENTML_PLATFORM_API_URL", default="https://api.centml.com") + CENTML_PLATFORM_API_URL: str = os.getenv( + "CENTML_PLATFORM_API_URL", default="https://api.centml.org" + ) - CENTML_WORKOS_CLIENT_ID: str = os.getenv("CENTML_WORKOS_CLIENT_ID", default="client_01JP5TWW2997MF8AYQXHJEGYR0") + CENTML_WORKOS_CLIENT_ID: str = os.getenv( + "CENTML_WORKOS_CLIENT_ID", default="client_01JP5TWVNBMQ5FVC777FQZC661" + ) # Long-term credentials - can be set via environment variables - CENTML_SERVICE_ACCOUNT_SECRET: Optional[str] = os.getenv("CENTML_SERVICE_ACCOUNT_SECRET", default=None) - CENTML_SERVICE_ACCOUNT_ID: Optional[str] = os.getenv("CENTML_SERVICE_ACCOUNT_ID", default=None) + CENTML_SERVICE_ACCOUNT_SECRET: Optional[str] = os.getenv( + "CENTML_SERVICE_ACCOUNT_SECRET", default=None + ) + CENTML_SERVICE_ACCOUNT_ID: Optional[str] = os.getenv( + "CENTML_SERVICE_ACCOUNT_ID", default=None + ) CENTML_SERVICE_ACCOUNT_TOKEN_URL: str = os.getenv( - "CENTML_SERVICE_ACCOUNT_TOKEN_URL", default="https://signin.centml.com/oauth2/token" + "CENTML_SERVICE_ACCOUNT_TOKEN_URL", + default="https://signin.centml.com/oauth2/token", ) diff --git a/centml/sdk/ops.py b/centml/sdk/ops.py new file mode 100644 index 0000000..4abb0ae --- /dev/null +++ b/centml/sdk/ops.py @@ -0,0 +1,229 @@ +from contextlib import contextmanager +from typing import Dict, Any, Optional + +try: + import platform_api_ops_client + from platform_api_ops_client import OPSApi + + OPS_CLIENT_AVAILABLE = True +except ImportError: + OPS_CLIENT_AVAILABLE = False + +import platform_api_python_client + +from centml.sdk import auth +from centml.sdk.config import settings + + +class CentMLOpsClient: + """ + Client for CentML OPS API operations. + Used for administrative tasks like managing CServe recipes. + """ + + def __init__( + self, + ops_api: Optional["OPSApi"] = None, + external_api: Optional[platform_api_python_client.EXTERNALApi] = None, + ): + self._ops_api = ops_api + self._external_api = external_api + + def get_clusters(self, include_hardware_instances: bool = False): + """ + Get available clusters for the organization. + + Args: + include_hardware_instances: If True, also fetch hardware instances for each cluster + + Returns: + If include_hardware_instances=False: List of cluster configurations + If include_hardware_instances=True: List of dicts with 'cluster' and 'hardware_instances' keys + + Example: + with get_centml_ops_client() as ops_client: + # Get clusters only + clusters = ops_client.get_clusters() + + # Get clusters with hardware instances + clusters_with_hw = ops_client.get_clusters(include_hardware_instances=True) + for item in clusters_with_hw: + cluster = item['cluster'] + print(f"Cluster {cluster.id}: {cluster.display_name}") + for hw in item['hardware_instances']: + print(f" - {hw.name}: {hw.num_accelerators}x{hw.gpu_type}") + """ + if self._external_api is None: + raise RuntimeError("External API client not available") + + clusters = self._external_api.get_clusters_clusters_get().results + + if include_hardware_instances: + result = [] + for cluster in clusters: + hw_instances = ( + self._external_api.get_hardware_instances_hardware_instances_get( + cluster_id=cluster.id + ).results + ) + result.append({"cluster": cluster, "hardware_instances": hw_instances}) + return result + + return clusters + + def get_hardware_instances(self, cluster_id: Optional[int] = None): + """ + Get hardware instances, optionally filtered by cluster. + + Args: + cluster_id: Optional cluster ID to filter hardware instances + + Returns: + List of hardware instance configurations + + Example: + with get_centml_ops_client() as ops_client: + # Get all hardware instances + all_hw = ops_client.get_hardware_instances() + + # Get hardware instances for specific cluster + cluster_hw = ops_client.get_hardware_instances(cluster_id=1000) + """ + if self._external_api is None: + raise RuntimeError("External API client not available") + + return self._external_api.get_hardware_instances_hardware_instances_get( + cluster_id=cluster_id + ).results + + def get_cserve_recipes( + self, model: Optional[str] = None, hf_token: Optional[str] = None + ): + """ + Get CServe recipe configurations. + + Args: + model: Optional model name to filter recipes (e.g., "meta-llama/Llama-3.3-70B-Instruct") + hf_token: Optional HuggingFace token for private models + + Returns: + List of CServe recipe configurations + + Example: + with get_centml_ops_client() as ops_client: + # Get all recipes + all_recipes = ops_client.get_cserve_recipes() + + # Get recipes for a specific model + recipes = ops_client.get_cserve_recipes(model="meta-llama/Llama-3.3-70B-Instruct") + """ + if self._external_api is None: + raise RuntimeError("External API client not available") + + return self._external_api.get_cserve_recipe_deployments_cserve_recipes_get( + model=model, hf_token=hf_token + ).results + + def update_cserve_recipes( + self, cluster_id: int, platform_data: Dict[str, Dict[str, Dict[str, Any]]] + ): + """ + Update CServe recipes from platform_db.json performance data. + + Args: + cluster_id: The cluster ID to associate with hardware instances + platform_data: Platform DB data in the format: + { + "model_name": { + "fastest": {...}, + "cheapest": {...}, # optional + "best_value": {...} # optional + } + } + + Returns: + Response containing processed models and any errors + + Example: + with get_centml_ops_client() as ops_client: + with open('platform_db.json') as f: + platform_data = json.load(f) + response = ops_client.update_cserve_recipes(cluster_id=1001, platform_data=platform_data) + print(f"Processed: {response.processed_models}") + """ + if self._ops_api is None: + raise RuntimeError( + "OPS API client not available. Install platform-api-ops-client." + ) + + return self._ops_api.update_cserve_recipes_ops_cserve_recipes_post( + cluster_id=cluster_id, request_body=platform_data + ) + + def delete_cserve_recipe(self, model: str): + """ + Delete CServe recipe configurations for a specific model. + + Args: + model: The model name to delete (e.g., "meta-llama/Llama-3.3-70B-Instruct") + + Returns: + Success response (200 OK) + + Example: + with get_centml_ops_client() as ops_client: + ops_client.delete_cserve_recipe(model="meta-llama/Llama-3.3-70B-Instruct") + """ + if self._ops_api is None: + raise RuntimeError( + "OPS API client not available. Install platform-api-ops-client." + ) + + return self._ops_api.delete_cserve_recipe_ops_cserve_recipes_delete(model=model) + + +@contextmanager +def get_centml_ops_client(): + """ + Context manager for CentML OPS API client. + + This client provides: + - get_clusters(): Get available clusters (uses external API, always available) + - get_cserve_recipes(): Read recipes (uses external API, always available) + - update_cserve_recipes(): Update recipes (requires platform-api-ops-client) + - delete_cserve_recipe(): Delete recipes (requires platform-api-ops-client) + + Usage: + with get_centml_ops_client() as ops_client: + # Get clusters (always works) + clusters = ops_client.get_clusters() + + # Get recipes (always works) + recipes = ops_client.get_cserve_recipes(model="meta-llama/Llama-3.3-70B-Instruct") + + # Update/delete requires platform-api-ops-client + response = ops_client.update_cserve_recipes(cluster_id=1001, platform_data=data) + """ + access_token = auth.get_centml_token() + + configuration = platform_api_python_client.Configuration( + host=settings.CENTML_PLATFORM_API_URL, access_token=access_token + ) + + # Always initialize external API for read operations + with platform_api_python_client.ApiClient(configuration) as external_client: + external_api = platform_api_python_client.EXTERNALApi(external_client) + + # Initialize OPS API if available for write operations + ops_api = None + if OPS_CLIENT_AVAILABLE: + ops_configuration = platform_api_ops_client.Configuration( + host=settings.CENTML_PLATFORM_API_URL, + access_token=access_token, + ) + with platform_api_ops_client.ApiClient(ops_configuration) as ops_client: + ops_api = OPSApi(ops_client) + yield CentMLOpsClient(ops_api=ops_api, external_api=external_api) + else: + # Still provide read-only functionality even without ops client + yield CentMLOpsClient(ops_api=None, external_api=external_api) diff --git a/examples/sdk/get_clusters.py b/examples/sdk/get_clusters.py new file mode 100644 index 0000000..7421356 --- /dev/null +++ b/examples/sdk/get_clusters.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Example: Get available clusters + +This example demonstrates how to retrieve cluster information +using the CentML SDK, with and without hardware instance details. +""" + +from centml.sdk.ops import get_centml_ops_client + + +def main(): + """Get and display cluster information""" + + with get_centml_ops_client() as ops_client: + # Example 1: Get clusters (basic information) + print("=" * 60) + print("Example 1: Get Clusters (Basic Info)") + print("=" * 60) + + clusters = ops_client.get_clusters() + + if not clusters: + print("No clusters found.") + return + + print(f"\nFound {len(clusters)} cluster(s):\n") + + for cluster in clusters: + print(f"Cluster ID: {cluster.id}") + print(f" Display Name: {cluster.display_name}") + print(f" Region: {cluster.region or 'N/A'}") + print() + + # Example 2: Get clusters with hardware instances + print("\n" + "=" * 60) + print("Example 2: Get Clusters (With Hardware Instances)") + print("=" * 60) + + clusters_with_hw = ops_client.get_clusters(include_hardware_instances=True) + + for item in clusters_with_hw: + cluster = item["cluster"] + hw_instances = item["hardware_instances"] + + print(f"\nCluster: {cluster.display_name} (ID: {cluster.id})") + + if hw_instances: + print(f" Hardware Instances ({len(hw_instances)} available):") + for hw in hw_instances: + gpu_info = ( + f"{hw.num_accelerators}x{hw.gpu_type}" + if hw.num_accelerators + else hw.gpu_type + ) + print(f" • {hw.name} (ID: {hw.id})") + print(f" GPU: {gpu_info}") + print(f" CPU: {hw.cpu} cores, Memory: {hw.memory} GB") + print(f" Cost: ${hw.cost_per_hr/100:.2f}/hr") + else: + print(" No hardware instances available") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk/manage_cserve_recipes.py b/examples/sdk/manage_cserve_recipes.py new file mode 100644 index 0000000..9ce4599 --- /dev/null +++ b/examples/sdk/manage_cserve_recipes.py @@ -0,0 +1,108 @@ +""" +Example demonstrating how to manage CServe recipes using the CentML SDK. + +This example shows how to: +1. List/Get CServe recipes (read-only, no special permissions needed) +2. Update CServe recipes from platform_db.json (requires OPS admin) +3. Delete CServe recipes for specific models (requires OPS admin) + +Note: Update and delete operations require platform-api-ops-client to be installed +and OPS admin permissions. Get/list operations work with just the base client. +""" + +import json +from centml.sdk.ops import get_centml_ops_client + + +def list_recipes_example(): + """List all CServe recipes or filter by model.""" + with get_centml_ops_client() as ops_client: + # List all recipes + all_recipes = ops_client.get_cserve_recipes() + print(f"Found {len(all_recipes)} recipes") + + for recipe in all_recipes[:3]: # Show first 3 + print(f"\nModel: {recipe.model}") + if recipe.fastest: + print( + f" Fastest - Hardware Instance: {recipe.fastest.hardware_instance_id}" + ) + if recipe.cheapest: + print( + f" Cheapest - Hardware Instance: {recipe.cheapest.hardware_instance_id}" + ) + + # Filter by specific model + model_name = "meta-llama/Llama-3.3-70B-Instruct" + specific_recipes = ops_client.get_cserve_recipes(model=model_name) + if specific_recipes: + print(f"\nRecipe for {model_name}:") + print( + f" Fastest config available: {specific_recipes[0].fastest is not None}" + ) + + +def update_recipes_example(): + """Update CServe recipes from platform_db.json file.""" + # Load platform_db.json data + # This file should contain performance data in the format: + # { + # "model_name": { + # "fastest": { "accelerator_type": "...", "accelerator_count": ..., ... }, + # "cheapest": { ... }, # optional + # "best_value": { ... } # optional + # }, + # ... + # } + with open("platform_db.json", "r") as f: + platform_data = json.load(f) + + cluster_id = 1001 # Replace with your cluster ID + + with get_centml_ops_client() as ops_client: + response = ops_client.update_cserve_recipes( + cluster_id=cluster_id, platform_data=platform_data + ) + + print(f"Message: {response.message}") + print(f"Processed Models: {response.processed_models}") + if response.errors: + print(f"Errors: {response.errors}") + + +def delete_recipe_example(): + """Delete CServe recipe for a specific model.""" + model_name = "meta-llama/Llama-3.3-70B-Instruct" + + with get_centml_ops_client() as ops_client: + ops_client.delete_cserve_recipe(model=model_name) + print(f"Successfully deleted recipe for model: {model_name}") + + +def main(): + # Example 1: List/Get recipes (read-only, always available) + print("=== Listing CServe Recipes ===") + try: + list_recipes_example() + except Exception as e: + print(f"Error listing recipes: {e}") + + # Example 2: Update recipes from platform_db.json (requires ops client) + print("\n=== Updating CServe Recipes ===") + try: + update_recipes_example() + except FileNotFoundError: + print("platform_db.json not found. Skipping update example.") + except Exception as e: + print(f"Error updating recipes: {e}") + + # Example 3: Delete a specific model's recipe (requires ops client) + print("\n=== Deleting CServe Recipe ===") + try: + delete_recipe_example() + except Exception as e: + print(f"Error deleting recipe: {e}") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 1fe82b6..9787a30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ prometheus-client>=0.20.0 scipy>=1.6.0 scikit-learn>=1.5.1 platform-api-python-client==4.1.9 +# platform-api-ops-client>=1.0.0 # Optional: only needed for update/delete CServe recipes (OPS admin operations)