feat: plug-and-play v0.5.0 — Proposals 1 + 2 (Pydantic config, builder, Protocol, testing plugin)#18
Merged
Merged
Conversation
Replaces all 11 @DataClass declarations in flow_doctor/core/config.py with pydantic.BaseModel via a shared _ConfigModel base. Field names and defaults are preserved so existing test fixtures and consumers (alpha-engine calls flow_doctor.init(config_path=...)) keep working unchanged. The migration is the foundation for the FlowDoctor.builder() fluent API and Pydantic BaseSettings-driven env-var injection landing in subsequent commits on this branch (per private/plug-and-play-260513.md Proposal 1). load_config() gains pass-through branches that accept already-constructed Pydantic instances in addition to dicts, so the upcoming builder can hand typed sub-configs straight to it without re-serializing through YAML. Adds pydantic>=2.0 to [project.dependencies]; alpha-engine + morning-signal already run Pydantic 2.x per the plan's compat audit. 296/296 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the SOTA-facing entry point for new consumers (plan Proposal 1): - flow_doctor/notify/configs.py defines SlackNotifierConfig, EmailNotifierConfig, GitHubNotifierConfig, S3NotifierConfig as Pydantic v2 models, exposed as the discriminated union NotifierConfig via Field(discriminator="type"). EmailNotifierConfig.recipients accepts either a CSV string or a list and normalizes via a field_validator on the way down to the legacy NotifyChannelConfig. - flow_doctor/core/builder.py adds FlowDoctorBuilder with fluent add_notifier / with_repo / with_dedup / with_store / with_diagnosis / with_github / with_auto_fix / with_remediation / with_handler / with_dependencies methods plus build_config() and build(strict=True). - FlowDoctor.builder(flow_name) classmethod is the recommended entry point for new code; flow_doctor.init(config_path=...) still works unchanged for 0.4.0 callers. The new typed configs and builder are re-exported from the flow_doctor and flow_doctor.notify package roots. 18 new tests cover: per-channel config -> legacy NotifyChannelConfig round-trip, list-to-CSV recipients normalization, fluent chaining, unspecified-section defaults, the morning-signal cutover use case, and the discriminated-union deserialization path (TypeAdapter + ValidationError for unknown channel types). Suite: 314/314 pass (296 baseline + 18 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First slice of Proposal 2 from the plan: - flow_doctor/_protocol.py defines @runtime_checkable FlowDoctorProtocol declaring report() / guard() / monitor() / report_async() as the cross-version public contract. Consumers can now type-hint against the Protocol and swap in test doubles (RecordingFlowDoctor lands in the next commit) with mypy / isinstance verification. - flow_doctor/core/_context.py defines per-task/-thread contextvars for flow_name + stage + arbitrary extras, exposed via flow_doctor.context(). Inner scopes shadow outer ones; the active snapshot is merged into every report's context payload at _build_context() time. Deep call-stacks no longer need to thread context=... explicitly. - FlowDoctor.report_async() coroutine runs the existing sync pipeline via asyncio.to_thread() so async callers don't block the event loop. contextvars are inherited across the thread boundary automatically via contextvars.copy_context() (asyncio.to_thread does this for free). Suite: 322/322 pass (8 new). Tests cover: runtime Protocol satisfaction for FlowDoctor + arbitrary stubs, contextvars propagation onto reports, inner-scope shadowing, post-scope reset (no leaks across tests), and contextvar inheritance into report_async's worker thread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the testing-module half of Proposal 2: - flow_doctor/testing/_recording.py defines ReportedIncident dataclass (error / severity / context / logs / message + exc_type/exc_message derived from BaseException + ambient_context snapshotted from any flow_doctor.context() scope at report time) and RecordingFlowDoctor which implements FlowDoctorProtocol with report() / report_async() / guard() / monitor() recording calls in-memory. Ergonomic helpers: .clear(), .last, .of_type(exc_name). - flow_doctor/testing/_plugin.py exposes a `flow_doctor_recorder` pytest fixture (fresh per test). - pyproject.toml registers the plugin via [project.entry-points.pytest11], so downstreams pip install flow-doctor and get the fixture auto-discovered with no imports in their tests. 13 new tests cover: Protocol satisfaction at runtime, exception/string metadata capture, ambient flow_doctor.context() propagation onto the recorded ambient_context dict, .clear()/.of_type()/.last helpers, guard() and monitor() decorator behaviour, async report_async() round- trip, fresh-per-test fixture isolation, and pytest11 entry-point wiring (the fixture arrives without an explicit import in the consumer test). Suite: 335/335 pass (322 prior + 13 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands Proposals 1 + 2 from
private/plug-and-play-260513.md— typed, IDE-discoverable configuration AND the Protocol + testing-plugin surface that unblocks morning-signal's cutover.Proposal 1 — Pydantic v2 config + builder
flow_doctor/core/config.py— all 11@dataclassconfig models → Pydantic v2BaseModelvia a shared_ConfigModelbase. Field names + defaults preserved → drop-in for existing test fixtures and 0.4.0 callers. Addspydantic>=2.0to runtime deps.flow_doctor/notify/configs.py—SlackNotifierConfig | EmailNotifierConfig | GitHubNotifierConfig | S3NotifierConfigas a Pydantic discriminated union (Field(discriminator="type")).EmailNotifierConfig.recipientsaccepts a CSV string or a list and normalizes via afield_validator.flow_doctor/core/builder.py—FlowDoctorBuilderwith fluentadd_notifier / with_repo / with_dedup / with_store / with_diagnosis / with_github / with_auto_fix / with_remediation / with_handler / with_dependenciesmethods +build_config()andbuild(strict=True).FlowDoctor.builder(flow_name)classmethod is the recommended new entry point;flow_doctor.init(config_path=...)still works unchanged.Proposal 2 — Protocol + contextvars + async + testing plugin
flow_doctor/_protocol.py—@runtime_checkable FlowDoctorProtocoldeclaringreport() / guard() / monitor() / report_async()as the cross-version public contract. Consumers type-hint against the Protocol; mypy/isinstance verifies swap-ins.flow_doctor/core/_context.py— per-task/-thread contextvars forflow_name + stage + extras, exposed viaflow_doctor.context(). Inner scopes shadow outer; the active snapshot merges into every report's context at_build_context()time. Deep call-stacks no longer threadcontext=...explicitly.FlowDoctor.report_async()— async coroutine running the existing sync pipeline viaasyncio.to_thread(). Contextvars inherit across the thread boundary automatically.flow_doctor/testing/_recording.py—RecordingFlowDoctorimplementingFlowDoctorProtocol+ReportedIncidentdataclass with.clear() / .last / .of_type(exc_name)ergonomic helpers.flow_doctor/testing/_plugin.py—flow_doctor_recorderpytest fixture, auto-discovered via[project.entry-points.pytest11]. Downstreamspip install flow-doctorand the fixture is available in any test file with no imports.Morning-signal cutover example (from the plan)
Downstream consumer test (no imports required):
Back-compat
flow_doctor.init(config_path=...)— unchanged. Alpha-engine, the 0.4.0 consumer, doesn't move.NotifyChannelConfig— kept as the internal lingua franca that_init_notifiersconsumes; typed configs convert via.to_channel_config().load_config()— accepts already-constructed Pydantic instances in addition to dicts (so the builder hands typed sub-configs straight down without re-serializing).Not in this PR (planned follow-ups)
pydantic-settingsdep) — deferred so the dep trade-off can be decided separately.@deprecatedmarkers oninit()+NotifyChannelConfig— Proposal 3.py.typedmarker + OTel mapping doc — Proposal 3.Test plan
pytest tests/→ 335/335 pass (296 baseline + 39 new across builder, Protocol/context, testing-plugin).TypeAdapter,ValidationError), list→CSV recipients normalization,Protocolruntime satisfaction forFlowDoctorand arbitrary stubs, contextvars nested-scope shadowing + post-exit reset, contextvar inheritance intoreport_async's worker thread,RecordingFlowDoctorergonomics, pytest11 entry-point wiring.🤖 Generated with Claude Code