# PyWatt Module Creation Guide

This guide provides comprehensive instructions on how to create a new module using the PyWatt Rust SDK. Modules are independent, long-running services managed by the PyWatt orchestrator. They typically expose an HTTP API (using Axum) and interact with the orchestrator for configuration, secrets, and lifecycle management.

## Core Concepts

*   **Orchestrator Interaction:** Modules communicate with the orchestrator over stdin/stdout using JSON messages for initial handshake, secret fetching/rotation, and shutdown signals.
*   **HTTP Server:** Modules run an HTTP server (typically Axum) to handle requests, usually proxied by the orchestrator.
*   **State Management:** Modules manage their own state, including configuration received from the orchestrator and any runtime data.
*   **Secrets:** Modules securely fetch secrets from the orchestrator's secret management system.
*   **Announcement:** Modules announce their listening address and API endpoints to the orchestrator upon startup.
*   **Lifecycle:** Modules follow a defined lifecycle: Initialize -> Handshake -> Fetch Secrets -> Build State -> Announce -> Serve API -> Process IPC -> Shutdown.

## Development Approaches

There are two primary ways to structure your module:

1.  **Using the `#[pywatt::module]` Macro (Recommended)**: This approach significantly reduces boilerplate code by generating the main function and handling most of the bootstrap and serving logic. It's ideal for simpler modules or when rapid development is prioritized. Requires the `proc_macros` feature.
2.  **Manual Setup using `serve_module`**: This approach offers fine-grained control over the entire bootstrap process, state creation, and router setup. It's suitable for complex modules with specific initialization requirements or when the `proc_macros` feature is unavailable.

## Prerequisites

*   Rust toolchain installed (latest stable recommended).
*   Understanding of Rust basics, async/await, and the Axum web framework.
*   Add `pywatt_sdk` to your `Cargo.toml`.

### `Cargo.toml` Configuration

```toml
[package]
name = "my-pywatt-module"
version = "0.1.0"
edition = "2021"

[dependencies]
# Core SDK - adjust path as needed
pywatt_sdk = { path = "../pywatt_sdk", features = [] } # Start with minimal features

# Async runtime
tokio = { version = "1", features = ["full"] } # "full" enables all tokio features

# Web framework
axum = "0.7"

# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } # json for structured logs

# Secrets
secrecy = { version = "1", features = ["serde"] } # For SecretString

# Other common dependencies
# thiserror = "1.0" # For custom error types
# anyhow = "1.0"   # For simple error handling in apps

# Enable SDK features as needed in the pywatt_sdk dependency line:
# features = ["proc_macros", "router_ext", "metrics", "jwt_auth", "discover_endpoints", "builder"]

[build-dependencies]
# Required if using #[pywatt::module] or default health endpoint
pywatt_sdk = { path = "../pywatt_sdk" } # Adjust path
```

### SDK Features Explained

*   `proc_macros`: Enables the `#[pywatt::module]` attribute macro.
*   `router_ext`: Provides the `RouterExt` trait for easily adding standard endpoints (`/health`, `/metrics`) to an Axum Router.
*   `metrics`: Enables Prometheus metrics collection and the `/metrics` endpoint (via `RouterExt`). Requires a metrics backend like `prometheus` to be added as a dependency.
*   `jwt_auth`: Provides middleware (`JwtAuthLayer`) and extension traits (`RouterJwtExt`) for integrating JWT-based authentication verified against keys fetched from the orchestrator.
*   `discover_endpoints`: Enables automatic discovery of Axum routes for announcement. The SDK attempts to parse the router structure. May not work for all complex routing setups.
*   `builder`: Provides `ModuleBuilder` and `AppStateBuilder` for more programmatic or test-focused setup (less common for typical modules).

### Build Script (`build.rs`)

If you use the `#[pywatt::module]` macro or the `with_default_health()` method from `RouterExt`, you need a `build.rs` file in your project root to embed build information:

```rust
// build.rs
fn main() {
    // Emits environment variables like PYWATT_GIT_HASH, PYWATT_BUILD_TIME_UTC, etc.
    pywatt_sdk::build::emit_build_info();
}
```

Make sure you have `pywatt_sdk` listed under `[build-dependencies]` in `Cargo.toml`.

## Method 1: Using the `#[pywatt::module]` Macro

This is the streamlined approach.

**Core Concept:** Define an `async` function that accepts an `AppState<()>` and returns an `axum::Router`. Annotate this function with `#[pywatt::module(...)]`. The macro generates the necessary `main` function and handles the bootstrap, announcement, IPC, and server setup.

**Example `src/main.rs`:**

```rust
use pywatt_sdk::prelude::*; // Includes common types like Result, Error, AppState, SecretClient, etc.
use pywatt_sdk::AppState;
use axum::{
    routing::{get, post},
    Router,
    Json,
    Extension, // Used to access AppState in handlers
    http::StatusCode,
};
use serde::{Serialize, Deserialize};
use secrecy::{ExposeSecret, SecretString}; // Use SecretString for sensitive data
use std::sync::{Arc, Mutex};

// --- Data Structures ---
#[derive(Serialize)]
struct InfoResponse {
    module_id: String,
    orchestrator: String,
    message: String,
}

#[derive(Serialize, Deserialize)]
struct DataPayload {
    id: u32,
    value: String,
}

// --- Custom State Management (Example using Arc<Mutex>) ---
// The macro provides AppState<()>, so custom state needs separate handling.
#[derive(Clone, Default)] // Clone is required for Axum extensions
struct MyModuleState {
    call_count: Arc<Mutex<u64>>,
    config_value: Arc<Mutex<Option<String>>>,
}

// --- The Core Module Logic Function ---
#[pywatt::module(
    // Specify secrets to fetch on startup. Values are cached in the SecretClient.
    secrets = ["MODULE_API_KEY", "EXTERNAL_SERVICE_URL"],
    // Customize the health check path. Defaults to "/health".
    health = "/healthz",
    // Enable the /metrics endpoint (requires 'metrics' feature and backend).
    metrics = true,
    // Prefix all non-health routes (requires 'discover_endpoints' feature for auto-discovery).
    version = "v1"
)]
async fn create_module_router(sdk_state: AppState<()>) -> Router {
    // sdk_state contains: module_id, orchestrator_api, secret_client (user_state is unit `()`)
    tracing::info!("Initializing module '{}' connected to {}", sdk_state.module_id(), sdk_state.orchestrator_api());

    // Initialize custom state
    let module_state = MyModuleState::default();

    // Example: Fetch a secret value configured in the attribute
    // Note: get_secret retrieves from cache if pre-fetched, otherwise fetches from orchestrator.
    let client = sdk_state.secret_client(); // Get Arc<SecretClient>
    let state_clone = module_state.clone(); // Clone for the async task
    tokio::spawn(async move {
        match get_typed_secret::<String>(client, "EXTERNAL_SERVICE_URL").await {
            Ok(url_secret) => {
                tracing::info!("Fetched external service URL.");
                let mut config = state_clone.config_value.lock().unwrap();
                *config = Some(url_secret.expose_secret().clone()); // Store sensitive data appropriately
            }
            Err(e) => tracing::error!("Failed to get EXTERNAL_SERVICE_URL: {}", e),
        }
    });

    // Define Axum Router
    Router::new()
        .route("/info", get(info_handler))
        .route("/data", post(data_handler))
        // Share the SDK state AND custom state with handlers via Extensions
        .layer(Extension(sdk_state))
        .layer(Extension(module_state))
    // Add other layers (tracing, CORS, etc.) as needed
    // .layer(TraceLayer::new_for_http())
}

// --- Axum Handlers ---

// Access both SDK state and custom state via Extension
async fn info_handler(
    Extension(sdk_state): Extension<AppState<()>>,
    Extension(module_state): Extension<MyModuleState>,
) -> Json<InfoResponse> {
    let count = {
        let mut counter = module_state.call_count.lock().unwrap();
        *counter += 1;
        *counter
    };

    let config_val = module_state.config_value.lock().unwrap().clone().unwrap_or_else(|| "Not Set".to_string());

    Json(InfoResponse {
        module_id: sdk_state.module_id().to_string(),
        orchestrator: sdk_state.orchestrator_api().to_string(),
        message: format!("Call count: {}. Config: {}", count, config_val),
    })
}

// Access SDK state to use the secret client
async fn data_handler(
    Extension(sdk_state): Extension<AppState<()>>,
    Json(payload): Json<DataPayload>,
) -> Result<StatusCode, (StatusCode, String)> { // Return proper error responses
    tracing::info!(payload_id = payload.id, "Received data payload");

    // Example: Use a secret needed only for this handler
    let client = sdk_state.secret_client();
    match get_secret(client, "MODULE_API_KEY").await {
        Ok(_key) => {
            // Use the key (_key is SecretString, use .expose_secret() carefully)
            tracing::info!("Authenticated request using MODULE_API_KEY (placeholder)");
            // Process payload...
            Ok(StatusCode::OK)
        }
        Err(e) => {
            tracing::error!("Failed to get MODULE_API_KEY for request: {}", e);
            Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Secret fetch error: {}", e)))
        }
    }
}

// No need to write `fn main()` - the macro generates it.
```

**Macro Attributes Details:**

*   `secrets = [...]`: Array of secret keys (string literals) to prefetch during startup. Essential secrets should be listed here.
*   `health = "/path"`: Sets the health check endpoint path (default: `/health`). Returns build info (requires `build.rs`).
*   `metrics = true | false`: Enables `/metrics` endpoint (default: `false`). Requires `metrics` feature and a compatible metrics crate (e.g., `prometheus`).
*   `version = "vX"`: If `discover_endpoints` feature is enabled, prefixes discovered routes (except health) with `/vX`. If `discover_endpoints` is *not* enabled, this attribute has no effect on routing but might be useful for manual announcement logic if needed.

**Generated `main` Function Logic:**

The `#[pywatt::module]` macro generates an `async fn main() -> pywatt_sdk::Result<()>` that performs roughly these steps:
1.  `pywatt_sdk::init_module()`: Initializes logging based on orchestrator settings.
2.  `pywatt_sdk::read_init()`: Reads `OrchestratorInit` message from stdin.
3.  `pywatt_sdk::get_module_secret_client()`: Creates the shared `SecretClient`.
4.  `pywatt_sdk::get_secrets()`: Fetches secrets listed in the `secrets` attribute.
5.  Creates `AppState<()>`.
6.  Calls your annotated function (`create_module_router`) with `AppState<()>` to get the `axum::Router`.
7.  Applies necessary layers (like `Extension(AppState<()>)`).
8.  Adds health endpoint (using build info if `build.rs` is present).
9.  Optionally adds metrics endpoint.
10. Performs endpoint discovery (if `discover_endpoints` enabled) or prepares for manual announcement.
11. Applies version prefix (if `version` specified and discovery enabled).
12. `pywatt_sdk::send_announce()`: Sends `ModuleAnnounce` message to stdout.
13. Spawns `pywatt_sdk::process_ipc_messages()` in a background task to handle stdin messages (rotations, shutdown).
14. Binds and serves the Axum router based on the listening address provided in `OrchestratorInit`.

## Method 2: Manual Setup using `serve_module`

This method provides maximum control over the initialization and serving process.

**Core Concept:** Implement `fn main` yourself. Define a custom state struct `T`. Provide two closures to `pywatt_sdk::serve_module`: one to build your state `T` from orchestrator info and secrets, and another to build the Axum router using `AppState<T>`.

**Example `src/main.rs`:**

```rust
use pywatt_sdk::prelude::*;
use pywatt_sdk::{serve_module, AppState, OrchestratorInit, AnnouncedEndpoint, ModuleAnnounce};
use axum::{
    routing::{get, post},
    Router,
    Json,
    Extension,
    http::StatusCode,
};
use serde::{Serialize, Deserialize};
use secrecy::{ExposeSecret, SecretString};
use std::sync::{Arc, Mutex};

// --- Custom State Structure ---
#[derive(Clone, Debug)] // Clone is essential for AppState
struct MyManagedState {
    db_pool: Option<String>, // Example: Placeholder for a DB pool or config
    api_key: Option<SecretString>, // Store sensitive data securely
    module_id: String,
    call_count: Arc<Mutex<u64>>,
}

// --- Data Structures ---
#[derive(Serialize)]
struct InfoResponse {
    module_id: String,
    message: String,
    db_status: String,
}

#[derive(Serialize, Deserialize)]
struct DataPayload {
    id: u32,
    value: String,
}

// --- Main Function ---
#[tokio::main]
async fn main() -> Result<()> { // Return PyWatt SDK's Result
    // Initialize logging *early*.
    // init_module() is called inside serve_module, but you might want earlier logging.
    // Consider using tracing_subscriber directly here if needed before serve_module runs.
    // tracing_subscriber::fmt::init(); // Basic setup

    // 1. Define secret keys required at startup
    let initial_secret_keys = vec![
        "DATABASE_URL".to_string(),
        "MODULE_API_KEY".to_string(),
    ];

    // 2. Define endpoints for manual announcement (Optional if 'discover_endpoints' is used)
    // If 'discover_endpoints' feature is enabled on the SDK, serve_module will attempt
    // to discover routes from the built router, and you can pass an empty Vec here.
    // Otherwise, you MUST define the endpoints manually.
    let announced_endpoints = vec![
        AnnouncedEndpoint { path: "/info".to_string(), methods: vec!["GET".to_string()], auth: None },
        AnnouncedEndpoint { path: "/data".to_string(), methods: vec!["POST".to_string()], auth: None },
        // Add health/metrics endpoints here if using manual announcement AND RouterExt
        // AnnouncedEndpoint { path: "/health".to_string(), methods: vec!["GET".to_string()], auth: None },
    ];

    // 3. Define the State Builder Closure
    // Takes orchestrator info and PRE-FETCHED secrets, returns your custom state `T`.
    let state_builder = |init: &OrchestratorInit, secrets: Vec<SecretString>| -> MyManagedState {
        tracing::info!("Building state for module '{}' from orchestrator '{}'", init.module_id, init.orchestrator_api);

        // Robustly map secrets to state fields based on the order in `initial_secret_keys`.
        // Handle cases where secrets might be missing or fetching failed (secrets vec might be shorter).
        let db_url = secrets.get(0).map(|s| s.expose_secret().clone());
        let api_key = secrets.get(1).cloned(); // Clone the SecretString

        if db_url.is_none() {
            tracing::warn!("DATABASE_URL secret not found during initial fetch.");
        }
        if api_key.is_none() {
            tracing::warn!("MODULE_API_KEY secret not found during initial fetch.");
        }

        MyManagedState {
            db_pool: db_url, // Store the exposed string or actual pool
            api_key,        // Store the SecretString
            module_id: init.module_id.clone(),
            call_count: Arc::new(Mutex::new(0)),
        }
    };

    // 4. Define the Router Builder Closure
    // Takes the fully constructed AppState<MyManagedState>, returns the Axum Router.
    let router_builder = |app_state: AppState<MyManagedState>| -> Router {
        tracing::info!("Building router for module '{}'", app_state.module_id());

        // Example: Access the secret client or user state during router setup if needed
        let _secret_client = app_state.secret_client();
        let _initial_db_config = app_state.user_state.db_pool.clone();

        // Build the Axum router
        let mut router = Router::new()
            .route("/info", get(info_handler))
            .route("/data", post(data_handler))
            // Share the full AppState (including SDK parts and your MyManagedState)
            .layer(Extension(app_state));

        // Optionally add standard endpoints using RouterExt (requires 'router_ext' feature)
        // router = router.with_default_health(); // Requires build.rs
        // #[cfg(feature = "metrics")]
        // { router = router.with_prometheus_metrics(); } // Requires 'metrics' feature

        router
    };

    // 5. Run the PyWatt Server
    // This function handles the main lifecycle: init, handshake, secret fetch,
    // calling your builders, announcement, IPC loop, and serving.
    serve_module(
        initial_secret_keys,
        announced_endpoints, // Pass manual endpoints or empty vec() if using discovery
        state_builder,
        router_builder,
    )
    .await?; // Propagate errors

    // serve_module runs indefinitely until shutdown or error
    tracing::info!("Module shutdown gracefully.");
    Ok(())
}

// --- Axum Handlers ---

// Access the full AppState<MyManagedState> via Extension
async fn info_handler(Extension(state): Extension<AppState<MyManagedState>>) -> Json<InfoResponse> {
    let count = {
        let mut counter = state.user_state.call_count.lock().unwrap();
        *counter += 1;
        *counter
    };
    let db_status = state.user_state.db_pool.as_deref().unwrap_or("Disconnected").to_string();

    Json(InfoResponse {
        module_id: state.module_id().to_string(),
        message: format!("Call count: {}", count),
        db_status,
    })
}

// Example using secrets from the custom state
async fn data_handler(
    Extension(state): Extension<AppState<MyManagedState>>,
    Json(payload): Json<DataPayload>,
) -> Result<StatusCode, (StatusCode, String)> {
    tracing::info!(payload_id = payload.id, "Received data payload for module {}", state.module_id());

    // Access the API key stored in the custom state
    if let Some(key) = &state.user_state.api_key {
        // Use the key - remember to expose_secret() only when necessary
        tracing::info!("Authenticated request using MODULE_API_KEY from state (placeholder): {}", key.expose_secret());
        // Process payload...
        Ok(StatusCode::OK)
    } else {
        tracing::error!("MODULE_API_KEY not available in state for request.");
        Err((StatusCode::UNAUTHORIZED, "Missing API Key configuration".to_string()))
    }
}

```

**Manual Setup Key Points:**

*   You write the `#[tokio::main] async fn main() -> Result<()>` function.
*   You explicitly define the list of initial `secret_keys`.
*   You define `announced_endpoints` manually OR rely on the `discover_endpoints` feature (pass `vec![]` to `serve_module` in that case).
*   The `state_builder` closure receives `&OrchestratorInit` and `Vec<SecretString>` (corresponding to `initial_secret_keys`) and must return your custom state struct `T`.
*   The `router_builder` closure receives the fully formed `AppState<T>` (containing SDK info *and* your custom `T` in the `user_state` field) and must return the `axum::Router`.
*   `serve_module` orchestrates the PyWatt lifecycle steps internally, using your provided closures.

## Common Concepts Deep Dive

### `AppState<T>`

*   **Purpose:** Holds the shared state accessible throughout your module, especially within Axum handlers.
*   **Contents:**
    *   `module_id()`: The unique ID assigned by the orchestrator (String).
    *   `orchestrator_api()`: The base URL for orchestrator APIs (String).
    *   `secret_client()`: An `Arc<SecretClient>` for interacting with the secret store.
    *   `user_state`: Your custom state struct (`T`). In the macro approach, `T` is `()`. In the manual approach, `T` is the type returned by your `state_builder`.
*   **Sharing:** Typically shared with Axum handlers using `.layer(Extension(app_state))`. Handlers then access it via the `Extension<AppState<T>>` extractor.
*   **Cloning:** `AppState<T>` is `Clone` if `T` is `Clone`. This is important for Axum extensions.

### Secret Management (`SecretClient`)

*   **Access:** Get the `Arc<SecretClient>` via `app_state.secret_client()` or `SecretClient::global()` (if initialized).
*   **Fetching:**
    *   `get_secret(client, "KEY") -> Result<SecretString>`: Fetches a secret as a `SecretString`. Returns cached value if available (e.g., from initial fetch), otherwise fetches from the orchestrator.
    *   `get_secrets(client, &["KEY1", "KEY2"]) -> Result<Vec<SecretString>>`: Fetches multiple secrets efficiently.
    *   `get_typed_secret<T: DeserializeOwned>(client, "KEY") -> Result<Secret<T>>`: Fetches a secret and deserializes its JSON value into type `T`. Returns `Secret<T>`, use `.expose_secret()` to get `&T`.
*   **Rotation:**
    *   `subscribe_secret_rotations(client, "KEY", callback) -> Result<()>`: Registers an `async` callback function or closure to be executed when the orchestrator notifies the module that the secret `KEY` has been rotated. The callback receives the `SecretClient` and the key name.
    *   The background IPC loop (`process_ipc_messages`) listens for rotation notifications and triggers registered callbacks.
*   **Errors:** Functions return `Result<_, ModuleSecretError>`.
*   **`SecretString` / `Secret<T>`:** These types from the `secrecy` crate prevent accidental exposure of secret values (e.g., in logs). Use `.expose_secret()` deliberately when you need the raw value.

### Logging (`tracing`)

*   The SDK uses the `tracing` crate.
*   `pywatt_sdk::init_module()` (called automatically by the macro or `serve_module`) sets up a `tracing_subscriber` that:
    *   Writes logs to stderr in JSON format (expected by the orchestrator).
    *   Respects the log level configured by the orchestrator via the initial handshake (`OrchestratorInit`).
*   Use standard `tracing::info!`, `tracing::warn!`, `tracing::error!`, etc. macros in your code.

### Endpoint Announcement

*   **Purpose:** Tells the orchestrator which HTTP routes your module serves, so it can proxy requests correctly.
*   **Mechanism:** Sending a `ModuleAnnounce` JSON message to stdout after initialization.
*   **`ModuleAnnounce` struct:** Contains `listen` (String representation of the address) and `endpoints` (`Vec<AnnouncedEndpoint>`).
*   **`AnnouncedEndpoint` struct:** Contains `path` (String), `methods` (`Vec<String>`, e.g., `["GET", "POST"]`), and `auth` (`Option<String>`, specifying auth requirements if any).
*   **Automatic Discovery (`discover_endpoints` feature):**
    *   When enabled, the SDK attempts to inspect the `axum::Router` structure to generate the `Vec<AnnouncedEndpoint>` automatically.
    *   Works for common routing patterns but might fail for highly dynamic or complex routers.
    *   Used by `#[pywatt::module]` if the feature is enabled.
    *   Used by `serve_module` if the feature is enabled and you pass `vec![]` as the endpoints argument.
*   **Manual Announcement:**
    *   Define the `Vec<AnnouncedEndpoint>` explicitly.
    *   Pass this vector to `serve_module`.
    *   Required if `discover_endpoints` feature is *not* enabled or if discovery fails for your router setup.
    *   The `#[pywatt::module]` macro *requires* `discover_endpoints` for announcement (it doesn't support manual endpoint lists).

### Error Handling

*   SDK functions typically return `pywatt_sdk::Result<T>`, which is `Result<T, pywatt_sdk::Error>`.
*   `pywatt_sdk::Error` is an enum covering various potential issues:
    *   `Init`: Handshake errors.
    *   `Secret`: Errors from `SecretClient`.
    *   `Announce`: Errors sending the announcement.
    *   `Config`: Configuration-related errors (e.g., missing env vars if applicable).
    *   `Io`: Stdin/stdout I/O errors.
    *   `Server`: Errors from the underlying Hyper/Axum server.
    *   `Json`: Serialization/deserialization errors.
    *   `Other`: Generic error category.
*   Your `main` function should ideally return `pywatt_sdk::Result<()>`.
*   Axum handlers should return appropriate HTTP error responses (e.g., `Result<Json<T>, (StatusCode, String)>`).

### Module Lifecycle Summary

1.  **Start:** Orchestrator launches the module process.
2.  **Logging Init:** `init_module()` sets up logging.
3.  **Handshake:** `read_init()` reads `OrchestratorInit` from stdin (contains module ID, API URL, listen address, log level).
4.  **Secret Client:** `get_module_secret_client()` creates the client.
5.  **Initial Secret Fetch:** `get_secrets()` (or individual `get_secret`) fetches secrets specified at startup.
6.  **State Building:** Your `state_builder` (manual) or logic within the macro-annotated function runs to create/initialize state.
7.  **Router Building:** Your `router_builder` (manual) or the macro-annotated function returns the `axum::Router`.
8.  **Announcement:** `send_announce()` sends `ModuleAnnounce` to stdout.
9.  **IPC Loop:** `process_ipc_messages()` starts listening on stdin in the background for `OrchestratorToModule` messages (e.g., `Secret`, `Rotated`, `Shutdown`).
10. **Serve:** Axum server starts listening on the address specified by the orchestrator.
11. **Run:** Handles HTTP requests and processes IPC messages.
12. **Shutdown:** Orchestrator sends `Shutdown` message via IPC, `process_ipc_messages` loop exits, `serve_module` returns, `main` exits.

## Example Project Structure

```
my-pywatt-module/
├── Cargo.toml
├── build.rs         # Optional: Needed for build info (macro/health endpoint)
└── src/
    └── main.rs      # Your module entry point and logic
    # └── handlers.rs  # Optional: Separate file for Axum handlers
    # └── state.rs     # Optional: Separate file for state structures
    # └── errors.rs    # Optional: Separate file for custom error types
```

This guide provides a detailed foundation. Always refer to the specific function and type documentation within the `pywatt_sdk` source or generated docs (using `cargo doc`) for the most precise information. 