@@ -27,14 +27,60 @@ def is_valid_path(input_path: str, expected_prefix: str) -> bool:
2727 return os.path.isabs(input_path) and input_path.startswith(expected_prefix)
2828
2929
30- def sanitised_name(name: str) -> str:
31- """Returns sanitised version of the name that can be used as a filename."""
30+ def sanitize_name(name: str) -> str:
3231 """Returns sanitised version of the name that can be used as a filename."""
3332 lower_spaceless_name = name.lower().replace(' ', '_')
3433 allowed_chars = set(string.ascii_lowercase + string.digits + '_-.')
3534 return ''.join(l for l in lower_spaceless_name if l in allowed_chars)
3635
3736
37+ def retrieve_sanitised_input_file_name(input_file: str) -> Optional[str]:
38+ """Retrieves a sanitised version of the input file name."""
39+ if not os.path.exists(input_file):
40+ raise FileNotFoundError(f"Input file '{input_file}' does not exist.")
41+ try:
42+ with open(input_file, 'r') as f:
43+ parser = ijson.items(f, '')
44+ try:
45+ root = next(parser)
46+ except StopIteration:
47+ raise ValueError("Empty JSON file.")
48+
49+ # Case 1: root is a list
50+ if isinstance(root, list):
51+ if len(root) != 1:
52+ raise ValueError(
53+ "[ERROR] The AlphaFold Server format is not supported: the list must contain exactly one dictionary item.")
54+ item = root[0]
55+ if not isinstance(item, dict):
56+ raise ValueError(
57+ "[ERROR] Item in Alphafold Server item must be a dictionary.")
58+
59+ # Case 2: root is a dict
60+ elif isinstance(root, dict):
61+ item = root
62+ else:
63+ raise ValueError(
64+ "[ERROR] Top-level JSON must be a list or a dictionary.")
65+
66+ # Validate and extract 'name'
67+ name = item.get('name')
68+ if not isinstance(name, str):
69+ raise ValueError(
70+ "[ERROR] 'name' key must exist and be a string.")
71+
72+ return sanitize_name(name)
73+
74+ except FileNotFoundError:
75+ print(f"[ERROR] Input file '{input_file}' not found.")
76+ raise FileNotFoundError(
77+ f"[ERROR] Input file '{input_file}' not found.")
78+ except Exception as e:
79+ print(f"[ERROR] Error processing input file '{input_file}': {e}")
80+ raise Exception(
81+ f"[ERROR] Error processing input file '{input_file}': {e}")
82+
83+
3884class AF3SlurmClient:
3985 """An AF3 client for interacting with the Slurm API."""
4086 SLURM_API_VERSION = "v0.0.41"
@@ -68,7 +114,7 @@ class AF3SlurmClient:
68114 self.slurm_base_url = f"http://{hostname}:{port}/slurm/{self.SLURM_API_VERSION}"
69115 self.slurmdb_base_url = f"http://{hostname}:{port}/slurmdb/{self.SLURM_API_VERSION}"
70116 self.token = token
71-
117+
72118 # Af3 default config
73119 self.af3_config = {
74120 "datapipeline_timeout": "{{ default_datapipeline_timeout }}",
@@ -183,33 +229,17 @@ class AF3SlurmClient:
183229 inference_model_cif_output_path: Optional[str] = None
184230 inference_summary_confidences_output_path: Optional[str] = None
185231 inference_confidences_output_path: Optional[str] = None
186- try:
187- sanitised_input_name: Optional[str] = None
188- with open(input_file, "r") as f:
189- name = None
190- parser = ijson.parse(f)
191- for prefix, event, value in parser:
192- if prefix == "name" and event == "string":
193- name = value
194- break
195- if not name:
196- raise ValueError("Missing 'name' in input file.")
197- sanitised_input_name = sanitised_name(name)
198-
199- inference_model_cif_output_path = os.path.join(
200- output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_model.cif")
201- inference_summary_confidences_output_path = os.path.join(
202- output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_summary_confidences.json")
203- inference_confidences_output_path = os.path.join(
204- output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_confidences.json")
205- except FileNotFoundError:
206- print(f"[ERROR] Input file '{input_file}' not found.")
207- raise FileNotFoundError(
208- f"[ERROR] Input file '{input_file}' not found.")
209- except Exception as e:
210- print(f"[ERROR] Error processing input file '{input_file}': {e}")
211- raise Exception(
212- f"[ERROR] Error processing input file '{input_file}': {e}")
232+
233+ sanitised_input_name: Optional[str] = retrieve_sanitised_input_file_name(
234+ input_file=input_file
235+ )
236+
237+ inference_model_cif_output_path = os.path.join(
238+ output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_model.cif")
239+ inference_summary_confidences_output_path = os.path.join(
240+ output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_summary_confidences.json")
241+ inference_confidences_output_path = os.path.join(
242+ output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_confidences.json")
213243
214244 job_config = {
215245 "account": self.af3_config["service_user"],
@@ -261,25 +291,10 @@ class AF3SlurmClient:
261291 f"{job_type}_result", file_name, f"{timestamp}"
262292 )
263293
264- datapipeline_output_file_path: Optional[str] = None
265- try:
266- sanitised_input_name: Optional[str] = None
267- with open(input_file, "r") as f:
268- name = None
269- parser = ijson.parse(f)
270- for prefix, event, value in parser:
271- if prefix == "name" and event == "string":
272- name = value
273- break
274- if not name:
275- raise ValueError("Missing 'name' in input file.")
276- sanitised_input_name = sanitised_name(name)
277- datapipeline_output_file_path = os.path.join(
278- output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_data.json")
279- except Exception as e:
280- print(f"[ERROR] Error processing input file '{input_file}': {e}")
281- raise Exception(
282- f"[ERROR] Error processing input file '{input_file}': {e}")
294+ sanitised_input_name = retrieve_sanitised_input_file_name(input_file)
295+ datapipeline_output_file_path = os.path.join(
296+ output_dir, f"{sanitised_input_name}", f"{sanitised_input_name}_data.json")
297+
283298 job_config = {
284299 "account": self.af3_config["service_user"],
285300 "tasks": 1,
0 commit comments