diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 51acdaa4..69508b72 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.37.0" + ".": "0.37.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 57b34843..5dbd77c9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/groqcloud%2Fgroqcloud-aa20a6b53361210f495bb7c44a6edc36f6a59f7c434d79b660d433d7cb419e54.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/groqcloud%2Fgroqcloud-72c846893a3d08d716638abd70d621c0ffc6c4b3aa9f5b8b06dde07c68fa529b.yml openapi_spec_hash: 7d29da2abb6833f7a891a53f49449122 config_hash: 4719968e58eab04c8641284bcf18ef5d diff --git a/CHANGELOG.md b/CHANGELOG.md index 103669e3..9d76bbab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.37.1 (2025-12-04) + +Full Changelog: [v0.37.0...v0.37.1](https://github.com/groq/groq-python/compare/v0.37.0...v0.37.1) + +### Bug Fixes + +* consume stream to prevent hanging ([#260](https://github.com/groq/groq-python/issues/260)) ([42fcb07](https://github.com/groq/groq-python/commit/42fcb07bf0ce1f9910650410574e93ce05590260)) + + +### Chores + +* **docs:** use environment variables for authentication in code snippets ([3ed8c12](https://github.com/groq/groq-python/commit/3ed8c1292fc8e2d02b838c253de2ea60cd483801)) +* update lockfile ([f240076](https://github.com/groq/groq-python/commit/f240076efb2c0ab64fd4c001c1db0cd24e25d14d)) + ## 0.37.0 (2025-12-01) Full Changelog: [v0.36.0...v0.37.0](https://github.com/groq/groq-python/compare/v0.36.0...v0.37.0) diff --git a/README.md b/README.md index ee175c06..add4340b 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ pip install groq[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from groq import DefaultAioHttpClient from groq import AsyncGroq @@ -102,7 +103,7 @@ from groq import AsyncGroq async def main() -> None: async with AsyncGroq( - api_key="My API Key", + api_key=os.environ.get("GROQ_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: chat_completion = await client.chat.completions.create( diff --git a/pyproject.toml b/pyproject.toml index 79a16204..1e5aea6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,22 @@ [project] name = "groq" -version = "0.37.0" +version = "0.37.1" description = "The official Python library for the groq API" dynamic = ["readme"] license = "Apache-2.0" authors = [ { name = "Groq", email = "support@groq.com" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", ] + requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", diff --git a/requirements-dev.lock b/requirements-dev.lock index d48866ec..a09a93cc 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via groq # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via groq # via httpx -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via groq -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,82 +63,87 @@ httpx==0.28.1 # via respx httpx-aiohttp==0.1.9 # via groq -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest pathspec==0.12.1 # via mypy -platformdirs==3.11.0 +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via groq -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via groq -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via groq # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 8b5eb6af..9c630f3c 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via groq # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via groq # via httpx async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via groq -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,25 +45,26 @@ httpx==0.28.1 # via httpx-aiohttp httpx-aiohttp==0.1.9 # via groq -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl pydantic==2.12.5 # via groq pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via groq typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via groq # via multidict # via pydantic @@ -71,5 +72,5 @@ typing-extensions==4.15.0 # via typing-inspection typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp diff --git a/src/groq/_streaming.py b/src/groq/_streaming.py index 2fb1229f..3d4e27af 100644 --- a/src/groq/_streaming.py +++ b/src/groq/_streaming.py @@ -55,50 +55,49 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - try: - for sse in iterator: - if sse.data.startswith("[DONE]"): - break - - if sse.event is None: - data = sse.json() - if is_mapping(data) and data.get("error"): - message = None - error = data.get("error") - if is_mapping(error): - message = error.get("message") - if not message or not isinstance(message, str): - message = "An error occurred during streaming" - - raise APIError( - message=message, - request=self.response.request, - body=data["error"], - ) - - yield process_data(data=data, cast_to=cast_to, response=response) - - else: - data = sse.json() - - if sse.event == "error" and is_mapping(data) and data.get("error"): - message = None - error = data.get("error") - if is_mapping(error): - message = error.get("message") - if not message or not isinstance(message, str): - message = "An error occurred during streaming" - - raise APIError( - message=message, - request=self.response.request, - body=data["error"], - ) - - yield process_data(data={"data": data, "event": sse.event}, cast_to=cast_to, response=response) - finally: - # Ensure the response is closed even if the consumer doesn't read all data - response.close() + for sse in iterator: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + data = sse.json() + if is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data=data, cast_to=cast_to, response=response) + + else: + data = sse.json() + + if sse.event == "error" and is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data={"data": data, "event": sse.event}, cast_to=cast_to, response=response) + # The stream needs to be fully consumed to close the response + for _sse in iterator: + ... def __enter__(self) -> Self: return self @@ -157,50 +156,49 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - try: - async for sse in iterator: - if sse.data.startswith("[DONE]"): - break - - if sse.event is None: - data = sse.json() - if is_mapping(data) and data.get("error"): - message = None - error = data.get("error") - if is_mapping(error): - message = error.get("message") - if not message or not isinstance(message, str): - message = "An error occurred during streaming" - - raise APIError( - message=message, - request=self.response.request, - body=data["error"], - ) - - yield process_data(data=data, cast_to=cast_to, response=response) - - else: - data = sse.json() - - if sse.event == "error" and is_mapping(data) and data.get("error"): - message = None - error = data.get("error") - if is_mapping(error): - message = error.get("message") - if not message or not isinstance(message, str): - message = "An error occurred during streaming" - - raise APIError( - message=message, - request=self.response.request, - body=data["error"], - ) - - yield process_data(data={"data": data, "event": sse.event}, cast_to=cast_to, response=response) - finally: - # Ensure the response is closed even if the consumer doesn't read all data - await response.aclose() + async for sse in iterator: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + data = sse.json() + if is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data=data, cast_to=cast_to, response=response) + + else: + data = sse.json() + + if sse.event == "error" and is_mapping(data) and data.get("error"): + message = None + error = data.get("error") + if is_mapping(error): + message = error.get("message") + if not message or not isinstance(message, str): + message = "An error occurred during streaming" + + raise APIError( + message=message, + request=self.response.request, + body=data["error"], + ) + + yield process_data(data={"data": data, "event": sse.event}, cast_to=cast_to, response=response) + # The stream needs to be fully consumed to close the response + async for _sse in iterator: + ... async def __aenter__(self) -> Self: return self diff --git a/src/groq/_version.py b/src/groq/_version.py index 62480810..4671b72b 100644 --- a/src/groq/_version.py +++ b/src/groq/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "groq" -__version__ = "0.37.0" # x-release-please-version +__version__ = "0.37.1" # x-release-please-version