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 @@
[)](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"}