mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
### Summary
This PR introduces a subcommand implementation that allows `langgraph
deploy list` and `langgraph deploy delete` subcommands.
#### `langgraph deploy list`
```bash
(env) andrewnguonly@Andrew-Nguonly-KC23X90J02 langgraph % langgraph deploy list --help ⎈ gke_langchain-test-387119_us-west1_langgraph-cloud-us-west1
Usage: langgraph deploy list [OPTIONS]
[Beta] List LangSmith Deployments.
Options:
--name-contains TEXT Only show deployments whose names contain this value.
--api-key TEXT API key. Can also be set via LANGGRAPH_HOST_API_KEY,
LANGSMITH_API_KEY, or LANGCHAIN_API_KEY environment
variable or .env file.
--help Show this message and exit.
```
Output example:
```bash
(env) andrewnguonly@Andrew-Nguonly-KC23X90J02 cli % langgraph deploy list
Deployment ID Deployment Name Deployment URL
------------------------------------ -------------------------- ------------------------------------------------------------------------------------
a40d6567-87c0-485a-a23d-94309a7d4519 ht-andrew-test-04 -
9da26acb-d0c9-4af0-af9e-f3fe8dfe85bc ht-anirudh-deployment-test https://ht-anirudh-deployment-test-428af4737f8a533cb2b107587eb8f38f.us.langgraph.app
```
#### `langgraph deploy delete`
```bash
(env) andrewnguonly@Andrew-Nguonly-KC23X90J02 langgraph % langgraph deploy delete --help ⎈ gke_langchain-test-387119_us-west1_langgraph-cloud-us-west1
Usage: langgraph deploy delete [OPTIONS] DEPLOYMENT_ID
[Beta] Delete a LangSmith Deployment.
Options:
--force Delete without prompting for confirmation.
--api-key TEXT API key. Can also be set via LANGGRAPH_HOST_API_KEY,
LANGSMITH_API_KEY, or LANGCHAIN_API_KEY environment variable
or .env file.
--help Show this message and exit.
```
Output example:
```bash
(env) andrewnguonly@Andrew-Nguonly-KC23X90J02 cli % langgraph deploy delete a40d6567-87c0-485a-a23d-94309a7d4519
Are you sure you want to delete deployment ID a40d6567-87c0-485a-a23d-94309a7d4519? (Y/n): Y
Host API key:
Deleted deployment a40d6567-87c0-485a-a23d-94309a7d4519.
```
```bash
(env) andrewnguonly@Andrew-Nguonly-KC23X90J02 cli % langgraph deploy delete a40d6567-87c0-485a-a23d-94309a7d4519
Are you sure you want to delete deployment ID a40d6567-87c0-485a-a23d-94309a7d4519? (Y/n): n
Aborted!
```
```bash
(env) andrewnguonly@Andrew-Nguonly-KC23X90J02 cli % langgraph deploy delete 9da26acb-d0c9-4af0-af9e-f3fe8dfe85bc --force
Host API key:
Deleted deployment 9da26acb-d0c9-4af0-af9e-f3fe8dfe85bc.
```
185 lines
5.8 KiB
Python
185 lines
5.8 KiB
Python
import httpx
|
|
import pytest
|
|
|
|
from langgraph_cli.host_backend import HostBackendClient, HostBackendError
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_transport():
|
|
return httpx.MockTransport(lambda req: httpx.Response(200, json={"ok": True}))
|
|
|
|
|
|
@pytest.fixture
|
|
def client(mock_transport):
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=mock_transport,
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
return c
|
|
|
|
|
|
def test_constructor_strips_trailing_slash():
|
|
c = HostBackendClient("https://api.example.com/", "key")
|
|
assert str(c._client.base_url) == "https://api.example.com"
|
|
|
|
|
|
def test_constructor_empty_url_raises():
|
|
with pytest.raises(Exception, match="Host backend URL is required"):
|
|
HostBackendClient("", "key")
|
|
|
|
|
|
def test_request_sends_headers():
|
|
def handler(req: httpx.Request) -> httpx.Response:
|
|
assert req.headers["x-api-key"] == "test-key"
|
|
assert req.headers["accept"] == "application/json"
|
|
return httpx.Response(200, json={"ok": True})
|
|
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=httpx.MockTransport(handler),
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
result = c._request("GET", "/test")
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_request_sends_json_payload():
|
|
def handler(req: httpx.Request) -> httpx.Response:
|
|
assert req.headers["content-type"] == "application/json"
|
|
assert req.content == b'{"key":"value"}'
|
|
return httpx.Response(200, json={"created": True})
|
|
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=httpx.MockTransport(handler),
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
result = c._request("POST", "/test", {"key": "value"})
|
|
assert result == {"created": True}
|
|
|
|
|
|
def test_request_empty_body_returns_none():
|
|
transport = httpx.MockTransport(lambda req: httpx.Response(200, content=b""))
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=transport,
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
assert c._request("DELETE", "/test") is None
|
|
|
|
|
|
def test_request_http_error_raises():
|
|
transport = httpx.MockTransport(lambda req: httpx.Response(404, text="not found"))
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=transport,
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
with pytest.raises(HostBackendError, match="404"):
|
|
c._request("GET", "/missing")
|
|
|
|
|
|
def test_request_invalid_json_raises():
|
|
transport = httpx.MockTransport(
|
|
lambda req: httpx.Response(200, content=b"not json")
|
|
)
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=transport,
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
with pytest.raises(HostBackendError, match="Failed to decode"):
|
|
c._request("GET", "/bad-json")
|
|
|
|
|
|
def test_request_transport_error_raises():
|
|
def handler(req: httpx.Request) -> httpx.Response:
|
|
raise httpx.ConnectError("connection refused")
|
|
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=httpx.MockTransport(handler),
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
with pytest.raises(HostBackendError, match="connection refused"):
|
|
c._request("GET", "/test")
|
|
|
|
|
|
def test_create_deployment(client):
|
|
result = client.create_deployment({"name": "my-deploy"})
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_get_deployment(client):
|
|
result = client.get_deployment("dep-123")
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_list_deployments(client):
|
|
result = client.list_deployments("my-app")
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_list_deployments_sends_query_params():
|
|
def handler(req: httpx.Request) -> httpx.Response:
|
|
assert req.url.path == "/v2/deployments"
|
|
assert req.url.params["name_contains"] == "my app"
|
|
return httpx.Response(200, json={"ok": True})
|
|
|
|
c = HostBackendClient("https://api.example.com", "test-key")
|
|
c._client = httpx.Client(
|
|
base_url="https://api.example.com",
|
|
transport=httpx.MockTransport(handler),
|
|
headers={"X-Api-Key": "test-key", "Accept": "application/json"},
|
|
timeout=30,
|
|
)
|
|
result = c.list_deployments("my app")
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_delete_deployment(client):
|
|
result = client.delete_deployment("dep-123")
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_request_push_token(client):
|
|
result = client.request_push_token("dep-123")
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_update_deployment(client):
|
|
result = client.update_deployment(
|
|
"dep-123", "image:latest", secrets=[{"name": "KEY", "value": "val"}]
|
|
)
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_update_deployment_no_secrets(client):
|
|
result = client.update_deployment("dep-123", "image:latest")
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_list_revisions(client):
|
|
result = client.list_revisions("dep-123", limit=5)
|
|
assert result == {"ok": True}
|
|
|
|
|
|
def test_get_revision(client):
|
|
result = client.get_revision("dep-123", "rev-456")
|
|
assert result == {"ok": True}
|