operx-cli 0.7.0__tar.gz → 0.7.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.7.0 → operx_cli-0.7.2}/PKG-INFO +1 -1
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/AGENTES.md +20 -18
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/__init__.py +1 -1
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/api_fields.py +44 -1
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/raw.py +3 -39
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/lookup.py +3 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/shell.py +30 -2
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli.egg-info/PKG-INFO +1 -1
- {operx_cli-0.7.0 → operx_cli-0.7.2}/pyproject.toml +1 -1
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_completar.py +2 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_shell.py +18 -1
- {operx_cli-0.7.0 → operx_cli-0.7.2}/README.md +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/agent_guide.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/app.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/browser_login.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/client.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/__init__.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/account.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/ambiental.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/articulos.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/avisos.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/common.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/completar.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/empresas.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/inventario.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/lecturas.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/ordenes.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/personas.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/planta.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/preventivos.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/registros.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/reportes.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/search.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/sst.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/commands/update.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/output.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/record_completion.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/self_update.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/settings.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/spreadsheet.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/updates.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/values.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli/welcome.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli.egg-info/SOURCES.txt +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli.egg-info/dependency_links.txt +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli.egg-info/entry_points.txt +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli.egg-info/requires.txt +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/operx_cli.egg-info/top_level.txt +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/setup.cfg +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_agentes.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_api_campos.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_app.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_articulos.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_cards.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_client.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_daily_flows.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_inventario_lecturas.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_json_writes.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_login.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_logout_and_routes.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_lookup.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_more_of_the_api.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_ordenes.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_output.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_personas.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_preventivos.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_record_completion.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_registros.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_self_update.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_settings.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_spreadsheet.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_sst_ambiental.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_updates.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_values.py +0 -0
- {operx_cli-0.7.0 → operx_cli-0.7.2}/tests/test_welcome.py +0 -0
|
@@ -59,17 +59,19 @@ llamada queda en el historial del token, marcada como hecha desde la CLI.
|
|
|
59
59
|
|
|
60
60
|
- Todos los comandos que escriben aceptan `--json`: la salida es solo la
|
|
61
61
|
respuesta de la API (el registro, con su `id`). Úsalo para encadenar pasos.
|
|
62
|
-
- Escriben
|
|
63
|
-
`ordenes
|
|
64
|
-
|
|
65
|
-
`equipos
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
62
|
+
- Escriben, y todos llevan `--si`:
|
|
63
|
+
- Trabajos: `ordenes crear`, `editar`, `estado`, `asignar`, `comentar`,
|
|
64
|
+
`adjuntar` y `repuesto`; `preventivos crear` y `nota`.
|
|
65
|
+
- Registros: `equipos crear` y `editar`, `clientes crear` y `editar`,
|
|
66
|
+
`lugares crear`.
|
|
67
|
+
- Inventario: `articulos crear`; `inventario entrada`, `salida`, `ajuste`,
|
|
68
|
+
`traslado` y `revertir`.
|
|
69
|
+
- Equipos: `equipos parar`, `reanudar`, `hallazgo` y `hallazgo-a-orden`;
|
|
70
|
+
`lecturas registrar` y `anular`.
|
|
71
|
+
- Personas: `personas importar --confirmar` y `personas ausencia`.
|
|
72
|
+
- SG-SST y Ambiental: `sst nueva-accion` y `accion-hecha`;
|
|
73
|
+
`ambiental nueva-accion`, `accion-hecha`, `residuo` y `lectura`.
|
|
74
|
+
- `avisos --marcar-leidos`, y `operx api` con POST, PUT, PATCH o DELETE.
|
|
73
75
|
- Un error se corrige con su contrario, nunca borrando: `inventario revertir`
|
|
74
76
|
deshace un movimiento, `lecturas anular` una lectura.
|
|
75
77
|
- SG-SST y Ambiental van por empresa: sin `--empresa`, la de la cuenta. Una
|
|
@@ -177,22 +179,22 @@ operx api GET work-orders/export/ -o ordenes.csv
|
|
|
177
179
|
operx ordenes listar --vencidas --asignada-a yo --todas --json
|
|
178
180
|
|
|
179
181
|
# Pasar una orden a completada, después de su sí
|
|
180
|
-
operx ordenes estado 1234 completada --si
|
|
182
|
+
operx ordenes estado 1234 completada --si --json
|
|
181
183
|
|
|
182
184
|
# Registrar una lectura: primero el número del punto
|
|
183
185
|
operx lecturas puntos BOM-04 --json
|
|
184
|
-
operx lecturas registrar --punto 12 --valor 73,4 --si
|
|
186
|
+
operx lecturas registrar --punto 12 --valor 73,4 --si --json
|
|
185
187
|
|
|
186
188
|
# Sacar stock: primero el código y el lugar exactos
|
|
187
189
|
operx inventario existencias --buscar filtro --json
|
|
188
|
-
operx inventario salida --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --motivo Consumo --si
|
|
190
|
+
operx inventario salida --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --motivo Consumo --si --json
|
|
189
191
|
|
|
190
192
|
# Un artículo nuevo y lo que llegó de él (crear da el código)
|
|
191
|
-
operx articulos crear --nombre "Filtro de aceite" --unidad und --tipo consumible --si
|
|
192
|
-
operx inventario entrada --articulo CON-0001 --lugar "Bodega central" --cantidad 10 --costo-unitario 25.000 --si
|
|
193
|
+
operx articulos crear --nombre "Filtro de aceite" --unidad und --tipo consumible --si --json
|
|
194
|
+
operx inventario entrada --articulo CON-0001 --lugar "Bodega central" --cantidad 10 --costo-unitario 25.000 --si --json
|
|
193
195
|
|
|
194
196
|
# Un equipo que entró a mantenimiento
|
|
195
|
-
operx equipos editar BOM-04 --estado en-mantenimiento --si
|
|
197
|
+
operx equipos editar BOM-04 --estado en-mantenimiento --si --json
|
|
196
198
|
|
|
197
199
|
# Un repuesto usado en una orden, y lo que ya lleva
|
|
198
200
|
operx ordenes repuesto 1234 --articulo FIL-010 --lugar "Bodega central" --cantidad 2 --si --json
|
|
@@ -229,5 +231,5 @@ operx sst acciones --estado abiertas --json
|
|
|
229
231
|
|
|
230
232
|
# Lo que vence este mes en lo ambiental, y registrar residuos
|
|
231
233
|
operx ambiental obligaciones --json
|
|
232
|
-
operx ambiental residuo --corriente "Aceite usado" --kg 12,5 --si
|
|
234
|
+
operx ambiental residuo --corriente "Aceite usado" --kg 12,5 --si --json
|
|
233
235
|
```
|
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
(auth/api-tokens/schema/, drf-spectacular): the values in its address, its
|
|
3
3
|
filters, the fields of its body and what it answers. `operx api campos`
|
|
4
4
|
prints it, so an AI agent can call any route with `operx api` without
|
|
5
|
-
guessing; no network here, so the MCP server can use it as it is
|
|
5
|
+
guessing; no network here, so the MCP server can use it as it is: its
|
|
6
|
+
image copies this one file (mcp/Dockerfile), so keep it free of imports
|
|
7
|
+
beyond the standard library.
|
|
6
8
|
|
|
7
9
|
The schema's shapes this follows: a field's type behind a $ref; an allOf
|
|
8
10
|
of one $ref (a nested record); a oneOf of an enum with BlankEnum and
|
|
9
11
|
NullEnum (a choice that may be "" or null); each enum's labels in its
|
|
10
12
|
description, one "* `value` - Label" a line.
|
|
13
|
+
|
|
14
|
+
It also lists the routes and finds them by words (`operx api rutas`, the
|
|
15
|
+
MCP's operx_rutas).
|
|
11
16
|
"""
|
|
12
17
|
|
|
13
18
|
import re
|
|
19
|
+
import unicodedata
|
|
14
20
|
|
|
15
21
|
METHODS = ("get", "post", "put", "patch", "delete")
|
|
16
22
|
# A field inside a list inside a list… is shown this many levels deep.
|
|
@@ -230,3 +236,40 @@ def _answer(components, operation):
|
|
|
230
236
|
record = _resolve(components, record.get("items"))
|
|
231
237
|
return {"estado": int(status), "pagina": paged, "campos": list(record.get("properties", {}))}
|
|
232
238
|
return {"estado": None, "pagina": False, "campos": []}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def routes_of(schema):
|
|
242
|
+
"""Every route of the schema, one row per method: its address, its
|
|
243
|
+
product (the docs' section, in Spanish) and its description."""
|
|
244
|
+
routes = []
|
|
245
|
+
for path, operations in schema.get("paths", {}).items():
|
|
246
|
+
for method, operation in operations.items():
|
|
247
|
+
if method not in METHODS:
|
|
248
|
+
continue
|
|
249
|
+
# The first paragraph: the docs add a note on the token a write needs.
|
|
250
|
+
description = (operation.get("description") or "").split("\n\n")[0].strip()
|
|
251
|
+
if description.startswith("Necesita un token"):
|
|
252
|
+
description = ""
|
|
253
|
+
routes.append({
|
|
254
|
+
"metodo": method.upper(), "ruta": path.removeprefix("/api/v1/"),
|
|
255
|
+
# The docs' section: the product, in Spanish ("SG-SST", "Inventario").
|
|
256
|
+
"producto": ", ".join(operation.get("tags", [])), "descripcion": description,
|
|
257
|
+
})
|
|
258
|
+
return routes
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def plain(text):
|
|
262
|
+
"""'Órdenes' → 'ordenes': no case, no accents."""
|
|
263
|
+
return "".join(char for char in unicodedata.normalize("NFKD", text.casefold()) if not unicodedata.combining(char))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def mentions(where, text):
|
|
267
|
+
"""Whether every word of `text` is in `where`, a plural also as its
|
|
268
|
+
singular: an agent asks for "repuestos" where the docs say "repuesto",
|
|
269
|
+
and "ordenes" for "órdenes"."""
|
|
270
|
+
where = plain(where)
|
|
271
|
+
for word in plain(text).split():
|
|
272
|
+
singular = word[:-2] if word.endswith("es") and len(word) > 5 else word.rstrip("s") if len(word) > 4 else word
|
|
273
|
+
if word not in where and singular not in where:
|
|
274
|
+
return False
|
|
275
|
+
return True
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
lists them), for what has no command of its own yet."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
-
import unicodedata
|
|
6
5
|
from contextlib import ExitStack
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from typing import Annotated, Optional
|
|
@@ -10,7 +9,7 @@ from typing import Annotated, Optional
|
|
|
10
9
|
import typer
|
|
11
10
|
from rich.text import Text
|
|
12
11
|
|
|
13
|
-
from ..api_fields import describe, find_route, methods_of
|
|
12
|
+
from ..api_fields import describe, find_route, mentions, methods_of, routes_of
|
|
14
13
|
from ..client import ApiError, api_path
|
|
15
14
|
from ..output import json_text, note, print_rows, screen_console
|
|
16
15
|
from .common import AllOption, CliError, YesOption, client_of, confirm_write
|
|
@@ -60,9 +59,9 @@ def _print_routes(client, text, as_json):
|
|
|
60
59
|
"""Every call the token may make, from the API's own docs
|
|
61
60
|
(users/api_docs.py): the method, the address as `operx api` takes it,
|
|
62
61
|
and what the docs say. `text` keeps the ones that mention it."""
|
|
63
|
-
routes =
|
|
62
|
+
routes = routes_of(client.get("auth/api-tokens/schema"))
|
|
64
63
|
if text:
|
|
65
|
-
routes = [route for route in routes if
|
|
64
|
+
routes = [route for route in routes if mentions(f"{route['ruta']} {route['producto']} {route['descripcion']}", text)]
|
|
66
65
|
print_rows(ROUTE_COLUMNS, routes, "json" if as_json else "tabla")
|
|
67
66
|
if not as_json and routes:
|
|
68
67
|
note("Lo que lleva cada una: operx api campos RUTA")
|
|
@@ -169,41 +168,6 @@ def _print_fields(client, ruta, method, as_json):
|
|
|
169
168
|
console.print(Text(f"Responde ({answer['estado']}) {what}: {', '.join(answer['campos'])}".replace(" :", ":"), style="dim"))
|
|
170
169
|
|
|
171
170
|
|
|
172
|
-
def _plain(text):
|
|
173
|
-
"""'Órdenes' → 'ordenes': no case, no accents."""
|
|
174
|
-
return "".join(char for char in unicodedata.normalize("NFKD", text.casefold()) if not unicodedata.combining(char))
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _mentions(where, text):
|
|
178
|
-
"""Whether every word of `text` is in `where`, a plural also as its
|
|
179
|
-
singular: an agent asks for "repuestos" where the docs say "repuesto",
|
|
180
|
-
and "ordenes" for "órdenes"."""
|
|
181
|
-
where = _plain(where)
|
|
182
|
-
for word in _plain(text).split():
|
|
183
|
-
singular = word[:-2] if word.endswith("es") and len(word) > 5 else word.rstrip("s") if len(word) > 4 else word
|
|
184
|
-
if word not in where and singular not in where:
|
|
185
|
-
return False
|
|
186
|
-
return True
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def _routes(schema):
|
|
190
|
-
routes = []
|
|
191
|
-
for path, operations in schema.get("paths", {}).items():
|
|
192
|
-
for method, operation in operations.items():
|
|
193
|
-
if method.upper() not in METHODS:
|
|
194
|
-
continue
|
|
195
|
-
# The first paragraph: the docs add a note on the token a write needs.
|
|
196
|
-
description = (operation.get("description") or "").split("\n\n")[0].strip()
|
|
197
|
-
if description.startswith("Necesita un token"):
|
|
198
|
-
description = ""
|
|
199
|
-
routes.append({
|
|
200
|
-
"metodo": method.upper(), "ruta": path.removeprefix("/api/v1/"),
|
|
201
|
-
# The docs' section: the product, in Spanish ("SG-SST", "Inventario").
|
|
202
|
-
"producto": ", ".join(operation.get("tags", [])), "descripcion": description,
|
|
203
|
-
})
|
|
204
|
-
return routes
|
|
205
|
-
|
|
206
|
-
|
|
207
171
|
def _show(response, salida):
|
|
208
172
|
if salida:
|
|
209
173
|
salida.write_bytes(response.content)
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
Only an exact match counts, ignoring case and extra spaces: a search's first
|
|
4
4
|
hit is never taken, so "Bodega" can't move stock into "Bodega central norte".
|
|
5
5
|
A number nobody is called is taken as the record's id.
|
|
6
|
+
|
|
7
|
+
The MCP server uses find_in as it is: its image copies this one file
|
|
8
|
+
(mcp/Dockerfile), so keep it free of imports.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
|
|
@@ -10,16 +10,19 @@ terminal (Git Bash, a pipe)."""
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import shlex
|
|
13
|
+
import traceback
|
|
13
14
|
|
|
14
15
|
import typer
|
|
15
16
|
from rich.console import Console
|
|
16
17
|
from rich.text import Text
|
|
17
18
|
|
|
18
|
-
from . import settings
|
|
19
|
+
from . import __version__, settings
|
|
19
20
|
from .output import print_aligned
|
|
20
21
|
from .record_completion import RecordNames, completion_text
|
|
21
22
|
|
|
22
23
|
PROMPT = "operx › "
|
|
24
|
+
# Where the CLI's own files are: a crash names its line closest to the error.
|
|
25
|
+
PACKAGE_FOLDER = os.path.normcase(os.path.dirname(os.path.abspath(__file__)))
|
|
23
26
|
STOP_WORDS = ("salir", "exit", "quit")
|
|
24
27
|
HELP_WORDS = ("/ayuda", "ayuda", "/help", "help", "?")
|
|
25
28
|
CLEAR_WORDS = ("limpiar", "/limpiar", "clear", "cls")
|
|
@@ -169,6 +172,31 @@ def print_help(root, console, products=None):
|
|
|
169
172
|
console.print(Text("Escribe un comando con --help para ver sus opciones.", style="dim"))
|
|
170
173
|
|
|
171
174
|
|
|
175
|
+
def where_it_failed(error):
|
|
176
|
+
"""The CLI's own line closest to the error, as "commands/search.py:22".
|
|
177
|
+
run_command, which caught it, is always one of them."""
|
|
178
|
+
frames = traceback.extract_tb(error.__traceback__)
|
|
179
|
+
ours = [frame for frame in frames if os.path.normcase(frame.filename).startswith(PACKAGE_FOLDER)]
|
|
180
|
+
if not ours:
|
|
181
|
+
return "?"
|
|
182
|
+
relative = os.path.relpath(ours[-1].filename, PACKAGE_FOLDER)
|
|
183
|
+
return f"{relative.replace(os.sep, '/')}:{ours[-1].lineno}"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def crash_message(error):
|
|
187
|
+
"""What a crash says inside the session: a line a person can send to
|
|
188
|
+
support (the error, where in the CLI, and the version those line numbers
|
|
189
|
+
belong to), never what was typed, which can hold anything."""
|
|
190
|
+
what = type(error).__name__
|
|
191
|
+
lines = str(error).strip().splitlines()
|
|
192
|
+
if lines:
|
|
193
|
+
what = f"{what}: {lines[0]}"
|
|
194
|
+
return (
|
|
195
|
+
f"Error: algo falló ({what}; en {where_it_failed(error)}, operx {__version__}).\n"
|
|
196
|
+
"Para ver todo el detalle, corre el mismo comando con operx delante, fuera de esta sesión."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
172
200
|
def run_command(session, root, argv):
|
|
173
201
|
"""One command, as `operx` would run it, but an error never ends the session."""
|
|
174
202
|
try:
|
|
@@ -181,7 +209,7 @@ def run_command(session, root, argv):
|
|
|
181
209
|
if hasattr(error, "format_message"):
|
|
182
210
|
typer.echo(f"Error: {spanish_usage_error(error)}", err=True)
|
|
183
211
|
else:
|
|
184
|
-
typer.echo(
|
|
212
|
+
typer.echo(crash_message(error), err=True)
|
|
185
213
|
|
|
186
214
|
|
|
187
215
|
def handle_line(session, root, line, console, products=None):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "operx-cli"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.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"
|
|
@@ -3,6 +3,7 @@ writes a small script to operx's folder and, after asking, one line in the
|
|
|
3
3
|
PowerShell profile that loads it; `--quitar` takes both away."""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
@@ -31,6 +32,7 @@ def test_the_script_completes_operx_and_leaves_tab_alone_elsewhere():
|
|
|
31
32
|
script.encode("ascii")
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
@pytest.mark.skipif(sys.platform != "win32", reason="PowerShell's profile is a Windows path")
|
|
34
36
|
def test_the_profile_line_names_the_folder_by_appdata(tmp_path):
|
|
35
37
|
"""No user name in the line: a profile saved in another encoding can't
|
|
36
38
|
garble it."""
|
|
@@ -3,7 +3,7 @@ import pytest
|
|
|
3
3
|
import typer.main
|
|
4
4
|
from typer.testing import CliRunner
|
|
5
5
|
|
|
6
|
-
from operx_cli import settings
|
|
6
|
+
from operx_cli import __version__, settings
|
|
7
7
|
from operx_cli.app import app
|
|
8
8
|
from operx_cli.commands.common import Session
|
|
9
9
|
from operx_cli.shell import STOP_WORDS, run_shell, split_line, suggestions
|
|
@@ -183,6 +183,23 @@ def test_a_refused_call_shows_the_apis_words_and_the_session_goes_on(capsys):
|
|
|
183
183
|
assert "Ana Ruiz" in out.out
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
def test_a_crash_says_what_failed_and_where_and_the_session_goes_on(capsys):
|
|
187
|
+
# A list where the CLI expects an object: an answer it doesn't know.
|
|
188
|
+
fake = FakeApi({("GET", "search/"): [{"kind": "employee", "id": 4, "title": "Ana Ríos"}], ("GET", "auth/me/"): ME})
|
|
189
|
+
|
|
190
|
+
shell(fake, "buscar prensa-zeta-77", "cuenta")
|
|
191
|
+
|
|
192
|
+
out = capsys.readouterr()
|
|
193
|
+
# The line a person sends to support: the error, the CLI's own line where
|
|
194
|
+
# it happened and the version, whose line numbers those are.
|
|
195
|
+
assert "AttributeError: 'list' object has no attribute 'get'" in out.err
|
|
196
|
+
assert "commands/search.py:" in out.err
|
|
197
|
+
assert f"operx {__version__}" in out.err
|
|
198
|
+
# Not what was typed: it can hold anything, a token included.
|
|
199
|
+
assert "prensa-zeta-77" not in out.err
|
|
200
|
+
assert "Ana Ruiz" in out.out
|
|
201
|
+
|
|
202
|
+
|
|
186
203
|
def test_help_inside_the_shell_does_not_end_it(capsys):
|
|
187
204
|
shell(FakeApi({("GET", "auth/me/"): ME}), "ordenes --help", "cuenta")
|
|
188
205
|
assert "Ana Ruiz" in capsys.readouterr().out
|
|
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
|
|
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
|