diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ff261bad..db3ed8a3 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -ARG VARIANT="3.9" +ARG VARIANT="3.10" FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode diff --git a/.python-version b/.python-version index 43077b24..251b3507 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9.18 +3.10.16 diff --git a/.stats.yml b/.stats.yml index c9a00f0b..189e2433 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 261 +configured_endpoints: 266 diff --git a/README.md b/README.md index 13f4f74e..e4250e60 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/mobilerun-sdk.svg?label=pypi%20(stable))](https://pypi.org/project/mobilerun-sdk/) -The Mobilerun Python library provides convenient access to the Mobilerun REST API from any Python 3.9+ +The Mobilerun Python library provides convenient access to the Mobilerun REST API from any Python 3.10+ 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). @@ -32,7 +32,7 @@ import os from mobilerun_sdk import Mobilerun client = Mobilerun( - api_key=os.environ.get("MOBILERUN_CLOUD_API_KEY"), # This is the default and can be omitted + api_key=os.environ.get("MOBILERUN_API_KEY"), # This is the default and can be omitted ) devices = client.devices.list() @@ -41,7 +41,7 @@ print(devices.items) While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `MOBILERUN_CLOUD_API_KEY="My API Key"` to your `.env` file +to add `MOBILERUN_API_KEY="My API Key"` to your `.env` file so that your API Key is not stored in source control. ## Async usage @@ -54,7 +54,7 @@ import asyncio from mobilerun_sdk import AsyncMobilerun client = AsyncMobilerun( - api_key=os.environ.get("MOBILERUN_CLOUD_API_KEY"), # This is the default and can be omitted + api_key=os.environ.get("MOBILERUN_API_KEY"), # This is the default and can be omitted ) @@ -90,7 +90,7 @@ from mobilerun_sdk import AsyncMobilerun async def main() -> None: async with AsyncMobilerun( - api_key=os.environ.get("MOBILERUN_CLOUD_API_KEY"), # This is the default and can be omitted + api_key=os.environ.get("MOBILERUN_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: devices = await client.devices.list() @@ -401,7 +401,7 @@ print(mobilerun_sdk.__version__) ## Requirements -Python 3.9 or higher. +Python 3.10 or higher. ## Contributing diff --git a/api.md b/api.md index aa5693e1..727aab1a 100644 --- a/api.md +++ b/api.md @@ -549,6 +549,29 @@ Methods: - client.proxies.list(\*\*params) -> ProxyListResponse - client.proxies.delete(proxy_id) -> ProxyDeleteResponse +# Scripts + +Types: + +```python +from mobilerun_sdk.types import ( + Script, + ScriptSource, + ScriptCreateResponse, + ScriptRetrieveResponse, + ScriptListResponse, + ScriptSourceResponse, +) +``` + +Methods: + +- client.scripts.create(\*\*params) -> ScriptCreateResponse +- client.scripts.retrieve(script_id) -> ScriptRetrieveResponse +- client.scripts.list(\*\*params) -> ScriptListResponse +- client.scripts.delete(script_id) -> None +- client.scripts.source(script_id) -> ScriptSourceResponse + # Connect ## Countries diff --git a/pyproject.toml b/pyproject.toml index 12592413..46128118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,11 +17,10 @@ dependencies = [ "sniffio", ] -requires-python = ">= 3.9" +requires-python = ">= 3.10" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -143,7 +142,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.9" +pythonVersion = "3.10" exclude = [ "_dev", @@ -213,7 +212,7 @@ ignore_missing_imports = true [tool.ruff] line-length = 120 output-format = "grouped" -target-version = "py38" +target-version = "py310" [tool.ruff.format] docstring-code-format = true diff --git a/requirements-dev.lock b/requirements-dev.lock index bd2a014e..f917130e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,36 +10,36 @@ # universal: false -e file:. -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.7.1 # via aiohttp -aiohttp==3.13.3 +aiohttp==3.14.5 # via httpx-aiohttp # via mobilerun-sdk aiosignal==1.4.0 # via aiohttp -annotated-types==0.7.0 +annotated-types==0.8.0 # via pydantic -anyio==4.12.1 +anyio==4.16.0 # via httpx # via mobilerun-sdk -argcomplete==3.6.3 +argcomplete==3.7.2 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.4.0 +attrs==26.1.0 # via aiohttp # via nox backports-asyncio-runner==1.2.0 # via pytest-asyncio -certifi==2026.1.4 +certifi==2026.10.23 # via httpcore # via httpx -colorlog==6.10.1 +colorlog==6.12.0 # via nox -dependency-groups==1.3.1 +dependency-groups==1.3.2 # via nox dirty-equals==0.11 -distlib==0.4.0 +distlib==0.4.4 # via virtualenv distro==1.9.0 # via mobilerun-sdk @@ -48,7 +48,8 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.19.1 +filelock==4.1.0 + # via python-discovery # via virtualenv frozenlist==1.8.0 # via aiohttp @@ -61,72 +62,74 @@ httpx==0.28.1 # via httpx-aiohttp # via mobilerun-sdk # via respx -httpx-aiohttp==0.1.12 +httpx-aiohttp==0.2.0 # via mobilerun-sdk -humanize==4.13.0 +humanize==4.16.0 # via nox -idna==3.11 +idna==3.20 # via anyio # via httpx # via yarl -importlib-metadata==8.7.1 -iniconfig==2.1.0 +importlib-metadata==9.0.1 +iniconfig==2.3.1 # via pytest -markdown-it-py==3.0.0 +markdown-it-py==4.2.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 +multidict==7.1.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.1.0 +mypy-extensions==1.2.0 # via mypy -nodeenv==1.10.0 +nodeenv==1.12.0 # via pyright -nox==2025.11.12 -packaging==25.0 +nox==2026.10.20 +packaging==26.3 # via dependency-groups # via nox # via pytest -pathspec==1.0.3 + # via virtualenv +pathspec==1.1.1 # via mypy -platformdirs==4.4.0 +platformdirs==4.12.4 + # via nox # via virtualenv -pluggy==1.6.0 +pluggy==1.7.0 # via pytest -propcache==0.4.1 +propcache==0.5.4 # via aiohttp # via yarl pydantic==2.12.5 # via mobilerun-sdk pydantic-core==2.41.5 # via pydantic -pygments==2.19.2 +pygments==2.21.0 # via pytest # via rich pyright==1.1.399 -pytest==8.4.2 +pytest==9.2.0 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==1.2.0 +pytest-asyncio==1.4.1.post1 pytest-xdist==3.8.0 -python-dateutil==2.9.0.post0 - # via time-machine -respx==0.22.0 -rich==14.2.0 +python-discovery==1.9.2 + # via nox + # via virtualenv +respx==0.23.1 +rich==15.0.0 ruff==0.14.13 -six==1.17.0 - # via python-dateutil sniffio==1.3.1 # via mobilerun-sdk -time-machine==2.19.0 -tomli==2.4.0 +time-machine==3.5.1 +tomli==2.5.0 # via dependency-groups # via mypy # via nox # via pytest -typing-extensions==4.15.0 +typing-extensions==4.16.0 + # via aiohttp # via aiosignal # via anyio # via exceptiongroup @@ -139,11 +142,11 @@ typing-extensions==4.15.0 # via pytest-asyncio # via typing-inspection # via virtualenv -typing-inspection==0.4.2 +typing-inspection==0.4.4 # via pydantic -virtualenv==20.36.1 +virtualenv==21.15.3 # via nox -yarl==1.22.0 +yarl==1.25.1 # via aiohttp -zipp==3.23.0 +zipp==4.1.2 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 5c59b394..d33b8113 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,23 +10,23 @@ # universal: false -e file:. -aiohappyeyeballs==2.6.1 +aiohappyeyeballs==2.7.1 # via aiohttp -aiohttp==3.13.3 +aiohttp==3.14.5 # via httpx-aiohttp # via mobilerun-sdk aiosignal==1.4.0 # via aiohttp -annotated-types==0.7.0 +annotated-types==0.8.0 # via pydantic -anyio==4.12.1 +anyio==4.16.0 # via httpx # via mobilerun-sdk async-timeout==5.0.1 # via aiohttp -attrs==25.4.0 +attrs==26.1.0 # via aiohttp -certifi==2026.1.4 +certifi==2026.10.23 # via httpcore # via httpx distro==1.9.0 @@ -43,16 +43,16 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via mobilerun-sdk -httpx-aiohttp==0.1.12 +httpx-aiohttp==0.2.0 # via mobilerun-sdk -idna==3.11 +idna==3.20 # via anyio # via httpx # via yarl -multidict==6.7.0 +multidict==7.1.0 # via aiohttp # via yarl -propcache==0.4.1 +propcache==0.5.4 # via aiohttp # via yarl pydantic==2.12.5 @@ -61,7 +61,8 @@ pydantic-core==2.41.5 # via pydantic sniffio==1.3.1 # via mobilerun-sdk -typing-extensions==4.15.0 +typing-extensions==4.16.0 + # via aiohttp # via aiosignal # via anyio # via exceptiongroup @@ -70,7 +71,7 @@ typing-extensions==4.15.0 # via pydantic # via pydantic-core # via typing-inspection -typing-inspection==0.4.2 +typing-inspection==0.4.4 # via pydantic -yarl==1.22.0 +yarl==1.25.1 # via aiohttp diff --git a/src/mobilerun_sdk/_client.py b/src/mobilerun_sdk/_client.py index 0cfb3e3b..64856be9 100644 --- a/src/mobilerun_sdk/_client.py +++ b/src/mobilerun_sdk/_client.py @@ -46,6 +46,7 @@ devices, numbers, proxies, + scripts, messages, webhooks, assistant, @@ -57,6 +58,7 @@ from .resources.files import FilesResource, AsyncFilesResource from .resources.models import ModelsResource, AsyncModelsResource from .resources.proxies import ProxiesResource, AsyncProxiesResource + from .resources.scripts import ScriptsResource, AsyncScriptsResource from .resources.tasks.tasks import TasksResource, AsyncTasksResource from .resources.agents.agents import AgentsResource, AsyncAgentsResource from .resources.connect.connect import ConnectResource, AsyncConnectResource @@ -110,10 +112,10 @@ def __init__( ) -> None: """Construct a new synchronous Mobilerun client instance. - This automatically infers the `api_key` argument from the `MOBILERUN_CLOUD_API_KEY` environment variable if it is not provided. + This automatically infers the `api_key` argument from the `MOBILERUN_API_KEY` environment variable (falling back to `MOBILERUN_CLOUD_API_KEY`) if it is not provided. """ if api_key is None: - api_key = os.environ.get("MOBILERUN_CLOUD_API_KEY") + api_key = os.environ.get("MOBILERUN_API_KEY") or os.environ.get("MOBILERUN_CLOUD_API_KEY") self.api_key = api_key if base_url is None: @@ -173,6 +175,12 @@ def proxies(self) -> ProxiesResource: return ProxiesResource(self) + @cached_property + def scripts(self) -> ScriptsResource: + from .resources.scripts import ScriptsResource + + return ScriptsResource(self) + @cached_property def connect(self) -> ConnectResource: from .resources.connect import ConnectResource @@ -270,7 +278,7 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: return raise TypeError( - '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' + '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted (api_key can be set via the MOBILERUN_API_KEY environment variable)"' ) def copy( @@ -387,10 +395,10 @@ def __init__( ) -> None: """Construct a new async AsyncMobilerun client instance. - This automatically infers the `api_key` argument from the `MOBILERUN_CLOUD_API_KEY` environment variable if it is not provided. + This automatically infers the `api_key` argument from the `MOBILERUN_API_KEY` environment variable (falling back to `MOBILERUN_CLOUD_API_KEY`) if it is not provided. """ if api_key is None: - api_key = os.environ.get("MOBILERUN_CLOUD_API_KEY") + api_key = os.environ.get("MOBILERUN_API_KEY") or os.environ.get("MOBILERUN_CLOUD_API_KEY") self.api_key = api_key if base_url is None: @@ -450,6 +458,12 @@ def proxies(self) -> AsyncProxiesResource: return AsyncProxiesResource(self) + @cached_property + def scripts(self) -> AsyncScriptsResource: + from .resources.scripts import AsyncScriptsResource + + return AsyncScriptsResource(self) + @cached_property def connect(self) -> AsyncConnectResource: from .resources.connect import AsyncConnectResource @@ -547,7 +561,7 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: return raise TypeError( - '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' + '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted (api_key can be set via the MOBILERUN_API_KEY environment variable)"' ) def copy( @@ -671,6 +685,12 @@ def proxies(self) -> proxies.ProxiesResourceWithRawResponse: return ProxiesResourceWithRawResponse(self._client.proxies) + @cached_property + def scripts(self) -> scripts.ScriptsResourceWithRawResponse: + from .resources.scripts import ScriptsResourceWithRawResponse + + return ScriptsResourceWithRawResponse(self._client.scripts) + @cached_property def connect(self) -> connect.ConnectResourceWithRawResponse: from .resources.connect import ConnectResourceWithRawResponse @@ -769,6 +789,12 @@ def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse: return AsyncProxiesResourceWithRawResponse(self._client.proxies) + @cached_property + def scripts(self) -> scripts.AsyncScriptsResourceWithRawResponse: + from .resources.scripts import AsyncScriptsResourceWithRawResponse + + return AsyncScriptsResourceWithRawResponse(self._client.scripts) + @cached_property def connect(self) -> connect.AsyncConnectResourceWithRawResponse: from .resources.connect import AsyncConnectResourceWithRawResponse @@ -867,6 +893,12 @@ def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse: return ProxiesResourceWithStreamingResponse(self._client.proxies) + @cached_property + def scripts(self) -> scripts.ScriptsResourceWithStreamingResponse: + from .resources.scripts import ScriptsResourceWithStreamingResponse + + return ScriptsResourceWithStreamingResponse(self._client.scripts) + @cached_property def connect(self) -> connect.ConnectResourceWithStreamingResponse: from .resources.connect import ConnectResourceWithStreamingResponse @@ -965,6 +997,12 @@ def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse: return AsyncProxiesResourceWithStreamingResponse(self._client.proxies) + @cached_property + def scripts(self) -> scripts.AsyncScriptsResourceWithStreamingResponse: + from .resources.scripts import AsyncScriptsResourceWithStreamingResponse + + return AsyncScriptsResourceWithStreamingResponse(self._client.scripts) + @cached_property def connect(self) -> connect.AsyncConnectResourceWithStreamingResponse: from .resources.connect import AsyncConnectResourceWithStreamingResponse diff --git a/src/mobilerun_sdk/resources/__init__.py b/src/mobilerun_sdk/resources/__init__.py index 1c55b811..0495f9b4 100644 --- a/src/mobilerun_sdk/resources/__init__.py +++ b/src/mobilerun_sdk/resources/__init__.py @@ -72,6 +72,14 @@ ProxiesResourceWithStreamingResponse, AsyncProxiesResourceWithStreamingResponse, ) +from .scripts import ( + ScriptsResource, + AsyncScriptsResource, + ScriptsResourceWithRawResponse, + AsyncScriptsResourceWithRawResponse, + ScriptsResourceWithStreamingResponse, + AsyncScriptsResourceWithStreamingResponse, +) from .messages import ( MessagesResource, AsyncMessagesResource, @@ -152,6 +160,12 @@ "AsyncProxiesResourceWithRawResponse", "ProxiesResourceWithStreamingResponse", "AsyncProxiesResourceWithStreamingResponse", + "ScriptsResource", + "AsyncScriptsResource", + "ScriptsResourceWithRawResponse", + "AsyncScriptsResourceWithRawResponse", + "ScriptsResourceWithStreamingResponse", + "AsyncScriptsResourceWithStreamingResponse", "ConnectResource", "AsyncConnectResource", "ConnectResourceWithRawResponse", diff --git a/src/mobilerun_sdk/resources/scripts.py b/src/mobilerun_sdk/resources/scripts.py new file mode 100644 index 00000000..d7e74cf9 --- /dev/null +++ b/src/mobilerun_sdk/resources/scripts.py @@ -0,0 +1,576 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import script_list_params, script_create_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.script_list_response import ScriptListResponse +from ..types.script_create_response import ScriptCreateResponse +from ..types.script_source_response import ScriptSourceResponse +from ..types.script_retrieve_response import ScriptRetrieveResponse + +__all__ = ["ScriptsResource", "AsyncScriptsResource"] + + +class ScriptsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ScriptsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/droidrun/mobilerun-sdk-python#accessing-raw-response-data-eg-headers + """ + return ScriptsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ScriptsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/droidrun/mobilerun-sdk-python#with_streaming_response + """ + return ScriptsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + source: str, + description: str | Omit = omit, + manifest: script_create_params.Manifest | Omit = omit, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScriptCreateResponse: + """ + Upload an immutable Python script as the next version of its name + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return self._post( + "/sandbox/scripts", + body=maybe_transform( + { + "name": name, + "source": source, + "description": description, + "manifest": manifest, + }, + script_create_params.ScriptCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScriptCreateResponse, + ) + + def retrieve( + self, + script_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ScriptRetrieveResponse: + """ + Get script metadata + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not script_id: + raise ValueError(f"Expected a non-empty value for `script_id` but received {script_id!r}") + return self._get( + path_template("/sandbox/scripts/{script_id}", script_id=script_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScriptRetrieveResponse, + ) + + def list( + self, + *, + latest: Literal["true", "false"] | Omit = omit, + name: str | Omit = omit, + page: int | Omit = omit, + page_size: int | Omit = omit, + search: str | Omit = omit, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ScriptListResponse: + """ + List scripts owned by this organisation + + Args: + latest: Return one row per script name: its newest upload (any status), with + versionCount. Combined with name, returns that single row. + + name: Exact script name. Returns every active version of that name, newest first. + + search: Case-insensitive substring match against the script name. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/sandbox/scripts", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "latest": latest, + "name": name, + "page": page, + "page_size": page_size, + "search": search, + }, + script_list_params.ScriptListParams, + ), + ), + cast_to=ScriptListResponse, + ) + + def delete( + self, + script_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> None: + """ + Delete a script + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not script_id: + raise ValueError(f"Expected a non-empty value for `script_id` but received {script_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/sandbox/scripts/{script_id}", script_id=script_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NoneType, + ) + + def source( + self, + script_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ScriptSourceResponse: + """ + Read verified script source + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not script_id: + raise ValueError(f"Expected a non-empty value for `script_id` but received {script_id!r}") + return self._get( + path_template("/sandbox/scripts/{script_id}/source", script_id=script_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScriptSourceResponse, + ) + + +class AsyncScriptsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncScriptsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/droidrun/mobilerun-sdk-python#accessing-raw-response-data-eg-headers + """ + return AsyncScriptsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncScriptsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/droidrun/mobilerun-sdk-python#with_streaming_response + """ + return AsyncScriptsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + source: str, + description: str | Omit = omit, + manifest: script_create_params.Manifest | Omit = omit, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> ScriptCreateResponse: + """ + Upload an immutable Python script as the next version of its name + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + return await self._post( + "/sandbox/scripts", + body=await async_maybe_transform( + { + "name": name, + "source": source, + "description": description, + "manifest": manifest, + }, + script_create_params.ScriptCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=ScriptCreateResponse, + ) + + async def retrieve( + self, + script_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ScriptRetrieveResponse: + """ + Get script metadata + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not script_id: + raise ValueError(f"Expected a non-empty value for `script_id` but received {script_id!r}") + return await self._get( + path_template("/sandbox/scripts/{script_id}", script_id=script_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScriptRetrieveResponse, + ) + + async def list( + self, + *, + latest: Literal["true", "false"] | Omit = omit, + name: str | Omit = omit, + page: int | Omit = omit, + page_size: int | Omit = omit, + search: str | Omit = omit, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ScriptListResponse: + """ + List scripts owned by this organisation + + Args: + latest: Return one row per script name: its newest upload (any status), with + versionCount. Combined with name, returns that single row. + + name: Exact script name. Returns every active version of that name, newest first. + + search: Case-insensitive substring match against the script name. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/sandbox/scripts", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "latest": latest, + "name": name, + "page": page, + "page_size": page_size, + "search": search, + }, + script_list_params.ScriptListParams, + ), + ), + cast_to=ScriptListResponse, + ) + + async def delete( + self, + script_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + idempotency_key: str | None = None, + ) -> None: + """ + Delete a script + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not script_id: + raise ValueError(f"Expected a non-empty value for `script_id` but received {script_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/sandbox/scripts/{script_id}", script_id=script_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=NoneType, + ) + + async def source( + self, + script_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ScriptSourceResponse: + """ + Read verified script source + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not script_id: + raise ValueError(f"Expected a non-empty value for `script_id` but received {script_id!r}") + return await self._get( + path_template("/sandbox/scripts/{script_id}/source", script_id=script_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ScriptSourceResponse, + ) + + +class ScriptsResourceWithRawResponse: + def __init__(self, scripts: ScriptsResource) -> None: + self._scripts = scripts + + self.create = to_raw_response_wrapper( + scripts.create, + ) + self.retrieve = to_raw_response_wrapper( + scripts.retrieve, + ) + self.list = to_raw_response_wrapper( + scripts.list, + ) + self.delete = to_raw_response_wrapper( + scripts.delete, + ) + self.source = to_raw_response_wrapper( + scripts.source, + ) + + +class AsyncScriptsResourceWithRawResponse: + def __init__(self, scripts: AsyncScriptsResource) -> None: + self._scripts = scripts + + self.create = async_to_raw_response_wrapper( + scripts.create, + ) + self.retrieve = async_to_raw_response_wrapper( + scripts.retrieve, + ) + self.list = async_to_raw_response_wrapper( + scripts.list, + ) + self.delete = async_to_raw_response_wrapper( + scripts.delete, + ) + self.source = async_to_raw_response_wrapper( + scripts.source, + ) + + +class ScriptsResourceWithStreamingResponse: + def __init__(self, scripts: ScriptsResource) -> None: + self._scripts = scripts + + self.create = to_streamed_response_wrapper( + scripts.create, + ) + self.retrieve = to_streamed_response_wrapper( + scripts.retrieve, + ) + self.list = to_streamed_response_wrapper( + scripts.list, + ) + self.delete = to_streamed_response_wrapper( + scripts.delete, + ) + self.source = to_streamed_response_wrapper( + scripts.source, + ) + + +class AsyncScriptsResourceWithStreamingResponse: + def __init__(self, scripts: AsyncScriptsResource) -> None: + self._scripts = scripts + + self.create = async_to_streamed_response_wrapper( + scripts.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + scripts.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + scripts.list, + ) + self.delete = async_to_streamed_response_wrapper( + scripts.delete, + ) + self.source = async_to_streamed_response_wrapper( + scripts.source, + ) diff --git a/src/mobilerun_sdk/types/__init__.py b/src/mobilerun_sdk/types/__init__.py index b80bfc96..9e09287d 100644 --- a/src/mobilerun_sdk/types/__init__.py +++ b/src/mobilerun_sdk/types/__init__.py @@ -4,6 +4,7 @@ from . import devices from .. import _compat +from .script import Script as Script from .shared import ( Meta as Meta, Location as Location, @@ -15,6 +16,7 @@ DeviceIdentifiers as DeviceIdentifiers, Socks5ProxyConfig as Socks5ProxyConfig, ) +from .script_source import ScriptSource as ScriptSource from .app_list_params import AppListParams as AppListParams from .task_run_params import TaskRunParams as TaskRunParams from .file_list_params import FileListParams as FileListParams @@ -27,6 +29,7 @@ from .file_update_params import FileUpdateParams as FileUpdateParams from .mailbox_otp_params import MailboxOtpParams as MailboxOtpParams from .number_list_params import NumberListParams as NumberListParams +from .script_list_params import ScriptListParams as ScriptListParams from .task_list_response import TaskListResponse as TaskListResponse from .task_stop_response import TaskStopResponse as TaskStopResponse from .agent_create_params import AgentCreateParams as AgentCreateParams @@ -48,6 +51,8 @@ from .number_create_params import NumberCreateParams as NumberCreateParams from .number_list_response import NumberListResponse as NumberListResponse from .number_update_params import NumberUpdateParams as NumberUpdateParams +from .script_create_params import ScriptCreateParams as ScriptCreateParams +from .script_list_response import ScriptListResponse as ScriptListResponse from .agent_create_response import AgentCreateResponse as AgentCreateResponse from .app_retrieve_response import AppRetrieveResponse as AppRetrieveResponse from .device_count_response import DeviceCountResponse as DeviceCountResponse @@ -72,6 +77,8 @@ from .number_create_response import NumberCreateResponse as NumberCreateResponse from .number_delete_response import NumberDeleteResponse as NumberDeleteResponse from .number_update_response import NumberUpdateResponse as NumberUpdateResponse +from .script_create_response import ScriptCreateResponse as ScriptCreateResponse +from .script_source_response import ScriptSourceResponse as ScriptSourceResponse from .task_retrieve_response import TaskRetrieveResponse as TaskRetrieveResponse from .device_summary_response import DeviceSummaryResponse as DeviceSummaryResponse from .device_terminate_params import DeviceTerminateParams as DeviceTerminateParams @@ -89,6 +96,7 @@ from .mailbox_restart_response import MailboxRestartResponse as MailboxRestartResponse from .number_capacity_response import NumberCapacityResponse as NumberCapacityResponse from .number_retrieve_response import NumberRetrieveResponse as NumberRetrieveResponse +from .script_retrieve_response import ScriptRetrieveResponse as ScriptRetrieveResponse from .task_get_status_response import TaskGetStatusResponse as TaskGetStatusResponse from .task_run_streamed_params import TaskRunStreamedParams as TaskRunStreamedParams from .task_send_message_params import TaskSendMessageParams as TaskSendMessageParams diff --git a/src/mobilerun_sdk/types/script.py b/src/mobilerun_sdk/types/script.py new file mode 100644 index 00000000..d9445e13 --- /dev/null +++ b/src/mobilerun_sdk/types/script.py @@ -0,0 +1,69 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Script", "InspectError", "Manifest", "ManifestCredential"] + + +class InspectError(BaseModel): + code: str + + message: str + + +class ManifestCredential(BaseModel): + name: str + + package_name: str = FieldInfo(alias="packageName") + + +class Manifest(BaseModel): + credentials: Optional[List[ManifestCredential]] = None + + hosts: Optional[List[str]] = None + + scopes: Optional[List[str]] = None + + +class Script(BaseModel): + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + created_by: str = FieldInfo(alias="createdBy") + + dataset_key: Optional[str] = FieldInfo(alias="datasetKey", default=None) + + description: Optional[str] = None + + input_schema: Optional[Dict[str, object]] = FieldInfo(alias="inputSchema", default=None) + + inspect_error: Optional[InspectError] = FieldInfo(alias="inspectError", default=None) + + manifest: Manifest + + name: str + + output_kind: Optional[str] = FieldInfo(alias="outputKind", default=None) + + output_schema: Optional[Dict[str, object]] = FieldInfo(alias="outputSchema", default=None) + + runtime_version: Optional[str] = FieldInfo(alias="runtimeVersion", default=None) + + sha256: str + + size_bytes: int = FieldInfo(alias="sizeBytes") + + status: Literal["pending_inspection", "ready", "rejected"] + + version: int + """Per-name version number, starting at 1 and never reused.""" + + version_count: Optional[int] = FieldInfo(alias="versionCount", default=None) + """Number of active versions of this name. Only set when listing with latest=true.""" diff --git a/src/mobilerun_sdk/types/script_create_params.py b/src/mobilerun_sdk/types/script_create_params.py new file mode 100644 index 00000000..3d7bf58e --- /dev/null +++ b/src/mobilerun_sdk/types/script_create_params.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Required, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo + +__all__ = ["ScriptCreateParams", "Manifest", "ManifestCredential"] + + +class ScriptCreateParams(TypedDict, total=False): + name: Required[str] + + source: Required[str] + + description: str + + manifest: Manifest + + +class ManifestCredential(TypedDict, total=False): + name: Required[str] + + package_name: Required[Annotated[str, PropertyInfo(alias="packageName")]] + + +class Manifest(TypedDict, total=False): + credentials: Iterable[ManifestCredential] + + hosts: SequenceNotStr[str] + + scopes: SequenceNotStr[str] diff --git a/src/mobilerun_sdk/types/script_create_response.py b/src/mobilerun_sdk/types/script_create_response.py new file mode 100644 index 00000000..47dbf8a0 --- /dev/null +++ b/src/mobilerun_sdk/types/script_create_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .script import Script +from .._models import BaseModel + +__all__ = ["ScriptCreateResponse"] + + +class ScriptCreateResponse(BaseModel): + data: Script diff --git a/src/mobilerun_sdk/types/script_list_params.py b/src/mobilerun_sdk/types/script_list_params.py new file mode 100644 index 00000000..5785bf2d --- /dev/null +++ b/src/mobilerun_sdk/types/script_list_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["ScriptListParams"] + + +class ScriptListParams(TypedDict, total=False): + latest: Literal["true", "false"] + """ + Return one row per script name: its newest upload (any status), with + versionCount. Combined with name, returns that single row. + """ + + name: str + """Exact script name. Returns every active version of that name, newest first.""" + + page: int + + page_size: Annotated[int, PropertyInfo(alias="pageSize")] + + search: str + """Case-insensitive substring match against the script name.""" diff --git a/src/mobilerun_sdk/types/script_list_response.py b/src/mobilerun_sdk/types/script_list_response.py new file mode 100644 index 00000000..843ff240 --- /dev/null +++ b/src/mobilerun_sdk/types/script_list_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .script import Script +from .._models import BaseModel +from .shared.pagination import Pagination + +__all__ = ["ScriptListResponse", "Data"] + + +class Data(BaseModel): + items: List[Script] + + pagination: Pagination + + +class ScriptListResponse(BaseModel): + data: Data diff --git a/src/mobilerun_sdk/types/script_retrieve_response.py b/src/mobilerun_sdk/types/script_retrieve_response.py new file mode 100644 index 00000000..e03e61ef --- /dev/null +++ b/src/mobilerun_sdk/types/script_retrieve_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .script import Script +from .._models import BaseModel + +__all__ = ["ScriptRetrieveResponse"] + + +class ScriptRetrieveResponse(BaseModel): + data: Script diff --git a/src/mobilerun_sdk/types/script_source.py b/src/mobilerun_sdk/types/script_source.py new file mode 100644 index 00000000..5ba5319d --- /dev/null +++ b/src/mobilerun_sdk/types/script_source.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ScriptSource"] + + +class ScriptSource(BaseModel): + script_id: str = FieldInfo(alias="scriptId") + + sha256: str + + size_bytes: int = FieldInfo(alias="sizeBytes") + + source: str diff --git a/src/mobilerun_sdk/types/script_source_response.py b/src/mobilerun_sdk/types/script_source_response.py new file mode 100644 index 00000000..f745ca7f --- /dev/null +++ b/src/mobilerun_sdk/types/script_source_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel +from .script_source import ScriptSource + +__all__ = ["ScriptSourceResponse"] + + +class ScriptSourceResponse(BaseModel): + data: ScriptSource diff --git a/tests/api_resources/test_scripts.py b/tests/api_resources/test_scripts.py new file mode 100644 index 00000000..7813bc7a --- /dev/null +++ b/tests/api_resources/test_scripts.py @@ -0,0 +1,475 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from mobilerun_sdk import Mobilerun, AsyncMobilerun +from mobilerun_sdk.types import ( + ScriptListResponse, + ScriptCreateResponse, + ScriptSourceResponse, + ScriptRetrieveResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestScripts: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Mobilerun) -> None: + script = client.scripts.create( + name="x", + source="x", + ) + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Mobilerun) -> None: + script = client.scripts.create( + name="x", + source="x", + description="description", + manifest={ + "credentials": [ + { + "name": "x", + "package_name": "x", + } + ], + "hosts": ["string"], + "scopes": ["_lk___klzn__.____uu"], + }, + ) + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Mobilerun) -> None: + response = client.scripts.with_raw_response.create( + name="x", + source="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = response.parse() + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Mobilerun) -> None: + with client.scripts.with_streaming_response.create( + name="x", + source="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = response.parse() + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Mobilerun) -> None: + script = client.scripts.retrieve( + "550e8400-e29b-41d4-a716-446655440000", + ) + assert_matches_type(ScriptRetrieveResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Mobilerun) -> None: + response = client.scripts.with_raw_response.retrieve( + "550e8400-e29b-41d4-a716-446655440000", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = response.parse() + assert_matches_type(ScriptRetrieveResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Mobilerun) -> None: + with client.scripts.with_streaming_response.retrieve( + "550e8400-e29b-41d4-a716-446655440000", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = response.parse() + assert_matches_type(ScriptRetrieveResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Mobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `script_id` but received ''"): + client.scripts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Mobilerun) -> None: + script = client.scripts.list() + assert_matches_type(ScriptListResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Mobilerun) -> None: + script = client.scripts.list( + latest="true", + name="x", + page=1, + page_size=1, + search="x", + ) + assert_matches_type(ScriptListResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Mobilerun) -> None: + response = client.scripts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = response.parse() + assert_matches_type(ScriptListResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Mobilerun) -> None: + with client.scripts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = response.parse() + assert_matches_type(ScriptListResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Mobilerun) -> None: + script = client.scripts.delete( + "550e8400-e29b-41d4-a716-446655440000", + ) + assert script is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Mobilerun) -> None: + response = client.scripts.with_raw_response.delete( + "550e8400-e29b-41d4-a716-446655440000", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = response.parse() + assert script is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Mobilerun) -> None: + with client.scripts.with_streaming_response.delete( + "550e8400-e29b-41d4-a716-446655440000", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = response.parse() + assert script is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Mobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `script_id` but received ''"): + client.scripts.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_source(self, client: Mobilerun) -> None: + script = client.scripts.source( + "550e8400-e29b-41d4-a716-446655440000", + ) + assert_matches_type(ScriptSourceResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_source(self, client: Mobilerun) -> None: + response = client.scripts.with_raw_response.source( + "550e8400-e29b-41d4-a716-446655440000", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = response.parse() + assert_matches_type(ScriptSourceResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_source(self, client: Mobilerun) -> None: + with client.scripts.with_streaming_response.source( + "550e8400-e29b-41d4-a716-446655440000", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = response.parse() + assert_matches_type(ScriptSourceResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_source(self, client: Mobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `script_id` but received ''"): + client.scripts.with_raw_response.source( + "", + ) + + +class TestAsyncScripts: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncMobilerun) -> None: + script = await async_client.scripts.create( + name="x", + source="x", + ) + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) -> None: + script = await async_client.scripts.create( + name="x", + source="x", + description="description", + manifest={ + "credentials": [ + { + "name": "x", + "package_name": "x", + } + ], + "hosts": ["string"], + "scopes": ["_lk___klzn__.____uu"], + }, + ) + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncMobilerun) -> None: + response = await async_client.scripts.with_raw_response.create( + name="x", + source="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = await response.parse() + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncMobilerun) -> None: + async with async_client.scripts.with_streaming_response.create( + name="x", + source="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = await response.parse() + assert_matches_type(ScriptCreateResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncMobilerun) -> None: + script = await async_client.scripts.retrieve( + "550e8400-e29b-41d4-a716-446655440000", + ) + assert_matches_type(ScriptRetrieveResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncMobilerun) -> None: + response = await async_client.scripts.with_raw_response.retrieve( + "550e8400-e29b-41d4-a716-446655440000", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = await response.parse() + assert_matches_type(ScriptRetrieveResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncMobilerun) -> None: + async with async_client.scripts.with_streaming_response.retrieve( + "550e8400-e29b-41d4-a716-446655440000", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = await response.parse() + assert_matches_type(ScriptRetrieveResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncMobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `script_id` but received ''"): + await async_client.scripts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncMobilerun) -> None: + script = await async_client.scripts.list() + assert_matches_type(ScriptListResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncMobilerun) -> None: + script = await async_client.scripts.list( + latest="true", + name="x", + page=1, + page_size=1, + search="x", + ) + assert_matches_type(ScriptListResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncMobilerun) -> None: + response = await async_client.scripts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = await response.parse() + assert_matches_type(ScriptListResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncMobilerun) -> None: + async with async_client.scripts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = await response.parse() + assert_matches_type(ScriptListResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncMobilerun) -> None: + script = await async_client.scripts.delete( + "550e8400-e29b-41d4-a716-446655440000", + ) + assert script is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncMobilerun) -> None: + response = await async_client.scripts.with_raw_response.delete( + "550e8400-e29b-41d4-a716-446655440000", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = await response.parse() + assert script is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncMobilerun) -> None: + async with async_client.scripts.with_streaming_response.delete( + "550e8400-e29b-41d4-a716-446655440000", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = await response.parse() + assert script is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncMobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `script_id` but received ''"): + await async_client.scripts.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_source(self, async_client: AsyncMobilerun) -> None: + script = await async_client.scripts.source( + "550e8400-e29b-41d4-a716-446655440000", + ) + assert_matches_type(ScriptSourceResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_source(self, async_client: AsyncMobilerun) -> None: + response = await async_client.scripts.with_raw_response.source( + "550e8400-e29b-41d4-a716-446655440000", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + script = await response.parse() + assert_matches_type(ScriptSourceResponse, script, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_source(self, async_client: AsyncMobilerun) -> None: + async with async_client.scripts.with_streaming_response.source( + "550e8400-e29b-41d4-a716-446655440000", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + script = await response.parse() + assert_matches_type(ScriptSourceResponse, script, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_source(self, async_client: AsyncMobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `script_id` but received ''"): + await async_client.scripts.with_raw_response.source( + "", + ) diff --git a/tests/test_client.py b/tests/test_client.py index 95b93e25..ce0d24c0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -404,7 +404,7 @@ def test_validate_headers(self) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with update_env(**{"MOBILERUN_CLOUD_API_KEY": Omit()}): + with update_env(**{"MOBILERUN_API_KEY": Omit(), "MOBILERUN_CLOUD_API_KEY": Omit()}): client2 = Mobilerun(base_url=base_url, api_key=None, _strict_response_validation=True) with pytest.raises( @@ -418,6 +418,24 @@ def test_validate_headers(self) -> None: ) assert request2.headers.get("Authorization") is None + def test_api_key_env_names(self) -> None: + none = {"MOBILERUN_API_KEY": Omit(), "MOBILERUN_CLOUD_API_KEY": Omit()} + + with update_env(**{**none, "MOBILERUN_API_KEY": "new key"}): + assert Mobilerun(base_url=base_url, _strict_response_validation=True).api_key == "new key" + + with update_env(**{**none, "MOBILERUN_CLOUD_API_KEY": "old key"}): + assert Mobilerun(base_url=base_url, _strict_response_validation=True).api_key == "old key" + + with update_env(**{**none, "MOBILERUN_API_KEY": "", "MOBILERUN_CLOUD_API_KEY": "old key"}): + assert Mobilerun(base_url=base_url, _strict_response_validation=True).api_key == "old key" + + with update_env(**{**none, "MOBILERUN_API_KEY": "new key", "MOBILERUN_CLOUD_API_KEY": "old key"}): + assert Mobilerun(base_url=base_url, _strict_response_validation=True).api_key == "new key" + + with update_env(**none): + assert Mobilerun(base_url=base_url, _strict_response_validation=True).api_key is None + def test_default_query_option(self) -> None: client = Mobilerun( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} @@ -1359,7 +1377,7 @@ def test_validate_headers(self) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with update_env(**{"MOBILERUN_CLOUD_API_KEY": Omit()}): + with update_env(**{"MOBILERUN_API_KEY": Omit(), "MOBILERUN_CLOUD_API_KEY": Omit()}): client2 = AsyncMobilerun(base_url=base_url, api_key=None, _strict_response_validation=True) with pytest.raises( @@ -1373,6 +1391,24 @@ def test_validate_headers(self) -> None: ) assert request2.headers.get("Authorization") is None + def test_api_key_env_names(self) -> None: + none = {"MOBILERUN_API_KEY": Omit(), "MOBILERUN_CLOUD_API_KEY": Omit()} + + with update_env(**{**none, "MOBILERUN_API_KEY": "new key"}): + assert AsyncMobilerun(base_url=base_url, _strict_response_validation=True).api_key == "new key" + + with update_env(**{**none, "MOBILERUN_CLOUD_API_KEY": "old key"}): + assert AsyncMobilerun(base_url=base_url, _strict_response_validation=True).api_key == "old key" + + with update_env(**{**none, "MOBILERUN_API_KEY": "", "MOBILERUN_CLOUD_API_KEY": "old key"}): + assert AsyncMobilerun(base_url=base_url, _strict_response_validation=True).api_key == "old key" + + with update_env(**{**none, "MOBILERUN_API_KEY": "new key", "MOBILERUN_CLOUD_API_KEY": "old key"}): + assert AsyncMobilerun(base_url=base_url, _strict_response_validation=True).api_key == "new key" + + with update_env(**none): + assert AsyncMobilerun(base_url=base_url, _strict_response_validation=True).api_key is None + async def test_default_query_option(self) -> None: client = AsyncMobilerun( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}