Skip to content

Commit dcd223c

Browse files
authored
Merge pull request #25 from context-dot-dev/release-please--branches--main--changes--next
release: 0.22.0
2 parents 583d5a8 + 9427b16 commit dcd223c

8 files changed

Lines changed: 153 additions & 10 deletions

File tree

‎.release-please-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.21.0"
2+
".": "0.22.0"
33
}

‎.stats.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 22
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/context-dev/context.dev-e8d4199dcf2a97bcd0858e18db7feb3bb4f5a64c9c2d0ee0105636d28c35227d.yml
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/context-dev/context.dev-c4e3d32c831ceda41c21a08b227addc6969457f9b2824f5afb4297d81ce6b379.yml
33
openapi_spec_hash: 8e825842065c24f11fd3f51fdd705337
4-
config_hash: ff13935d1231ad44cd3822ffe39904b4
4+
config_hash: 70354330f92ce169beabc98696ebb9a3

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## 0.22.0 (2026-05-12)
4+
5+
Full Changelog: [v0.21.0...v0.22.0](https://github.com/context-dot-dev/context-python-sdk/compare/v0.21.0...v0.22.0)
6+
7+
### Features
8+
9+
* **api:** manual updates ([b79c985](https://github.com/context-dot-dev/context-python-sdk/commit/b79c9856d25222663dcb28637b048dc6ea092c76))
10+
* **internal/types:** support eagerly validating pydantic iterators ([6386dc9](https://github.com/context-dot-dev/context-python-sdk/commit/6386dc9f7931b592a5c9aa2874debae51a8f7276))
11+
312
## 0.21.0 (2026-05-11)
413

514
Full Changelog: [v0.20.0...v0.21.0](https://github.com/context-dot-dev/context-python-sdk/compare/v0.20.0...v0.21.0)

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/).
1313

1414
Use the Context Dev MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
1515

16-
[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=context-dev-mcp&config=eyJuYW1lIjoiY29udGV4dC1kZXYtbWNwIiwidHJhbnNwb3J0IjoiaHR0cCIsInVybCI6Imh0dHBzOi8vY29udGV4dC1kZXYuc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1jb250ZXh0LWRldi1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ)
17-
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22context-dev-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fcontext-dev.stlmcp.com%22%2C%22headers%22%3A%7B%22x-context-dev-api-key%22%3A%22My%20API%20Key%22%7D%7D)
16+
[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=context.dev-mcp&config=eyJuYW1lIjoiY29udGV4dC5kZXYtbWNwIiwidHJhbnNwb3J0IjoiaHR0cCIsInVybCI6Imh0dHBzOi8vY29udGV4dC1kZXYuc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1jb250ZXh0LWRldi1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ)
17+
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22context.dev-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fcontext-dev.stlmcp.com%22%2C%22headers%22%3A%7B%22x-context-dev-api-key%22%3A%22My%20API%20Key%22%7D%7D)
1818

1919
> Note: You may need to set environment variables in your MCP client.
2020

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "context.dev"
3-
version = "0.21.0"
3+
version = "0.22.0"
44
description = "The official Python library for the context.dev API"
55
dynamic = ["readme"]
66
license = "Apache-2.0"

‎src/context/dev/_models.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
ClassVar,
2626
Protocol,
2727
Required,
28+
Annotated,
2829
ParamSpec,
30+
TypeAlias,
2931
TypedDict,
3032
TypeGuard,
3133
final,
@@ -79,7 +81,15 @@
7981
from ._constants import RAW_RESPONSE_HEADER
8082

8183
if TYPE_CHECKING:
84+
from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler
85+
from pydantic_core import CoreSchema, core_schema
8286
from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
87+
else:
88+
try:
89+
from pydantic_core import CoreSchema, core_schema
90+
except ImportError:
91+
CoreSchema = None
92+
core_schema = None
8393

8494
__all__ = ["BaseModel", "GenericModel"]
8595

@@ -396,6 +406,76 @@ def model_dump_json(
396406
)
397407

398408

409+
class _EagerIterable(list[_T], Generic[_T]):
410+
"""
411+
Accepts any Iterable[T] input (including generators), consumes it
412+
eagerly, and validates all items upfront.
413+
414+
Validation preserves the original container type where possible
415+
(e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON)
416+
always emits a list — round-tripping through model_dump() will not
417+
restore the original container type.
418+
"""
419+
420+
@classmethod
421+
def __get_pydantic_core_schema__(
422+
cls,
423+
source_type: Any,
424+
handler: GetCoreSchemaHandler,
425+
) -> CoreSchema:
426+
(item_type,) = get_args(source_type) or (Any,)
427+
item_schema: CoreSchema = handler.generate_schema(item_type)
428+
list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema)
429+
430+
return core_schema.no_info_wrap_validator_function(
431+
cls._validate,
432+
list_of_items_schema,
433+
serialization=core_schema.plain_serializer_function_ser_schema(
434+
cls._serialize,
435+
info_arg=False,
436+
),
437+
)
438+
439+
@staticmethod
440+
def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any:
441+
original_type: type[Any] = type(v)
442+
443+
# Normalize to list so list_schema can validate each item
444+
if isinstance(v, list):
445+
items: list[_T] = v
446+
else:
447+
try:
448+
items = list(v)
449+
except TypeError as e:
450+
raise TypeError("Value is not iterable") from e
451+
452+
# Validate items against the inner schema
453+
validated: list[_T] = handler(items)
454+
455+
# Reconstruct original container type
456+
if original_type is list:
457+
return validated
458+
# str(list) produces the list's repr, not a string built from items,
459+
# so skip reconstruction for str and its subclasses.
460+
if issubclass(original_type, str):
461+
return validated
462+
try:
463+
return original_type(validated)
464+
except (TypeError, ValueError):
465+
# If the type cannot be reconstructed, just return the validated list
466+
return validated
467+
468+
@staticmethod
469+
def _serialize(v: Iterable[_T]) -> list[_T]:
470+
"""Always serialize as a list so Pydantic's JSON encoder is happy."""
471+
if isinstance(v, list):
472+
return v
473+
return list(v)
474+
475+
476+
EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable]
477+
478+
399479
def _construct_field(value: object, field: FieldInfo, key: str) -> object:
400480
if value is None:
401481
return field_get_default(field)

‎src/context/dev/_version.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "context.dev"
4-
__version__ = "0.21.0" # x-release-please-version
4+
__version__ = "0.22.0" # x-release-please-version

‎tests/test_models.py‎

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import json
2-
from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
2+
from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast
33
from datetime import datetime, timezone
4-
from typing_extensions import Literal, Annotated, TypeAliasType
4+
from collections import deque
5+
from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType
56

67
import pytest
78
import pydantic
89
from pydantic import Field
910

1011
from context.dev._utils import PropertyInfo
1112
from context.dev._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
12-
from context.dev._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
13+
from context.dev._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type
1314

1415

1516
class BasicModel(BaseModel):
@@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ...
961962
assert model.a.prop == 1
962963
assert isinstance(model.a, Item)
963964
assert model.other == "foo"
965+
966+
967+
# NOTE: Workaround for Pydantic Iterable behavior.
968+
# Iterable fields are replaced with a ValidatorIterator and may be consumed
969+
# during serialization, which can cause subsequent dumps to return empty data.
970+
# See: https://github.com/pydantic/pydantic/issues/9541
971+
@pytest.mark.parametrize(
972+
"data, expected_validated",
973+
[
974+
([1, 2, 3], [1, 2, 3]),
975+
((1, 2, 3), (1, 2, 3)),
976+
(set([1, 2, 3]), set([1, 2, 3])),
977+
(iter([1, 2, 3]), [1, 2, 3]),
978+
([], []),
979+
((x for x in [1, 2, 3]), [1, 2, 3]),
980+
(map(lambda x: x, [1, 2, 3]), [1, 2, 3]),
981+
(frozenset([1, 2, 3]), frozenset([1, 2, 3])),
982+
(deque([1, 2, 3]), deque([1, 2, 3])),
983+
],
984+
ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"],
985+
)
986+
@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
987+
def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None:
988+
class TypeWithIterable(TypedDict):
989+
items: EagerIterable[int]
990+
991+
class Model(BaseModel):
992+
data: TypeWithIterable
993+
994+
m = Model.model_validate({"data": {"items": data}})
995+
assert m.data["items"] == expected_validated
996+
997+
# Verify repeated dumps don't lose data (the original bug)
998+
assert m.model_dump()["data"]["items"] == list(expected_validated)
999+
assert m.model_dump()["data"]["items"] == list(expected_validated)
1000+
1001+
1002+
@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
1003+
def test_iterable_construction_str_falls_back_to_list() -> None:
1004+
# str is iterable (over chars), but str(list_of_chars) produces the list's repr
1005+
# rather than reconstructing a string from items. We special-case str to fall
1006+
# back to list instead of attempting reconstruction.
1007+
class TypeWithIterable(TypedDict):
1008+
items: EagerIterable[str]
1009+
1010+
class Model(BaseModel):
1011+
data: TypeWithIterable
1012+
1013+
m = Model.model_validate({"data": {"items": "hello"}})
1014+
1015+
# falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"])
1016+
assert m.data["items"] == ["h", "e", "l", "l", "o"]
1017+
assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]

0 commit comments

Comments
 (0)