ctxprotocol 0.18.0__tar.gz → 0.19.1__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 (55) hide show
  1. ctxprotocol-0.19.1/CHANGELOG.md +17 -0
  2. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/PKG-INFO +1 -1
  3. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/__init__.py +1 -1
  4. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/__init__.py +2 -0
  5. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/resources/query.py +4 -2
  6. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/types.py +26 -2
  7. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/pyproject.toml +1 -1
  8. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/tests/test_query.py +68 -4
  9. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/uv.lock +1 -1
  10. ctxprotocol-0.18.0/CHANGELOG.md +0 -7
  11. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/.codexignore +0 -0
  12. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/.gitignore +0 -0
  13. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/README.md +0 -0
  14. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/auth/__init__.py +0 -0
  15. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/client.py +0 -0
  16. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/resources/__init__.py +0 -0
  17. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/resources/developer.py +0 -0
  18. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/resources/discovery.py +0 -0
  19. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/client/resources/tools.py +0 -0
  20. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/context/__init__.py +0 -0
  21. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/context/hyperliquid.py +0 -0
  22. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/context/polymarket.py +0 -0
  23. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/context/wallet.py +0 -0
  24. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/contrib/__init__.py +0 -0
  25. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/contrib/search/__init__.py +0 -0
  26. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/contrib/search/core.py +0 -0
  27. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/contrib/search/trace.py +0 -0
  28. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/contrib/search/types.py +0 -0
  29. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/contrib/search/validation.py +0 -0
  30. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/handshake/__init__.py +0 -0
  31. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/ctxprotocol/py.typed +0 -0
  32. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/docs/README.md +0 -0
  33. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/execute_client.py +0 -0
  34. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/polymarket_query_trace_validation.py +0 -0
  35. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/query_client.py +0 -0
  36. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/test_favorites_only.py +0 -0
  37. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/test_get_tool.py +0 -0
  38. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/two_surfaces_client.py +0 -0
  39. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/validation/generate_contributor_search_validation_artifacts.py +0 -0
  40. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/validation/named-regression-iran-boots-on-ground.json +0 -0
  41. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/validation/named-regression-supreme-court-tariffs.json +0 -0
  42. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/validation/shared-capability-miss-unsupported-venue.json +0 -0
  43. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/validation/shared-generic-overlap-best-match.json +0 -0
  44. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/client/validation/shared-still-ambiguous-shortlist.json +0 -0
  45. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/server/hummingbot-contributor/README.md +0 -0
  46. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/server/hummingbot-contributor/env.example +0 -0
  47. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/server/hummingbot-contributor/requirements.txt +0 -0
  48. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/examples/server/hummingbot-contributor/server.py +0 -0
  49. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/polymarket-query-trace-results-py.json +0 -0
  50. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/tests/__init__.py +0 -0
  51. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/tests/contrib_search_validation_cases.py +0 -0
  52. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/tests/test_client.py +0 -0
  53. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/tests/test_contrib_search.py +0 -0
  54. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/tests/test_discovery.py +0 -0
  55. {ctxprotocol-0.18.0 → ctxprotocol-0.19.1}/tests/test_tools.py +0 -0
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.19.1
4
+
5
+ - Fixed `client.query.start()` returning `400 "Invalid query job request"` when called without explicit `tools`. The request body builder now omits optional fields (notably `tools`) when unset instead of serializing them as JSON `null`; the durable jobs endpoint's Zod schema rejects `null` for optional fields. `client.query.run()` and `client.query.stream()` were not failing (the `/api/v1/query` endpoint tolerates `null`), but are now consistent with the TS SDK and MCP behavior.
6
+
7
+ ## 0.19.0
8
+
9
+ - Fixed `client.query.run()`/`client.query.stream()` raising a `ValidationError` on queries that returned a rendered image artifact (e.g. server-rendered chart PNGs). `computedArtifacts` now accepts both `chart` (structured spec + data) and `image` (hosted URL) variants.
10
+ - Added `QueryImageArtifact` (`kind: "image"` with `url`, `alt`, `title`, `content_hash`, `bytes`, `width`, `height`) and made `QueryComputedArtifact` a discriminated union over `kind`.
11
+ - Exported `QueryImageArtifact` from the client entry point alongside `QueryChartArtifact`.
12
+
13
+ ## 0.18.0
14
+
15
+ - Added durable async Query jobs with `client.query.start()`, `client.query.get_status()`, and `client.query.poll()`.
16
+ - Exported `QueryJobStartResult`, `QueryJobStatusResult`, and `QueryJobStatus` from the root and client entry points.
17
+ - Kept existing `client.query.run()` and `client.query.stream()` behavior unchanged.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxprotocol
3
- Version: 0.18.0
3
+ Version: 0.19.1
4
4
  Summary: Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically
5
5
  Project-URL: Homepage, https://ctxprotocol.com
6
6
  Project-URL: Documentation, https://docs.ctxprotocol.com
@@ -31,7 +31,7 @@ Example:
31
31
  For more information, visit: https://ctxprotocol.com
32
32
  """
33
33
 
34
- __version__ = "0.18.0"
34
+ __version__ = "0.19.1"
35
35
 
36
36
  # Re-export everything from client module
37
37
  from ctxprotocol.client import (
@@ -44,6 +44,7 @@ from ctxprotocol.client.types import (
44
44
  QueryChartType,
45
45
  QueryChartValueFormat,
46
46
  QueryComputedArtifact,
47
+ QueryImageArtifact,
47
48
  QueryCost,
48
49
  QueryCompletenessRepairEvent,
49
50
  QueryDeveloperTrace,
@@ -133,6 +134,7 @@ __all__ = [
133
134
  "QueryChartType",
134
135
  "QueryChartValueFormat",
135
136
  "QueryComputedArtifact",
137
+ "QueryImageArtifact",
136
138
  "QueryStreamToolStatusEvent",
137
139
  "QueryStreamTextDeltaEvent",
138
140
  "QueryStreamDeveloperTraceEvent",
@@ -62,8 +62,9 @@ class Query:
62
62
  ) -> dict[str, Any]:
63
63
  request_body: dict[str, Any] = {
64
64
  "query": query,
65
- "tools": tools,
66
65
  }
66
+ if tools is not None:
67
+ request_body["tools"] = tools
67
68
  if stream is not None:
68
69
  request_body["stream"] = stream
69
70
  if resume_from is not None:
@@ -443,9 +444,10 @@ class Query:
443
444
  """
444
445
  request_body: dict[str, Any] = {
445
446
  "query": query,
446
- "tools": tools,
447
447
  "stream": True,
448
448
  }
449
+ if tools is not None:
450
+ request_body["tools"] = tools
449
451
  if resume_from is not None:
450
452
  request_body["resumeFrom"] = (
451
453
  resume_from.model_dump(by_alias=True, exclude_none=True)
@@ -6,7 +6,7 @@ This module contains all Pydantic models and type definitions used by the client
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import Any, Literal, Union
9
+ from typing import Annotated, Any, Literal, Union
10
10
 
11
11
  from pydantic import BaseModel, Field
12
12
 
@@ -929,7 +929,31 @@ class QueryChartArtifact(BaseModel):
929
929
  title: str | None = None
930
930
 
931
931
 
932
- QueryComputedArtifact = QueryChartArtifact
932
+ class QueryImageArtifact(BaseModel):
933
+ """Rendered image artifact (e.g. a server-rendered chart PNG).
934
+
935
+ Emitted by the code interpreter alongside or instead of the structured
936
+ ``chart`` spec. ``url`` points at a hosted, already-rendered image so
937
+ consumers that cannot render a chart spec (image-first surfaces such as
938
+ social posting) can attach it directly.
939
+ """
940
+
941
+ kind: Literal["image"]
942
+ url: str
943
+ alt: str | None = None
944
+ title: str | None = None
945
+ content_hash: str | None = Field(default=None, alias="contentHash")
946
+ bytes: int | None = None
947
+ width: int | None = None
948
+ height: int | None = None
949
+
950
+ model_config = {"populate_by_name": True}
951
+
952
+
953
+ QueryComputedArtifact = Annotated[
954
+ Union[QueryChartArtifact, QueryImageArtifact],
955
+ Field(discriminator="kind"),
956
+ ]
933
957
 
934
958
 
935
959
  class QueryToolCallFailureSample(BaseModel):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ctxprotocol"
7
- version = "0.18.0"
7
+ version = "0.19.1"
8
8
  description = "Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -420,7 +420,6 @@ class TestQueryRun:
420
420
  method="POST",
421
421
  json_body={
422
422
  "query": "What are the top whale movements?",
423
- "tools": None,
424
423
  "stream": True,
425
424
  },
426
425
  extra_headers=None,
@@ -480,7 +479,6 @@ class TestQueryRun:
480
479
  method="POST",
481
480
  json_body={
482
481
  "query": "Analyze whale activity",
483
- "tools": None,
484
482
  "stream": True,
485
483
  "agentModelId": "kimi-k2.6-model",
486
484
  "responseShape": "answer_with_evidence",
@@ -897,6 +895,41 @@ class TestQueryRun:
897
895
  assert mixed_axis.spec.series[1].y_axis == "right"
898
896
  assert mixed_axis.spec.series[1].satisfies == "volume"
899
897
 
898
+ async def test_parses_rendered_image_artifact(self) -> None:
899
+ """Rendered image artifacts (kind: image) deserialize without error."""
900
+ client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
901
+ response = {
902
+ **MOCK_SUCCESS_RESPONSE,
903
+ "computedArtifacts": [
904
+ {
905
+ "kind": "image",
906
+ "url": "https://blob.example.com/charts/abc123.png",
907
+ "alt": "Sector ETF YTD normalized performance chart",
908
+ "title": "Sector ETF YTD Performance",
909
+ "contentHash": "abc123",
910
+ "bytes": 320749,
911
+ "width": 1926,
912
+ "height": 1030,
913
+ },
914
+ ],
915
+ }
916
+ with patch.object(
917
+ client, "fetch_stream", new_callable=AsyncMock
918
+ ) as mock_stream:
919
+ mock_stream.return_value = _make_done_stream_response(response)
920
+ result = await client.query.run("Render a chart image")
921
+
922
+ assert result.computed_artifacts is not None
923
+ assert len(result.computed_artifacts) == 1
924
+ image = result.computed_artifacts[0]
925
+ assert image.kind == "image"
926
+ assert image.url == "https://blob.example.com/charts/abc123.png"
927
+ assert image.title == "Sector ETF YTD Performance"
928
+ assert image.content_hash == "abc123"
929
+ assert image.bytes == 320749
930
+ assert image.width == 1926
931
+ assert image.height == 1030
932
+
900
933
  async def test_parses_shared_ungrounded_capability_miss_fixture(self) -> None:
901
934
  """Ungrounded runtime outcomes deserialize as capability_miss."""
902
935
  client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
@@ -1056,7 +1089,6 @@ class TestQueryStream:
1056
1089
  method="POST",
1057
1090
  json_body={
1058
1091
  "query": "What are whale movements?",
1059
- "tools": None,
1060
1092
  "stream": True,
1061
1093
  },
1062
1094
  extra_headers=None,
@@ -1499,7 +1531,6 @@ class TestQueryJobs:
1499
1531
  method="POST",
1500
1532
  json_body={
1501
1533
  "query": "long query",
1502
- "tools": None,
1503
1534
  "stream": False,
1504
1535
  "responseShape": "evidence_only",
1505
1536
  "includeDataUrl": True,
@@ -1509,6 +1540,39 @@ class TestQueryJobs:
1509
1540
  },
1510
1541
  )
1511
1542
 
1543
+ async def test_start_omits_null_optional_fields(self) -> None:
1544
+ """Regression: optional fields must be absent (not JSON null) in the job body.
1545
+
1546
+ The /api/v1/query/jobs endpoint validates the body with a Zod schema whose
1547
+ optional fields accept `undefined`/absent but reject `null`. Sending
1548
+ `"tools": null` (as the SDK previously did) made query.start() 400 with
1549
+ "Invalid query job request". Optional fields must be omitted when unset,
1550
+ matching the TS SDK (JSON.stringify drops undefined keys).
1551
+ """
1552
+ client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
1553
+ payload = {
1554
+ "status": "running",
1555
+ "jobId": "11111111-1111-4111-8111-111111111111",
1556
+ "pollingTool": "context_query_status",
1557
+ "message": "running",
1558
+ "progress": None,
1559
+ "querySession": None,
1560
+ "createdAt": "2026-06-14T00:00:00.000Z",
1561
+ "updatedAt": "2026-06-14T00:00:00.000Z",
1562
+ }
1563
+
1564
+ with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
1565
+ mock_fetch.return_value = payload
1566
+ await client.query.start(query="long query")
1567
+
1568
+ body = mock_fetch.call_args.kwargs["json_body"]
1569
+ # No optional field may serialize to JSON null.
1570
+ assert all(value is not None for value in body.values()), body
1571
+ # tools specifically must be omitted when not supplied (was the 400 trigger).
1572
+ assert "tools" not in body, body
1573
+ # query is always present and non-null.
1574
+ assert body["query"] == "long query"
1575
+
1512
1576
  async def test_get_status_fetches_query_job(self) -> None:
1513
1577
  client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
1514
1578
  payload = {
@@ -205,7 +205,7 @@ wheels = [
205
205
 
206
206
  [[package]]
207
207
  name = "ctxprotocol"
208
- version = "0.18.0"
208
+ version = "0.19.1"
209
209
  source = { editable = "." }
210
210
  dependencies = [
211
211
  { name = "cryptography" },
@@ -1,7 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.18.0
4
-
5
- - Added durable async Query jobs with `client.query.start()`, `client.query.get_status()`, and `client.query.poll()`.
6
- - Exported `QueryJobStartResult`, `QueryJobStatusResult`, and `QueryJobStatus` from the root and client entry points.
7
- - Kept existing `client.query.run()` and `client.query.stream()` behavior unchanged.
File without changes
File without changes
File without changes