operx-cli 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {operx_cli-0.3.0 → operx_cli-0.3.2}/PKG-INFO +8 -5
- {operx_cli-0.3.0 → operx_cli-0.3.2}/README.md +7 -4
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/__init__.py +1 -1
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/app.py +6 -1
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/common.py +3 -0
- operx_cli-0.3.2/operx_cli/commands/update.py +37 -0
- operx_cli-0.3.2/operx_cli/self_update.py +79 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/shell.py +53 -17
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/updates.py +6 -2
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/welcome.py +1 -2
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli.egg-info/PKG-INFO +8 -5
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli.egg-info/SOURCES.txt +3 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/pyproject.toml +1 -1
- operx_cli-0.3.2/tests/test_self_update.py +155 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_shell.py +54 -2
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_welcome.py +1 -1
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/AGENTES.md +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/agent_guide.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/browser_login.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/client.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/__init__.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/account.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/ambiental.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/empresas.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/inventario.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/lecturas.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/ordenes.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/personas.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/preventivos.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/raw.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/registros.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/search.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/commands/sst.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/lookup.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/output.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/record_completion.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/settings.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/spreadsheet.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli/values.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli.egg-info/dependency_links.txt +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli.egg-info/entry_points.txt +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli.egg-info/requires.txt +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/operx_cli.egg-info/top_level.txt +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/setup.cfg +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_agentes.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_app.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_cards.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_client.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_inventario_lecturas.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_login.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_logout_and_routes.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_lookup.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_ordenes.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_output.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_personas.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_preventivos.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_record_completion.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_registros.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_settings.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_spreadsheet.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_sst_ambiental.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_updates.py +0 -0
- {operx_cli-0.3.0 → operx_cli-0.3.2}/tests/test_values.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: operx-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: La línea de comandos de OperX: tu empresa desde la terminal, con tu token de API.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -36,8 +36,10 @@ curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install operx-cli
|
|
|
36
36
|
Python 3.10 o más nuevo, también sirve `pipx install operx-cli`. Abre una
|
|
37
37
|
terminal nueva y comprueba con `operx --version`.
|
|
38
38
|
|
|
39
|
-
Para actualizar:
|
|
40
|
-
|
|
39
|
+
Para actualizar: `operx update`. La pantalla de bienvenida avisa cuando hay una
|
|
40
|
+
versión nueva. En Windows la instala otra ventana, que espera a que se cierre
|
|
41
|
+
`operx` (Windows no deja reemplazar un programa abierto); si quedó a medias,
|
|
42
|
+
`uv tool install operx-cli --force --refresh` lo deja bien.
|
|
41
43
|
|
|
42
44
|
## Entrar
|
|
43
45
|
|
|
@@ -87,8 +89,9 @@ operx › ayuda
|
|
|
87
89
|
operx › salir
|
|
88
90
|
```
|
|
89
91
|
|
|
90
|
-
`salir` (o Ctrl+D) termina la sesión sin borrar tu token.
|
|
91
|
-
|
|
92
|
+
`salir` (o Ctrl+D) termina la sesión sin borrar tu token. `ayuda` y Tab
|
|
93
|
+
muestran solo los comandos de los productos que tiene tu empresa. En un script
|
|
94
|
+
o una tubería, `operx` solo muestra la ayuda y no espera a nadie.
|
|
92
95
|
|
|
93
96
|
## Comandos
|
|
94
97
|
|
|
@@ -22,8 +22,10 @@ curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install operx-cli
|
|
|
22
22
|
Python 3.10 o más nuevo, también sirve `pipx install operx-cli`. Abre una
|
|
23
23
|
terminal nueva y comprueba con `operx --version`.
|
|
24
24
|
|
|
25
|
-
Para actualizar:
|
|
26
|
-
|
|
25
|
+
Para actualizar: `operx update`. La pantalla de bienvenida avisa cuando hay una
|
|
26
|
+
versión nueva. En Windows la instala otra ventana, que espera a que se cierre
|
|
27
|
+
`operx` (Windows no deja reemplazar un programa abierto); si quedó a medias,
|
|
28
|
+
`uv tool install operx-cli --force --refresh` lo deja bien.
|
|
27
29
|
|
|
28
30
|
## Entrar
|
|
29
31
|
|
|
@@ -73,8 +75,9 @@ operx › ayuda
|
|
|
73
75
|
operx › salir
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
`salir` (o Ctrl+D) termina la sesión sin borrar tu token.
|
|
77
|
-
|
|
78
|
+
`salir` (o Ctrl+D) termina la sesión sin borrar tu token. `ayuda` y Tab
|
|
79
|
+
muestran solo los comandos de los productos que tiene tu empresa. En un script
|
|
80
|
+
o una tubería, `operx` solo muestra la ayuda y no espera a nadie.
|
|
78
81
|
|
|
79
82
|
## Comandos
|
|
80
83
|
|
|
@@ -12,7 +12,9 @@ from typer.core import TyperGroup
|
|
|
12
12
|
from . import __version__, settings, shell
|
|
13
13
|
from .agent_guide import guide_text
|
|
14
14
|
from .client import ApiError
|
|
15
|
-
from .commands import
|
|
15
|
+
from .commands import (
|
|
16
|
+
account, ambiental, inventario, lecturas, ordenes, personas, preventivos, raw, registros, search, sst, update,
|
|
17
|
+
)
|
|
16
18
|
from .commands.common import CliError, Session
|
|
17
19
|
from .lookup import NoSingleMatch
|
|
18
20
|
from .spreadsheet import SpreadsheetError
|
|
@@ -123,6 +125,9 @@ app.command()(account.logout)
|
|
|
123
125
|
app.command(name="entrar", hidden=True)(account.entrar)
|
|
124
126
|
app.command(name="salir", hidden=True)(account.logout)
|
|
125
127
|
app.command()(account.cuenta)
|
|
128
|
+
app.command()(update.update)
|
|
129
|
+
# In Spanish too, as the other commands are: out of the help.
|
|
130
|
+
app.command(name="actualizar", hidden=True)(update.update)
|
|
126
131
|
app.command()(search.buscar)
|
|
127
132
|
app.command()(raw.api)
|
|
128
133
|
app.add_typer(ordenes.app, name="ordenes")
|
|
@@ -36,6 +36,9 @@ class Session:
|
|
|
36
36
|
# While the interactive prompt runs: what is typed there must not open
|
|
37
37
|
# another one.
|
|
38
38
|
in_shell: bool = False
|
|
39
|
+
# Set by a command that needs this operx closed (`update` on Windows):
|
|
40
|
+
# the prompt ends after it.
|
|
41
|
+
leave_shell: bool = False
|
|
39
42
|
|
|
40
43
|
def address(self):
|
|
41
44
|
return settings.address_of(self.profile)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""`operx update` (self_update.py does the installing)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .. import __version__, self_update, updates
|
|
8
|
+
from .common import CliError, Session
|
|
9
|
+
|
|
10
|
+
NO_INSTALLER = (
|
|
11
|
+
"No encontré uv ni pipx para actualizar. Actualiza como instalaste operx: "
|
|
12
|
+
"uv tool upgrade operx-cli (o pipx upgrade operx-cli)."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def update(ctx: typer.Context):
|
|
17
|
+
"""Instala la versión más nueva de operx."""
|
|
18
|
+
# Asked now, not the welcome's once-a-day answer: the person wants to know.
|
|
19
|
+
latest = updates.fetch_latest()
|
|
20
|
+
if latest and not updates.is_newer(latest, __version__):
|
|
21
|
+
typer.echo(f"Ya tienes la versión más nueva: {__version__}.")
|
|
22
|
+
return
|
|
23
|
+
command = self_update.installer_command(latest)
|
|
24
|
+
if command is None:
|
|
25
|
+
raise CliError(NO_INSTALLER)
|
|
26
|
+
target = f"la {latest}" if latest else "la versión más nueva"
|
|
27
|
+
|
|
28
|
+
if self_update.on_windows():
|
|
29
|
+
self_update.start_window(self_update.window_script(command, os.getpid()))
|
|
30
|
+
typer.echo(f"Instalando {target} en otra ventana. Esta sesión de operx se cierra para que Windows deje reemplazarlo.")
|
|
31
|
+
ctx.find_object(Session).leave_shell = True
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if self_update.run_here(command) != 0:
|
|
35
|
+
raise CliError("No se pudo actualizar: mira el mensaje de arriba.")
|
|
36
|
+
typer.echo(f"Listo: tienes {target}. Abre operx de nuevo para usarla.")
|
|
37
|
+
ctx.find_object(Session).leave_shell = True
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""`operx update`: the newest operx from PyPI, installed the way it came
|
|
2
|
+
(uv, or pipx).
|
|
3
|
+
|
|
4
|
+
On Windows a running program can't be replaced: uv answers "Access is
|
|
5
|
+
denied" and leaves an install that no longer starts. There the work goes to
|
|
6
|
+
a PowerShell window that waits for this operx to close first. Elsewhere it
|
|
7
|
+
happens right here."""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
PACKAGE = "operx-cli"
|
|
15
|
+
# How long the window waits for this operx to close before trying anyway.
|
|
16
|
+
WAIT_SECONDS = 60
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def on_windows():
|
|
20
|
+
return os.name == "nt"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def installer_command(latest=None):
|
|
24
|
+
"""The command that installs the newest operx, or None when neither uv
|
|
25
|
+
nor pipx is here. --force reinstalls without a pin (a pinned install
|
|
26
|
+
never upgrades); --refresh skips uv's cached answers, which can lag
|
|
27
|
+
behind a release (--refresh-package alone put 0.3.0 back the minute
|
|
28
|
+
0.3.1 came out). With `latest`, PyPI's newest, uv must install at least
|
|
29
|
+
that one: it fails rather than put the old one back."""
|
|
30
|
+
if "pipx" in sys.prefix.lower():
|
|
31
|
+
pipx = shutil.which("pipx")
|
|
32
|
+
return [pipx, "upgrade", PACKAGE] if pipx else None
|
|
33
|
+
uv = shutil.which("uv")
|
|
34
|
+
spec = f"{PACKAGE}>={latest}" if latest else PACKAGE
|
|
35
|
+
return [uv, "tool", "install", spec, "--force", "--refresh"] if uv else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _quoted(part):
|
|
39
|
+
"""A PowerShell literal: single quotes, a quote inside doubled."""
|
|
40
|
+
return "'" + str(part).replace("'", "''") + "'"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def window_script(command, pid):
|
|
44
|
+
"""What the window runs: wait for operx (process `pid`) and any other
|
|
45
|
+
open operx to close, install, and say how it went. It closes by itself
|
|
46
|
+
when it worked; when not, it says the command that repairs it, since a
|
|
47
|
+
half-done install may not start operx anymore."""
|
|
48
|
+
install = "& " + " ".join(_quoted(part) for part in command)
|
|
49
|
+
by_hand = " ".join([os.path.splitext(os.path.basename(command[0]))[0], *command[1:]])
|
|
50
|
+
return "; ".join([
|
|
51
|
+
"$Host.UI.RawUI.WindowTitle = 'Actualizando operx'",
|
|
52
|
+
"Write-Host 'Esperando a que se cierre operx…'",
|
|
53
|
+
f"Wait-Process -Id {pid} -Timeout {WAIT_SECONDS} -ErrorAction SilentlyContinue",
|
|
54
|
+
# operx.exe, the launcher around this Python, closes right after it.
|
|
55
|
+
"Start-Sleep -Seconds 1",
|
|
56
|
+
# Another window with operx open would make the install fail halfway.
|
|
57
|
+
"$others = Get-Process operx -ErrorAction SilentlyContinue",
|
|
58
|
+
"if ($others) { Write-Host 'Hay otra ventana con operx abierto: ciérrala para seguir.' -ForegroundColor Yellow; "
|
|
59
|
+
f"$others | Wait-Process -Timeout {WAIT_SECONDS * 2} -ErrorAction SilentlyContinue }}",
|
|
60
|
+
install,
|
|
61
|
+
"if ($LASTEXITCODE -eq 0) { "
|
|
62
|
+
"Write-Host ''; Write-Host 'Listo: ya tienes la versión nueva. Abre operx de nuevo.' -ForegroundColor Green; "
|
|
63
|
+
"Start-Sleep -Seconds 6 } else { "
|
|
64
|
+
"Write-Host ''; Write-Host 'No se pudo actualizar. Cierra las ventanas con operx abierto y escribe: "
|
|
65
|
+
f"{by_hand}' -ForegroundColor Red; Read-Host 'Pulsa Enter para cerrar' }}",
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def start_window(script):
|
|
70
|
+
"""A PowerShell window of its own: it outlives this operx."""
|
|
71
|
+
subprocess.Popen(
|
|
72
|
+
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script],
|
|
73
|
+
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_here(command):
|
|
78
|
+
"""The install in this terminal, its output shown; its exit code."""
|
|
79
|
+
return subprocess.run(command).returncode
|
|
@@ -30,6 +30,27 @@ SHELL_WORDS = [
|
|
|
30
30
|
("ayuda", "Muestra los comandos. También: help, ?."),
|
|
31
31
|
]
|
|
32
32
|
AMBER = "#fbbf24"
|
|
33
|
+
# What a command needs: one of these products (the backend's tenants/products.py
|
|
34
|
+
# API_PRODUCT). Without one, the API answers "Tu empresa no tiene …", so the
|
|
35
|
+
# prompt doesn't offer it. A command not here works for every company.
|
|
36
|
+
COMMAND_PRODUCTS = {
|
|
37
|
+
"ordenes": ("maintenance",),
|
|
38
|
+
"preventivos": ("maintenance",),
|
|
39
|
+
"equipos": ("maintenance", "inventory", "equipment", "environment"),
|
|
40
|
+
"clientes": ("maintenance", "inventory", "sst", "environment"),
|
|
41
|
+
"inventario": ("inventory",),
|
|
42
|
+
"lecturas": ("equipment",),
|
|
43
|
+
"personas importar": ("people",),
|
|
44
|
+
"sst": ("sst",),
|
|
45
|
+
"ambiental": ("environment",),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def offered_to(command_path, products):
|
|
50
|
+
"""Whether the company's products let it use the command; every command
|
|
51
|
+
when they aren't known (not signed in)."""
|
|
52
|
+
needed = COMMAND_PRODUCTS.get(command_path)
|
|
53
|
+
return products is None or needed is None or bool(set(needed) & set(products))
|
|
33
54
|
|
|
34
55
|
|
|
35
56
|
def split_line(line, windows=None):
|
|
@@ -66,16 +87,18 @@ def _option_named(command, word):
|
|
|
66
87
|
return None
|
|
67
88
|
|
|
68
89
|
|
|
69
|
-
def suggestions(root, text):
|
|
90
|
+
def suggestions(root, text, products=None):
|
|
70
91
|
"""What to offer after `text`, as (word, help) pairs: the commands of
|
|
71
|
-
where the line has got to
|
|
72
|
-
option allows."""
|
|
92
|
+
where the line has got to (only those `products` allow, when known), the
|
|
93
|
+
options of a command, or the values an option allows."""
|
|
73
94
|
words = text.split()
|
|
74
95
|
current = "" if not words or text.endswith(" ") else words.pop()
|
|
75
96
|
node = root
|
|
97
|
+
path = []
|
|
76
98
|
for word in words:
|
|
77
99
|
if hasattr(node, "commands") and word in node.commands:
|
|
78
100
|
node = node.commands[word]
|
|
101
|
+
path.append(word)
|
|
79
102
|
|
|
80
103
|
option = _option_named(node, words[-1]) if words else None
|
|
81
104
|
if option is not None and not getattr(option, "is_flag", False):
|
|
@@ -83,7 +106,10 @@ def suggestions(root, text):
|
|
|
83
106
|
return [(choice, "") for choice in choices if choice.startswith(current)]
|
|
84
107
|
|
|
85
108
|
if hasattr(node, "commands"):
|
|
86
|
-
offered = [
|
|
109
|
+
offered = [
|
|
110
|
+
(name, _first_line(command.help)) for name, command in node.commands.items()
|
|
111
|
+
if _visible(command) and offered_to(" ".join([*path, name]), products)
|
|
112
|
+
]
|
|
87
113
|
if node is root:
|
|
88
114
|
offered += SHELL_WORDS
|
|
89
115
|
return [(name, help_text) for name, help_text in offered if name.startswith(current)]
|
|
@@ -114,9 +140,12 @@ def spanish_usage_error(error):
|
|
|
114
140
|
return f"No entendí eso ({message}). Prueba con --help."
|
|
115
141
|
|
|
116
142
|
|
|
117
|
-
def print_help(root, console):
|
|
143
|
+
def print_help(root, console, products=None):
|
|
118
144
|
console.print(Text("Comandos", style=f"bold {AMBER}"))
|
|
119
|
-
commands = [
|
|
145
|
+
commands = [
|
|
146
|
+
(name, _first_line(command.help)) for name, command in root.commands.items()
|
|
147
|
+
if _visible(command) and offered_to(name, products)
|
|
148
|
+
]
|
|
120
149
|
# Two columns: on a narrow screen a long description wraps inside its own.
|
|
121
150
|
print_aligned(console, commands + SHELL_WORDS, label_style="bold")
|
|
122
151
|
console.print(Text("Escribe un comando con --help para ver sus opciones.", style="dim"))
|
|
@@ -137,8 +166,9 @@ def run_command(session, root, argv):
|
|
|
137
166
|
typer.echo(f"Error: algo falló ({type(error).__name__}).", err=True)
|
|
138
167
|
|
|
139
168
|
|
|
140
|
-
def handle_line(session, root, line, console):
|
|
141
|
-
"""Runs one typed line; False when the person asked to leave.
|
|
169
|
+
def handle_line(session, root, line, console, products=None):
|
|
170
|
+
"""Runs one typed line; False when the person asked to leave. `products`:
|
|
171
|
+
the company's, so `ayuda` lists only what it can use."""
|
|
142
172
|
try:
|
|
143
173
|
argv = split_line(line)
|
|
144
174
|
except ValueError:
|
|
@@ -151,7 +181,7 @@ def handle_line(session, root, line, console):
|
|
|
151
181
|
if word in STOP_WORDS:
|
|
152
182
|
return False
|
|
153
183
|
if word in HELP_WORDS:
|
|
154
|
-
print_help(root, console)
|
|
184
|
+
print_help(root, console, products)
|
|
155
185
|
return True
|
|
156
186
|
if word in CLEAR_WORDS:
|
|
157
187
|
console.clear()
|
|
@@ -166,11 +196,11 @@ def history_path():
|
|
|
166
196
|
return folder / "historial"
|
|
167
197
|
|
|
168
198
|
|
|
169
|
-
def make_reader(root, status="", records=None):
|
|
199
|
+
def make_reader(root, status="", records=None, products=None):
|
|
170
200
|
"""A function that reads a line: prompt_toolkit with completion and a
|
|
171
201
|
history where the terminal allows it, else the plain input(). `records`
|
|
172
202
|
(record_completion.RecordNames) adds the company's records where a
|
|
173
|
-
command takes one."""
|
|
203
|
+
command takes one; `products` leaves out the commands it can't use."""
|
|
174
204
|
try:
|
|
175
205
|
from prompt_toolkit import PromptSession
|
|
176
206
|
from prompt_toolkit.completion import Completer, Completion, ThreadedCompleter
|
|
@@ -182,7 +212,7 @@ def make_reader(root, status="", records=None):
|
|
|
182
212
|
text = document.text_before_cursor
|
|
183
213
|
words = text.split()
|
|
184
214
|
typed = "" if not text or text.endswith(" ") else words.pop()
|
|
185
|
-
offered = suggestions(root, text)
|
|
215
|
+
offered = suggestions(root, text, products)
|
|
186
216
|
for name, help_text in offered:
|
|
187
217
|
yield Completion(name, start_position=-len(typed), display_meta=help_text)
|
|
188
218
|
if offered or records is None:
|
|
@@ -246,14 +276,17 @@ def enter(session, root):
|
|
|
246
276
|
console = Console()
|
|
247
277
|
me = print_welcome(session, console)
|
|
248
278
|
print_tips(console, (me or {}).get("products") or [])
|
|
249
|
-
|
|
279
|
+
# Unknown without signing in: then every command is offered.
|
|
280
|
+
products = me.get("products") if me else None
|
|
281
|
+
run_shell(session, root, console, reader=session.reader, status=status_line(me), products=products)
|
|
250
282
|
|
|
251
283
|
|
|
252
|
-
def run_shell(session, root, console=None, reader=None, status=""):
|
|
284
|
+
def run_shell(session, root, console=None, reader=None, status="", products=None):
|
|
253
285
|
"""The prompt: reads a line, runs it, until the person leaves (salir,
|
|
254
|
-
Ctrl+D). Ctrl+C only clears the line.
|
|
286
|
+
Ctrl+D). Ctrl+C only clears the line. `products`: the company's, so
|
|
287
|
+
`ayuda` and Tab offer only what it can use."""
|
|
255
288
|
console = console or Console()
|
|
256
|
-
reader = reader or make_reader(root, status, RecordNames(session))
|
|
289
|
+
reader = reader or make_reader(root, status, RecordNames(session), products)
|
|
257
290
|
session.in_shell = True
|
|
258
291
|
try:
|
|
259
292
|
while True:
|
|
@@ -263,7 +296,10 @@ def run_shell(session, root, console=None, reader=None, status=""):
|
|
|
263
296
|
break
|
|
264
297
|
except KeyboardInterrupt:
|
|
265
298
|
continue
|
|
266
|
-
if not handle_line(session, root, line, console):
|
|
299
|
+
if not handle_line(session, root, line, console, products):
|
|
300
|
+
break
|
|
301
|
+
if session.leave_shell:
|
|
302
|
+
# `update` on Windows: this operx must close to be replaced.
|
|
267
303
|
break
|
|
268
304
|
finally:
|
|
269
305
|
session.in_shell = False
|
|
@@ -14,7 +14,7 @@ PYPI_URL = "https://pypi.org/pypi/operx-cli/json"
|
|
|
14
14
|
CHECK_EVERY = timedelta(days=1)
|
|
15
15
|
TIMEOUT_SECONDS = 1.0
|
|
16
16
|
SAVED_FILE = "version.json"
|
|
17
|
-
UPGRADE_LINE = "
|
|
17
|
+
UPGRADE_LINE = "operx update"
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def fetch_latest(transport=None):
|
|
@@ -40,11 +40,15 @@ def newer_version(current, fetch=None, now=None):
|
|
|
40
40
|
latest = fetch()
|
|
41
41
|
# Kept even when PyPI didn't answer: offline, it's tried again tomorrow.
|
|
42
42
|
_save(now, latest)
|
|
43
|
-
if latest and
|
|
43
|
+
if latest and is_newer(latest, current):
|
|
44
44
|
return latest
|
|
45
45
|
return None
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def is_newer(version, current):
|
|
49
|
+
return _parts(version) > _parts(current)
|
|
50
|
+
|
|
51
|
+
|
|
48
52
|
def _parts(version):
|
|
49
53
|
"""'0.10.2' → (0, 10, 2), so 0.10 comes after 0.9."""
|
|
50
54
|
try:
|
|
@@ -87,8 +87,7 @@ def print_welcome(session, console=None):
|
|
|
87
87
|
console.print(Text(f"OperX desde la terminal · v{__version__}", style="dim"))
|
|
88
88
|
newer = updates.newer_version(__version__)
|
|
89
89
|
if newer:
|
|
90
|
-
|
|
91
|
-
console.print(Text(f"Hay una versión nueva: {newer}. Cierra operx y escribe: {updates.UPGRADE_LINE}", style=AMBER))
|
|
90
|
+
console.print(Text(f"Hay una versión nueva: {newer}. Escribe: {updates.UPGRADE_LINE}", style=AMBER))
|
|
92
91
|
console.print()
|
|
93
92
|
|
|
94
93
|
token = settings.token_of(session.profile)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: operx-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: La línea de comandos de OperX: tu empresa desde la terminal, con tu token de API.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -36,8 +36,10 @@ curl -LsSf https://astral.sh/uv/install.sh | sh && uv tool install operx-cli
|
|
|
36
36
|
Python 3.10 o más nuevo, también sirve `pipx install operx-cli`. Abre una
|
|
37
37
|
terminal nueva y comprueba con `operx --version`.
|
|
38
38
|
|
|
39
|
-
Para actualizar:
|
|
40
|
-
|
|
39
|
+
Para actualizar: `operx update`. La pantalla de bienvenida avisa cuando hay una
|
|
40
|
+
versión nueva. En Windows la instala otra ventana, que espera a que se cierre
|
|
41
|
+
`operx` (Windows no deja reemplazar un programa abierto); si quedó a medias,
|
|
42
|
+
`uv tool install operx-cli --force --refresh` lo deja bien.
|
|
41
43
|
|
|
42
44
|
## Entrar
|
|
43
45
|
|
|
@@ -87,8 +89,9 @@ operx › ayuda
|
|
|
87
89
|
operx › salir
|
|
88
90
|
```
|
|
89
91
|
|
|
90
|
-
`salir` (o Ctrl+D) termina la sesión sin borrar tu token.
|
|
91
|
-
|
|
92
|
+
`salir` (o Ctrl+D) termina la sesión sin borrar tu token. `ayuda` y Tab
|
|
93
|
+
muestran solo los comandos de los productos que tiene tu empresa. En un script
|
|
94
|
+
o una tubería, `operx` solo muestra la ayuda y no espera a nadie.
|
|
92
95
|
|
|
93
96
|
## Comandos
|
|
94
97
|
|
|
@@ -9,6 +9,7 @@ operx_cli/client.py
|
|
|
9
9
|
operx_cli/lookup.py
|
|
10
10
|
operx_cli/output.py
|
|
11
11
|
operx_cli/record_completion.py
|
|
12
|
+
operx_cli/self_update.py
|
|
12
13
|
operx_cli/settings.py
|
|
13
14
|
operx_cli/shell.py
|
|
14
15
|
operx_cli/spreadsheet.py
|
|
@@ -35,6 +36,7 @@ operx_cli/commands/raw.py
|
|
|
35
36
|
operx_cli/commands/registros.py
|
|
36
37
|
operx_cli/commands/search.py
|
|
37
38
|
operx_cli/commands/sst.py
|
|
39
|
+
operx_cli/commands/update.py
|
|
38
40
|
tests/test_agentes.py
|
|
39
41
|
tests/test_app.py
|
|
40
42
|
tests/test_cards.py
|
|
@@ -49,6 +51,7 @@ tests/test_personas.py
|
|
|
49
51
|
tests/test_preventivos.py
|
|
50
52
|
tests/test_record_completion.py
|
|
51
53
|
tests/test_registros.py
|
|
54
|
+
tests/test_self_update.py
|
|
52
55
|
tests/test_settings.py
|
|
53
56
|
tests/test_shell.py
|
|
54
57
|
tests/test_spreadsheet.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "operx-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "La línea de comandos de OperX: tu empresa desde la terminal, con tu token de API."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""`operx update`: the newest operx from PyPI, installed the way it was. On
|
|
2
|
+
Windows a running program can't be replaced, so a PowerShell window does it
|
|
3
|
+
once this operx has closed."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from operx_cli import __version__, self_update
|
|
11
|
+
from operx_cli.commands.common import Session
|
|
12
|
+
from operx_cli.shell import run_shell
|
|
13
|
+
|
|
14
|
+
from .fake_api import FakeApi, run
|
|
15
|
+
from .test_shell import ROOT, typed
|
|
16
|
+
|
|
17
|
+
UV = r"C:\Users\Ana\.local\bin\uv.exe"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def started(monkeypatch):
|
|
22
|
+
"""The PowerShell windows `operx update` asked for, and the commands it ran here."""
|
|
23
|
+
calls = {"windows": [], "ran": []}
|
|
24
|
+
monkeypatch.setattr(self_update, "start_window", lambda script: calls["windows"].append(script))
|
|
25
|
+
monkeypatch.setattr(self_update, "run_here", lambda command: calls["ran"].append(command) or 0)
|
|
26
|
+
monkeypatch.setattr(
|
|
27
|
+
self_update, "installer_command", lambda latest=None: [UV, "tool", "install", "operx-cli", "--force"],
|
|
28
|
+
)
|
|
29
|
+
return calls
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def newest_is(monkeypatch, version):
|
|
33
|
+
monkeypatch.setattr("operx_cli.updates.fetch_latest", lambda transport=None: version)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_with_the_newest_already_it_does_nothing(monkeypatch, started):
|
|
37
|
+
newest_is(monkeypatch, __version__)
|
|
38
|
+
|
|
39
|
+
result = run(FakeApi({}), "update")
|
|
40
|
+
|
|
41
|
+
assert result.exit_code == 0, result.output
|
|
42
|
+
assert f"Ya tienes la versión más nueva: {__version__}" in result.output
|
|
43
|
+
assert started == {"windows": [], "ran": []}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_on_windows_a_window_updates_once_this_operx_closes(monkeypatch, started):
|
|
47
|
+
newest_is(monkeypatch, "9.9.9")
|
|
48
|
+
monkeypatch.setattr(self_update, "on_windows", lambda: True)
|
|
49
|
+
|
|
50
|
+
result = run(FakeApi({}), "update")
|
|
51
|
+
|
|
52
|
+
assert result.exit_code == 0, result.output
|
|
53
|
+
assert "9.9.9" in result.output and "otra ventana" in result.output
|
|
54
|
+
[script] = started["windows"]
|
|
55
|
+
assert f"Wait-Process -Id {os.getpid()}" in script
|
|
56
|
+
assert f"& '{UV}' 'tool' 'install' 'operx-cli' '--force'" in script
|
|
57
|
+
assert started["ran"] == []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_at_the_prompt_on_windows_the_session_ends_so_it_can_be_replaced(monkeypatch, started):
|
|
61
|
+
newest_is(monkeypatch, "9.9.9")
|
|
62
|
+
monkeypatch.setattr(self_update, "on_windows", lambda: True)
|
|
63
|
+
session = Session(transport=httpx.MockTransport(FakeApi({})))
|
|
64
|
+
lines_left = typed("update", "cuenta")
|
|
65
|
+
|
|
66
|
+
run_shell(session, ROOT, reader=lines_left)
|
|
67
|
+
|
|
68
|
+
assert len(started["windows"]) == 1
|
|
69
|
+
assert lines_left() == "cuenta" # never read: the prompt closed after update
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_elsewhere_it_updates_right_here(monkeypatch, started):
|
|
73
|
+
newest_is(monkeypatch, "9.9.9")
|
|
74
|
+
monkeypatch.setattr(self_update, "on_windows", lambda: False)
|
|
75
|
+
|
|
76
|
+
result = run(FakeApi({}), "update")
|
|
77
|
+
|
|
78
|
+
assert result.exit_code == 0, result.output
|
|
79
|
+
assert started["ran"] == [[UV, "tool", "install", "operx-cli", "--force"]]
|
|
80
|
+
assert "9.9.9" in result.output
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_a_failed_install_says_so(monkeypatch, started):
|
|
84
|
+
newest_is(monkeypatch, "9.9.9")
|
|
85
|
+
monkeypatch.setattr(self_update, "on_windows", lambda: False)
|
|
86
|
+
monkeypatch.setattr(self_update, "run_here", lambda command: 1)
|
|
87
|
+
|
|
88
|
+
result = run(FakeApi({}), "update")
|
|
89
|
+
|
|
90
|
+
assert result.exit_code == 1
|
|
91
|
+
assert "No se pudo actualizar" in result.output
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_when_pypi_cannot_say_it_updates_anyway(monkeypatch, started):
|
|
95
|
+
newest_is(monkeypatch, None)
|
|
96
|
+
monkeypatch.setattr(self_update, "on_windows", lambda: False)
|
|
97
|
+
|
|
98
|
+
assert run(FakeApi({}), "update").exit_code == 0
|
|
99
|
+
assert len(started["ran"]) == 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_without_uv_or_pipx_it_says_how(monkeypatch, started):
|
|
103
|
+
newest_is(monkeypatch, "9.9.9")
|
|
104
|
+
monkeypatch.setattr(self_update, "installer_command", lambda latest=None: None)
|
|
105
|
+
|
|
106
|
+
result = run(FakeApi({}), "update")
|
|
107
|
+
|
|
108
|
+
assert result.exit_code == 1
|
|
109
|
+
assert "uv tool upgrade operx-cli" in result.output
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_the_installer_is_uv_unless_operx_came_with_pipx(monkeypatch):
|
|
113
|
+
monkeypatch.setattr(self_update.shutil, "which", lambda name: f"/bin/{name}")
|
|
114
|
+
monkeypatch.setattr(self_update.sys, "prefix", "/home/ana/.local/share/uv/tools/operx-cli")
|
|
115
|
+
assert self_update.installer_command() == ["/bin/uv", "tool", "install", "operx-cli", "--force", "--refresh"]
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(self_update.sys, "prefix", "/home/ana/.local/pipx/venvs/operx-cli")
|
|
118
|
+
assert self_update.installer_command() == ["/bin/pipx", "upgrade", "operx-cli"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_uv_is_asked_for_at_least_the_version_pypi_named(monkeypatch):
|
|
122
|
+
# Right after a release uv's answer can lag behind PyPI's: then it fails
|
|
123
|
+
# instead of putting the old version back and saying it worked.
|
|
124
|
+
monkeypatch.setattr(self_update.shutil, "which", lambda name: f"/bin/{name}")
|
|
125
|
+
monkeypatch.setattr(self_update.sys, "prefix", "/home/ana/.local/share/uv/tools/operx-cli")
|
|
126
|
+
|
|
127
|
+
assert self_update.installer_command("0.3.2") == [
|
|
128
|
+
"/bin/uv", "tool", "install", "operx-cli>=0.3.2", "--force", "--refresh",
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_update_passes_pypi_s_version_to_the_installer(monkeypatch, started):
|
|
133
|
+
newest_is(monkeypatch, "9.9.9")
|
|
134
|
+
monkeypatch.setattr(self_update, "on_windows", lambda: False)
|
|
135
|
+
asked = []
|
|
136
|
+
monkeypatch.setattr(self_update, "installer_command", lambda latest=None: asked.append(latest) or ["uv"])
|
|
137
|
+
|
|
138
|
+
run(FakeApi({}), "update")
|
|
139
|
+
|
|
140
|
+
assert asked == ["9.9.9"]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_the_window_quotes_a_path_with_spaces_and_quotes():
|
|
144
|
+
script = self_update.window_script([r"C:\Users\Ana O'Neil\uv.exe", "tool"], 4242)
|
|
145
|
+
|
|
146
|
+
assert "& 'C:\\Users\\Ana O''Neil\\uv.exe' 'tool'" in script
|
|
147
|
+
assert "Wait-Process -Id 4242" in script
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_the_window_waits_for_other_operx_windows_and_says_how_to_repair():
|
|
151
|
+
# Another open operx makes uv fail halfway and leaves an operx that won't start.
|
|
152
|
+
script = self_update.window_script([r"C:\bin\uv.exe", "tool", "install", "operx-cli", "--force"], 4242)
|
|
153
|
+
|
|
154
|
+
assert "Get-Process operx" in script
|
|
155
|
+
assert "uv tool install operx-cli --force" in script
|
|
@@ -32,10 +32,10 @@ def typed(*lines):
|
|
|
32
32
|
return read
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def shell(fake, *lines):
|
|
35
|
+
def shell(fake, *lines, products=None):
|
|
36
36
|
settings.save_token("principal", "opx_test")
|
|
37
37
|
session = Session(transport=httpx.MockTransport(fake), sleep=lambda seconds: None)
|
|
38
|
-
run_shell(session, ROOT, reader=typed(*lines))
|
|
38
|
+
run_shell(session, ROOT, reader=typed(*lines), products=products)
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
# ── Reading a line the way a person types it ─────────────────────────────────
|
|
@@ -82,6 +82,19 @@ def test_operx_alone_on_a_terminal_shows_the_welcome_and_opens_the_prompt(monkey
|
|
|
82
82
|
assert result.output.index("Para empezar") < result.output.index("Correo")
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def test_the_prompt_takes_the_company_s_products_from_who_signed_in(monkeypatch):
|
|
86
|
+
monkeypatch.setattr("operx_cli.app.is_interactive", lambda: True)
|
|
87
|
+
settings.save_token("principal", "opx_test")
|
|
88
|
+
fake = FakeApi({("GET", "auth/me/"): ME, ("GET", "work-orders/facets/"): FACETS}) # ME has only maintenance
|
|
89
|
+
session = Session(transport=httpx.MockTransport(fake), reader=typed("ayuda", "salir"))
|
|
90
|
+
|
|
91
|
+
result = CliRunner().invoke(app, [], obj=session)
|
|
92
|
+
|
|
93
|
+
listed = help_commands(result.output.split("Comandos", 1)[1])
|
|
94
|
+
assert "ordenes" in listed
|
|
95
|
+
assert "sst" not in listed and "inventario" not in listed
|
|
96
|
+
|
|
97
|
+
|
|
85
98
|
def test_operx_alone_without_a_token_still_opens_the_prompt_so_login_can_be_typed(monkeypatch):
|
|
86
99
|
monkeypatch.setattr("operx_cli.app.is_interactive", lambda: True)
|
|
87
100
|
session = Session(transport=httpx.MockTransport(FakeApi({})), reader=typed("salir"))
|
|
@@ -206,6 +219,45 @@ def test_ayuda_lists_the_commands(capsys):
|
|
|
206
219
|
assert "entrar" not in out
|
|
207
220
|
|
|
208
221
|
|
|
222
|
+
def help_commands(out):
|
|
223
|
+
"""The command names `ayuda` printed, one per line."""
|
|
224
|
+
return {line.split()[0] for line in out.splitlines()[1:] if line.startswith(" ") and line.strip()}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_ayuda_shows_only_the_products_the_company_has(capsys):
|
|
228
|
+
shell(FakeApi({}), "ayuda", products=["maintenance"])
|
|
229
|
+
listed = help_commands(capsys.readouterr().out)
|
|
230
|
+
|
|
231
|
+
assert {"ordenes", "preventivos", "equipos", "clientes", "lugares", "buscar", "personas"} <= listed
|
|
232
|
+
assert not {"inventario", "lecturas", "sst", "ambiental"} & listed
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_a_shared_record_shows_with_any_of_its_products(capsys):
|
|
236
|
+
shell(FakeApi({}), "ayuda", products=["inventory"])
|
|
237
|
+
listed = help_commands(capsys.readouterr().out)
|
|
238
|
+
|
|
239
|
+
assert {"inventario", "equipos", "clientes"} <= listed
|
|
240
|
+
assert not {"ordenes", "preventivos", "lecturas"} & listed
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_without_knowing_the_products_ayuda_shows_everything(capsys):
|
|
244
|
+
shell(FakeApi({}), "ayuda")
|
|
245
|
+
|
|
246
|
+
assert {"ordenes", "inventario", "lecturas", "sst", "ambiental"} <= help_commands(capsys.readouterr().out)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_tab_offers_only_the_products_the_company_has():
|
|
250
|
+
offered = names(suggestions(ROOT, "", products=["sst"]))
|
|
251
|
+
|
|
252
|
+
assert "sst" in offered and "lugares" in offered
|
|
253
|
+
assert "ordenes" not in offered and "ambiental" not in offered
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_tab_hides_a_subcommand_of_a_product_the_company_lacks():
|
|
257
|
+
assert names(suggestions(ROOT, "personas ", products=["maintenance"])) == ["listar"]
|
|
258
|
+
assert set(names(suggestions(ROOT, "personas ", products=["people"]))) == {"listar", "importar"}
|
|
259
|
+
|
|
260
|
+
|
|
209
261
|
def test_ayuda_lists_every_word_the_prompt_understands(capsys):
|
|
210
262
|
shell(FakeApi({}), "ayuda")
|
|
211
263
|
out = capsys.readouterr().out
|
|
@@ -64,7 +64,7 @@ def test_a_newer_version_is_announced_with_how_to_update(monkeypatch):
|
|
|
64
64
|
text = welcome(api())
|
|
65
65
|
|
|
66
66
|
assert "9.9.9" in text
|
|
67
|
-
assert "
|
|
67
|
+
assert "operx update" in text
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def test_no_notice_when_this_is_the_newest_version():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|