Skip to content

Migrate ConnectorConfig to BaseSettings and add ConnectorSpecificConfig#75

Open
b-Tomas wants to merge 4 commits into
nextfrom
basesettings-migration
Open

Migrate ConnectorConfig to BaseSettings and add ConnectorSpecificConfig#75
b-Tomas wants to merge 4 commits into
nextfrom
basesettings-migration

Conversation

@b-Tomas
Copy link
Copy Markdown
Member

@b-Tomas b-Tomas commented May 26, 2026

Summary

  • Rename ConnectorConfig to ConnectorRootConfig and rebase it from BaseModel to pydantic-settings BaseSettings
  • Add ConnectorSpecificConfig(BaseSettings) base class for per-connector settings
  • Make ConnectorRootConfig generic over T: ConnectorSpecificConfig — connectors parametrize directly instead of writing a boilerplate subclass
  • to_singular_config returns Self, preserving both generic parameters and subclass types for type checkers
  • Add pydantic-settings as a framework dependency
  • Add tests/ to tox flake8/black targets and fix pre-existing lint violations

Motivation

ConnectorConfig used os.getenv() as class-level field defaults for api_key and api_url. These evaluated once at import time, before load_dotenv() or container-injected secrets were available. Connectors worked around this with fragile load_dotenv("config/.env") calls wedged before framework imports. This was brittle and undocumented.

Additionally, every connector independently declared a BaseSettings subclass with the same SettingsConfigDict (env_ignore_empty, case_sensitive, env_file, extra) and an env_prefix derived from CONNECTOR_TYPE -- multiple connectors repeating the same ~10 lines verbatim.

On top of that, every connector needed a boilerplate ConnectorRootConfig subclass whose only purpose was to narrow the connector_config field type. These subclasses contained no logic.

What this fixes

Environment variable resolution at instantiation time. api_key and api_url (and all other fields) are now resolved from INORBIT_* env vars when the config object is constructed, not when the module is imported. config/.env is read by pydantic-settings automatically -- no load_dotenv() hacks needed.

Eliminated per-connector boilerplate. A new ConnectorSpecificConfig base class provides standard env loading with prefix INORBIT_{CONNECTOR_TYPE}_. A connector config collapses from ~15 lines of SettingsConfigDict setup to:

class MyConfig(ConnectorSpecificConfig):
    CONNECTOR_TYPE = "my_connector"
    api_app_key: str
    base_url: str = "https://example.com"

No SettingsConfigDict, no DEFAULT_ENV_FILE, no pydantic_settings imports needed in each connector.

Eliminated root config subclasses. ConnectorRootConfig is now Generic[T] where T is bound to ConnectorSpecificConfig. Connectors parametrize it directly — no subclass needed:

config = ConnectorRootConfig[MyConfig](**yaml_data)
config.connector_config.api_app_key  # type checker resolves to str

Subclassing is still supported for connectors that need root-level validators or additional fields.

Automatic nested env resolution. When ConnectorRootConfig receives connector_config as a dict (from YAML), a model validator explicitly instantiates the annotated ConnectorSpecificConfig subclass via __init__(), ensuring env sources participate in field resolution. Pydantic's default dict-to-model coercion uses model_validate() which does not trigger BaseSettings env resolution -- only __init__() does.

Precedence is preserved. Init kwargs (YAML values) > env vars > .env file > field defaults. This matches the previous behavior where YAML always won.

Breaking changes and migration guide

Change Migration
ConnectorConfig renamed to ConnectorRootConfig s/ConnectorConfig/ConnectorRootConfig/ in imports and subclass declarations
connector_config field type changed from BaseModel to ConnectorSpecificConfig Connector-specific config classes should extend ConnectorSpecificConfig instead of BaseModel, and set CONNECTOR_TYPE as a class variable
extra="forbid" changed to extra="ignore" Extra init kwargs are now silently discarded instead of raising ValidationError. Remove any code that relied on catching extra-field errors
pydantic-settings is now a transitive dependency Remove redundant pydantic-settings pins from connector pyproject.toml or align with >=2.14,<3.0
os.getenv() defaults removed from api_key / api_url No action needed -- constructing a ConnectorRootConfig instance now correctly reads env vars at instantiation time
Root config subclasses (e.g. MyConnectorConfig(ConnectorRootConfig)) Still work, but no longer needed if the subclass only narrowed connector_config. Use ConnectorRootConfig[MyConfig] directly.

Before / after example

Before:

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
from inorbit_connector.models import ConnectorConfig

DEFAULT_ENV_FILE = "config/.env"

class MyConfig(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="INORBIT_MY_CONNECTOR_",
        env_ignore_empty=True,
        case_sensitive=False,
        env_file=DEFAULT_ENV_FILE,
        extra="allow",
    )
    fleet_host: str
    fleet_port: int = 8080

class MyConnectorConfig(ConnectorConfig):
    connector_config: MyConfig

After:

from inorbit_connector.models import ConnectorRootConfig, ConnectorSpecificConfig

class MyConfig(ConnectorSpecificConfig):
    CONNECTOR_TYPE = "my_connector"
    fleet_host: str
    fleet_port: int = 8080

# No subclass needed — parametrize directly:
config = ConnectorRootConfig[MyConfig](**yaml_data)
config.connector_config.fleet_host  # type checker knows this is str

b-Tomas and others added 4 commits May 25, 2026 22:48
Rename ConnectorConfig to ConnectorRootConfig, rebase from BaseModel to
pydantic-settings BaseSettings. Add ConnectorSpecificConfig base class
for per-connector settings with automatic INORBIT_{CONNECTOR_TYPE}_ env
prefix. Env vars now resolve at instantiation time instead of import
time, eliminating load_dotenv() workarounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ConnectorRootConfig is now Generic[T] where T is bound to
ConnectorSpecificConfig. Connectors parametrize it directly
(ConnectorRootConfig[MyConfig](**yaml_data)) instead of writing
a boilerplate subclass. Type checkers resolve connector_config
to the concrete type. Existing subclasses continue to work.

Also uses Self return type for to_singular_config to preserve
both generic parameters and subclass types for type checkers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tox was only linting inorbit_connector/, missing test files entirely.
Add tests/ to both flake8 and black commands, then fix all pre-existing
violations: unused imports, long docstrings, blank line issues, and
formatting inconsistencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@b-Tomas b-Tomas marked this pull request as ready for review May 26, 2026 02:41
@b-Tomas b-Tomas requested a review from Copilot May 26, 2026 02:41
@b-Tomas b-Tomas self-assigned this May 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the framework’s connector configuration model to pydantic-settings so INORBIT_* environment variables (and config/.env) are resolved at instantiation time rather than import time, and introduces a standardized ConnectorSpecificConfig base to reduce per-connector boilerplate.

Changes:

  • Replaced ConnectorConfig(BaseModel) with ConnectorRootConfig(BaseSettings) and added ConnectorSpecificConfig(BaseSettings) for per-connector settings.
  • Made root config generic (ConnectorRootConfig[T]) and updated connectors/examples/docs/tests to use the new API.
  • Added pydantic-settings dependency and expanded tox lint targets to include tests/.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
inorbit_connector/models.py Introduces ConnectorSpecificConfig/ConnectorRootConfig (BaseSettings + generic) and updates env resolution behavior.
inorbit_connector/connector.py Updates connector base classes to accept ConnectorRootConfig instead of ConnectorConfig.
pyproject.toml Adds pydantic-settings dependency; runs flake8/black over tests/ in tox.
tests/conftest.py Adds autouse fixture to clear INORBIT_* env vars for test isolation.
tests/test_models.py Updates config model tests and adds coverage for env + dotenv + generic parametrization.
tests/test_connector.py Migrates tests to ConnectorRootConfig/ConnectorSpecificConfig.
tests/test_connector_metrics.py Migrates metrics tests to new config types.
tests/test_metrics_server.py Minor formatting-only change.
tests/test_utils.py Removes stray whitespace line.
tests/test_commands.py Minor docstring/text formatting updates.
examples/simple-connector/connector.py Updates example to use ConnectorRootConfig[ExampleBotConfig] + ConnectorSpecificConfig.
examples/simple-fleet-connector/connector.py Updates fleet example to use ConnectorRootConfig[ExampleBotConfig] + ConnectorSpecificConfig.
examples/robot-connector/main.py Updates example entrypoint to construct ConnectorRootConfig[ExampleBotConfig].
examples/robot-connector/datatypes.py Replaces connector-specific root subclass with ConnectorSpecificConfig.
examples/robot-connector/connector.py Updates example connector type hints to ConnectorRootConfig[ExampleBotConfig].
examples/fleet-connector/main.py Updates example entrypoint to construct ConnectorRootConfig[ExampleBotConfig].
examples/fleet-connector/datatypes.py Replaces connector-specific root subclass with ConnectorSpecificConfig.
examples/fleet-connector/connector.py Updates example connector type hints to ConnectorRootConfig[ExampleBotConfig].
docs/contents/configuration.md Updates configuration docs to describe ConnectorRootConfig + ConnectorSpecificConfig usage and env behavior.
docs/contents/specification/models.md Updates spec to document ConnectorRootConfig and adds ConnectorSpecificConfig section.
docs/contents/specification/index.md Updates exported symbol table to reflect new config types.
docs/contents/usage/single-robot.md Updates usage docs to reference ConnectorRootConfig in constructor signature.
docs/contents/usage/fleet.md Updates usage docs to reference ConnectorRootConfig in constructor signature.
docs/contents/usage/metrics.md Updates metrics docs to reference ConnectorRootConfig.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +363 to +374
if isinstance(data, dict) and isinstance(data.get("connector_config"), dict):
ann_type = cls.model_fields["connector_config"].annotation
if (
isinstance(ann_type, type)
and issubclass(ann_type, ConnectorSpecificConfig)
and ann_type is not ConnectorSpecificConfig
):
data = {
**data,
"connector_config": ann_type(**data["connector_config"]),
}
return data
Comment on lines 20 to 22
# Third-party
from pydantic import field_validator, BaseModel
from pydantic import field_validator

Comment on lines 29 to 32
CONFIG_FILE = Path(__file__).resolve().parent.parent / "example.yaml" # ../example.yaml
ROBOT_ID = "my-example-robot"
CONNECTOR_TYPE = "example_bot"

Comment on lines 20 to 22
# Third-party
from pydantic import field_validator, BaseModel
from pydantic import field_validator

Comment on lines 29 to 33
@@ -32,7 +32,7 @@
CONNECTOR_TYPE = "example_bot"

from inorbit_connector.models import ConnectorSpecificConfig


CONNECTOR_TYPE = "example_bot"
from inorbit_connector.models import ConnectorSpecificConfig


CONNECTOR_TYPE = "example_bot"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants