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.
- {runapi_core-0.3.5 → runapi_core-0.4.0}/PKG-INFO +2 -2
- {runapi_core-0.3.5 → runapi_core-0.4.0}/pyproject.toml +1 -1
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/contract_gen.py +185 -5
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/http_client.py +7 -5
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/resource.py +47 -12
- runapi_core-0.4.0/src/runapi/core/version.py +1 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_http_client.py +2 -1
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_resource.py +10 -0
- runapi_core-0.3.5/src/runapi/core/version.py +0 -1
- {runapi_core-0.3.5 → runapi_core-0.4.0}/README.md +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/__init__.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/account.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/auth.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/config.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/constants.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/errors.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/files.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/models.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/multipart.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/options.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/polling.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/pricing.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/provider_client.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/py.typed +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/src/runapi/core/response.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_account.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_auth.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_config.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_errors.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_files.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_models.py +0 -0
- {runapi_core-0.3.5 → runapi_core-0.4.0}/tests/test_polling.py +0 -0
- {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
|
+
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
136
|
+
files.append((key, (filename, handle, part.content_type)))
|
|
135
137
|
else:
|
|
136
|
-
files
|
|
137
|
-
return None,
|
|
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
|
-
|
|
166
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
203
|
-
return
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|