Обзор
Обзор
URL проекта: https://github.com/milvus-io/milvus_cli
Подготовка: Python3.8
, Click 8.0.x
Группировка команд
Создание команды
import click
from utils import PyOrm
@click.group(no_args_is_help=False, add_help_option=False, invoke_without_command=True)
@click.pass_context
def cli(ctx):
"""Milvus CLI"""
ctx.obj = PyOrm() # PyOrm is a util class which wraps the milvus python SDK. You can pass any class instance here. Any command function passed by @click.obj can call it.
if __name__ == '__main__':
cli()
Как показано в коде выше, мы используем @click.group()
для создания группы команд cli
в качестве точки входа. Чтобы реализовать подсказку CLI, нам нужно отключить справочные сообщения для входа, поэтому мы добавляем no_args_is_help=False
, add_help_option=False
и invoke_without_command=True
. И ничего не будет выведено, если мы введем cli
только в терминале.
Кроме того, мы используем @click.pass_context
для передачи контекста в эту группу для дальнейшего использования.
Создание подкоманды группы команд
Затем мы добавляем первую подкоманду help
в группу cli
:
# Print the help message of specified command.
def print_help_msg(command):
with click.Context(command) as ctx:
click.echo(command.get_help(ctx))
# Use @cli.command() to create a sub command of cli.
@cli.command()
def help():
"""Show help messages."""
# Print help message of cli.
click.echo(print_help_msg(cli))
Теперь мы можем использовать cli help
в терминале:
$ python milvus_cli/scripts/milvus_cli.py help
Создание подгруппы группы команд
Мы хотим иметь не только подкоманду cli help
, но и подгруппу команд cli list collection
, cli list partition
и cli list indexes
.
Сначала мы создадим подгруппу команд list
, здесь мы можем передать первый параметр @cli.group
в качестве имени команды вместо использования имени функции defult, так что мы можем уменьшить дублирование имен функций.
Внимание, здесь мы используем @cli.group()
вместо @click.group
, чтобы создать подгруппу группы происхождения.
Затем мы используем @click.pass_obj
для передачи context.obj
подкомандам этой подгруппы.
@cli.group('list', no_args_is_help=False)
@click.pass_obj
def listDetails(obj):
"""List collections, partitions and indexes."""
pass
Затем мы добавляем некоторые подкоманды в эту подгруппу с помощью @listDetails.command()
(не @cli.command()
). Это просто пример, вы можете проигнорировать его, и мы обсудим его позже.
@listDetails.command()
@click.option('--timeout', 'timeout', help="[Optional] - An optional duration of time in seconds to allow for the RPC. When timeout is set to None, client waits until server response or error occur.", default=None)
@click.option('--show-loaded', 'showLoaded', help="[Optional] - Only show loaded collections.", default=False)
@click.pass_obj
def collections(obj, timeout, showLoaded):
"""List all collections."""
try:
obj.checkConnection()
click.echo(obj.listCollections(timeout, showLoaded))
except Exception as e:
click.echo(message=e, err=True)
@listDetails.command()
@click.option('-c', '--collection', 'collection', help='The name of collection.', default='')
@click.pass_obj
def partitions(obj, collection):
"""List all partitions of the specified collection."""
try:
obj.checkConnection()
validateParamsByCustomFunc(
obj.getTargetCollection, 'Collection Name Error!', collection)
click.echo(obj.listPartitions(collection))
except Exception as e:
click.echo(message=e, err=True)
После всего этого у нас есть команда miltigroup, которая выглядит следующим образом:
image
Пользовательская команда
Добавление опций
Вы можете добавить некоторые опции к команде, которые будут использоваться как cli --test-option value
.
Вот пример, мы добавляем три параметра alias
, host
и port
для указания адреса для подключения к Milvus.
Первые два параметра определяют краткое и полное имя опции, третий параметр - имя переменной, параметр help
- краткое справочное сообщение, параметр default
- значение по умолчанию, а type
- тип значения.
Значения всех опций будут переданы в функцию в порядке их определения.
@cli.command(no_args_is_help=False)
@click.option('-a', '--alias', 'alias', help="Milvus link alias name, default is `default`.", default='default', type=str)
@click.option('-h', '--host', 'host', help="Host name, default is `127.0.0.1`.", default='127.0.0.1', type=str)
@click.option('-p', '--port', 'port', help="Port, default is `19530`.", default=19530, type=int)
@click.pass_obj
def connect(obj, alias, host, port):
pass
Добавление опций флага
Выше мы использовали опции для передачи значения, но иногда нам просто нужен флаг в виде булевого значения.
Как показано в примере ниже, опция autoId
является опцией флага и не передает никаких данных в функцию, поэтому мы можем использовать ее как cli create collection -c c_name -p p_name -a
.
@createDetails.command('collection')
@click.option('-c', '--collection-name', 'collectionName', help='Collection name to be created.', default='')
@click.option('-p', '--schema-primary-field', 'primaryField', help='Primary field name.', default='')
@click.option('-a', '--schema-auto-id', 'autoId', help='Enable auto id.', default=False, is_flag=True)
@click.pass_obj
def createCollection(obj, collectionName, primaryField, autoId, description, fields):
pass
Добавляем аргументы
В этом проекте мы заменили использование аргументов на использование опций. Но здесь мы все же представим использование аргументов. В отличие от опций, аргументы используются как cli COMMAND [OPTIONS] ARGUEMENTS
. Если мы преобразуем пример выше в использование аргументов, это будет выглядеть так:
@createDetails.command('collection')
@click.argument('collectionName')
@click.option('-p', '--schema-primary-field', 'primaryField', help='Primary field name.', default='')
@click.option('-a', '--schema-auto-id', 'autoId', help='Enable auto id.', default=False, is_flag=True)
@click.pass_obj
def createCollection(obj, collectionName, primaryField, autoId, description, fields):
pass
Тогда использование должно выглядеть так: cli create collection c_name -p p_name -a
.
Добавьте полное сообщение справки
Как мы определили краткое сообщение справки выше, мы можем определить полное сообщение справки в функции:
@cli.command(no_args_is_help=False)
@click.option('-a', '--alias', 'alias', help="Milvus link alias name, default is `default`.", default='default', type=str)
@click.option('-h', '--host', 'host', help="Host name, default is `127.0.0.1`.", default='127.0.0.1', type=str)
@click.option('-p', '--port', 'port', help="Port, default is `19530`.", default=19530, type=int)
@click.pass_obj
def connect(obj, alias, host, port):
"""
Connect to Milvus.
Example:
milvus_cli > connect -h 127.0.0.1 -p 19530 -a default
"""
try:
obj.connect(alias, host, port)
except Exception as e:
click.echo(message=e, err=True)
else:
click.echo("Connect Milvus successfully!")
click.echo(obj.showConnection(alias))
Первый блок внутри функции - это справочное сообщение, которое будет выведено после ввода cli connect --help
.
milvus_cli > connect --help
Usage: milvus_cli.py connect [OPTIONS]
Connect to Milvus.
Example:
milvus_cli > connect -h 127.0.0.1 -p 19530 -a default
Options:
-a, --alias TEXT Milvus link alias name, default is `default`.
-h, --host TEXT Host name, default is `127.0.0.1`.
-p, --port INTEGER Port, default is `19530`.
--help Show this message and exit.
Добавить подтверждение
Иногда нам нужно, чтобы пользователь подтвердил какое-то действие, особенно удаление чего-либо. Мы можем добавить click.confirm
, чтобы сделать паузу и попросить пользователя подтвердить действие:
@deleteSth.command('collection')
@click.option('-c', '--collection', 'collectionName', help='The name of collection to be deleted.', default='')
@click.option('-t', '--timeout', 'timeout', help='An optional duration of time in seconds to allow for the RPC. If timeout is set to None, the client keeps waiting until the server responds or an error occurs.', default=None, type=int)
@click.pass_obj
def deleteCollection(obj, collectionName, timeout):
"""
Drops the collection together with its index files.
Example:
milvus_cli > delete collection -c car
"""
click.echo(
"Warning!\nYou are trying to delete the collection with data. This action cannot be undone!\n")
if not click.confirm('Do you want to continue?'):
return
pass
Как показано в примере выше, подтверждение разговора будет выглядеть так: Aborted!ant to continue? [y/N]:
.
Добавить подсказки
Для реализации подсказок нам нужно всего лишь добавить click.prompt
.
@cli.command()
@click.pass_obj
def query(obj):
"""
Query with a set of criteria, and results in a list of records that match the query exactly.
"""
collectionName = click.prompt(
'Collection name', type=click.Choice(obj._list_collection_names()))
expr = click.prompt('The query expression(field_name in [x,y])')
partitionNames = click.prompt(
f'The names of partitions to search(split by "," if multiple) {obj._list_partition_names(collectionName)}', default='')
outputFields = click.prompt(
f'Fields to return(split by "," if multiple) {obj._list_field_names(collectionName)}', default='')
timeout = click.prompt('timeout', default='')
pass
Подсказка будет появляться при каждом click.prompt
. Мы используем несколько подсказок последовательно, чтобы это выглядело как непрерывный разговор. Это гарантирует, что пользователь будет вводить данные в нужном нам порядке. В данном случае нам нужно, чтобы пользователь сначала выбрал коллекцию, а затем получил все разделы из этой коллекции и показал их пользователю для выбора.
Добавить варианты
Иногда вы хотите, чтобы пользователь просто ввел ограниченный диапазон/тип значений, вы можете добавить type=click.Choice([<any>])
к click.prompt
, click.options
и т. д..
Например,
collectionName = click.prompt(
'Collection name', type=click.Choice(['collection_1', 'collection_2']))
Тогда пользователь может вводить только collection_1
или collection_2
, при любом другом вводе будет выдана ошибка.
Добавить прозрачный экран
Для этого можно использовать click.clear()
.
@cli.command()
def clear():
"""Clear screen."""
click.clear()
Дополнительные советы
- Значение по умолчанию -
None
, поэтому бессмысленно указывать значение по умолчаниюNone
. А значение по умолчаниюNone
приведет к постоянному отображениюclick.prompt
, если вы хотите оставить значение пустым, чтобы перепрыгнуть через него.
Реализация подсказки CLI для ввода пользователем
Зачем нужен prompt CLI
Для работы с базой данных нам необходимо постоянное соединение с экземпляром. Если мы используем режим командной строки origin, соединение будет обрываться после каждой выполненной команды. Мы также хотим хранить некоторые данные при использовании CLI и очищать их после выхода.
Реализуйте
- Используйте
while True
для постоянного прослушивания пользовательского ввода.
def runCliPrompt():
while True:
astr = input('milvus_cli > ')
try:
cli(astr.split())
except SystemExit:
# trap argparse error message
# print('error', SystemExit)
continue
if __name__ == '__main__':
runCliPrompt()
- Использование
input
приведет к тому, что клавиши со стрелкамиup
,down
,left
,right
,tab
и некоторые другие клавиши будут автоматически преобразовываться в строку Acsii. Кроме того, команды истории не могут быть прочитаны из сессии. Поэтому мы добавимreadline
в функциюrunCliPrompt
.
def runCliPrompt():
while True:
import readline
readline.set_completer_delims(' \t\n;')
astr = input('milvus_cli > ')
try:
cli(astr.split())
except SystemExit:
# trap argparse error message
# print('error', SystemExit)
continue
- Добавьте
quit
CLI.
@cli.command('exit')
def quitapp():
"""Exit the CLI."""
global quitapp
quitapp = True
quitapp = False # global flag
def runCliPrompt():
while not quitapp:
import readline
readline.set_completer_delims(' \t\n;')
astr = input('milvus_cli > ')
try:
cli(astr.split())
except SystemExit:
# trap argparse error message
# print('error', SystemExit)
continue
- Перехват ошибки
KeyboardInterrupt
при использованииctrl C
для выхода.
def runCliPrompt():
try:
while not quitapp:
import readline
readline.set_completer_delims(' \t\n;')
astr = input('milvus_cli > ')
try:
cli(astr.split())
except SystemExit:
# trap argparse error message
# print('error', SystemExit)
continue
except KeyboardInterrupt:
sys.exit(0)
- После всех этих действий CLI теперь выглядит так:
milvus_cli >
milvus_cli > connect
+-------+-----------+
| Host | 127.0.0.1 |
| Port | 19530 |
| Alias | default |
+-------+-----------+
milvus_cli > help
Usage: [OPTIONS] COMMAND [ARGS]...
Milvus CLI
Commands:
clear Clear screen.
connect Connect to Milvus.
create Create collection, partition and index.
delete Delete specified collection, partition and index.
describe Describe collection or partition.
exit Exit the CLI.
help Show help messages.
import Import data from csv file with headers and insert into target...
list List collections, partitions and indexes.
load Load specified collection.
query Query with a set of criteria, and results in a list of...
release Release specified collection.
search Conducts a vector similarity search with an optional boolean...
show Show connection, loading_progress and index_progress.
version Get Milvus CLI version.
milvus_cli > exit
Ручная реализация автозаполнения
В отличие от автозаполнения оболочки в click, наш проект оборачивает командную строку и использует цикл для получения пользовательского ввода для реализации быстрого ввода командной строки. Поэтому нам нужно привязать компилятор к readline
.
class Completer(object):
RE_SPACE = re.compile('.*\s+$', re.M)
CMDS_DICT = {
'clear': [],
'connect': [],
'create': ['collection', 'partition', 'index'],
'delete': ['collection', 'partition', 'index'],
'describe': ['collection', 'partition'],
'exit': [],
'help': [],
'import': [],
'list': ['collections', 'partitions', 'indexes'],
'load': [],
'query': [],
'release': [],
'search': [],
'show': ['connection', 'index_progress', 'loading_progress'],
'version': [],
}
def __init__(self) -> None:
super().__init__()
self.COMMANDS = list(self.CMDS_DICT.keys())
self.createCompleteFuncs(self.CMDS_DICT)
def createCompleteFuncs(self, cmdDict):
for cmd in cmdDict:
sub_cmds = cmdDict[cmd]
complete_example = self.makeComplete(cmd, sub_cmds)
setattr(self, 'complete_%s' % cmd, complete_example)
def makeComplete(self, cmd, sub_cmds):
def f_complete(args):
f"Completions for the {cmd} command."
if not args:
return self._complete_path('.')
if len(args) <= 1 and not cmd == 'import':
return self._complete_2nd_level(sub_cmds, args[-1])
return self._complete_path(args[-1])
return f_complete
def _listdir(self, root):
"List directory 'root' appending the path separator to subdirs."
res = []
for name in os.listdir(root):
path = os.path.join(root, name)
if os.path.isdir(path):
name += os.sep
res.append(name)
return res
def _complete_path(self, path=None):
"Perform completion of filesystem path."
if not path:
return self._listdir('.')
dirname, rest = os.path.split(path)
tmp = dirname if dirname else '.'
res = [os.path.join(dirname, p)
for p in self._listdir(tmp) if p.startswith(rest)]
# more than one match, or single match which does not exist (typo)
if len(res) > 1 or not os.path.exists(path):
return res
# resolved to a single directory, so return list of files below it
if os.path.isdir(path):
return [os.path.join(path, p) for p in self._listdir(path)]
# exact file match terminates this completion
return [path + ' ']
def _complete_2nd_level(self, SUB_COMMANDS=[], cmd=None):
if not cmd:
return [c + ' ' for c in SUB_COMMANDS]
res = [c for c in SUB_COMMANDS if c.startswith(cmd)]
if len(res) > 1 or not (cmd in SUB_COMMANDS):
return res
return [cmd + ' ']
def complete(self, text, state):
"Generic readline completion entry point."
buffer = readline.get_line_buffer()
line = readline.get_line_buffer().split()
# show all commands
if not line:
return [c + ' ' for c in self.COMMANDS][state]
# account for last argument ending in a space
if self.RE_SPACE.match(buffer):
line.append('')
# resolve command to the implementation function
cmd = line[0].strip()
if cmd in self.COMMANDS:
impl = getattr(self, 'complete_%s' % cmd)
args = line[1:]
if args:
return (impl(args) + [None])[state]
return [cmd + ' '][state]
results = [
c + ' ' for c in self.COMMANDS if c.startswith(cmd)] + [None]
return results[state]
После определения Completer
мы можем связать его с readline:
comp = Completer()
def runCliPrompt():
try:
while not quitapp:
import readline
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(comp.complete)
astr = input('milvus_cli > ')
try:
cli(astr.split())
except SystemExit:
# trap argparse error message
# print('error', SystemExit)
continue
except KeyboardInterrupt:
sys.exit(0)
Добавить одноразовую опцию
Для командной строки быстрого доступа иногда мы не хотим полностью запускать скрипты, чтобы получить некоторую информацию, например версию. Хорошим примером является Python
, когда вы вводите python
в терминале, появляется командная строка promtp, но она возвращает только сообщение о версии и не вводит скрипты prompt, если вы вводите python -V
. Поэтому мы можем использовать sys.args
в нашем коде для реализации.
def runCliPrompt():
args = sys.argv
if args and (args[-1] == '--version'):
print(f"Milvus Cli v{getPackageVersion()}")
return
try:
while not quitapp:
import readline
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(comp.complete)
astr = input('milvus_cli > ')
try:
cli(astr.split())
except SystemExit:
# trap argparse error message
# print('error', SystemExit)
continue
except KeyboardInterrupt:
sys.exit(0)
if __name__ == '__main__':
runCliPrompt()
При первом запуске CLI-скриптов мы получим sys.args
перед циклом. Если последним аргументом будет --version
, то код вернет версию пакета, не запуская цикл.
Это будет полезно после того, как мы соберем коды в виде пакета. Пользователь может набрать milvus_cli
, чтобы перейти к приглашению CLI, или набрать milvus_cli --version
, чтобы получить только версию.
Сборка и выпуск
Наконец, мы хотим собрать пакет и выпустить его с помощью PYPI. Чтобы пользователь мог просто использовать pip install <package name>
для установки.
Локальная установка для тестирования
Прежде чем публиковать пакет в PYPI, вы можете захотеть установить его локально для некоторых тестов.
В этом случае вы можете просто cd
в каталог пакета и запустить pip install -e .
(не забудьте про .
).
Создание файлов пакета
См.: https://packaging.python.org/tutorials/packaging-projects/
Структура пакета должна выглядеть следующим образом:
package_example/
├── LICENSE
├── README.md
├── setup.py
├── src/
│ ├── __init__.py
│ ├── main.py
│ └── scripts/
│ ├── __init__.py
│ └── example.py
└── tests/
Создайте каталог пакета
Создайте каталог Milvus_cli
со структурой, приведенной ниже:
Milvus_cli/
├── LICENSE
├── README.md
├── setup.py
├── milvus_cli/
│ ├── __init__.py
│ ├── main.py
│ ├── utils.py
│ └── scripts/
│ ├── __init__.py
│ └── milvus_cli.py
└── dist/
Напишите код входа
Запись скрипта должна находиться в каталоге Milvus_cli/milvus_cli/scripts
, а каталог Milvus_cli/milvus_cli/scripts/milvus_cli.py
должен иметь следующий вид:
import sys
import os
import click
from utils import PyOrm, Completer
pass_context = click.make_pass_decorator(PyOrm, ensure=True)
@click.group(no_args_is_help=False, add_help_option=False, invoke_without_command=True)
@click.pass_context
def cli(ctx):
"""Milvus CLI"""
ctx.obj = PyOrm()
"""
...
Here your code.
...
"""
@cli.command('exit')
def quitapp():
"""Exit the CLI."""
global quitapp
quitapp = True
quitapp = False # global flag
comp = Completer()
def runCliPrompt():
args = sys.argv
if args and (args[-1] == '--version'):
print(f"Milvus Cli v{getPackageVersion()}")
return
try:
while not quitapp:
import readline
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(comp.complete)
astr = input('milvus_cli > ')
try:
cli(astr.split())
except SystemExit:
# trap argparse error message
# print('error', SystemExit)
continue
except Exception as e:
click.echo(
message=f"Error occurred!\n{str(e)}", err=True)
except KeyboardInterrupt:
sys.exit(0)
if __name__ == '__main__':
runCliPrompt()
Отредактируйте setup.py
from setuptools import setup, find_packages
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name='milvus_cli',
version='0.1.6',
author='Milvus Team',
author_email='milvus-team@zilliz.com',
url='https://github.com/milvus-io/milvus_cli',
description='CLI for Milvus',
long_description=long_description,
long_description_content_type='text/markdown',
license='Apache-2.0',
packages=find_packages(),
include_package_data=True,
install_requires=[
'Click==8.0.1',
'pymilvus==2.0.0rc5',
'tabulate==0.8.9'
],
entry_points={
'console_scripts': [
'milvus_cli = milvus_cli.scripts.milvus_cli:runCliPrompt',
],
},
python_requires='>=3.8'
)
Несколько советов:
- Мы используем содержимое
README.md
в качестве длинного описания пакета. - Добавьте все зависимости в
install_requires
. - Укажите
entry_points
. В данном случае мы устанавливаемmilvus_cli
в качестве дочернего элементаconsole_scripts
, чтобы мы могли ввестиmilvus_cli
в качестве команды непосредственно после установки этого пакета. А точкой входаmilvus_cli
является функцияrunCliPrompt
вmilvus_cli/scripts/milvus_cli.py
.
Сборка
Обновите пакет
build
:python3 -m pip install --upgrade build
Запустите build:
python -m build --sdist --wheel --outdir dist/ .
В каталоге
dist/
будут сгенерированы два файла:
dist/
example_package_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
example_package_YOUR_USERNAME_HERE-0.0.1.tar.gz
Опубликовать релиз
Обратитесь к: https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives
- Обновить пакет
twine
:python3 -m pip install --upgrade twine
- Загрузите на
PYPI
тестовую среду:python3 -m twine upload --repository testpypi dist/*
- Загрузить на
PYPI
:python3 -m twine upload dist/*
CI/CD с помощью рабочих процессов Github
Ссылайтесь на: https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
Нам нужен способ автоматической загрузки активов, он может собирать пакеты и загружать их в релизы на github и PYPI.
(По некоторым причинам мы хотим, чтобы рабочий процесс публиковал только релиз для тестирования PYPI).
# This is a basic workflow to help you get started with Actions
name: Update the release's assets after it published
# Controls when the workflow will run
on:
release:
# The workflow will run after release published
types: [published]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
architecture: 'x64'
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Clean dist/
run: |
sudo rm -fr dist/*
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
# Update target github release's assets
- name: Update assets
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: ./dist/*
- name: Publish distribution 📦 to Test PyPI
if: contains(github.ref, 'beta') && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
packages_dir: dist/
verify_metadata: false
Подробнее о Milvus
Milvus - это мощный инструмент, способный обеспечить работу огромного количества приложений для искусственного интеллекта и поиска векторного сходства. Чтобы узнать больше о проекте, ознакомьтесь со следующими ресурсами:
- Группировка команд
- Пользовательская команда
- Реализация подсказки CLI для ввода пользователем
- Ручная реализация автозаполнения
- Добавить одноразовую опцию
- Сборка и выпуск
- Подробнее о Milvus
On This Page
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word