Skip to content

Public API Compatibility

nats-sinks is intended to be imported by external Python projects as well as run from the command line. That means the documented import paths are part of the user experience, not an internal implementation detail.

Public API compatibility tests protect those import paths before every release. They make refactoring safer by allowing maintainers to move internal code while keeping the external contract stable.

What The Public API Contract Protects

The compatibility contract covers symbols that users are encouraged to import from their own projects, extension modules, tests, or operational tooling.

The most important top-level imports are:

from nats_sinks import FanoutSink, JetStreamSinkRunner, NatsEnvelope, Sink
from nats_sinks.file import FileSink
from nats_sinks.mysql import MySqlSink
from nats_sinks.oracle import OracleSink
from nats_sinks.s3 import S3Sink
from nats_sinks.sinks import SinkConnector, SinkRegistry

These imports are intentionally stable. A user should not need to know whether JetStreamSinkRunner lives internally in nats_sinks.core.runner, whether the file sink stores helper code in a mapping module, or whether the Oracle sink has several internal SQL builders. The package-level imports are the supported surface.

The tests also cover:

  • framework error classes such as TemporarySinkError, PermanentSinkError, ConfigurationError, AckError, and DeadLetterError,
  • payload encryption helpers such as EncryptionConfig, PayloadEncryptor, SubjectPayloadEncryptor, PayloadKeyRegistry, and decrypt_payload,
  • message authenticity helpers such as MessageAuthenticityConfig, MessageAuthenticityRuleConfig, canonical_message_authenticity_bytes, canonical_message_authenticity_document, hmac_sha256_signature_b64, and evaluate_message_authenticity,
  • custody helpers such as CustodyConfig, compute_custody_metadata, attach_custody_metadata, and canonical_json_bytes,
  • durable consumer-management helpers such as ConsumerManagementConfig, ensure_jetstream_consumer, detect_consumer_drift, and build_consumer_config, including the richer durable pull-consumer policy fields for filter subjects, BackOff, replicas, memory storage, and metadata,
  • push-consumer guardrail helpers such as PushConsumerConfig, ensure_jetstream_push_consumer, detect_push_consumer_capabilities, and build_push_consumer_config for explicit manual-ACK push mode,
  • JetStream advisory helpers such as JetStreamAdvisoryConfig, JetStreamAdvisoryMonitor, parse_jetstream_advisory, and observe_jetstream_advisory_message,
  • message metadata configuration classes,
  • pre-sink policy configuration and evaluation helpers such as PreSinkPolicyConfig, PreSinkPolicyRuleConfig, PolicyViolationError, and evaluate_pre_sink_policy,
  • routing and ACK-gate helpers such as RoutingMatchPolicyConfig, RouteTargetConfig, select_route_targets, and wait_for_fanout_ack_gate,
  • the production fan-out orchestration sink FanoutSink,
  • payload normalization helpers,
  • metrics classes and helpers such as MetricNames, InMemoryMetrics, JsonFileMetrics, load_metrics_snapshot, and metric_rows_from_snapshot,
  • observability policy and connector helpers such as ObservabilityPolicy, PrometheusTextfilePolicy, PrometheusHttpEndpointPolicy, OtlpMetricsPolicy, ElasticObservabilityPolicy, GrafanaAlloyObservabilityPolicy, SplunkHecObservabilityPolicy, StatsdObservabilityPolicy, SyslogObservabilityPolicy, NatsServerMonitoringPolicy, SubjectAwareObservabilityPolicy, SubjectAwareRule, SubjectAwareDecision, evaluate_subject_observability_policy, SubjectFamilyAggregationResult, aggregate_subject_family_counter, attach_labeled_metric_rows, collect_nats_monitoring_snapshot, render_otlp_metrics_json, export_otlp_metrics, render_elastic_otlp_metrics_json, export_elastic_observability_metrics, and render_grafana_alloy_otlp_metrics_json, export_grafana_alloy_metrics, render_grafana_alloy_config, render_splunk_hec_event_json, export_splunk_hec_metrics, render_statsd_lines, export_statsd_metrics, render_datadog_lines, export_datadog_metrics, render_syslog_messages, export_syslog_metrics, and render_nats_monitoring_prometheus,
  • sink extension points such as Sink, HealthCheckableSink, SchemaAwareSink, FlushableSink, SinkRegistry, SinkConnector, load_entry_point_connectors, and normalize_connector_name,
  • testing helpers such as run_subject_observability_certification for reusable subject-aware observability release evidence,
  • production sink package exports for nats_sinks.file, nats_sinks.mysql, nats_sinks.oracle, and nats_sinks.s3,
  • documented configuration helpers such as load_config and redacted_config,
  • command entry points for nats-sink, nats-sink-metrics, and nats-sink-observe.

Why This Matters

Without compatibility tests, an internal cleanup can accidentally break users. For example, moving a class from one module to another might still pass the unit tests for that class, but fail a real user application that imports the documented path.

The compatibility tests catch that kind of problem early.

flowchart LR
    Docs[README and docs examples] --> Contract[Public API contract]
    PyPI[PyPI package metadata] --> Contract
    Contract --> Tests[tests/unit/test_public_api.py]
    Tests --> CI[CI and release validation]
    CI --> Users[External Python projects]

The tests do not freeze every internal module. They protect the parts that are documented as stable. Internal helpers can still evolve when needed, as long as the documented import paths and command names remain available.

Compatibility Test File

The contract lives in:

tests/unit/test_public_api.py

That test file contains a manifest of supported module exports and documented imports. It checks three things:

  1. Each documented symbol can be imported.
  2. Stable public packages expose those symbols through __all__.
  3. The console script names in pyproject.toml still resolve to Typer apps.

Run the focused test with:

pytest tests/unit/test_public_api.py

The same test is also included in the normal test suite and the repository check script:

scripts/check.sh

Adding A New Public Symbol

When a feature should become part of the supported Python API, update the code, documentation, and compatibility test together.

For example, the first-party Oracle MySQL sink exposes this stable public import:

from nats_sinks.mysql import MySqlSink

When adding another first-party sink, use the same release-ready checklist:

  1. src/nats_sinks/mysql/__init__.py exporting MySqlSink.
  2. Documentation showing the import.
  3. A tests/unit/test_public_api.py contract entry for nats_sinks.mysql.
  4. Changelog text explaining the new public API.
  5. Sink-specific unit tests and integration tests behind the appropriate markers.
  6. A SinkConnector descriptor registered in the explicit sink registry.

This keeps new sinks additive. Existing imports such as from nats_sinks.oracle import OracleSink and from nats_sinks.file import FileSink should continue to work.

Third-party connector packages should normally expose a SinkConnector descriptor through the nats_sinks.sinks entry-point group. The descriptor API is part of the public extension surface, but plugin discovery remains disabled by default and allow-list based. See Sink Framework for the connector descriptor and certification requirements.

Sink certification helpers are also documented public imports for maintainers and connector authors:

from nats_sinks.testing import (
    SinkCertificationCase,
    certification_envelope,
    certify_sink_write_success,
)

These helpers are release-tested so future sink packages can share the same baseline evidence without copying internal test code. See Sink Certification.

Subject-aware observability certification helpers are also part of the documented testing surface:

from nats_sinks.testing import run_subject_observability_certification


def test_subject_observability_contract() -> None:
    report = run_subject_observability_certification()

    assert report.raw_subject_leaks == ()
    assert report.delivery_probe_before == report.delivery_probe_after

These helpers use synthetic subjects and prepared labeled_metrics rows to prove connector behavior without exposing real subject names. See Subject-Aware Observability Runbook.

Breaking Changes

A breaking change is any change that removes, renames, or changes the meaning of a documented public import, documented configuration field, console command, or delivery safety invariant.

Breaking changes should be avoided while the project is still building trust with users. If one becomes necessary, it should be handled deliberately:

  • document the reason,
  • provide a migration path,
  • update the changelog,
  • update tests,
  • consider a compatibility alias or deprecation period,
  • use semantic versioning to communicate the impact.

The commit-then-acknowledge invariant is not a compatibility option. It is a project safety rule. A future version must not turn ACK-before-durable-success into supported behavior.

What Is Not Public API

Some modules exist so the package can stay maintainable, but they are not intended as stable imports unless a documentation page says otherwise.

Examples include low-level SQL builders, row mappers, private CLI helper functions, and destination-specific implementation details. External users should prefer the documented package imports, sink classes, configuration models, and CLI commands.

If a user needs a lower-level helper to become stable, it should be promoted intentionally: document it, test it in the compatibility contract, and describe it in the changelog.