Skip to content

Commit 0f780fd

Browse files
committed
[low-code connectors]: Assert there are no custom top-level fields (#15489)
* move components to definitions field * Also update the references * validate the top level fields and add version * raise exception on unknown fields * newline * unit tests * set version to 0.1.0 * newline
1 parent a3ee499 commit 0f780fd

File tree

6 files changed

+104
-38
lines changed

6 files changed

+104
-38
lines changed

airbyte-cdk/python/airbyte_cdk/sources/declarative/read_exception.py renamed to airbyte-cdk/python/airbyte_cdk/sources/declarative/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@ class ReadException(Exception):
77
"""
88
Raise when there is an error reading data from an API Source
99
"""
10+
11+
12+
class InvalidConnectorDefinitionException(Exception):
13+
"""
14+
Raise when the connector definition is invalid
15+
"""

airbyte-cdk/python/airbyte_cdk/sources/declarative/retrievers/simple_retriever.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import requests
99
from airbyte_cdk.models import SyncMode
10+
from airbyte_cdk.sources.declarative.exceptions import ReadException
1011
from airbyte_cdk.sources.declarative.extractors.http_selector import HttpSelector
11-
from airbyte_cdk.sources.declarative.read_exception import ReadException
1212
from airbyte_cdk.sources.declarative.requesters.error_handlers.response_action import ResponseAction
1313
from airbyte_cdk.sources.declarative.requesters.paginators.no_pagination import NoPagination
1414
from airbyte_cdk.sources.declarative.requesters.paginators.paginator import Paginator

airbyte-cdk/python/airbyte_cdk/sources/declarative/yaml_declarative_source.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from airbyte_cdk.sources.declarative.checks.connection_checker import ConnectionChecker
1010
from airbyte_cdk.sources.declarative.declarative_source import DeclarativeSource
11+
from airbyte_cdk.sources.declarative.exceptions import InvalidConnectorDefinitionException
1112
from airbyte_cdk.sources.declarative.parsers.factory import DeclarativeComponentFactory
1213
from airbyte_cdk.sources.declarative.parsers.yaml_parser import YamlParser
1314
from airbyte_cdk.sources.streams import Stream
@@ -16,6 +17,8 @@
1617
class YamlDeclarativeSource(DeclarativeSource):
1718
"""Declarative source defined by a yaml file"""
1819

20+
VALID_TOP_LEVEL_FIELDS = {"definitions", "streams", "check", "version"}
21+
1922
def __init__(self, path_to_yaml):
2023
"""
2124
:param path_to_yaml: Path to the yaml file describing the source
@@ -25,6 +28,11 @@ def __init__(self, path_to_yaml):
2528
self._path_to_yaml = path_to_yaml
2629
self._source_config = self._read_and_parse_yaml_file(path_to_yaml)
2730

31+
# Stopgap to protect the top-level namespace until it's validated through the schema
32+
unknown_fields = [key for key in self._source_config.keys() if key not in self.VALID_TOP_LEVEL_FIELDS]
33+
if unknown_fields:
34+
raise InvalidConnectorDefinitionException(f"Found unknown top-level fields: {unknown_fields}")
35+
2836
@property
2937
def connection_checker(self) -> ConnectionChecker:
3038
check = self._source_config["check"]

airbyte-cdk/python/unit_tests/sources/declarative/retrievers/test_simple_retriever.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
import requests
1010
from airbyte_cdk.models import SyncMode
11-
from airbyte_cdk.sources.declarative.read_exception import ReadException
11+
from airbyte_cdk.sources.declarative.exceptions import ReadException
1212
from airbyte_cdk.sources.declarative.requesters.error_handlers.response_action import ResponseAction
1313
from airbyte_cdk.sources.declarative.requesters.error_handlers.response_status import ResponseStatus
1414
from airbyte_cdk.sources.declarative.requesters.request_option import RequestOptionType
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#
2+
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import os
6+
import tempfile
7+
import unittest
8+
9+
from airbyte_cdk.sources.declarative.exceptions import InvalidConnectorDefinitionException
10+
from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
11+
12+
13+
class TestYamlDeclarativeSource(unittest.TestCase):
14+
def test_source_is_created_if_toplevel_fields_are_known(self):
15+
content = """
16+
version: "version"
17+
streams: "streams"
18+
check: "check"
19+
"""
20+
temporary_file = TestFileContent(content)
21+
YamlDeclarativeSource(temporary_file.filename)
22+
23+
def test_source_is_not_created_if_toplevel_fields_are_unknown(self):
24+
content = """
25+
version: "version"
26+
streams: "streams"
27+
check: "check"
28+
not_a_valid_field: "error"
29+
"""
30+
temporary_file = TestFileContent(content)
31+
with self.assertRaises(InvalidConnectorDefinitionException):
32+
YamlDeclarativeSource(temporary_file.filename)
33+
34+
35+
class TestFileContent:
36+
def __init__(self, content):
37+
self.file = tempfile.NamedTemporaryFile(mode="w", delete=False)
38+
39+
with self.file as f:
40+
f.write(content)
41+
42+
@property
43+
def filename(self):
44+
return self.file.name
45+
46+
def __enter__(self):
47+
return self
48+
49+
def __exit__(self, type, value, traceback):
50+
os.unlink(self.filename)
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,44 @@
1-
schema_loader:
2-
type: JsonSchema
3-
file_path: "./source_{{snakeCase name}}/schemas/\{{ options['name'] }}.json"
4-
selector:
5-
type: RecordSelector
6-
extractor:
7-
type: JelloExtractor
8-
transform: "_"
9-
requester:
10-
type: HttpRequester
11-
name: "\{{ options['name'] }}"
12-
http_method: "GET"
13-
authenticator:
14-
type: BearerAuthenticator
15-
api_token: "\{{ config['api_key'] }}"
16-
retriever:
17-
type: SimpleRetriever
18-
$options:
19-
url_base: TODO "your_api_base_url"
20-
name: "\{{ options['name'] }}"
21-
primary_key: "\{{ options['primary_key'] }}"
22-
record_selector:
23-
$ref: "*ref(selector)"
24-
paginator:
25-
type: NoPagination
26-
customers_stream:
27-
type: DeclarativeStream
28-
$options:
29-
name: "customers"
30-
primary_key: "id"
1+
version: "0.1.0"
2+
3+
definitions:
314
schema_loader:
32-
$ref: "*ref(schema_loader)"
5+
type: JsonSchema
6+
file_path: "./source_{{snakeCase name}}/schemas/\{{ options['name'] }}.json"
7+
selector:
8+
type: RecordSelector
9+
extractor:
10+
type: JelloExtractor
11+
transform: "_"
12+
requester:
13+
type: HttpRequester
14+
name: "\{{ options['name'] }}"
15+
http_method: "GET"
16+
authenticator:
17+
type: BearerAuthenticator
18+
api_token: "\{{ config['api_key'] }}"
3319
retriever:
34-
$ref: "*ref(retriever)"
35-
requester:
36-
$ref: "*ref(requester)"
37-
path: TODO "your_endpoint_path"
20+
type: SimpleRetriever
21+
$options:
22+
url_base: TODO "your_api_base_url"
23+
name: "\{{ options['name'] }}"
24+
primary_key: "\{{ options['primary_key'] }}"
25+
record_selector:
26+
$ref: "*ref(definitions.selector)"
27+
paginator:
28+
type: NoPagination
29+
3830
streams:
39-
- "*ref(customers_stream)"
31+
- type: DeclarativeStream
32+
$options:
33+
name: "customers"
34+
primary_key: "id"
35+
schema_loader:
36+
$ref: "*ref(definitions.schema_loader)"
37+
retriever:
38+
$ref: "*ref(definitions.retriever)"
39+
requester:
40+
$ref: "*ref(definitions.requester)"
41+
path: TODO "your_endpoint_path"
4042
check:
4143
type: CheckStream
4244
stream_names: ["customers"]

0 commit comments

Comments
 (0)