Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
cdd1ac9
feat: skip config validation during discovery for sources with Dynami…
devin-ai-integration[bot] Apr 8, 2025
7490ad1
style: fix formatting issues
devin-ai-integration[bot] Apr 8, 2025
9e84e1c
fix: update entrypoint to make --config optional for discovery
devin-ai-integration[bot] Apr 8, 2025
47bd67c
style: fix formatting issues
devin-ai-integration[bot] Apr 8, 2025
36d7f1f
fix: add type annotation for empty_config
devin-ai-integration[bot] Apr 8, 2025
b002218
refactor: use generator comprehension instead of list comprehension
devin-ai-integration[bot] Apr 8, 2025
acbab7e
Update airbyte_cdk/entrypoint.py
aaronsteers Apr 8, 2025
d33dcdd
Update CHANGELOG.md
aaronsteers Apr 8, 2025
4253f28
Update airbyte_cdk/sources/declarative/manifest_declarative_source.py
aaronsteers Apr 8, 2025
64610b9
feat: add check_config_during_discover flag for targeted config valid…
devin-ai-integration[bot] Apr 8, 2025
b228857
style: fix formatting issues
devin-ai-integration[bot] Apr 8, 2025
77772c3
Update CHANGELOG.md
aaronsteers Apr 8, 2025
6ca213c
refactor: push check_config_during_discover flag into connector base …
devin-ai-integration[bot] Apr 8, 2025
dce4f8c
style: fix formatting issues
devin-ai-integration[bot] Apr 8, 2025
24a0919
fix: resolve MyPy type checking issues with check_config_during_disco…
devin-ai-integration[bot] Apr 9, 2025
f920f04
refactor: move check_config_during_discover to BaseConnector class
devin-ai-integration[bot] Apr 9, 2025
f01525f
Update airbyte_cdk/connector.py
aaronsteers Apr 9, 2025
769d361
Update airbyte_cdk/connector.py
aaronsteers Apr 9, 2025
c3cbad8
Update airbyte_cdk/sources/declarative/manifest_declarative_source.py
aaronsteers Apr 9, 2025
3cb8faf
Auto-fix lint and format issues
Apr 9, 2025
08397ad
fix condition direction
aaronsteers Apr 9, 2025
4066c75
fix direction of comparison
aaronsteers May 20, 2025
943ea41
Merge remote-tracking branch 'origin/main' into aj/feat/unprivileged-…
aaronsteers May 20, 2025
fac853c
fix config parse
aaronsteers May 20, 2025
9b37bc5
handle case of missing config
aaronsteers May 20, 2025
640c811
fix: don't assume bad config will fail discover
aaronsteers May 20, 2025
5894a1f
improve read check
aaronsteers May 20, 2025
a79ad83
quote the name
aaronsteers May 20, 2025
46170ad
Auto-fix lint and format issues
May 20, 2025
c8dd910
default to not checking before discover
aaronsteers May 20, 2025
2ce3b99
don't raise on missing error in helper function
aaronsteers May 20, 2025
dd94e86
improve tests
aaronsteers May 20, 2025
451ce82
Merge branch 'main' into aj/feat/unprivileged-discover-for-declarativ…
aaronsteers Jul 18, 2025
59353af
Auto-commit Resolving dependencies... changes
Jul 18, 2025
e58bdd3
Apply suggestion from @coderabbitai[bot]
aaronsteers Jul 18, 2025
bfaf62f
Apply suggestion from @coderabbitai[bot]
aaronsteers Jul 18, 2025
b1cd8af
Apply suggestion from @coderabbitai[bot]
aaronsteers Jul 18, 2025
f87c884
Apply suggestion from @coderabbitai[bot]
aaronsteers Jul 18, 2025
f3b6928
Apply suggestion from @coderabbitai[bot]
aaronsteers Jul 18, 2025
b956a30
Apply suggestion from @aaronsteers
aaronsteers Jul 18, 2025
6cf6e1c
fix: Add missing config parameter in _uses_dynamic_schema_loader method
devin-ai-integration[bot] Jul 18, 2025
49d571e
fix: Resolve MyPy error and update test plan with successful results
devin-ai-integration[bot] Jul 18, 2025
6319699
fix: Add missing blank line for ruff formatting compliance
devin-ai-integration[bot] Jul 18, 2025
3798fb4
fix: Resolve Pydantic frozen instance error in connector_base.py
devin-ai-integration[bot] Jul 18, 2025
ff14901
Apply suggestion from @aaronsteers
aaronsteers Jul 18, 2025
2345164
selective import from 'origin/devin/1752808596-manual-tests-static-sc…
aaronsteers Jul 18, 2025
3245f0f
catch up with changes on main
aaronsteers Jul 18, 2025
6748db0
add exception arg
aaronsteers Jul 18, 2025
e27697e
remove unused import
aaronsteers Jul 18, 2025
a1760e1
Merge main into aj/feat/unprivileged-discover-for-declarative-static-…
devin-ai-integration[bot] Oct 31, 2025
a69c252
fix: Update test imports to use legacy module location
devin-ai-integration[bot] Oct 31, 2025
7048b30
feat: Add unprivileged discover support for declarative sources with …
devin-ai-integration[bot] Oct 31, 2025
711635f
Potential fix for pull request finding 'Unused import'
aaronsteers Oct 31, 2025
88c59f3
Merge remote-tracking branch 'origin/main' into aj/feat/unprivileged-…
devin-ai-integration[bot] Oct 19, 2026
6f519c7
fix: tighten config-free discover detection and errors after main merge
devin-ai-integration[bot] Oct 19, 2026
991c972
fix: keep spec validation for configured discover; require config for…
devin-ai-integration[bot] Oct 19, 2026
784a5f9
fix: only require --config for discover when the config is missing
devin-ai-integration[bot] Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions airbyte_cdk/cli/source_declarative_manifest/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
AirbyteTraceMessage,
ConfiguredAirbyteCatalog,
ConnectorSpecificationSerializer,
FailureType,
TraceType,
Type,
)
Expand Down Expand Up @@ -108,6 +109,9 @@ def _get_local_yaml_source(args: list[str]) -> SourceLocalYaml:
config_path=parsed_args.config if hasattr(parsed_args, "config") else None,
)
except Exception as error:
is_config_free_discover = args[:1] == ["discover"] and not any(
arg.startswith("--config") for arg in args
)
print(
orjson.dumps(
AirbyteMessageSerializer.dump(
Expand All @@ -117,8 +121,15 @@ def _get_local_yaml_source(args: list[str]) -> SourceLocalYaml:
type=TraceType.ERROR,
emitted_at=ab_datetime_now().to_epoch_millis(),
error=AirbyteErrorTraceMessage(
message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}",
message=(
f"This connector could not run `discover` without a config. Provide one with `--config`. Error: {error}"
if is_config_free_discover
else f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}"
),
stack_trace=traceback.format_exc(),
failure_type=FailureType.config_error
if is_config_free_discover
else None,
),
),
)
Expand Down Expand Up @@ -222,6 +233,9 @@ def create_declarative_source(
custom_components_trusted=False,
)
except Exception as error:
is_config_free_discover = args[:1] == ["discover"] and not any(
arg.startswith("--config") for arg in args
)
print(
orjson.dumps(
AirbyteMessageSerializer.dump(
Expand All @@ -231,8 +245,15 @@ def create_declarative_source(
type=TraceType.ERROR,
emitted_at=ab_datetime_now().to_epoch_millis(),
error=AirbyteErrorTraceMessage(
message=f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}",
message=(
f"This connector could not run `discover` without a config. Provide one with `--config`. Error: {error}"
if is_config_free_discover
else f"Error starting the sync. This could be due to an invalid configuration or catalog. Please contact Support for assistance. Error: {error}"
),
stack_trace=traceback.format_exc(),
failure_type=FailureType.config_error
if is_config_free_discover
else None,
),
),
)
Expand All @@ -251,8 +272,8 @@ def _parse_inputs_into_config_catalog_state(
]:
config = (
ConcurrentDeclarativeSource.read_config(parsed_args.config)
if hasattr(parsed_args, "config")
else None
if hasattr(parsed_args, "config") and parsed_args.config
else {}
)
catalog = (
ConcurrentDeclarativeSource.read_catalog(parsed_args.catalog)
Expand Down
9 changes: 8 additions & 1 deletion airbyte_cdk/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,15 @@ def load_optional_package_file(package: str, filename: str) -> Optional[bytes]:


class BaseConnector(ABC, Generic[TConfig]):
# configure whether the `check_config_against_spec_or_exit()` needs to be called
check_config_against_spec: bool = True
"""Configure whether `check_config_against_spec_or_exit()` needs to be called."""

check_config_during_discover: bool = True
"""Whether `discover` requires a config and validates it against the spec.

Set to `False` for sources that can build their catalog without a config (and therefore
without credentials), which allows `discover` to run without `--config`.
"""

@abstractmethod
def configure(self, config: Mapping[str, Any], temp_dir: str) -> TConfig:
Expand Down
52 changes: 45 additions & 7 deletions airbyte_cdk/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,11 @@ def parse_args(args: List[str]) -> argparse.Namespace:
help="outputs a catalog describing the source's schema",
parents=[parent_parser],
)
required_discover_parser = discover_parser.add_argument_group("required named arguments")
required_discover_parser.add_argument(
"--config", type=str, required=True, help="path to the json configuration file"
discover_parser.add_argument(
"--config",
type=str,
required=False,
help="path to the json configuration file (optional if the source supports config-free discover)",
)
discover_parser.add_argument(
"--manifest-path",
Expand Down Expand Up @@ -179,19 +181,38 @@ def run(self, parsed_args: argparse.Namespace) -> Iterable[str]:
)
if cmd == "spec":
message = AirbyteMessage(type=Type.SPEC, spec=source_spec)
yield from [
yield from (
self.airbyte_message_to_string(queued_message)
for queued_message in self._emit_queued_messages(self.source)
]
)
yield self.airbyte_message_to_string(message)
elif (
cmd == "discover"
and not parsed_args.config
and not self.source.check_config_during_discover
):
yield from (
self.airbyte_message_to_string(queued_message)
for queued_message in self._emit_queued_messages(self.source)
)
yield from map(
AirbyteEntrypoint.airbyte_message_to_string,
self._discover_without_config(),
)
elif cmd == "discover" and not parsed_args.config:
raise AirbyteTracedException(
message="This connector requires a config to run `discover`. Provide one with `--config`.",
internal_message="`--config` was not provided and the source does not support config-free discover.",
failure_type=FailureType.config_error,
)
Comment thread
aaronsteers marked this conversation as resolved.
else:
raw_config = self.source.read_config(parsed_args.config)
config = self.source.configure(raw_config, temp_dir)

yield from [
yield from (
self.airbyte_message_to_string(queued_message)
for queued_message in self._emit_queued_messages(self.source)
]
)
if cmd == "check":
yield from map(
AirbyteEntrypoint.airbyte_message_to_string,
Expand Down Expand Up @@ -267,6 +288,23 @@ def discover(
yield from self._emit_queued_messages(self.source)
yield AirbyteMessage(type=Type.CATALOG, catalog=catalog)

def _discover_without_config(self) -> Iterable[AirbyteMessage]:
"""Run `discover` with an empty config, for sources that support config-free discover."""
try:
catalog = self.source.discover(self.logger, {})
except AirbyteTracedException:
raise
except Exception as error:
raise AirbyteTracedException(
message="This connector could not run `discover` without a config. Provide one with `--config`.",
internal_message=f"Config-free discover failed: {error!r}",
failure_type=FailureType.config_error,
exception=error,
) from error

yield from self._emit_queued_messages(self.source)
yield AirbyteMessage(type=Type.CATALOG, catalog=catalog)

def read(
self, source_spec: ConnectorSpecification, config: TConfig, catalog: Any, state: list[Any]
) -> Iterable[AirbyteMessage]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ def __init__(
message_repository=self._message_repository,
)

self.check_config_during_discover = self._requires_config_for_discover()
Comment thread
aaronsteers marked this conversation as resolved.

def _pre_process_manifest(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
"""
Preprocesses the provided manifest dictionary by resolving any manifest references.
Expand Down Expand Up @@ -978,3 +980,35 @@ def _select_streams(
as_airbyte_message(configured_stream.stream, AirbyteStreamStatus.INCOMPLETE)
)
return abstract_streams

def _requires_config_for_discover(self) -> bool:
"""Return whether `discover` needs a config to build this source's catalog.

A config is required when the set of streams depends on the config (`ConditionalStreams`,
or `dynamic_streams` resolved from the API or the config rather than static parameters), or
when any stream schema is fetched from the API (`DynamicSchemaLoader`).
"""
for dynamic_definition in self._source_config.get("dynamic_streams", []):
resolver_type = dynamic_definition.get("components_resolver", {}).get("type")
if resolver_type != "ParametrizedComponentsResolver":
return True

stream_definitions = [
self._source_config.get("streams", []),
self._source_config.get("dynamic_streams", []),
]
return any(
_contains_component_type(stream_definitions, component_type)
for component_type in ("ConditionalStreams", "DynamicSchemaLoader")
)


def _contains_component_type(node: Any, component_type: str) -> bool:
"""Return whether any mapping nested under `node` has the given component `type`."""
if isinstance(node, Mapping):
return node.get("type") == component_type or any(
_contains_component_type(value, component_type) for value in node.values()
)
if isinstance(node, list):
return any(_contains_component_type(item, component_type) for item in node)
return False
Loading
Loading