From 28528674765e2337be6c50f0ae2efbd88ecab54c Mon Sep 17 00:00:00 2001 From: David Calavera <1050+calavera@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:21:49 -0800 Subject: [PATCH 1/3] Add UTC timezone to dates fetched from the database. When we store the created_at objects in the database, they're stored as timestamp automatically. Unfortunately, they're restored without any timezone information, making them invalid strings to parse as RCF3339. I'm creating a custom type that can handle those cases when we read the data. Preserving the format when we serialize it to other clients. The dates are always UTC, so we don't need to worry about other timezone offsets except +00:00, or Z as a shortcut. --- crates/cloud-sdk/src/applications/models.rs | 173 +++++++++++++++++--- 1 file changed, 151 insertions(+), 22 deletions(-) diff --git a/crates/cloud-sdk/src/applications/models.rs b/crates/cloud-sdk/src/applications/models.rs index 29d5f53..e765c7d 100644 --- a/crates/cloud-sdk/src/applications/models.rs +++ b/crates/cloud-sdk/src/applications/models.rs @@ -2,13 +2,43 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use futures::Stream; use reqwest::header::HeaderValue; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json; use std::{collections::HashMap, pin::Pin}; use uuid::Uuid; use crate::error::SdkError; +/// A custom DateTime type that handles RFC3339 timestamps with missing 'Z' timezone indicator. +/// When deserializing, if the timestamp doesn't end with 'Z', it's automatically appended. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[serde(transparent)] +pub struct Rfc3339DateTime(DateTime); + +impl<'de> Deserialize<'de> for Rfc3339DateTime { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut s = String::deserialize(deserializer)?; + if !s.ends_with("Z") && !s.ends_with("+00:00") { + s.push('Z'); + } + + DateTime::parse_from_rfc3339(&s) + .map(|dt| Rfc3339DateTime(dt.with_timezone(&Utc))) + .map_err(serde::de::Error::custom) + } +} + +impl std::ops::Deref for Rfc3339DateTime { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, Builder)] pub struct ApplicationManifest { #[builder(setter(into))] @@ -644,7 +674,7 @@ pub struct RequestProgressUpdated { #[serde(default)] pub attributes: Option, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for RequestProgressUpdated { @@ -665,11 +695,11 @@ impl RequestEventMetadata for RequestProgressUpdated { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -682,7 +712,7 @@ pub struct RequestFinishedEvent { #[serde(default)] pub outcome: RequestOutcome, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for RequestFinishedEvent { @@ -703,11 +733,11 @@ impl RequestEventMetadata for RequestFinishedEvent { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -718,7 +748,7 @@ pub struct RequestStartedEvent { pub application_version: String, pub request_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for RequestStartedEvent { @@ -739,11 +769,11 @@ impl RequestEventMetadata for RequestStartedEvent { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -756,7 +786,7 @@ pub struct FunctionRunCreated { pub function_name: String, pub function_run_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunCreated { @@ -777,11 +807,11 @@ impl RequestEventMetadata for FunctionRunCreated { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -796,7 +826,7 @@ pub struct FunctionRunAssigned { pub allocation_id: String, pub executor_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunAssigned { @@ -817,11 +847,11 @@ impl RequestEventMetadata for FunctionRunAssigned { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -844,7 +874,7 @@ pub struct FunctionRunCompleted { pub allocation_id: String, pub outcome: FunctionRunOutcomeSummary, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunCompleted { @@ -865,11 +895,11 @@ impl RequestEventMetadata for FunctionRunCompleted { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -882,7 +912,7 @@ pub struct FunctionRunMatchedCache { pub function_name: String, pub function_run_id: String, #[serde(default)] - pub created_at: Option>, + pub created_at: Option, } impl RequestEventMetadata for FunctionRunMatchedCache { @@ -903,11 +933,11 @@ impl RequestEventMetadata for FunctionRunMatchedCache { } fn created_at(&self) -> Option<&DateTime> { - self.created_at.as_ref() + self.created_at.as_ref().map(|rfc| &rfc.0) } fn set_created_at(&mut self, date: DateTime) { - self.created_at = Some(date); + self.created_at = Some(Rfc3339DateTime(date)); } } @@ -1227,3 +1257,102 @@ pub struct ProgressUpdatesJson { pub updates: Vec, pub next_token: Option, } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Datelike; + use serde_json::json; + + #[test] + fn test_rfc3339_datetime_with_z() { + let json = json!("2024-01-15T10:30:45Z"); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + } + + #[test] + fn test_rfc3339_datetime_without_z() { + let json = json!("2024-01-15T10:30:45"); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + let dt = result.unwrap(); + // Verify it was parsed correctly as UTC + assert_eq!(dt.0.year(), 2024); + assert_eq!(dt.0.month(), 1); + assert_eq!(dt.0.day(), 15); + } + + #[test] + fn test_rfc3339_datetime_with_timezone_offset() { + let json = json!("2024-01-15T10:30:45+00:00"); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + } + + #[test] + fn test_request_started_event_deserialization() { + let json = json!({ + "namespace": "test", + "application_name": "app", + "application_version": "1.0", + "request_id": "req-123", + "created_at": "2024-01-15T10:30:45" + }); + let result: Result = serde_json::from_value(json); + assert!(result.is_ok()); + let event = result.unwrap(); + assert!(event.created_at.is_some()); + } + + #[test] + fn test_rfc3339_datetime_serialization() { + // Test that serializing Rfc3339DateTime produces a plain string, not a nested struct + let now = chrono::Utc::now(); + let rfc_dt = Rfc3339DateTime(now); + let serialized = serde_json::to_value(&rfc_dt).unwrap(); + + // Should be a string, not an object + assert!( + serialized.is_string(), + "Expected serialized DateTime to be a string, got: {:?}", + serialized + ); + + // Should contain 'Z' at the end + let date_str = serialized.as_str().unwrap(); + assert!( + date_str.ends_with('Z'), + "Expected 'Z' at end of serialized DateTime" + ); + } + + #[test] + fn test_request_started_event_serialization() { + // Test that serializing an event doesn't nest the created_at field + let event = RequestStartedEvent { + namespace: "test".to_string(), + application_name: "app".to_string(), + application_version: "1.0".to_string(), + request_id: "req-123".to_string(), + created_at: Some(Rfc3339DateTime(Utc::now())), + }; + + let serialized = serde_json::to_value(&event).unwrap(); + let obj = serialized.as_object().unwrap(); + + // created_at should be a string directly, not an object + let created_at = &obj["created_at"]; + assert!( + created_at.is_string(), + "Expected created_at to be a string, got: {:?}", + created_at + ); + + let date_str = created_at.as_str().unwrap(); + assert!( + date_str.ends_with('Z'), + "Expected 'Z' at end of created_at value" + ); + } +} From bad42034333ded19b050f2a59049f2db963fb77b Mon Sep 17 00:00:00 2001 From: David Calavera <1050+calavera@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:43:55 -0800 Subject: [PATCH 2/3] Add functions to convert and display dates. --- crates/cloud-sdk/src/applications/models.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/cloud-sdk/src/applications/models.rs b/crates/cloud-sdk/src/applications/models.rs index e765c7d..79eb479 100644 --- a/crates/cloud-sdk/src/applications/models.rs +++ b/crates/cloud-sdk/src/applications/models.rs @@ -4,7 +4,7 @@ use futures::Stream; use reqwest::header::HeaderValue; use serde::{Deserialize, Deserializer, Serialize}; use serde_json; -use std::{collections::HashMap, pin::Pin}; +use std::{collections::HashMap, fmt::Display, pin::Pin}; use uuid::Uuid; use crate::error::SdkError; @@ -15,6 +15,24 @@ use crate::error::SdkError; #[serde(transparent)] pub struct Rfc3339DateTime(DateTime); +impl Rfc3339DateTime { + pub fn now() -> Self { + Self(Utc::now()) + } +} + +impl From> for Rfc3339DateTime { + fn from(value: DateTime) -> Self { + Self(value) + } +} + +impl Display for Rfc3339DateTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.to_rfc3339()) + } +} + impl<'de> Deserialize<'de> for Rfc3339DateTime { fn deserialize(deserializer: D) -> Result where From e2866838a61874c479c3499faf84ebae11936102 Mon Sep 17 00:00:00 2001 From: David Calavera <1050+calavera@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:14:45 -0800 Subject: [PATCH 3/3] Use semver to install the sdk in integration tests. --- crates/cloud-sdk/src/images/mod.rs | 4 ++-- crates/cloud-sdk/src/images/models.rs | 11 ++++++++++- crates/cloud-sdk/tests/common.rs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/cloud-sdk/src/images/mod.rs b/crates/cloud-sdk/src/images/mod.rs index 4aedb75..a53354a 100644 --- a/crates/cloud-sdk/src/images/mod.rs +++ b/crates/cloud-sdk/src/images/mod.rs @@ -24,7 +24,7 @@ //! .application_name("my-app") //! .application_version("1.0.0") //! .function_name("main") -//! .sdk_version("0.3.12") +//! .sdk_version("0.2") //! .build().unwrap(); //! //! images_client.build_image(build_request); @@ -113,7 +113,7 @@ impl ImagesClient { /// .application_name("my-app") /// .application_version("1.0.0") /// .function_name("main") - /// .sdk_version("0.2.75") + /// .sdk_version("0.2") /// .build()?; /// /// images_client.build_image(request).await?; diff --git a/crates/cloud-sdk/src/images/models.rs b/crates/cloud-sdk/src/images/models.rs index 5de26ea..cc054aa 100644 --- a/crates/cloud-sdk/src/images/models.rs +++ b/crates/cloud-sdk/src/images/models.rs @@ -321,7 +321,16 @@ impl Image { lines.push(render_build_operation(op)); } - lines.push(format!("RUN pip install tensorlake=={}", sdk_version)); + if sdk_version.starts_with("~=") + || sdk_version.starts_with(">=") + || sdk_version.starts_with("<=") + || sdk_version.starts_with("!=") + || sdk_version.starts_with("==") + { + lines.push(format!("RUN pip install tensorlake{}", sdk_version)); + } else { + lines.push(format!("RUN pip install tensorlake=={}", sdk_version)); + } lines.join("\n") } diff --git a/crates/cloud-sdk/tests/common.rs b/crates/cloud-sdk/tests/common.rs index 4d30b86..e5c7a98 100644 --- a/crates/cloud-sdk/tests/common.rs +++ b/crates/cloud-sdk/tests/common.rs @@ -39,7 +39,7 @@ pub async fn build_test_image( .application_name(application_name) .application_version(application_version) .function_name(func_name) - .sdk_version("0.2.75") + .sdk_version("~=0.2") .build() .unwrap();