From fa2e13b5747d18aeb478700f1e5426af2fd087a1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:35:22 -0800 Subject: [PATCH] release: 0.13.0 (#135) Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- .stats.yml | 2 +- CHANGELOG.md | 27 ++++++ README.md | 6 +- mypy.ini | 5 +- pyproject.toml | 4 +- requirements-dev.lock | 1 + src/groq/_base_client.py | 12 ++- src/groq/_compat.py | 8 +- src/groq/_utils/_sync.py | 90 +++++++++---------- src/groq/_version.py | 2 +- src/groq/resources/audio/transcriptions.py | 2 + .../audio/transcription_create_params.py | 1 + .../audio/test_transcriptions.py | 4 +- tests/api_resources/chat/test_completions.py | 60 +------------ tests/test_client.py | 38 ++++++++ tests/test_models.py | 8 ++ 17 files changed, 147 insertions(+), 125 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a713055..d52d2b9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.12.0" + ".": "0.13.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index d23e89e..37e3c10 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/groqcloud%2Fgroqcloud-2e2427d7a1c97af4cb989c736fabccf1531532dd3487d330c851db96d6c5da1c.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/groqcloud%2Fgroqcloud-98874226a7dcad763f5fb96534de25a43f9187733a5293fa278b0b61bf71a9b3.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index d56901f..a780cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 0.13.0 (2024-11-28) + +Full Changelog: [v0.12.0...v0.13.0](https://github.com/groq/groq-python/compare/v0.12.0...v0.13.0) + +### Features + +* **api:** api update ([#134](https://github.com/groq/groq-python/issues/134)) ([fde3a90](https://github.com/groq/groq-python/commit/fde3a90bf0d3d37dc46765198445becaf588ee96)) + + +### Bug Fixes + +* **client:** compat with new httpx 0.28.0 release ([#142](https://github.com/groq/groq-python/issues/142)) ([b8abf89](https://github.com/groq/groq-python/commit/b8abf8961949a9698c7eb666562acbaab15ce9ea)) + + +### Chores + +* **internal:** exclude mypy from running on tests ([#141](https://github.com/groq/groq-python/issues/141)) ([fb4456e](https://github.com/groq/groq-python/commit/fb4456e36a6630a3a07e9f1fec09ae335b0554d7)) +* **internal:** fix compat model_dump method when warnings are passed ([#138](https://github.com/groq/groq-python/issues/138)) ([91402fd](https://github.com/groq/groq-python/commit/91402fd2ac19b0be2ec1b360e541c891cb4d651f)) +* rebuild project due to codegen change ([#136](https://github.com/groq/groq-python/issues/136)) ([b79c1f9](https://github.com/groq/groq-python/commit/b79c1f96496074cc38c489f4b79fa63b871354b7)) +* rebuild project due to codegen change ([#137](https://github.com/groq/groq-python/issues/137)) ([2899a88](https://github.com/groq/groq-python/commit/2899a88502cb48aaf99fdc3a277c925ec1137583)) +* remove now unused `cached-property` dep ([#140](https://github.com/groq/groq-python/issues/140)) ([d001e95](https://github.com/groq/groq-python/commit/d001e95f7d3f8882eb741c87f66422ef6343260f)) + + +### Documentation + +* add info log level to readme ([#139](https://github.com/groq/groq-python/issues/139)) ([804a891](https://github.com/groq/groq-python/commit/804a8910914001d9e21cff974b281011979352f3)) + ## 0.12.0 (2024-11-12) Full Changelog: [v0.11.0...v0.12.0](https://github.com/groq/groq-python/compare/v0.11.0...v0.12.0) diff --git a/README.md b/README.md index f690c80..1a2e494 100644 --- a/README.md +++ b/README.md @@ -220,12 +220,14 @@ Note that requests that time out are [retried twice by default](#retries). We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. -You can enable logging by setting the environment variable `GROQ_LOG` to `debug`. +You can enable logging by setting the environment variable `GROQ_LOG` to `info`. ```shell -$ export GROQ_LOG=debug +$ export GROQ_LOG=info ``` +Or to `debug` for more verbose logging. + ### How to tell whether `None` means `null` or missing In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: diff --git a/mypy.ini b/mypy.ini index 9584299..971bfe9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,10 @@ show_error_codes = True # Exclude _files.py because mypy isn't smart enough to apply # the correct type narrowing and as this is an internal module # it's fine to just use Pyright. -exclude = ^(src/groq/_files\.py|_dev/.*\.py)$ +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ^(src/groq/_files\.py|_dev/.*\.py|tests/.*)$ strict_equality = True implicit_reexport = True diff --git a/pyproject.toml b/pyproject.toml index f9e1aa2..236b8a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "groq" -version = "0.12.0" +version = "0.13.0" description = "The official Python library for the groq API" dynamic = ["readme"] license = "Apache-2.0" @@ -14,7 +14,6 @@ dependencies = [ "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", - "cached-property; python_version < '3.8'", ] requires-python = ">= 3.8" classifiers = [ @@ -55,6 +54,7 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", + "nest_asyncio==1.6.0" ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index 88dcb05..5ac9e92 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -51,6 +51,7 @@ mdurl==0.1.2 mypy==1.13.0 mypy-extensions==1.0.0 # via mypy +nest-asyncio==1.6.0 nodeenv==1.8.0 # via pyright nox==2023.4.22 diff --git a/src/groq/_base_client.py b/src/groq/_base_client.py index f94212c..f4622fb 100644 --- a/src/groq/_base_client.py +++ b/src/groq/_base_client.py @@ -792,6 +792,7 @@ def __init__( custom_query: Mapping[str, object] | None = None, _strict_response_validation: bool, ) -> None: + kwargs: dict[str, Any] = {} if limits is not None: warnings.warn( "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", @@ -804,6 +805,7 @@ def __init__( limits = DEFAULT_CONNECTION_LIMITS if transport is not None: + kwargs["transport"] = transport warnings.warn( "The `transport` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -813,6 +815,7 @@ def __init__( raise ValueError("The `http_client` argument is mutually exclusive with `transport`") if proxies is not None: + kwargs["proxies"] = proxies warnings.warn( "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -856,10 +859,9 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, limits=limits, follow_redirects=True, + **kwargs, # type: ignore ) def is_closed(self) -> bool: @@ -1358,6 +1360,7 @@ def __init__( custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: + kwargs: dict[str, Any] = {} if limits is not None: warnings.warn( "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", @@ -1370,6 +1373,7 @@ def __init__( limits = DEFAULT_CONNECTION_LIMITS if transport is not None: + kwargs["transport"] = transport warnings.warn( "The `transport` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -1379,6 +1383,7 @@ def __init__( raise ValueError("The `http_client` argument is mutually exclusive with `transport`") if proxies is not None: + kwargs["proxies"] = proxies warnings.warn( "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -1422,10 +1427,9 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, limits=limits, follow_redirects=True, + **kwargs, # type: ignore ) def is_closed(self) -> bool: diff --git a/src/groq/_compat.py b/src/groq/_compat.py index 4794129..92d9ee6 100644 --- a/src/groq/_compat.py +++ b/src/groq/_compat.py @@ -145,7 +145,8 @@ def model_dump( exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, - warnings=warnings, + # warnings are not supported in Pydantic v1 + warnings=warnings if PYDANTIC_V2 else True, ) return cast( "dict[str, Any]", @@ -213,9 +214,6 @@ def __set_name__(self, owner: type[Any], name: str) -> None: ... # __set__ is not defined at runtime, but @cached_property is designed to be settable def __set__(self, instance: object, value: _T) -> None: ... else: - try: - from functools import cached_property as cached_property - except ImportError: - from cached_property import cached_property as cached_property + from functools import cached_property as cached_property typed_cached_property = cached_property diff --git a/src/groq/_utils/_sync.py b/src/groq/_utils/_sync.py index d0d8103..8b3aaf2 100644 --- a/src/groq/_utils/_sync.py +++ b/src/groq/_utils/_sync.py @@ -1,56 +1,62 @@ from __future__ import annotations +import sys +import asyncio import functools -from typing import TypeVar, Callable, Awaitable +import contextvars +from typing import Any, TypeVar, Callable, Awaitable from typing_extensions import ParamSpec -import anyio -import anyio.to_thread - -from ._reflection import function_has_argument - T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") -# copied from `asyncer`, https://github.com/tiangolo/asyncer -def asyncify( - function: Callable[T_ParamSpec, T_Retval], - *, - cancellable: bool = False, - limiter: anyio.CapacityLimiter | None = None, -) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: +if sys.version_info >= (3, 9): + 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 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) + + +# inspired by `asyncer`, https://github.com/tiangolo/asyncer +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, and that when called, calls the original function - in a worker thread using `anyio.to_thread.run_sync()`. Internally, - `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports - keyword arguments additional to positional arguments and it adds better support for - autocompletion and inline errors for the arguments of the function called and the - return value. - - If the `cancellable` option is enabled and the task waiting for its completion is - cancelled, the thread will still run its course but its return value (or any raised - exception) will be ignored. + 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. - Use it like this: + Usage: - ```Python - def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: - # Do work - return "Some result" + ```python + def blocking_func(arg1, arg2, kwarg1=None): + # blocking code + return result - result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b") - print(result) + result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) ``` ## Arguments `function`: a blocking regular callable (e.g. a function) - `cancellable`: `True` to allow cancellation of the operation - `limiter`: capacity limiter to use to limit the total amount of threads running - (if omitted, the default limiter is used) ## Return @@ -60,22 +66,6 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: """ async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: - partial_f = functools.partial(function, *args, **kwargs) - - # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old - # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid - # surfacing deprecation warnings. - if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"): - return await anyio.to_thread.run_sync( - partial_f, - abandon_on_cancel=cancellable, - limiter=limiter, - ) - - return await anyio.to_thread.run_sync( - partial_f, - cancellable=cancellable, - limiter=limiter, - ) + return await to_thread(function, *args, **kwargs) return wrapper diff --git a/src/groq/_version.py b/src/groq/_version.py index da943c2..f34185a 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.12.0" # x-release-please-version +__version__ = "0.13.0" # x-release-please-version diff --git a/src/groq/resources/audio/transcriptions.py b/src/groq/resources/audio/transcriptions.py index df3d9e9..1fbbef5 100644 --- a/src/groq/resources/audio/transcriptions.py +++ b/src/groq/resources/audio/transcriptions.py @@ -86,6 +86,7 @@ def create( "da", "hu", "ta", + "no", "th", "ur", "hr", @@ -295,6 +296,7 @@ async def create( "da", "hu", "ta", + "no", "th", "ur", "hr", diff --git a/src/groq/types/audio/transcription_create_params.py b/src/groq/types/audio/transcription_create_params.py index 9c09a17..f300076 100644 --- a/src/groq/types/audio/transcription_create_params.py +++ b/src/groq/types/audio/transcription_create_params.py @@ -52,6 +52,7 @@ class TranscriptionCreateParams(TypedDict, total=False): "da", "hu", "ta", + "no", "th", "ur", "hr", diff --git a/tests/api_resources/audio/test_transcriptions.py b/tests/api_resources/audio/test_transcriptions.py index 1ced07b..a583fbc 100644 --- a/tests/api_resources/audio/test_transcriptions.py +++ b/tests/api_resources/audio/test_transcriptions.py @@ -34,7 +34,7 @@ def test_method_create_with_all_params(self, client: Groq) -> None: prompt="prompt", response_format="json", temperature=0, - timestamp_granularities=["word", "segment"], + timestamp_granularities=["word"], ) assert_matches_type(Transcription, transcription, path=["response"]) @@ -85,7 +85,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGroq) -> N prompt="prompt", response_format="json", temperature=0, - timestamp_granularities=["word", "segment"], + timestamp_granularities=["word"], ) assert_matches_type(Transcription, transcription, path=["response"]) diff --git a/tests/api_resources/chat/test_completions.py b/tests/api_resources/chat/test_completions.py index 345a6d4..f89e357 100644 --- a/tests/api_resources/chat/test_completions.py +++ b/tests/api_resources/chat/test_completions.py @@ -48,17 +48,7 @@ def test_method_create_with_all_params(self, client: Groq) -> None: "name": "name", "description": "description", "parameters": {"foo": "bar"}, - }, - { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, + } ], logit_bias={"foo": 0}, logprobs=True, @@ -80,23 +70,7 @@ def test_method_create_with_all_params(self, client: Groq) -> None: "parameters": {"foo": "bar"}, }, "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, + } ], top_logprobs=0, top_p=1, @@ -175,17 +149,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGroq) -> N "name": "name", "description": "description", "parameters": {"foo": "bar"}, - }, - { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, + } ], logit_bias={"foo": 0}, logprobs=True, @@ -207,23 +171,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGroq) -> N "parameters": {"foo": "bar"}, }, "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, + } ], top_logprobs=0, top_p=1, diff --git a/tests/test_client.py b/tests/test_client.py index 6eedb65..1d86aef 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,11 +4,14 @@ import gc import os +import sys import json import asyncio import inspect +import subprocess import tracemalloc from typing import Any, Union, cast +from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -1677,3 +1680,38 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_get_platform(self) -> None: + # A previous implementation of asyncify could leave threads unterminated when + # used with nest_asyncio. + # + # Since nest_asyncio.apply() is global and cannot be un-applied, this + # test is run in a separate process to avoid affecting other tests. + test_code = dedent(""" + import asyncio + import nest_asyncio + import threading + + from groq._utils import asyncify + from groq._base_client import get_platform + + async def test_main() -> None: + result = await asyncify(get_platform)() + print(result) + for thread in threading.enumerate(): + print(thread.name) + + nest_asyncio.apply() + asyncio.run(test_main()) + """) + with subprocess.Popen( + [sys.executable, "-c", test_code], + text=True, + ) as process: + try: + process.wait(2) + if process.returncode: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + except subprocess.TimeoutExpired as e: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e diff --git a/tests/test_models.py b/tests/test_models.py index e486da8..439865c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -561,6 +561,14 @@ class Model(BaseModel): m.model_dump(warnings=False) +def test_compat_method_no_error_for_warnings() -> None: + class Model(BaseModel): + foo: Optional[str] + + m = Model(foo="hello") + assert isinstance(model_dump(m, warnings=False), dict) + + def test_to_json() -> None: class Model(BaseModel): foo: Optional[str] = Field(alias="FOO", default=None)