From ad8f1e720c3ef231f22d98e9640182b9139db6a0 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 3 Jul 2024 01:12:11 +0000 Subject: [PATCH] feat(api): OpenAPI spec update via Stainless API --- .gitignore | 1 + .stats.yml | 2 +- pyproject.toml | 16 +++++++++ requirements-dev.lock | 10 +++++- requirements.lock | 3 +- src/runloop_api_client/_base_client.py | 20 +++++++++-- src/runloop_api_client/_models.py | 27 +++++++++++++++ src/runloop_api_client/_utils/__init__.py | 5 ++- src/runloop_api_client/_utils/_reflection.py | 34 +++++++++++++++++++ .../resources/devboxes/devboxes.py | 8 +++++ .../types/devbox_create_params.py | 3 ++ src/runloop_api_client/types/devbox_view.py | 3 ++ .../types/devboxes/devbox_logs_list_view.py | 4 +++ tests/api_resources/test_devboxes.py | 2 ++ 14 files changed, 132 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 0f9a66a97..877974080 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.prism.log .vscode _dev diff --git a/.stats.yml b/.stats.yml index 5484ced01..abad5da29 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-ad7847a96db411953d1b7601aa60c0da482335255dd581c26b04d533cc99d930.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-ce4dc6d4d8a6587f304abdd5adf686e08612cab1c5f1cae3cdf8e20ff4a98d2b.yml diff --git a/pyproject.toml b/pyproject.toml index d77d92bc3..b4626fab8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ dev-dependencies = [ "nox", "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", + "rich>=13.7.1", ] @@ -99,6 +100,21 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/runloop_api_client"] +[tool.hatch.build.targets.sdist] +# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) +include = [ + "/*.toml", + "/*.json", + "/*.lock", + "/*.md", + "/mypy.ini", + "/noxfile.py", + "bin/*", + "examples/*", + "src/*", + "tests/*", +] + [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" diff --git a/requirements-dev.lock b/requirements-dev.lock index 0852c56d1..18a7b48a4 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,7 +10,7 @@ -e file:. annotated-types==0.6.0 # via pydantic -anyio==4.1.0 +anyio==4.4.0 # via httpx # via runloop-api-client argcomplete==3.1.2 @@ -44,6 +44,10 @@ idna==3.4 importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py mypy==1.7.1 mypy-extensions==1.0.0 # via mypy @@ -63,6 +67,8 @@ pydantic==2.7.1 # via runloop-api-client pydantic-core==2.18.2 # via pydantic +pygments==2.18.0 + # via rich pyright==1.1.364 pytest==7.1.1 # via pytest-asyncio @@ -72,6 +78,7 @@ python-dateutil==2.8.2 pytz==2023.3.post1 # via dirty-equals respx==0.20.2 +rich==13.7.1 ruff==0.1.9 setuptools==68.2.2 # via nodeenv @@ -86,6 +93,7 @@ tomli==2.0.1 # via mypy # via pytest typing-extensions==4.8.0 + # via anyio # via mypy # via pydantic # via pydantic-core diff --git a/requirements.lock b/requirements.lock index 70c5bb66c..4b4984989 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,7 +10,7 @@ -e file:. annotated-types==0.6.0 # via pydantic -anyio==4.1.0 +anyio==4.4.0 # via httpx # via runloop-api-client certifi==2023.7.22 @@ -38,6 +38,7 @@ sniffio==1.3.0 # via httpx # via runloop-api-client typing-extensions==4.8.0 + # via anyio # via pydantic # via pydantic-core # via runloop-api-client diff --git a/src/runloop_api_client/_base_client.py b/src/runloop_api_client/_base_client.py index 7e585689b..8537b9733 100644 --- a/src/runloop_api_client/_base_client.py +++ b/src/runloop_api_client/_base_client.py @@ -58,6 +58,7 @@ HttpxSendArgs, AsyncTransport, RequestOptions, + HttpxRequestFiles, ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping @@ -459,6 +460,7 @@ def _build_request( headers = self._build_headers(options) params = _merge_mappings(self.default_query, options.params) content_type = headers.get("Content-Type") + files = options.files # If the given Content-Type header is multipart/form-data then it # has to be removed so that httpx can generate the header with @@ -472,7 +474,7 @@ def _build_request( headers.pop("Content-Type") # As we are now sending multipart/form-data instead of application/json - # we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding if json_data: if not is_dict(json_data): raise TypeError( @@ -480,6 +482,15 @@ def _build_request( ) kwargs["data"] = self._serialize_multipartform(json_data) + # httpx determines whether or not to send a "multipart/form-data" + # request based on the truthiness of the "files" argument. + # This gets around that issue by generating a dict value that + # evaluates to true. + # + # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 + if not files: + files = cast(HttpxRequestFiles, ForceMultipartDict()) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -492,7 +503,7 @@ def _build_request( # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, json=json_data, - files=options.files, + files=files, **kwargs, ) @@ -1863,6 +1874,11 @@ def make_request_options( return options +class ForceMultipartDict(Dict[str, None]): + def __bool__(self) -> bool: + return True + + class OtherPlatform: def __init__(self, name: str) -> None: self.name = name diff --git a/src/runloop_api_client/_models.py b/src/runloop_api_client/_models.py index 75c68cc73..5d95bb4b2 100644 --- a/src/runloop_api_client/_models.py +++ b/src/runloop_api_client/_models.py @@ -10,6 +10,7 @@ ClassVar, Protocol, Required, + ParamSpec, TypedDict, TypeGuard, final, @@ -67,6 +68,9 @@ __all__ = ["BaseModel", "GenericModel"] _T = TypeVar("_T") +_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") + +P = ParamSpec("P") @runtime_checkable @@ -379,6 +383,29 @@ def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericMo return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + def construct_type(*, value: object, type_: object) -> object: """Loose coercion to the expected type with construction of nested values. diff --git a/src/runloop_api_client/_utils/__init__.py b/src/runloop_api_client/_utils/__init__.py index 667e2473f..3efe66c8e 100644 --- a/src/runloop_api_client/_utils/__init__.py +++ b/src/runloop_api_client/_utils/__init__.py @@ -49,4 +49,7 @@ maybe_transform as maybe_transform, async_maybe_transform as async_maybe_transform, ) -from ._reflection import function_has_argument as function_has_argument +from ._reflection import ( + function_has_argument as function_has_argument, + assert_signatures_in_sync as assert_signatures_in_sync, +) diff --git a/src/runloop_api_client/_utils/_reflection.py b/src/runloop_api_client/_utils/_reflection.py index e134f58e0..9a53c7bd2 100644 --- a/src/runloop_api_client/_utils/_reflection.py +++ b/src/runloop_api_client/_utils/_reflection.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import inspect from typing import Any, Callable @@ -6,3 +8,35 @@ def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: """Returns whether or not the given function has a specific parameter""" sig = inspect.signature(func) return arg_name in sig.parameters + + +def assert_signatures_in_sync( + source_func: Callable[..., Any], + check_func: Callable[..., Any], + *, + exclude_params: set[str] = set(), +) -> None: + """Ensure that the signature of the second function matches the first.""" + + check_sig = inspect.signature(check_func) + source_sig = inspect.signature(source_func) + + errors: list[str] = [] + + for name, source_param in source_sig.parameters.items(): + if name in exclude_params: + continue + + custom_param = check_sig.parameters.get(name) + if not custom_param: + errors.append(f"the `{name}` param is missing") + continue + + if custom_param.annotation != source_param.annotation: + errors.append( + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(source_param.annotation)}" + ) + continue + + if errors: + raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index f88876d2f..7d03a0706 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -57,6 +57,7 @@ def create( code_handle: str | NotGiven = NOT_GIVEN, entrypoint: str | NotGiven = NOT_GIVEN, environment_variables: Dict[str, str] | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, setup_commands: List[str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -79,6 +80,8 @@ def create( environment_variables: (Optional) Environment variables used to configure your Devbox. + name: (Optional) A user specified name to give the Devbox. + setup_commands: (Optional) List of commands needed to set up your Devbox. Examples might include fetching a tool or building your dependencies. Runloop will look optimize these steps for you. @@ -98,6 +101,7 @@ def create( "code_handle": code_handle, "entrypoint": entrypoint, "environment_variables": environment_variables, + "name": name, "setup_commands": setup_commands, }, devbox_create_params.DevboxCreateParams, @@ -272,6 +276,7 @@ async def create( code_handle: str | NotGiven = NOT_GIVEN, entrypoint: str | NotGiven = NOT_GIVEN, environment_variables: Dict[str, str] | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, setup_commands: List[str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -294,6 +299,8 @@ async def create( environment_variables: (Optional) Environment variables used to configure your Devbox. + name: (Optional) A user specified name to give the Devbox. + setup_commands: (Optional) List of commands needed to set up your Devbox. Examples might include fetching a tool or building your dependencies. Runloop will look optimize these steps for you. @@ -313,6 +320,7 @@ async def create( "code_handle": code_handle, "entrypoint": entrypoint, "environment_variables": environment_variables, + "name": name, "setup_commands": setup_commands, }, devbox_create_params.DevboxCreateParams, diff --git a/src/runloop_api_client/types/devbox_create_params.py b/src/runloop_api_client/types/devbox_create_params.py index 588469cf4..49834e010 100644 --- a/src/runloop_api_client/types/devbox_create_params.py +++ b/src/runloop_api_client/types/devbox_create_params.py @@ -22,6 +22,9 @@ class DevboxCreateParams(TypedDict, total=False): environment_variables: Dict[str, str] """(Optional) Environment variables used to configure your Devbox.""" + name: str + """(Optional) A user specified name to give the Devbox.""" + setup_commands: List[str] """(Optional) List of commands needed to set up your Devbox. diff --git a/src/runloop_api_client/types/devbox_view.py b/src/runloop_api_client/types/devbox_view.py index 826b180e9..5dec94295 100644 --- a/src/runloop_api_client/types/devbox_view.py +++ b/src/runloop_api_client/types/devbox_view.py @@ -14,6 +14,9 @@ class DevboxView(BaseModel): create_time_ms: Optional[int] = None """Creation time of the Devbox (Unix timestamp milliseconds).""" + name: Optional[str] = None + """The name of the Devbox.""" + status: Optional[str] = None """ The current status of the Devbox (provisioning, initializing, running, failure, diff --git a/src/runloop_api_client/types/devboxes/devbox_logs_list_view.py b/src/runloop_api_client/types/devboxes/devbox_logs_list_view.py index 7ecff42c9..e73fec580 100644 --- a/src/runloop_api_client/types/devboxes/devbox_logs_list_view.py +++ b/src/runloop_api_client/types/devboxes/devbox_logs_list_view.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional +from typing_extensions import Literal from ..._models import BaseModel @@ -23,6 +24,9 @@ class Log(BaseModel): message: Optional[str] = None """Log line message.""" + source: Optional[Literal["setup_commands", "entrypoint", "exec"]] = None + """The source of the log.""" + timestamp_ms: Optional[int] = None """Time of log (Unix timestamp milliseconds).""" diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index 513112570..b6940e903 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -32,6 +32,7 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: code_handle="string", entrypoint="string", environment_variables={"foo": "string"}, + name="string", setup_commands=["string", "string", "string"], ) assert_matches_type(DevboxView, devbox, path=["response"]) @@ -225,6 +226,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - code_handle="string", entrypoint="string", environment_variables={"foo": "string"}, + name="string", setup_commands=["string", "string", "string"], ) assert_matches_type(DevboxView, devbox, path=["response"])