44from typing import Any
55
66from connector .src .custom .client_api .client_api_base import BaseClientAPI
7- from connector .src .utils .api_engine .exceptions .api_http_error import ApiHttpError
87
98LOG_PREFIX = "[FetcherShared]"
109
@@ -22,28 +21,29 @@ def __init__(
2221 """Initialize Shared Client API."""
2322 super ().__init__ (config , logger , api_client , fetcher_factory )
2423
25- async def fetch_subentities_ids (
24+ async def fetch_subentities (
2625 self , entity_name : str , entity_id : str , subentity_types : list [str ]
27- ) -> dict [str , list [str ]]:
28- """Fetch subentities IDs from the API.
26+ ) -> dict [str , list [Any ]]:
27+ """Fetch related subentities with full payloads from the API.
2928
3029 Args:
3130 entity_name (str): The name of the entity.
3231 entity_id (str): The ID of the entity.
3332 subentity_types (list[str]): The type of subentities to fetch.
3433
3534 Returns:
36- dict[str, list[str ]]: The fetched subentities IDs .
35+ dict[str, list[Any ]]: Related subentities grouped by type .
3736
3837 """
39- subentities_ids = {}
38+ subentities : dict [str , list [Any ]] = {}
39+ total_collection_calls = 0
4040
4141 relationships_fetcher = self .fetcher_factory .create_fetcher_by_name (
4242 "relationships" , base_url = self .config .api_url .unicode_string ()
4343 )
4444 try :
4545 for subentity_type in subentity_types :
46- all_ids = []
46+ all_entities : list [ Any ] = []
4747
4848 params = {
4949 entity_name : entity_id ,
@@ -55,24 +55,17 @@ async def fetch_subentities_ids(
5555 async for page_data in self ._paginate_with_cursor (
5656 relationships_fetcher , params , f"{ subentity_type } relationships"
5757 ):
58+ total_collection_calls += 1
5859 if isinstance (page_data , list ):
59- all_ids .extend (
60- [
61- item ["id" ]
62- for item in page_data
63- if isinstance (item , dict ) and item .get ("id" )
64- ]
65- )
60+ all_entities .extend (page_data )
6661 elif isinstance (page_data , dict ) and "data" in page_data :
6762 data = page_data ["data" ]
6863 if isinstance (data , list ):
69- all_ids .extend (
70- [
71- item ["id" ]
72- for item in data
73- if isinstance (item , dict ) and item .get ("id" )
74- ]
75- )
64+ all_entities .extend (data )
65+ elif isinstance (data , dict ):
66+ all_entities .append (data )
67+ elif isinstance (page_data , dict ):
68+ all_entities .append (page_data )
7669
7770 except Exception as e :
7871 self .logger .debug (
@@ -84,21 +77,24 @@ async def fetch_subentities_ids(
8477 },
8578 )
8679
87- if all_ids :
80+ if all_entities :
81+ typed_entities = self ._deserialize_subentities (
82+ subentity_type , all_entities
83+ )
8884 self .logger .info (
89- "Retrieved relationship IDs " ,
85+ "Retrieved related entities " ,
9086 {
9187 "prefix" : LOG_PREFIX ,
92- "count" : len (all_ids ),
88+ "count" : len (typed_entities ),
9389 "subentity_type" : subentity_type ,
9490 "entity_name" : entity_name ,
9591 "entity_id" : entity_id ,
9692 },
9793 )
98- subentities_ids [subentity_type ] = all_ids
94+ subentities [subentity_type ] = typed_entities
9995 else :
10096 self .logger .debug (
101- "No relationship IDs found" ,
97+ "No related entities found" ,
10298 {
10399 "prefix" : LOG_PREFIX ,
104100 "subentity_type" : subentity_type ,
@@ -107,7 +103,6 @@ async def fetch_subentities_ids(
107103 },
108104 )
109105
110- return subentities_ids
111106 except Exception as e :
112107 self .logger .error (
113108 "Failed to gather relationships" ,
@@ -119,7 +114,7 @@ async def fetch_subentities_ids(
119114 },
120115 )
121116 return {entity_type : [] for entity_type in subentity_types }
122- finally :
117+ else :
123118 self .logger .info (
124119 "Finished gathering relationships" ,
125120 {
@@ -128,85 +123,43 @@ async def fetch_subentities_ids(
128123 "entity_id" : entity_id ,
129124 },
130125 )
126+ return subentities
131127
132- async def fetch_subentity_details (
133- self , subentity_ids : dict [str , list [str ]]
134- ) -> dict [str , list [Any ]]:
135- """Fetch subentity details in parallel for multiple IDs.
136-
137- Args:
138- subentity_ids: dictionary mapping entity types to lists of IDs
139-
140- Returns:
141- dictionary mapping entity types to lists of fetched entities
142-
143- """
144- subentities : dict [str , list [Any ]] = {}
145- total_to_fetch = sum (len (ids ) for ids in subentity_ids .values ())
146-
147- if total_to_fetch > 0 :
148- self .logger .info (
149- "Fetching details for subentities" ,
150- {"prefix" : LOG_PREFIX , "total_to_fetch" : total_to_fetch },
128+ def _deserialize_subentities (
129+ self , subentity_type : str , entities : list [Any ]
130+ ) -> list [Any ]:
131+ """Deserialize related entities to their configured model when available."""
132+ try :
133+ fetcher = self .fetcher_factory .create_fetcher_by_name (
134+ subentity_type , base_url = self .config .api_url .unicode_string ()
151135 )
136+ response_model = fetcher .config .response_model
137+ except Exception as e :
138+ self .logger .debug (
139+ "Could not create typed fetcher for related entities" ,
140+ {
141+ "prefix" : LOG_PREFIX ,
142+ "subentity_type" : subentity_type ,
143+ "error" : str (e ),
144+ },
145+ )
146+ response_model = None
152147
153- for entity_type , ids in subentity_ids .items ():
154- if not ids :
155- subentities [entity_type ] = []
156- continue
157-
158- try :
159- fetcher = self .fetcher_factory .create_fetcher_by_name (
160- entity_type , base_url = self .config .api_url .unicode_string ()
161- )
162- entities = await fetcher .fetch_multiple (ids )
163- subentities [entity_type ] = entities
164- self .logger .debug (
165- "Fetched entities" ,
166- {
167- "prefix" : LOG_PREFIX ,
168- "count" : len (entities ),
169- "entity_type" : entity_type ,
170- },
171- )
172-
173- except ApiHttpError as e :
174- if e .status_code == 404 and entity_type == "files" :
175- self .logger .info (
176- "404 errors expected for files (files may no longer exist in VirusTotal). Treating as normal behavior." ,
177- {"prefix" : LOG_PREFIX },
178- )
179- subentities [entity_type ] = []
180- else :
181- self .logger .error (
182- "HTTP error fetching details" ,
148+ deserialized_entities : list [Any ] = []
149+ for entity in entities :
150+ if response_model and isinstance (entity , dict ):
151+ try :
152+ deserialized_entities .append (response_model .model_validate (entity ))
153+ continue
154+ except Exception as e :
155+ self .logger .debug (
156+ "Failed to deserialize related entity, keeping raw payload" ,
183157 {
184158 "prefix" : LOG_PREFIX ,
185- "status_code" : e .status_code ,
186- "entity_type" : entity_type ,
159+ "subentity_type" : subentity_type ,
187160 "error" : str (e ),
188161 },
189162 )
190- subentities [entity_type ] = []
191- except Exception as e :
192- self .logger .error (
193- "Failed to fetch details" ,
194- {
195- "prefix" : LOG_PREFIX ,
196- "entity_type" : entity_type ,
197- "error" : str (e ),
198- },
199- )
200- subentities [entity_type ] = []
201-
202- if total_to_fetch > 0 :
203- fetched_summary = ", " .join (
204- [f"{ k } : { len (v )} " for k , v in subentities .items () if len (v ) > 0 ]
205- )
206- if fetched_summary :
207- self .logger .info (
208- "Fetched details" ,
209- {"prefix" : LOG_PREFIX , "summary" : fetched_summary },
210- )
163+ deserialized_entities .append (entity )
211164
212- return subentities
165+ return deserialized_entities
0 commit comments