11"""Business logic for protocols.io operations."""
22
3+ import json
34import re
45from typing import Any
56
1516)
1617
1718
19+ def _parse_draftjs_content (content : str | dict | None ) -> str :
20+ """Parse Draft.js JSON content to plain text.
21+
22+ The protocols.io API returns rich text fields as Draft.js JSON.
23+ This function extracts the plain text from those blocks.
24+
25+ Parameters
26+ ----------
27+ content : str | dict | None
28+ Raw content, either as JSON string or parsed dict.
29+
30+ Returns
31+ -------
32+ str
33+ Plain text extracted from Draft.js blocks.
34+ """
35+ if not content :
36+ return ""
37+
38+ if isinstance (content , str ):
39+ # Check if it's JSON
40+ if content .startswith ("{" ) or content .startswith ("[" ):
41+ try :
42+ content = json .loads (content )
43+ except json .JSONDecodeError :
44+ return content
45+ else :
46+ return content
47+
48+ if isinstance (content , dict ):
49+ blocks = content .get ("blocks" , [])
50+ if blocks :
51+ return "\n \n " .join (block .get ("text" , "" ) for block in blocks if block .get ("text" ))
52+
53+ return str (content ) if content else ""
54+
55+
1856def _parse_search_item (item : dict [str , Any ]) -> ProtocolSearchItem :
1957 """Parse a single search result item.
2058
@@ -52,6 +90,8 @@ def _parse_search_item(item: dict[str, Any]) -> ProtocolSearchItem:
5290def _parse_step (step : dict [str , Any ]) -> ProtocolStep :
5391 """Parse a single protocol step.
5492
93+ Handles both v3 and v4 API response formats.
94+
5595 Parameters
5696 ----------
5797 step : dict[str, Any]
@@ -62,11 +102,15 @@ def _parse_step(step: dict[str, Any]) -> ProtocolStep:
62102 ProtocolStep
63103 Parsed step.
64104 """
105+ # v4 API uses 'number' and 'step' (content), v3 uses 'step_number' and 'description'
106+ step_number = step .get ("number" ) or step .get ("step_number" )
107+ description = _parse_draftjs_content (step .get ("step" ) or step .get ("description" ))
108+
65109 return ProtocolStep (
66110 id = step .get ("id" , 0 ),
67- step_number = step . get ( " step_number" ) ,
111+ step_number = step_number ,
68112 title = step .get ("title" ),
69- description = step . get ( " description" ) ,
113+ description = description ,
70114 section = step .get ("section" ),
71115 duration = step .get ("duration" ),
72116 duration_unit = step .get ("duration_unit" ),
@@ -101,7 +145,6 @@ async def search_protocols(
101145 client : ProtocolsIOClient ,
102146 query : str ,
103147 max_results : int = 10 ,
104- peer_reviewed_only : bool = False ,
105148) -> ProtocolSearchResponse :
106149 """Search for protocols.
107150
@@ -113,20 +156,15 @@ async def search_protocols(
113156 Search query string.
114157 max_results : int
115158 Maximum number of results to return.
116- peer_reviewed_only : bool
117- Filter to peer-reviewed protocols only.
118159
119160 Returns
120161 -------
121162 ProtocolSearchResponse
122163 Search results with pagination info.
123164 """
124- filter_type = "peer_reviewed" if peer_reviewed_only else "public"
125-
126165 response = await client .search_protocols (
127166 query = query ,
128167 page_size = max_results ,
129- filter_type = filter_type ,
130168 )
131169
132170 items = [_parse_search_item (item ) for item in response .get ("items" , [])]
@@ -199,7 +237,8 @@ async def get_protocol_detail(
199237 normalized_id = _normalize_protocol_id (protocol_id )
200238 response = await client .get_protocol (normalized_id )
201239
202- protocol = response .get ("protocol" , response )
240+ # v4 API returns data in 'payload', v3 uses 'protocol'
241+ protocol = response .get ("payload" ) or response .get ("protocol" ) or response
203242
204243 creator = None
205244 if protocol .get ("creator" ):
@@ -213,10 +252,10 @@ async def get_protocol_detail(
213252 title = protocol .get ("title" , "" ),
214253 uri = protocol .get ("uri" , "" ),
215254 doi = protocol .get ("doi" ),
216- description = protocol .get ("description" ),
217- before_start = protocol .get ("before_start" ),
218- warning = protocol .get ("warning" ),
219- guidelines = protocol .get ("guidelines" ),
255+ description = _parse_draftjs_content ( protocol .get ("description" ) ),
256+ before_start = _parse_draftjs_content ( protocol .get ("before_start" ) ),
257+ warning = _parse_draftjs_content ( protocol .get ("warning" ) ),
258+ guidelines = _parse_draftjs_content ( protocol .get ("guidelines" ) ),
220259 steps = [],
221260 materials = [],
222261 creator = creator ,
@@ -244,7 +283,15 @@ async def get_protocol_steps(
244283 """
245284 normalized_id = _normalize_protocol_id (protocol_id )
246285 response = await client .get_protocol_steps (normalized_id )
247- steps_data = response .get ("steps" , response .get ("items" , []))
286+
287+ # v4 API returns steps directly in 'payload' as a list
288+ # v3 API returns them in 'steps' or 'items'
289+ payload = response .get ("payload" )
290+ if isinstance (payload , list ):
291+ steps_data = payload
292+ else :
293+ steps_data = response .get ("steps" , response .get ("items" , []))
294+
248295 return [_parse_step (step ) for step in steps_data ]
249296
250297
@@ -268,7 +315,15 @@ async def get_protocol_materials(
268315 """
269316 normalized_id = _normalize_protocol_id (protocol_id )
270317 response = await client .get_protocol_materials (normalized_id )
271- materials_data = response .get ("materials" , response .get ("items" , []))
318+
319+ # v4 API returns full protocol in 'payload', materials are in payload.materials
320+ # v3 API returns them in 'materials' or 'items'
321+ payload = response .get ("payload" )
322+ if isinstance (payload , dict ):
323+ materials_data = payload .get ("materials" , [])
324+ else :
325+ materials_data = response .get ("materials" , response .get ("items" , []))
326+
272327 return [_parse_material (mat ) for mat in materials_data ]
273328
274329
0 commit comments