From ab535ef8229088c5f623407e9f20402fcbda8b37 Mon Sep 17 00:00:00 2001 From: Michael Assaf Date: Wed, 15 Jul 2026 18:48:39 -0400 Subject: [PATCH] Add pydantic/logfire Phase 1 catalog integration. Provisional OUTPUT_FIELDS until discover; Wave 7 fixed-base family PR. --- .../src/providers/mod.rs | 6 +- .../src/providers/pydantic/logfire.rs | 145 ++++++++++++++++++ .../src/providers/pydantic/mod.rs | 11 ++ crates/stackless-integrations/src/registry.rs | 1 + 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 crates/stackless-integrations/src/providers/pydantic/logfire.rs create mode 100644 crates/stackless-integrations/src/providers/pydantic/mod.rs diff --git a/crates/stackless-integrations/src/providers/mod.rs b/crates/stackless-integrations/src/providers/mod.rs index 79f95b4..a7dde07 100644 --- a/crates/stackless-integrations/src/providers/mod.rs +++ b/crates/stackless-integrations/src/providers/mod.rs @@ -32,6 +32,7 @@ pub mod postalform; pub mod posthog; pub mod prisma; pub mod privy; +pub mod pydantic; pub mod railway; pub mod render_db; pub mod runloop; @@ -53,8 +54,8 @@ mod tests { agentmail, agentphone, algolia, amplitude, auth0, base44_projects, blaxel, browserbase, chroma, clickhouse, cloudflare, daytona, e2b, elevenlabs, exa, firecrawl, flyio, gitlab, heygen, huggingface, inngest, kernel, laravel_cloud, metronome, mixpanel, neon, openrouter, - parallel, planetscale, postalform, posthog, prisma, privy, railway, render_db, runloop, - sentry, supabase, supermemory, turso, upstash, wix, wordpress_com, workos, + parallel, planetscale, postalform, posthog, prisma, privy, pydantic, railway, render_db, + runloop, sentry, supabase, supermemory, turso, upstash, wix, wordpress_com, workos, }; fn assert_outputs_match() { @@ -117,6 +118,7 @@ mod tests { assert_outputs_match::(); assert_outputs_match::(); assert_outputs_match::(); + assert_outputs_match::(); assert_outputs_match::(); assert_outputs_match::(); assert_outputs_match::(); diff --git a/crates/stackless-integrations/src/providers/pydantic/logfire.rs b/crates/stackless-integrations/src/providers/pydantic/logfire.rs new file mode 100644 index 0000000..f198664 --- /dev/null +++ b/crates/stackless-integrations/src/providers/pydantic/logfire.rs @@ -0,0 +1,145 @@ +//! `pydantic/logfire` integration. + +use std::collections::BTreeMap; + +use serde::Serialize; +use stackless_stripe_projects::catalog::verify::CatalogService; +use stackless_stripe_projects::provision::ProvisionContext; + +use super::FamilyResource; +use crate::error::IntegrationError; +use crate::hostable::{ConfigScope, Hostable, IntegrationHosting}; + +pub const RESOURCE_KIND: &str = "integration-pydantic"; + +#[derive(Debug, Serialize)] +pub struct PydanticLogfireConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub organization: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub project_name: Option, +} + +impl CatalogService for PydanticLogfireConfig { + const REFERENCE: &'static str = "pydantic/logfire"; +} + +#[derive(Debug)] +pub struct PydanticLogfire; + +impl Hostable for PydanticLogfire { + const PROVIDER: &'static str = "pydantic"; + const HOSTING: IntegrationHosting = IntegrationHosting::Managed; + const CONFIG_SCOPE: ConfigScope = ConfigScope::GlobalOnly; + const RESOURCE_KIND: &'static str = RESOURCE_KIND; + const OUTPUTS: &'static [&'static str] = &["api_key"]; +} + +impl FamilyResource for PydanticLogfire { + type Config = PydanticLogfireConfig; + const PROVIDER_PREFIX: &'static str = "PYDANTIC"; + // Provisional until pinned by `mise run discover pydantic/logfire`. + const OUTPUT_FIELDS: &'static [(&'static str, &'static str, bool)] = + &[("API_KEY", "api_key", true)]; + + fn build_config(ctx: &ProvisionContext<'_>) -> Result { + let config = super::integration_config(ctx)?; + Ok(PydanticLogfireConfig { + organization: super::interp_optional(ctx, &config, "organization")?, + project_name: super::interp_optional(ctx, &config, "project_name")?, + }) + } +} + +pub fn validate_config( + name: &str, + config: &BTreeMap, +) -> Result<(), IntegrationError> { + let _ = (name, config); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ProviderOps; + use crate::resource::ResourcePayload; + use stackless_core::def::StackDef; + use stackless_stripe_projects::stripe::StripeProjects; + use stackless_stripe_projects::test_support; + + #[test] + fn config_matches_catalog() { + const FIXTURE: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../stackless-stripe-projects/tests/fixtures/catalog.json" + )); + let catalog = stackless_stripe_projects::Catalog::from_json_envelope(FIXTURE).unwrap(); + let failures = stackless_stripe_projects::verify_service( + &catalog, + &PydanticLogfireConfig { + organization: None, + project_name: None, + }, + ); + assert!( + failures.is_empty(), + "pydantic/logfire catalog gaps:\n{}", + failures.join("\n") + ); + } + + const CATALOG_ENVELOPE: &str = r##"{"ok":true,"command":"projects catalog","data":{"last_updated":"2026-07-11T00:00:00Z","services":[{"id":"prvsvc_logfire","object":"v2.provisioning.provider_service_detail","provider_id":"prvdr_pydantic","provider_name":"Pydantic","service_id":"logfire","categories":["database"],"kind":"deployable","scope":"project","availability":"available","development":false,"livemode":true,"pricing":{"type":"component"},"configuration_schema":{"properties":{"organization":{"type":"string"},"project_name":{"type":"string"}},"type":"object"}}]}}"##; + + fn test_def() -> StackDef { + StackDef::parse( + r#" +[stack] +name = "atto" +[stack.projects.stripe] +project = "project_1" +[integrations.res] +provider = "pydantic" +[services.api] +source = { repo = "r", ref = "main" } +env = { OUT = "${integrations.res.api_key}" } +health = { path = "/health" } +[services.api.local] +run = "true" +"#, + ) + .unwrap() + } + + #[tokio::test] + async fn provision_records_outputs() { + let runner = test_support::provision_script( + CATALOG_ENVELOPE, + serde_json::json!({"PYDANTIC_API_KEY": "val_api_key"}), + 0, + ); + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("stackless.toml"), + "[stack]\nname=\"atto\"\n", + ) + .unwrap(); + let stripe = StripeProjects::new(&runner, dir.path()); + + let resource = PydanticLogfire + .provision( + &stripe.as_dyn(), + &test_def(), + dir.path(), + "demo", + "res", + "local", + false, + ) + .await + .unwrap(); + assert_eq!(resource.resource_kind, "integration-pydantic"); + let payload: ResourcePayload = serde_json::from_str(&resource.payload).unwrap(); + assert_eq!(payload.outputs["api_key"], "val_api_key"); + } +} diff --git a/crates/stackless-integrations/src/providers/pydantic/mod.rs b/crates/stackless-integrations/src/providers/pydantic/mod.rs new file mode 100644 index 0000000..9f0d4be --- /dev/null +++ b/crates/stackless-integrations/src/providers/pydantic/mod.rs @@ -0,0 +1,11 @@ +//! Pydantic catalog resources via Stripe Projects. +//! +//! Output envelopes are provisional until pinned by `xtask discover`. + +pub mod logfire; + +#[allow(unused_imports)] +pub(crate) use crate::resource::{ + CatalogResource as FamilyResource, bool_optional, bool_required, int_optional, int_required, + integration_config, interp_optional, interp_required, +}; diff --git a/crates/stackless-integrations/src/registry.rs b/crates/stackless-integrations/src/registry.rs index 75faafd..f86f3c6 100644 --- a/crates/stackless-integrations/src/registry.rs +++ b/crates/stackless-integrations/src/registry.rs @@ -115,6 +115,7 @@ register_providers! { (posthog::analytics, PostHogAnalytics), (prisma::database, PrismaDatabase), (privy::app, PrivyApp), + (pydantic::logfire, PydanticLogfire), (railway::bucket, RailwayBucket), (railway::hosting, RailwayHosting), (railway::mongo, RailwayMongo),