197197 ],
198198}
199199
200-
200+ _MANIFEST_WITH_HTTP_COMPONENT_RESOLVER_WITH_RETRIEVER_WITH_PARENT_STREAM = {
201+ "version" : "6.7.0" ,
202+ "type" : "DeclarativeSource" ,
203+ "check" : {"type" : "CheckStream" , "stream_names" : ["Rates" ]},
204+ "dynamic_streams" : [
205+ {
206+ "type" : "DynamicDeclarativeStream" ,
207+ "stream_template" : {
208+ "type" : "DeclarativeStream" ,
209+ "name" : "" ,
210+ "primary_key" : [],
211+ "schema_loader" : {
212+ "type" : "InlineSchemaLoader" ,
213+ "schema" : {
214+ "$schema" : "http://json-schema.org/schema#" ,
215+ "properties" : {
216+ "ABC" : {"type" : "number" },
217+ "AED" : {"type" : "number" },
218+ },
219+ "type" : "object" ,
220+ },
221+ },
222+ "retriever" : {
223+ "type" : "SimpleRetriever" ,
224+ "requester" : {
225+ "type" : "HttpRequester" ,
226+ "url_base" : "https://api.test.com" ,
227+ "path" : "" ,
228+ "http_method" : "GET" ,
229+ "authenticator" : {
230+ "type" : "ApiKeyAuthenticator" ,
231+ "header" : "apikey" ,
232+ "api_token" : "{{ config['api_key'] }}" ,
233+ },
234+ },
235+ "record_selector" : {
236+ "type" : "RecordSelector" ,
237+ "extractor" : {"type" : "DpathExtractor" , "field_path" : []},
238+ },
239+ "paginator" : {"type" : "NoPagination" },
240+ },
241+ },
242+ "components_resolver" : {
243+ "type" : "HttpComponentsResolver" ,
244+ "retriever" : {
245+ "type" : "SimpleRetriever" ,
246+ "requester" : {
247+ "type" : "HttpRequester" ,
248+ "url_base" : "https://api.test.com" ,
249+ "path" : "parent/{{ stream_partition.parent_id }}/items" ,
250+ "http_method" : "GET" ,
251+ "authenticator" : {
252+ "type" : "ApiKeyAuthenticator" ,
253+ "header" : "apikey" ,
254+ "api_token" : "{{ config['api_key'] }}" ,
255+ },
256+ },
257+ "record_selector" : {
258+ "type" : "RecordSelector" ,
259+ "extractor" : {"type" : "DpathExtractor" , "field_path" : []},
260+ },
261+ "paginator" : {"type" : "NoPagination" },
262+ "partition_router" : {
263+ "type" : "SubstreamPartitionRouter" ,
264+ "parent_stream_configs" : [
265+ {
266+ "type" : "ParentStreamConfig" ,
267+ "parent_key" : "id" ,
268+ "partition_field" : "parent_id" ,
269+ "stream" : {
270+ "type" : "DeclarativeStream" ,
271+ "name" : "parent" ,
272+ "retriever" : {
273+ "type" : "SimpleRetriever" ,
274+ "requester" : {
275+ "type" : "HttpRequester" ,
276+ "url_base" : "https://api.test.com" ,
277+ "path" : "/parents" ,
278+ "http_method" : "GET" ,
279+ "authenticator" : {
280+ "type" : "ApiKeyAuthenticator" ,
281+ "header" : "apikey" ,
282+ "api_token" : "{{ config['api_key'] }}" ,
283+ },
284+ },
285+ "record_selector" : {
286+ "type" : "RecordSelector" ,
287+ "extractor" : {"type" : "DpathExtractor" , "field_path" : []},
288+ },
289+ },
290+ "schema_loader" : {
291+ "type" : "InlineSchemaLoader" ,
292+ "schema" : {
293+ "$schema" : "http://json-schema.org/schema#" ,
294+ "properties" : {
295+ "id" : {"type" : "integer" }
296+ },
297+ "type" : "object" ,
298+ },
299+ },
300+ }
301+ }
302+ ]
303+ }
304+ },
305+ "components_mapping" : [
306+ {
307+ "type" : "ComponentMappingDefinition" ,
308+ "field_path" : ["name" ],
309+ "value" : "parent_{{stream_slice['parent_id']}}_{{components_values['name']}}" ,
310+ },
311+ {
312+ "type" : "ComponentMappingDefinition" ,
313+ "field_path" : [
314+ "retriever" ,
315+ "requester" ,
316+ "path" ,
317+ ],
318+ "value" : "{{ stream_slice['parent_id'] }}/{{ components_values['id'] }}" ,
319+ },
320+ ],
321+ },
322+ }
323+ ],
324+ }
201325@pytest .mark .parametrize (
202326 "components_mapping, retriever_data, stream_template_config, expected_result" ,
203327 [
@@ -234,6 +358,41 @@ def test_http_components_resolver(
234358 result = list (resolver .resolve_components (stream_template_config = stream_template_config ))
235359 assert result == expected_result
236360
361+ @pytest .mark .parametrize (
362+ "components_mapping, retriever_data, stream_template_config, expected_result" ,
363+ [
364+ (
365+ [
366+ ComponentMappingDefinition (
367+ field_path = [InterpolatedString .create ("path" , parameters = {})],
368+ value = "{{stream_slice['parent_id']}}/{{components_values['id']}}" ,
369+ value_type = str ,
370+ parameters = {},
371+ )
372+ ],
373+ [{"id" : "1" , "field1" : "data1" }, {"id" : "2" , "field1" : "data2" }],
374+ {"path" : None },
375+ [{"path" : "1/1" }, {"path" : "1/2" }, {"path" : "2/1" }, {"path" : "2/2" }],
376+ )
377+ ],
378+ )
379+ def test_http_components_resolver_with_stream_slices (
380+ components_mapping , retriever_data , stream_template_config , expected_result
381+ ):
382+ mock_retriever = MagicMock ()
383+ mock_retriever .read_records .return_value = retriever_data
384+ mock_retriever .stream_slices .return_value = [{"parent_id" : 1 }, {"parent_id" : 2 }]
385+ config = {}
386+
387+ resolver = HttpComponentsResolver (
388+ retriever = mock_retriever ,
389+ config = config ,
390+ components_mapping = components_mapping ,
391+ parameters = {},
392+ )
393+
394+ result = list (resolver .resolve_components (stream_template_config = stream_template_config ))
395+ assert result == expected_result
237396
238397def test_dynamic_streams_read_with_http_components_resolver ():
239398 expected_stream_names = ["item_1" , "item_2" ]
@@ -306,3 +465,59 @@ def test_duplicated_dynamic_streams_read_with_http_components_resolver():
306465 str (exc_info .value )
307466 == "Dynamic streams list contains a duplicate name: item_2. Please contact Airbyte Support."
308467 )
468+
469+ def test_dynamic_streams_with_http_components_resolver_retriever_with_parent_stream ():
470+ expected_stream_names = [
471+ "parent_1_item_1" , "parent_1_item_2" , "parent_2_item_1" , "parent_2_item_2"
472+ ]
473+ with HttpMocker () as http_mocker :
474+ http_mocker .get (
475+ HttpRequest (url = "https://api.test.com/parents" ),
476+ HttpResponse (
477+ body = json .dumps ([{"id" : 1 }, {"id" : 2 }])
478+ ),
479+ )
480+ parent_ids = [1 , 2 ]
481+ for parent_id in parent_ids :
482+ http_mocker .get (
483+ HttpRequest (url = f"https://api.test.com/parent/{ parent_id } /items" ),
484+ HttpResponse (
485+ body = json .dumps (
486+ [
487+ {"id" : 1 , "name" : "item_1" },
488+ {"id" : 2 , "name" : "item_2" },
489+ ]
490+ )
491+ ),
492+ )
493+ dynamic_stream_paths = ["1/1" , "2/1" , "1/2" , "2/2" ]
494+ for dynamic_stream_path in dynamic_stream_paths :
495+ http_mocker .get (
496+ HttpRequest (url = f"https://api.test.com/{ dynamic_stream_path } " ),
497+ HttpResponse (
498+ body = json .dumps ([{"ABC" : 1 , "AED" : 2 }])
499+ ),
500+ )
501+
502+ source = ConcurrentDeclarativeSource (
503+ source_config = _MANIFEST_WITH_HTTP_COMPONENT_RESOLVER_WITH_RETRIEVER_WITH_PARENT_STREAM , config = _CONFIG , catalog = None , state = None
504+ )
505+
506+ actual_catalog = source .discover (logger = source .logger , config = _CONFIG )
507+
508+ configured_streams = [
509+ to_configured_stream (stream , primary_key = stream .source_defined_primary_key )
510+ for stream in actual_catalog .streams
511+ ]
512+ configured_catalog = to_configured_catalog (configured_streams )
513+
514+ records = [
515+ message .record
516+ for message in source .read (MagicMock (), _CONFIG , configured_catalog )
517+ if message .type == Type .RECORD
518+ ]
519+
520+ assert len (actual_catalog .streams ) == 4
521+ assert [stream .name for stream in actual_catalog .streams ] == expected_stream_names
522+ assert len (records ) == 4
523+ assert [record .stream for record in records ] == expected_stream_names
0 commit comments