agmen 0.2.0__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.
- agmen-0.2.0/.gitignore +5 -0
- agmen-0.2.0/CHANGELOG.md +66 -0
- agmen-0.2.0/LICENSE +21 -0
- agmen-0.2.0/PKG-INFO +55 -0
- agmen-0.2.0/VERSION +1 -0
- agmen-0.2.0/pyproject.toml +49 -0
- agmen-0.2.0/python/README.md +33 -0
- agmen-0.2.0/python/agmen/__init__.py +29 -0
- agmen-0.2.0/python/agmen/__main__.py +7 -0
- agmen-0.2.0/python/agmen/_version.py +2 -0
- agmen-0.2.0/python/agmen/cli.py +121 -0
- agmen-0.2.0/python/agmen/panel/__init__.py +5 -0
- agmen-0.2.0/python/agmen/panel/index.html +461 -0
- agmen-0.2.0/python/agmen/panel/server.py +327 -0
- agmen-0.2.0/python/agmen/run.py +442 -0
- agmen-0.2.0/python/agmen/wire.py +122 -0
agmen-0.2.0/.gitignore
ADDED
agmen-0.2.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Todos los cambios importantes de Agmen se anotan aquí. Formato basado en
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/); versiones según
|
|
5
|
+
[SemVer](https://semver.org/lang/es/). Política completa en
|
|
6
|
+
[`docs/VERSIONES.md`](docs/VERSIONES.md).
|
|
7
|
+
|
|
8
|
+
## [Sin publicar]
|
|
9
|
+
|
|
10
|
+
### Planeado (nivel 1, versión 0.3.0)
|
|
11
|
+
- Sobre v1 de 64 bytes + codificación `compact` + esquema en texto (RFC 0001).
|
|
12
|
+
|
|
13
|
+
## [0.2.0] — 2026-10-30
|
|
14
|
+
|
|
15
|
+
**Todo el robot con un solo comando** y un panel web para controlarlo
|
|
16
|
+
(RFC 0003). Se acabaron las diez terminales.
|
|
17
|
+
|
|
18
|
+
### Agregado
|
|
19
|
+
- Comando `agmen` (se instala con `pip install agmen`).
|
|
20
|
+
- `agmen run robot.toml`: lanza todos los nodos descritos en un archivo, junta
|
|
21
|
+
sus logs en una sola terminal con color por nodo, los reinicia si fallan
|
|
22
|
+
(`restart = "on-failure"`), compila antes si se pide (`before`) y con Ctrl+C
|
|
23
|
+
los detiene a todos de forma ordenada. Errores de configuración con
|
|
24
|
+
sugerencias ("¿Quisiste decir 'name'?").
|
|
25
|
+
- Panel web de control: estado de cada nodo con botones iniciar / detener /
|
|
26
|
+
reiniciar, logs de todos los nodos con filtros, gráficas de sensores y
|
|
27
|
+
**manejo del robot** con teclado (W A S D) o botones táctiles. Al soltar, el
|
|
28
|
+
robot frena.
|
|
29
|
+
- `agmen web`: el panel solo, sin administrar nodos.
|
|
30
|
+
- `examples/tutorial/robot.toml` y capítulo 5 del tutorial.
|
|
31
|
+
- Pruebas `scripts/run_smoke.py` en el CI.
|
|
32
|
+
|
|
33
|
+
### Cambiado
|
|
34
|
+
- `tools/agmen-web/bridge.py` se reemplaza por `agmen web`, incluido en el paquete.
|
|
35
|
+
|
|
36
|
+
### Corregido (encontrado por el primer CI en GitHub)
|
|
37
|
+
- Windows: los nodos de Python ya no truenan al imprimir acentos o "→" cuando
|
|
38
|
+
su salida va a un archivo u otro programa (`agmen.ensure_utf8_output()`).
|
|
39
|
+
- macOS: el panel ya no tarda en arrancar por una consulta DNS lenta.
|
|
40
|
+
|
|
41
|
+
### Seguridad
|
|
42
|
+
- El panel rechaza peticiones de otros sitios (protección CSRF), recorta los
|
|
43
|
+
comandos de manejo a la velocidad máxima y limita su frecuencia.
|
|
44
|
+
|
|
45
|
+
## [0.1.0] — 2026-10-30
|
|
46
|
+
|
|
47
|
+
Primera versión pública: **Nivel 0**.
|
|
48
|
+
|
|
49
|
+
### Agregado
|
|
50
|
+
- Núcleo en C++20 (`#include <agmen/agmen.hpp>`, `namespace agmen::`) con CMake.
|
|
51
|
+
- Formato de cable v0: header de 32 bytes, `type_id` por hash del esquema y
|
|
52
|
+
errores claros (`agmen::wire::WireError`).
|
|
53
|
+
- Socket UDP multiplataforma (Windows/Linux/macOS) y sleep de alta resolución
|
|
54
|
+
en Windows.
|
|
55
|
+
- Ejemplos `publisher` y `subscriber` con medición de latencia y pérdidas.
|
|
56
|
+
- Paquete de Python `agmen` (implementación de referencia del formato).
|
|
57
|
+
- Prototipo de interfaz web `tools/agmen-web` (el navegador dibuja; el robot
|
|
58
|
+
solo publica).
|
|
59
|
+
- Tutorial: capítulos 0 a 4.
|
|
60
|
+
- RFC 0001 (formato de mensajes v1, **aceptado**) y RFC 0002 (interfaz web,
|
|
61
|
+
propuesta).
|
|
62
|
+
- CI en Windows (MSVC), Linux (GCC + sanitizers) y macOS (Clang).
|
|
63
|
+
|
|
64
|
+
[Sin publicar]: https://github.com/flkochu/Agmen/compare/v0.2.0...HEAD
|
|
65
|
+
[0.2.0]: https://github.com/flkochu/Agmen/releases/tag/v0.2.0
|
|
66
|
+
[0.1.0]: https://github.com/flkochu/Agmen/releases/tag/v0.1.0
|
agmen-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adán López Lozano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
agmen-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agmen
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Agmen: el sistema nervioso para robótica, IA e IoT
|
|
5
|
+
Project-URL: Repository, https://github.com/flkochu/Agmen
|
|
6
|
+
Project-URL: Changelog, https://github.com/flkochu/Agmen/blob/main/CHANGELOG.md
|
|
7
|
+
Author: Adán López Lozano
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai,iot,middleware,pubsub,robotica,robotics,ros
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Natural Language :: Spanish
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: tomli>=1.1; python_version < '3.11'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# agmen (Python)
|
|
24
|
+
|
|
25
|
+
**Agmen** es un framework de código abierto para robótica, IA e IoT. Su núcleo
|
|
26
|
+
(en C++) es el "sistema nervioso" del robot: mueve mensajes entre nodos de
|
|
27
|
+
forma rápida, confiable y con marca de tiempo.
|
|
28
|
+
|
|
29
|
+
Este paquete es la **implementación de referencia en Python** del formato de
|
|
30
|
+
mensajes. Con él, un nodo de Python habla con nodos de C++ sin dependencias
|
|
31
|
+
extra.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install agmen
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from agmen import wire
|
|
39
|
+
|
|
40
|
+
msg = wire.ImuSample(accel=(0.0, 0.0, 9.81), gyro=(0.0, 0.0, 0.1))
|
|
41
|
+
packet = wire.encode(msg, seq=0, stamp_ns=wire.now_ns())
|
|
42
|
+
header, back = wire.decode(wire.ImuSample, packet)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
También instala el comando `agmen`, que levanta todo tu robot con un solo
|
|
46
|
+
comando y abre un panel web para controlarlo:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
agmen run robot.toml # todos los nodos + panel web (nodos, logs, sensores, manejar)
|
|
50
|
+
agmen web # solo el panel
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> ⚠ Versión 0.x: la API todavía va a cambiar entre versiones menores.
|
|
54
|
+
|
|
55
|
+
Documentación, tutorial y código: <https://github.com/flkochu/Agmen>
|
agmen-0.2.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Paquete de Python `agmen` (pip install agmen).
|
|
2
|
+
#
|
|
3
|
+
# Hoy contiene la implementación de referencia del formato en Python puro.
|
|
4
|
+
# En el nivel 6 este mismo paquete se compilará desde el núcleo de C++
|
|
5
|
+
# (nanobind + scikit-build-core) sin cambiar de nombre.
|
|
6
|
+
|
|
7
|
+
[build-system]
|
|
8
|
+
requires = ["hatchling>=1.27"]
|
|
9
|
+
build-backend = "hatchling.build"
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "agmen"
|
|
13
|
+
dynamic = ["version"]
|
|
14
|
+
description = "Agmen: el sistema nervioso para robótica, IA e IoT"
|
|
15
|
+
readme = "python/README.md"
|
|
16
|
+
requires-python = ">=3.10"
|
|
17
|
+
dependencies = [
|
|
18
|
+
"tomli>=1.1; python_version < '3.11'", # robot.toml en Python 3.10
|
|
19
|
+
]
|
|
20
|
+
license = "MIT"
|
|
21
|
+
license-files = ["LICENSE"]
|
|
22
|
+
authors = [{ name = "Adán López Lozano" }]
|
|
23
|
+
keywords = ["robotics", "robotica", "middleware", "pubsub", "ros", "ai", "iot"]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Intended Audience :: Science/Research",
|
|
28
|
+
"Natural Language :: Spanish",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Topic :: Scientific/Engineering",
|
|
32
|
+
"Topic :: Software Development :: Libraries",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
agmen = "agmen.cli:main"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Repository = "https://github.com/flkochu/Agmen"
|
|
40
|
+
Changelog = "https://github.com/flkochu/Agmen/blob/main/CHANGELOG.md"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
path = "python/agmen/_version.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["python/agmen"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = ["python/agmen", "python/README.md", "LICENSE", "VERSION", "CHANGELOG.md"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# agmen (Python)
|
|
2
|
+
|
|
3
|
+
**Agmen** es un framework de código abierto para robótica, IA e IoT. Su núcleo
|
|
4
|
+
(en C++) es el "sistema nervioso" del robot: mueve mensajes entre nodos de
|
|
5
|
+
forma rápida, confiable y con marca de tiempo.
|
|
6
|
+
|
|
7
|
+
Este paquete es la **implementación de referencia en Python** del formato de
|
|
8
|
+
mensajes. Con él, un nodo de Python habla con nodos de C++ sin dependencias
|
|
9
|
+
extra.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install agmen
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from agmen import wire
|
|
17
|
+
|
|
18
|
+
msg = wire.ImuSample(accel=(0.0, 0.0, 9.81), gyro=(0.0, 0.0, 0.1))
|
|
19
|
+
packet = wire.encode(msg, seq=0, stamp_ns=wire.now_ns())
|
|
20
|
+
header, back = wire.decode(wire.ImuSample, packet)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
También instala el comando `agmen`, que levanta todo tu robot con un solo
|
|
24
|
+
comando y abre un panel web para controlarlo:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
agmen run robot.toml # todos los nodos + panel web (nodos, logs, sensores, manejar)
|
|
28
|
+
agmen web # solo el panel
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> ⚠ Versión 0.x: la API todavía va a cambiar entre versiones menores.
|
|
32
|
+
|
|
33
|
+
Documentación, tutorial y código: <https://github.com/flkochu/Agmen>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Agmen para Python.
|
|
2
|
+
|
|
3
|
+
Por ahora contiene la implementación de referencia del formato de cable en
|
|
4
|
+
Python puro (`agmen.wire`). Más adelante este paquete se compilará desde el
|
|
5
|
+
núcleo de C++ (nanobind) y `agmen.wire` quedará como referencia y oráculo de pruebas.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import wire
|
|
11
|
+
from ._version import __version__
|
|
12
|
+
|
|
13
|
+
__all__ = ["wire", "__version__", "ensure_utf8_output"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_utf8_output() -> None:
|
|
17
|
+
"""Hace que print() nunca truene por acentos, "→" o "µs".
|
|
18
|
+
|
|
19
|
+
En Windows, cuando la salida va a un archivo o a otro programa (no a la
|
|
20
|
+
consola), Python usa la codificación vieja cp1252 y falla con
|
|
21
|
+
UnicodeEncodeError. Llama a esto al inicio de tu nodo.
|
|
22
|
+
"""
|
|
23
|
+
for stream in (sys.stdout, sys.stderr):
|
|
24
|
+
if hasattr(stream, "reconfigure"):
|
|
25
|
+
try:
|
|
26
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
27
|
+
except (ValueError, OSError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""El comando `agmen`.
|
|
2
|
+
|
|
3
|
+
agmen run [robot.toml] lanza todo el robot + panel web, con un solo comando
|
|
4
|
+
agmen web solo el panel web (sin administrar nodos)
|
|
5
|
+
agmen version muestra la versión
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
|
|
15
|
+
from ._version import __version__
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _wait_for_ctrl_c(stop: threading.Event) -> None:
|
|
19
|
+
def handler(signum, frame):
|
|
20
|
+
stop.set()
|
|
21
|
+
|
|
22
|
+
signal.signal(signal.SIGINT, handler)
|
|
23
|
+
signal.signal(signal.SIGTERM, handler)
|
|
24
|
+
if hasattr(signal, "SIGBREAK"): # Windows
|
|
25
|
+
signal.signal(signal.SIGBREAK, handler)
|
|
26
|
+
while not stop.wait(0.3):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cmd_run(args) -> int:
|
|
31
|
+
from .panel import Panel, Teleop
|
|
32
|
+
from .run import ConfigError, Launcher, load_config
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
config = load_config(args.archivo)
|
|
36
|
+
except ConfigError as e:
|
|
37
|
+
print(f"error en {args.archivo}: {e}", file=sys.stderr)
|
|
38
|
+
return 2
|
|
39
|
+
|
|
40
|
+
launcher = Launcher(config)
|
|
41
|
+
launcher.say(f"Agmen {__version__} · robot '{config.name}' · {len(config.nodes)} nodos")
|
|
42
|
+
if not launcher.run_before():
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
panel = None
|
|
46
|
+
if config.web.enabled and not args.no_web:
|
|
47
|
+
teleop = Teleop(config.web.teleop.target, config.web.teleop.max_speed) if config.web.teleop else None
|
|
48
|
+
try:
|
|
49
|
+
panel = Panel(args.http or config.web.http, config.web.listen, config.name, launcher, teleop)
|
|
50
|
+
except OSError as e:
|
|
51
|
+
print(f"no se pudo abrir el panel web en {args.http or config.web.http}: {e}. "
|
|
52
|
+
f"¿Ya hay otro 'agmen run' corriendo? Usa --http 127.0.0.1:8081", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
panel.start(open_browser=config.web.open_browser and not args.no_browser)
|
|
55
|
+
launcher.say(f"panel web: {panel.url}")
|
|
56
|
+
|
|
57
|
+
launcher.start_all()
|
|
58
|
+
launcher.say("todo arrancado. Ctrl+C para detener todo.")
|
|
59
|
+
stop = threading.Event()
|
|
60
|
+
try:
|
|
61
|
+
_wait_for_ctrl_c(stop)
|
|
62
|
+
finally:
|
|
63
|
+
launcher.stop_all()
|
|
64
|
+
if panel:
|
|
65
|
+
panel.shutdown()
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_web(args) -> int:
|
|
70
|
+
from .panel import Panel, Teleop
|
|
71
|
+
|
|
72
|
+
teleop = Teleop(args.teleop, 1.0) if args.teleop else None
|
|
73
|
+
try:
|
|
74
|
+
panel = Panel(args.http, args.listen, "", None, teleop)
|
|
75
|
+
except OSError as e:
|
|
76
|
+
print(f"no se pudo abrir el panel en {args.http}: {e}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
panel.start(open_browser=not args.no_browser)
|
|
79
|
+
print(f"[agmen web] recibiendo Agmen en udp://{args.listen}")
|
|
80
|
+
print(f"[agmen web] abre {panel.url} en tu navegador")
|
|
81
|
+
if args.http.startswith("0.0.0.0"):
|
|
82
|
+
print("[agmen web] también accesible desde otros equipos de tu red (sin contraseña)")
|
|
83
|
+
stop = threading.Event()
|
|
84
|
+
try:
|
|
85
|
+
_wait_for_ctrl_c(stop)
|
|
86
|
+
finally:
|
|
87
|
+
panel.shutdown()
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main(argv: list[str] | None = None) -> int:
|
|
92
|
+
ap = argparse.ArgumentParser(prog="agmen", description="Agmen: el sistema nervioso para robótica, IA e IoT")
|
|
93
|
+
ap.add_argument("--version", action="version", version=f"agmen {__version__}")
|
|
94
|
+
sub = ap.add_subparsers(dest="cmd", required=True, metavar="comando")
|
|
95
|
+
|
|
96
|
+
p = sub.add_parser("run", help="lanza todos los nodos de robot.toml + el panel web")
|
|
97
|
+
p.add_argument("archivo", nargs="?", default="robot.toml", help="archivo del robot (default: robot.toml)")
|
|
98
|
+
p.add_argument("--no-web", action="store_true", help="no abrir el panel web")
|
|
99
|
+
p.add_argument("--no-browser", action="store_true", help="no abrir el navegador automáticamente")
|
|
100
|
+
p.add_argument("--http", help="dónde servir el panel (ej. 0.0.0.0:8080 para verlo desde el celular)")
|
|
101
|
+
p.set_defaults(func=cmd_run)
|
|
102
|
+
|
|
103
|
+
p = sub.add_parser("web", help="solo el panel web (sin administrar nodos)")
|
|
104
|
+
p.add_argument("--listen", default="127.0.0.1:7400", help="dónde recibir mensajes Agmen (UDP)")
|
|
105
|
+
p.add_argument("--http", default="127.0.0.1:8080", help="dónde servir el panel")
|
|
106
|
+
p.add_argument("--teleop", metavar="IP:PUERTO", help="habilita manejar el robot: destino de MotorCommand")
|
|
107
|
+
p.add_argument("--no-browser", action="store_true", help="no abrir el navegador automáticamente")
|
|
108
|
+
p.set_defaults(func=cmd_web)
|
|
109
|
+
|
|
110
|
+
p = sub.add_parser("version", help="muestra la versión")
|
|
111
|
+
p.set_defaults(func=lambda a: print(f"agmen {__version__}") or 0)
|
|
112
|
+
|
|
113
|
+
from . import ensure_utf8_output
|
|
114
|
+
|
|
115
|
+
ensure_utf8_output()
|
|
116
|
+
args = ap.parse_args(argv)
|
|
117
|
+
return args.func(args)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
sys.exit(main())
|