dlt 自定义配置提供器实战:用 YAML Profile + 环境变量占位符替换 secrets.toml
2026/9/18 8:23:18 网站建设 项目流程

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 为配置载体,而本示例演示的是:

  1. 将配置集中存放在一个YAML 文件profiles.yaml)中;
  2. 该文件内含多个可切换的 Profileproddev),按需选取;
  3. 文件中的敏感值使用类 Jinja 占位符{{GITHUB_API_KEY}})书写,加载时被替换为对应的环境变量值;
  4. 将上述逻辑封装为一个自定义 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节层级完全一致;
  • 占位符prodapi_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 value

eval_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是三层职责的叠加:

  1. 读取yaml.safe_load解析当前目录下的profiles.yaml
  2. 选择 Profile:按profile_name取出对应节;若找不到,抛出带绝对路径的RuntimeError,便于定位问题;
  3. 递归替换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_key

github资源使用 dlt 的注入标记声明两个参数:urldlt.config.value注入(非敏感配置),api_keydlt.secrets.value注入(敏感密钥)。当 dlt 解析该资源时,会按提供器链查询sources.github_api.github.urlsources.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 中的urldlt.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:

参数含义:

参数含义默认值
nameProvider 名称,会出现在异常与 trace 中(示例中为"profiles"必填
loader用户提供的无参函数,返回包含配置/密钥的 Python dict;典型做法是"读取字符串(如文件)→ 解析(如 toml/yaml)→ 加工 → 返回 dict"必填
supports_secrets是否允许存放密钥。False时若查询到密钥值会触发ValueNotSecretExceptionTrue
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/SecretsTomlProviderconfig.toml/secrets.toml文件;
  • SettingsTomlProvider:可合并多目录 toml 的基类;
  • VaultDocProviderGoogleSecretsProviderAwsSecretsManagerProvider:Vault / Google / AWS 密钥管理服务;
  • ContextProvider:上下文注入。

这些 Provider 均实现抽象基类ConfigProvider(见 dlt/common/configuration/providers/provider.py)定义的get_valuesupports_secretssupports_sectionsname等接口。有意思的是ConfigTomlProviderSecretsTomlProvider本身也是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.githubsources.github_apisources→ 根;若存在 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),断言其namesupports_secretsto_toml()/to_yaml()输出,然后add_provider注册,最后用resolve_configuration成功解析destination.postgres节的凭证——证明自定义 Provider 注册后能真实参与标准配置解析流程。

实战要点与注意事项

  1. Profile 名的来源:示例用dlt.config["dlt_config_profile_name"]传递 Profile 名,这是示例自定义的键(源码中并无内置)。生产中建议通过config.toml或环境变量注入该值,避免在代码里硬编码环境。
  2. 占位符替换时机CustomLoaderDocProvider构造时立即调用 loader,因此占位符求值发生在注册之前。若环境变量在注册后才设置,将取不到值。
  3. 模板能力边界:正则占位符只支持{{ 单词 }}形态;真正的模板逻辑(条件、循环、嵌套引用)需要引入 Jinja 等模板引擎,在 loader 内部完成渲染。
  4. 安全分层dlt.secrets只查询支持密钥的 Provider;CustomLoaderDocProvider默认supports_secrets=True,因此 YAML 中的api_key能被当作密钥解析。若你的 YAML 只放非敏感配置,应显式传supports_secrets=False
  5. 名称唯一性add_provider会拒绝重复名称,注册前应避免与现有 Provider 重名;且新 Provider 追加在链尾,优先级最低,仅在前置提供器未命中时生效。
  6. 与内置 toml 提供器的取舍:本方案适合"一份模板文件 + 环境变量注入"的多环境场景;若项目已经依赖config.toml/secrets.toml的现有生态(如dlt init生成的模板),可继续使用标准提供器,自定义 Provider 可作为补充或渐进迁移的桥梁。
  7. 调试手段:利用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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询