Credentials dataclass: схема profiles.yml
Когда пользователь пишет в profiles.yml:
my_project:
outputs:
dev:
type: oceanbase
host: localhost
port: 2881
user: root
database: test
dbt должен превратить это в Python-объект. Этим занимается Credentials dataclass. В этом уроке — полный разбор: какие поля обязательны, что такое ALIASES, как _connection_keys влияет на caching, что такое unique_field.
@dataclass: автогенерация __init__ и field defaults env_var(): чтение environment variables и секреты (dbt I)
Базовая структура
# dbt-oceanbase/dbt/adapters/oceanbase/credentials.py
from dataclasses import dataclass, field
from typing import Optional
from dbt.adapters.contracts.connection import Credentials
@dataclass
class OceanBaseCredentials(Credentials):
# Required positional args (no default)
host: str
# Optional with defaults
port: int = 2881
user: str = 'root'
password: str = ''
database: str = 'test'
schema: str = ''
# Standard dbt fields (inherited from Credentials)
# database, schema — already в parent class
# Adapter-specific
tenant: Optional[str] = None
cluster: Optional[str] = None
threads: int = 4
@property
def type(self) -> str:
return 'oceanbase'
@property
def unique_field(self) -> str:
return self.host
def _connection_keys(self):
return ('host', 'port', 'user', 'database', 'schema', 'tenant')
Разберём каждую часть.
Поля dataclass
Поля определяют структуру profiles.yml. Каждое поле в dataclass = один key в YAML.
@dataclass
class OceanBaseCredentials(Credentials):
host: str # Required — no default
port: int = 2881 # Optional — default 2881
user: str = 'root'
password: str = ''
database: str = 'test'
schema: str = ''
tenant: Optional[str] = None # Может быть None
cluster: Optional[str] = None
threads: int = 4
Required vs Optional:
- Required field — без default value. Если пользователь не укажет в
profiles.yml, dbt fails сmissing required field: host. - Optional field — с default. Можно опустить в YAML.
Stand-я поля (inherited from base Credentials):
database— name of database (Snowflake’s DB, Postgres’sdb, BigQuery’s project)schema— name of default schemathreads— count of parallel threads для dbt run
Вы должны включить database и schema. Это используется dbt-core’s CLI и internal logic.
Adapter-specific — что хотите. Для OceanBase это tenant, cluster. Для Snowflake — account, warehouse, role. Для BigQuery — project, keyfile.
ALIASES — синонимы для полей
Иногда пользователи привыкли к другим именам полей из похожих warehouses. Например, MySQL пользователи знают username, dbt’s standard — user. Можно поддержать оба:
@dataclass
class OceanBaseCredentials(Credentials):
host: str
user: str = 'root'
# ALIASES — alternative names accepted в profiles.yml
ALIASES = {
'username': 'user', # username -> user
'pass': 'password', # pass -> password
'db': 'database', # db -> database
}
Теперь оба работают:
# Standard
dev:
type: oceanbase
user: alice
database: analytics
# С aliases
dev:
type: oceanbase
username: alice # ← treated as 'user'
db: analytics # ← treated as 'database'
dbt при загрузке profiles.yml применяет ALIASES до создания dataclass.
Use case:
- Migration friendly: пользователи переходящие с другого dbt-adapter, привыкли к alternative names.
- Multiple конвенций: warehouse имеет два standard, оба должны работать.
Best practice: не использовать ALIASES без причины. Single canonical name проще для документации. Используйте только когда есть strong reason (migration path).
type property — идентификатор adapter
@property
def type(self) -> str:
return 'oceanbase'
Это критический property. Используется:
-
profiles.yml
typefield должно совпадать:dev: type: oceanbase # ← должно совпадать с self.type -
AdapterPlugin registration — dbt-core хранит plugins по type:
FACTORY.plugins = { 'oceanbase': OceanBasePlugin, 'snowflake': SnowflakePlugin, ... } -
Dispatch macros:
<type>__macro_name— например,oceanbase__list_schemas. dbt используетself.typeчтобы определить, какие macros искать.
Naming convention: lowercase, no spaces, no underscores в начале. Match имя pip-package: dbt-oceanbase -> type oceanbase.
unique_field — для статистики
@property
def unique_field(self) -> str:
return self.host
Этот property возвращает строку, идентифицирующую deploy. Используется dbt Labs для анонимной статистики (опционально, можно disable).
Для каждого adapter unique_field обычно:
- Postgres: host
- Snowflake: account (e.g., ‘xy12345.us-east-1’)
- BigQuery: project
- DuckDB: path
- OceanBase: host
Зачем: dbt анонимизирует данные о использовании, sending к telemetry endpoint. unique_field ХЕШИРУЕТСЯ перед отправкой — dbt не видит actual host/account, видит только хеш. Цель: посчитать кол-во unique deploys, не identifying specific user.
Privacy: пользователи могут отключить telemetry в ~/.dbt/profiles.yml:
config:
send_anonymous_usage_stats: False
Тогда unique_field не отправляется куда-либо.
_connection_keys — для caching
def _connection_keys(self):
return ('host', 'port', 'user', 'database', 'schema', 'tenant')
Этот method возвращает tuple полей, которые идентифицируют unique connection. dbt использует это для caching connections.
Зачем:
dbt поддерживает threading — несколько моделей могут выполняться parallel. Каждый thread имеет свой connection. Если несколько threads запрашивают connection с одинаковыми credentials — dbt reuses connection из pool.
Cache key для connection — это hash of _connection_keys values.
Example:
# Thread 1: connection с (host='localhost', port=2881, user='alice', db='analytics', schema='dev', tenant=None)
# Thread 2: connection с тем же — REUSES from cache
# Thread 3: connection с (host='localhost', port=2881, user='alice', db='analytics', schema='prod', tenant=None)
# ← different schema -> DIFFERENT connection (нужен новый)
Что включать в _connection_keys:
- Yes: host, port, user, database, schema (главные)
- Yes: warehouse-specific identifiers: tenant, role, warehouse (Snowflake), project (BigQuery)
- No: password (не identifies connection, identifies user — privacy)
- No: threads (это runtime config, не connection identity)
Common mistake: забыть включить warehouse-specific identifier. Например, на Snowflake забыть role — тогда два разных role’а share connection, что приводит к permission bugs.
Полный пример — Snowflake
Для сравнения, реальный Snowflake credentials:
# dbt-snowflake/dbt/adapters/snowflake/connections.py (упрощённо)
@dataclass
class SnowflakeCredentials(Credentials):
account: str
user: str
# Auth options (one of these required)
password: Optional[str] = None
private_key: Optional[str] = None
private_key_path: Optional[str] = None
private_key_passphrase: Optional[str] = None
authenticator: Optional[str] = None
oauth_client_id: Optional[str] = None
oauth_client_secret: Optional[str] = None
token: Optional[str] = None
# Connection options
warehouse: Optional[str] = None
role: Optional[str] = None
database: Optional[str] = None
schema: Optional[str] = None
# Behavior
client_session_keep_alive: bool = False
query_tag: Optional[str] = None
connect_retries: int = 1
connect_timeout: Optional[int] = None
ALIASES = {
'auth_user': 'user',
'sf_account': 'account',
}
@property
def type(self) -> str:
return 'snowflake'
@property
def unique_field(self) -> str:
return self.account
def _connection_keys(self):
return (
'account', 'user', 'role', 'warehouse',
'database', 'schema', 'authenticator',
)
Видны несколько паттернов:
- Multiple auth methods: password, private_key, OAuth, SSO. Snowflake supports все из них.
- Optional fields: большинство Optional, потому что разные auth methods требуют разный набор.
- Behavior knobs:
client_session_keep_alive,query_tag,connect_retries— runtime options. - ALIASES: для migration от dbt-cloud (which used different field names исторически).
Validation в credentials
Иногда нужно validate credentials. Например: «password ИЛИ private_key должен быть указан». dbt предоставляет __post_init__:
@dataclass
class OceanBaseCredentials(Credentials):
host: str
user: str = 'root'
password: Optional[str] = None
auth_token: Optional[str] = None
def __post_init__(self):
# Validation после dataclass init
if not self.password and not self.auth_token:
raise ValueError(
'OceanBaseCredentials requires either password or auth_token'
)
if self.password and self.auth_token:
raise ValueError(
'OceanBaseCredentials: provide either password or auth_token, not both'
)
__post_init__ вызывается dataclass’ом после standard init. Хорошее место для cross-field validation.
Best practice:
- Validate at parse time (когда credentials loaded), не at connection time
- Provide clear error messages
- Don’t hide credentials в error messages
profile_template.yml
Cookiecutter scaffold генерирует dbt/include/oceanbase/profile_template.yml:
# profile_template.yml
fixed:
type: oceanbase
prompts:
host:
hint: 'The hostname for the OceanBase instance'
port:
default: 2881
hint: 'The port for the OceanBase instance'
user:
hint: 'Username для authentication'
password:
hint: 'Password для user'
hide_input: true
database:
hint: 'Database name'
schema:
hint: 'Schema name'
threads:
default: 4
hint: 'Number of threads'
Этот файл используется командой dbt init — interactive setup для нового проекта. dbt спрашивает у пользователя поля по template.
dbt init my_oceanbase_project --adapter oceanbase
# Interactive prompts:
# host: localhost
# port [2881]:
# user: alice
# password [hidden]: ********
# database: analytics
# schema: dev
# threads [4]:
Создаёт правильный profiles.yml. Полезно для UX.
Что happens при загрузке profiles.yml
Lifecycle credentials loading:
Этот lifecycle — same для всех adapter’ов. Customization точки:
- Field definitions — define schema
- ALIASES — accept alternative names
__post_init__— cross-field validation_connection_keys— каждое поле для cache identity
env_var() в profiles.yml
Пользователи часто используют env_var для secrets:
dev:
type: oceanbase
host: '{{ env_var("OB_HOST") }}'
password: '{{ env_var("OB_PASSWORD") }}'
dbt evaluates Jinja до dataclass creation. То есть OceanBaseCredentials(host=os.environ['OB_HOST']), не host='{{ env_var(...) }}'.
В credentials class нет ничего special для env_var — это работает via dbt-core’s Jinja evaluation.
Попробуй сам
-
Создайте credentials.py с базовой структурой:
from dataclasses import dataclass from dbt.adapters.contracts.connection import Credentials @dataclass class MyAdapterCredentials(Credentials): host: str port: int = 5432 user: str = 'admin' password: str = '' database: str = 'mydb' schema: str = 'public' @property def type(self) -> str: return 'myadapter' @property def unique_field(self) -> str: return self.host def _connection_keys(self): return ('host', 'port', 'user', 'database', 'schema') -
Создайте profiles.yml:
test_project: target: dev outputs: dev: type: myadapter host: localhost user: alice database: testdb schema: dev -
Добавьте ALIASES — позвольте
usernameinstead ofuser:ALIASES = { 'username': 'user', } -
Добавьте post_init validation:
def __post_init__(self): if not self.password and 'PASSWORD' not in os.environ: # Either explicit password or env var pass -
Test loading через
dbt parse. Если credentials structure правильная — должно работать.
Ключевые выводы
-
Credentials dataclass определяет схему profiles.yml. Required (no default) vs optional (with default) fields.
-
typeproperty — identifier adapter. Должно совпадать с profiles.ymltypefield и pip-package name. -
unique_field— для анонимной статистики. Обычно host / account / project. -
_connection_keys— поля для connection caching. Включайте все, что identifies unique connection (host, port, user, db, schema, role, warehouse). -
ALIASES — alternative field names. Не используйте без причины.
-
__post_init__— для cross-field validation. Clear error messages. -
profile_template.yml — для
dbt initinteractive setup. UX-friendly.