36dda85259
Arquitectura asincronica completa para correr 24/7 a bordo del buque.
vmssailor/runtime/server/tag_store.py
- TagStore in-memory con pub/sub asyncio.Queue
- register_tag/register_many con valores iniciales
- TagValue dataclass: value + quality + timestamp + raw_value
- subscribe()/unsubscribe() para fan-out
- stats() con breakdown por Quality
vmssailor/runtime/server/historian.py
- Historian DuckDB embebido (in-memory o archivo)
- Reader loop suscrito al tag_store + buffer + flush periodico (1s)
- query(tag_id, since, until, limit) para series temporales
- Soporta valores numericos y boolean separadamente
vmssailor/runtime/server/alarm_engine.py
- Suscriptor al tag_store que evalua AlarmConfig por update
- Operators: >, >=, <, <=, ==, !=
- Hysteresis correcta: aplica al SALIR de alarma, no a entrar
- Delay configurable (persistencia minima antes de disparar)
- Estados: ACTIVE -> ACK (con user) -> CLEARED
- ack(alarm_id, user) reconoce sin clear
vmssailor/runtime/server/drivers.py
- SimulatorDriver: produce valores sinteticos creibles por UnitSI
- Tick configurable (default 0.5s)
- Respeta range_normal_min/max del tag para mantenerse en rango
- Permite probar UI/API sin hardware ni Modbus real
vmssailor/runtime/server/runtime_app.py
- RuntimeApp dataclass ensambla todos los servicios
- build_runtime(project) construye listo para correr
- start()/stop() async lifecycle ordenado
vmssailor/runtime/server/api.py
- FastAPI app con lifespan que arranca/detiene el runtime
- GET /health, /project
- GET /tags, /tags/{id}, /tags/{id}/history
- GET /alarms, POST /alarms/{id}/ack
- WebSocket /ws/realtime con snapshot inicial + push + heartbeat
runtime_server_main.py
- Entry point con argparse: --vmsproj, --host, --port, --db
- Sin --vmsproj usa proyecto demo Sprint 0 (genera simulator vivo)
- Lanza con uvicorn
Tests (tests/runtime/, 16 nuevos, total 142/142):
- test_tag_store: register, update, subscribe, unsubscribe, stats
- test_historian: roundtrip query, stats
- test_alarm_engine: fire when below, hysteresis clears, ack
- test_api: health, project, tags listing, history, alarms via httpx.ASGITransport
Para correr el servidor en vivo:
uv run python runtime_server_main.py --verbose
Luego en otro shell:
curl http://127.0.0.1:8765/health
curl http://127.0.0.1:8765/tags | jq .
Dependencias agregadas:
- fastapi >=0.110
- uvicorn[standard] >=0.27
- websockets >=12.0
- duckdb >=0.10
- pymodbus >=3.5 (Sprint 5)
- python-can >=4.3 (Sprint 5)
- httpx >=0.27 (testing + cliente HTTP)
142/142 pytest verde, ruff clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
3.4 KiB
TOML
150 lines
3.4 KiB
TOML
[project]
|
|
name = "vmssailor"
|
|
version = "0.1.0.dev0"
|
|
description = "VMS-Sailor — Vessel Management System integrado (IAS) para buques 30-40m. Studio + Runtime + Mobile + Firmware."
|
|
readme = "README.md"
|
|
requires-python = ">=3.11,<3.12"
|
|
license = { text = "Proprietary — Álvaro" }
|
|
authors = [
|
|
{ name = "Álvaro (Aerom)" }
|
|
]
|
|
keywords = ["marine", "vessel-management", "ias", "automation", "nmea2000", "modbus", "ecdis"]
|
|
|
|
dependencies = [
|
|
"pydantic>=2.5,<3.0",
|
|
"pyyaml>=6.0",
|
|
"python-dateutil>=2.8",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
studio = [
|
|
"PySide6>=6.6,<7.0",
|
|
]
|
|
runtime = [
|
|
"fastapi>=0.110",
|
|
"uvicorn[standard]>=0.27",
|
|
"websockets>=12.0",
|
|
"duckdb>=0.10",
|
|
"pymodbus>=3.5,<4.0",
|
|
"python-can>=4.3",
|
|
"httpx>=0.27",
|
|
]
|
|
dev = [
|
|
"pytest>=7.4",
|
|
"pytest-cov>=4.1",
|
|
"pytest-asyncio>=0.23",
|
|
"pytest-qt>=4.4",
|
|
"ruff>=0.4.0",
|
|
"mypy>=1.10",
|
|
"types-PyYAML",
|
|
"types-python-dateutil",
|
|
"PySide6>=6.6,<7.0",
|
|
"fastapi>=0.110",
|
|
"uvicorn[standard]>=0.27",
|
|
"duckdb>=0.10",
|
|
"httpx>=0.27",
|
|
]
|
|
|
|
[project.scripts]
|
|
vms-validate-library = "vmssailor.tools.validate_library:main"
|
|
vms-generate-test-project = "vmssailor.tools.generate_test_project:main"
|
|
vms-studio = "vmssailor.studio.app:run_studio"
|
|
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["vmssailor"]
|
|
|
|
# ---------- ruff ----------
|
|
[tool.ruff]
|
|
line-length = 100
|
|
target-version = "py311"
|
|
extend-exclude = ["projects", "firmware", "mobile", "docs/mockups"]
|
|
|
|
[tool.ruff.lint]
|
|
select = [
|
|
"E", # pycodestyle errors
|
|
"W", # pycodestyle warnings
|
|
"F", # pyflakes
|
|
"I", # isort
|
|
"B", # bugbear
|
|
"C4", # comprehensions
|
|
"UP", # pyupgrade
|
|
"SIM", # simplify
|
|
"RUF", # ruff-specific
|
|
]
|
|
ignore = [
|
|
"E501", # handled by formatter
|
|
]
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
"tests/**" = ["B011"]
|
|
"tools/**" = ["E402"]
|
|
"vmssailor/studio/**" = ["RUF001", "E402"] # caracteres tipograficos intencionales en UI ES; imports diferidos de Signal/Property en QWizard
|
|
|
|
[tool.ruff.format]
|
|
quote-style = "double"
|
|
|
|
# ---------- mypy ----------
|
|
[tool.mypy]
|
|
python_version = "3.11"
|
|
strict = true
|
|
warn_unused_configs = true
|
|
disallow_untyped_defs = true
|
|
disallow_incomplete_defs = true
|
|
no_implicit_optional = true
|
|
warn_redundant_casts = true
|
|
warn_unused_ignores = true
|
|
warn_no_return = true
|
|
warn_return_any = true
|
|
warn_unreachable = true
|
|
exclude = ["^build/", "^dist/", "^projects/", "^firmware/", "^mobile/", "^docs/"]
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = ["tests.*", "tools.*"]
|
|
disallow_untyped_defs = false
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = ["yaml.*", "dateutil.*"]
|
|
ignore_missing_imports = true
|
|
|
|
# ---------- pytest ----------
|
|
[tool.pytest.ini_options]
|
|
minversion = "7.4"
|
|
testpaths = ["tests"]
|
|
python_files = ["test_*.py"]
|
|
python_classes = ["Test*"]
|
|
python_functions = ["test_*"]
|
|
addopts = [
|
|
"-ra",
|
|
"--strict-markers",
|
|
"--strict-config",
|
|
"--tb=short",
|
|
]
|
|
markers = [
|
|
"slow: marks tests as slow",
|
|
"integration: cross-module integration tests",
|
|
]
|
|
asyncio_mode = "auto"
|
|
|
|
[tool.coverage.run]
|
|
source = ["vmssailor"]
|
|
omit = [
|
|
"vmssailor/studio/*",
|
|
"vmssailor/runtime/*",
|
|
"vmssailor/__init__.py",
|
|
"vmssailor/version.py",
|
|
]
|
|
|
|
[tool.coverage.report]
|
|
exclude_lines = [
|
|
"pragma: no cover",
|
|
"raise NotImplementedError",
|
|
"if __name__ == .__main__.:",
|
|
"if TYPE_CHECKING:",
|
|
]
|
|
fail_under = 80
|
|
show_missing = true
|