Skip to content

Commit c55fbbe

Browse files
darynaishchenkooctavia-squidington-iii
and
octavia-squidington-iii
authored
feat(low-code): added condition to TypesMap of DynamicSchemaLoader (#224)
Co-authored-by: octavia-squidington-iii <contact@airbyte.com>
1 parent d08f1ae commit c55fbbe

File tree

5 files changed

+114
-4
lines changed

5 files changed

+114
-4
lines changed

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,10 @@ definitions:
17881788
- type: array
17891789
items:
17901790
type: string
1791+
condition:
1792+
type: string
1793+
interpolation_context:
1794+
- raw_schema
17911795
SchemaTypeIdentifier:
17921796
title: Schema Type Identifier
17931797
description: (This component is experimental. Use at your own risk.) Identifies schema details for dynamic schema extraction and processing.

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

+1
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ class HttpResponseFilter(BaseModel):
719719
class TypesMap(BaseModel):
720720
target_type: Union[str, List[str]]
721721
current_type: Union[str, List[str]]
722+
condition: Optional[str]
722723

723724

724725
class SchemaTypeIdentifier(BaseModel):

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1696,7 +1696,11 @@ def create_inline_schema_loader(
16961696

16971697
@staticmethod
16981698
def create_types_map(model: TypesMapModel, **kwargs: Any) -> TypesMap:
1699-
return TypesMap(target_type=model.target_type, current_type=model.current_type)
1699+
return TypesMap(
1700+
target_type=model.target_type,
1701+
current_type=model.current_type,
1702+
condition=model.condition if model.condition is not None else "True",
1703+
)
17001704

17011705
def create_schema_type_identifier(
17021706
self, model: SchemaTypeIdentifierModel, config: Config, **kwargs: Any

airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import dpath
1111
from typing_extensions import deprecated
1212

13+
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
1314
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
1415
from airbyte_cdk.sources.declarative.retrievers.retriever import Retriever
1516
from airbyte_cdk.sources.declarative.schema.schema_loader import SchemaLoader
@@ -53,6 +54,7 @@ class TypesMap:
5354

5455
target_type: Union[List[str], str]
5556
current_type: Union[List[str], str]
57+
condition: Optional[str]
5658

5759

5860
@deprecated("This class is experimental. Use at your own risk.", category=ExperimentalClassWarning)
@@ -177,7 +179,7 @@ def _get_type(
177179
if field_type_path
178180
else "string"
179181
)
180-
mapped_field_type = self._replace_type_if_not_valid(raw_field_type)
182+
mapped_field_type = self._replace_type_if_not_valid(raw_field_type, raw_schema)
181183
if (
182184
isinstance(mapped_field_type, list)
183185
and len(mapped_field_type) == 2
@@ -194,14 +196,22 @@ def _get_type(
194196
)
195197

196198
def _replace_type_if_not_valid(
197-
self, field_type: Union[List[str], str]
199+
self,
200+
field_type: Union[List[str], str],
201+
raw_schema: MutableMapping[str, Any],
198202
) -> Union[List[str], str]:
199203
"""
200204
Replaces a field type if it matches a type mapping in `types_map`.
201205
"""
202206
if self.schema_type_identifier.types_mapping:
203207
for types_map in self.schema_type_identifier.types_mapping:
204-
if field_type == types_map.current_type:
208+
# conditional is optional param, setting to true if not provided
209+
condition = InterpolatedBoolean(
210+
condition=types_map.condition if types_map.condition is not None else "True",
211+
parameters={},
212+
).eval(config=self.config, raw_schema=raw_schema)
213+
214+
if field_type == types_map.current_type and condition:
205215
return types_map.target_type
206216
return field_type
207217

unit_tests/sources/declarative/schema/test_dynamic_schema_loader.py

+91
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44

55
import json
6+
from copy import deepcopy
67
from unittest.mock import MagicMock
78

89
import pytest
@@ -286,3 +287,93 @@ def test_dynamic_schema_loader_manifest_flow():
286287

287288
assert len(actual_catalog.streams) == 1
288289
assert actual_catalog.streams[0].json_schema == expected_schema
290+
291+
292+
def test_dynamic_schema_loader_with_type_conditions():
293+
_MANIFEST_WITH_TYPE_CONDITIONS = deepcopy(_MANIFEST)
294+
_MANIFEST_WITH_TYPE_CONDITIONS["definitions"]["party_members_stream"]["schema_loader"][
295+
"schema_type_identifier"
296+
]["types_mapping"].append(
297+
{
298+
"target_type": "number",
299+
"current_type": "formula",
300+
"condition": "{{ raw_schema['result']['type'] == 'number' }}",
301+
}
302+
)
303+
_MANIFEST_WITH_TYPE_CONDITIONS["definitions"]["party_members_stream"]["schema_loader"][
304+
"schema_type_identifier"
305+
]["types_mapping"].append(
306+
{
307+
"target_type": "number",
308+
"current_type": "formula",
309+
"condition": "{{ raw_schema['result']['type'] == 'currency' }}",
310+
}
311+
)
312+
_MANIFEST_WITH_TYPE_CONDITIONS["definitions"]["party_members_stream"]["schema_loader"][
313+
"schema_type_identifier"
314+
]["types_mapping"].append({"target_type": "array", "current_type": "formula"})
315+
316+
expected_schema = {
317+
"$schema": "http://json-schema.org/draft-07/schema#",
318+
"type": "object",
319+
"properties": {
320+
"id": {"type": ["null", "integer"]},
321+
"first_name": {"type": ["null", "string"]},
322+
"description": {"type": ["null", "string"]},
323+
"static_field": {"type": ["null", "string"]},
324+
"currency": {"type": ["null", "number"]},
325+
"salary": {"type": ["null", "number"]},
326+
"working_days": {"type": ["null", "array"]},
327+
},
328+
}
329+
source = ConcurrentDeclarativeSource(
330+
source_config=_MANIFEST_WITH_TYPE_CONDITIONS, config=_CONFIG, catalog=None, state=None
331+
)
332+
with HttpMocker() as http_mocker:
333+
http_mocker.get(
334+
HttpRequest(url="https://api.test.com/party_members"),
335+
HttpResponse(
336+
body=json.dumps(
337+
[
338+
{
339+
"id": 1,
340+
"first_name": "member_1",
341+
"description": "First member",
342+
"salary": 20000,
343+
"currency": 10.4,
344+
"working_days": ["Monday", "Tuesday"],
345+
},
346+
{
347+
"id": 2,
348+
"first_name": "member_2",
349+
"description": "Second member",
350+
"salary": 22000,
351+
"currency": 10.4,
352+
"working_days": ["Tuesday", "Wednesday"],
353+
},
354+
]
355+
)
356+
),
357+
)
358+
http_mocker.get(
359+
HttpRequest(url="https://api.test.com/party_members/schema"),
360+
HttpResponse(
361+
body=json.dumps(
362+
{
363+
"fields": [
364+
{"name": "Id", "type": "integer"},
365+
{"name": "FirstName", "type": "string"},
366+
{"name": "Description", "type": "singleLineText"},
367+
{"name": "Salary", "type": "formula", "result": {"type": "number"}},
368+
{"name": "Currency", "type": "formula", "result": {"type": "currency"}},
369+
{"name": "WorkingDays", "type": "formula"},
370+
]
371+
}
372+
)
373+
),
374+
)
375+
376+
actual_catalog = source.discover(logger=source.logger, config=_CONFIG)
377+
378+
assert len(actual_catalog.streams) == 1
379+
assert actual_catalog.streams[0].json_schema == expected_schema

0 commit comments

Comments
 (0)