Files
AR-VMS-Seaman/pyproject.toml
T
alro65 36dda85259 sprint-4: Runtime server base — tag_store + historian + alarm engine + API FastAPI
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>
2026-05-17 20:03:19 -04:00

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