mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-12-17 16:32:38 +00:00
Merge branch 'main' into ci-ui-tests
This commit is contained in:
commit
2e33659bfb
10 changed files with 63 additions and 242 deletions
2
.github/TRIAGERS.md
vendored
2
.github/TRIAGERS.md
vendored
|
|
@ -1,2 +1,2 @@
|
||||||
# This file documents Triage members in the Llama Stack community
|
# This file documents Triage members in the Llama Stack community
|
||||||
@bbrowning @franciscojavierarceo @leseb
|
@franciscojavierarceo
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,7 @@ new_vector_database
|
||||||
```{include} ../../../tests/README.md
|
```{include} ../../../tests/README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Benchmarking
|
## Advanced Topics
|
||||||
|
|
||||||
```{include} ../../../docs/source/distributions/k8s-benchmark/README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Topics
|
|
||||||
|
|
||||||
For developers who need deeper understanding of the testing system internals:
|
For developers who need deeper understanding of the testing system internals:
|
||||||
|
|
||||||
|
|
@ -37,3 +32,8 @@ For developers who need deeper understanding of the testing system internals:
|
||||||
|
|
||||||
testing/record-replay
|
testing/record-replay
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Benchmarking
|
||||||
|
|
||||||
|
```{include} ../../../docs/source/distributions/k8s-benchmark/README.md
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# This source code is licensed under the terms described in the LICENSE file in
|
|
||||||
# the root directory of this source tree.
|
|
||||||
|
|
||||||
LLAMA_STACK_DIR=${LLAMA_STACK_DIR:-}
|
|
||||||
LLAMA_STACK_CLIENT_DIR=${LLAMA_STACK_CLIENT_DIR:-}
|
|
||||||
TEST_PYPI_VERSION=${TEST_PYPI_VERSION:-}
|
|
||||||
PYPI_VERSION=${PYPI_VERSION:-}
|
|
||||||
# This timeout (in seconds) is necessary when installing PyTorch via uv since it's likely to time out
|
|
||||||
# Reference: https://github.com/astral-sh/uv/pull/1694
|
|
||||||
UV_HTTP_TIMEOUT=${UV_HTTP_TIMEOUT:-500}
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Define color codes
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
|
|
||||||
source "$SCRIPT_DIR/common.sh"
|
|
||||||
|
|
||||||
# Usage function
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 --env-name <conda_env_name> --build-file-path <build_file_path> --normal-deps <pip_dependencies> [--external-provider-deps <external_provider_deps>] [--optional-deps <special_pip_deps>]"
|
|
||||||
echo "Example: $0 --env-name my-conda-env --build-file-path ./my-stack-build.yaml --normal-deps 'numpy pandas scipy' --external-provider-deps 'foo' --optional-deps 'bar'"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
env_name=""
|
|
||||||
build_file_path=""
|
|
||||||
normal_deps=""
|
|
||||||
external_provider_deps=""
|
|
||||||
optional_deps=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
key="$1"
|
|
||||||
case "$key" in
|
|
||||||
--env-name)
|
|
||||||
if [[ -z "$2" || "$2" == --* ]]; then
|
|
||||||
echo "Error: --env-name requires a string value" >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
env_name="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--build-file-path)
|
|
||||||
if [[ -z "$2" || "$2" == --* ]]; then
|
|
||||||
echo "Error: --build-file-path requires a string value" >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
build_file_path="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--normal-deps)
|
|
||||||
if [[ -z "$2" || "$2" == --* ]]; then
|
|
||||||
echo "Error: --normal-deps requires a string value" >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
normal_deps="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--external-provider-deps)
|
|
||||||
if [[ -z "$2" || "$2" == --* ]]; then
|
|
||||||
echo "Error: --external-provider-deps requires a string value" >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
external_provider_deps="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--optional-deps)
|
|
||||||
if [[ -z "$2" || "$2" == --* ]]; then
|
|
||||||
echo "Error: --optional-deps requires a string value" >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
optional_deps="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1" >&2
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check required arguments
|
|
||||||
if [[ -z "$env_name" || -z "$build_file_path" || -z "$normal_deps" ]]; then
|
|
||||||
echo "Error: --env-name, --build-file-path, and --normal-deps are required." >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$LLAMA_STACK_DIR" ]; then
|
|
||||||
echo "Using llama-stack-dir=$LLAMA_STACK_DIR"
|
|
||||||
fi
|
|
||||||
if [ -n "$LLAMA_STACK_CLIENT_DIR" ]; then
|
|
||||||
echo "Using llama-stack-client-dir=$LLAMA_STACK_CLIENT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_conda_env_python310() {
|
|
||||||
# Use only global variables set by flag parser
|
|
||||||
local python_version="3.12"
|
|
||||||
|
|
||||||
if ! is_command_available conda; then
|
|
||||||
printf "${RED}Error: conda command not found. Is Conda installed and in your PATH?${NC}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if conda env list | grep -q "^${env_name} "; then
|
|
||||||
printf "Conda environment '${env_name}' exists. Checking Python version...\n"
|
|
||||||
current_version=$(conda run -n "${env_name}" python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
|
|
||||||
if [ "$current_version" = "$python_version" ]; then
|
|
||||||
printf "Environment '${env_name}' already has Python ${python_version}. No action needed.\n"
|
|
||||||
else
|
|
||||||
printf "Updating environment '${env_name}' to Python ${python_version}...\n"
|
|
||||||
conda install -n "${env_name}" python="${python_version}" -y
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "Conda environment '${env_name}' does not exist. Creating with Python ${python_version}...\n"
|
|
||||||
conda create -n "${env_name}" python="${python_version}" -y
|
|
||||||
fi
|
|
||||||
|
|
||||||
eval "$(conda shell.bash hook)"
|
|
||||||
conda deactivate && conda activate "${env_name}"
|
|
||||||
"$CONDA_PREFIX"/bin/pip install uv
|
|
||||||
|
|
||||||
if [ -n "$TEST_PYPI_VERSION" ]; then
|
|
||||||
uv pip install fastapi libcst
|
|
||||||
uv pip install --extra-index-url https://test.pypi.org/simple/ \
|
|
||||||
llama-stack=="$TEST_PYPI_VERSION" \
|
|
||||||
"$normal_deps"
|
|
||||||
if [ -n "$optional_deps" ]; then
|
|
||||||
IFS='#' read -ra parts <<<"$optional_deps"
|
|
||||||
for part in "${parts[@]}"; do
|
|
||||||
echo "$part"
|
|
||||||
uv pip install $part
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
if [ -n "$external_provider_deps" ]; then
|
|
||||||
IFS='#' read -ra parts <<<"$external_provider_deps"
|
|
||||||
for part in "${parts[@]}"; do
|
|
||||||
echo "$part"
|
|
||||||
uv pip install "$part"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ -n "$LLAMA_STACK_DIR" ]; then
|
|
||||||
if [ ! -d "$LLAMA_STACK_DIR" ]; then
|
|
||||||
printf "${RED}Warning: LLAMA_STACK_DIR is set but directory does not exist: $LLAMA_STACK_DIR${NC}\n" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf "Installing from LLAMA_STACK_DIR: $LLAMA_STACK_DIR\n"
|
|
||||||
uv pip install --no-cache-dir -e "$LLAMA_STACK_DIR"
|
|
||||||
else
|
|
||||||
PYPI_VERSION="${PYPI_VERSION:-}"
|
|
||||||
if [ -n "$PYPI_VERSION" ]; then
|
|
||||||
SPEC_VERSION="llama-stack==${PYPI_VERSION}"
|
|
||||||
else
|
|
||||||
SPEC_VERSION="llama-stack"
|
|
||||||
fi
|
|
||||||
uv pip install --no-cache-dir "$SPEC_VERSION"
|
|
||||||
fi
|
|
||||||
if [ -n "$LLAMA_STACK_CLIENT_DIR" ]; then
|
|
||||||
if [ ! -d "$LLAMA_STACK_CLIENT_DIR" ]; then
|
|
||||||
printf "${RED}Warning: LLAMA_STACK_CLIENT_DIR is set but directory does not exist: $LLAMA_STACK_CLIENT_DIR${NC}\n" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf "Installing from LLAMA_STACK_CLIENT_DIR: $LLAMA_STACK_CLIENT_DIR\n"
|
|
||||||
uv pip install --no-cache-dir -e "$LLAMA_STACK_CLIENT_DIR"
|
|
||||||
fi
|
|
||||||
printf "Installing pip dependencies\n"
|
|
||||||
uv pip install $normal_deps
|
|
||||||
if [ -n "$optional_deps" ]; then
|
|
||||||
IFS='#' read -ra parts <<<"$optional_deps"
|
|
||||||
for part in "${parts[@]}"; do
|
|
||||||
echo "$part"
|
|
||||||
uv pip install $part
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
if [ -n "$external_provider_deps" ]; then
|
|
||||||
IFS='#' read -ra parts <<<"$external_provider_deps"
|
|
||||||
for part in "${parts[@]}"; do
|
|
||||||
echo "Getting provider spec for module: $part and installing dependencies"
|
|
||||||
package_name=$(echo "$part" | sed 's/[<>=!].*//')
|
|
||||||
python3 -c "
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(f'$package_name.provider')
|
|
||||||
spec = module.get_provider_spec()
|
|
||||||
if hasattr(spec, 'pip_packages') and spec.pip_packages:
|
|
||||||
print('\\n'.join(spec.pip_packages))
|
|
||||||
except Exception as e:
|
|
||||||
print(f'Error getting provider spec for $package_name: {e}', file=sys.stderr)
|
|
||||||
" | uv pip install -r -
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
mv "$build_file_path" "$CONDA_PREFIX"/llamastack-build.yaml
|
|
||||||
echo "Build spec configuration saved at $CONDA_PREFIX/llamastack-build.yaml"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_conda_env_python310 "$env_name" "$build_file_path" "$normal_deps" "$optional_deps" "$external_provider_deps"
|
|
||||||
|
|
@ -7,13 +7,11 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
from logging.config import dictConfig
|
from logging.config import dictConfig
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.errors import MarkupError
|
from rich.errors import MarkupError
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
from termcolor import cprint
|
|
||||||
|
|
||||||
from llama_stack.core.datatypes import LoggingConfig
|
from llama_stack.core.datatypes import LoggingConfig
|
||||||
|
|
||||||
|
|
@ -66,7 +64,6 @@ def config_to_category_levels(category: str, level: str):
|
||||||
category_levels["root"] = level_value
|
category_levels["root"] = level_value
|
||||||
elif category in CATEGORIES:
|
elif category in CATEGORIES:
|
||||||
category_levels[category] = level_value
|
category_levels[category] = level_value
|
||||||
logging.info(f"Setting '{category}' category to level '{level}'.")
|
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Unknown logging category: {category}. No changes made.")
|
logging.warning(f"Unknown logging category: {category}. No changes made.")
|
||||||
return category_levels
|
return category_levels
|
||||||
|
|
@ -256,7 +253,6 @@ def get_logger(
|
||||||
|
|
||||||
env_config = os.environ.get("LLAMA_STACK_LOGGING", "")
|
env_config = os.environ.get("LLAMA_STACK_LOGGING", "")
|
||||||
if env_config:
|
if env_config:
|
||||||
cprint(f"Environment variable LLAMA_STACK_LOGGING found: {env_config}", color="yellow", file=sys.stderr)
|
|
||||||
_category_levels.update(parse_environment_config(env_config))
|
_category_levels.update(parse_environment_config(env_config))
|
||||||
|
|
||||||
log_file = os.environ.get("LLAMA_STACK_LOG_FILE")
|
log_file = os.environ.get("LLAMA_STACK_LOG_FILE")
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,15 @@ from openai.types.chat import (
|
||||||
from openai.types.chat import (
|
from openai.types.chat import (
|
||||||
ChatCompletionContentPartTextParam as OpenAIChatCompletionContentPartTextParam,
|
ChatCompletionContentPartTextParam as OpenAIChatCompletionContentPartTextParam,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
from openai.types.chat import (
|
from openai.types.chat import (
|
||||||
ChatCompletionMessageFunctionToolCall as OpenAIChatCompletionMessageFunctionToolCall,
|
ChatCompletionMessageFunctionToolCall as OpenAIChatCompletionMessageFunctionToolCall,
|
||||||
)
|
)
|
||||||
|
except ImportError:
|
||||||
|
from openai.types.chat.chat_completion_message_tool_call import (
|
||||||
|
ChatCompletionMessageToolCall as OpenAIChatCompletionMessageFunctionToolCall,
|
||||||
|
)
|
||||||
from openai.types.chat import (
|
from openai.types.chat import (
|
||||||
ChatCompletionMessageParam as OpenAIChatCompletionMessage,
|
ChatCompletionMessageParam as OpenAIChatCompletionMessage,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ class PostgresKVStoreConfig(CommonConfig):
|
||||||
db: str = "llamastack"
|
db: str = "llamastack"
|
||||||
user: str
|
user: str
|
||||||
password: str | None = None
|
password: str | None = None
|
||||||
|
ssl_mode: str | None = None
|
||||||
|
ca_cert_path: str | None = None
|
||||||
table_name: str = "llamastack_kvstore"
|
table_name: str = "llamastack_kvstore"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ class PostgresKVStoreImpl(KVStore):
|
||||||
database=self.config.db,
|
database=self.config.db,
|
||||||
user=self.config.user,
|
user=self.config.user,
|
||||||
password=self.config.password,
|
password=self.config.password,
|
||||||
|
sslmode=self.config.ssl_mode,
|
||||||
|
sslrootcert=self.config.ca_cert_path,
|
||||||
)
|
)
|
||||||
self.conn.autocommit = True
|
self.conn.autocommit = True
|
||||||
self.cursor = self.conn.cursor(cursor_factory=DictCursor)
|
self.cursor = self.conn.cursor(cursor_factory=DictCursor)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ dependencies = [
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"llama-stack-client>=0.2.17",
|
"llama-stack-client>=0.2.17",
|
||||||
"llama-api-client>=0.1.2",
|
"llama-api-client>=0.1.2",
|
||||||
"openai>=1.99.6",
|
"openai>=1.99.6,<1.100.0",
|
||||||
"prompt-toolkit",
|
"prompt-toolkit",
|
||||||
"python-dotenv",
|
"python-dotenv",
|
||||||
"python-jose[cryptography]",
|
"python-jose[cryptography]",
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,10 @@ else
|
||||||
EXTRA_PARAMS=""
|
EXTRA_PARAMS=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
THIS_DIR=$(dirname "$0")
|
||||||
|
ROOT_DIR="$THIS_DIR/.."
|
||||||
|
cd $ROOT_DIR
|
||||||
|
|
||||||
# Set recording directory
|
# Set recording directory
|
||||||
if [[ "$RUN_VISION_TESTS" == "true" ]]; then
|
if [[ "$RUN_VISION_TESTS" == "true" ]]; then
|
||||||
export LLAMA_STACK_TEST_RECORDING_DIR="tests/integration/recordings/vision"
|
export LLAMA_STACK_TEST_RECORDING_DIR="tests/integration/recordings/vision"
|
||||||
|
|
@ -140,10 +144,27 @@ else
|
||||||
export LLAMA_STACK_TEST_RECORDING_DIR="tests/integration/recordings"
|
export LLAMA_STACK_TEST_RECORDING_DIR="tests/integration/recordings"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# check if "llama" and "pytest" are available. this script does not use `uv run` given
|
||||||
|
# it can be used in a pre-release environment where we have not been able to tell
|
||||||
|
# uv about pre-release dependencies properly (yet).
|
||||||
|
if ! command -v llama &> /dev/null; then
|
||||||
|
echo "llama could not be found, ensure llama-stack is installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v pytest &> /dev/null; then
|
||||||
|
echo "pytest could not be found, ensure pytest is installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Start Llama Stack Server if needed
|
# Start Llama Stack Server if needed
|
||||||
if [[ "$STACK_CONFIG" == *"server:"* ]]; then
|
if [[ "$STACK_CONFIG" == *"server:"* ]]; then
|
||||||
|
# check if server is already running
|
||||||
|
if curl -s http://localhost:8321/v1/health 2>/dev/null | grep -q "OK"; then
|
||||||
|
echo "Llama Stack Server is already running, skipping start"
|
||||||
|
else
|
||||||
echo "=== Starting Llama Stack Server ==="
|
echo "=== Starting Llama Stack Server ==="
|
||||||
nohup uv run llama stack run ci-tests --image-type venv > server.log 2>&1 &
|
nohup llama stack run ci-tests --image-type venv > server.log 2>&1 &
|
||||||
|
|
||||||
echo "Waiting for Llama Stack Server to start..."
|
echo "Waiting for Llama Stack Server to start..."
|
||||||
for i in {1..30}; do
|
for i in {1..30}; do
|
||||||
|
|
@ -161,6 +182,7 @@ if [[ "$STACK_CONFIG" == *"server:"* ]]; then
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
echo "=== Running Integration Tests ==="
|
echo "=== Running Integration Tests ==="
|
||||||
|
|
@ -180,7 +202,7 @@ fi
|
||||||
if [[ "$RUN_VISION_TESTS" == "true" ]]; then
|
if [[ "$RUN_VISION_TESTS" == "true" ]]; then
|
||||||
echo "Running vision tests..."
|
echo "Running vision tests..."
|
||||||
set +e
|
set +e
|
||||||
uv run pytest -s -v tests/integration/inference/test_vision_inference.py \
|
pytest -s -v tests/integration/inference/test_vision_inference.py \
|
||||||
--stack-config="$STACK_CONFIG" \
|
--stack-config="$STACK_CONFIG" \
|
||||||
-k "$PYTEST_PATTERN" \
|
-k "$PYTEST_PATTERN" \
|
||||||
--vision-model=ollama/llama3.2-vision:11b \
|
--vision-model=ollama/llama3.2-vision:11b \
|
||||||
|
|
@ -248,7 +270,7 @@ echo "=== Running all collected tests in a single pytest command ==="
|
||||||
echo "Total test files: $(echo $TEST_FILES | wc -w)"
|
echo "Total test files: $(echo $TEST_FILES | wc -w)"
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
uv run pytest -s -v $TEST_FILES \
|
pytest -s -v $TEST_FILES \
|
||||||
--stack-config="$STACK_CONFIG" \
|
--stack-config="$STACK_CONFIG" \
|
||||||
-k "$PYTEST_PATTERN" \
|
-k "$PYTEST_PATTERN" \
|
||||||
--text-model="$TEXT_MODEL" \
|
--text-model="$TEXT_MODEL" \
|
||||||
|
|
|
||||||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -1856,7 +1856,7 @@ requires-dist = [
|
||||||
{ name = "llama-api-client", specifier = ">=0.1.2" },
|
{ name = "llama-api-client", specifier = ">=0.1.2" },
|
||||||
{ name = "llama-stack-client", specifier = ">=0.2.17" },
|
{ name = "llama-stack-client", specifier = ">=0.2.17" },
|
||||||
{ name = "llama-stack-client", marker = "extra == 'ui'", specifier = ">=0.2.17" },
|
{ name = "llama-stack-client", marker = "extra == 'ui'", specifier = ">=0.2.17" },
|
||||||
{ name = "openai", specifier = ">=1.99.6" },
|
{ name = "openai", specifier = ">=1.99.6,<1.100.0" },
|
||||||
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30.0" },
|
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30.0" },
|
||||||
{ name = "opentelemetry-sdk", specifier = ">=1.30.0" },
|
{ name = "opentelemetry-sdk", specifier = ">=1.30.0" },
|
||||||
{ name = "pandas", marker = "extra == 'ui'" },
|
{ name = "pandas", marker = "extra == 'ui'" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue