sarvamai 0.1.22a3__py3-none-any.whl → 0.1.23a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. sarvamai/__init__.py +405 -206
  2. sarvamai/chat/raw_client.py +20 -20
  3. sarvamai/client.py +186 -34
  4. sarvamai/core/__init__.py +76 -21
  5. sarvamai/core/client_wrapper.py +19 -3
  6. sarvamai/core/force_multipart.py +4 -2
  7. sarvamai/core/http_client.py +217 -97
  8. sarvamai/core/http_response.py +1 -1
  9. sarvamai/core/http_sse/__init__.py +42 -0
  10. sarvamai/core/http_sse/_api.py +112 -0
  11. sarvamai/core/http_sse/_decoders.py +61 -0
  12. sarvamai/core/http_sse/_exceptions.py +7 -0
  13. sarvamai/core/http_sse/_models.py +17 -0
  14. sarvamai/core/jsonable_encoder.py +8 -0
  15. sarvamai/core/pydantic_utilities.py +110 -4
  16. sarvamai/errors/__init__.py +40 -6
  17. sarvamai/errors/bad_request_error.py +1 -1
  18. sarvamai/errors/forbidden_error.py +1 -1
  19. sarvamai/errors/internal_server_error.py +1 -1
  20. sarvamai/errors/service_unavailable_error.py +1 -1
  21. sarvamai/errors/too_many_requests_error.py +1 -1
  22. sarvamai/errors/unprocessable_entity_error.py +1 -1
  23. sarvamai/requests/__init__.py +150 -62
  24. sarvamai/requests/audio_data.py +0 -6
  25. sarvamai/requests/error_response_data.py +1 -1
  26. sarvamai/requests/file_signed_url_details.py +1 -1
  27. sarvamai/requests/speech_to_text_transcription_data.py +2 -8
  28. sarvamai/requests/speech_to_text_translate_transcription_data.py +0 -6
  29. sarvamai/speech_to_text/raw_client.py +54 -52
  30. sarvamai/speech_to_text_job/job.py +100 -2
  31. sarvamai/speech_to_text_job/raw_client.py +134 -130
  32. sarvamai/speech_to_text_streaming/__init__.py +38 -10
  33. sarvamai/speech_to_text_streaming/client.py +0 -44
  34. sarvamai/speech_to_text_streaming/raw_client.py +0 -44
  35. sarvamai/speech_to_text_streaming/types/__init__.py +36 -8
  36. sarvamai/speech_to_text_translate_job/job.py +100 -2
  37. sarvamai/speech_to_text_translate_job/raw_client.py +134 -130
  38. sarvamai/speech_to_text_translate_streaming/__init__.py +36 -9
  39. sarvamai/speech_to_text_translate_streaming/client.py +0 -44
  40. sarvamai/speech_to_text_translate_streaming/raw_client.py +0 -44
  41. sarvamai/speech_to_text_translate_streaming/types/__init__.py +36 -9
  42. sarvamai/text/client.py +0 -12
  43. sarvamai/text/raw_client.py +60 -72
  44. sarvamai/text_to_speech/client.py +18 -0
  45. sarvamai/text_to_speech/raw_client.py +38 -20
  46. sarvamai/text_to_speech_streaming/__init__.py +28 -1
  47. sarvamai/text_to_speech_streaming/types/__init__.py +30 -1
  48. sarvamai/types/__init__.py +222 -102
  49. sarvamai/types/audio_data.py +0 -6
  50. sarvamai/types/chat_completion_request_message.py +6 -2
  51. sarvamai/types/completion_event_flag.py +3 -1
  52. sarvamai/types/error_response_data.py +1 -1
  53. sarvamai/types/file_signed_url_details.py +1 -1
  54. sarvamai/types/speech_to_text_transcription_data.py +2 -8
  55. sarvamai/types/speech_to_text_translate_transcription_data.py +0 -6
  56. {sarvamai-0.1.22a3.dist-info → sarvamai-0.1.23a1.dist-info}/METADATA +2 -1
  57. {sarvamai-0.1.22a3.dist-info → sarvamai-0.1.23a1.dist-info}/RECORD +58 -59
  58. sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_input_audio_codec.py +0 -33
  59. sarvamai/speech_to_text_streaming/types/speech_to_text_streaming_stream_ongoing_speech_results.py +0 -5
  60. sarvamai/speech_to_text_translate_streaming/types/speech_to_text_translate_streaming_input_audio_codec.py +0 -33
  61. sarvamai/speech_to_text_translate_streaming/types/speech_to_text_translate_streaming_stream_ongoing_speech_results.py +0 -5
  62. sarvamai/types/audio_data_input_audio_codec.py +0 -33
  63. sarvamai/types/response_speech_state.py +0 -7
  64. {sarvamai-0.1.22a3.dist-info → sarvamai-0.1.23a1.dist-info}/WHEEL +0 -0
@@ -3,7 +3,6 @@
3
3
  import typing
4
4
 
5
5
  import typing_extensions
6
- from ..types.response_speech_state import ResponseSpeechState
7
6
  from .transcription_metrics import TranscriptionMetricsParams
8
7
 
9
8
 
@@ -18,12 +17,12 @@ class SpeechToTextTranscriptionDataParams(typing_extensions.TypedDict):
18
17
  Transcript of the provided speech in original language
19
18
  """
20
19
 
21
- timestamps: typing_extensions.NotRequired[typing.Dict[str, typing.Optional[typing.Any]]]
20
+ timestamps: typing_extensions.NotRequired[typing.Dict[str, typing.Any]]
22
21
  """
23
22
  Timestamp information (if available)
24
23
  """
25
24
 
26
- diarized_transcript: typing_extensions.NotRequired[typing.Dict[str, typing.Optional[typing.Any]]]
25
+ diarized_transcript: typing_extensions.NotRequired[typing.Dict[str, typing.Any]]
27
26
  """
28
27
  Diarized transcript of the provided speech
29
28
  """
@@ -33,9 +32,4 @@ class SpeechToTextTranscriptionDataParams(typing_extensions.TypedDict):
33
32
  BCP-47 code of detected language
34
33
  """
35
34
 
36
- response_speech_state: typing_extensions.NotRequired[ResponseSpeechState]
37
- """
38
- Current state of speech detection and processing
39
- """
40
-
41
35
  metrics: TranscriptionMetricsParams
@@ -1,7 +1,6 @@
1
1
  # This file was auto-generated by Fern from our API Definition.
2
2
 
3
3
  import typing_extensions
4
- from ..types.response_speech_state import ResponseSpeechState
5
4
  from .transcription_metrics import TranscriptionMetricsParams
6
5
 
7
6
 
@@ -21,9 +20,4 @@ class SpeechToTextTranslateTranscriptionDataParams(typing_extensions.TypedDict):
21
20
  BCP-47 code of detected source language (null when language detection is in progress)
22
21
  """
23
22
 
24
- response_speech_state: typing_extensions.NotRequired[ResponseSpeechState]
25
- """
26
- Current state of speech detection and processing
27
- """
28
-
29
23
  metrics: TranscriptionMetricsParams
@@ -1,5 +1,6 @@
1
1
  # This file was auto-generated by Fern from our API Definition.
2
2
 
3
+ import json
3
4
  import typing
4
5
  from json.decoder import JSONDecodeError
5
6
 
@@ -7,6 +8,7 @@ from .. import core
7
8
  from ..core.api_error import ApiError
8
9
  from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
9
10
  from ..core.http_response import AsyncHttpResponse, HttpResponse
11
+ from ..core.jsonable_encoder import jsonable_encoder
10
12
  from ..core.pydantic_utilities import parse_obj_as
11
13
  from ..core.request_options import RequestOptions
12
14
  from ..errors.bad_request_error import BadRequestError
@@ -84,7 +86,7 @@ class RawSpeechToTextClient:
84
86
  base_url=self._client_wrapper.get_environment().base,
85
87
  method="POST",
86
88
  data={
87
- "model": model,
89
+ "model": json.dumps(jsonable_encoder(model)),
88
90
  "language_code": language_code,
89
91
  "input_audio_codec": input_audio_codec,
90
92
  },
@@ -109,9 +111,9 @@ class RawSpeechToTextClient:
109
111
  raise BadRequestError(
110
112
  headers=dict(_response.headers),
111
113
  body=typing.cast(
112
- typing.Optional[typing.Any],
114
+ typing.Any,
113
115
  parse_obj_as(
114
- type_=typing.Optional[typing.Any], # type: ignore
116
+ type_=typing.Any, # type: ignore
115
117
  object_=_response.json(),
116
118
  ),
117
119
  ),
@@ -120,9 +122,9 @@ class RawSpeechToTextClient:
120
122
  raise ForbiddenError(
121
123
  headers=dict(_response.headers),
122
124
  body=typing.cast(
123
- typing.Optional[typing.Any],
125
+ typing.Any,
124
126
  parse_obj_as(
125
- type_=typing.Optional[typing.Any], # type: ignore
127
+ type_=typing.Any, # type: ignore
126
128
  object_=_response.json(),
127
129
  ),
128
130
  ),
@@ -131,9 +133,9 @@ class RawSpeechToTextClient:
131
133
  raise UnprocessableEntityError(
132
134
  headers=dict(_response.headers),
133
135
  body=typing.cast(
134
- typing.Optional[typing.Any],
136
+ typing.Any,
135
137
  parse_obj_as(
136
- type_=typing.Optional[typing.Any], # type: ignore
138
+ type_=typing.Any, # type: ignore
137
139
  object_=_response.json(),
138
140
  ),
139
141
  ),
@@ -142,9 +144,9 @@ class RawSpeechToTextClient:
142
144
  raise TooManyRequestsError(
143
145
  headers=dict(_response.headers),
144
146
  body=typing.cast(
145
- typing.Optional[typing.Any],
147
+ typing.Any,
146
148
  parse_obj_as(
147
- type_=typing.Optional[typing.Any], # type: ignore
149
+ type_=typing.Any, # type: ignore
148
150
  object_=_response.json(),
149
151
  ),
150
152
  ),
@@ -153,9 +155,9 @@ class RawSpeechToTextClient:
153
155
  raise InternalServerError(
154
156
  headers=dict(_response.headers),
155
157
  body=typing.cast(
156
- typing.Optional[typing.Any],
158
+ typing.Any,
157
159
  parse_obj_as(
158
- type_=typing.Optional[typing.Any], # type: ignore
160
+ type_=typing.Any, # type: ignore
159
161
  object_=_response.json(),
160
162
  ),
161
163
  ),
@@ -164,9 +166,9 @@ class RawSpeechToTextClient:
164
166
  raise ServiceUnavailableError(
165
167
  headers=dict(_response.headers),
166
168
  body=typing.cast(
167
- typing.Optional[typing.Any],
169
+ typing.Any,
168
170
  parse_obj_as(
169
- type_=typing.Optional[typing.Any], # type: ignore
171
+ type_=typing.Any, # type: ignore
170
172
  object_=_response.json(),
171
173
  ),
172
174
  ),
@@ -228,7 +230,7 @@ class RawSpeechToTextClient:
228
230
  method="POST",
229
231
  data={
230
232
  "prompt": prompt,
231
- "model": model,
233
+ "model": json.dumps(jsonable_encoder(model)),
232
234
  "input_audio_codec": input_audio_codec,
233
235
  },
234
236
  files={
@@ -252,9 +254,9 @@ class RawSpeechToTextClient:
252
254
  raise BadRequestError(
253
255
  headers=dict(_response.headers),
254
256
  body=typing.cast(
255
- typing.Optional[typing.Any],
257
+ typing.Any,
256
258
  parse_obj_as(
257
- type_=typing.Optional[typing.Any], # type: ignore
259
+ type_=typing.Any, # type: ignore
258
260
  object_=_response.json(),
259
261
  ),
260
262
  ),
@@ -263,9 +265,9 @@ class RawSpeechToTextClient:
263
265
  raise ForbiddenError(
264
266
  headers=dict(_response.headers),
265
267
  body=typing.cast(
266
- typing.Optional[typing.Any],
268
+ typing.Any,
267
269
  parse_obj_as(
268
- type_=typing.Optional[typing.Any], # type: ignore
270
+ type_=typing.Any, # type: ignore
269
271
  object_=_response.json(),
270
272
  ),
271
273
  ),
@@ -274,9 +276,9 @@ class RawSpeechToTextClient:
274
276
  raise UnprocessableEntityError(
275
277
  headers=dict(_response.headers),
276
278
  body=typing.cast(
277
- typing.Optional[typing.Any],
279
+ typing.Any,
278
280
  parse_obj_as(
279
- type_=typing.Optional[typing.Any], # type: ignore
281
+ type_=typing.Any, # type: ignore
280
282
  object_=_response.json(),
281
283
  ),
282
284
  ),
@@ -285,9 +287,9 @@ class RawSpeechToTextClient:
285
287
  raise TooManyRequestsError(
286
288
  headers=dict(_response.headers),
287
289
  body=typing.cast(
288
- typing.Optional[typing.Any],
290
+ typing.Any,
289
291
  parse_obj_as(
290
- type_=typing.Optional[typing.Any], # type: ignore
292
+ type_=typing.Any, # type: ignore
291
293
  object_=_response.json(),
292
294
  ),
293
295
  ),
@@ -296,9 +298,9 @@ class RawSpeechToTextClient:
296
298
  raise InternalServerError(
297
299
  headers=dict(_response.headers),
298
300
  body=typing.cast(
299
- typing.Optional[typing.Any],
301
+ typing.Any,
300
302
  parse_obj_as(
301
- type_=typing.Optional[typing.Any], # type: ignore
303
+ type_=typing.Any, # type: ignore
302
304
  object_=_response.json(),
303
305
  ),
304
306
  ),
@@ -307,9 +309,9 @@ class RawSpeechToTextClient:
307
309
  raise ServiceUnavailableError(
308
310
  headers=dict(_response.headers),
309
311
  body=typing.cast(
310
- typing.Optional[typing.Any],
312
+ typing.Any,
311
313
  parse_obj_as(
312
- type_=typing.Optional[typing.Any], # type: ignore
314
+ type_=typing.Any, # type: ignore
313
315
  object_=_response.json(),
314
316
  ),
315
317
  ),
@@ -378,7 +380,7 @@ class AsyncRawSpeechToTextClient:
378
380
  base_url=self._client_wrapper.get_environment().base,
379
381
  method="POST",
380
382
  data={
381
- "model": model,
383
+ "model": json.dumps(jsonable_encoder(model)),
382
384
  "language_code": language_code,
383
385
  "input_audio_codec": input_audio_codec,
384
386
  },
@@ -403,9 +405,9 @@ class AsyncRawSpeechToTextClient:
403
405
  raise BadRequestError(
404
406
  headers=dict(_response.headers),
405
407
  body=typing.cast(
406
- typing.Optional[typing.Any],
408
+ typing.Any,
407
409
  parse_obj_as(
408
- type_=typing.Optional[typing.Any], # type: ignore
410
+ type_=typing.Any, # type: ignore
409
411
  object_=_response.json(),
410
412
  ),
411
413
  ),
@@ -414,9 +416,9 @@ class AsyncRawSpeechToTextClient:
414
416
  raise ForbiddenError(
415
417
  headers=dict(_response.headers),
416
418
  body=typing.cast(
417
- typing.Optional[typing.Any],
419
+ typing.Any,
418
420
  parse_obj_as(
419
- type_=typing.Optional[typing.Any], # type: ignore
421
+ type_=typing.Any, # type: ignore
420
422
  object_=_response.json(),
421
423
  ),
422
424
  ),
@@ -425,9 +427,9 @@ class AsyncRawSpeechToTextClient:
425
427
  raise UnprocessableEntityError(
426
428
  headers=dict(_response.headers),
427
429
  body=typing.cast(
428
- typing.Optional[typing.Any],
430
+ typing.Any,
429
431
  parse_obj_as(
430
- type_=typing.Optional[typing.Any], # type: ignore
432
+ type_=typing.Any, # type: ignore
431
433
  object_=_response.json(),
432
434
  ),
433
435
  ),
@@ -436,9 +438,9 @@ class AsyncRawSpeechToTextClient:
436
438
  raise TooManyRequestsError(
437
439
  headers=dict(_response.headers),
438
440
  body=typing.cast(
439
- typing.Optional[typing.Any],
441
+ typing.Any,
440
442
  parse_obj_as(
441
- type_=typing.Optional[typing.Any], # type: ignore
443
+ type_=typing.Any, # type: ignore
442
444
  object_=_response.json(),
443
445
  ),
444
446
  ),
@@ -447,9 +449,9 @@ class AsyncRawSpeechToTextClient:
447
449
  raise InternalServerError(
448
450
  headers=dict(_response.headers),
449
451
  body=typing.cast(
450
- typing.Optional[typing.Any],
452
+ typing.Any,
451
453
  parse_obj_as(
452
- type_=typing.Optional[typing.Any], # type: ignore
454
+ type_=typing.Any, # type: ignore
453
455
  object_=_response.json(),
454
456
  ),
455
457
  ),
@@ -458,9 +460,9 @@ class AsyncRawSpeechToTextClient:
458
460
  raise ServiceUnavailableError(
459
461
  headers=dict(_response.headers),
460
462
  body=typing.cast(
461
- typing.Optional[typing.Any],
463
+ typing.Any,
462
464
  parse_obj_as(
463
- type_=typing.Optional[typing.Any], # type: ignore
465
+ type_=typing.Any, # type: ignore
464
466
  object_=_response.json(),
465
467
  ),
466
468
  ),
@@ -522,7 +524,7 @@ class AsyncRawSpeechToTextClient:
522
524
  method="POST",
523
525
  data={
524
526
  "prompt": prompt,
525
- "model": model,
527
+ "model": json.dumps(jsonable_encoder(model)),
526
528
  "input_audio_codec": input_audio_codec,
527
529
  },
528
530
  files={
@@ -546,9 +548,9 @@ class AsyncRawSpeechToTextClient:
546
548
  raise BadRequestError(
547
549
  headers=dict(_response.headers),
548
550
  body=typing.cast(
549
- typing.Optional[typing.Any],
551
+ typing.Any,
550
552
  parse_obj_as(
551
- type_=typing.Optional[typing.Any], # type: ignore
553
+ type_=typing.Any, # type: ignore
552
554
  object_=_response.json(),
553
555
  ),
554
556
  ),
@@ -557,9 +559,9 @@ class AsyncRawSpeechToTextClient:
557
559
  raise ForbiddenError(
558
560
  headers=dict(_response.headers),
559
561
  body=typing.cast(
560
- typing.Optional[typing.Any],
562
+ typing.Any,
561
563
  parse_obj_as(
562
- type_=typing.Optional[typing.Any], # type: ignore
564
+ type_=typing.Any, # type: ignore
563
565
  object_=_response.json(),
564
566
  ),
565
567
  ),
@@ -568,9 +570,9 @@ class AsyncRawSpeechToTextClient:
568
570
  raise UnprocessableEntityError(
569
571
  headers=dict(_response.headers),
570
572
  body=typing.cast(
571
- typing.Optional[typing.Any],
573
+ typing.Any,
572
574
  parse_obj_as(
573
- type_=typing.Optional[typing.Any], # type: ignore
575
+ type_=typing.Any, # type: ignore
574
576
  object_=_response.json(),
575
577
  ),
576
578
  ),
@@ -579,9 +581,9 @@ class AsyncRawSpeechToTextClient:
579
581
  raise TooManyRequestsError(
580
582
  headers=dict(_response.headers),
581
583
  body=typing.cast(
582
- typing.Optional[typing.Any],
584
+ typing.Any,
583
585
  parse_obj_as(
584
- type_=typing.Optional[typing.Any], # type: ignore
586
+ type_=typing.Any, # type: ignore
585
587
  object_=_response.json(),
586
588
  ),
587
589
  ),
@@ -590,9 +592,9 @@ class AsyncRawSpeechToTextClient:
590
592
  raise InternalServerError(
591
593
  headers=dict(_response.headers),
592
594
  body=typing.cast(
593
- typing.Optional[typing.Any],
595
+ typing.Any,
594
596
  parse_obj_as(
595
- type_=typing.Optional[typing.Any], # type: ignore
597
+ type_=typing.Any, # type: ignore
596
598
  object_=_response.json(),
597
599
  ),
598
600
  ),
@@ -601,9 +603,9 @@ class AsyncRawSpeechToTextClient:
601
603
  raise ServiceUnavailableError(
602
604
  headers=dict(_response.headers),
603
605
  body=typing.cast(
604
- typing.Optional[typing.Any],
606
+ typing.Any,
605
607
  parse_obj_as(
606
- type_=typing.Optional[typing.Any], # type: ignore
608
+ type_=typing.Any, # type: ignore
607
609
  object_=_response.json(),
608
610
  ),
609
611
  ),
@@ -146,9 +146,58 @@ class AsyncSpeechToTextJob:
146
146
  "output_file": detail.outputs[0].file_name,
147
147
  }
148
148
  for detail in (job_status.job_details or [])
149
- if detail.inputs and detail.outputs
149
+ if detail.inputs and detail.outputs and detail.state == "Success"
150
150
  ]
151
151
 
152
+ async def get_file_results(
153
+ self,
154
+ ) -> typing.Dict[str, typing.List[typing.Dict[str, typing.Any]]]:
155
+ """
156
+ Get detailed results for each file in the batch job.
157
+
158
+ Returns
159
+ -------
160
+ Dict[str, List[Dict[str, Any]]]
161
+ Dictionary with 'successful' and 'failed' keys, each containing a list of file details.
162
+ Each file detail includes:
163
+ - 'file_name': Name of the input file
164
+ - 'status': Status of processing ('Success' or 'Failed')
165
+ - 'error_message': Error message if failed (None if successful)
166
+ - 'output_file': Name of output file if successful (None if failed)
167
+ """
168
+ job_status = await self.get_status()
169
+ results: typing.Dict[str, typing.List[typing.Dict[str, typing.Any]]] = {
170
+ "successful": [],
171
+ "failed": [],
172
+ }
173
+
174
+ for detail in job_status.job_details or []:
175
+ # Check for empty lists explicitly
176
+ if not detail.inputs or len(detail.inputs) == 0:
177
+ continue
178
+
179
+ try:
180
+ file_info = {
181
+ "file_name": detail.inputs[0].file_name,
182
+ "status": detail.state,
183
+ "error_message": detail.error_message,
184
+ "output_file": (
185
+ detail.outputs[0].file_name
186
+ if detail.outputs and len(detail.outputs) > 0
187
+ else None
188
+ ),
189
+ }
190
+
191
+ if detail.state == "Success":
192
+ results["successful"].append(file_info)
193
+ else:
194
+ results["failed"].append(file_info)
195
+ except (IndexError, AttributeError):
196
+ # Skip malformed job details
197
+ continue
198
+
199
+ return results
200
+
152
201
  async def download_outputs(self, output_dir: str) -> bool:
153
202
  """
154
203
  Download output files to the specified directory.
@@ -387,9 +436,58 @@ class SpeechToTextJob:
387
436
  "output_file": detail.outputs[0].file_name,
388
437
  }
389
438
  for detail in (job_status.job_details or [])
390
- if detail.inputs and detail.outputs
439
+ if detail.inputs and detail.outputs and detail.state == "Success"
391
440
  ]
392
441
 
442
+ def get_file_results(
443
+ self,
444
+ ) -> typing.Dict[str, typing.List[typing.Dict[str, typing.Any]]]:
445
+ """
446
+ Get detailed results for each file in the batch job.
447
+
448
+ Returns
449
+ -------
450
+ Dict[str, List[Dict[str, Any]]]
451
+ Dictionary with 'successful' and 'failed' keys, each containing a list of file details.
452
+ Each file detail includes:
453
+ - 'file_name': Name of the input file
454
+ - 'status': Status of processing ('Success' or 'Failed')
455
+ - 'error_message': Error message if failed (None if successful)
456
+ - 'output_file': Name of output file if successful (None if failed)
457
+ """
458
+ job_status = self.get_status()
459
+ results: typing.Dict[str, typing.List[typing.Dict[str, typing.Any]]] = {
460
+ "successful": [],
461
+ "failed": [],
462
+ }
463
+
464
+ for detail in job_status.job_details or []:
465
+ # Check for empty lists explicitly
466
+ if not detail.inputs or len(detail.inputs) == 0:
467
+ continue
468
+
469
+ try:
470
+ file_info = {
471
+ "file_name": detail.inputs[0].file_name,
472
+ "status": detail.state,
473
+ "error_message": detail.error_message,
474
+ "output_file": (
475
+ detail.outputs[0].file_name
476
+ if detail.outputs and len(detail.outputs) > 0
477
+ else None
478
+ ),
479
+ }
480
+
481
+ if detail.state == "Success":
482
+ results["successful"].append(file_info)
483
+ else:
484
+ results["failed"].append(file_info)
485
+ except (IndexError, AttributeError):
486
+ # Skip malformed job details
487
+ continue
488
+
489
+ return results
490
+
393
491
  def download_outputs(self, output_dir: str) -> bool:
394
492
  """
395
493
  Download output files to the specified directory.