courier-encode 0.1.1__tar.gz → 0.1.2__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 (50) hide show
  1. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/workspace.xml +4 -5
  2. {courier_encode-0.1.1 → courier_encode-0.1.2}/PKG-INFO +2 -2
  3. {courier_encode-0.1.1 → courier_encode-0.1.2}/pyproject.toml +2 -2
  4. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/_http.py +11 -0
  5. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/relay.py +2 -2
  6. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_streaming.py +66 -0
  7. {courier_encode-0.1.1 → courier_encode-0.1.2}/uv.lock +1 -1
  8. {courier_encode-0.1.1 → courier_encode-0.1.2}/.gitignore +0 -0
  9. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/.gitignore +0 -0
  10. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/encode.iml +0 -0
  11. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/inspectionProfiles/Project_Default.xml +0 -0
  12. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  13. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/misc.xml +0 -0
  14. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/modules.xml +0 -0
  15. {courier_encode-0.1.1 → courier_encode-0.1.2}/.idea/vcs.xml +0 -0
  16. {courier_encode-0.1.1 → courier_encode-0.1.2}/LICENSE +0 -0
  17. {courier_encode-0.1.1 → courier_encode-0.1.2}/README.md +0 -0
  18. {courier_encode-0.1.1 → courier_encode-0.1.2}/docs.md +0 -0
  19. {courier_encode-0.1.1 → courier_encode-0.1.2}/examples/basic_chat.py +0 -0
  20. {courier_encode-0.1.1 → courier_encode-0.1.2}/examples/intercept.py +0 -0
  21. {courier_encode-0.1.1 → courier_encode-0.1.2}/examples/responses_endpoint.py +0 -0
  22. {courier_encode-0.1.1 → courier_encode-0.1.2}/examples/structured_output.py +0 -0
  23. {courier_encode-0.1.1 → courier_encode-0.1.2}/examples/tools_loop.py +0 -0
  24. {courier_encode-0.1.1 → courier_encode-0.1.2}/examples/whisper_transcribe.py +0 -0
  25. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/__init__.py +0 -0
  26. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/_config.py +0 -0
  27. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/_schema.py +0 -0
  28. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/_streaming.py +0 -0
  29. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/_version.py +0 -0
  30. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/client.py +0 -0
  31. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/errors.py +0 -0
  32. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/messages.py +0 -0
  33. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/py.typed +0 -0
  34. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/responses.py +0 -0
  35. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/session.py +0 -0
  36. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/tools.py +0 -0
  37. {courier_encode-0.1.1 → courier_encode-0.1.2}/src/encode/whisper.py +0 -0
  38. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/__init__.py +0 -0
  39. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/conftest.py +0 -0
  40. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_async.py +0 -0
  41. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_errors.py +0 -0
  42. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_messages.py +0 -0
  43. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_messages_obj.py +0 -0
  44. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_relay_chat.py +0 -0
  45. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_relay_responses.py +0 -0
  46. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_response_format.py +0 -0
  47. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_session.py +0 -0
  48. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_tool_loop.py +0 -0
  49. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_tools_schema.py +0 -0
  50. {courier_encode-0.1.1 → courier_encode-0.1.2}/tests/test_whisper.py +0 -0
@@ -5,12 +5,11 @@
5
5
  </component>
6
6
  <component name="ChangeListManager">
7
7
  <list default="true" id="9259bfb5-884c-4336-83fd-f640b45e88c5" name="Changes" comment="">
8
- <change beforePath="$PROJECT_DIR$/docs.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs.md" afterDir="false" />
9
- <change beforePath="$PROJECT_DIR$/src/encode/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/encode/__init__.py" afterDir="false" />
10
- <change beforePath="$PROJECT_DIR$/src/encode/_streaming.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/encode/_streaming.py" afterDir="false" />
11
- <change beforePath="$PROJECT_DIR$/src/encode/messages.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/encode/messages.py" afterDir="false" />
8
+ <change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
9
+ <change beforePath="$PROJECT_DIR$/src/encode/_http.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/encode/_http.py" afterDir="false" />
12
10
  <change beforePath="$PROJECT_DIR$/src/encode/relay.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/encode/relay.py" afterDir="false" />
13
11
  <change beforePath="$PROJECT_DIR$/tests/test_streaming.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_streaming.py" afterDir="false" />
12
+ <change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" afterPath="$PROJECT_DIR$/uv.lock" afterDir="false" />
14
13
  </list>
15
14
  <option name="SHOW_DIALOG" value="false" />
16
15
  <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -74,7 +73,7 @@
74
73
  <option name="number" value="Default" />
75
74
  <option name="presentableId" value="Default" />
76
75
  <updated>1778249777914</updated>
77
- <workItem from="1778249778942" duration="7832000" />
76
+ <workItem from="1778249778942" duration="11261000" />
78
77
  </task>
79
78
  <servers />
80
79
  </component>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: courier-encode
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Python SDK for OpenAI-compatible inference endpoints (Courier and friends) with auto tool-call loops, structured outputs, and Whisper.
5
5
  Project-URL: Homepage, https://getcourier.ai
6
6
  Project-URL: Documentation, https://getcourier.ai/docs
@@ -211,7 +211,7 @@ License-File: LICENSE
211
211
  Keywords: agents,courier,llm,openai,sdk,tool-calling,whisper
212
212
  Classifier: Development Status :: 3 - Alpha
213
213
  Classifier: Intended Audience :: Developers
214
- Classifier: License :: OSI Approved :: MIT License
214
+ Classifier: License :: OSI Approved :: Apache Software License
215
215
  Classifier: Programming Language :: Python :: 3
216
216
  Classifier: Programming Language :: Python :: 3.10
217
217
  Classifier: Programming Language :: Python :: 3.11
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "courier-encode"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Python SDK for OpenAI-compatible inference endpoints (Courier and friends) with auto tool-call loops, structured outputs, and Whisper."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -14,7 +14,7 @@ keywords = ["openai", "courier", "llm", "sdk", "agents", "tool-calling", "whispe
14
14
  classifiers = [
15
15
  "Development Status :: 3 - Alpha",
16
16
  "Intended Audience :: Developers",
17
- "License :: OSI Approved :: MIT License",
17
+ "License :: OSI Approved :: Apache Software License",
18
18
  "Programming Language :: Python :: 3",
19
19
  "Programming Language :: Python :: 3.10",
20
20
  "Programming Language :: Python :: 3.11",
@@ -47,6 +47,17 @@ def parse_body(resp: httpx.Response) -> Any:
47
47
  def raise_for_status(resp: httpx.Response) -> None:
48
48
  if resp.is_success:
49
49
  return
50
+ # Idempotent for already-buffered responses; required for sync streaming.
51
+ resp.read()
52
+ body = parse_body(resp)
53
+ raise errors.from_envelope(body, status=resp.status_code)
54
+
55
+
56
+ async def araise_for_status(resp: httpx.Response) -> None:
57
+ if resp.is_success:
58
+ return
59
+ # Required for responses obtained via async_client.stream(...).
60
+ await resp.aread()
50
61
  body = parse_body(resp)
51
62
  raise errors.from_envelope(body, status=resp.status_code)
52
63
 
@@ -1442,7 +1442,7 @@ async def _stream_chat_async(
1442
1442
  async with client._http.stream(
1443
1443
  "POST", "/v1/chat/completions", json=payload
1444
1444
  ) as resp:
1445
- _http.raise_for_status(resp)
1445
+ await _http.araise_for_status(resp)
1446
1446
  async for ev in aiter_chat_completions(resp):
1447
1447
  last_chunk = ev.raw
1448
1448
  if ev.type == "content.delta":
@@ -1616,7 +1616,7 @@ async def _stream_responses_async(
1616
1616
  async with client._http.stream(
1617
1617
  "POST", "/v1/responses", json=payload
1618
1618
  ) as resp:
1619
- _http.raise_for_status(resp)
1619
+ await _http.araise_for_status(resp)
1620
1620
  async for ev in aiter_responses(resp):
1621
1621
  if ev.type == "response.output_text.delta":
1622
1622
  delta = (ev.data or {}).get("delta", "")
@@ -270,3 +270,69 @@ def test_responses_stream_with_tools_two_iterations(respx_mock, base_url):
270
270
  assert "content.delta" in types
271
271
  text = "".join(e.data for e in events if e.type == "content.delta")
272
272
  assert text == "It's 72F."
273
+
274
+
275
+ # ---------------------------------------------------------------------------
276
+ # Streaming + error envelope (regression: httpx.ResponseNotRead)
277
+ # ---------------------------------------------------------------------------
278
+
279
+
280
+ _AUTH_ENVELOPE = {"error": {"message": "bad key", "type": "auth_error"}}
281
+
282
+
283
+ def test_stream_chat_sync_error_status_parses_envelope(respx_mock, base_url):
284
+ respx_mock.post(f"{base_url}/v1/chat/completions").mock(
285
+ return_value=httpx.Response(401, json=_AUTH_ENVELOPE)
286
+ )
287
+ handle = encode.relay(
288
+ model="m",
289
+ messages=[{"role": "user", "content": "hi"}],
290
+ stream=True,
291
+ )
292
+ with pytest.raises(encode.errors.AuthError):
293
+ list(handle)
294
+
295
+
296
+ def test_stream_responses_sync_error_status_parses_envelope(respx_mock, base_url):
297
+ respx_mock.post(f"{base_url}/v1/responses").mock(
298
+ return_value=httpx.Response(401, json=_AUTH_ENVELOPE)
299
+ )
300
+ handle = encode.relay(
301
+ model="m",
302
+ input="hi",
303
+ stream=True,
304
+ endpoint="responses",
305
+ )
306
+ with pytest.raises(encode.errors.AuthError):
307
+ list(handle)
308
+
309
+
310
+ @pytest.mark.asyncio
311
+ async def test_stream_chat_async_error_status_parses_envelope(respx_mock, base_url):
312
+ respx_mock.post(f"{base_url}/v1/chat/completions").mock(
313
+ return_value=httpx.Response(401, json=_AUTH_ENVELOPE)
314
+ )
315
+ handle = encode.relay_async(
316
+ model="m",
317
+ messages=[{"role": "user", "content": "hi"}],
318
+ stream=True,
319
+ )
320
+ with pytest.raises(encode.errors.AuthError):
321
+ async for _ in handle:
322
+ pass
323
+
324
+
325
+ @pytest.mark.asyncio
326
+ async def test_stream_responses_async_error_status_parses_envelope(respx_mock, base_url):
327
+ respx_mock.post(f"{base_url}/v1/responses").mock(
328
+ return_value=httpx.Response(401, json=_AUTH_ENVELOPE)
329
+ )
330
+ handle = encode.relay_async(
331
+ model="m",
332
+ input="hi",
333
+ stream=True,
334
+ endpoint="responses",
335
+ )
336
+ with pytest.raises(encode.errors.AuthError):
337
+ async for _ in handle:
338
+ pass
@@ -112,7 +112,7 @@ wheels = [
112
112
 
113
113
  [[package]]
114
114
  name = "courier-encode"
115
- version = "0.1.0"
115
+ version = "0.1.1"
116
116
  source = { editable = "." }
117
117
  dependencies = [
118
118
  { name = "httpx" },
File without changes
File without changes
File without changes