diff --git a/Cargo.toml b/Cargo.toml index a67d7d7..b4456c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ authors = ["Tracel team"] homepage = "https://burn.dev" repository = "https://github.com/tracel-ai/burn-central-client" license = "MIT OR Apache-2.0" +rust-version = "1.97.0" [workspace.dependencies] reqwest = { version = "0.13.4", features = ["blocking", "form"] } diff --git a/tracel-client/Cargo.toml b/tracel-client/Cargo.toml index 88a5ceb..9c3e6e5 100644 --- a/tracel-client/Cargo.toml +++ b/tracel-client/Cargo.toml @@ -2,6 +2,7 @@ name = "tracel-client" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true description = "Collection of HTTP calls required to communicate with the Tracel backend" homepage.workspace = true diff --git a/tracel-client/src/console/job/mod.rs b/tracel-client/src/console/job/mod.rs index b5ea954..1a1dfc2 100644 --- a/tracel-client/src/console/job/mod.rs +++ b/tracel-client/src/console/job/mod.rs @@ -1,28 +1,104 @@ +pub mod response; + mod body; use crate::{ ClientError, - console::{Client, job::body::ComputeProviderQueueJobRequest}, + console::{ + Client, + job::{ + body::ComputeProviderQueueJobRequest, + response::{JobLogResponse, JobResponse, ProjectJobsResponse, QueuedJobResponse}, + }, + }, }; impl Client { - pub fn start_remote_job( + /// Queue a job that runs the code version `digest` on a compute provider group. + /// + /// `command` holds the program's arguments, which the compute provider splits into + /// words with shell quoting rules. + /// + /// The client must be logged in before calling this method. + pub fn queue_job( &self, - compute_provider_group_name: &str, - owner_name: &str, + namespace: &str, project_name: &str, + compute_provider_group_name: &str, digest: &str, command: &str, - ) -> Result<(), ClientError> { - let path: &str = &format!("projects/{owner_name}/{project_name}/jobs/queue"); - let url = self.transport.join(path); - + ) -> Result { let body = ComputeProviderQueueJobRequest { compute_provider_group_name: compute_provider_group_name.to_string(), digest: digest.to_string(), command: command.to_string(), }; - self.transport.post(url, Some(body)) + self.transport.post_json( + format!("projects/{namespace}/{project_name}/jobs/queue"), + Some(body), + ) + } + + /// List the jobs of a project. + /// + /// The client must be logged in before calling this method. + pub fn list_jobs( + &self, + namespace: &str, + project_name: &str, + ) -> Result { + self.transport + .get_json(format!("projects/{namespace}/{project_name}/jobs")) + } + + /// Get a job by its project-scoped number. + /// + /// The client must be logged in before calling this method. + pub fn get_job( + &self, + namespace: &str, + project_name: &str, + job_num: i32, + ) -> Result { + self.transport.get_json(format!( + "projects/{namespace}/{project_name}/jobs/{job_num}" + )) + } + + /// Read a page of a job's log file from byte offset `start`, or from the beginning. + /// + /// The client must be logged in before calling this method. + pub fn get_job_logs( + &self, + namespace: &str, + project_name: &str, + job_num: i32, + start: Option, + ) -> Result { + let mut url = self.transport.join(&format!( + "projects/{namespace}/{project_name}/jobs/{job_num}/logs" + )); + if let Some(start) = start { + url.query_pairs_mut() + .append_pair("start", &start.to_string()); + } + + self.transport.get_json(url) + } + + /// Cancel a new, queued or running job. + /// + /// The client must be logged in before calling this method. + pub fn cancel_job( + &self, + namespace: &str, + project_name: &str, + job_num: i32, + ) -> Result<(), ClientError> { + self.transport.put( + format!("projects/{namespace}/{project_name}/jobs/{job_num}/cancel"), + None::<()>, + ) } } diff --git a/tracel-client/src/console/job/response.rs b/tracel-client/src/console/job/response.rs new file mode 100644 index 0000000..4f2c211 --- /dev/null +++ b/tracel-client/src/console/job/response.rs @@ -0,0 +1,87 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct QueuedJobResponse { + pub job_id: String, + pub compute_provider_group_id: String, + /// The project-scoped job number, or `None` from a server that does not send it. + #[serde(default)] + pub job_num: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ProjectJobsResponse { + pub jobs: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ProjectJobResponse { + pub job_num: i32, + pub job_project_version: String, + pub job_command: String, + /// `new`, `queued`, `running`, `pending_cancellation`, `completed`, `failed` or `cancelled`. + pub status: String, + pub execution_context: Option, + pub created_at: String, + pub started_at: Option, + pub completed_at: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct JobResponse { + pub job_num: i32, + pub compute_provider_id: Option, + pub job_project: JobProjectResponse, + pub job_project_version: String, + pub job_command: String, + /// `new`, `queued`, `running`, `pending_cancellation`, `completed`, `failed` or `cancelled`. + pub status: String, + pub status_message: String, + pub execution_context: Option, + pub created_at: String, + pub started_at: Option, + pub completed_at: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct JobProjectResponse { + pub project_name: String, + pub namespace_name: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "type")] +pub enum ExecutionContextResponse { + Managed { + compute_provider_name: String, + memory_gb: i32, + cpu_cores: i32, + num_gpus: Option, + lock_price_per_hour: MoneyResponse, + final_cost: Option, + estimated_cost: Option, + }, + SelfManaged { + compute_provider_name: String, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MoneyResponse { + /// A decimal amount. + pub amount: String, + pub currency: String, +} + +/// One page of a job's log file. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct JobLogResponse { + pub logs: String, + /// Byte offset of the first byte in `logs`. + pub start: u64, + /// Byte offset after the last byte in `logs`; pass it as the next `start`. + pub end: u64, + /// Size of the log file in bytes. + pub total_size: u64, + pub has_more: bool, +} diff --git a/tracel-client/src/error.rs b/tracel-client/src/error.rs index 9f454f1..a72335f 100644 --- a/tracel-client/src/error.rs +++ b/tracel-client/src/error.rs @@ -23,6 +23,8 @@ pub enum ApiErrorCode { ModelVersionUploadIncomplete, MultipartUploadIncomplete, ModelVersionConflict, + ComputeProviderJob, + JobLog, Unauthenticated, CredentialNotAllowed, // ... diff --git a/tracel-client/src/transport.rs b/tracel-client/src/transport.rs index ff5e3c4..084f557 100644 --- a/tracel-client/src/transport.rs +++ b/tracel-client/src/transport.rs @@ -236,6 +236,13 @@ impl ApiTransport { Ok(serde_json::from_slice::(&bytes)?) } + pub fn put(&self, path: impl AsRef, body: Option) -> Result<(), ClientError> + where + T: serde::Serialize, + { + self.req(reqwest::Method::PUT, path, body).map(|_| ()) + } + pub fn delete(&self, path: impl AsRef) -> Result<(), ClientError> { self.req(reqwest::Method::DELETE, path, None::) .map(|_| ())