Visión general
Visión general
URL del proyecto: https://github.com/milvus-io/milvus_cli
Preparación: Python3.8
, Click 8.0.x
Agrupar comandos
Crear un comando
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()
Como en el código anterior, usamos @click.group()
para crear un grupo de comandos cli
como punto de entrada. Para implementar un prompt CLI necesitamos deshabilitar los mensajes de ayuda para la entrada, así que añadimos no_args_is_help=False
, add_help_option=False
y invoke_without_command=True
. Y no se imprimirá nada si introducimos cli
sólo en el terminal.
Además usamos @click.pass_context
para pasar un contexto a este grupo para su uso posterior.
Crear un subcomando del grupo de comandos
Luego agregamos el primer sub comando help
bajo 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))
Ahora podemos usar cli help
en la terminal:
$ python milvus_cli/scripts/milvus_cli.py help
Crear un subgrupo de un grupo de comandos
No sólo queremos tener un subcomando como cli help
, sino que también necesitamos un subgrupo de comandos como cli list collection
, cli list partition
y cli list indexes
.
Primero creamos un comando de subgrupo list
, aquí podemos pasar el primer parámetro a @cli.group
como el nombre del comando en lugar de usar el nombre de la función predeterminada, así podemos reducir la duplicación de nombres de funciones.
Atención aquí, usamos @cli.group()
en lugar de @click.group
para crear un subgrupo del grupo de origen.
Usamos @click.pass_obj
para pasar context.obj
a los subcomandos de este subgrupo.
@cli.group('list', no_args_is_help=False)
@click.pass_obj
def listDetails(obj):
"""List collections, partitions and indexes."""
pass
Luego agregamos algunos sub comandos en este sub grupo por @listDetails.command()
(no @cli.command()
). Esto es solo un ejemplo, puedes ignorar el implemento y lo discutiremos mas tarde.
@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)
Después de todo esto completo, tenemos un miltigroup comandos que se parecen:
imagen
Personalizar un comando
Añadir opciones
Puede añadir algunas opciones a un comando que se utilizará como cli --test-option value
.
Aquí hay un ejemplo, agregamos tres opciones alias
, host
y port
para especificar una dirección para conectarse a Milvus.
Los dos primeros parámetros definen el nombre corto y completo de la opción, el tercer parámetro define el nombre de la variable, el parámetro help
especifica el mensaje corto de ayuda, el parámetro default
especifica el valor por defecto y el type
especifica el tipo de valor.
Y todos los valores de las opciones se pasarán a la función por orden de definición.
@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
Añadir opciones de bandera
Usamos las opciones anteriores para pasar un valor, pero algunas veces sólo necesitamos una bandera como valor booleano.
Como en el ejemplo de abajo, la opción autoId
es una opción de bandera y no pasa ningún dato a la función, así que podemos usarla como 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
Añadir argumentos
En este proyecto reemplazamos el uso de argumentos por el uso de opciones. Pero todavía introducimos el uso de argumentos aquí. A diferencia de las opciones, los argumentos se utilizan como cli COMMAND [OPTIONS] ARGUEMENTS
. Si convertimos el ejemplo anterior en el uso de argumentos, será así:
@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
Entonces el uso debería ser cli create collection c_name -p p_name -a
.
Añadir mensaje de ayuda completo
Así como definimos el mensaje de ayuda corto arriba, podemos definir el mensaje de ayuda completo en la función:
@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))
El primer bloque dentro de la función es el mensaje de ayuda que se imprimirá después de introducir 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.
Añadir confirmación
A veces necesitamos que el usuario confirme alguna acción, especialmente borrar algo. Podemos añadir click.confirm
para pausar y pedir al usuario que confirme:
@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
Como en el ejemplo anterior, una conversación de confirmación se mostrará como Aborted!ant to continue? [y/N]:
.
Añadir avisos
Para implementar avisos sólo necesitamos añadir 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
El aviso se mostrará cada vez que click.prompt
. Utilizaremos varios avisos en serie para que parezca una conversación continua. Esto asegura que el usuario introducirá los datos en el orden que queremos. En este caso necesitamos que el usuario elija una colección primero, y necesitamos obtener todas las particiones bajo esta colección, luego mostrárselas al usuario para que elija.
Añadir opciones
A veces queremos que el usuario sólo introduzca un rango limitado/tipo de valor, podemos añadir type=click.Choice([<any>])
a click.prompt
, click.options
y etc...
Por ejemplo,
collectionName = click.prompt(
'Collection name', type=click.Choice(['collection_1', 'collection_2']))
Entonces el usuario solo puede ingresar collection_1
o collection_2
, se producirá un error si ingresa cualquier otro valor.
Añadir pantalla clara
Puede utilizar click.clear()
para implementarlo.
@cli.command()
def clear():
"""Clear screen."""
click.clear()
Consejos adicionales
- El valor por defecto es
None
, por lo que no tiene sentido si se especifica el valor por defecto comoNone
. Y por defectoNone
causaráclick.prompt
continuamente mostrar si desea dejar un valor vacío para saltar por encima de ella.
Implementar prompt CLI para que el usuario introduzca
Por qué prompt CLI
Para el funcionamiento de la base de datos, necesitamos una conexión continua a una instancia. Si utilizamos el modo de línea de comandos de origen, la conexión se caerá después de cada comando ejecutado. También queremos almacenar algunos datos cuando se utiliza CLI, y limpiarlos después de la salida.
Implementar
- Utilice
while True
para escuchar continuamente la entrada del usuario.
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()
- El uso de
input
sólo causaráup
,down
,left
,right
teclas de flecha,tab
tecla y algunas otras teclas convertidas a cadena Acsii automáticamente. Además, los comandos del historial no se pueden leer desde la sesión. Asi que agregamosreadline
a la funcionrunCliPrompt
.
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
- Agregue
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
- Captura
KeyboardInterrupt
error cuando se utilizactrl C
para salir.
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)
- Despues de todo arreglado, el CLI ahora se ve asi:
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
Implementar manualmente autocompletar
A diferencia de click's shell autocomplete, nuestro proyecto envuelve la linea de comandos y usa un bucle para obtener la entrada del usuario para implementar una linea de comandos prompt. Así que necesitamos enlazar un completador a 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]
Después de definir Completer
podemos enlazarlo con 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)
Añadir opción única
Para la línea de comandos prompt, a veces no queremos ejecutar completamente los scripts para obtener alguna información como la versión. Un buen ejemplo es Python
, cuando escribes python
en la terminal la línea de comandos promtp se mostrará, pero sólo devuelve un mensaje de versión y no entrará en los scripts prompt si escribes python -V
. Así que podemos usar sys.args
en nuestro código para implementarlo.
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()
Obtenemos sys.args
antes del bucle cuando se ejecuta por primera vez en las secuencias de comandos CLI. Si el último argumento es --version
, el código devolverá la versión del paquete sin entrar en el bucle.
Será útil después de construir los códigos como un paquete. El usuario puede escribir milvus_cli
para saltar a un prompt CLI, o escribir milvus_cli --version
para obtener sólo la versión.
Construir y liberar
Finalmente queremos construir un paquete y liberarlo mediante PYPI. Así que el usuario puede simplemente utilizar pip install <package name>
para instalar.
Instalar localmente para probar
Antes de publicar el paquete en PYPI, es posible que desee instalarlo localmente para realizar algunas pruebas.
En este caso, puede simplemente cd
en el directorio del paquete y ejecutar pip install -e .
(No olvide el .
).
Crear los archivos del paquete
Consulte: https://packaging.python.org/tutorials/packaging-projects/
La estructura de un paquete debería ser la siguiente
package_example/
├── LICENSE
├── README.md
├── setup.py
├── src/
│ ├── __init__.py
│ ├── main.py
│ └── scripts/
│ ├── __init__.py
│ └── example.py
└── tests/
Crear el directorio del paquete
Cree el directorio Milvus_cli
con la siguiente estructura:
Milvus_cli/
├── LICENSE
├── README.md
├── setup.py
├── milvus_cli/
│ ├── __init__.py
│ ├── main.py
│ ├── utils.py
│ └── scripts/
│ ├── __init__.py
│ └── milvus_cli.py
└── dist/
Escriba el código de entrada
La entrada del script debe estar en Milvus_cli/milvus_cli/scripts
, y el Milvus_cli/milvus_cli/scripts/milvus_cli.py
debe ser como:
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()
Editar el 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'
)
Algunos consejos aquí:
- Usamos el contenido de
README.md
como descripción larga del paquete. - Añade todas las dependencias a
install_requires
. - Especifique
entry_points
. En este caso, establecemosmilvus_cli
como hijo deconsole_scripts
, para que podamos escribirmilvus_cli
como comando directamente después de instalar este paquete. Y el punto de entrada demilvus_cli
es la funciónrunCliPrompt
enmilvus_cli/scripts/milvus_cli.py
.
Construir
Actualiza el paquete
build
:python3 -m pip install --upgrade build
Ejecute build:
python -m build --sdist --wheel --outdir dist/ .
Se generarán dos archivos en el directorio
dist/
:
dist/
example_package_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
example_package_YOUR_USERNAME_HERE-0.0.1.tar.gz
Publicar versión
Consulte: https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives
- Actualice el paquete
twine
:python3 -m pip install --upgrade twine
- Subir a
PYPI
env. de prueba:python3 -m twine upload --repository testpypi dist/*
- Subir a
PYPI
:python3 -m twine upload dist/*
Flujos de trabajo CI/CD por Github
Consulte: https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
Queremos una manera de subir los activos de forma automática, puede construir los paquetes y subirlos a github libera y PYPI.
(Por alguna razón sólo queremos que el flujo de trabajo sólo publicar la versión para probar 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
Más información sobre Milvus
Milvus es una poderosa herramienta capaz de potenciar una amplia gama de aplicaciones de inteligencia artificial y búsqueda de similitud vectorial. Para saber más sobre el proyecto, consulte los siguientes recursos:
- Agrupar comandos
- Personalizar un comando
- Implementar prompt CLI para que el usuario introduzca
- Implementar manualmente autocompletar
- Añadir opción única
- Construir y liberar
- Más información sobre 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