runapi-core 0.3.5__tar.gz → 0.4.0__tar.gz

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 (33) hide show
  1. {runapi_core-0.3.5 → runapi_core-0.4.0}/PKG-INFO +2 -2
  2. {runapi_core-0.3.5 → runapi_core-0.4.0}/pyproject.toml +1 -1
  3. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/contract_gen.py +185 -5
  4. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/http_client.py +7 -5
  5. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/resource.py +47 -12
  6. runapi_core-0.4.0/src/runapi/core/version.py +1 -0
  7. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_http_client.py +2 -1
  8. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_resource.py +10 -0
  9. runapi_core-0.3.5/src/runapi/core/version.py +0 -1
  10. {runapi_core-0.3.5 → runapi_core-0.4.0}/README.md +0 -0
  11. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/__init__.py +0 -0
  12. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/account.py +0 -0
  13. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/auth.py +0 -0
  14. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/config.py +0 -0
  15. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/constants.py +0 -0
  16. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/errors.py +0 -0
  17. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/files.py +0 -0
  18. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/models.py +0 -0
  19. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/multipart.py +0 -0
  20. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/options.py +0 -0
  21. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/polling.py +0 -0
  22. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/pricing.py +0 -0
  23. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/provider_client.py +0 -0
  24. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/py.typed +0 -0
  25. {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/response.py +0 -0
  26. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_account.py +0 -0
  27. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_auth.py +0 -0
  28. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_config.py +0 -0
  29. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_errors.py +0 -0
  30. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_files.py +0 -0
  31. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_models.py +0 -0
  32. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_polling.py +0 -0
  33. {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_pricing.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runapi-core
3
- Version: 0.3.5
3
+ Version: 0.4.0
4
4
  Summary: RunAPI core SDK for JavaScript, Python, Ruby, Go, Java, and PHP
5
5
  Project-URL: Homepage, https://runapi.ai/models
6
6
  Project-URL: Documentation, https://runapi.ai/docs/resources/sdks
7
7
  Project-URL: Source, https://github.com/runapi-ai/core-sdk
8
8
  Project-URL: Issues, https://github.com/runapi-ai/core-sdk/issues
9
9
  Project-URL: Changelog, https://github.com/runapi-ai/core-sdk/blob/main/CHANGELOG.md
10
- Project-URL: Release Notes, https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.3.5
10
+ Project-URL: Release Notes, https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.4.0
11
11
  Author-email: RunAPI <contact@runapi.ai>
12
12
  License-Expression: Apache-2.0
13
13
  Keywords: api,core,golang,gradle,java,maven,pypi,python,ruby,runapi,runapi-ai,sdk,typescript
@@ -19,7 +19,7 @@ Documentation = "https://runapi.ai/docs/resources/sdks"
19
19
  Source = "https://github.com/runapi-ai/core-sdk"
20
20
  Issues = "https://github.com/runapi-ai/core-sdk/issues"
21
21
  Changelog = "https://github.com/runapi-ai/core-sdk/blob/main/CHANGELOG.md"
22
- "Release Notes" = "https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.3.5"
22
+ "Release Notes" = "https://github.com/runapi-ai/core-sdk/releases/tag/python%2Fv0.4.0"
23
23
 
24
24
  [tool.hatch.version]
25
25
  path = "src/runapi/core/version.py"
@@ -39,10 +39,41 @@ CONTRACT = {
39
39
  }
40
40
  },
41
41
  "fish-audio/text-to-speech": {
42
- "models": ["s1", "s2-pro"],
42
+ "models": ["s1", "s2-pro", "s2.1-pro"],
43
43
  "fields_by_model": {
44
- "s1": {},
45
- "s2-pro": {}
44
+ "s1": {
45
+ "bitrate_kbps": {
46
+ "enum": [64, 128, 192]
47
+ },
48
+ "output_format": {
49
+ "enum": ["mp3", "wav"]
50
+ },
51
+ "sample_rate_hz": {
52
+ "enum": [8000, 16000, 24000, 32000, 44100]
53
+ }
54
+ },
55
+ "s2-pro": {
56
+ "bitrate_kbps": {
57
+ "enum": [64, 128, 192]
58
+ },
59
+ "output_format": {
60
+ "enum": ["mp3", "wav"]
61
+ },
62
+ "sample_rate_hz": {
63
+ "enum": [8000, 16000, 24000, 32000, 44100]
64
+ }
65
+ },
66
+ "s2.1-pro": {
67
+ "bitrate_kbps": {
68
+ "enum": [64, 128, 192]
69
+ },
70
+ "output_format": {
71
+ "enum": ["mp3", "wav"]
72
+ },
73
+ "sample_rate_hz": {
74
+ "enum": [8000, 16000, 24000, 32000, 44100]
75
+ }
76
+ }
46
77
  }
47
78
  },
48
79
  "flux-2/remix-image": {
@@ -331,7 +362,7 @@ CONTRACT = {
331
362
  "enum": ["1:1", "16:9", "9:16", "3:2", "2:3", "auto"]
332
363
  },
333
364
  "output_resolution": {
334
- "enum": ["480p", "720p"]
365
+ "enum": ["480p", "720p", "1080p"]
335
366
  }
336
367
  }
337
368
  }
@@ -373,7 +404,7 @@ CONTRACT = {
373
404
  "enum": ["1:1", "16:9", "9:16", "3:2", "2:3", "auto"]
374
405
  },
375
406
  "output_resolution": {
376
- "enum": ["480p", "720p"]
407
+ "enum": ["480p", "720p", "1080p"]
377
408
  }
378
409
  }
379
410
  }
@@ -893,6 +924,35 @@ CONTRACT = {
893
924
  "midjourney-v8.1": {}
894
925
  }
895
926
  },
927
+ "minimax-h3/image-to-video": {
928
+ "models": ["minimax-h3"],
929
+ "fields_by_model": {
930
+ "minimax-h3": {
931
+ "duration_seconds": {
932
+ "enum": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
933
+ },
934
+ "output_resolution": {
935
+ "enum": ["768p", "2k"]
936
+ }
937
+ }
938
+ }
939
+ },
940
+ "minimax-h3/text-to-video": {
941
+ "models": ["minimax-h3"],
942
+ "fields_by_model": {
943
+ "minimax-h3": {
944
+ "aspect_ratio": {
945
+ "enum": ["adaptive", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"]
946
+ },
947
+ "duration_seconds": {
948
+ "enum": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
949
+ },
950
+ "output_resolution": {
951
+ "enum": ["768p", "2k"]
952
+ }
953
+ }
954
+ }
955
+ },
896
956
  "nano-banana/edit-image": {
897
957
  "models": ["nano-banana-2-lite", "nano-banana-edit"],
898
958
  "fields_by_model": {
@@ -973,6 +1033,27 @@ CONTRACT = {
973
1033
  "omnihuman-1.5-subject-detection": {}
974
1034
  }
975
1035
  },
1036
+ "openai-transcription/speech-to-text": {
1037
+ "models": ["gpt-transcribe", "whisper-1"],
1038
+ "fields_by_model": {
1039
+ "gpt-transcribe": {
1040
+ "model": {
1041
+ "enum": ["gpt-transcribe"]
1042
+ },
1043
+ "response_format": {
1044
+ "enum": ["json", "text"]
1045
+ }
1046
+ },
1047
+ "whisper-1": {
1048
+ "model": {
1049
+ "enum": ["whisper-1"]
1050
+ },
1051
+ "response_format": {
1052
+ "enum": ["json", "text", "srt", "verbose_json", "vtt"]
1053
+ }
1054
+ }
1055
+ }
1056
+ },
976
1057
  "openai-tts/text-to-speech": {
977
1058
  "models": ["tts-1", "tts-1-hd"],
978
1059
  "fields_by_model": {
@@ -987,6 +1068,9 @@ CONTRACT = {
987
1068
  "aspect_ratio": {
988
1069
  "enum": ["16:9", "4:3", "1:1", "3:4", "9:16", "2:3", "3:2", "21:9"]
989
1070
  },
1071
+ "enable_audio": {
1072
+ "enum": [True, False]
1073
+ },
990
1074
  "output_resolution": {
991
1075
  "enum": ["360p", "540p", "720p", "1080p"]
992
1076
  }
@@ -997,6 +1081,9 @@ CONTRACT = {
997
1081
  "models": ["pixverse-v6"],
998
1082
  "fields_by_model": {
999
1083
  "pixverse-v6": {
1084
+ "enable_audio": {
1085
+ "enum": [True, False]
1086
+ },
1000
1087
  "output_resolution": {
1001
1088
  "enum": ["360p", "540p", "720p", "1080p"]
1002
1089
  }
@@ -1007,6 +1094,9 @@ CONTRACT = {
1007
1094
  "models": ["pixverse-v6"],
1008
1095
  "fields_by_model": {
1009
1096
  "pixverse-v6": {
1097
+ "enable_audio": {
1098
+ "enum": [True, False]
1099
+ },
1010
1100
  "output_resolution": {
1011
1101
  "enum": ["360p", "540p", "720p", "1080p"]
1012
1102
  }
@@ -1020,6 +1110,9 @@ CONTRACT = {
1020
1110
  "aspect_ratio": {
1021
1111
  "enum": ["16:9", "4:3", "1:1", "3:4", "9:16", "2:3", "3:2", "21:9"]
1022
1112
  },
1113
+ "enable_audio": {
1114
+ "enum": [True, False]
1115
+ },
1023
1116
  "output_resolution": {
1024
1117
  "enum": ["360p", "540p", "720p", "1080p"]
1025
1118
  }
@@ -1030,6 +1123,9 @@ CONTRACT = {
1030
1123
  "models": ["pixverse-v6"],
1031
1124
  "fields_by_model": {
1032
1125
  "pixverse-v6": {
1126
+ "enable_audio": {
1127
+ "enum": [True, False]
1128
+ },
1033
1129
  "output_resolution": {
1034
1130
  "enum": ["360p", "540p", "720p", "1080p"]
1035
1131
  }
@@ -1107,6 +1203,60 @@ CONTRACT = {
1107
1203
  }
1108
1204
  }
1109
1205
  },
1206
+ "qwen-3/edit-image": {
1207
+ "models": ["qwen-3-edit-image", "qwen-3-pro-edit-image"],
1208
+ "fields_by_model": {
1209
+ "qwen-3-edit-image": {
1210
+ "aspect_ratio": {
1211
+ "enum": ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16", "21:9"]
1212
+ },
1213
+ "output_format": {
1214
+ "enum": ["png", "jpeg"]
1215
+ },
1216
+ "output_resolution": {
1217
+ "enum": ["1k", "2k"]
1218
+ }
1219
+ },
1220
+ "qwen-3-pro-edit-image": {
1221
+ "aspect_ratio": {
1222
+ "enum": ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16", "21:9"]
1223
+ },
1224
+ "output_format": {
1225
+ "enum": ["png", "jpeg"]
1226
+ },
1227
+ "output_resolution": {
1228
+ "enum": ["1k", "2k"]
1229
+ }
1230
+ }
1231
+ }
1232
+ },
1233
+ "qwen-3/text-to-image": {
1234
+ "models": ["qwen-3-pro-text-to-image", "qwen-3-text-to-image"],
1235
+ "fields_by_model": {
1236
+ "qwen-3-pro-text-to-image": {
1237
+ "aspect_ratio": {
1238
+ "enum": ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16", "21:9"]
1239
+ },
1240
+ "output_format": {
1241
+ "enum": ["png", "jpeg"]
1242
+ },
1243
+ "output_resolution": {
1244
+ "enum": ["1k", "2k"]
1245
+ }
1246
+ },
1247
+ "qwen-3-text-to-image": {
1248
+ "aspect_ratio": {
1249
+ "enum": ["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16", "21:9"]
1250
+ },
1251
+ "output_format": {
1252
+ "enum": ["png", "jpeg"]
1253
+ },
1254
+ "output_resolution": {
1255
+ "enum": ["1k", "2k"]
1256
+ }
1257
+ }
1258
+ }
1259
+ },
1110
1260
  "qwen-image/edit-image": {
1111
1261
  "models": ["qwen-image-edit-image"],
1112
1262
  "fields_by_model": {
@@ -1359,6 +1509,16 @@ CONTRACT = {
1359
1509
  }
1360
1510
  }
1361
1511
  },
1512
+ "suno/add-samples": {
1513
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1514
+ "fields_by_model": {
1515
+ "suno-v4": {},
1516
+ "suno-v4.5": {},
1517
+ "suno-v4.5-plus": {},
1518
+ "suno-v5": {},
1519
+ "suno-v5.5": {}
1520
+ }
1521
+ },
1362
1522
  "suno/add-vocals": {
1363
1523
  "models": ["suno-v4.5-plus", "suno-v5"],
1364
1524
  "fields_by_model": {
@@ -1657,6 +1817,16 @@ CONTRACT = {
1657
1817
  "_": {}
1658
1818
  }
1659
1819
  },
1820
+ "suno/remaster-audio": {
1821
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1822
+ "fields_by_model": {
1823
+ "suno-v4": {},
1824
+ "suno-v4.5": {},
1825
+ "suno-v4.5-plus": {},
1826
+ "suno-v5": {},
1827
+ "suno-v5.5": {}
1828
+ }
1829
+ },
1660
1830
  "suno/replace-section": {
1661
1831
  "models": [],
1662
1832
  "fields_by_model": {
@@ -1680,6 +1850,16 @@ CONTRACT = {
1680
1850
  }
1681
1851
  }
1682
1852
  },
1853
+ "suno/stitch-audio": {
1854
+ "models": ["suno-v4", "suno-v4.5", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1855
+ "fields_by_model": {
1856
+ "suno-v4": {},
1857
+ "suno-v4.5": {},
1858
+ "suno-v4.5-plus": {},
1859
+ "suno-v5": {},
1860
+ "suno-v5.5": {}
1861
+ }
1862
+ },
1683
1863
  "suno/text-to-music": {
1684
1864
  "models": ["suno-v4", "suno-v4.5", "suno-v4.5-all", "suno-v4.5-plus", "suno-v5", "suno-v5.5"],
1685
1865
  "fields_by_model": {
@@ -123,18 +123,20 @@ class HttpClient:
123
123
  self, body: Any
124
124
  ) -> Tuple[Any, Optional[Dict[str, Any]], Optional[Dict[str, Any]], List[Any]]:
125
125
  if isinstance(body, MultipartBody):
126
- data = {key: str(value) for key, value in body.fields.items()}
127
- files: Dict[str, Any] = {}
126
+ files = []
127
+ for key, value in body.fields.items():
128
+ values = value if isinstance(value, (list, tuple)) else [value]
129
+ files.extend((key, (None, str(item))) for item in values)
128
130
  opened: List[Any] = []
129
131
  for key, part in body.files.items():
130
132
  handle = open(part.path, "rb")
131
133
  opened.append(handle)
132
134
  filename = part.filename
133
135
  if part.content_type:
134
- files[key] = (filename, handle, part.content_type)
136
+ files.append((key, (filename, handle, part.content_type)))
135
137
  else:
136
- files[key] = (filename, handle)
137
- return None, data, files, opened
138
+ files.append((key, (filename, handle)))
139
+ return None, None, files, opened
138
140
  if body is not None:
139
141
  return body, None, None, []
140
142
  return None, None, None, []
@@ -162,8 +162,10 @@ class Resource:
162
162
  def _enum_allowed(enum: Sequence[Any], value: Any) -> bool:
163
163
  value_is_num = isinstance(value, (int, float)) and not isinstance(value, bool)
164
164
  for allowed in enum:
165
- allowed_is_num = isinstance(allowed, (int, float)) and not isinstance(allowed, bool)
166
- if allowed_is_num:
165
+ if isinstance(allowed, bool):
166
+ if isinstance(value, bool) and value is allowed:
167
+ return True
168
+ elif isinstance(allowed, (int, float)):
167
169
  if value_is_num and value == allowed:
168
170
  return True
169
171
  elif value_is_num:
@@ -175,32 +177,65 @@ class Resource:
175
177
 
176
178
  def _enforce_rule(self, params: Dict[str, Any], rule: Dict[str, Any]) -> None:
177
179
  conditions = rule.get("when", {})
178
- if not all(self._rule_condition_met(params, key, val) for key, val in conditions.items()):
180
+ if not all(
181
+ self._rule_condition_met(params, key, val) for key, val in conditions.items()
182
+ ):
179
183
  return
180
184
 
181
- context = " and ".join(f"{key} is {val}" for key, val in conditions.items())
185
+ context = " and ".join(
186
+ self._rule_condition_label(key, val) for key, val in conditions.items()
187
+ )
188
+ qualifier = f" when {context}" if context else ""
189
+
182
190
  for field in rule.get("required", []):
183
191
  if not self._field_present(params, field):
184
- raise ValidationError(f"{field} is required when {context}")
192
+ raise ValidationError(f"{field} is required{qualifier}")
193
+
194
+ required_any = rule.get("required_any", [])
195
+ if required_any and not any(self._field_present(params, f) for f in required_any):
196
+ raise ValidationError(f"one of {', '.join(required_any)} is required{qualifier}")
197
+
185
198
  for field in rule.get("forbidden", []):
186
199
  if self._field_present(params, field):
187
- raise ValidationError(f"{field} is not allowed when {context}")
200
+ raise ValidationError(f"{field} is not allowed{qualifier}")
201
+
202
+ for field, allowed in (rule.get("enum") or {}).items():
203
+ if not self._field_present(params, field):
204
+ continue
205
+ if any(str(candidate) == str(params[field]) for candidate in allowed):
206
+ continue
207
+ joined = ", ".join(str(candidate) for candidate in allowed)
208
+ raise ValidationError(f"{field} must be one of: {joined}{qualifier}")
209
+
210
+ def _rule_condition_met(self, params: Dict[str, Any], field: str, condition: Any) -> bool:
211
+ """A ``when`` entry is either ``{"present": bool}`` or a scalar the supplied
212
+ value must equal. Rules never resolve declared defaults."""
213
+ if self._is_presence_condition(condition):
214
+ return self._field_present(params, field) is (condition["present"] is True)
188
215
 
189
- @staticmethod
190
- def _rule_condition_met(params: Dict[str, Any], field: str, value: Any) -> bool:
191
216
  if field not in params:
192
217
  return False
193
- return str(params[field]) == str(value)
218
+ return str(params[field]) == str(condition)
219
+
220
+ @staticmethod
221
+ def _is_presence_condition(condition: Any) -> bool:
222
+ return isinstance(condition, dict) and "present" in condition
223
+
224
+ def _rule_condition_label(self, field: str, condition: Any) -> str:
225
+ if self._is_presence_condition(condition):
226
+ return f"{field} is present" if condition["present"] is True else f"{field} is absent"
227
+ return f"{field} is {condition}"
194
228
 
195
- def _field_present(self, params: Dict[str, Any], field: str) -> bool:
229
+ @classmethod
230
+ def _field_present(cls, params: Dict[str, Any], field: str) -> bool:
196
231
  if field not in params:
197
232
  return False
198
233
  value = params[field]
199
234
  if value is False:
200
235
  return True
201
236
  if isinstance(value, (list, tuple)):
202
- return any(self._present(item) for item in value)
203
- return self._present(value)
237
+ return any(cls._present(item) for item in value)
238
+ return cls._present(value)
204
239
 
205
240
  @staticmethod
206
241
  def _present(value: Any) -> bool:
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -133,7 +133,7 @@ def test_sends_multipart_without_json_content_type():
133
133
  path = handle.name
134
134
  try:
135
135
  body = MultipartBody(
136
- fields={"file_name": "image.png"},
136
+ fields={"file_name": "image.png", "languages[]": ["en", "zh"]},
137
137
  files={"file": MultipartFile(path=path, filename="image.png", content_type="image/png")},
138
138
  )
139
139
  make_client(handler).request("post", "/api/v1/files", body=body)
@@ -142,6 +142,7 @@ def test_sends_multipart_without_json_content_type():
142
142
 
143
143
  assert captured["content_type"].startswith("multipart/form-data")
144
144
  assert b"image.png" in captured["body"]
145
+ assert captured["body"].count(b'name="languages[]"') == 2
145
146
  assert b"png" in captured["body"]
146
147
 
147
148
 
@@ -135,6 +135,16 @@ def test_validate_integer_rejects_bool_and_whole_float():
135
135
  assert _run_validate({"model": "m", "tolerance": 5}) == ""
136
136
 
137
137
 
138
+ def test_validate_boolean_enum_requires_boolean_value():
139
+ schema = {"models": ["m"], "fields_by_model": {"m": {"flag": {"enum": [True, False]}}}}
140
+ resource = SampleResource(FakeHttp())
141
+
142
+ resource._validate_contract(schema, {"model": "m", "flag": True})
143
+ resource._validate_contract(schema, {"model": "m", "flag": False})
144
+ with pytest.raises(ValidationError, match="^flag must be one of: True, False$"):
145
+ resource._validate_contract(schema, {"model": "m", "flag": "true"})
146
+
147
+
138
148
  def test_validate_array_item_count_constraints():
139
149
  schema = {
140
150
  "models": ["m"],
@@ -1 +0,0 @@
1
- __version__ = "0.3.5"
File without changes