diff --git a/Cargo.toml b/Cargo.toml index fb09335..d94b6dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,9 @@ biome_css_syntax = "0.5.8" biome_rowan = "0.5.8" schemars = "1.2.0" +# Temporary files +tempfile = "3.15" + [package] name = "apx" version = "0.2.3" @@ -152,6 +155,7 @@ rusqlite.workspace = true opentelemetry-proto.workspace = true prost.workspace = true similar.workspace = true +tempfile.workspace = true [dev-dependencies] -tempfile = "3.15" +tempfile.workspace = true \ No newline at end of file diff --git a/src/apx/__init__.py b/src/apx/__init__.py index 049c5b2..684079a 100644 --- a/src/apx/__init__.py +++ b/src/apx/__init__.py @@ -1,8 +1,6 @@ import sys -from apx._core import get_bun_binary_path, run_cli - -__version__ = "0.1.0" +from apx._core import get_bun_binary_path, run_cli, __version__ def main() -> None: diff --git a/src/apx/_core.pyi b/src/apx/_core.pyi index 9ed00b6..0747486 100644 --- a/src/apx/_core.pyi +++ b/src/apx/_core.pyi @@ -1,5 +1,7 @@ from pathlib import Path +__version__: str + def run_cli(args: list[str]) -> int: ... def get_bun_binary_path() -> Path: ... def generate_openapi(project_root: Path) -> bool: ... diff --git a/src/dev/process.rs b/src/dev/process.rs index 3419e5f..3b9cd1f 100644 --- a/src/dev/process.rs +++ b/src/dev/process.rs @@ -55,6 +55,30 @@ fn format_log_line(source: LogSource, message: &str) -> String { format!("{timestamp} | {source:>4} | {message}") } +/// Setup sitecustomize.py for Databricks SDK user-agent tracking. +/// This is non-critical telemetry - failures are silently ignored. +/// Returns the temp directory path if successful, None otherwise. +fn setup_sitecustomize() -> Option { + let temp_dir = tempfile::tempdir().ok()?; + let sitecustomize_path = temp_dir.path().join("sitecustomize.py"); + std::fs::write( + &sitecustomize_path, + r#"import os +try: + from databricks.sdk.core import with_user_agent_extra, with_product + from apx import __version__ + if os.getenv('APX_UVICORN') == '1': + with_user_agent_extra('apx', __version__) + with_product('apx', __version__) +except Exception: + pass +"#, + ) + .ok()?; + // Use into_path() to prevent automatic cleanup - dir persists for uvicorn's lifetime + Some(temp_dir.keep()) +} + #[derive(Debug)] pub struct ProcessManager { frontend_child: Arc>>, @@ -310,6 +334,9 @@ impl ProcessManager { resolve_log_config(&self.dev_config, &self.app_slug, app_dir).await?; let log_config = log_config_result.to_string_path(); + // Setup sitecustomize.py for user-agent tracking (non-critical, best-effort) + let sitecustomize_dir = setup_sitecustomize(); + // Run uvicorn via uv to ensure correct Python environment let mut cmd = UvCommand::new("uvicorn").tokio_command(); cmd.args([ @@ -336,9 +363,20 @@ impl ProcessManager { cmd.env("APX_DEV_SERVER_PORT", self.dev_server_port.to_string()); cmd.env("APX_DEV_SERVER_HOST", &self.host); cmd.env("APX_DEV_TOKEN", &self.dev_token); + cmd.env("APX_UVICORN", "1"); + // Force Python to flush stdout/stderr immediately (no buffering) cmd.env("PYTHONUNBUFFERED", "1"); + // Prepend sitecustomize dir to PYTHONPATH if setup succeeded (non-critical) + if let Some(sitecustomize_path) = sitecustomize_dir { + let pythonpath = match std::env::var("PYTHONPATH") { + Ok(existing) => format!("{}:{}", sitecustomize_path.display(), existing), + Err(_) => sitecustomize_path.display().to_string(), + }; + cmd.env("PYTHONPATH", pythonpath); + } + // Apply dotenv variables let vars = self.dotenv_vars.lock().await; for (key, value) in vars.iter() { diff --git a/src/lib.rs b/src/lib.rs index 62ba6c2..c298774 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -341,6 +341,7 @@ fn get_dotenv_vars() -> PyResult> { #[pymodule] fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { init_tracing(); + m.add("__version__", env!("CARGO_PKG_VERSION"))?; m.add_function(wrap_pyfunction!(run_cli, m)?)?; m.add_function(wrap_pyfunction!(get_bun_binary_path, m)?)?; m.add_function(wrap_pyfunction!(generate_openapi_py, m)?)?;