11"""Formatters for assets retrieved from Kili API."""
22
3+ import asyncio
34import json
45
6+ import requests
7+
58from kili .adapters .http_client import HttpClient
6- from kili .core .helpers import get_response_json , is_url , log_raise_for_status
9+ from kili .adapters .kili_api_gateway .helpers .http_json import load_json_from_link
10+ from kili .core .helpers import is_url
711from kili .domain .types import ListOrTuple
812
13+ # Batch size for parallel JSON response downloads (same as export service)
14+ JSON_RESPONSE_BATCH_SIZE = 10
15+
16+
17+ async def _download_json_response (url : str , http_client : HttpClient ) -> dict :
18+ """Download and parse JSON response from a URL using asyncio.
19+
20+ Args:
21+ url: URL to download the JSON response from
22+ http_client: HttpClient instance with SSL verification already configured
923
10- def load_json_from_link (link : str , http_client : HttpClient ) -> dict :
11- """Load json from link."""
12- if link == "" or not is_url (link ):
24+ Returns:
25+ Parsed JSON response as a dictionary
26+ """
27+ try :
28+ # Run synchronous requests call in a thread
29+ response = await asyncio .to_thread (http_client .get , url , timeout = 30 )
30+ response .raise_for_status ()
31+ return response .json ()
32+ except (requests .RequestException , json .JSONDecodeError ):
33+ # Return empty dict on error to ensure consistent response format
1334 return {}
1435
15- response = http_client .get (link , timeout = 30 )
16- log_raise_for_status (response )
17- return get_response_json (response )
36+
37+ async def _download_json_responses_async (
38+ url_to_label_mapping : list [tuple [str , dict ]], http_client : HttpClient
39+ ) -> None :
40+ """Download JSON responses in parallel using asyncio.
41+
42+ Args:
43+ url_to_label_mapping: List of tuples (url, label_dict) to download
44+ http_client: HttpClient instance with SSL verification already configured
45+ """
46+ # Process in batches to limit concurrent connections
47+ for i in range (0 , len (url_to_label_mapping ), JSON_RESPONSE_BATCH_SIZE ):
48+ batch = url_to_label_mapping [i : i + JSON_RESPONSE_BATCH_SIZE ]
49+
50+ # Download all URLs in the batch in parallel using asyncio.gather
51+ download_tasks = [_download_json_response (url , http_client ) for url , _ in batch ]
52+ json_responses = await asyncio .gather (* download_tasks )
53+
54+ # Assign the downloaded responses back to their labels and remove the URL
55+ for (_ , label ), json_response in zip (batch , json_responses , strict = False ):
56+ label ["jsonResponse" ] = json_response
57+ if "jsonResponseUrl" in label :
58+ del label ["jsonResponseUrl" ]
59+
60+
61+ def download_json_responses_parallel (
62+ url_to_label_mapping : list [tuple [str , dict ]], http_client : HttpClient
63+ ) -> None :
64+ """Download JSON responses in parallel and assign to labels.
65+
66+ Args:
67+ url_to_label_mapping: List of tuples (url, label_dict) to download
68+ http_client: HttpClient instance with SSL verification already configured
69+ """
70+ if not url_to_label_mapping :
71+ return
72+
73+ # Check if we're already in an event loop (e.g., Jupyter notebook, async web framework)
74+ try :
75+ asyncio .get_running_loop ()
76+ except RuntimeError :
77+ # No running loop, safe to use asyncio.run() for parallel downloads
78+ asyncio .run (_download_json_responses_async (url_to_label_mapping , http_client ))
79+ else :
80+ # Already in a loop - fall back to sequential downloads to avoid RuntimeError
81+ # This happens in Jupyter notebooks, FastAPI, and other async contexts
82+ for url , label in url_to_label_mapping :
83+ try :
84+ response = http_client .get (url , timeout = 30 )
85+ response .raise_for_status ()
86+ label ["jsonResponse" ] = response .json ()
87+ except (requests .RequestException , json .JSONDecodeError ):
88+ label ["jsonResponse" ] = {}
89+
90+ if "jsonResponseUrl" in label :
91+ del label ["jsonResponseUrl" ]
92+
93+
94+ def _parse_label_json_response (label : dict ) -> None :
95+ """Parse jsonResponse string to dict for a single label.
96+
97+ Args:
98+ label: Label dict to update in place
99+ """
100+ json_response_value = label .get ("jsonResponse" , "{}" )
101+ try :
102+ label ["jsonResponse" ] = json .loads (json_response_value )
103+ except json .JSONDecodeError :
104+ label ["jsonResponse" ] = {}
105+
106+
107+ def _process_label_json_response (label : dict , url_to_label_mapping : list [tuple [str , dict ]]) -> None :
108+ """Process a single label's jsonResponse, either scheduling URL download or parsing.
109+
110+ Args:
111+ label: Label dict to process
112+ url_to_label_mapping: List to append URL mapping if download needed
113+ """
114+ json_response_url = label .get ("jsonResponseUrl" )
115+ if json_response_url and is_url (json_response_url ):
116+ url_to_label_mapping .append ((json_response_url , label ))
117+ else :
118+ _parse_label_json_response (label )
18119
19120
20121def load_asset_json_fields (asset : dict , fields : ListOrTuple [str ], http_client : HttpClient ) -> dict :
@@ -28,18 +129,17 @@ def load_asset_json_fields(asset: dict, fields: ListOrTuple[str], http_client: H
28129 if "ocrMetadata" in fields and asset .get ("ocrMetadata" ) is not None :
29130 asset ["ocrMetadata" ] = load_json_from_link (asset .get ("ocrMetadata" , "" ), http_client )
30131
132+ # Collect all URLs to download in parallel (similar to export service)
133+ url_to_label_mapping = []
134+
31135 if "labels.jsonResponse" in fields :
32- asset_labels = asset .get ("labels" , [])
33- for label in asset_labels :
34- try :
35- label ["jsonResponse" ] = json .loads (label ["jsonResponse" ])
36- except json .JSONDecodeError :
37- label ["jsonResponse" ] = {}
136+ for label in asset .get ("labels" , []):
137+ _process_label_json_response (label , url_to_label_mapping )
38138
39139 if "latestLabel.jsonResponse" in fields and asset .get ("latestLabel" ) is not None :
40- try :
41- asset [ "latestLabel" ][ "jsonResponse" ] = json . loads ( asset [ "latestLabel" ][ "jsonResponse" ])
42- except json . JSONDecodeError :
43- asset [ "latestLabel" ][ "jsonResponse" ] = {}
140+ _process_label_json_response ( asset [ "latestLabel" ], url_to_label_mapping )
141+
142+ if url_to_label_mapping :
143+ download_json_responses_parallel ( url_to_label_mapping , http_client )
44144
45145 return asset
0 commit comments