runapi-core 0.1.5__tar.gz → 0.2.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 (29) hide show
  1. {runapi_core-0.1.5 → runapi_core-0.2.0}/PKG-INFO +9 -5
  2. runapi_core-0.2.0/pyproject.toml +37 -0
  3. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/contract_gen.py +92 -6
  4. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/errors.py +26 -0
  5. runapi_core-0.2.0/src/runapi/core/version.py +1 -0
  6. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_errors.py +31 -0
  7. runapi_core-0.1.5/pyproject.toml +0 -33
  8. runapi_core-0.1.5/src/runapi/core/version.py +0 -1
  9. {runapi_core-0.1.5 → runapi_core-0.2.0}/README.md +0 -0
  10. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/__init__.py +0 -0
  11. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/auth.py +0 -0
  12. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/config.py +0 -0
  13. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/constants.py +0 -0
  14. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/files.py +0 -0
  15. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/http_client.py +0 -0
  16. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/models.py +0 -0
  17. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/multipart.py +0 -0
  18. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/options.py +0 -0
  19. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/polling.py +0 -0
  20. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/py.typed +0 -0
  21. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/resource.py +0 -0
  22. {runapi_core-0.1.5 → runapi_core-0.2.0}/src/runapi/core/response.py +0 -0
  23. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_auth.py +0 -0
  24. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_config.py +0 -0
  25. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_files.py +0 -0
  26. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_http_client.py +0 -0
  27. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_models.py +0 -0
  28. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_polling.py +0 -0
  29. {runapi_core-0.1.5 → runapi_core-0.2.0}/tests/test_resource.py +0 -0
@@ -1,12 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runapi-core
3
- Version: 0.1.5
4
- Summary: Core module for the RunAPI Python SDK
5
- Project-URL: Homepage, https://runapi.ai
6
- Project-URL: Documentation, https://runapi.ai/docs#sdk
3
+ Version: 0.2.0
4
+ Summary: RunAPI core SDK for JavaScript, Python, Ruby, Go, Java, and PHP
5
+ Project-URL: Homepage, https://runapi.ai/models
6
+ Project-URL: Documentation, https://runapi.ai/docs#runapi-sdks
7
+ Project-URL: Source, https://github.com/runapi-ai/core-sdk
8
+ Project-URL: Issues, https://github.com/runapi-ai/core-sdk/issues
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.2.0
7
11
  Author-email: RunAPI <contact@runapi.ai>
8
12
  License-Expression: Apache-2.0
9
- Keywords: ai,api,runapi,sdk
13
+ Keywords: api,core,golang,gradle,java,maven,pypi,python,ruby,runapi,runapi-ai,sdk,typescript
10
14
  Requires-Python: >=3.9
11
15
  Requires-Dist: httpx>=0.27
12
16
  Description-Content-Type: text/markdown
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "runapi-core"
7
+ dynamic = ["version"]
8
+ description = "RunAPI core SDK for JavaScript, Python, Ruby, Go, Java, and PHP"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0"
12
+ authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
13
+ keywords = ["runapi", "runapi-ai", "core", "api", "sdk", "typescript", "python", "ruby", "golang", "java", "pypi", "maven", "gradle"]
14
+ dependencies = ["httpx>=0.27"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://runapi.ai/models"
18
+ Documentation = "https://runapi.ai/docs#runapi-sdks"
19
+ Source = "https://github.com/runapi-ai/core-sdk"
20
+ Issues = "https://github.com/runapi-ai/core-sdk/issues"
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.2.0"
23
+
24
+ [tool.hatch.version]
25
+ path = "src/runapi/core/version.py"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/runapi"]
29
+
30
+ [tool.uv]
31
+ package = true
32
+
33
+ [dependency-groups]
34
+ dev = ["pytest>=8", "httpx>=0.27"]
35
+
36
+ [tool.runapi]
37
+ slug = "core"
@@ -38,6 +38,13 @@ CONTRACT = {
38
38
  "text-to-speech-turbo-v2.5": {}
39
39
  }
40
40
  },
41
+ "fish-audio/text-to-speech": {
42
+ "models": ["s1", "s2-pro"],
43
+ "fields_by_model": {
44
+ "s1": {},
45
+ "s2-pro": {}
46
+ }
47
+ },
41
48
  "flux-2/remix-image": {
42
49
  "models": ["flux-2-flex-remix-image", "flux-2-pro-remix-image"],
43
50
  "fields_by_model": {
@@ -114,8 +121,16 @@ CONTRACT = {
114
121
  }
115
122
  },
116
123
  "gemini-omni/text-to-video": {
117
- "models": ["gemini-omni-text-to-video"],
124
+ "models": ["gemini-omni-flash-preview", "gemini-omni-text-to-video"],
118
125
  "fields_by_model": {
126
+ "gemini-omni-flash-preview": {
127
+ "aspect_ratio": {
128
+ "enum": ["16:9", "9:16"]
129
+ },
130
+ "output_resolution": {
131
+ "enum": ["720p"]
132
+ }
133
+ },
119
134
  "gemini-omni-text-to-video": {
120
135
  "aspect_ratio": {
121
136
  "enum": ["16:9", "9:16"]
@@ -129,6 +144,13 @@ CONTRACT = {
129
144
  }
130
145
  }
131
146
  },
147
+ "gemini-tts/text-to-speech": {
148
+ "models": ["gemini-2.5-pro-tts", "gemini-3.1-flash-tts"],
149
+ "fields_by_model": {
150
+ "gemini-2.5-pro-tts": {},
151
+ "gemini-3.1-flash-tts": {}
152
+ }
153
+ },
132
154
  "gpt-4o-image/text-to-image": {
133
155
  "models": ["gpt-4o-image"],
134
156
  "fields_by_model": {
@@ -211,7 +233,7 @@ CONTRACT = {
211
233
  }
212
234
  },
213
235
  "grok-imagine/image-to-video": {
214
- "models": ["grok-imagine-image-to-video", "grok-imagine-video-1.5-preview"],
236
+ "models": ["grok-imagine-image-to-video", "grok-imagine-video-1.5-fast", "grok-imagine-video-1.5-preview"],
215
237
  "fields_by_model": {
216
238
  "grok-imagine-image-to-video": {
217
239
  "aspect_ratio": {
@@ -224,6 +246,14 @@ CONTRACT = {
224
246
  "enum": ["480p", "720p"]
225
247
  }
226
248
  },
249
+ "grok-imagine-video-1.5-fast": {
250
+ "aspect_ratio": {
251
+ "enum": ["1:1", "16:9", "9:16", "3:2", "2:3"]
252
+ },
253
+ "output_resolution": {
254
+ "enum": ["480p", "720p"]
255
+ }
256
+ },
227
257
  "grok-imagine-video-1.5-preview": {
228
258
  "aspect_ratio": {
229
259
  "enum": ["1:1", "16:9", "9:16", "3:2", "2:3", "auto"]
@@ -245,7 +275,7 @@ CONTRACT = {
245
275
  }
246
276
  },
247
277
  "grok-imagine/text-to-video": {
248
- "models": ["grok-imagine-text-to-video", "grok-imagine-video-1.5-preview"],
278
+ "models": ["grok-imagine-text-to-video", "grok-imagine-video-1.5-fast", "grok-imagine-video-1.5-preview"],
249
279
  "fields_by_model": {
250
280
  "grok-imagine-text-to-video": {
251
281
  "aspect_ratio": {
@@ -258,6 +288,14 @@ CONTRACT = {
258
288
  "enum": ["480p", "720p"]
259
289
  }
260
290
  },
291
+ "grok-imagine-video-1.5-fast": {
292
+ "aspect_ratio": {
293
+ "enum": ["1:1", "16:9", "9:16", "3:2", "2:3"]
294
+ },
295
+ "output_resolution": {
296
+ "enum": ["480p", "720p"]
297
+ }
298
+ },
261
299
  "grok-imagine-video-1.5-preview": {
262
300
  "aspect_ratio": {
263
301
  "enum": ["1:1", "16:9", "9:16", "3:2", "2:3", "auto"]
@@ -649,6 +687,12 @@ CONTRACT = {
649
687
  }
650
688
  }
651
689
  },
690
+ "midjourney/shorten-prompt": {
691
+ "models": [],
692
+ "fields_by_model": {
693
+ "_": {}
694
+ }
695
+ },
652
696
  "midjourney/text-to-image": {
653
697
  "models": ["midjourney-v8.1"],
654
698
  "fields_by_model": {
@@ -735,6 +779,13 @@ CONTRACT = {
735
779
  "omnihuman-1.5-subject-detection": {}
736
780
  }
737
781
  },
782
+ "openai-tts/text-to-speech": {
783
+ "models": ["tts-1", "tts-1-hd"],
784
+ "fields_by_model": {
785
+ "tts-1": {},
786
+ "tts-1-hd": {}
787
+ }
788
+ },
738
789
  "qwen-2/edit-image": {
739
790
  "models": ["qwen-2-edit-image"],
740
791
  "fields_by_model": {
@@ -890,7 +941,7 @@ CONTRACT = {
890
941
  }
891
942
  },
892
943
  "seedream/edit-image": {
893
- "models": ["seedream-4.5-edit", "seedream-5-lite-edit", "seedream-v4-edit"],
944
+ "models": ["seedream-4.5-edit", "seedream-5-lite-edit", "seedream-5-pro-edit", "seedream-v4-edit"],
894
945
  "fields_by_model": {
895
946
  "seedream-4.5-edit": {
896
947
  "aspect_ratio": {
@@ -904,6 +955,20 @@ CONTRACT = {
904
955
  "aspect_ratio": {
905
956
  "enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"]
906
957
  },
958
+ "output_format": {
959
+ "enum": ["png", "jpeg"]
960
+ },
961
+ "output_quality": {
962
+ "enum": ["basic", "high"]
963
+ }
964
+ },
965
+ "seedream-5-pro-edit": {
966
+ "aspect_ratio": {
967
+ "enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"]
968
+ },
969
+ "output_format": {
970
+ "enum": ["png", "jpeg"]
971
+ },
907
972
  "output_quality": {
908
973
  "enum": ["basic", "high"]
909
974
  }
@@ -922,7 +987,7 @@ CONTRACT = {
922
987
  }
923
988
  },
924
989
  "seedream/text-to-image": {
925
- "models": ["seedream-4.5-text-to-image", "seedream-5-lite-text-to-image", "seedream-v4-text-to-image"],
990
+ "models": ["seedream-4.5-text-to-image", "seedream-5-lite-text-to-image", "seedream-5-pro-text-to-image", "seedream-v4-text-to-image"],
926
991
  "fields_by_model": {
927
992
  "seedream-4.5-text-to-image": {
928
993
  "aspect_ratio": {
@@ -936,6 +1001,20 @@ CONTRACT = {
936
1001
  "aspect_ratio": {
937
1002
  "enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"]
938
1003
  },
1004
+ "output_format": {
1005
+ "enum": ["png", "jpeg"]
1006
+ },
1007
+ "output_quality": {
1008
+ "enum": ["basic", "high"]
1009
+ }
1010
+ },
1011
+ "seedream-5-pro-text-to-image": {
1012
+ "aspect_ratio": {
1013
+ "enum": ["1:1", "4:3", "3:4", "16:9", "9:16", "2:3", "3:2", "21:9"]
1014
+ },
1015
+ "output_format": {
1016
+ "enum": ["png", "jpeg"]
1017
+ },
939
1018
  "output_quality": {
940
1019
  "enum": ["basic", "high"]
941
1020
  }
@@ -1278,7 +1357,14 @@ CONTRACT = {
1278
1357
  "suno/separate-audio-stems": {
1279
1358
  "models": [],
1280
1359
  "fields_by_model": {
1281
- "_": {}
1360
+ "_": {
1361
+ "stem_name": {
1362
+ "enum": ["Lead Vocal", "Drum Kit", "Kick", "Snare", "Risers", "Bass", "Backing Vocals", "Piano", "Electric Guitar", "Percussion", "String Section", "Synth", "Acoustic Guitar", "Sound Effects", "Synth Pad", "Synth Bass", "Guitar", "Brass Section", "Organ", "Electronic Drum Kit", "Lead Electric Guitar", "Synth Keys", "Rhythm Electric Guitar", "Electric Piano", "Upright Bass", "Keyboards", "Distorted Electric Guitar", "Synth Strings", "Synth Lead", "Woodwinds", "Rhythm Acoustic Guitar", "Flute", "Harp", "Tambourine", "Trumpet", "Arpeggiator", "Accordion", "Fiddle", "Pedal Steel Guitar", "Synth Voice", "Violin", "Digital Piano", "Synth Brass", "Mandolin", "Choir", "Banjo", "Bells", "Clarinet", "Tenor Saxophone", "Trombone", "Shaker", "French Horn", "Glockenspiel", "Electric Bass", "Cello", "Timpani", "Harmonica", "Marimba", "Vibraphone", "Lap Steel Guitar", "Saxophone", "Orchestra", "Horns", "Cymbals", "Hand Clap", "Oboe", "Celesta", "Congas", "Drone", "Alto Saxophone", "Double Bass", "Ukulele", "Harpsichord", "Baritone Saxophone", "Xylophone", "Tuba", "Bass Guitar", "Whistle", "Lead Guitar", "Rhodes", "808", "Bongos", "Bassoon", "Cowbell", "Viola", "Sitar", "Steel Drums", "Piccolo", "Theremin", "Bagpipes", "Hi-Hat", "Music Box", "Melodica", "Tabla", "Koto", "Djembe", "Taiko", "Didgeridoo"]
1363
+ },
1364
+ "type": {
1365
+ "enum": ["separate_vocal", "split_stem", "split_stem_advanced"]
1366
+ }
1367
+ }
1282
1368
  }
1283
1369
  },
1284
1370
  "suno/text-to-music": {
@@ -28,6 +28,7 @@ class Error(Exception):
28
28
  self,
29
29
  message: Optional[str] = None,
30
30
  *,
31
+ code: Optional[str] = None,
31
32
  status: Optional[int] = None,
32
33
  request_id: Optional[str] = None,
33
34
  details: Any = None,
@@ -35,6 +36,7 @@ class Error(Exception):
35
36
  ) -> None:
36
37
  super().__init__(message)
37
38
  self.message = message
39
+ self.code = code
38
40
  self.status = status
39
41
  self.request_id = request_id
40
42
  self.details = details
@@ -53,6 +55,7 @@ class Error(Exception):
53
55
  data = {
54
56
  "error": type(self).__name__,
55
57
  "message": self.message,
58
+ "code": self.code,
56
59
  "status": self.status,
57
60
  "request_id": self.request_id,
58
61
  "details": self.details,
@@ -64,6 +67,7 @@ class AuthenticationError(Error):
64
67
  """API key is missing or invalid (HTTP 401)."""
65
68
 
66
69
  def __init__(self, message: str = "Unauthorized", *, status: int = 401, **kwargs: Any) -> None:
70
+ kwargs.setdefault("code", "authentication")
67
71
  super().__init__(message, status=status, **kwargs)
68
72
 
69
73
 
@@ -78,6 +82,7 @@ class RateLimitError(Error):
78
82
  retry_after: Optional[float] = None,
79
83
  **kwargs: Any,
80
84
  ) -> None:
85
+ kwargs.setdefault("code", "rate_limit")
81
86
  super().__init__(message, status=status, **kwargs)
82
87
  self.retry_after = retry_after
83
88
 
@@ -86,6 +91,7 @@ class InsufficientCreditsError(Error):
86
91
  """Account has insufficient credits (HTTP 402)."""
87
92
 
88
93
  def __init__(self, message: str = "Insufficient credits", *, status: int = 402, **kwargs: Any) -> None:
94
+ kwargs.setdefault("code", "insufficient_credits")
89
95
  super().__init__(message, status=status, **kwargs)
90
96
 
91
97
 
@@ -93,6 +99,7 @@ class NotFoundError(Error):
93
99
  """Requested resource does not exist (HTTP 404)."""
94
100
 
95
101
  def __init__(self, message: str = "Not found", *, status: int = 404, **kwargs: Any) -> None:
102
+ kwargs.setdefault("code", "not_found")
96
103
  super().__init__(message, status=status, **kwargs)
97
104
 
98
105
 
@@ -100,6 +107,7 @@ class ValidationError(Error):
100
107
  """Request validation failed (HTTP 400 / 422), or a client-side check failed."""
101
108
 
102
109
  def __init__(self, message: str = "Validation failed", **kwargs: Any) -> None:
110
+ kwargs.setdefault("code", "validation")
103
111
  super().__init__(message, **kwargs)
104
112
 
105
113
 
@@ -107,6 +115,7 @@ class ServiceUnavailableError(Error):
107
115
  """Service is temporarily unavailable (HTTP 503)."""
108
116
 
109
117
  def __init__(self, message: str = "Service unavailable", *, status: Optional[int] = None, **kwargs: Any) -> None:
118
+ kwargs.setdefault("code", "service_unavailable")
110
119
  super().__init__(message, status=503 if status is None else status, **kwargs)
111
120
 
112
121
 
@@ -114,6 +123,7 @@ class ConflictError(Error):
114
123
  """Request conflicts with the current resource state (HTTP 409)."""
115
124
 
116
125
  def __init__(self, message: str = "Conflict", *, status: int = 409, **kwargs: Any) -> None:
126
+ kwargs.setdefault("code", "conflict")
117
127
  super().__init__(message, status=status, **kwargs)
118
128
 
119
129
 
@@ -121,6 +131,7 @@ class ServerError(Error):
121
131
  """Server encountered an internal error (HTTP 5xx)."""
122
132
 
123
133
  def __init__(self, message: str = "Server error", *, status: Optional[int] = None, **kwargs: Any) -> None:
134
+ kwargs.setdefault("code", "server")
124
135
  super().__init__(message, status=500 if status is None else status, **kwargs)
125
136
 
126
137
 
@@ -128,6 +139,7 @@ class NetworkError(Error):
128
139
  """Network connection failed or the request could not be sent."""
129
140
 
130
141
  def __init__(self, message: str = "Network error", **kwargs: Any) -> None:
142
+ kwargs.setdefault("code", "network")
131
143
  super().__init__(message, **kwargs)
132
144
 
133
145
 
@@ -135,6 +147,7 @@ class TimeoutError(Error): # noqa: A001 - intentional SDK error name, parallels
135
147
  """HTTP request exceeded the configured timeout."""
136
148
 
137
149
  def __init__(self, message: str = "Request timed out", **kwargs: Any) -> None:
150
+ kwargs.setdefault("code", "timeout")
138
151
  super().__init__(message, **kwargs)
139
152
 
140
153
 
@@ -142,6 +155,7 @@ class TaskTimeoutError(Error):
142
155
  """Polling for task completion exceeded the maximum wait time."""
143
156
 
144
157
  def __init__(self, message: str = "Task polling timed out", **kwargs: Any) -> None:
158
+ kwargs.setdefault("code", "task_timeout")
145
159
  super().__init__(message, **kwargs)
146
160
 
147
161
 
@@ -149,6 +163,7 @@ class TaskFailedError(Error):
149
163
  """Async task failed during processing."""
150
164
 
151
165
  def __init__(self, message: str = "Task failed", **kwargs: Any) -> None:
166
+ kwargs.setdefault("code", "task_failed")
152
167
  super().__init__(message, **kwargs)
153
168
 
154
169
 
@@ -198,6 +213,7 @@ def error_from_response(response: "Any") -> Error:
198
213
  error_class = STATUS_MAP.get(status, Error)
199
214
 
200
215
  kwargs: Dict[str, Any] = {
216
+ "code": _extract_code(parsed_body),
201
217
  "status": status,
202
218
  "request_id": request_id,
203
219
  "details": parsed_body,
@@ -209,6 +225,16 @@ def error_from_response(response: "Any") -> Error:
209
225
  return error_class(message, **kwargs)
210
226
 
211
227
 
228
+ def _extract_code(body: Any) -> Optional[str]:
229
+ if not isinstance(body, dict):
230
+ return None
231
+ error = body.get("error")
232
+ if not isinstance(error, dict):
233
+ return None
234
+ code = error.get("code")
235
+ return code if isinstance(code, str) and code else None
236
+
237
+
212
238
  def _parse_body(body: Optional[str]) -> Any:
213
239
  if not body:
214
240
  return None
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -70,6 +70,36 @@ def test_extracts_message_from_nested_error():
70
70
  assert error.message == "Nested message"
71
71
 
72
72
 
73
+ def test_preserves_explicit_http_error_code_and_leaves_missing_code_none():
74
+ explicit = errors.error_from_response(
75
+ response(409, body='{"error":{"code":"source_task_not_ready","message":"wait"}}')
76
+ )
77
+ missing = errors.error_from_response(response(409, body='{"error":{"message":"wait"}}'))
78
+
79
+ assert explicit.code == "source_task_not_ready"
80
+ assert missing.code is None
81
+
82
+
83
+ def test_continuation_errors_preserve_codes_and_status_classification():
84
+ cases = [
85
+ (400, "invalid_resource_id", errors.ValidationError),
86
+ (409, "request_conflict", errors.ConflictError),
87
+ (409, "source_task_not_ready", errors.ConflictError),
88
+ (422, "source_task_unusable", errors.ValidationError),
89
+ (422, "continuation_not_supported", errors.ValidationError),
90
+ (429, "rate_limited", errors.RateLimitError),
91
+ (503, "continuation_unavailable", errors.ServiceUnavailableError),
92
+ ]
93
+
94
+ for status, code, error_class in cases:
95
+ error = errors.error_from_response(
96
+ response(status, body=f'{{"error":{{"code":"{code}","message":"failed"}}}}')
97
+ )
98
+ assert isinstance(error, error_class)
99
+ assert error.status == status
100
+ assert error.code == code
101
+
102
+
73
103
  def test_extracts_message_from_errors_array():
74
104
  error = errors.error_from_response(response(400, body='{"errors":["First error"]}'))
75
105
  assert error.message == "First error"
@@ -99,6 +129,7 @@ def test_to_dict_is_compact():
99
129
  assert error.to_dict() == {
100
130
  "error": "AuthenticationError",
101
131
  "message": "Bad key",
132
+ "code": "authentication",
102
133
  "status": 401,
103
134
  "request_id": "req-1",
104
135
  }
@@ -1,33 +0,0 @@
1
- [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "runapi-core"
7
- dynamic = ["version"]
8
- description = "Core module for the RunAPI Python SDK"
9
- readme = "README.md"
10
- requires-python = ">=3.9"
11
- license = "Apache-2.0"
12
- authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
13
- keywords = ["runapi", "ai", "sdk", "api"]
14
- dependencies = ["httpx>=0.27"]
15
-
16
- [project.urls]
17
- Homepage = "https://runapi.ai"
18
- Documentation = "https://runapi.ai/docs#sdk"
19
-
20
- [tool.hatch.version]
21
- path = "src/runapi/core/version.py"
22
-
23
- [tool.hatch.build.targets.wheel]
24
- packages = ["src/runapi"]
25
-
26
- [tool.uv]
27
- package = true
28
-
29
- [dependency-groups]
30
- dev = ["pytest>=8", "httpx>=0.27"]
31
-
32
- [tool.runapi]
33
- slug = "core"
@@ -1 +0,0 @@
1
- __version__ = "0.1.5"
File without changes