總覽
總覽
專案網址: 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
。如果我們將上面的範例轉換成 argements 的用法,就會變成這樣:
@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 讓使用者輸入
為什麼要提示 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 字串。此外,歷史指令無法從 session 讀取。因此我們將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
test env: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