Skip to content

[PR-739] [PR-740] Unify Context Management Under a Single config Command #709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
39 changes: 39 additions & 0 deletions clarifai/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,45 @@ Clarifai offers a user-friendly interface for deploying your local model into pr
* Easy implementation and testing in Python
* No need for MLops expertise.

## Context Management

Manage CLI contexts for authentication and environment configuration:
### List all contexts
```bash
clarifai config get-contexts
```

### Switch context
```bash
clarifai config use-context production
```
### Show current context
```bash
clarifai config current-context
```

### Create new context
```bash
clarifai config create-context staging --user-id myuser --pat 678***
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be set-context according to kubectl commands?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I'm not sure because Matt also used create-context in the initial setup instead of set-context. we could add alias for set-context

```
### View entire configuration
```bash
clarifai config view
```
### Delete a context
```bash
clarifai config delete-context old-context
```
### Edit configuration file
```bash
clarifai config edit
```

### Print environment variables for the active context
```bash
clarifai context env
```

## Compute Orchestration

Quick example for deploying a `visual-classifier` model
Expand Down
225 changes: 107 additions & 118 deletions clarifai/cli/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
import os
import shutil
import sys

import click
Expand All @@ -13,7 +12,6 @@
from clarifai.utils.logging import logger


# @click.group(cls=CustomMultiGroup)
@click.group(cls=AliasedGroup)
@click.version_option(version=__version__)
@click.option('--config', default=DEFAULT_CONFIG)
Expand Down Expand Up @@ -54,42 +52,71 @@ def shell_completion(shell):
os.system(f"_CLARIFAI_COMPLETE={shell}_source clarifai")


@cli.group(
['cfg'],
cls=AliasedGroup,
context_settings={'max_content_width': shutil.get_terminal_size().columns - 10},
)
@cli.group(cls=AliasedGroup)
def config():
"""Manage CLI configuration"""
"""
Manage multiple configuration profiles (contexts).

Authentication Precedence:\n
1. Environment variables (e.g., `CLARIFAI_PAT`) are used first if set.
2. The settings from the active context are used if no environment variables are provided.\n
"""


@config.command(['e'])
@cli.command()
@click.argument('api_url', default=DEFAULT_BASE)
@click.option('--user_id', required=False, help='User ID')
@click.pass_context
def edit(ctx):
"""Edit the configuration file"""
os.system(f'{os.environ.get("EDITOR", "vi")} {ctx.obj.filename}')
def login(ctx, api_url, user_id):
"""Login command to set PAT and other configurations."""
from clarifai.utils.cli import validate_context_auth

name = input('context name (default: "default"): ')
user_id = user_id if user_id is not None else input('user id: ')
pat = input_or_default(
'personal access token value (default: "ENVVAR" to get out of env var rather than config): ',
'ENVVAR',
)

@config.command(['current'])
@click.option('-o', '--output-format', default='name', type=click.Choice(['name', 'json', 'yaml']))
@click.pass_context
def current_context(ctx, output_format):
"""Get the current context"""
if output_format == 'name':
print(ctx.obj.current_context)
elif output_format == 'json':
print(json.dumps(ctx.obj.contexts[ctx.obj.current_context].to_serializable_dict()))
else:
print(yaml.safe_dump(ctx.obj.contexts[ctx.obj.current_context].to_serializable_dict()))
# Validate the Context Credentials
validate_context_auth(pat, user_id, api_url)

context = Context(
name,
CLARIFAI_API_BASE=api_url,
CLARIFAI_USER_ID=user_id,
CLARIFAI_PAT=pat,
)

if context.name == '':
context.name = 'default'

ctx.obj.contexts[context.name] = context
ctx.obj.current_context = context.name

ctx.obj.to_yaml()
logger.info(
f"Login successful and Configuration saved successfully for context '{context.name}'"
)


def pat_display(pat):
return pat[:5] + "****"


def input_or_default(prompt, default):
value = input(prompt)
return value if value else default


@config.command(['list', 'ls'])
# Context management commands under config group
@config.command(aliases=['get-contexts', 'list-contexts'])
@click.option(
'-o', '--output-format', default='wide', type=click.Choice(['wide', 'name', 'json', 'yaml'])
)
@click.pass_context
def get_contexts(ctx, output_format):
"""Get all contexts"""
"""List all available contexts."""
if output_format == 'wide':
columns = {
'': lambda c: '*' if c.name == ctx.obj.current_context else '',
Expand All @@ -106,7 +133,6 @@ def get_contexts(ctx, output_format):
additional_columns.add(key)
for key in sorted(additional_columns):
columns[key] = lambda c, k=key: getattr(c, k) if hasattr(c, k) else ""

formatter = TableFormatter(
custom_columns=columns,
)
Expand All @@ -123,101 +149,45 @@ def get_contexts(ctx, output_format):
print(yaml.safe_dump(dicts))


@config.command(['use'])
@click.argument('context-name', type=str)
@config.command(aliases=['use-context'])
@click.argument('name', type=str)
@click.pass_context
def use_context(ctx, context_name):
"""Set the current context"""
if context_name not in ctx.obj.contexts:
def use_context(ctx, name):
"""Set the current context."""
if name not in ctx.obj.contexts:
raise click.UsageError('Context not found')
ctx.obj.current_context = context_name
ctx.obj.current_context = name
ctx.obj.to_yaml()
print(f'Set {context_name} as the current context')


@config.command(['cat'])
@click.option('-o', '--output-format', default='yaml', type=click.Choice(['yaml', 'json']))
@click.pass_obj
def dump(ctx_obj, output_format):
"""Dump the configuration to stdout"""
if output_format == 'yaml':
yaml.safe_dump(ctx_obj.to_dict(), sys.stdout)
else:
json.dump(ctx_obj.to_dict(), sys.stdout, indent=2)


@config.command(['cat'])
@click.pass_obj
def env(ctx_obj):
"""Print env vars. Use: eval "$(clarifai config env)" """
ctx_obj.current.print_env_vars()
print(f'Set {name} as the current context')


@cli.command()
@click.argument('api_url', default=DEFAULT_BASE)
@click.option('--user_id', required=False, help='User ID')
@config.command(aliases=['current-context'])
@click.option('-o', '--output-format', default='name', type=click.Choice(['name', 'json', 'yaml']))
@click.pass_context
def login(ctx, api_url, user_id):
"""Login command to set PAT and other configurations."""
from clarifai.utils.cli import validate_context_auth

name = input('context name (default: "default"): ')
user_id = user_id if user_id is not None else input('user id: ')
pat = input_or_default(
'personal access token value (default: "ENVVAR" to get our of env var rather than config): ',
'ENVVAR',
)

# Validate the Context Credentials
validate_context_auth(pat, user_id, api_url)

context = Context(
name,
CLARIFAI_API_BASE=api_url,
CLARIFAI_USER_ID=user_id,
CLARIFAI_PAT=pat,
)

if context.name == '':
context.name = 'default'

ctx.obj.contexts[context.name] = context
ctx.obj.current_context = context.name

ctx.obj.to_yaml()
logger.info(
f"Login successful and Configuration saved successfully for context '{context.name}'"
)


@cli.group(cls=AliasedGroup)
def context():
"""Manage contexts"""


def pat_display(pat):
return pat[:5] + "****"


def input_or_default(prompt, default):
value = input(prompt)
return value if value else default
def current_context(ctx, output_format):
"""Show the current context's details."""
if output_format == 'name':
print(ctx.obj.current_context)
elif output_format == 'json':
print(json.dumps(ctx.obj.contexts[ctx.obj.current_context].to_serializable_dict()))
else:
print(yaml.safe_dump(ctx.obj.contexts[ctx.obj.current_context].to_serializable_dict()))


@context.command()
@config.command(aliases=['create-context', 'set-context'])
@click.argument('name')
@click.option('--user-id', required=False, help='User ID')
@click.option('--base-url', required=False, help='Base URL')
@click.option('--pat', required=False, help='Personal access token')
@click.pass_context
def create(
def create_context(
ctx,
name,
user_id=None,
base_url=None,
pat=None,
):
"""Create a new context"""
"""Create a new context."""
from clarifai.utils.cli import validate_context_auth

if name in ctx.obj.contexts:
Expand All @@ -234,22 +204,28 @@ def create(
'personal access token value (default: "ENVVAR" to get our of env var rather than config): ',
'ENVVAR',
)

# Validate the Context Credentials
validate_context_auth(pat, user_id, base_url)

context = Context(name, CLARIFAI_USER_ID=user_id, CLARIFAI_API_BASE=base_url, CLARIFAI_PAT=pat)
ctx.obj.contexts[context.name] = context
ctx.obj.to_yaml()
logger.info(f"Context '{name}' created successfully")


# write a click command to delete a context
@context.command(['rm'])
@config.command(aliases=['e'])
@click.pass_context
def edit(
ctx,
):
"""Open the configuration file for editing."""
# For now, just open the config file (not per-context)
os.system(f'{os.environ.get("EDITOR", "vi")} {ctx.obj.filename}')
Copy link
Preview

Copilot AI Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using os.system() with user-controlled input (filename) can be dangerous if the filename contains shell metacharacters. Consider using subprocess.run() with proper argument separation for safer execution.

Suggested change
os.system(f'{os.environ.get("EDITOR", "vi")} {ctx.obj.filename}')
import subprocess
editor = os.environ.get("EDITOR", "vi")
subprocess.run([editor, ctx.obj.filename])

Copilot uses AI. Check for mistakes.



@config.command(aliases=['delete-context'])
@click.argument('name')
@click.pass_context
def delete(ctx, name):
"""Delete a context"""
def delete_context(ctx, name):
"""Delete a context."""
if name not in ctx.obj.contexts:
print(f'{name} is not a valid context')
sys.exit(1)
Expand All @@ -258,16 +234,29 @@ def delete(ctx, name):
print(f'{name} deleted')


@context.command()
@click.argument('name', type=str)
@config.command(aliases=['get-env'])
@click.pass_context
def use(ctx, name):
"""Set the current context"""
if name not in ctx.obj.contexts:
raise click.UsageError('Context not found')
ctx.obj.current_context = name
ctx.obj.to_yaml()
print(f'Set {name} as the current context')
def env(ctx):
"""Print env vars for the active context."""
ctx.obj.current.print_env_vars()


@config.command(aliases=['show'])
@click.option('-o', '--output-format', default='yaml', type=click.Choice(['json', 'yaml']))
@click.pass_context
def view(ctx, output_format):
"""Display the current configuration."""
config_dict = {
'current-context': ctx.obj.current_context,
'contexts': {
name: context.to_serializable_dict() for name, context in ctx.obj.contexts.items()
},
}

if output_format == 'json':
print(json.dumps(config_dict, indent=2))
else:
print(yaml.safe_dump(config_dict, default_flow_style=False))


@cli.command()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ workflow:
description: Custom crop model
output_info:
params:
margin: 1.33
margin: 1.3
node_inputs:
- node_id: detector
Loading