runapi-core 0.3.6__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.6 → runapi_core-0.4.0}/PKG-INFO +2 -2
  2. {runapi_core-0.3.6 → runapi_core-0.4.0}/pyproject.toml +1 -1
  3. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/contract_gen.py +149 -5
  4. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/http_client.py +7 -5
  5. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/resource.py +43 -10
  6. runapi_core-0.4.0/src/runapi/core/version.py +1 -0
  7. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_http_client.py +2 -1
  8. runapi_core-0.3.6/src/runapi/core/version.py +0 -1
  9. {runapi_core-0.3.6 → runapi_core-0.4.0}/README.md +0 -0
  10. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/__init__.py +0 -0
  11. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/account.py +0 -0
  12. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/auth.py +0 -0
  13. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/config.py +0 -0
  14. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/constants.py +0 -0
  15. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/errors.py +0 -0
  16. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/files.py +0 -0
  17. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/models.py +0 -0
  18. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/multipart.py +0 -0
  19. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/options.py +0 -0
  20. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/polling.py +0 -0
  21. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/pricing.py +0 -0
  22. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/provider_client.py +0 -0
  23. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/py.typed +0 -0
  24. {runapi_core-0.3.6 → runapi_core-0.4.0}/src/runapi/core/response.py +0 -0
  25. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_account.py +0 -0
  26. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_auth.py +0 -0
  27. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_config.py +0 -0
  28. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_errors.py +0 -0
  29. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_files.py +0 -0
  30. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_models.py +0 -0
  31. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_polling.py +0 -0
  32. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_pricing.py +0 -0
  33. {runapi_core-0.3.6 → runapi_core-0.4.0}/tests/test_resource.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runapi-core
3
- Version: 0.3.6
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.6
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.6"
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
  }
@@ -1036,6 +1123,9 @@ CONTRACT = {
1036
1123
  "models": ["pixverse-v6"],
1037
1124
  "fields_by_model": {
1038
1125
  "pixverse-v6": {
1126
+ "enable_audio": {
1127
+ "enum": [True, False]
1128
+ },
1039
1129
  "output_resolution": {
1040
1130
  "enum": ["360p", "540p", "720p", "1080p"]
1041
1131
  }
@@ -1113,6 +1203,60 @@ CONTRACT = {
1113
1203
  }
1114
1204
  }
1115
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
+ },
1116
1260
  "qwen-image/edit-image": {
1117
1261
  "models": ["qwen-image-edit-image"],
1118
1262
  "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, []
@@ -177,32 +177,65 @@ class Resource:
177
177
 
178
178
  def _enforce_rule(self, params: Dict[str, Any], rule: Dict[str, Any]) -> None:
179
179
  conditions = rule.get("when", {})
180
- 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
+ ):
181
183
  return
182
184
 
183
- 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
+
184
190
  for field in rule.get("required", []):
185
191
  if not self._field_present(params, field):
186
- 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
+
187
198
  for field in rule.get("forbidden", []):
188
199
  if self._field_present(params, field):
189
- 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)
190
215
 
191
- @staticmethod
192
- def _rule_condition_met(params: Dict[str, Any], field: str, value: Any) -> bool:
193
216
  if field not in params:
194
217
  return False
195
- 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}"
196
228
 
197
- 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:
198
231
  if field not in params:
199
232
  return False
200
233
  value = params[field]
201
234
  if value is False:
202
235
  return True
203
236
  if isinstance(value, (list, tuple)):
204
- return any(self._present(item) for item in value)
205
- return self._present(value)
237
+ return any(cls._present(item) for item in value)
238
+ return cls._present(value)
206
239
 
207
240
  @staticmethod
208
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
 
@@ -1 +0,0 @@
1
- __version__ = "0.3.6"
File without changes