Skip to content

Commit 7770f10

Browse files
authored
Merge pull request #219 from tower/develop
v0.3.52 release
2 parents 48d1fb5 + 6d703fd commit 7770f10

File tree

14 files changed

+395
-17
lines changed

14 files changed

+395
-17
lines changed

Cargo.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ resolver = "2"
44

55
[workspace.package]
66
edition = "2021"
7-
version = "0.3.51"
7+
version = "0.3.52"
88
description = "Tower is the best way to host Python data apps in production"
99
rust-version = "1.81"
1010
authors = ["Brad Heller <brad@tower.dev>"]

crates/tower-cmd/src/api.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,3 +980,35 @@ impl ResponseEntity for tower_api::apis::default_api::DeleteScheduleSuccess {
980980
}
981981
}
982982
}
983+
984+
pub async fn cancel_run(
985+
config: &Config,
986+
name: &str,
987+
seq: i64,
988+
) -> Result<
989+
tower_api::models::CancelRunResponse,
990+
Error<tower_api::apis::default_api::CancelRunError>,
991+
> {
992+
let api_config = &config.into();
993+
994+
let params = tower_api::apis::default_api::CancelRunParams {
995+
name: name.to_string(),
996+
seq,
997+
};
998+
999+
unwrap_api_response(tower_api::apis::default_api::cancel_run(
1000+
api_config, params,
1001+
))
1002+
.await
1003+
}
1004+
1005+
impl ResponseEntity for tower_api::apis::default_api::CancelRunSuccess {
1006+
type Data = tower_api::models::CancelRunResponse;
1007+
1008+
fn extract_data(self) -> Option<Self::Data> {
1009+
match self {
1010+
Self::Status200(data) => Some(data),
1011+
Self::UnknownValue(_) => None,
1012+
}
1013+
}
1014+
}

crates/tower-cmd/src/apps.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,24 @@ pub fn apps_cmd() -> Command {
7676
)
7777
.about("Delete an app in Tower"),
7878
)
79+
.subcommand(
80+
Command::new("cancel")
81+
.arg(
82+
Arg::new("app_name")
83+
.value_parser(value_parser!(String))
84+
.index(1)
85+
.required(true)
86+
.help("Name of the app"),
87+
)
88+
.arg(
89+
Arg::new("run_number")
90+
.value_parser(value_parser!(i64))
91+
.index(2)
92+
.required(true)
93+
.help("Run number to cancel"),
94+
)
95+
.about("Cancel a running app run"),
96+
)
7997
}
8098

8199
pub async fn do_logs(config: Config, cmd: &ArgMatches) {
@@ -223,6 +241,26 @@ pub async fn do_delete(config: Config, cmd: &ArgMatches) {
223241
output::with_spinner("Deleting app", api::delete_app(&config, name)).await;
224242
}
225243

244+
pub async fn do_cancel(config: Config, cmd: &ArgMatches) {
245+
let name = cmd
246+
.get_one::<String>("app_name")
247+
.expect("app_name should be required");
248+
let seq = cmd
249+
.get_one::<i64>("run_number")
250+
.copied()
251+
.expect("run_number should be required");
252+
253+
let response =
254+
output::with_spinner("Cancelling run", api::cancel_run(&config, name, seq)).await;
255+
256+
let run = &response.run;
257+
let status = format!("{:?}", run.status);
258+
output::success_with_data(
259+
&format!("Run #{} for '{}' cancelled (status: {})", seq, name, status),
260+
Some(response),
261+
);
262+
}
263+
226264
async fn latest_run_number(config: &Config, name: &str) -> i64 {
227265
match api::describe_app(config, name).await {
228266
Ok(resp) => resp.runs
@@ -715,4 +753,30 @@ mod tests {
715753
assert!(should_emit_line(&mut last_line_num, 4));
716754
assert_eq!(last_line_num, Some(4));
717755
}
756+
757+
#[test]
758+
fn test_cancel_args_parsing() {
759+
let matches = apps_cmd()
760+
.try_get_matches_from(["apps", "cancel", "my-app", "42"])
761+
.unwrap();
762+
let (cmd, sub_matches) = matches.subcommand().unwrap();
763+
764+
assert_eq!(cmd, "cancel");
765+
assert_eq!(
766+
sub_matches
767+
.get_one::<String>("app_name")
768+
.map(|s| s.as_str()),
769+
Some("my-app")
770+
);
771+
assert_eq!(sub_matches.get_one::<i64>("run_number"), Some(&42));
772+
}
773+
774+
#[test]
775+
fn test_cancel_requires_both_args() {
776+
let result = apps_cmd().try_get_matches_from(["apps", "cancel", "my-app"]);
777+
assert!(result.is_err());
778+
779+
let result = apps_cmd().try_get_matches_from(["apps", "cancel"]);
780+
assert!(result.is_err());
781+
}
718782
}

crates/tower-cmd/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ impl App {
124124
Some(("show", args)) => apps::do_show(sessionized_config, args).await,
125125
Some(("logs", args)) => apps::do_logs(sessionized_config, args).await,
126126
Some(("delete", args)) => apps::do_delete(sessionized_config, args).await,
127+
Some(("cancel", args)) => apps::do_cancel(sessionized_config, args).await,
127128
_ => {
128129
apps::apps_cmd().print_help().unwrap();
129130
std::process::exit(2);

crates/tower-cmd/src/mcp.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ struct GenerateTowerfileRequest {
9898
script_path: Option<String>,
9999
}
100100

101+
#[derive(Debug, Deserialize, JsonSchema)]
102+
struct CancelRunRequest {
103+
name: String,
104+
run_number: String,
105+
}
106+
101107
#[derive(Debug, Deserialize, JsonSchema)]
102108
struct ScheduleRequest {
103109
app_name: String,
@@ -481,6 +487,28 @@ impl TowerService {
481487
}
482488
}
483489

490+
#[tool(description = "Cancel a running Tower app run")]
491+
async fn tower_apps_cancel(
492+
&self,
493+
Parameters(request): Parameters<CancelRunRequest>,
494+
) -> Result<CallToolResult, McpError> {
495+
let seq: i64 = request
496+
.run_number
497+
.parse()
498+
.map_err(|_| McpError::invalid_params("run_number must be a number", None))?;
499+
500+
match api::cancel_run(&self.config, &request.name, seq).await {
501+
Ok(response) => {
502+
let status = format!("{:?}", response.run.status);
503+
Self::text_success(format!(
504+
"Cancelled run #{} for '{}' (status: {})",
505+
seq, request.name, status
506+
))
507+
}
508+
Err(e) => Self::error_result("Failed to cancel run", e),
509+
}
510+
}
511+
484512
#[tool(description = "List secrets in your Tower account (shows only previews for security)")]
485513
async fn tower_secrets_list(
486514
&self,

crates/tower-cmd/src/output.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,6 @@ fn output_response_content_error<T>(err: ResponseContent<T>) {
298298

299299
match err.status {
300300
StatusCode::CONFLICT => {
301-
error("There was a conflict while trying to do that!");
302301
output_full_error_details(&error_model);
303302
}
304303
StatusCode::UNPROCESSABLE_ENTITY => {
@@ -316,7 +315,10 @@ fn output_response_content_error<T>(err: ResponseContent<T>) {
316315
);
317316
}
318317
_ => {
319-
error("The Tower API returned an error that the Tower CLI doesn't know what to do with! Maybe try again in a bit.");
318+
if error_model.detail.is_none() && error_model.errors.is_none() {
319+
error("The Tower API returned an error that the Tower CLI doesn't know what to do with! Maybe try again in a bit.");
320+
}
321+
output_full_error_details(&error_model);
320322
}
321323
}
322324
}

crates/tower-cmd/src/run.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ where
184184
let session = config.session.as_ref().ok_or(Error::NoSession)?;
185185

186186
env_vars.insert("TOWER_JWT".to_string(), session.token.jwt.to_string());
187+
env_vars.insert("TOWER__RUNTIME__IS_LOCAL".to_string(), "true".to_string());
187188

188189
// Load the Towerfile
189190
let towerfile_path = path.join("Towerfile");

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "tower"
7-
version = "0.3.51"
7+
version = "0.3.52"
88
description = "Tower CLI and runtime environment for Tower."
99
authors = [{ name = "Tower Computing Inc.", email = "brad@tower.dev" }]
1010
readme = "README.md"

src/tower/info/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,18 @@ def is_cloud_run() -> bool:
8383
Returns:
8484
bool: True if running in Tower cloud, otherwise False.
8585
"""
86-
val = _get_runtime_env_variable("IS_TOWER_MANAGED", "")
86+
val = _get_runtime_env_variable("IS_TOWER_MANAGED", "false")
87+
return _strtobool(val)
88+
89+
90+
def is_local() -> bool:
91+
"""
92+
Check if the current run is executing locally via the Tower CLI.
93+
94+
Returns:
95+
bool: True if running locally, otherwise False.
96+
"""
97+
val = _get_runtime_env_variable("IS_LOCAL", "false")
8798
return _strtobool(val)
8899

89100

0 commit comments

Comments
 (0)