mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
feat: add a Horizon host option to login
This commit is contained in:
parent
35ecc14234
commit
e877785511
6 changed files with 70 additions and 1 deletions
|
|
@ -98,6 +98,13 @@ fastmcp logout
|
|||
It opens a browser when the terminal supports it.
|
||||
If the browser does not open, use the shown URL and code on another device.
|
||||
|
||||
Use `--host` to connect to another Horizon environment.
|
||||
The CLI saves the host and clears the stored key before a host change.
|
||||
|
||||
```bash
|
||||
fastmcp login --host https://horizon.example.com
|
||||
```
|
||||
|
||||
Login stores only the personal Horizon API key.
|
||||
It does not select or store a deployment organization.
|
||||
`fastmcp whoami` gets the current user and organization memberships from Horizon.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ fastmcp login
|
|||
The command shows a verification URL and code before it opens the browser.
|
||||
If the browser cannot open, visit the shown URL and enter the code.
|
||||
New users can register and create their first Horizon organization in the browser.
|
||||
Use `fastmcp login --host <url>` to save a different Horizon host.
|
||||
A host change clears the stored credential before login.
|
||||
|
||||
Check the active account and its current organization memberships after login.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,13 @@ JsonOption = Annotated[
|
|||
negative=(),
|
||||
),
|
||||
]
|
||||
HostOption = Annotated[
|
||||
str | None,
|
||||
Parameter(
|
||||
name="--host",
|
||||
help="Use and save a different Horizon host URL",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _can_open_browser() -> bool:
|
||||
|
|
@ -149,13 +156,29 @@ async def _get_identity(
|
|||
|
||||
async def login(
|
||||
*,
|
||||
host: HostOption = None,
|
||||
json_output: JsonOption = False,
|
||||
) -> None:
|
||||
"""Sign in to Prefect Horizon."""
|
||||
credentials = CredentialStore()
|
||||
|
||||
try:
|
||||
configuration = ConfigurationStore().load()
|
||||
configuration_store = ConfigurationStore()
|
||||
if host is None:
|
||||
configuration = configuration_store.load()
|
||||
else:
|
||||
try:
|
||||
configuration = configuration_store.set_api_origin(
|
||||
host,
|
||||
credentials=credentials,
|
||||
)
|
||||
except ValueError:
|
||||
_fail(
|
||||
"login",
|
||||
"invalid_host",
|
||||
"The Horizon host must be an HTTP origin.",
|
||||
json_output=json_output,
|
||||
)
|
||||
|
||||
async def device_authorization():
|
||||
async with HorizonClient(configuration.api_origin) as client:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ ErrorCategory = Literal[
|
|||
"authorization_failed",
|
||||
"horizon_error",
|
||||
"horizon_unavailable",
|
||||
"invalid_host",
|
||||
"remote_revocation_failed",
|
||||
"state_error",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -154,6 +154,35 @@ async def test_json_login_writes_one_result_and_challenge_to_stderr(
|
|||
assert not (fastmcp.settings.home / "cli" / "config.json").exists()
|
||||
|
||||
|
||||
async def test_login_host_is_saved_before_device_authorization(
|
||||
use_horizon_api: Callable[[HorizonAuthAPI], None],
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
api = HorizonAuthAPI()
|
||||
use_horizon_api(api)
|
||||
|
||||
await login(host="https://dev.horizon.prefect.io/", json_output=True)
|
||||
|
||||
assert json.loads(capsys.readouterr().out)["ok"] is True
|
||||
configuration_path = fastmcp.settings.home / "cli" / "config.json"
|
||||
assert json.loads(configuration_path.read_text()) == {
|
||||
"schemaVersion": 1,
|
||||
"apiOrigin": "https://dev.horizon.prefect.io",
|
||||
}
|
||||
assert {request.url.host for request in api.requests} == {"dev.horizon.prefect.io"}
|
||||
|
||||
|
||||
async def test_login_rejects_an_invalid_host(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
await login(host="https://horizon.prefect.io/path", json_output=True)
|
||||
|
||||
result = json.loads(capsys.readouterr().out)
|
||||
assert result["error"]["category"] == "invalid_host"
|
||||
assert CredentialStore().path.exists() is False
|
||||
|
||||
|
||||
async def test_tty_login_survives_browser_open_failure(
|
||||
use_horizon_api: Callable[[HorizonAuthAPI], None],
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ class TestMainCLI:
|
|||
assert command.__name__ == name # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
||||
assert bound.arguments == {"json_output": True}
|
||||
|
||||
def test_login_accepts_a_horizon_host(self):
|
||||
_, bound, _ = app.parse_args(
|
||||
["login", "--host", "https://dev.horizon.prefect.io"]
|
||||
)
|
||||
|
||||
assert bound.arguments == {"host": "https://dev.horizon.prefect.io"}
|
||||
|
||||
|
||||
class TestVersionCommand:
|
||||
"""Test the version command."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue