Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/basilica-sdk-python/python/basilica/rl.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ def fleet(count: int) -> dict:
def get_cluster(self, name: str) -> dict:
return json.loads(self._core.rl_get_cluster(name))

def delete_cluster(self, name: str) -> dict:
"""Delete a cluster. Refused (ValueError) while a job is active —
the error names the blocking job; delete the job first (that IS the
cancel path). Deleting the namespace's last cluster also tears down
its RL prerequisites server-side."""
return json.loads(self._core.rl_delete_cluster(name))

def delete_job(self, name: str) -> dict:
"""Delete a job — valid in any phase. Deleting a running job IS the
cancel path (pods are torn down by the operator's stop ladder)."""
return json.loads(self._core.rl_delete_job(name))

def wait_cluster(
self, name: str, timeout_s: float = 1800.0, poll_s: float = 15.0
) -> dict:
Expand Down
26 changes: 26 additions & 0 deletions crates/basilica-sdk-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,32 @@ impl BasilicaClient {
serde_json::to_string(&response).map_err(|e| PyRuntimeError::new_err(e.to_string()))
}

/// Delete an RL cluster (refused with an actionable error while a job
/// is active — delete the job first; that is the cancel path).
fn rl_delete_cluster(&self, py: Python, name: String) -> PyResult<String> {
let client = Arc::clone(&self.inner);
let response = py
.detach(|| {
self.runtime
.block_on(async move { client.delete_rl_cluster(&name).await })
})
.map_err(|e| self.map_error_to_python(e))?;
serde_json::to_string(&response).map_err(|e| PyRuntimeError::new_err(e.to_string()))
}

/// Delete an RL job — valid in any phase; deleting a running job IS the
/// cancel path.
fn rl_delete_job(&self, py: Python, name: String) -> PyResult<String> {
let client = Arc::clone(&self.inner);
let response = py
.detach(|| {
self.runtime
.block_on(async move { client.delete_rl_job(&name).await })
})
.map_err(|e| self.map_error_to_python(e))?;
serde_json::to_string(&response).map_err(|e| PyRuntimeError::new_err(e.to_string()))
}

/// Get an RL cluster's status (phase, modelLoaded, activeJobName).
fn rl_get_cluster(&self, py: Python, name: String) -> PyResult<String> {
let client = Arc::clone(&self.inner);
Expand Down
19 changes: 18 additions & 1 deletion crates/basilica-sdk/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ use crate::{
},
rl::{
CreateRlClusterRequest, CreateRlClusterResponse, CreateRlJobRequest, CreateRlJobResponse,
RlClusterStatusResponse, RlJobStatusResponse, RlManifestRequest, RlManifestResponse,
DeleteRlClusterResponse, DeleteRlJobResponse, RlClusterStatusResponse, RlJobStatusResponse,
RlManifestRequest, RlManifestResponse,
},
types::{
ApiKeyInfo, ApiKeyResponse, ApiListRentalsResponse, BalanceResponse, CardPurchaseResponse,
Expand Down Expand Up @@ -383,6 +384,22 @@ impl BasilicaClient {
self.get(&format!("/rl/jobs/{}", name)).await
}

/// Delete a cluster. Refused with an actionable 400 while a job is
/// active (the error names the blocking job) — delete the job first;
/// that IS the cancel path. Deleting the namespace's last cluster also
/// tears down its RL prerequisites server-side.
pub async fn delete_rl_cluster(&self, name: &str) -> Result<DeleteRlClusterResponse> {
Self::validate_rl_name(name)?;
self.delete(&format!("/rl/clusters/{}", name)).await
}

/// Delete a job — valid in any phase. Deleting a running job IS the
/// cancel path: the operator's stop ladder owns pod teardown.
pub async fn delete_rl_job(&self, name: &str) -> Result<DeleteRlJobResponse> {
Self::validate_rl_name(name)?;
self.delete(&format!("/rl/jobs/{}", name)).await
}

/// Submit a declarative manifest (renders a cluster and/or a job).
pub async fn submit_rl_manifest(
&self,
Expand Down
27 changes: 27 additions & 0 deletions crates/basilica-sdk/src/rl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,22 @@ pub struct RlManifestResponse {
pub job: Option<CreateRlJobResponse>,
}

/// Response after deleting a cluster (`DELETE /rl/clusters/{name}`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteRlClusterResponse {
/// The deleted cluster's name.
pub name: String,
}

/// Response after deleting a job (`DELETE /rl/jobs/{name}`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteRlJobResponse {
/// The deleted job's name.
pub name: String,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -296,6 +312,17 @@ mod tests {
assert!(v.get("lr").is_none());
}

#[test]
fn delete_response_wire_shape() {
// The server serializes camelCase; both delete responses carry only
// `name`. Pin the deserialization so a server-side field rename is
// caught here, not by a user.
let c: DeleteRlClusterResponse = serde_json::from_str(r#"{"name":"my-pool"}"#).unwrap();
assert_eq!(c.name, "my-pool");
let j: DeleteRlJobResponse = serde_json::from_str(r#"{"name":"my-pool-job"}"#).unwrap();
assert_eq!(j.name, "my-pool-job");
}

#[test]
fn cluster_request_wire_shape() {
let req = CreateRlClusterRequest {
Expand Down
Loading