dlt 自定义配置提供器实战:用 YAML Profile + 环境变量占位符替换 secrets.toml
【免费下载链接】dltdata load tool (dlt) is an open source Python library that makes data loading easy 🛠️项目地址: https://gitcode.com/GitHub_Trending/dl/dlt
导读
dlt(data load tool)默认通过config.toml/secrets.toml与系统环境变量解析配置,但在多环境(开发、生产)或需要模板化配置的场景下,这远远不够灵活。本文基于仓库中的官方示例 custom_config_provider.md,讲解如何用一份支持多 Profile(prod/dev)的 YAML 文件替代 toml 配置文件,并通过{{ PLACEHOLDER }}占位符自动注入环境变量中的密钥。读完本文,你将掌握实现自定义配置加载器(Loader)、实例化并注册CustomLoaderDocProvider提供器的完整流程,并理解 dlt 配置提供器链的底层解析机制。
示例要解决的问题
dlt 的配置解析依赖一组被称为config providers(配置提供器)的对象,它们负责按固定的查询链提供配置项与密钥——例如读取环境变量、读取secrets.toml文件等。默认提供器均以 toml 为配置载体,而本示例演示的是:
- 将配置集中存放在一个YAML 文件(
profiles.yaml)中; - 该文件内含多个可切换的 Profile(
prod与dev),按需选取; - 文件中的敏感值使用类 Jinja 占位符(
{{GITHUB_API_KEY}})书写,加载时被替换为对应的环境变量值; - 将上述逻辑封装为一个自定义 Provider 并注册到 dlt 的解析链中,使 dlt 像使用标准 Provider 一样使用它。
示例的核心价值在于:你不再需要把secrets.toml提交到仓库或手工维护多份配置文件,只需维护一份 YAML 模板 + 各环境的环境变量,即可完成配置与密钥的分离和按环境切换。
完整源码与配套文件
1. profiles.yaml:带 Profile 的配置文档
配套的配置文件位于 docs/website/docs/examples/profiles.yaml:
prod: sources: github_api: # source level github: # resource level url: https://github.com/api api_key: "{{GITHUB_API_KEY}}" dev: sources: github_api: url: https://github.com/api api_key: "" # no keys in dev env注意两处细节:
- 层级结构:顶层是 Profile 名(
prod/dev),向下依次是sources→ source 名(github_api)→ resource 名(github)→ 字段名,这与 dlt 标准的config.toml节层级完全一致; - 占位符:
prod下api_key的值为"{{GITHUB_API_KEY}}",加载时会被替换成同名环境变量的值;dev下则留空,表示开发环境不注入真实密钥。
2. 主代码:加载、替换、注册、验证
示例主代码(见 custom_config_provider.md 全文)按功能拆解如下。
导入与模块级配置
import os import re import dlt import yaml import functools from dlt.common.configuration.providers import CustomLoaderDocProvider from dlt.common.utils import map_nested_values_in_place # config for all resources found in this file will be grouped in this source level config section __source_name__ = "github_api"__source_name__ = "github_api"让本文件中所有资源(resource)的配置统一归入sources.github_api这一 source 级节,与profiles.yaml中的sources.github_api键对应。
占位符求值函数
def eval_placeholder(value): """Replaces jinja placeholders {{ PLACEHOLDER }} with environment variables""" if isinstance(value, str): def replacer(match): return os.environ[match.group(1)] return re.sub(r"\{\{\s*(\w+)\s*\}\}", replacer, value) return valueeval_placeholder用正则\{\{\s*(\w+)\s*\}\}匹配所有{{ 名称 }}形态的占位符,并直接从os.environ中取出对应环境变量的值完成替换。注意它只处理字符串值,非字符串原样返回——这决定了占位符必须以字符串形式写在 YAML 中。
YAML 加载器:选 Profile + 递归替换
def loader(profile_name: str): """Loads yaml file from profiles.yaml in current working folder, selects profile, replaces placeholders with env variables and returns Python dict with final config """ path = os.path.abspath("profiles.yaml") with open(path, "r", encoding="utf-8") as f: config = yaml.safe_load(f) # get the requested environment config = config.get(profile_name, None) if config is None: raise RuntimeError( f"Profile with name {profile_name} not found in {os.path.abspath(path)}" ) # evaluate all placeholders # NOTE: this method only works with placeholders wrapped as strings in yaml. use jinja lib for real templating return map_nested_values_in_place(eval_placeholder, config)loader是三层职责的叠加:
- 读取:
yaml.safe_load解析当前目录下的profiles.yaml; - 选择 Profile:按
profile_name取出对应节;若找不到,抛出带绝对路径的RuntimeError,便于定位问题; - 递归替换:
map_nested_values_in_place(实现于 dlt/common/utils.py,其行为有测试覆盖,见 tests/common/test_utils.py)会遍历整个嵌套 dict,对每个叶子值应用eval_placeholder,从而一次性完成所有占位符的替换。
代码注释特别提醒:这种方式只适用于 YAML 中以字符串包裹的占位符,若需要真正的模板语法(条件、循环等),应改用 Jinja 之类的模板库。
消费配置的 resource
@dlt.resource def github(url: str = dlt.config.value, api_key=dlt.secrets.value): # just return the injected config and secret yield url, api_keygithub资源使用 dlt 的注入标记声明两个参数:url由dlt.config.value注入(非敏感配置),api_key由dlt.secrets.value注入(敏感密钥)。当 dlt 解析该资源时,会按提供器链查询sources.github_api.github.url与sources.github_api.github.api_key。
切换 Profile 并注册 Provider
# mock env variables to fill placeholders in profiles.yaml os.environ["GITHUB_API_KEY"] = "secret_key" # mock expected var # set the active profile explicitly (normally this comes from config.toml or an env var) dlt.config["dlt_config_profile_name"] = "prod" # dlt standard providers work at this point (we have the profile name in config) profile_name = dlt.config["dlt_config_profile_name"] # instantiate custom provider using `prod` profile # NOTE: all placeholders (ie. GITHUB_API_KEY) will be evaluated in next line! provider = CustomLoaderDocProvider("profiles", functools.partial(loader, profile_name)) # register provider, it will be added as the last one in chain dlt.config.register_provider(provider)这段代码说明三个要点:
os.environ["GITHUB_API_KEY"] = "secret_key"只是模拟环境变量(实际场景中应通过部署平台注入);- 活动 Profile 名通过
dlt.config["dlt_config_profile_name"] = "prod"写入配置(生产环境中它通常来自config.toml或环境变量,从而做到"配置文件里不写死 Profile"); - 使用
functools.partial(loader, profile_name)把"选哪个 Profile"固化为无参调用形式,正好匹配CustomLoaderDocProvider期望的Callable[[], Dict[str, Any]]签名。注意:注释强调所有占位符在下一行实例化 Provider 时就会被求值(因为构造函数内部会立即调用loader())。
注册后即可生效并验证
# your pipeline will now be able to use your yaml provider # p = Pipeline(...) # p.run(...) # show the final config # print(provider.to_yaml()) # or if you like toml # print(provider.to_toml()) # the registered provider now resolves config and secrets for the github_api source assert dlt.config["sources.github_api.github.url"] == "https://github.com/api" assert dlt.secrets["sources.github_api.github.api_key"] == "secret_key"注册之后,自定义 Provider 就会参与 dlt 后续所有配置解析(包括Pipeline(...)/p.run(...)中对 source、resource 参数的注入)。示例末尾用两个断言验证:dlt.config能读到 YAML 中的url,dlt.secrets能读到经占位符替换后的api_key。同时provider.to_yaml()/provider.to_toml()可以把最终(替换后)的配置导出为 YAML 或 TOML 格式,便于调试与审计。
源码级剖析:CustomLoaderDocProvider 内部机制
类层次与能力
CustomLoaderDocProvider定义在 dlt/common/configuration/providers/doc.py,它继承自BaseDocProvider(同文件 L12-L157)。构造函数签名如下:
def __init__( self, name: str, loader: Callable[[], Dict[str, Any]], supports_secrets: bool = True, locations: Sequence[str] = None, ) -> None:参数含义:
| 参数 | 含义 | 默认值 |
|---|---|---|
name | Provider 名称,会出现在异常与 trace 中(示例中为"profiles") | 必填 |
loader | 用户提供的无参函数,返回包含配置/密钥的 Python dict;典型做法是"读取字符串(如文件)→ 解析(如 toml/yaml)→ 加工 → 返回 dict" | 必填 |
supports_secrets | 是否允许存放密钥。False时若查询到密钥值会触发ValueNotSecretException | True |
locations | 人类可读的配置来源位置列表,用于在配置未解析时生成有意义的错误信息 | None |
构造时super().__init__(loader())会立即调用 loader 并缓存返回的 dict(即示例注释所说的"占位符在实例化那一刻就被求值"),之后对配置的查询都在这个 dict 上进行。
BaseDocProvider 提供的查询与写入能力
get_value(key, hint, pipeline_name, *sections):按pipeline_name+ sections 组成的路径在 dict 中逐层下钻取值;取不到时返回(None, full_key),不抛异常(由上层解析逻辑判断是否缺失);set_value(...):写入配置。若目标位置已是 dict 且新值也是 dict,则递归合并(见 doc.py L93-L127);set_fragment(key, value_or_fragment, ...):把一段 toml/yaml/json 片段解析后合并进配置文档,简单值则回退到set_value;preserve():上下文管理器,退出时恢复进入前的配置文档,用于临时覆盖配置而不污染全局状态;to_toml()/to_yaml():把内部 dict 序列化为 TOML / YAML 字符串(示例代码中注释掉了这两个调用,用于展示最终配置);supports_sections=True:声明该 Provider 支持按节(section)查询,dlt 会为它枚举所有合法的节组合路径。
在提供器链中的位置与注册语义
dlt.config.register_provider(provider)的实际逻辑见 dlt/common/configuration/accessors.py L118-L122:它把 Provider 追加到Container()[PluggableRunContext].providers。而 dlt/common/configuration/specs/config_providers_context.py L95-L98 中的add_provider规定:
def add_provider(self, provider: ConfigProvider) -> None: if provider.name in self: raise DuplicateConfigProviderException(provider.name) self.providers.append(provider)即Provider 以"追加到链尾"的方式注册,且名称必须唯一(重复名称会抛DuplicateConfigProviderException)。文档注释"it will be added as the last one in chain"正是对这一实现的表述:自定义 Provider 的优先级最低,只有前面的标准 Provider(环境变量、toml 文件等)都解析不到时才会轮到它。
解析链如何工作:一次配置查询的完整路径
标准提供器清单
dlt 内置的标准提供器统一从 dlt/common/configuration/providers/init.py 导出,包括:
EnvironProvider:环境变量;ConfigTomlProvider/SecretsTomlProvider:config.toml/secrets.toml文件;SettingsTomlProvider:可合并多目录 toml 的基类;VaultDocProvider、GoogleSecretsProvider、AwsSecretsManagerProvider:Vault / Google / AWS 密钥管理服务;ContextProvider:上下文注入。
这些 Provider 均实现抽象基类ConfigProvider(见 dlt/common/configuration/providers/provider.py)定义的get_value、supports_secrets、supports_sections、name等接口。有意思的是:ConfigTomlProvider与SecretsTomlProvider本身也是CustomLoaderDocProvider的子类(经由SettingsTomlProvider继承,见 toml.py L58-L110)——也就是说,本示例"自定义加载器 + 文档型 Provider"的模式正是 dlt 内置 toml 支持所采用的同一套机制。
查询顺序与节路径构建
dlt 的解析核心位于 dlt/common/configuration/resolve.py:
_resolve_single_value(L510-L573)从Container中取出提供器列表,按注册顺序依次查询,一旦某个 Provider 返回非空值立即停止("first match wins");- 对支持节(section)的 Provider,
_build_section_lookup_paths(L576-L614)会按"从最具体到最不具体"的顺序生成候选路径:例如sources.github_api.github→sources.github_api→sources→ 根;若存在 pipeline 名,还会先以 pipeline 名作为顶层节查询一次; resolve_single_provider_value(L617-L660)逐路径调用provider.get_value,并且有一个安全约束:若从supports_secrets=False的 Provider 中解析到密钥类型值(is_secret_hint(hint)为真),会抛出ValueNotSecretException,防止把密钥误存在非安全提供器中。
这就是示例中dlt.config["sources.github_api.github.url"]与dlt.secrets["sources.github_api.github.api_key"]能被解析的原因:dlt.config/dlt.secrets这两个访问器(见 dlt/common/configuration/accessors.py)内部就是按上述链路查询的,其中dlt.secrets只查询supports_secrets=True的提供器。
单元测试佐证
仓库测试 tests/common/configuration/test_toml_provider.py L787-L816 的test_custom_loader完整复现了这一模式:定义一个从config.yml读取并yaml.safe_load的 loader,实例化CustomLoaderDocProvider("yaml", loader, True),断言其name、supports_secrets、to_toml()/to_yaml()输出,然后add_provider注册,最后用resolve_configuration成功解析destination.postgres节的凭证——证明自定义 Provider 注册后能真实参与标准配置解析流程。
实战要点与注意事项
- Profile 名的来源:示例用
dlt.config["dlt_config_profile_name"]传递 Profile 名,这是示例自定义的键(源码中并无内置)。生产中建议通过config.toml或环境变量注入该值,避免在代码里硬编码环境。 - 占位符替换时机:
CustomLoaderDocProvider构造时立即调用 loader,因此占位符求值发生在注册之前。若环境变量在注册后才设置,将取不到值。 - 模板能力边界:正则占位符只支持
{{ 单词 }}形态;真正的模板逻辑(条件、循环、嵌套引用)需要引入 Jinja 等模板引擎,在 loader 内部完成渲染。 - 安全分层:
dlt.secrets只查询支持密钥的 Provider;CustomLoaderDocProvider默认supports_secrets=True,因此 YAML 中的api_key能被当作密钥解析。若你的 YAML 只放非敏感配置,应显式传supports_secrets=False。 - 名称唯一性:
add_provider会拒绝重复名称,注册前应避免与现有 Provider 重名;且新 Provider 追加在链尾,优先级最低,仅在前置提供器未命中时生效。 - 与内置 toml 提供器的取舍:本方案适合"一份模板文件 + 环境变量注入"的多环境场景;若项目已经依赖
config.toml/secrets.toml的现有生态(如dlt init生成的模板),可继续使用标准提供器,自定义 Provider 可作为补充或渐进迁移的桥梁。 - 调试手段:利用
provider.to_yaml()/provider.to_toml()输出替换后的最终配置;配置缺失时,dlt 的LookupTrace会记录每个 Provider 的查询路径与结果,便于定位是哪一层没有命中。
小结
本示例展示了 dlt 配置系统的可扩展性:借助CustomLoaderDocProvider,你可以把任意来源(YAML、远程文件、密钥服务等)接入 dlt 的标准解析链,并让dlt.config/dlt.secrets、source/resource 参数注入、Pipeline.run等所有上层机制透明地使用它。掌握"加载器(loader)→ 文档型 Provider → 注册进解析链"这一模式后,你就能按自己的规范设计多环境、模板化的配置体系,同时继续享受 dlt 提供的密钥安全校验、section 路径解析与配置追踪能力。
进一步阅读:示例源码 docs/website/docs/examples/custom_config_provider.md、配套 YAML docs/website/docs/examples/profiles.yaml、Provider 实现 dlt/common/configuration/providers/doc.py、解析内核 dlt/common/configuration/resolve.py 及测试用例 tests/common/configuration/test_toml_provider.py。
【免费下载链接】dltdata load tool (dlt) is an open source Python library that makes data loading easy 🛠️项目地址: https://gitcode.com/GitHub_Trending/dl/dlt
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考