Skip to content
Open
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.2.1"
".": "0.2.2"
}
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## 0.2.2 (2025-11-12)

Full Changelog: [v0.2.1...v0.2.2](https://github.com/miruml/python-agent-sdk/compare/v0.2.1...v0.2.2)

### Bug Fixes

* **client:** close streams without requiring full consumption ([842ca7c](https://github.com/miruml/python-agent-sdk/commit/842ca7c72218a02fa2249ee68fbcc1645a2193d5))
* compat with Python 3.14 ([ef95edb](https://github.com/miruml/python-agent-sdk/commit/ef95edbaa00a842f9d40c30d329f6398851582d0))
* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([d607c00](https://github.com/miruml/python-agent-sdk/commit/d607c004fb803cd532fa344b44ab3b3404bdeb6e))


### Chores

* bump `httpx-aiohttp` version to 0.1.9 ([74ef8cb](https://github.com/miruml/python-agent-sdk/commit/74ef8cbc0912a8a5f32c62d1f5e2d2c9e6967358))
* **internal/tests:** avoid race condition with implicit client cleanup ([2bd8067](https://github.com/miruml/python-agent-sdk/commit/2bd80673bbdb96bbfd82e93a1e927542211bf890))
* **internal:** detect missing future annotations with ruff ([5749281](https://github.com/miruml/python-agent-sdk/commit/5749281cfd3192819fdaa6faa3409932a6a8e15e))
* **internal:** grammar fix (it's -> its) ([3d0e23a](https://github.com/miruml/python-agent-sdk/commit/3d0e23a5a6b703718047bf698ad41f154133abcc))
* **package:** drop Python 3.8 support ([5544c35](https://github.com/miruml/python-agent-sdk/commit/5544c352c00a7c5d1ec53a90c0719ce96fd5b846))

## 0.2.1 (2025-09-21)

Full Changelog: [v0.2.0...v0.2.1](https://github.com/miruml/python-agent-sdk/compare/v0.2.0...v0.2.1)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- prettier-ignore -->
[![PyPI version](https://img.shields.io/pypi/v/miru_agent_sdk.svg?label=pypi%20(stable))](https://pypi.org/project/miru_agent_sdk/)

The Miru Python library provides convenient access to the Miru REST API from any Python 3.8+
The Miru Python library provides convenient access to the Miru REST API from any Python 3.9+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).

Expand Down Expand Up @@ -344,7 +344,7 @@ print(miru_agent_sdk.__version__)

## Requirements

Python 3.8 or higher.
Python 3.9 or higher.

## Contributing

Expand Down
13 changes: 8 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "miru_agent_sdk"
version = "0.2.1"
version = "0.2.2"
description = "The official Python library for the miru API"
dynamic = ["readme"]
license = "MIT"
Expand All @@ -15,11 +15,10 @@ dependencies = [
"distro>=1.7.0, <2",
"sniffio",
]
requires-python = ">= 3.8"
requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand All @@ -39,7 +38,7 @@ Homepage = "https://github.com/miruml/python-agent-sdk"
Repository = "https://github.com/miruml/python-agent-sdk"

[project.optional-dependencies]
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"]
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]

[tool.rye]
managed = true
Expand Down Expand Up @@ -141,7 +140,7 @@ filterwarnings = [
# there are a couple of flags that are still disabled by
# default in strict mode as they are experimental and niche.
typeCheckingMode = "strict"
pythonVersion = "3.8"
pythonVersion = "3.9"

exclude = [
"_dev",
Expand Down Expand Up @@ -224,6 +223,8 @@ select = [
"B",
# remove unused imports
"F401",
# check for missing future annotations
"FA102",
# bare except statements
"E722",
# unused arguments
Expand All @@ -246,6 +247,8 @@ unfixable = [
"T203",
]

extend-safe-fixes = ["FA102"]

[tool.ruff.lint.flake8-tidy-imports.banned-api]
"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead"

Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ httpx==0.28.1
# via httpx-aiohttp
# via miru-agent-sdk
# via respx
httpx-aiohttp==0.1.8
httpx-aiohttp==0.1.9
# via miru-agent-sdk
idna==3.4
# via anyio
Expand Down
2 changes: 1 addition & 1 deletion requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ httpcore==1.0.9
httpx==0.28.1
# via httpx-aiohttp
# via miru-agent-sdk
httpx-aiohttp==0.1.8
httpx-aiohttp==0.1.9
# via miru-agent-sdk
idna==3.4
# via anyio
Expand Down
52 changes: 37 additions & 15 deletions src/miru_agent_sdk/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import inspect
import weakref
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
from datetime import date, datetime
from typing_extensions import (
Expand Down Expand Up @@ -256,32 +257,41 @@ def model_dump(
mode: Literal["json", "python"] | str = "python",
include: IncEx | None = None,
exclude: IncEx | None = None,
context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
context: dict[str, Any] | None = None,
serialize_as_any: bool = False,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump

Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.

Args:
mode: The mode in which `to_python` should run.
If mode is 'json', the dictionary will only contain JSON serializable types.
If mode is 'python', the dictionary may contain any Python objects.
include: A list of fields to include in the output.
exclude: A list of fields to exclude from the output.
If mode is 'json', the output will only contain JSON serializable types.
If mode is 'python', the output may contain non-JSON-serializable Python objects.
include: A set of fields to include in the output.
exclude: A set of fields to exclude from the output.
context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
exclude_unset: Whether to exclude fields that are unset or None from the output.
exclude_defaults: Whether to exclude fields that are set to their default value from the output.
exclude_none: Whether to exclude fields that have a value of `None` from the output.
round_trip: Whether to enable serialization and deserialization round-trip support.
warnings: Whether to log warnings when invalid fields are encountered.
exclude_unset: Whether to exclude fields that have not been explicitly set.
exclude_defaults: Whether to exclude fields that are set to their default value.
exclude_none: Whether to exclude fields that have a value of `None`.
exclude_computed_fields: Whether to exclude computed fields.
While this can be useful for round-tripping, it is usually recommended to use the dedicated
`round_trip` parameter instead.
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
fallback: A function to call when an unknown value is encountered. If not provided,
a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.

Returns:
A dictionary representation of the model.
Expand All @@ -298,6 +308,8 @@ def model_dump(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
if exclude_computed_fields != False:
raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
Expand All @@ -314,15 +326,17 @@ def model_dump_json(
self,
*,
indent: int | None = None,
ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
context: dict[str, Any] | None = None,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> str:
Expand Down Expand Up @@ -354,6 +368,10 @@ def model_dump_json(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
if ensure_ascii != False:
raise ValueError("ensure_ascii is only supported in Pydantic v2")
if exclude_computed_fields != False:
raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
Expand Down Expand Up @@ -573,6 +591,9 @@ class CachedDiscriminatorType(Protocol):
__discriminator__: DiscriminatorDetails


DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()


class DiscriminatorDetails:
field_name: str
"""The name of the discriminator field in the variant class, e.g.
Expand Down Expand Up @@ -615,8 +636,9 @@ def __init__(


def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
if isinstance(union, CachedDiscriminatorType):
return union.__discriminator__
cached = DISCRIMINATOR_CACHE.get(union)
if cached is not None:
return cached

discriminator_field_name: str | None = None

Expand Down Expand Up @@ -669,7 +691,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
discriminator_field=discriminator_field_name,
discriminator_alias=discriminator_alias,
)
cast(CachedDiscriminatorType, union).__discriminator__ = details
DISCRIMINATOR_CACHE.setdefault(union, details)
return details


Expand Down
10 changes: 4 additions & 6 deletions src/miru_agent_sdk/_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]:
for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)

# Ensure the entire stream is consumed
for _sse in iterator:
...
# As we might not fully consume the response stream, we need to close it explicitly
response.close()

def __enter__(self) -> Self:
return self
Expand Down Expand Up @@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]:
async for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)

# Ensure the entire stream is consumed
async for _sse in iterator:
...
# As we might not fully consume the response stream, we need to close it explicitly
await response.aclose()

async def __aenter__(self) -> Self:
return self
Expand Down
34 changes: 3 additions & 31 deletions src/miru_agent_sdk/_utils/_sync.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from __future__ import annotations

import sys
import asyncio
import functools
import contextvars
from typing import Any, TypeVar, Callable, Awaitable
from typing import TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec

import anyio
Expand All @@ -15,34 +13,11 @@
T_ParamSpec = ParamSpec("T_ParamSpec")


if sys.version_info >= (3, 9):
_asyncio_to_thread = asyncio.to_thread
else:
# backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
# for Python 3.8 support
async def _asyncio_to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> Any:
"""Asynchronously run function *func* in a separate thread.

Any *args and **kwargs supplied for this function are directly passed
to *func*. Also, the current :class:`contextvars.Context` is propagated,
allowing context variables from the main thread to be accessed in the
separate thread.

Returns a coroutine that can be awaited to get the eventual result of *func*.
"""
loop = asyncio.events.get_running_loop()
ctx = contextvars.copy_context()
func_call = functools.partial(ctx.run, func, *args, **kwargs)
return await loop.run_in_executor(None, func_call)


async def to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> T_Retval:
if sniffio.current_async_library() == "asyncio":
return await _asyncio_to_thread(func, *args, **kwargs)
return await asyncio.to_thread(func, *args, **kwargs)

return await anyio.to_thread.run_sync(
functools.partial(func, *args, **kwargs),
Expand All @@ -53,10 +28,7 @@ async def to_thread(
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
Take a blocking function and create an async one that receives the same
positional and keyword arguments. For python version 3.9 and above, it uses
asyncio.to_thread to run the function in a separate thread. For python version
3.8, it uses locally defined copy of the asyncio.to_thread function which was
introduced in python 3.9.
positional and keyword arguments.

Usage:

Expand Down
2 changes: 1 addition & 1 deletion src/miru_agent_sdk/_utils/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
# Type safe methods for narrowing types with TypeVars.
# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown],
# however this cause Pyright to rightfully report errors. As we know we don't
# care about the contained types we can safely use `object` in it's place.
# care about the contained types we can safely use `object` in its place.
#
# There are two separate functions defined, `is_*` and `is_*_t` for different use cases.
# `is_*` is for when you're dealing with an unknown input
Expand Down
2 changes: 1 addition & 1 deletion src/miru_agent_sdk/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "miru_agent_sdk"
__version__ = "0.2.1" # x-release-please-version
__version__ = "0.2.2" # x-release-please-version
Loading