Python Nodes
Providers
Shared runtime services for Python nodes and hooks.
Providers are shared runtime services that SQLBuild discovers, configures, and injects into your Python nodes and hooks. Use them for external connections, API clients, or any stateful service that multiple nodes need access to.
Defining a provider
Section titled “Defining a provider”Create a Python file under providers/ in your project. A provider is a class that subclasses Provider from sqlbuild.providers:
from sqlbuild.providers import Provider
class WarehouseClient(Provider): api_key: str endpoint: str = "https://api.example.com"
def setup(self, ctx): self.session = create_session(self.api_key, self.endpoint)
def teardown(self): self.session.close()Provider extends pydantic-settings BaseSettings, so provider fields are validated and can be populated from environment variables automatically.
Provider name
Section titled “Provider name”Each provider has a runtime name used for injection. By default, the name is derived from the class name by converting to lower_snake_case:
WarehouseClientbecomeswarehouse_clientSlackNotifierbecomesslack_notifier
Override the name explicitly with provider_name:
class WarehouseClient(Provider): provider_name = "warehouse" api_key: strProvider names must be valid Python identifiers (lower_snake_case). They share the global project resource namespace with models, sources, seeds, functions, loaders, tasks, assets, checks, and hooks.
Using providers in Python nodes
Section titled “Using providers in Python nodes”Providers are injected into Python node functions by parameter name. Add a parameter whose name matches the provider’s runtime name:
from sqlbuild.tasks import task
@taskdef export_orders(ctx, warehouse_client): warehouse_client.session.upload(ctx.query("SELECT * FROM orders"))SQLBuild matches the parameter name warehouse_client to the discovered provider with that name, sets it up if it hasn’t been already, and passes it to the function.
You can also type-annotate the parameter for IDE support and compile-time validation:
from sqlbuild.tasks import taskfrom providers.warehouse_client import WarehouseClient
@taskdef export_orders(ctx, warehouse_client: WarehouseClient): warehouse_client.session.upload(ctx.query("SELECT * FROM orders"))When a type annotation is present, SQLBuild validates that the discovered provider is an instance of the annotated class. A mismatch raises a compile-time error.
Provider injection works in all Python node types:
- Loaders (
@loader) - Tasks (
@task) - Assets (
@asset) - Checks (
@check)
Using providers in hooks
Section titled “Using providers in hooks”Python lifecycle hooks also support provider injection by parameter name:
from sqlbuild.hooks import hook
@hookdef notify_complete(ctx, slack_notifier): slack_notifier.send(f"Model {ctx.model_name} built successfully")MODEL ( materialized table, post_hooks [python("notify_complete")],);Providers are also available on the HookContext via ctx.providers:
@hookdef notify_complete(ctx): notifier = ctx.providers.slack_notifier notifier.send(f"Model {ctx.model_name} built successfully")Using providers via context
Section titled “Using providers via context”All Python node contexts (TaskContext, AssetContext, CheckContext, LoaderContext) and HookContext expose a ctx.providers container for name-based access:
@taskdef export_orders(ctx): client = ctx.providers.warehouse_client client.session.upload(ctx.query("SELECT * FROM orders"))Both approaches (parameter injection and ctx.providers) are equivalent. Parameter injection is more explicit and enables type checking; ctx.providers is useful when provider access is conditional or dynamic.
Lifecycle
Section titled “Lifecycle”Providers follow a lazy setup, reverse-teardown lifecycle scoped to the command invocation:
- Discovery - on compile, SQLBuild discovers all
Providersubclasses underproviders/and validates their settings (from environment variables or field defaults). - Lazy setup -
setup(ctx)is called the first time a provider is accessed during a build, not at startup. Providers that are never used are never set up. - Teardown - after the command completes,
teardown()is called on all providers that were set up, in reverse setup order. Teardown runs even if the build failed.
class WarehouseClient(Provider): api_key: str
def setup(self, ctx): # Called once, the first time any node accesses this provider self.connection = connect(self.api_key)
def teardown(self): # Called after the command completes self.connection.close()Both setup and teardown are optional. A provider with only field declarations and no lifecycle methods is valid; it acts as a validated configuration object.
Configuration from environment variables
Section titled “Configuration from environment variables”Because Provider extends pydantic-settings BaseSettings, fields without defaults are read from environment variables. The environment variable name matches the field name in uppercase:
class SlackNotifier(Provider): slack_token: str # reads SLACK_TOKEN from environment channel: str = "#builds" # has a default, environment variable is optionalSee the pydantic-settings documentation for advanced configuration like custom env prefixes, .env file support, and nested settings.
Discovery rules
Section titled “Discovery rules”- Provider classes are discovered from
.pyfiles underproviders/recursively - Files named
__init__.pyor starting with_are skipped - Each concrete (non-abstract) subclass of
Provideris registered - Provider names must be globally unique across project resources
- Settings are validated at discovery time. Missing required fields (without environment variables set) raise a discovery error immediately, not at runtime
Plan output
Section titled “Plan output”When providers are used by Python nodes or hooks, sqb plan shows a Providers section listing each provider and its consumers:
Providers warehouse_client 2 nodes slack_notifier 1 nodeUse --verbose to see which specific nodes consume each provider.
Project layout
Section titled “Project layout”my-project/ providers/ warehouse_client.py slack_notifier.py hooks/ sql/ record_notification.sql python/ notify.py loaders/ load_orders.py tasks/ export_orders.py