项目概述
项目概述
项目 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
将上下文传递给该命令组,以便进一步使用。
创建命令组的子命令
然后,我们在cli
下添加第一个子命令help
:
# 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
,这样我们就创建了 origin 组的子组。
我们使用@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 的命令:
图像
自定义命令
添加选项
你可以在命令中添加一些选项,这些选项将像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
添加参数
在本项目中,我们用选项取代了所有参数用法。但我们仍然在此介绍参数用法。与选项不同,arguments 的用法与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
时都会显示提示。我们使用几个提示串联起来,这样看起来就像一个连续的对话。这样可以确保用户按照我们希望的顺序输入数据。在本例中,我们需要用户先选择一个 Collections,然后获取该 Collections 下的所有分区,再显示给用户选择。
添加选择
有时您希望用户只输入有限范围/类型的值,您可以将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 供用户输入
为什么要提示 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
- 当使用
ctrl C
退出时,捕捉KeyboardInterrupt
的错误。
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 的 shell 自动完成不同,我们的项目对命令行进行包装,并使用循环来获取用户输入,以实现提示命令行。因此,我们需要在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 命令行会显示出来,但它只返回版本信息,如果你输入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
的入口点是milvus_cli/scripts/milvus_cli.py
中的runCliPrompt
函数。
构建
升级
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/*
通过 Github 工作流程实现 CI/CD
参考: 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 是一款强大的工具,能够为大量人工智能和向量相似性搜索应用提供动力。要了解有关该项目的更多信息,请查看以下资源:
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word