Skip to content

Commit 5361610

Browse files
committed
update cli config and include start command
1 parent 9c9d86b commit 5361610

File tree

8 files changed

+180
-46
lines changed

8 files changed

+180
-46
lines changed

antares-python/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ build/
77
.coverage
88
.coverage.*
99
htmlcov/
10+
antares.log

antares-python/pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies = [
2020
dev = ["pytest-asyncio>=0.26.0"]
2121

2222
[project.scripts]
23-
antares = "antares.cli:app"
23+
antares-cli = "antares.cli:app"
2424

2525
[build-system]
2626
requires = ["setuptools>=61.0", "wheel"]
@@ -40,7 +40,6 @@ files = ["src"]
4040
pythonpath = "src"
4141
asyncio_mode = "auto"
4242
asyncio_default_fixture_loop_scope = "function"
43-
addopts = "-ra -q -ra -q --cov=src --cov-report=term-missing"
4443

4544
[tool.setuptools.packages.find]
4645
where = ["src"]
@@ -50,8 +49,8 @@ lint = "ruff check . --fix"
5049
format = "ruff format ."
5150
typecheck = "mypy src/"
5251
test = "pytest"
53-
coverage = "pytest --cov=src --cov-report=term-missing"
52+
coverage = "pytest -ra -q --cov=src --cov-report=term-missing"
5453
build = "python -m build"
5554
publish = "twine upload dist/* --repository antares-python"
56-
check = "task lint && task format && task typecheck && task test"
55+
check = "task lint && task format && task typecheck && task coverage"
5756
release = "task check && task build && task publish"

antares-python/src/antares/cli.py

Lines changed: 94 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import asyncio
22
import json
33
import logging
4+
import shutil
5+
import subprocess
6+
from pathlib import Path
47
from typing import NoReturn
58

69
import typer
@@ -12,46 +15,67 @@
1215
from antares.errors import ConnectionError, SimulationError, SubscriptionError
1316
from antares.logger import setup_logging
1417

15-
app = typer.Typer(name="antares", help="Antares CLI for ship simulation", no_args_is_help=True)
18+
app = typer.Typer(name="antares-cli", help="Antares CLI for ship simulation", no_args_is_help=True)
1619
console = Console(theme=Theme({"info": "green", "warn": "yellow", "error": "bold red"}))
1720

1821

19-
def handle_error(message: str, code: int, json_output: bool = False) -> NoReturn:
20-
logger = logging.getLogger("antares.cli")
21-
if json_output:
22-
typer.echo(json.dumps({"error": message}), err=True)
23-
else:
24-
console.print(f"[error]{message}")
25-
logger.error("Exiting with error: %s", message)
26-
raise typer.Exit(code)
27-
28-
29-
def build_client(config_path: str | None, verbose: bool, json_output: bool) -> AntaresClient:
30-
setup_logging(level=logging.DEBUG if verbose else logging.INFO)
31-
logger = logging.getLogger("antares.cli")
22+
@app.command()
23+
def start(
24+
executable: str = typer.Option("antares", help="Path to the Antares executable"),
25+
config: str | None = typer.Option(None, help="Path to the TOML configuration file"),
26+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
27+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
28+
) -> None:
29+
"""
30+
Start the Antares simulation engine in the background.
31+
32+
This command attempts to locate and launch the Antares executable either from the system's PATH
33+
or from the provided path using the --executable option. If a config path is provided, it is
34+
passed to the executable via --config.
35+
36+
This command does not use the Python client and directly invokes the native binary.
37+
"""
38+
# Locate executable (either absolute path or in system PATH)
39+
path = shutil.which(executable) if not Path(executable).exists() else executable
40+
if path is None:
41+
msg = f"Executable '{executable}' not found in PATH or at specified location."
42+
console.print(f"[error]{msg}")
43+
raise typer.Exit(1)
44+
45+
# Prepare command
46+
command = [path]
47+
if config:
48+
command += ["--config", config]
49+
50+
if verbose:
51+
console.print(f"[info]Starting Antares with command: {command}")
3252

3353
try:
34-
settings = load_config(config_path)
35-
if verbose:
36-
console.print(f"[info]Using settings: {settings.model_dump()}")
37-
logger.debug("Loaded settings: %s", settings.model_dump())
38-
return AntaresClient(
39-
base_url=settings.base_url,
40-
tcp_host=settings.tcp_host,
41-
tcp_port=settings.tcp_port,
42-
timeout=settings.timeout,
43-
auth_token=settings.auth_token,
44-
)
54+
process = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
4555
except Exception as e:
46-
handle_error(f"Failed to load configuration: {e}", code=1, json_output=json_output)
56+
msg = f"Failed to start Antares: {e}"
57+
if json_output:
58+
typer.echo(json.dumps({"error": msg}), err=True)
59+
else:
60+
console.print(f"[error]{msg}")
61+
raise typer.Exit(2) from e
62+
63+
msg = f"Antares started in background with PID {process.pid}"
64+
if json_output:
65+
typer.echo(json.dumps({"message": msg, "pid": process.pid}))
66+
else:
67+
console.print(f"[success]{msg}")
4768

4869

4970
@app.command()
5071
def reset(
51-
config: str = typer.Option(None),
52-
verbose: bool = typer.Option(False, "--verbose", "-v"),
72+
config: str = typer.Option(None, help="Path to the TOML configuration file"),
73+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
5374
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
5475
) -> None:
76+
"""
77+
Reset the current simulation state.
78+
"""
5579
client = build_client(config, verbose, json_output)
5680
try:
5781
client.reset_simulation()
@@ -65,10 +89,13 @@ def reset(
6589
def add_ship(
6690
x: float = typer.Option(..., help="X coordinate of the ship"),
6791
y: float = typer.Option(..., help="Y coordinate of the ship"),
68-
config: str = typer.Option(None, help="Path to the configuration file"),
92+
config: str = typer.Option(None, help="Path to the TOML configuration file"),
6993
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
7094
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
7195
) -> None:
96+
"""
97+
Add a ship to the simulation with the specified parameters.
98+
"""
7299
client = build_client(config, verbose, json_output)
73100
try:
74101
ship = ShipConfig(initial_position=(x, y))
@@ -81,11 +108,14 @@ def add_ship(
81108

82109
@app.command()
83110
def subscribe(
84-
config: str = typer.Option(None),
85-
verbose: bool = typer.Option(False, "--verbose", "-v"),
111+
config: str = typer.Option(None, help="Path to the TOML configuration file"),
112+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
86113
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
87114
log_file: str = typer.Option("antares.log", help="Path to log file"),
88115
) -> None:
116+
"""
117+
Subscribe to simulation events and print them to the console.
118+
"""
89119
setup_logging(log_file=log_file, level=logging.DEBUG if verbose else logging.INFO)
90120
logger = logging.getLogger("antares.cli")
91121

@@ -103,3 +133,36 @@ async def _sub() -> None:
103133
handle_error(str(e), code=3, json_output=json_output)
104134

105135
asyncio.run(_sub())
136+
137+
138+
def handle_error(message: str, code: int, json_output: bool = False) -> NoReturn:
139+
"""
140+
Handle errors by logging and printing them to the console.
141+
"""
142+
logger = logging.getLogger("antares.cli")
143+
if json_output:
144+
typer.echo(json.dumps({"error": message}), err=True)
145+
else:
146+
console.print(f"[error]{message}")
147+
logger.error("Exiting with error: %s", message)
148+
raise typer.Exit(code)
149+
150+
151+
def build_client(config_path: str | None, verbose: bool, json_output: bool) -> AntaresClient:
152+
"""
153+
Build the Antares client using the provided configuration file.
154+
"""
155+
156+
try:
157+
settings = load_config(config_path)
158+
if verbose:
159+
console.print(f"[info]Using settings: {settings.model_dump()}")
160+
return AntaresClient(
161+
host=settings.host,
162+
http_port=settings.http_port,
163+
tcp_port=settings.tcp_port,
164+
timeout=settings.timeout,
165+
auth_token=settings.auth_token,
166+
)
167+
except Exception as e:
168+
handle_error(f"Failed to load configuration: {e}", code=1, json_output=json_output)

antares-python/src/antares/client/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ def __init__(
2525
# Merge provided arguments with environment/.env via AntaresSettings
2626
self._settings = AntaresSettings(**filtered_kwargs)
2727

28+
base_url = f"http://{self._settings.host}:{self._settings.http_port}"
2829
self._rest = RestClient(
29-
base_url=self._settings.base_url,
30+
base_url=base_url,
3031
timeout=self._settings.timeout,
3132
auth_token=self._settings.auth_token,
3233
)
33-
self._tcp = TCPSubscriber(host=self._settings.tcp_host, port=self._settings.tcp_port)
34+
self._tcp = TCPSubscriber(
35+
host=self._settings.host,
36+
port=self._settings.tcp_port,
37+
)
3438

3539
def reset_simulation(self) -> None:
3640
"""

antares-python/src/antares/client/rest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import httpx
22

3-
from antares.errors import ConnectionError, SimulationError
3+
from antares.errors import ConnectionError, ShipConfigError, SimulationError
44
from antares.models.ship import ShipConfig
55

66

@@ -56,4 +56,4 @@ def add_ship(self, ship: ShipConfig) -> None:
5656
except httpx.RequestError as e:
5757
raise ConnectionError(f"Could not reach Antares API: {e}") from e
5858
except httpx.HTTPStatusError as e:
59-
raise SimulationError(f"Add ship failed: {e.response.text}") from e
59+
raise ShipConfigError(f"Add ship failed: {e.response.text}") from e

antares-python/src/antares/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ class AntaresSettings(BaseSettings):
77
Supports environment variables and `.env` file loading.
88
"""
99

10-
base_url: str = "http://localhost:8000"
11-
tcp_host: str = "localhost"
12-
tcp_port: int = 9000
10+
host: str = "localhost"
11+
http_port: int = 9000
12+
tcp_port: int = 9001
1313
timeout: float = 5.0
1414
auth_token: str | None = None
1515

antares-python/tests/client/test_rest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pytest
33

44
from antares.client.rest import RestClient
5-
from antares.errors import ConnectionError, SimulationError
5+
from antares.errors import ConnectionError, ShipConfigError, SimulationError
66
from antares.models.ship import ShipConfig
77

88

@@ -42,7 +42,7 @@ def test_add_ship_invalid_response(mocker):
4242
)
4343
ship = ShipConfig(initial_position=(0, 0))
4444
client = RestClient(base_url="http://localhost")
45-
with pytest.raises(SimulationError):
45+
with pytest.raises(ShipConfigError):
4646
client.add_ship(ship)
4747

4848

antares-python/tests/test_cli.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import subprocess
23

34
import pytest
45
from typer.testing import CliRunner
@@ -14,8 +15,8 @@ def fake_config(tmp_path):
1415
config_file = tmp_path / "config.toml"
1516
config_file.write_text("""
1617
[antares]
17-
base_url = "http://test.local"
18-
tcp_host = "127.0.0.1"
18+
host = "localhost"
19+
http_port = 9000
1920
tcp_port = 9001
2021
timeout = 2.0
2122
auth_token = "fake-token"
@@ -134,3 +135,69 @@ async def __anext__(self):
134135

135136
assert result.exit_code == 0
136137
assert '{"event": "test"}' in result.output
138+
139+
140+
def test_start_success(mocker):
141+
mock_which = mocker.patch("shutil.which", return_value="/usr/local/bin/antares")
142+
mock_popen = mocker.patch("subprocess.Popen", return_value=mocker.Mock(pid=1234))
143+
144+
result = runner.invoke(app, ["start"])
145+
assert result.exit_code == 0
146+
assert "Antares started in background with PID 1234" in result.output
147+
mock_which.assert_called_once()
148+
mock_popen.assert_called_once_with(
149+
["/usr/local/bin/antares"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
150+
)
151+
152+
153+
def test_start_executable_not_found(mocker):
154+
mocker.patch("shutil.which", return_value=None)
155+
156+
result = runner.invoke(app, ["start", "--executable", "fake-antares"])
157+
assert result.exit_code == 1
158+
assert "Executable 'fake-antares' not found" in result.output
159+
160+
161+
def test_start_popen_failure(mocker):
162+
mocker.patch("shutil.which", return_value="/usr/bin/antares")
163+
mocker.patch("subprocess.Popen", side_effect=OSError("boom"))
164+
165+
result = runner.invoke(app, ["start"])
166+
expected_exit_code = 2
167+
assert result.exit_code == expected_exit_code
168+
assert "Failed to start Antares" in result.output
169+
170+
171+
def test_start_popen_failure_with_json_verbose(mocker):
172+
mocker.patch("shutil.which", return_value="/usr/bin/antares")
173+
mocker.patch("subprocess.Popen", side_effect=OSError("boom"))
174+
175+
result = runner.invoke(app, ["start", "--json", "-v"])
176+
177+
expected_exit_code = 2
178+
assert result.exit_code == expected_exit_code
179+
assert '{"error":' in result.stdout or result.stderr
180+
assert "Failed to start Antares: boom" in result.output
181+
182+
183+
def test_start_with_json_output(mocker):
184+
mocker.patch("shutil.which", return_value="/usr/bin/antares")
185+
mocker.patch("subprocess.Popen", return_value=mocker.Mock(pid=4321))
186+
187+
result = runner.invoke(app, ["start", "--json"])
188+
assert result.exit_code == 0
189+
assert '{"message":' in result.output
190+
assert '"pid": 4321' in result.output
191+
192+
193+
def test_start_with_config(mocker):
194+
mocker.patch("shutil.which", return_value="/usr/local/bin/antares")
195+
mock_popen = mocker.patch("subprocess.Popen", return_value=mocker.Mock(pid=5678))
196+
197+
result = runner.invoke(app, ["start", "--config", "config.toml"])
198+
assert result.exit_code == 0
199+
mock_popen.assert_called_once_with(
200+
["/usr/local/bin/antares", "--config", "config.toml"],
201+
stdout=subprocess.DEVNULL,
202+
stderr=subprocess.DEVNULL,
203+
)

0 commit comments

Comments
 (0)