diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e2e170..347cfcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/droidrun-cloud-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -46,7 +46,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/droidrun-cloud-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -67,7 +67,7 @@ jobs: github.repository == 'stainless-sdks/droidrun-cloud-python' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -87,7 +87,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/droidrun-cloud-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 965bfe5..112bd43 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 9f5c3c4..ba61562 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'droidrun/mobilerun-sdk-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e0dc500..1f73031 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.1.0" + ".": "3.2.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 1578830..4d26ea4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 89 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/droidrun%2Fdroidrun-cloud-5354e1e393f7a2c470fba288fd927027d7f8ab0f76350be330392a32d61321d7.yml -openapi_spec_hash: 61d176c5697051a52251e67cc2a143b7 -config_hash: 2e5f796057a879ad2efd58b3ec8289cf +configured_endpoints: 94 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/droidrun/droidrun-cloud-c33c250c3235c14a1e5a173b18ef347fe2789e0b21f7e1b0649bd27ae255975e.yml +openapi_spec_hash: 7710fc1424f709edd45d9dfe90121649 +config_hash: 2202392fa81fb6cad4fcd79e62717a43 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b66e4c..2dc4853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 3.2.0 (2026-05-13) + +Full Changelog: [v3.1.0...v3.2.0](https://github.com/droidrun/mobilerun-sdk-python/compare/v3.1.0...v3.2.0) + +### Features + +* **api:** api update ([1e39fcb](https://github.com/droidrun/mobilerun-sdk-python/commit/1e39fcbdc514e9ef20feda52cd0232c8f6b568aa)) +* **api:** api update ([8b8b8b1](https://github.com/droidrun/mobilerun-sdk-python/commit/8b8b8b1d56db10de229123323ccdcb28b3903086)) +* **api:** api update ([75bbc88](https://github.com/droidrun/mobilerun-sdk-python/commit/75bbc88550c6f53e16c25c692ea0febc2ed07acd)) +* **api:** labels for objects ([0b8ee8c](https://github.com/droidrun/mobilerun-sdk-python/commit/0b8ee8c2cd690761a249fb97ac22c393a574f8f3)) +* **api:** manual updates ([7861088](https://github.com/droidrun/mobilerun-sdk-python/commit/786108846d965ae14d619615b1d991d0cf937cb1)) +* **api:** manual updates ([4579552](https://github.com/droidrun/mobilerun-sdk-python/commit/4579552f608c8de6aeab1731f512509ff049b6a4)) +* **internal/types:** support eagerly validating pydantic iterators ([8c7fc10](https://github.com/droidrun/mobilerun-sdk-python/commit/8c7fc10dae7a32f182a614733b3ea7a79455b288)) +* support setting headers via env ([273abbc](https://github.com/droidrun/mobilerun-sdk-python/commit/273abbc5339e39f57f59d885a9bfdf5df5d2e4fe)) + + +### Bug Fixes + +* **client:** add missing f-string prefix in file type error message ([8d073c2](https://github.com/droidrun/mobilerun-sdk-python/commit/8d073c20fc48f1ae04718591bcd2b7f8805c9c3c)) +* use correct field name format for multipart file arrays ([7ad94ce](https://github.com/droidrun/mobilerun-sdk-python/commit/7ad94ce83e73be25a9dc55c91aabe36065594480)) + + +### Performance Improvements + +* **client:** optimize file structure copying in multipart requests ([b6562d3](https://github.com/droidrun/mobilerun-sdk-python/commit/b6562d3c3c02a73d9e7e36f26bfd6d86728993d2)) + + +### Chores + +* **internal:** more robust bootstrap script ([d8ed809](https://github.com/droidrun/mobilerun-sdk-python/commit/d8ed809f6aec7556e96cd697a8b517120cd4ebff)) +* **internal:** reformat pyproject.toml ([d108c45](https://github.com/droidrun/mobilerun-sdk-python/commit/d108c45430e484b935c7ee5bd17138c2bc998d0f)) + ## 3.1.0 (2026-04-16) Full Changelog: [v3.0.0...v3.1.0](https://github.com/droidrun/mobilerun-sdk-python/compare/v3.0.0...v3.1.0) diff --git a/README.md b/README.md index 748a763..73b8b1d 100644 --- a/README.md +++ b/README.md @@ -124,11 +124,17 @@ from mobilerun_sdk import Mobilerun client = Mobilerun() -profile = client.profiles.create( - name="x", - spec={}, +device = client.devices.create( + carrier={ + "gsm_operator_alpha": "GsmOperatorAlpha", + "gsm_operator_numeric": 0, + "gsm_sim_operator_alpha": "GsmSimOperatorAlpha", + "gsm_sim_operator_iso_country": "GsmSimOperatorIsoCountry", + "gsm_sim_operator_numeric": 0, + "persist_sys_timezone": "PersistSysTimezone", + }, ) -print(profile.spec) +print(device.carrier) ``` ## File uploads diff --git a/api.md b/api.md index dfe1856..f60b9fc 100644 --- a/api.md +++ b/api.md @@ -5,138 +5,136 @@ from mobilerun_sdk.types import ( DeviceCarrier, DeviceIdentifiers, DeviceSpec, + Location, Meta, Pagination, PaginationMeta, PermissionSet, + Socks5, ) ``` -# Tasks +# Agents Types: ```python -from mobilerun_sdk.types import ( - PackageCredentials, - Task, - TaskStatus, - UsageResult, - TaskRetrieveResponse, - TaskListResponse, - TaskGetStatusResponse, - TaskGetTrajectoryResponse, - TaskRunResponse, - TaskSendMessageResponse, - TaskStopResponse, -) +from mobilerun_sdk.types import AgentListResponse ``` Methods: -- client.tasks.retrieve(task_id) -> TaskRetrieveResponse -- client.tasks.list(\*\*params) -> TaskListResponse -- client.tasks.attach(task_id) -> None -- client.tasks.get_status(task_id) -> TaskGetStatusResponse -- client.tasks.get_trajectory(task_id) -> TaskGetTrajectoryResponse -- client.tasks.run(\*\*params) -> TaskRunResponse -- client.tasks.run_streamed(\*\*params) -> object -- client.tasks.send_message(task_id, \*\*params) -> TaskSendMessageResponse -- client.tasks.stop(task_id) -> TaskStopResponse +- client.agents.list() -> AgentListResponse -## Screenshots +# Apps Types: ```python -from mobilerun_sdk.types.tasks import MediaResponse, ScreenshotListResponse +from mobilerun_sdk.types import ( + AppRetrieveResponse, + AppListResponse, + AppDeleteResponse, + AppConfirmUploadResponse, + AppCreateSignedUploadURLResponse, + AppMarkFailedResponse, +) ``` Methods: -- client.tasks.screenshots.retrieve(index, \*, task_id) -> MediaResponse -- client.tasks.screenshots.list(task_id) -> ScreenshotListResponse +- client.apps.retrieve(id) -> AppRetrieveResponse +- client.apps.list(\*\*params) -> AppListResponse +- client.apps.delete(id) -> AppDeleteResponse +- client.apps.confirm_upload(id) -> AppConfirmUploadResponse +- client.apps.create_signed_upload_url(\*\*params) -> AppCreateSignedUploadURLResponse +- client.apps.mark_failed(id) -> AppMarkFailedResponse -## UiStates +# Carriers Types: ```python -from mobilerun_sdk.types.tasks import UiStateListResponse +from mobilerun_sdk.types import ( + CarrierCreateResponse, + CarrierRetrieveResponse, + CarrierUpdateResponse, + CarrierListResponse, + CarrierDeleteResponse, + CarrierLookupResponse, +) ``` Methods: -- client.tasks.ui_states.retrieve(index, \*, task_id) -> MediaResponse -- client.tasks.ui_states.list(task_id) -> UiStateListResponse +- client.carriers.create(\*\*params) -> CarrierCreateResponse +- client.carriers.retrieve(carrier_id) -> CarrierRetrieveResponse +- client.carriers.update(carrier_id, \*\*params) -> CarrierUpdateResponse +- client.carriers.list(\*\*params) -> CarrierListResponse +- client.carriers.delete(carrier_id) -> CarrierDeleteResponse +- client.carriers.lookup(\*\*params) -> CarrierLookupResponse -# Agents +# Credentials Types: ```python -from mobilerun_sdk.types import AgentListResponse +from mobilerun_sdk.types import CredentialListResponse ``` Methods: -- client.agents.list() -> AgentListResponse +- client.credentials.list(\*\*params) -> CredentialListResponse -# Proxies +## Packages Types: ```python -from mobilerun_sdk.types import ( - ProxyConfig, - ProxyCreateResponse, - ProxyRetrieveResponse, - ProxyUpdateResponse, - ProxyListResponse, - ProxyDeleteResponse, -) +from mobilerun_sdk.types.credentials import PackageCreateResponse, PackageListResponse ``` Methods: -- client.proxies.create(\*\*params) -> ProxyCreateResponse -- client.proxies.retrieve(proxy_id) -> ProxyRetrieveResponse -- client.proxies.update(proxy_id, \*\*params) -> ProxyUpdateResponse -- client.proxies.list(\*\*params) -> ProxyListResponse -- client.proxies.delete(proxy_id) -> ProxyDeleteResponse +- client.credentials.packages.create(\*\*params) -> PackageCreateResponse +- client.credentials.packages.list(package_name) -> PackageListResponse -# Carriers +### Credentials Types: ```python -from mobilerun_sdk.types import Carrier, CarrierListResponse, CarrierDeleteResponse +from mobilerun_sdk.types.credentials.packages import ( + Credential, + CredentialCreateResponse, + CredentialRetrieveResponse, + CredentialDeleteResponse, +) ``` Methods: -- client.carriers.create(\*\*params) -> Carrier -- client.carriers.retrieve(carrier_id) -> Carrier -- client.carriers.update(carrier_id, \*\*params) -> Carrier -- client.carriers.list(\*\*params) -> CarrierListResponse -- client.carriers.delete(carrier_id) -> CarrierDeleteResponse -- client.carriers.lookup(\*\*params) -> Carrier +- client.credentials.packages.credentials.create(package_name, \*\*params) -> CredentialCreateResponse +- client.credentials.packages.credentials.retrieve(credential_name, \*, package_name) -> CredentialRetrieveResponse +- client.credentials.packages.credentials.delete(credential_name, \*, package_name) -> CredentialDeleteResponse -# Profiles +#### Fields Types: ```python -from mobilerun_sdk.types import Profile, ProfileListResponse, ProfileDeleteResponse +from mobilerun_sdk.types.credentials.packages.credentials import ( + FieldCreateResponse, + FieldUpdateResponse, + FieldDeleteResponse, +) ``` Methods: -- client.profiles.create(\*\*params) -> Profile -- client.profiles.retrieve(profile_id) -> Profile -- client.profiles.update(profile_id, \*\*params) -> Profile -- client.profiles.list(\*\*params) -> ProfileListResponse -- client.profiles.delete(profile_id) -> ProfileDeleteResponse +- client.credentials.packages.credentials.fields.create(credential_name, \*, package_name, \*\*params) -> FieldCreateResponse +- client.credentials.packages.credentials.fields.update(field_type, \*, package_name, credential_name, \*\*params) -> FieldUpdateResponse +- client.credentials.packages.credentials.fields.delete(field_type, \*, package_name, credential_name) -> FieldDeleteResponse # Devices @@ -156,132 +154,127 @@ Methods: - client.devices.terminate(device_id, \*\*params) -> None - client.devices.wait_ready(device_id) -> Device -## Time +## Actions Types: ```python -from mobilerun_sdk.types.devices import TimeTimeResponse, TimeTimezoneResponse +from mobilerun_sdk.types.devices import ActionOverlayVisibleResponse ``` Methods: -- client.devices.time.set_timezone(device_id, \*\*params) -> None -- client.devices.time.time(device_id) -> str -- client.devices.time.timezone(device_id) -> TimeTimezoneResponse - -## Profile - -Methods: - -- client.devices.profile.update(device_id, \*\*params) -> None +- client.devices.actions.global\_(device_id, \*\*params) -> None +- client.devices.actions.overlay_visible(device_id) -> ActionOverlayVisibleResponse +- client.devices.actions.set_overlay_visible(device_id, \*\*params) -> None +- client.devices.actions.swipe(device_id, \*\*params) -> None +- client.devices.actions.tap(device_id, \*\*params) -> None -## Files +## Apps Types: ```python -from mobilerun_sdk.types.devices import FileInfo, FileListResponse, FileDownloadResponse +from mobilerun_sdk.types.devices import AppListResponse ``` Methods: -- client.devices.files.list(device_id, \*\*params) -> FileListResponse -- client.devices.files.delete(device_id, \*\*params) -> None -- client.devices.files.download(device_id, \*\*params) -> str -- client.devices.files.upload(device_id, \*\*params) -> None +- client.devices.apps.update(package_name, \*, device_id) -> None +- client.devices.apps.list(device_id, \*\*params) -> Optional[AppListResponse] +- client.devices.apps.delete(package_name, \*, device_id) -> None +- client.devices.apps.install(device_id, \*\*params) -> None +- client.devices.apps.start(package_name, \*, device_id, \*\*params) -> None -## Proxy +## Esim Types: ```python -from mobilerun_sdk.types.devices import ProxyStatusResponse +from mobilerun_sdk.types.devices import EsimListResponse, EsimActivateResponse ``` Methods: -- client.devices.proxy.connect(device_id, \*\*params) -> None -- client.devices.proxy.disconnect(device_id) -> None -- client.devices.proxy.status(device_id) -> ProxyStatusResponse +- client.devices.esim.list(device_id) -> Optional[EsimListResponse] +- client.devices.esim.activate(device_id, \*\*params) -> EsimActivateResponse +- client.devices.esim.enable(device_id, \*\*params) -> None +- client.devices.esim.remove(device_id, \*\*params) -> None -## Location +## Files Types: ```python -from mobilerun_sdk.types.devices import LocationGetResponse +from mobilerun_sdk.types.devices import FileInfo, FileListResponse, FileDownloadResponse ``` Methods: -- client.devices.location.get(device_id) -> LocationGetResponse -- client.devices.location.set(device_id, \*\*params) -> None +- client.devices.files.list(device_id, \*\*params) -> FileListResponse +- client.devices.files.delete(device_id, \*\*params) -> None +- client.devices.files.download(device_id, \*\*params) -> str +- client.devices.files.upload(device_id, \*\*params) -> None -## Actions +## Keyboard -Types: +Methods: -```python -from mobilerun_sdk.types.devices import ActionOverlayVisibleResponse -``` +- client.devices.keyboard.clear(device_id) -> None +- client.devices.keyboard.key(device_id, \*\*params) -> None +- client.devices.keyboard.write(device_id, \*\*params) -> None + +## Location Methods: -- client.devices.actions.global\_(device_id, \*\*params) -> None -- client.devices.actions.overlay_visible(device_id) -> ActionOverlayVisibleResponse -- client.devices.actions.set_overlay_visible(device_id, \*\*params) -> None -- client.devices.actions.swipe(device_id, \*\*params) -> None -- client.devices.actions.tap(device_id, \*\*params) -> None +- client.devices.location.get(device_id) -> Location +- client.devices.location.set(device_id, \*\*params) -> None -## State +## Packages Types: ```python -from mobilerun_sdk.types.devices import Rect, StateScreenshotResponse, StateUiResponse +from mobilerun_sdk.types.devices import PackageListResponse ``` Methods: -- client.devices.state.screenshot(device_id, \*\*params) -> str -- client.devices.state.ui(device_id, \*\*params) -> StateUiResponse +- client.devices.packages.list(device_id, \*\*params) -> Optional[PackageListResponse] -## Apps +## Profile + +Methods: + +- client.devices.profile.update(device_id, \*\*params) -> None + +## Proxy Types: ```python -from mobilerun_sdk.types.devices import AppListResponse +from mobilerun_sdk.types.devices import ProxyStatusResponse ``` Methods: -- client.devices.apps.update(package_name, \*, device_id) -> None -- client.devices.apps.list(device_id, \*\*params) -> Optional[AppListResponse] -- client.devices.apps.delete(package_name, \*, device_id) -> None -- client.devices.apps.install(device_id, \*\*params) -> None -- client.devices.apps.start(package_name, \*, device_id, \*\*params) -> None +- client.devices.proxy.connect(device_id, \*\*params) -> None +- client.devices.proxy.disconnect(device_id) -> None +- client.devices.proxy.status(device_id) -> ProxyStatusResponse -## Packages +## State Types: ```python -from mobilerun_sdk.types.devices import PackageListResponse +from mobilerun_sdk.types.devices import Rect, StateScreenshotResponse, StateUiResponse ``` Methods: -- client.devices.packages.list(device_id, \*\*params) -> Optional[PackageListResponse] - -## Keyboard - -Methods: - -- client.devices.keyboard.clear(device_id) -> None -- client.devices.keyboard.key(device_id, \*\*params) -> None -- client.devices.keyboard.write(device_id, \*\*params) -> None +- client.devices.state.screenshot(device_id, \*\*params) -> str +- client.devices.state.ui(device_id, \*\*params) -> StateUiResponse ## Tasks @@ -295,129 +288,151 @@ Methods: - client.devices.tasks.list(device_id, \*\*params) -> TaskListResponse -## Esim +## Time Types: ```python -from mobilerun_sdk.types.devices import EsimListResponse, EsimActivateResponse +from mobilerun_sdk.types.devices import TimeTimeResponse, TimeTimezoneResponse ``` Methods: -- client.devices.esim.list(device_id) -> Optional[EsimListResponse] -- client.devices.esim.activate(device_id, \*\*params) -> EsimActivateResponse -- client.devices.esim.enable(device_id, \*\*params) -> None -- client.devices.esim.remove(device_id, \*\*params) -> None +- client.devices.time.set_timezone(device_id, \*\*params) -> None +- client.devices.time.time(device_id) -> str +- client.devices.time.timezone(device_id) -> TimeTimezoneResponse -# Apps +# Hooks Types: ```python -from mobilerun_sdk.types import AppListResponse +from mobilerun_sdk.types import ( + HookRetrieveResponse, + HookUpdateResponse, + HookListResponse, + HookGetSampleDataResponse, + HookPerformResponse, + HookSubscribeResponse, + HookUnsubscribeResponse, +) ``` Methods: -- client.apps.list(\*\*params) -> AppListResponse +- client.hooks.retrieve(hook_id) -> HookRetrieveResponse +- client.hooks.update(hook_id, \*\*params) -> HookUpdateResponse +- client.hooks.list(\*\*params) -> HookListResponse +- client.hooks.get_sample_data() -> HookGetSampleDataResponse +- client.hooks.perform(\*\*params) -> HookPerformResponse +- client.hooks.subscribe(\*\*params) -> HookSubscribeResponse +- client.hooks.unsubscribe(hook_id) -> HookUnsubscribeResponse -# Credentials +# Models Types: ```python -from mobilerun_sdk.types import CredentialListResponse +from mobilerun_sdk.types import ModelListResponse ``` Methods: -- client.credentials.list(\*\*params) -> CredentialListResponse +- client.models.list() -> ModelListResponse -## Packages +# Profiles Types: ```python -from mobilerun_sdk.types.credentials import PackageCreateResponse, PackageListResponse +from mobilerun_sdk.types import Profile, ProfileListResponse, ProfileDeleteResponse ``` Methods: -- client.credentials.packages.create(\*\*params) -> PackageCreateResponse -- client.credentials.packages.list(package_name) -> PackageListResponse +- client.profiles.create(\*\*params) -> Profile +- client.profiles.retrieve(profile_id) -> Profile +- client.profiles.update(profile_id, \*\*params) -> Profile +- client.profiles.list(\*\*params) -> ProfileListResponse +- client.profiles.delete(profile_id) -> ProfileDeleteResponse -### Credentials +# Proxies Types: ```python -from mobilerun_sdk.types.credentials.packages import ( - Credential, - CredentialCreateResponse, - CredentialRetrieveResponse, - CredentialDeleteResponse, +from mobilerun_sdk.types import ( + ProxyConfig, + ProxyCreateResponse, + ProxyRetrieveResponse, + ProxyUpdateResponse, + ProxyListResponse, + ProxyDeleteResponse, ) ``` Methods: -- client.credentials.packages.credentials.create(package_name, \*\*params) -> CredentialCreateResponse -- client.credentials.packages.credentials.retrieve(credential_name, \*, package_name) -> CredentialRetrieveResponse -- client.credentials.packages.credentials.delete(credential_name, \*, package_name) -> CredentialDeleteResponse +- client.proxies.create(\*\*params) -> ProxyCreateResponse +- client.proxies.retrieve(proxy_id) -> ProxyRetrieveResponse +- client.proxies.update(proxy_id, \*\*params) -> ProxyUpdateResponse +- client.proxies.list(\*\*params) -> ProxyListResponse +- client.proxies.delete(proxy_id) -> ProxyDeleteResponse -#### Fields +# Tasks Types: ```python -from mobilerun_sdk.types.credentials.packages.credentials import ( - FieldCreateResponse, - FieldUpdateResponse, - FieldDeleteResponse, +from mobilerun_sdk.types import ( + PackageCredentials, + Task, + TaskStatus, + UsageResult, + TaskRetrieveResponse, + TaskListResponse, + TaskGetStatusResponse, + TaskGetTrajectoryResponse, + TaskRunResponse, + TaskSendMessageResponse, + TaskStopResponse, ) ``` Methods: -- client.credentials.packages.credentials.fields.create(credential_name, \*, package_name, \*\*params) -> FieldCreateResponse -- client.credentials.packages.credentials.fields.update(field_type, \*, package_name, credential_name, \*\*params) -> FieldUpdateResponse -- client.credentials.packages.credentials.fields.delete(field_type, \*, package_name, credential_name) -> FieldDeleteResponse +- client.tasks.retrieve(task_id) -> TaskRetrieveResponse +- client.tasks.list(\*\*params) -> TaskListResponse +- client.tasks.attach(task_id) -> None +- client.tasks.get_status(task_id) -> TaskGetStatusResponse +- client.tasks.get_trajectory(task_id) -> TaskGetTrajectoryResponse +- client.tasks.run(\*\*params) -> TaskRunResponse +- client.tasks.run_streamed(\*\*params) -> object +- client.tasks.send_message(task_id, \*\*params) -> TaskSendMessageResponse +- client.tasks.stop(task_id) -> TaskStopResponse -# Hooks +## Screenshots Types: ```python -from mobilerun_sdk.types import ( - HookRetrieveResponse, - HookUpdateResponse, - HookListResponse, - HookGetSampleDataResponse, - HookPerformResponse, - HookSubscribeResponse, - HookUnsubscribeResponse, -) +from mobilerun_sdk.types.tasks import MediaResponse, ScreenshotListResponse ``` Methods: -- client.hooks.retrieve(hook_id) -> HookRetrieveResponse -- client.hooks.update(hook_id, \*\*params) -> HookUpdateResponse -- client.hooks.list(\*\*params) -> HookListResponse -- client.hooks.get_sample_data() -> HookGetSampleDataResponse -- client.hooks.perform(\*\*params) -> HookPerformResponse -- client.hooks.subscribe(\*\*params) -> HookSubscribeResponse -- client.hooks.unsubscribe(hook_id) -> HookUnsubscribeResponse +- client.tasks.screenshots.retrieve(index, \*, task_id) -> MediaResponse +- client.tasks.screenshots.list(task_id) -> ScreenshotListResponse -# Models +## UiStates Types: ```python -from mobilerun_sdk.types import ModelListResponse +from mobilerun_sdk.types.tasks import UiStateListResponse ``` Methods: -- client.models.list() -> ModelListResponse +- client.tasks.ui_states.retrieve(index, \*, task_id) -> MediaResponse +- client.tasks.ui_states.list(task_id) -> UiStateListResponse diff --git a/pyproject.toml b/pyproject.toml index 4a1ba0a..b097c23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mobilerun-sdk" -version = "3.1.0" +version = "3.2.0" description = "The official Python library for the mobilerun API" dynamic = ["readme"] license = "Apache-2.0" @@ -168,7 +168,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/mobilerun_sdk/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/mobilerun_sdk/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee..fe8451e 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response diff --git a/src/mobilerun_sdk/_client.py b/src/mobilerun_sdk/_client.py index 09cae30..469c26c 100644 --- a/src/mobilerun_sdk/_client.py +++ b/src/mobilerun_sdk/_client.py @@ -20,7 +20,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream @@ -96,6 +100,15 @@ def __init__( if base_url is None: base_url = f"https://api.mobilerun.ai/v1" + custom_headers_env = os.environ.get("MOBILERUN_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -107,13 +120,6 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - @cached_property - def tasks(self) -> TasksResource: - """Tasks API""" - from .resources.tasks import TasksResource - - return TasksResource(self) - @cached_property def agents(self) -> AgentsResource: """Agents API""" @@ -122,54 +128,66 @@ def agents(self) -> AgentsResource: return AgentsResource(self) @cached_property - def proxies(self) -> ProxiesResource: - from .resources.proxies import ProxiesResource + def apps(self) -> AppsResource: + """App Management""" + from .resources.apps import AppsResource - return ProxiesResource(self) + return AppsResource(self) @cached_property def carriers(self) -> CarriersResource: + """Mobile Carriers""" from .resources.carriers import CarriersResource return CarriersResource(self) @cached_property - def profiles(self) -> ProfilesResource: - from .resources.profiles import ProfilesResource + def credentials(self) -> CredentialsResource: + """Vault & Secrets""" + from .resources.credentials import CredentialsResource - return ProfilesResource(self) + return CredentialsResource(self) @cached_property def devices(self) -> DevicesResource: + """Device Management""" from .resources.devices import DevicesResource return DevicesResource(self) - @cached_property - def apps(self) -> AppsResource: - from .resources.apps import AppsResource - - return AppsResource(self) - - @cached_property - def credentials(self) -> CredentialsResource: - from .resources.credentials import CredentialsResource - - return CredentialsResource(self) - @cached_property def hooks(self) -> HooksResource: - """Webhooks API""" from .resources.hooks import HooksResource return HooksResource(self) @cached_property def models(self) -> ModelsResource: + """LLM Models""" from .resources.models import ModelsResource return ModelsResource(self) + @cached_property + def profiles(self) -> ProfilesResource: + from .resources.profiles import ProfilesResource + + return ProfilesResource(self) + + @cached_property + def proxies(self) -> ProxiesResource: + """Network Proxies""" + from .resources.proxies import ProxiesResource + + return ProxiesResource(self) + + @cached_property + def tasks(self) -> TasksResource: + """Tasks API""" + from .resources.tasks import TasksResource + + return TasksResource(self) + @cached_property def with_raw_response(self) -> MobilerunWithRawResponse: return MobilerunWithRawResponse(self) @@ -334,6 +352,15 @@ def __init__( if base_url is None: base_url = f"https://api.mobilerun.ai/v1" + custom_headers_env = os.environ.get("MOBILERUN_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -345,13 +372,6 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - @cached_property - def tasks(self) -> AsyncTasksResource: - """Tasks API""" - from .resources.tasks import AsyncTasksResource - - return AsyncTasksResource(self) - @cached_property def agents(self) -> AsyncAgentsResource: """Agents API""" @@ -360,54 +380,66 @@ def agents(self) -> AsyncAgentsResource: return AsyncAgentsResource(self) @cached_property - def proxies(self) -> AsyncProxiesResource: - from .resources.proxies import AsyncProxiesResource + def apps(self) -> AsyncAppsResource: + """App Management""" + from .resources.apps import AsyncAppsResource - return AsyncProxiesResource(self) + return AsyncAppsResource(self) @cached_property def carriers(self) -> AsyncCarriersResource: + """Mobile Carriers""" from .resources.carriers import AsyncCarriersResource return AsyncCarriersResource(self) @cached_property - def profiles(self) -> AsyncProfilesResource: - from .resources.profiles import AsyncProfilesResource + def credentials(self) -> AsyncCredentialsResource: + """Vault & Secrets""" + from .resources.credentials import AsyncCredentialsResource - return AsyncProfilesResource(self) + return AsyncCredentialsResource(self) @cached_property def devices(self) -> AsyncDevicesResource: + """Device Management""" from .resources.devices import AsyncDevicesResource return AsyncDevicesResource(self) - @cached_property - def apps(self) -> AsyncAppsResource: - from .resources.apps import AsyncAppsResource - - return AsyncAppsResource(self) - - @cached_property - def credentials(self) -> AsyncCredentialsResource: - from .resources.credentials import AsyncCredentialsResource - - return AsyncCredentialsResource(self) - @cached_property def hooks(self) -> AsyncHooksResource: - """Webhooks API""" from .resources.hooks import AsyncHooksResource return AsyncHooksResource(self) @cached_property def models(self) -> AsyncModelsResource: + """LLM Models""" from .resources.models import AsyncModelsResource return AsyncModelsResource(self) + @cached_property + def profiles(self) -> AsyncProfilesResource: + from .resources.profiles import AsyncProfilesResource + + return AsyncProfilesResource(self) + + @cached_property + def proxies(self) -> AsyncProxiesResource: + """Network Proxies""" + from .resources.proxies import AsyncProxiesResource + + return AsyncProxiesResource(self) + + @cached_property + def tasks(self) -> AsyncTasksResource: + """Tasks API""" + from .resources.tasks import AsyncTasksResource + + return AsyncTasksResource(self) + @cached_property def with_raw_response(self) -> AsyncMobilerunWithRawResponse: return AsyncMobilerunWithRawResponse(self) @@ -538,13 +570,6 @@ class MobilerunWithRawResponse: def __init__(self, client: Mobilerun) -> None: self._client = client - @cached_property - def tasks(self) -> tasks.TasksResourceWithRawResponse: - """Tasks API""" - from .resources.tasks import TasksResourceWithRawResponse - - return TasksResourceWithRawResponse(self._client.tasks) - @cached_property def agents(self) -> agents.AgentsResourceWithRawResponse: """Agents API""" @@ -553,67 +578,72 @@ def agents(self) -> agents.AgentsResourceWithRawResponse: return AgentsResourceWithRawResponse(self._client.agents) @cached_property - def proxies(self) -> proxies.ProxiesResourceWithRawResponse: - from .resources.proxies import ProxiesResourceWithRawResponse + def apps(self) -> apps.AppsResourceWithRawResponse: + """App Management""" + from .resources.apps import AppsResourceWithRawResponse - return ProxiesResourceWithRawResponse(self._client.proxies) + return AppsResourceWithRawResponse(self._client.apps) @cached_property def carriers(self) -> carriers.CarriersResourceWithRawResponse: + """Mobile Carriers""" from .resources.carriers import CarriersResourceWithRawResponse return CarriersResourceWithRawResponse(self._client.carriers) @cached_property - def profiles(self) -> profiles.ProfilesResourceWithRawResponse: - from .resources.profiles import ProfilesResourceWithRawResponse + def credentials(self) -> credentials.CredentialsResourceWithRawResponse: + """Vault & Secrets""" + from .resources.credentials import CredentialsResourceWithRawResponse - return ProfilesResourceWithRawResponse(self._client.profiles) + return CredentialsResourceWithRawResponse(self._client.credentials) @cached_property def devices(self) -> devices.DevicesResourceWithRawResponse: + """Device Management""" from .resources.devices import DevicesResourceWithRawResponse return DevicesResourceWithRawResponse(self._client.devices) - @cached_property - def apps(self) -> apps.AppsResourceWithRawResponse: - from .resources.apps import AppsResourceWithRawResponse - - return AppsResourceWithRawResponse(self._client.apps) - - @cached_property - def credentials(self) -> credentials.CredentialsResourceWithRawResponse: - from .resources.credentials import CredentialsResourceWithRawResponse - - return CredentialsResourceWithRawResponse(self._client.credentials) - @cached_property def hooks(self) -> hooks.HooksResourceWithRawResponse: - """Webhooks API""" from .resources.hooks import HooksResourceWithRawResponse return HooksResourceWithRawResponse(self._client.hooks) @cached_property def models(self) -> models.ModelsResourceWithRawResponse: + """LLM Models""" from .resources.models import ModelsResourceWithRawResponse return ModelsResourceWithRawResponse(self._client.models) + @cached_property + def profiles(self) -> profiles.ProfilesResourceWithRawResponse: + from .resources.profiles import ProfilesResourceWithRawResponse -class AsyncMobilerunWithRawResponse: - _client: AsyncMobilerun + return ProfilesResourceWithRawResponse(self._client.profiles) - def __init__(self, client: AsyncMobilerun) -> None: - self._client = client + @cached_property + def proxies(self) -> proxies.ProxiesResourceWithRawResponse: + """Network Proxies""" + from .resources.proxies import ProxiesResourceWithRawResponse + + return ProxiesResourceWithRawResponse(self._client.proxies) @cached_property - def tasks(self) -> tasks.AsyncTasksResourceWithRawResponse: + def tasks(self) -> tasks.TasksResourceWithRawResponse: """Tasks API""" - from .resources.tasks import AsyncTasksResourceWithRawResponse + from .resources.tasks import TasksResourceWithRawResponse - return AsyncTasksResourceWithRawResponse(self._client.tasks) + return TasksResourceWithRawResponse(self._client.tasks) + + +class AsyncMobilerunWithRawResponse: + _client: AsyncMobilerun + + def __init__(self, client: AsyncMobilerun) -> None: + self._client = client @cached_property def agents(self) -> agents.AsyncAgentsResourceWithRawResponse: @@ -623,67 +653,72 @@ def agents(self) -> agents.AsyncAgentsResourceWithRawResponse: return AsyncAgentsResourceWithRawResponse(self._client.agents) @cached_property - def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse: - from .resources.proxies import AsyncProxiesResourceWithRawResponse + def apps(self) -> apps.AsyncAppsResourceWithRawResponse: + """App Management""" + from .resources.apps import AsyncAppsResourceWithRawResponse - return AsyncProxiesResourceWithRawResponse(self._client.proxies) + return AsyncAppsResourceWithRawResponse(self._client.apps) @cached_property def carriers(self) -> carriers.AsyncCarriersResourceWithRawResponse: + """Mobile Carriers""" from .resources.carriers import AsyncCarriersResourceWithRawResponse return AsyncCarriersResourceWithRawResponse(self._client.carriers) @cached_property - def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse: - from .resources.profiles import AsyncProfilesResourceWithRawResponse + def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: + """Vault & Secrets""" + from .resources.credentials import AsyncCredentialsResourceWithRawResponse - return AsyncProfilesResourceWithRawResponse(self._client.profiles) + return AsyncCredentialsResourceWithRawResponse(self._client.credentials) @cached_property def devices(self) -> devices.AsyncDevicesResourceWithRawResponse: + """Device Management""" from .resources.devices import AsyncDevicesResourceWithRawResponse return AsyncDevicesResourceWithRawResponse(self._client.devices) - @cached_property - def apps(self) -> apps.AsyncAppsResourceWithRawResponse: - from .resources.apps import AsyncAppsResourceWithRawResponse - - return AsyncAppsResourceWithRawResponse(self._client.apps) - - @cached_property - def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: - from .resources.credentials import AsyncCredentialsResourceWithRawResponse - - return AsyncCredentialsResourceWithRawResponse(self._client.credentials) - @cached_property def hooks(self) -> hooks.AsyncHooksResourceWithRawResponse: - """Webhooks API""" from .resources.hooks import AsyncHooksResourceWithRawResponse return AsyncHooksResourceWithRawResponse(self._client.hooks) @cached_property def models(self) -> models.AsyncModelsResourceWithRawResponse: + """LLM Models""" from .resources.models import AsyncModelsResourceWithRawResponse return AsyncModelsResourceWithRawResponse(self._client.models) + @cached_property + def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse: + from .resources.profiles import AsyncProfilesResourceWithRawResponse -class MobilerunWithStreamedResponse: - _client: Mobilerun + return AsyncProfilesResourceWithRawResponse(self._client.profiles) - def __init__(self, client: Mobilerun) -> None: - self._client = client + @cached_property + def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse: + """Network Proxies""" + from .resources.proxies import AsyncProxiesResourceWithRawResponse + + return AsyncProxiesResourceWithRawResponse(self._client.proxies) @cached_property - def tasks(self) -> tasks.TasksResourceWithStreamingResponse: + def tasks(self) -> tasks.AsyncTasksResourceWithRawResponse: """Tasks API""" - from .resources.tasks import TasksResourceWithStreamingResponse + from .resources.tasks import AsyncTasksResourceWithRawResponse - return TasksResourceWithStreamingResponse(self._client.tasks) + return AsyncTasksResourceWithRawResponse(self._client.tasks) + + +class MobilerunWithStreamedResponse: + _client: Mobilerun + + def __init__(self, client: Mobilerun) -> None: + self._client = client @cached_property def agents(self) -> agents.AgentsResourceWithStreamingResponse: @@ -693,67 +728,72 @@ def agents(self) -> agents.AgentsResourceWithStreamingResponse: return AgentsResourceWithStreamingResponse(self._client.agents) @cached_property - def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse: - from .resources.proxies import ProxiesResourceWithStreamingResponse + def apps(self) -> apps.AppsResourceWithStreamingResponse: + """App Management""" + from .resources.apps import AppsResourceWithStreamingResponse - return ProxiesResourceWithStreamingResponse(self._client.proxies) + return AppsResourceWithStreamingResponse(self._client.apps) @cached_property def carriers(self) -> carriers.CarriersResourceWithStreamingResponse: + """Mobile Carriers""" from .resources.carriers import CarriersResourceWithStreamingResponse return CarriersResourceWithStreamingResponse(self._client.carriers) @cached_property - def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse: - from .resources.profiles import ProfilesResourceWithStreamingResponse + def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: + """Vault & Secrets""" + from .resources.credentials import CredentialsResourceWithStreamingResponse - return ProfilesResourceWithStreamingResponse(self._client.profiles) + return CredentialsResourceWithStreamingResponse(self._client.credentials) @cached_property def devices(self) -> devices.DevicesResourceWithStreamingResponse: + """Device Management""" from .resources.devices import DevicesResourceWithStreamingResponse return DevicesResourceWithStreamingResponse(self._client.devices) - @cached_property - def apps(self) -> apps.AppsResourceWithStreamingResponse: - from .resources.apps import AppsResourceWithStreamingResponse - - return AppsResourceWithStreamingResponse(self._client.apps) - - @cached_property - def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: - from .resources.credentials import CredentialsResourceWithStreamingResponse - - return CredentialsResourceWithStreamingResponse(self._client.credentials) - @cached_property def hooks(self) -> hooks.HooksResourceWithStreamingResponse: - """Webhooks API""" from .resources.hooks import HooksResourceWithStreamingResponse return HooksResourceWithStreamingResponse(self._client.hooks) @cached_property def models(self) -> models.ModelsResourceWithStreamingResponse: + """LLM Models""" from .resources.models import ModelsResourceWithStreamingResponse return ModelsResourceWithStreamingResponse(self._client.models) + @cached_property + def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse: + from .resources.profiles import ProfilesResourceWithStreamingResponse -class AsyncMobilerunWithStreamedResponse: - _client: AsyncMobilerun + return ProfilesResourceWithStreamingResponse(self._client.profiles) - def __init__(self, client: AsyncMobilerun) -> None: - self._client = client + @cached_property + def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse: + """Network Proxies""" + from .resources.proxies import ProxiesResourceWithStreamingResponse + + return ProxiesResourceWithStreamingResponse(self._client.proxies) @cached_property - def tasks(self) -> tasks.AsyncTasksResourceWithStreamingResponse: + def tasks(self) -> tasks.TasksResourceWithStreamingResponse: """Tasks API""" - from .resources.tasks import AsyncTasksResourceWithStreamingResponse + from .resources.tasks import TasksResourceWithStreamingResponse - return AsyncTasksResourceWithStreamingResponse(self._client.tasks) + return TasksResourceWithStreamingResponse(self._client.tasks) + + +class AsyncMobilerunWithStreamedResponse: + _client: AsyncMobilerun + + def __init__(self, client: AsyncMobilerun) -> None: + self._client = client @cached_property def agents(self) -> agents.AsyncAgentsResourceWithStreamingResponse: @@ -763,54 +803,66 @@ def agents(self) -> agents.AsyncAgentsResourceWithStreamingResponse: return AsyncAgentsResourceWithStreamingResponse(self._client.agents) @cached_property - def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse: - from .resources.proxies import AsyncProxiesResourceWithStreamingResponse + def apps(self) -> apps.AsyncAppsResourceWithStreamingResponse: + """App Management""" + from .resources.apps import AsyncAppsResourceWithStreamingResponse - return AsyncProxiesResourceWithStreamingResponse(self._client.proxies) + return AsyncAppsResourceWithStreamingResponse(self._client.apps) @cached_property def carriers(self) -> carriers.AsyncCarriersResourceWithStreamingResponse: + """Mobile Carriers""" from .resources.carriers import AsyncCarriersResourceWithStreamingResponse return AsyncCarriersResourceWithStreamingResponse(self._client.carriers) @cached_property - def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse: - from .resources.profiles import AsyncProfilesResourceWithStreamingResponse + def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingResponse: + """Vault & Secrets""" + from .resources.credentials import AsyncCredentialsResourceWithStreamingResponse - return AsyncProfilesResourceWithStreamingResponse(self._client.profiles) + return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) @cached_property def devices(self) -> devices.AsyncDevicesResourceWithStreamingResponse: + """Device Management""" from .resources.devices import AsyncDevicesResourceWithStreamingResponse return AsyncDevicesResourceWithStreamingResponse(self._client.devices) - @cached_property - def apps(self) -> apps.AsyncAppsResourceWithStreamingResponse: - from .resources.apps import AsyncAppsResourceWithStreamingResponse - - return AsyncAppsResourceWithStreamingResponse(self._client.apps) - - @cached_property - def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingResponse: - from .resources.credentials import AsyncCredentialsResourceWithStreamingResponse - - return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) - @cached_property def hooks(self) -> hooks.AsyncHooksResourceWithStreamingResponse: - """Webhooks API""" from .resources.hooks import AsyncHooksResourceWithStreamingResponse return AsyncHooksResourceWithStreamingResponse(self._client.hooks) @cached_property def models(self) -> models.AsyncModelsResourceWithStreamingResponse: + """LLM Models""" from .resources.models import AsyncModelsResourceWithStreamingResponse return AsyncModelsResourceWithStreamingResponse(self._client.models) + @cached_property + def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse: + from .resources.profiles import AsyncProfilesResourceWithStreamingResponse + + return AsyncProfilesResourceWithStreamingResponse(self._client.profiles) + + @cached_property + def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse: + """Network Proxies""" + from .resources.proxies import AsyncProxiesResourceWithStreamingResponse + + return AsyncProxiesResourceWithStreamingResponse(self._client.proxies) + + @cached_property + def tasks(self) -> tasks.AsyncTasksResourceWithStreamingResponse: + """Tasks API""" + from .resources.tasks import AsyncTasksResourceWithStreamingResponse + + return AsyncTasksResourceWithStreamingResponse(self._client.tasks) + Client = Mobilerun diff --git a/src/mobilerun_sdk/_files.py b/src/mobilerun_sdk/_files.py index 0f0f150..22855ba 100644 --- a/src/mobilerun_sdk/_files.py +++ b/src/mobilerun_sdk/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -97,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/mobilerun_sdk/_models.py b/src/mobilerun_sdk/_models.py index 29070e0..8c5ab26 100644 --- a/src/mobilerun_sdk/_models.py +++ b/src/mobilerun_sdk/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/src/mobilerun_sdk/_qs.py b/src/mobilerun_sdk/_qs.py index de8c99b..4127c19 100644 --- a/src/mobilerun_sdk/_qs.py +++ b/src/mobilerun_sdk/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/mobilerun_sdk/_types.py b/src/mobilerun_sdk/_types.py index 0f64915..c41c84a 100644 --- a/src/mobilerun_sdk/_types.py +++ b/src/mobilerun_sdk/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/mobilerun_sdk/_utils/__init__.py b/src/mobilerun_sdk/_utils/__init__.py index 10cb66d..1c090e5 100644 --- a/src/mobilerun_sdk/_utils/__init__.py +++ b/src/mobilerun_sdk/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/mobilerun_sdk/_utils/_utils.py b/src/mobilerun_sdk/_utils/_utils.py index 63b8cd6..199cd23 100644 --- a/src/mobilerun_sdk/_utils/_utils.py +++ b/src/mobilerun_sdk/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) @@ -177,21 +203,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/src/mobilerun_sdk/_version.py b/src/mobilerun_sdk/_version.py index f3f6069..a3beadb 100644 --- a/src/mobilerun_sdk/_version.py +++ b/src/mobilerun_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "mobilerun_sdk" -__version__ = "3.1.0" # x-release-please-version +__version__ = "3.2.0" # x-release-please-version diff --git a/src/mobilerun_sdk/resources/__init__.py b/src/mobilerun_sdk/resources/__init__.py index 2ded3d8..21f473c 100644 --- a/src/mobilerun_sdk/resources/__init__.py +++ b/src/mobilerun_sdk/resources/__init__.py @@ -82,54 +82,36 @@ ) __all__ = [ - "TasksResource", - "AsyncTasksResource", - "TasksResourceWithRawResponse", - "AsyncTasksResourceWithRawResponse", - "TasksResourceWithStreamingResponse", - "AsyncTasksResourceWithStreamingResponse", "AgentsResource", "AsyncAgentsResource", "AgentsResourceWithRawResponse", "AsyncAgentsResourceWithRawResponse", "AgentsResourceWithStreamingResponse", "AsyncAgentsResourceWithStreamingResponse", - "ProxiesResource", - "AsyncProxiesResource", - "ProxiesResourceWithRawResponse", - "AsyncProxiesResourceWithRawResponse", - "ProxiesResourceWithStreamingResponse", - "AsyncProxiesResourceWithStreamingResponse", - "CarriersResource", - "AsyncCarriersResource", - "CarriersResourceWithRawResponse", - "AsyncCarriersResourceWithRawResponse", - "CarriersResourceWithStreamingResponse", - "AsyncCarriersResourceWithStreamingResponse", - "ProfilesResource", - "AsyncProfilesResource", - "ProfilesResourceWithRawResponse", - "AsyncProfilesResourceWithRawResponse", - "ProfilesResourceWithStreamingResponse", - "AsyncProfilesResourceWithStreamingResponse", - "DevicesResource", - "AsyncDevicesResource", - "DevicesResourceWithRawResponse", - "AsyncDevicesResourceWithRawResponse", - "DevicesResourceWithStreamingResponse", - "AsyncDevicesResourceWithStreamingResponse", "AppsResource", "AsyncAppsResource", "AppsResourceWithRawResponse", "AsyncAppsResourceWithRawResponse", "AppsResourceWithStreamingResponse", "AsyncAppsResourceWithStreamingResponse", + "CarriersResource", + "AsyncCarriersResource", + "CarriersResourceWithRawResponse", + "AsyncCarriersResourceWithRawResponse", + "CarriersResourceWithStreamingResponse", + "AsyncCarriersResourceWithStreamingResponse", "CredentialsResource", "AsyncCredentialsResource", "CredentialsResourceWithRawResponse", "AsyncCredentialsResourceWithRawResponse", "CredentialsResourceWithStreamingResponse", "AsyncCredentialsResourceWithStreamingResponse", + "DevicesResource", + "AsyncDevicesResource", + "DevicesResourceWithRawResponse", + "AsyncDevicesResourceWithRawResponse", + "DevicesResourceWithStreamingResponse", + "AsyncDevicesResourceWithStreamingResponse", "HooksResource", "AsyncHooksResource", "HooksResourceWithRawResponse", @@ -142,4 +124,22 @@ "AsyncModelsResourceWithRawResponse", "ModelsResourceWithStreamingResponse", "AsyncModelsResourceWithStreamingResponse", + "ProfilesResource", + "AsyncProfilesResource", + "ProfilesResourceWithRawResponse", + "AsyncProfilesResourceWithRawResponse", + "ProfilesResourceWithStreamingResponse", + "AsyncProfilesResourceWithStreamingResponse", + "ProxiesResource", + "AsyncProxiesResource", + "ProxiesResourceWithRawResponse", + "AsyncProxiesResourceWithRawResponse", + "ProxiesResourceWithStreamingResponse", + "AsyncProxiesResourceWithStreamingResponse", + "TasksResource", + "AsyncTasksResource", + "TasksResourceWithRawResponse", + "AsyncTasksResourceWithRawResponse", + "TasksResourceWithStreamingResponse", + "AsyncTasksResourceWithStreamingResponse", ] diff --git a/src/mobilerun_sdk/resources/apps.py b/src/mobilerun_sdk/resources/apps.py index 1b80d91..2587290 100644 --- a/src/mobilerun_sdk/resources/apps.py +++ b/src/mobilerun_sdk/resources/apps.py @@ -2,13 +2,14 @@ from __future__ import annotations +from typing import Iterable from typing_extensions import Literal import httpx -from ..types import app_list_params +from ..types import app_list_params, app_create_signed_upload_url_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -19,11 +20,18 @@ ) from .._base_client import make_request_options from ..types.app_list_response import AppListResponse +from ..types.app_delete_response import AppDeleteResponse +from ..types.app_retrieve_response import AppRetrieveResponse +from ..types.app_mark_failed_response import AppMarkFailedResponse +from ..types.app_confirm_upload_response import AppConfirmUploadResponse +from ..types.app_create_signed_upload_url_response import AppCreateSignedUploadURLResponse __all__ = ["AppsResource", "AsyncAppsResource"] class AppsResource(SyncAPIResource): + """App Management""" + @cached_property def with_raw_response(self) -> AppsResourceWithRawResponse: """ @@ -43,15 +51,49 @@ def with_streaming_response(self) -> AppsResourceWithStreamingResponse: """ return AppsResourceWithStreamingResponse(self) + def retrieve( + self, + 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, + ) -> AppRetrieveResponse: + """ + Retrieves an app by its ID + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/apps/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppRetrieveResponse, + ) + def list( self, *, order: Literal["asc", "desc"] | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, + platform: Literal["all", "android", "ios"] | Omit = omit, query: str | Omit = omit, sort_by: Literal["createdAt", "name"] | Omit = omit, - source: Literal["all", "uploaded", "store", "queued"] | Omit = omit, + status: Literal["all", "queued", "available", "failed"] | 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, @@ -83,9 +125,10 @@ def list( "order": order, "page": page, "page_size": page_size, + "platform": platform, "query": query, "sort_by": sort_by, - "source": source, + "status": status, }, app_list_params.AppListParams, ), @@ -93,8 +136,173 @@ def list( cast_to=AppListResponse, ) + def delete( + self, + 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, + ) -> AppDeleteResponse: + """Deletes an uploaded app by ID. + + Removes files from R2 storage and the database + entry. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._delete( + path_template("/apps/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppDeleteResponse, + ) + + def confirm_upload( + self, + 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, + ) -> AppConfirmUploadResponse: + """ + Verifies the APK file exists in R2 and sets the app status to available. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + path_template("/apps/{id}/confirm-upload", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppConfirmUploadResponse, + ) + + def create_signed_upload_url( + self, + *, + bundle_id: str, + display_name: str, + files: Iterable[app_create_signed_upload_url_params.File], + size_bytes: float, + version_code: float, + version_name: str, + country: str | Omit = omit, + description: str | Omit = omit, + developer_name: str | Omit = omit, + icon_url: str | Omit = omit, + platform: Literal["android", "ios"] | Omit = omit, + target_sdk: float | 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, + ) -> AppCreateSignedUploadURLResponse: + """ + Creates or updates an app and returns pre-signed Cloudflare R2 upload URLs for + each file + + Args: + country: Country code for Search Results + + 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._post( + "/apps/create-signed-upload-url", + body=maybe_transform( + { + "bundle_id": bundle_id, + "display_name": display_name, + "files": files, + "size_bytes": size_bytes, + "version_code": version_code, + "version_name": version_name, + "country": country, + "description": description, + "developer_name": developer_name, + "icon_url": icon_url, + "platform": platform, + "target_sdk": target_sdk, + }, + app_create_signed_upload_url_params.AppCreateSignedUploadURLParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppCreateSignedUploadURLResponse, + ) + + def mark_failed( + self, + 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, + ) -> AppMarkFailedResponse: + """ + Sets the app status to failed. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + path_template("/apps/{id}/mark-failed", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppMarkFailedResponse, + ) + class AsyncAppsResource(AsyncAPIResource): + """App Management""" + @cached_property def with_raw_response(self) -> AsyncAppsResourceWithRawResponse: """ @@ -114,15 +322,49 @@ def with_streaming_response(self) -> AsyncAppsResourceWithStreamingResponse: """ return AsyncAppsResourceWithStreamingResponse(self) + async def retrieve( + self, + 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, + ) -> AppRetrieveResponse: + """ + Retrieves an app by its ID + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/apps/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppRetrieveResponse, + ) + async def list( self, *, order: Literal["asc", "desc"] | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, + platform: Literal["all", "android", "ios"] | Omit = omit, query: str | Omit = omit, sort_by: Literal["createdAt", "name"] | Omit = omit, - source: Literal["all", "uploaded", "store", "queued"] | Omit = omit, + status: Literal["all", "queued", "available", "failed"] | 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, @@ -154,9 +396,10 @@ async def list( "order": order, "page": page, "page_size": page_size, + "platform": platform, "query": query, "sort_by": sort_by, - "source": source, + "status": status, }, app_list_params.AppListParams, ), @@ -164,38 +407,261 @@ async def list( cast_to=AppListResponse, ) + async def delete( + self, + 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, + ) -> AppDeleteResponse: + """Deletes an uploaded app by ID. + + Removes files from R2 storage and the database + entry. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._delete( + path_template("/apps/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppDeleteResponse, + ) + + async def confirm_upload( + self, + 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, + ) -> AppConfirmUploadResponse: + """ + Verifies the APK file exists in R2 and sets the app status to available. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + path_template("/apps/{id}/confirm-upload", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppConfirmUploadResponse, + ) + + async def create_signed_upload_url( + self, + *, + bundle_id: str, + display_name: str, + files: Iterable[app_create_signed_upload_url_params.File], + size_bytes: float, + version_code: float, + version_name: str, + country: str | Omit = omit, + description: str | Omit = omit, + developer_name: str | Omit = omit, + icon_url: str | Omit = omit, + platform: Literal["android", "ios"] | Omit = omit, + target_sdk: float | 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, + ) -> AppCreateSignedUploadURLResponse: + """ + Creates or updates an app and returns pre-signed Cloudflare R2 upload URLs for + each file + + Args: + country: Country code for Search Results + + 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._post( + "/apps/create-signed-upload-url", + body=await async_maybe_transform( + { + "bundle_id": bundle_id, + "display_name": display_name, + "files": files, + "size_bytes": size_bytes, + "version_code": version_code, + "version_name": version_name, + "country": country, + "description": description, + "developer_name": developer_name, + "icon_url": icon_url, + "platform": platform, + "target_sdk": target_sdk, + }, + app_create_signed_upload_url_params.AppCreateSignedUploadURLParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppCreateSignedUploadURLResponse, + ) + + async def mark_failed( + self, + 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, + ) -> AppMarkFailedResponse: + """ + Sets the app status to failed. + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + path_template("/apps/{id}/mark-failed", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AppMarkFailedResponse, + ) + class AppsResourceWithRawResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps + self.retrieve = to_raw_response_wrapper( + apps.retrieve, + ) self.list = to_raw_response_wrapper( apps.list, ) + self.delete = to_raw_response_wrapper( + apps.delete, + ) + self.confirm_upload = to_raw_response_wrapper( + apps.confirm_upload, + ) + self.create_signed_upload_url = to_raw_response_wrapper( + apps.create_signed_upload_url, + ) + self.mark_failed = to_raw_response_wrapper( + apps.mark_failed, + ) class AsyncAppsResourceWithRawResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps + self.retrieve = async_to_raw_response_wrapper( + apps.retrieve, + ) self.list = async_to_raw_response_wrapper( apps.list, ) + self.delete = async_to_raw_response_wrapper( + apps.delete, + ) + self.confirm_upload = async_to_raw_response_wrapper( + apps.confirm_upload, + ) + self.create_signed_upload_url = async_to_raw_response_wrapper( + apps.create_signed_upload_url, + ) + self.mark_failed = async_to_raw_response_wrapper( + apps.mark_failed, + ) class AppsResourceWithStreamingResponse: def __init__(self, apps: AppsResource) -> None: self._apps = apps + self.retrieve = to_streamed_response_wrapper( + apps.retrieve, + ) self.list = to_streamed_response_wrapper( apps.list, ) + self.delete = to_streamed_response_wrapper( + apps.delete, + ) + self.confirm_upload = to_streamed_response_wrapper( + apps.confirm_upload, + ) + self.create_signed_upload_url = to_streamed_response_wrapper( + apps.create_signed_upload_url, + ) + self.mark_failed = to_streamed_response_wrapper( + apps.mark_failed, + ) class AsyncAppsResourceWithStreamingResponse: def __init__(self, apps: AsyncAppsResource) -> None: self._apps = apps + self.retrieve = async_to_streamed_response_wrapper( + apps.retrieve, + ) self.list = async_to_streamed_response_wrapper( apps.list, ) + self.delete = async_to_streamed_response_wrapper( + apps.delete, + ) + self.confirm_upload = async_to_streamed_response_wrapper( + apps.confirm_upload, + ) + self.create_signed_upload_url = async_to_streamed_response_wrapper( + apps.create_signed_upload_url, + ) + self.mark_failed = async_to_streamed_response_wrapper( + apps.mark_failed, + ) diff --git a/src/mobilerun_sdk/resources/carriers.py b/src/mobilerun_sdk/resources/carriers.py index 642493e..d627159 100644 --- a/src/mobilerun_sdk/resources/carriers.py +++ b/src/mobilerun_sdk/resources/carriers.py @@ -18,14 +18,19 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.carrier import Carrier from ..types.carrier_list_response import CarrierListResponse +from ..types.carrier_create_response import CarrierCreateResponse from ..types.carrier_delete_response import CarrierDeleteResponse +from ..types.carrier_lookup_response import CarrierLookupResponse +from ..types.carrier_update_response import CarrierUpdateResponse +from ..types.carrier_retrieve_response import CarrierRetrieveResponse __all__ = ["CarriersResource", "AsyncCarriersResource"] class CarriersResource(SyncAPIResource): + """Mobile Carriers""" + @cached_property def with_raw_response(self) -> CarriersResourceWithRawResponse: """ @@ -70,7 +75,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierCreateResponse: """ Create a new carrier @@ -141,7 +146,7 @@ def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Carrier, + cast_to=CarrierCreateResponse, ) def retrieve( @@ -154,7 +159,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierRetrieveResponse: """ Get carrier by ID @@ -174,7 +179,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Carrier, + cast_to=CarrierRetrieveResponse, ) def update( @@ -201,7 +206,7 @@ def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierUpdateResponse: """ Update a carrier @@ -268,7 +273,7 @@ def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Carrier, + cast_to=CarrierUpdateResponse, ) def list( @@ -377,7 +382,7 @@ def lookup( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierLookupResponse: """ Get carrier by MCC and MNC @@ -409,11 +414,13 @@ def lookup( carrier_lookup_params.CarrierLookupParams, ), ), - cast_to=Carrier, + cast_to=CarrierLookupResponse, ) class AsyncCarriersResource(AsyncAPIResource): + """Mobile Carriers""" + @cached_property def with_raw_response(self) -> AsyncCarriersResourceWithRawResponse: """ @@ -458,7 +465,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierCreateResponse: """ Create a new carrier @@ -529,7 +536,7 @@ async def create( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Carrier, + cast_to=CarrierCreateResponse, ) async def retrieve( @@ -542,7 +549,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierRetrieveResponse: """ Get carrier by ID @@ -562,7 +569,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Carrier, + cast_to=CarrierRetrieveResponse, ) async def update( @@ -589,7 +596,7 @@ async def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierUpdateResponse: """ Update a carrier @@ -656,7 +663,7 @@ async def update( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Carrier, + cast_to=CarrierUpdateResponse, ) async def list( @@ -765,7 +772,7 @@ async def lookup( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Carrier: + ) -> CarrierLookupResponse: """ Get carrier by MCC and MNC @@ -797,7 +804,7 @@ async def lookup( carrier_lookup_params.CarrierLookupParams, ), ), - cast_to=Carrier, + cast_to=CarrierLookupResponse, ) diff --git a/src/mobilerun_sdk/resources/credentials/credentials.py b/src/mobilerun_sdk/resources/credentials/credentials.py index 43d30f4..9276d9f 100644 --- a/src/mobilerun_sdk/resources/credentials/credentials.py +++ b/src/mobilerun_sdk/resources/credentials/credentials.py @@ -30,8 +30,11 @@ class CredentialsResource(SyncAPIResource): + """Vault & Secrets""" + @cached_property def packages(self) -> PackagesResource: + """Vault & Secrets""" return PackagesResource(self._client) @cached_property @@ -97,8 +100,11 @@ def list( class AsyncCredentialsResource(AsyncAPIResource): + """Vault & Secrets""" + @cached_property def packages(self) -> AsyncPackagesResource: + """Vault & Secrets""" return AsyncPackagesResource(self._client) @cached_property @@ -173,6 +179,7 @@ def __init__(self, credentials: CredentialsResource) -> None: @cached_property def packages(self) -> PackagesResourceWithRawResponse: + """Vault & Secrets""" return PackagesResourceWithRawResponse(self._credentials.packages) @@ -186,6 +193,7 @@ def __init__(self, credentials: AsyncCredentialsResource) -> None: @cached_property def packages(self) -> AsyncPackagesResourceWithRawResponse: + """Vault & Secrets""" return AsyncPackagesResourceWithRawResponse(self._credentials.packages) @@ -199,6 +207,7 @@ def __init__(self, credentials: CredentialsResource) -> None: @cached_property def packages(self) -> PackagesResourceWithStreamingResponse: + """Vault & Secrets""" return PackagesResourceWithStreamingResponse(self._credentials.packages) @@ -212,4 +221,5 @@ def __init__(self, credentials: AsyncCredentialsResource) -> None: @cached_property def packages(self) -> AsyncPackagesResourceWithStreamingResponse: + """Vault & Secrets""" return AsyncPackagesResourceWithStreamingResponse(self._credentials.packages) diff --git a/src/mobilerun_sdk/resources/credentials/packages/credentials/credentials.py b/src/mobilerun_sdk/resources/credentials/packages/credentials/credentials.py index 1802563..df12c15 100644 --- a/src/mobilerun_sdk/resources/credentials/packages/credentials/credentials.py +++ b/src/mobilerun_sdk/resources/credentials/packages/credentials/credentials.py @@ -34,8 +34,11 @@ class CredentialsResource(SyncAPIResource): + """Vault & Secrets""" + @cached_property def fields(self) -> FieldsResource: + """Vault & Secrets""" return FieldsResource(self._client) @cached_property @@ -181,8 +184,11 @@ def delete( class AsyncCredentialsResource(AsyncAPIResource): + """Vault & Secrets""" + @cached_property def fields(self) -> AsyncFieldsResource: + """Vault & Secrets""" return AsyncFieldsResource(self._client) @cached_property @@ -343,6 +349,7 @@ def __init__(self, credentials: CredentialsResource) -> None: @cached_property def fields(self) -> FieldsResourceWithRawResponse: + """Vault & Secrets""" return FieldsResourceWithRawResponse(self._credentials.fields) @@ -362,6 +369,7 @@ def __init__(self, credentials: AsyncCredentialsResource) -> None: @cached_property def fields(self) -> AsyncFieldsResourceWithRawResponse: + """Vault & Secrets""" return AsyncFieldsResourceWithRawResponse(self._credentials.fields) @@ -381,6 +389,7 @@ def __init__(self, credentials: CredentialsResource) -> None: @cached_property def fields(self) -> FieldsResourceWithStreamingResponse: + """Vault & Secrets""" return FieldsResourceWithStreamingResponse(self._credentials.fields) @@ -400,4 +409,5 @@ def __init__(self, credentials: AsyncCredentialsResource) -> None: @cached_property def fields(self) -> AsyncFieldsResourceWithStreamingResponse: + """Vault & Secrets""" return AsyncFieldsResourceWithStreamingResponse(self._credentials.fields) diff --git a/src/mobilerun_sdk/resources/credentials/packages/credentials/fields.py b/src/mobilerun_sdk/resources/credentials/packages/credentials/fields.py index 77940d6..38ebfd9 100644 --- a/src/mobilerun_sdk/resources/credentials/packages/credentials/fields.py +++ b/src/mobilerun_sdk/resources/credentials/packages/credentials/fields.py @@ -26,6 +26,8 @@ class FieldsResource(SyncAPIResource): + """Vault & Secrets""" + @cached_property def with_raw_response(self) -> FieldsResourceWithRawResponse: """ @@ -192,6 +194,8 @@ def delete( class AsyncFieldsResource(AsyncAPIResource): + """Vault & Secrets""" + @cached_property def with_raw_response(self) -> AsyncFieldsResourceWithRawResponse: """ diff --git a/src/mobilerun_sdk/resources/credentials/packages/packages.py b/src/mobilerun_sdk/resources/credentials/packages/packages.py index f871c41..135ccdd 100644 --- a/src/mobilerun_sdk/resources/credentials/packages/packages.py +++ b/src/mobilerun_sdk/resources/credentials/packages/packages.py @@ -31,8 +31,11 @@ class PackagesResource(SyncAPIResource): + """Vault & Secrets""" + @cached_property def credentials(self) -> CredentialsResource: + """Vault & Secrets""" return CredentialsResource(self._client) @cached_property @@ -121,8 +124,11 @@ def list( class AsyncPackagesResource(AsyncAPIResource): + """Vault & Secrets""" + @cached_property def credentials(self) -> AsyncCredentialsResource: + """Vault & Secrets""" return AsyncCredentialsResource(self._client) @cached_property @@ -223,6 +229,7 @@ def __init__(self, packages: PackagesResource) -> None: @cached_property def credentials(self) -> CredentialsResourceWithRawResponse: + """Vault & Secrets""" return CredentialsResourceWithRawResponse(self._packages.credentials) @@ -239,6 +246,7 @@ def __init__(self, packages: AsyncPackagesResource) -> None: @cached_property def credentials(self) -> AsyncCredentialsResourceWithRawResponse: + """Vault & Secrets""" return AsyncCredentialsResourceWithRawResponse(self._packages.credentials) @@ -255,6 +263,7 @@ def __init__(self, packages: PackagesResource) -> None: @cached_property def credentials(self) -> CredentialsResourceWithStreamingResponse: + """Vault & Secrets""" return CredentialsResourceWithStreamingResponse(self._packages.credentials) @@ -271,4 +280,5 @@ def __init__(self, packages: AsyncPackagesResource) -> None: @cached_property def credentials(self) -> AsyncCredentialsResourceWithStreamingResponse: + """Vault & Secrets""" return AsyncCredentialsResourceWithStreamingResponse(self._packages.credentials) diff --git a/src/mobilerun_sdk/resources/devices/__init__.py b/src/mobilerun_sdk/resources/devices/__init__.py index e361ca9..ae8f789 100644 --- a/src/mobilerun_sdk/resources/devices/__init__.py +++ b/src/mobilerun_sdk/resources/devices/__init__.py @@ -106,78 +106,78 @@ ) __all__ = [ - "TimeResource", - "AsyncTimeResource", - "TimeResourceWithRawResponse", - "AsyncTimeResourceWithRawResponse", - "TimeResourceWithStreamingResponse", - "AsyncTimeResourceWithStreamingResponse", - "ProfileResource", - "AsyncProfileResource", - "ProfileResourceWithRawResponse", - "AsyncProfileResourceWithRawResponse", - "ProfileResourceWithStreamingResponse", - "AsyncProfileResourceWithStreamingResponse", - "FilesResource", - "AsyncFilesResource", - "FilesResourceWithRawResponse", - "AsyncFilesResourceWithRawResponse", - "FilesResourceWithStreamingResponse", - "AsyncFilesResourceWithStreamingResponse", - "ProxyResource", - "AsyncProxyResource", - "ProxyResourceWithRawResponse", - "AsyncProxyResourceWithRawResponse", - "ProxyResourceWithStreamingResponse", - "AsyncProxyResourceWithStreamingResponse", - "LocationResource", - "AsyncLocationResource", - "LocationResourceWithRawResponse", - "AsyncLocationResourceWithRawResponse", - "LocationResourceWithStreamingResponse", - "AsyncLocationResourceWithStreamingResponse", "ActionsResource", "AsyncActionsResource", "ActionsResourceWithRawResponse", "AsyncActionsResourceWithRawResponse", "ActionsResourceWithStreamingResponse", "AsyncActionsResourceWithStreamingResponse", - "StateResource", - "AsyncStateResource", - "StateResourceWithRawResponse", - "AsyncStateResourceWithRawResponse", - "StateResourceWithStreamingResponse", - "AsyncStateResourceWithStreamingResponse", "AppsResource", "AsyncAppsResource", "AppsResourceWithRawResponse", "AsyncAppsResourceWithRawResponse", "AppsResourceWithStreamingResponse", "AsyncAppsResourceWithStreamingResponse", - "PackagesResource", - "AsyncPackagesResource", - "PackagesResourceWithRawResponse", - "AsyncPackagesResourceWithRawResponse", - "PackagesResourceWithStreamingResponse", - "AsyncPackagesResourceWithStreamingResponse", + "EsimResource", + "AsyncEsimResource", + "EsimResourceWithRawResponse", + "AsyncEsimResourceWithRawResponse", + "EsimResourceWithStreamingResponse", + "AsyncEsimResourceWithStreamingResponse", + "FilesResource", + "AsyncFilesResource", + "FilesResourceWithRawResponse", + "AsyncFilesResourceWithRawResponse", + "FilesResourceWithStreamingResponse", + "AsyncFilesResourceWithStreamingResponse", "KeyboardResource", "AsyncKeyboardResource", "KeyboardResourceWithRawResponse", "AsyncKeyboardResourceWithRawResponse", "KeyboardResourceWithStreamingResponse", "AsyncKeyboardResourceWithStreamingResponse", + "LocationResource", + "AsyncLocationResource", + "LocationResourceWithRawResponse", + "AsyncLocationResourceWithRawResponse", + "LocationResourceWithStreamingResponse", + "AsyncLocationResourceWithStreamingResponse", + "PackagesResource", + "AsyncPackagesResource", + "PackagesResourceWithRawResponse", + "AsyncPackagesResourceWithRawResponse", + "PackagesResourceWithStreamingResponse", + "AsyncPackagesResourceWithStreamingResponse", + "ProfileResource", + "AsyncProfileResource", + "ProfileResourceWithRawResponse", + "AsyncProfileResourceWithRawResponse", + "ProfileResourceWithStreamingResponse", + "AsyncProfileResourceWithStreamingResponse", + "ProxyResource", + "AsyncProxyResource", + "ProxyResourceWithRawResponse", + "AsyncProxyResourceWithRawResponse", + "ProxyResourceWithStreamingResponse", + "AsyncProxyResourceWithStreamingResponse", + "StateResource", + "AsyncStateResource", + "StateResourceWithRawResponse", + "AsyncStateResourceWithRawResponse", + "StateResourceWithStreamingResponse", + "AsyncStateResourceWithStreamingResponse", "TasksResource", "AsyncTasksResource", "TasksResourceWithRawResponse", "AsyncTasksResourceWithRawResponse", "TasksResourceWithStreamingResponse", "AsyncTasksResourceWithStreamingResponse", - "EsimResource", - "AsyncEsimResource", - "EsimResourceWithRawResponse", - "AsyncEsimResourceWithRawResponse", - "EsimResourceWithStreamingResponse", - "AsyncEsimResourceWithStreamingResponse", + "TimeResource", + "AsyncTimeResource", + "TimeResourceWithRawResponse", + "AsyncTimeResourceWithRawResponse", + "TimeResourceWithStreamingResponse", + "AsyncTimeResourceWithStreamingResponse", "DevicesResource", "AsyncDevicesResource", "DevicesResourceWithRawResponse", diff --git a/src/mobilerun_sdk/resources/devices/devices.py b/src/mobilerun_sdk/resources/devices/devices.py index b6743a9..17b6bae 100644 --- a/src/mobilerun_sdk/resources/devices/devices.py +++ b/src/mobilerun_sdk/resources/devices/devices.py @@ -119,6 +119,7 @@ from ...types.device import Device from ...types.device_list_response import DeviceListResponse from ...types.device_count_response import DeviceCountResponse +from ...types.shared_params.location import Location from ...types.shared_params.device_carrier import DeviceCarrier from ...types.shared_params.device_identifiers import DeviceIdentifiers @@ -126,53 +127,56 @@ class DevicesResource(SyncAPIResource): + """Device Management""" + @cached_property - def time(self) -> TimeResource: - return TimeResource(self._client) + def actions(self) -> ActionsResource: + return ActionsResource(self._client) @cached_property - def profile(self) -> ProfileResource: - return ProfileResource(self._client) + def apps(self) -> AppsResource: + return AppsResource(self._client) + + @cached_property + def esim(self) -> EsimResource: + return EsimResource(self._client) @cached_property def files(self) -> FilesResource: return FilesResource(self._client) @cached_property - def proxy(self) -> ProxyResource: - return ProxyResource(self._client) + def keyboard(self) -> KeyboardResource: + return KeyboardResource(self._client) @cached_property def location(self) -> LocationResource: return LocationResource(self._client) @cached_property - def actions(self) -> ActionsResource: - return ActionsResource(self._client) - - @cached_property - def state(self) -> StateResource: - return StateResource(self._client) + def packages(self) -> PackagesResource: + return PackagesResource(self._client) @cached_property - def apps(self) -> AppsResource: - return AppsResource(self._client) + def profile(self) -> ProfileResource: + return ProfileResource(self._client) @cached_property - def packages(self) -> PackagesResource: - return PackagesResource(self._client) + def proxy(self) -> ProxyResource: + return ProxyResource(self._client) @cached_property - def keyboard(self) -> KeyboardResource: - return KeyboardResource(self._client) + def state(self) -> StateResource: + return StateResource(self._client) @cached_property def tasks(self) -> TasksResource: + """Device Management""" return TasksResource(self._client) @cached_property - def esim(self) -> EsimResource: - return EsimResource(self._client) + def time(self) -> TimeResource: + return TimeResource(self._client) @cached_property def with_raw_response(self) -> DevicesResourceWithRawResponse: @@ -196,16 +200,23 @@ def with_streaming_response(self) -> DevicesResourceWithStreamingResponse: def create( self, *, + query_country: str | Omit = omit, device_type: Literal[ "dedicated_physical_device", "dedicated_premium_device", "dedicated_emulated_device", "dedicated_ios_device" ] | Omit = omit, + profile_id: str | Omit = omit, + android_version: int | Omit = omit, apps: Optional[SequenceNotStr[str]] | Omit = omit, carrier: DeviceCarrier | Omit = omit, + body_country: str | Omit = omit, files: Optional[SequenceNotStr[str]] | Omit = omit, identifiers: DeviceIdentifiers | Omit = omit, + locale: str | Omit = omit, + location: Location | Omit = omit, name: str | Omit = omit, proxy: device_create_params.Proxy | Omit = omit, + timezone: 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, @@ -213,10 +224,16 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Device: - """ - Provision a new device + """Provision a new device Args: + query_country: ISO 3166-1 alpha-2 country code. + + If omitted the system picks the country with + the most availability. + + profile_id: Profile ID to use as device spec + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -229,12 +246,17 @@ def create( "/devices", body=maybe_transform( { + "android_version": android_version, "apps": apps, "carrier": carrier, + "body_country": body_country, "files": files, "identifiers": identifiers, + "locale": locale, + "location": location, "name": name, "proxy": proxy, + "timezone": timezone, }, device_create_params.DeviceCreateParams, ), @@ -243,7 +265,14 @@ def create( extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform({"device_type": device_type}, device_create_params.DeviceCreateParams), + query=maybe_transform( + { + "query_country": query_country, + "device_type": device_type, + "profile_id": profile_id, + }, + device_create_params.DeviceCreateParams, + ), ), cast_to=Device, ) @@ -479,53 +508,56 @@ def wait_ready( class AsyncDevicesResource(AsyncAPIResource): + """Device Management""" + @cached_property - def time(self) -> AsyncTimeResource: - return AsyncTimeResource(self._client) + def actions(self) -> AsyncActionsResource: + return AsyncActionsResource(self._client) @cached_property - def profile(self) -> AsyncProfileResource: - return AsyncProfileResource(self._client) + def apps(self) -> AsyncAppsResource: + return AsyncAppsResource(self._client) + + @cached_property + def esim(self) -> AsyncEsimResource: + return AsyncEsimResource(self._client) @cached_property def files(self) -> AsyncFilesResource: return AsyncFilesResource(self._client) @cached_property - def proxy(self) -> AsyncProxyResource: - return AsyncProxyResource(self._client) + def keyboard(self) -> AsyncKeyboardResource: + return AsyncKeyboardResource(self._client) @cached_property def location(self) -> AsyncLocationResource: return AsyncLocationResource(self._client) @cached_property - def actions(self) -> AsyncActionsResource: - return AsyncActionsResource(self._client) - - @cached_property - def state(self) -> AsyncStateResource: - return AsyncStateResource(self._client) + def packages(self) -> AsyncPackagesResource: + return AsyncPackagesResource(self._client) @cached_property - def apps(self) -> AsyncAppsResource: - return AsyncAppsResource(self._client) + def profile(self) -> AsyncProfileResource: + return AsyncProfileResource(self._client) @cached_property - def packages(self) -> AsyncPackagesResource: - return AsyncPackagesResource(self._client) + def proxy(self) -> AsyncProxyResource: + return AsyncProxyResource(self._client) @cached_property - def keyboard(self) -> AsyncKeyboardResource: - return AsyncKeyboardResource(self._client) + def state(self) -> AsyncStateResource: + return AsyncStateResource(self._client) @cached_property def tasks(self) -> AsyncTasksResource: + """Device Management""" return AsyncTasksResource(self._client) @cached_property - def esim(self) -> AsyncEsimResource: - return AsyncEsimResource(self._client) + def time(self) -> AsyncTimeResource: + return AsyncTimeResource(self._client) @cached_property def with_raw_response(self) -> AsyncDevicesResourceWithRawResponse: @@ -549,16 +581,23 @@ def with_streaming_response(self) -> AsyncDevicesResourceWithStreamingResponse: async def create( self, *, + query_country: str | Omit = omit, device_type: Literal[ "dedicated_physical_device", "dedicated_premium_device", "dedicated_emulated_device", "dedicated_ios_device" ] | Omit = omit, + profile_id: str | Omit = omit, + android_version: int | Omit = omit, apps: Optional[SequenceNotStr[str]] | Omit = omit, carrier: DeviceCarrier | Omit = omit, + body_country: str | Omit = omit, files: Optional[SequenceNotStr[str]] | Omit = omit, identifiers: DeviceIdentifiers | Omit = omit, + locale: str | Omit = omit, + location: Location | Omit = omit, name: str | Omit = omit, proxy: device_create_params.Proxy | Omit = omit, + timezone: 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, @@ -566,10 +605,16 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Device: - """ - Provision a new device + """Provision a new device Args: + query_country: ISO 3166-1 alpha-2 country code. + + If omitted the system picks the country with + the most availability. + + profile_id: Profile ID to use as device spec + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -582,12 +627,17 @@ async def create( "/devices", body=await async_maybe_transform( { + "android_version": android_version, "apps": apps, "carrier": carrier, + "body_country": body_country, "files": files, "identifiers": identifiers, + "locale": locale, + "location": location, "name": name, "proxy": proxy, + "timezone": timezone, }, device_create_params.DeviceCreateParams, ), @@ -597,7 +647,12 @@ async def create( extra_body=extra_body, timeout=timeout, query=await async_maybe_transform( - {"device_type": device_type}, device_create_params.DeviceCreateParams + { + "query_country": query_country, + "device_type": device_type, + "profile_id": profile_id, + }, + device_create_params.DeviceCreateParams, ), ), cast_to=Device, @@ -860,52 +915,53 @@ def __init__(self, devices: DevicesResource) -> None: ) @cached_property - def time(self) -> TimeResourceWithRawResponse: - return TimeResourceWithRawResponse(self._devices.time) + def actions(self) -> ActionsResourceWithRawResponse: + return ActionsResourceWithRawResponse(self._devices.actions) @cached_property - def profile(self) -> ProfileResourceWithRawResponse: - return ProfileResourceWithRawResponse(self._devices.profile) + def apps(self) -> AppsResourceWithRawResponse: + return AppsResourceWithRawResponse(self._devices.apps) + + @cached_property + def esim(self) -> EsimResourceWithRawResponse: + return EsimResourceWithRawResponse(self._devices.esim) @cached_property def files(self) -> FilesResourceWithRawResponse: return FilesResourceWithRawResponse(self._devices.files) @cached_property - def proxy(self) -> ProxyResourceWithRawResponse: - return ProxyResourceWithRawResponse(self._devices.proxy) + def keyboard(self) -> KeyboardResourceWithRawResponse: + return KeyboardResourceWithRawResponse(self._devices.keyboard) @cached_property def location(self) -> LocationResourceWithRawResponse: return LocationResourceWithRawResponse(self._devices.location) @cached_property - def actions(self) -> ActionsResourceWithRawResponse: - return ActionsResourceWithRawResponse(self._devices.actions) - - @cached_property - def state(self) -> StateResourceWithRawResponse: - return StateResourceWithRawResponse(self._devices.state) + def packages(self) -> PackagesResourceWithRawResponse: + return PackagesResourceWithRawResponse(self._devices.packages) @cached_property - def apps(self) -> AppsResourceWithRawResponse: - return AppsResourceWithRawResponse(self._devices.apps) + def profile(self) -> ProfileResourceWithRawResponse: + return ProfileResourceWithRawResponse(self._devices.profile) @cached_property - def packages(self) -> PackagesResourceWithRawResponse: - return PackagesResourceWithRawResponse(self._devices.packages) + def proxy(self) -> ProxyResourceWithRawResponse: + return ProxyResourceWithRawResponse(self._devices.proxy) @cached_property - def keyboard(self) -> KeyboardResourceWithRawResponse: - return KeyboardResourceWithRawResponse(self._devices.keyboard) + def state(self) -> StateResourceWithRawResponse: + return StateResourceWithRawResponse(self._devices.state) @cached_property def tasks(self) -> TasksResourceWithRawResponse: + """Device Management""" return TasksResourceWithRawResponse(self._devices.tasks) @cached_property - def esim(self) -> EsimResourceWithRawResponse: - return EsimResourceWithRawResponse(self._devices.esim) + def time(self) -> TimeResourceWithRawResponse: + return TimeResourceWithRawResponse(self._devices.time) class AsyncDevicesResourceWithRawResponse: @@ -935,52 +991,53 @@ def __init__(self, devices: AsyncDevicesResource) -> None: ) @cached_property - def time(self) -> AsyncTimeResourceWithRawResponse: - return AsyncTimeResourceWithRawResponse(self._devices.time) + def actions(self) -> AsyncActionsResourceWithRawResponse: + return AsyncActionsResourceWithRawResponse(self._devices.actions) @cached_property - def profile(self) -> AsyncProfileResourceWithRawResponse: - return AsyncProfileResourceWithRawResponse(self._devices.profile) + def apps(self) -> AsyncAppsResourceWithRawResponse: + return AsyncAppsResourceWithRawResponse(self._devices.apps) + + @cached_property + def esim(self) -> AsyncEsimResourceWithRawResponse: + return AsyncEsimResourceWithRawResponse(self._devices.esim) @cached_property def files(self) -> AsyncFilesResourceWithRawResponse: return AsyncFilesResourceWithRawResponse(self._devices.files) @cached_property - def proxy(self) -> AsyncProxyResourceWithRawResponse: - return AsyncProxyResourceWithRawResponse(self._devices.proxy) + def keyboard(self) -> AsyncKeyboardResourceWithRawResponse: + return AsyncKeyboardResourceWithRawResponse(self._devices.keyboard) @cached_property def location(self) -> AsyncLocationResourceWithRawResponse: return AsyncLocationResourceWithRawResponse(self._devices.location) @cached_property - def actions(self) -> AsyncActionsResourceWithRawResponse: - return AsyncActionsResourceWithRawResponse(self._devices.actions) - - @cached_property - def state(self) -> AsyncStateResourceWithRawResponse: - return AsyncStateResourceWithRawResponse(self._devices.state) + def packages(self) -> AsyncPackagesResourceWithRawResponse: + return AsyncPackagesResourceWithRawResponse(self._devices.packages) @cached_property - def apps(self) -> AsyncAppsResourceWithRawResponse: - return AsyncAppsResourceWithRawResponse(self._devices.apps) + def profile(self) -> AsyncProfileResourceWithRawResponse: + return AsyncProfileResourceWithRawResponse(self._devices.profile) @cached_property - def packages(self) -> AsyncPackagesResourceWithRawResponse: - return AsyncPackagesResourceWithRawResponse(self._devices.packages) + def proxy(self) -> AsyncProxyResourceWithRawResponse: + return AsyncProxyResourceWithRawResponse(self._devices.proxy) @cached_property - def keyboard(self) -> AsyncKeyboardResourceWithRawResponse: - return AsyncKeyboardResourceWithRawResponse(self._devices.keyboard) + def state(self) -> AsyncStateResourceWithRawResponse: + return AsyncStateResourceWithRawResponse(self._devices.state) @cached_property def tasks(self) -> AsyncTasksResourceWithRawResponse: + """Device Management""" return AsyncTasksResourceWithRawResponse(self._devices.tasks) @cached_property - def esim(self) -> AsyncEsimResourceWithRawResponse: - return AsyncEsimResourceWithRawResponse(self._devices.esim) + def time(self) -> AsyncTimeResourceWithRawResponse: + return AsyncTimeResourceWithRawResponse(self._devices.time) class DevicesResourceWithStreamingResponse: @@ -1010,52 +1067,53 @@ def __init__(self, devices: DevicesResource) -> None: ) @cached_property - def time(self) -> TimeResourceWithStreamingResponse: - return TimeResourceWithStreamingResponse(self._devices.time) + def actions(self) -> ActionsResourceWithStreamingResponse: + return ActionsResourceWithStreamingResponse(self._devices.actions) @cached_property - def profile(self) -> ProfileResourceWithStreamingResponse: - return ProfileResourceWithStreamingResponse(self._devices.profile) + def apps(self) -> AppsResourceWithStreamingResponse: + return AppsResourceWithStreamingResponse(self._devices.apps) + + @cached_property + def esim(self) -> EsimResourceWithStreamingResponse: + return EsimResourceWithStreamingResponse(self._devices.esim) @cached_property def files(self) -> FilesResourceWithStreamingResponse: return FilesResourceWithStreamingResponse(self._devices.files) @cached_property - def proxy(self) -> ProxyResourceWithStreamingResponse: - return ProxyResourceWithStreamingResponse(self._devices.proxy) + def keyboard(self) -> KeyboardResourceWithStreamingResponse: + return KeyboardResourceWithStreamingResponse(self._devices.keyboard) @cached_property def location(self) -> LocationResourceWithStreamingResponse: return LocationResourceWithStreamingResponse(self._devices.location) @cached_property - def actions(self) -> ActionsResourceWithStreamingResponse: - return ActionsResourceWithStreamingResponse(self._devices.actions) - - @cached_property - def state(self) -> StateResourceWithStreamingResponse: - return StateResourceWithStreamingResponse(self._devices.state) + def packages(self) -> PackagesResourceWithStreamingResponse: + return PackagesResourceWithStreamingResponse(self._devices.packages) @cached_property - def apps(self) -> AppsResourceWithStreamingResponse: - return AppsResourceWithStreamingResponse(self._devices.apps) + def profile(self) -> ProfileResourceWithStreamingResponse: + return ProfileResourceWithStreamingResponse(self._devices.profile) @cached_property - def packages(self) -> PackagesResourceWithStreamingResponse: - return PackagesResourceWithStreamingResponse(self._devices.packages) + def proxy(self) -> ProxyResourceWithStreamingResponse: + return ProxyResourceWithStreamingResponse(self._devices.proxy) @cached_property - def keyboard(self) -> KeyboardResourceWithStreamingResponse: - return KeyboardResourceWithStreamingResponse(self._devices.keyboard) + def state(self) -> StateResourceWithStreamingResponse: + return StateResourceWithStreamingResponse(self._devices.state) @cached_property def tasks(self) -> TasksResourceWithStreamingResponse: + """Device Management""" return TasksResourceWithStreamingResponse(self._devices.tasks) @cached_property - def esim(self) -> EsimResourceWithStreamingResponse: - return EsimResourceWithStreamingResponse(self._devices.esim) + def time(self) -> TimeResourceWithStreamingResponse: + return TimeResourceWithStreamingResponse(self._devices.time) class AsyncDevicesResourceWithStreamingResponse: @@ -1085,49 +1143,50 @@ def __init__(self, devices: AsyncDevicesResource) -> None: ) @cached_property - def time(self) -> AsyncTimeResourceWithStreamingResponse: - return AsyncTimeResourceWithStreamingResponse(self._devices.time) + def actions(self) -> AsyncActionsResourceWithStreamingResponse: + return AsyncActionsResourceWithStreamingResponse(self._devices.actions) @cached_property - def profile(self) -> AsyncProfileResourceWithStreamingResponse: - return AsyncProfileResourceWithStreamingResponse(self._devices.profile) + def apps(self) -> AsyncAppsResourceWithStreamingResponse: + return AsyncAppsResourceWithStreamingResponse(self._devices.apps) + + @cached_property + def esim(self) -> AsyncEsimResourceWithStreamingResponse: + return AsyncEsimResourceWithStreamingResponse(self._devices.esim) @cached_property def files(self) -> AsyncFilesResourceWithStreamingResponse: return AsyncFilesResourceWithStreamingResponse(self._devices.files) @cached_property - def proxy(self) -> AsyncProxyResourceWithStreamingResponse: - return AsyncProxyResourceWithStreamingResponse(self._devices.proxy) + def keyboard(self) -> AsyncKeyboardResourceWithStreamingResponse: + return AsyncKeyboardResourceWithStreamingResponse(self._devices.keyboard) @cached_property def location(self) -> AsyncLocationResourceWithStreamingResponse: return AsyncLocationResourceWithStreamingResponse(self._devices.location) @cached_property - def actions(self) -> AsyncActionsResourceWithStreamingResponse: - return AsyncActionsResourceWithStreamingResponse(self._devices.actions) - - @cached_property - def state(self) -> AsyncStateResourceWithStreamingResponse: - return AsyncStateResourceWithStreamingResponse(self._devices.state) + def packages(self) -> AsyncPackagesResourceWithStreamingResponse: + return AsyncPackagesResourceWithStreamingResponse(self._devices.packages) @cached_property - def apps(self) -> AsyncAppsResourceWithStreamingResponse: - return AsyncAppsResourceWithStreamingResponse(self._devices.apps) + def profile(self) -> AsyncProfileResourceWithStreamingResponse: + return AsyncProfileResourceWithStreamingResponse(self._devices.profile) @cached_property - def packages(self) -> AsyncPackagesResourceWithStreamingResponse: - return AsyncPackagesResourceWithStreamingResponse(self._devices.packages) + def proxy(self) -> AsyncProxyResourceWithStreamingResponse: + return AsyncProxyResourceWithStreamingResponse(self._devices.proxy) @cached_property - def keyboard(self) -> AsyncKeyboardResourceWithStreamingResponse: - return AsyncKeyboardResourceWithStreamingResponse(self._devices.keyboard) + def state(self) -> AsyncStateResourceWithStreamingResponse: + return AsyncStateResourceWithStreamingResponse(self._devices.state) @cached_property def tasks(self) -> AsyncTasksResourceWithStreamingResponse: + """Device Management""" return AsyncTasksResourceWithStreamingResponse(self._devices.tasks) @cached_property - def esim(self) -> AsyncEsimResourceWithStreamingResponse: - return AsyncEsimResourceWithStreamingResponse(self._devices.esim) + def time(self) -> AsyncTimeResourceWithStreamingResponse: + return AsyncTimeResourceWithStreamingResponse(self._devices.time) diff --git a/src/mobilerun_sdk/resources/devices/files.py b/src/mobilerun_sdk/resources/devices/files.py index 0b20330..ffaba39 100644 --- a/src/mobilerun_sdk/resources/devices/files.py +++ b/src/mobilerun_sdk/resources/devices/files.py @@ -6,16 +6,9 @@ import httpx +from ..._files import deepcopy_with_paths from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given -from ..._utils import ( - is_given, - extract_files, - path_template, - maybe_transform, - strip_not_given, - deepcopy_minimal, - async_maybe_transform, -) +from ..._utils import is_given, extract_files, path_template, maybe_transform, strip_not_given, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -222,7 +215,7 @@ def upload( ), **(extra_headers or {}), } - body = deepcopy_minimal({"file": file}) + body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -434,7 +427,7 @@ async def upload( ), **(extra_headers or {}), } - body = deepcopy_minimal({"file": file}) + body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. diff --git a/src/mobilerun_sdk/resources/devices/keyboard.py b/src/mobilerun_sdk/resources/devices/keyboard.py index 66c14c6..97e2695 100644 --- a/src/mobilerun_sdk/resources/devices/keyboard.py +++ b/src/mobilerun_sdk/resources/devices/keyboard.py @@ -130,6 +130,7 @@ def write( *, text: str, clear: bool | Omit = omit, + error_rate: float | Omit = omit, stealth: bool | Omit = omit, wpm: int | Omit = omit, x_device_display_id: int | Omit = omit, @@ -143,9 +144,11 @@ def write( """Input text Args: - wpm: Words per minute for stealth typing. + error_rate: Per-character mistake rate for humantouch typing. - 0 uses portal default. + -1 uses server default. + + wpm: Words per minute for stealth typing. 0 uses portal default. extra_headers: Send extra headers @@ -170,6 +173,7 @@ def write( { "text": text, "clear": clear, + "error_rate": error_rate, "stealth": stealth, "wpm": wpm, }, @@ -292,6 +296,7 @@ async def write( *, text: str, clear: bool | Omit = omit, + error_rate: float | Omit = omit, stealth: bool | Omit = omit, wpm: int | Omit = omit, x_device_display_id: int | Omit = omit, @@ -305,9 +310,11 @@ async def write( """Input text Args: - wpm: Words per minute for stealth typing. + error_rate: Per-character mistake rate for humantouch typing. + + -1 uses server default. - 0 uses portal default. + wpm: Words per minute for stealth typing. 0 uses portal default. extra_headers: Send extra headers @@ -332,6 +339,7 @@ async def write( { "text": text, "clear": clear, + "error_rate": error_rate, "stealth": stealth, "wpm": wpm, }, diff --git a/src/mobilerun_sdk/resources/devices/location.py b/src/mobilerun_sdk/resources/devices/location.py index 799ac2c..f9469fc 100644 --- a/src/mobilerun_sdk/resources/devices/location.py +++ b/src/mobilerun_sdk/resources/devices/location.py @@ -16,7 +16,7 @@ ) from ..._base_client import make_request_options from ...types.devices import location_set_params -from ...types.devices.location_get_response import LocationGetResponse +from ...types.shared.location import Location __all__ = ["LocationResource", "AsyncLocationResource"] @@ -52,7 +52,7 @@ def get( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> LocationGetResponse: + ) -> Location: """ Get device location @@ -78,7 +78,7 @@ def get( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=LocationGetResponse, + cast_to=Location, ) def set( @@ -163,7 +163,7 @@ async def get( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> LocationGetResponse: + ) -> Location: """ Get device location @@ -189,7 +189,7 @@ async def get( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=LocationGetResponse, + cast_to=Location, ) async def set( diff --git a/src/mobilerun_sdk/resources/devices/proxy.py b/src/mobilerun_sdk/resources/devices/proxy.py index 5cf46cb..08e0c04 100644 --- a/src/mobilerun_sdk/resources/devices/proxy.py +++ b/src/mobilerun_sdk/resources/devices/proxy.py @@ -52,7 +52,6 @@ def connect( smart_ip: bool | Omit = omit, socks5: proxy_connect_params.Socks5 | Omit = omit, user: str | Omit = omit, - wireguard: str | Omit = omit, x_device_display_id: int | 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. @@ -65,12 +64,10 @@ def connect( Connect proxy Args: - name: Proxy name (used for wireguard tunnel name) + name: Proxy name socks5: SOCKS5 proxy configuration (required for socks5). - wireguard: WireGuard tunnel configuration file content (required for wireguard). - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -99,7 +96,6 @@ def connect( "smart_ip": smart_ip, "socks5": socks5, "user": user, - "wireguard": wireguard, }, proxy_connect_params.ProxyConnectParams, ), @@ -222,7 +218,6 @@ async def connect( smart_ip: bool | Omit = omit, socks5: proxy_connect_params.Socks5 | Omit = omit, user: str | Omit = omit, - wireguard: str | Omit = omit, x_device_display_id: int | 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. @@ -235,12 +230,10 @@ async def connect( Connect proxy Args: - name: Proxy name (used for wireguard tunnel name) + name: Proxy name socks5: SOCKS5 proxy configuration (required for socks5). - wireguard: WireGuard tunnel configuration file content (required for wireguard). - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -269,7 +262,6 @@ async def connect( "smart_ip": smart_ip, "socks5": socks5, "user": user, - "wireguard": wireguard, }, proxy_connect_params.ProxyConnectParams, ), diff --git a/src/mobilerun_sdk/resources/devices/tasks.py b/src/mobilerun_sdk/resources/devices/tasks.py index 62c8d6f..061d2ca 100644 --- a/src/mobilerun_sdk/resources/devices/tasks.py +++ b/src/mobilerun_sdk/resources/devices/tasks.py @@ -24,6 +24,8 @@ class TasksResource(SyncAPIResource): + """Device Management""" + @cached_property def with_raw_response(self) -> TasksResourceWithRawResponse: """ @@ -94,6 +96,8 @@ def list( class AsyncTasksResource(AsyncAPIResource): + """Device Management""" + @cached_property def with_raw_response(self) -> AsyncTasksResourceWithRawResponse: """ diff --git a/src/mobilerun_sdk/resources/hooks.py b/src/mobilerun_sdk/resources/hooks.py index c86af7d..185b05e 100644 --- a/src/mobilerun_sdk/resources/hooks.py +++ b/src/mobilerun_sdk/resources/hooks.py @@ -31,8 +31,6 @@ class HooksResource(SyncAPIResource): - """Webhooks API""" - @cached_property def with_raw_response(self) -> HooksResourceWithRawResponse: """ @@ -316,8 +314,6 @@ def unsubscribe( class AsyncHooksResource(AsyncAPIResource): - """Webhooks API""" - @cached_property def with_raw_response(self) -> AsyncHooksResourceWithRawResponse: """ diff --git a/src/mobilerun_sdk/resources/models.py b/src/mobilerun_sdk/resources/models.py index d1edbdd..4bb2764 100644 --- a/src/mobilerun_sdk/resources/models.py +++ b/src/mobilerun_sdk/resources/models.py @@ -20,6 +20,8 @@ class ModelsResource(SyncAPIResource): + """LLM Models""" + @cached_property def with_raw_response(self) -> ModelsResourceWithRawResponse: """ @@ -60,6 +62,8 @@ def list( class AsyncModelsResource(AsyncAPIResource): + """LLM Models""" + @cached_property def with_raw_response(self) -> AsyncModelsResourceWithRawResponse: """ diff --git a/src/mobilerun_sdk/resources/proxies.py b/src/mobilerun_sdk/resources/proxies.py index b3cf10c..cd4235f 100644 --- a/src/mobilerun_sdk/resources/proxies.py +++ b/src/mobilerun_sdk/resources/proxies.py @@ -28,6 +28,8 @@ class ProxiesResource(SyncAPIResource): + """Network Proxies""" + @cached_property def with_raw_response(self) -> ProxiesResourceWithRawResponse: """ @@ -349,6 +351,8 @@ def delete( class AsyncProxiesResource(AsyncAPIResource): + """Network Proxies""" + @cached_property def with_raw_response(self) -> AsyncProxiesResourceWithRawResponse: """ diff --git a/src/mobilerun_sdk/types/__init__.py b/src/mobilerun_sdk/types/__init__.py index 6a946aa..dff397e 100644 --- a/src/mobilerun_sdk/types/__init__.py +++ b/src/mobilerun_sdk/types/__init__.py @@ -6,6 +6,8 @@ from .device import Device as Device from .shared import ( Meta as Meta, + Socks5 as Socks5, + Location as Location, DeviceSpec as DeviceSpec, Pagination as Pagination, DeviceCarrier as DeviceCarrier, @@ -13,7 +15,6 @@ PaginationMeta as PaginationMeta, DeviceIdentifiers as DeviceIdentifiers, ) -from .carrier import Carrier as Carrier from .profile import Profile as Profile from .task_status import TaskStatus as TaskStatus from .proxy_config import ProxyConfig as ProxyConfig @@ -31,6 +32,7 @@ from .task_list_response import TaskListResponse as TaskListResponse from .task_stop_response import TaskStopResponse as TaskStopResponse from .agent_list_response import AgentListResponse as AgentListResponse +from .app_delete_response import AppDeleteResponse as AppDeleteResponse from .carrier_list_params import CarrierListParams as CarrierListParams from .hook_perform_params import HookPerformParams as HookPerformParams from .model_list_response import ModelListResponse as ModelListResponse @@ -42,6 +44,7 @@ from .device_create_params import DeviceCreateParams as DeviceCreateParams from .device_list_response import DeviceListResponse as DeviceListResponse from .hook_update_response import HookUpdateResponse as HookUpdateResponse +from .app_retrieve_response import AppRetrieveResponse as AppRetrieveResponse from .carrier_create_params import CarrierCreateParams as CarrierCreateParams from .carrier_list_response import CarrierListResponse as CarrierListResponse from .carrier_lookup_params import CarrierLookupParams as CarrierLookupParams @@ -59,17 +62,25 @@ from .device_set_name_params import DeviceSetNameParams as DeviceSetNameParams from .hook_retrieve_response import HookRetrieveResponse as HookRetrieveResponse from .task_retrieve_response import TaskRetrieveResponse as TaskRetrieveResponse +from .carrier_create_response import CarrierCreateResponse as CarrierCreateResponse from .carrier_delete_response import CarrierDeleteResponse as CarrierDeleteResponse +from .carrier_lookup_response import CarrierLookupResponse as CarrierLookupResponse +from .carrier_update_response import CarrierUpdateResponse as CarrierUpdateResponse from .device_terminate_params import DeviceTerminateParams as DeviceTerminateParams from .hook_subscribe_response import HookSubscribeResponse as HookSubscribeResponse from .profile_delete_response import ProfileDeleteResponse as ProfileDeleteResponse from .proxy_retrieve_response import ProxyRetrieveResponse as ProxyRetrieveResponse +from .app_mark_failed_response import AppMarkFailedResponse as AppMarkFailedResponse from .credential_list_response import CredentialListResponse as CredentialListResponse 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 +from .carrier_retrieve_response import CarrierRetrieveResponse as CarrierRetrieveResponse from .hook_unsubscribe_response import HookUnsubscribeResponse as HookUnsubscribeResponse from .package_credentials_param import PackageCredentialsParam as PackageCredentialsParam from .task_send_message_response import TaskSendMessageResponse as TaskSendMessageResponse +from .app_confirm_upload_response import AppConfirmUploadResponse as AppConfirmUploadResponse from .task_get_trajectory_response import TaskGetTrajectoryResponse as TaskGetTrajectoryResponse from .hook_get_sample_data_response import HookGetSampleDataResponse as HookGetSampleDataResponse +from .app_create_signed_upload_url_params import AppCreateSignedUploadURLParams as AppCreateSignedUploadURLParams +from .app_create_signed_upload_url_response import AppCreateSignedUploadURLResponse as AppCreateSignedUploadURLResponse diff --git a/src/mobilerun_sdk/types/app_confirm_upload_response.py b/src/mobilerun_sdk/types/app_confirm_upload_response.py new file mode 100644 index 0000000..da579dc --- /dev/null +++ b/src/mobilerun_sdk/types/app_confirm_upload_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["AppConfirmUploadResponse"] + + +class AppConfirmUploadResponse(BaseModel): + message: str + + success: Literal[True] diff --git a/src/mobilerun_sdk/types/app_create_signed_upload_url_params.py b/src/mobilerun_sdk/types/app_create_signed_upload_url_params.py new file mode 100644 index 0000000..a374c4b --- /dev/null +++ b/src/mobilerun_sdk/types/app_create_signed_upload_url_params.py @@ -0,0 +1,43 @@ +# 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 Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["AppCreateSignedUploadURLParams", "File"] + + +class AppCreateSignedUploadURLParams(TypedDict, total=False): + bundle_id: Required[Annotated[str, PropertyInfo(alias="bundleId")]] + + display_name: Required[Annotated[str, PropertyInfo(alias="displayName")]] + + files: Required[Iterable[File]] + + size_bytes: Required[Annotated[float, PropertyInfo(alias="sizeBytes")]] + + version_code: Required[Annotated[float, PropertyInfo(alias="versionCode")]] + + version_name: Required[Annotated[str, PropertyInfo(alias="versionName")]] + + country: str + """Country code for Search Results""" + + description: str + + developer_name: Annotated[str, PropertyInfo(alias="developerName")] + + icon_url: Annotated[str, PropertyInfo(alias="iconURL")] + + platform: Literal["android", "ios"] + + target_sdk: Annotated[float, PropertyInfo(alias="targetSdk")] + + +class File(TypedDict, total=False): + content_type: Required[Annotated[str, PropertyInfo(alias="contentType")]] + + file_name: Required[Annotated[str, PropertyInfo(alias="fileName")]] diff --git a/src/mobilerun_sdk/types/app_create_signed_upload_url_response.py b/src/mobilerun_sdk/types/app_create_signed_upload_url_response.py new file mode 100644 index 0000000..8d15581 --- /dev/null +++ b/src/mobilerun_sdk/types/app_create_signed_upload_url_response.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["AppCreateSignedUploadURLResponse", "R2UploadURL"] + + +class R2UploadURL(BaseModel): + file_name: str = FieldInfo(alias="fileName") + + r2_upload_url: str = FieldInfo(alias="r2UploadUrl") + + +class AppCreateSignedUploadURLResponse(BaseModel): + app_id: str = FieldInfo(alias="appId") + """App ID in the database""" + + r2_upload_urls: List[R2UploadURL] = FieldInfo(alias="r2UploadUrls") + """Pre-signed Cloudflare R2 URLs for uploading app files""" + + version_id: str = FieldInfo(alias="versionId") + """App version ID in the database""" diff --git a/src/mobilerun_sdk/types/app_delete_response.py b/src/mobilerun_sdk/types/app_delete_response.py new file mode 100644 index 0000000..5dce5fa --- /dev/null +++ b/src/mobilerun_sdk/types/app_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["AppDeleteResponse"] + + +class AppDeleteResponse(BaseModel): + message: str + + success: Literal[True] diff --git a/src/mobilerun_sdk/types/app_list_params.py b/src/mobilerun_sdk/types/app_list_params.py index 01fe905..319c937 100644 --- a/src/mobilerun_sdk/types/app_list_params.py +++ b/src/mobilerun_sdk/types/app_list_params.py @@ -16,8 +16,10 @@ class AppListParams(TypedDict, total=False): page_size: Annotated[int, PropertyInfo(alias="pageSize")] + platform: Literal["all", "android", "ios"] + query: str sort_by: Annotated[Literal["createdAt", "name"], PropertyInfo(alias="sortBy")] - source: Literal["all", "uploaded", "store", "queued"] + status: Literal["all", "queued", "available", "failed"] diff --git a/src/mobilerun_sdk/types/app_list_response.py b/src/mobilerun_sdk/types/app_list_response.py index e211771..f7e2394 100644 --- a/src/mobilerun_sdk/types/app_list_response.py +++ b/src/mobilerun_sdk/types/app_list_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Union, Optional +from typing import List, Optional from datetime import datetime from typing_extensions import Literal @@ -9,25 +9,23 @@ from .._models import BaseModel from .shared.pagination import Pagination -__all__ = ["AppListResponse", "Count", "Item"] +__all__ = ["AppListResponse", "Count", "Item", "ItemVersion"] class Count(BaseModel): available_count: float = FieldInfo(alias="availableCount") - queued_count: float = FieldInfo(alias="queuedCount") + failed_count: float = FieldInfo(alias="failedCount") - store_count: float = FieldInfo(alias="storeCount") + queued_count: float = FieldInfo(alias="queuedCount") total_count: float = FieldInfo(alias="totalCount") - upload_count: float = FieldInfo(alias="uploadCount") - -class Item(BaseModel): +class ItemVersion(BaseModel): id: str - category_name: Optional[str] = FieldInfo(alias="categoryName", default=None) + app_id: str = FieldInfo(alias="appId") country: Literal[ "AF", @@ -286,47 +284,45 @@ class Item(BaseModel): created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) - description: Optional[str] = None + queued_at: Optional[datetime] = FieldInfo(alias="queuedAt", default=None) - developer_name: Optional[str] = FieldInfo(alias="developerName", default=None) + size_bytes: Optional[int] = FieldInfo(alias="sizeBytes", default=None) - display_name: str = FieldInfo(alias="displayName") + source: Literal["user", "system", "portal"] - expected_files: Union[str, float, bool, Dict[str, Optional[object]], List[Optional[object]], None] = FieldInfo( - alias="expectedFiles", default=None - ) + status: Literal["queued", "available", "failed"] - icon_url: str = FieldInfo(alias="iconURL") + target_sdk: Optional[int] = FieldInfo(alias="targetSdk", default=None) - package_name: str = FieldInfo(alias="packageName") + updated_at: Optional[datetime] = FieldInfo(alias="updatedAt", default=None) - privacy_policy_url: Optional[str] = FieldInfo(alias="privacyPolicyUrl", default=None) + user_id: Optional[str] = FieldInfo(alias="userId", default=None) - queued_at: Optional[datetime] = FieldInfo(alias="queuedAt", default=None) + version_code: int = FieldInfo(alias="versionCode") - rating_count: Optional[int] = FieldInfo(alias="ratingCount", default=None) + version_name: str = FieldInfo(alias="versionName") - rating_score: Optional[str] = FieldInfo(alias="ratingScore", default=None) - size_bytes: Optional[int] = FieldInfo(alias="sizeBytes", default=None) +class Item(BaseModel): + id: str - source: Literal["uploaded", "store"] + bundle_id: str = FieldInfo(alias="bundleId") - status: Literal["queued", "available", "failed"] + created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) - stealth_tier: Optional[Literal["tier1", "tier2", "tier3"]] = FieldInfo(alias="stealthTier", default=None) + description: Optional[str] = None - target_sdk: Optional[int] = FieldInfo(alias="targetSdk", default=None) + developer_name: Optional[str] = FieldInfo(alias="developerName", default=None) - type: Literal["android", "ios"] + display_name: str = FieldInfo(alias="displayName") - updated_at: Optional[datetime] = FieldInfo(alias="updatedAt", default=None) + icon_url: str = FieldInfo(alias="iconURL") - user_id: Optional[str] = FieldInfo(alias="userId", default=None) + platform: Literal["android", "ios"] - version_code: int = FieldInfo(alias="versionCode") + updated_at: Optional[datetime] = FieldInfo(alias="updatedAt", default=None) - version_name: str = FieldInfo(alias="versionName") + version: ItemVersion class AppListResponse(BaseModel): diff --git a/src/mobilerun_sdk/types/app_mark_failed_response.py b/src/mobilerun_sdk/types/app_mark_failed_response.py new file mode 100644 index 0000000..4df598b --- /dev/null +++ b/src/mobilerun_sdk/types/app_mark_failed_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["AppMarkFailedResponse"] + + +class AppMarkFailedResponse(BaseModel): + message: str + + success: Literal[True] diff --git a/src/mobilerun_sdk/types/app_retrieve_response.py b/src/mobilerun_sdk/types/app_retrieve_response.py new file mode 100644 index 0000000..57fd551 --- /dev/null +++ b/src/mobilerun_sdk/types/app_retrieve_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["AppRetrieveResponse", "Data"] + + +class Data(BaseModel): + id: str + + bundle_id: str = FieldInfo(alias="bundleId") + + created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) + + description: Optional[str] = None + + developer_name: Optional[str] = FieldInfo(alias="developerName", default=None) + + display_name: str = FieldInfo(alias="displayName") + + icon_url: str = FieldInfo(alias="iconURL") + + platform: Literal["android", "ios"] + + updated_at: Optional[datetime] = FieldInfo(alias="updatedAt", default=None) + + +class AppRetrieveResponse(BaseModel): + data: Data diff --git a/src/mobilerun_sdk/types/carrier.py b/src/mobilerun_sdk/types/carrier_create_response.py similarity index 90% rename from src/mobilerun_sdk/types/carrier.py rename to src/mobilerun_sdk/types/carrier_create_response.py index 7289eec..643be23 100644 --- a/src/mobilerun_sdk/types/carrier.py +++ b/src/mobilerun_sdk/types/carrier_create_response.py @@ -7,10 +7,10 @@ from .._models import BaseModel -__all__ = ["Carrier"] +__all__ = ["CarrierCreateResponse"] -class Carrier(BaseModel): +class CarrierCreateResponse(BaseModel): id: int company: str diff --git a/src/mobilerun_sdk/types/carrier_list_response.py b/src/mobilerun_sdk/types/carrier_list_response.py index 8d6b97b..098754a 100644 --- a/src/mobilerun_sdk/types/carrier_list_response.py +++ b/src/mobilerun_sdk/types/carrier_list_response.py @@ -1,18 +1,59 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional +from datetime import datetime from pydantic import Field as FieldInfo -from .carrier import Carrier from .._models import BaseModel from .shared.meta import Meta -__all__ = ["CarrierListResponse"] +__all__ = ["CarrierListResponse", "Item"] + + +class Item(BaseModel): + id: int + + company: str + + country: str + + country_code: str + + country_iso: str + + created_at: datetime + + detail_url: str + + gsm_bands: str + + lte_bands: str + + mcc: str + + mnc: str + + mobile_prefix: str + + nsn_size: str + + number_format: str + + operator: str + + protocols: str + + umts_bands: str + + website: str + + schema_: Optional[str] = FieldInfo(alias="$schema", default=None) + """A URL to the JSON Schema for this object.""" class CarrierListResponse(BaseModel): - items: Optional[List[Carrier]] = None + items: Optional[List[Item]] = None pagination: Meta diff --git a/src/mobilerun_sdk/types/carrier_lookup_response.py b/src/mobilerun_sdk/types/carrier_lookup_response.py new file mode 100644 index 0000000..925e0d6 --- /dev/null +++ b/src/mobilerun_sdk/types/carrier_lookup_response.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["CarrierLookupResponse"] + + +class CarrierLookupResponse(BaseModel): + id: int + + company: str + + country: str + + country_code: str + + country_iso: str + + created_at: datetime + + detail_url: str + + gsm_bands: str + + lte_bands: str + + mcc: str + + mnc: str + + mobile_prefix: str + + nsn_size: str + + number_format: str + + operator: str + + protocols: str + + umts_bands: str + + website: str + + schema_: Optional[str] = FieldInfo(alias="$schema", default=None) + """A URL to the JSON Schema for this object.""" diff --git a/src/mobilerun_sdk/types/carrier_retrieve_response.py b/src/mobilerun_sdk/types/carrier_retrieve_response.py new file mode 100644 index 0000000..f4bf58c --- /dev/null +++ b/src/mobilerun_sdk/types/carrier_retrieve_response.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["CarrierRetrieveResponse"] + + +class CarrierRetrieveResponse(BaseModel): + id: int + + company: str + + country: str + + country_code: str + + country_iso: str + + created_at: datetime + + detail_url: str + + gsm_bands: str + + lte_bands: str + + mcc: str + + mnc: str + + mobile_prefix: str + + nsn_size: str + + number_format: str + + operator: str + + protocols: str + + umts_bands: str + + website: str + + schema_: Optional[str] = FieldInfo(alias="$schema", default=None) + """A URL to the JSON Schema for this object.""" diff --git a/src/mobilerun_sdk/types/carrier_update_response.py b/src/mobilerun_sdk/types/carrier_update_response.py new file mode 100644 index 0000000..ba0d7ef --- /dev/null +++ b/src/mobilerun_sdk/types/carrier_update_response.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["CarrierUpdateResponse"] + + +class CarrierUpdateResponse(BaseModel): + id: int + + company: str + + country: str + + country_code: str + + country_iso: str + + created_at: datetime + + detail_url: str + + gsm_bands: str + + lte_bands: str + + mcc: str + + mnc: str + + mobile_prefix: str + + nsn_size: str + + number_format: str + + operator: str + + protocols: str + + umts_bands: str + + website: str + + schema_: Optional[str] = FieldInfo(alias="$schema", default=None) + """A URL to the JSON Schema for this object.""" diff --git a/src/mobilerun_sdk/types/device_create_params.py b/src/mobilerun_sdk/types/device_create_params.py index 751f16e..0722b7a 100644 --- a/src/mobilerun_sdk/types/device_create_params.py +++ b/src/mobilerun_sdk/types/device_create_params.py @@ -3,17 +3,25 @@ from __future__ import annotations from typing import Optional -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Literal, Annotated, TypedDict from .._types import SequenceNotStr from .._utils import PropertyInfo +from .shared_params.socks5 import Socks5 +from .shared_params.location import Location from .shared_params.device_carrier import DeviceCarrier from .shared_params.device_identifiers import DeviceIdentifiers -__all__ = ["DeviceCreateParams", "Proxy", "ProxySocks5"] +__all__ = ["DeviceCreateParams", "Proxy"] class DeviceCreateParams(TypedDict, total=False): + query_country: Annotated[str, PropertyInfo(alias="country")] + """ISO 3166-1 alpha-2 country code. + + If omitted the system picks the country with the most availability. + """ + device_type: Annotated[ Literal[ "dedicated_physical_device", "dedicated_premium_device", "dedicated_emulated_device", "dedicated_ios_device" @@ -21,27 +29,30 @@ class DeviceCreateParams(TypedDict, total=False): PropertyInfo(alias="deviceType"), ] + profile_id: Annotated[str, PropertyInfo(alias="profileId")] + """Profile ID to use as device spec""" + + android_version: Annotated[int, PropertyInfo(alias="androidVersion")] + apps: Optional[SequenceNotStr[str]] carrier: DeviceCarrier + body_country: Annotated[str, PropertyInfo(alias="country")] + files: Optional[SequenceNotStr[str]] identifiers: DeviceIdentifiers - name: str - - proxy: Proxy - + locale: str -class ProxySocks5(TypedDict, total=False): - host: Required[str] + location: Location - password: Required[str] + name: str - port: Required[int] + proxy: Proxy - user: Required[str] + timezone: str class Proxy(TypedDict, total=False): @@ -49,6 +60,4 @@ class Proxy(TypedDict, total=False): smart_ip: Annotated[bool, PropertyInfo(alias="smartIp")] - socks5: ProxySocks5 - - wireguard: str + socks5: Socks5 diff --git a/src/mobilerun_sdk/types/devices/__init__.py b/src/mobilerun_sdk/types/devices/__init__.py index c5f89e4..b9ba498 100644 --- a/src/mobilerun_sdk/types/devices/__init__.py +++ b/src/mobilerun_sdk/types/devices/__init__.py @@ -30,7 +30,6 @@ from .file_download_params import FileDownloadParams as FileDownloadParams from .proxy_connect_params import ProxyConnectParams as ProxyConnectParams from .keyboard_write_params import KeyboardWriteParams as KeyboardWriteParams -from .location_get_response import LocationGetResponse as LocationGetResponse from .package_list_response import PackageListResponse as PackageListResponse from .profile_update_params import ProfileUpdateParams as ProfileUpdateParams from .proxy_status_response import ProxyStatusResponse as ProxyStatusResponse diff --git a/src/mobilerun_sdk/types/devices/keyboard_write_params.py b/src/mobilerun_sdk/types/devices/keyboard_write_params.py index f0fa72f..c1340d7 100644 --- a/src/mobilerun_sdk/types/devices/keyboard_write_params.py +++ b/src/mobilerun_sdk/types/devices/keyboard_write_params.py @@ -14,6 +14,9 @@ class KeyboardWriteParams(TypedDict, total=False): clear: bool + error_rate: Annotated[float, PropertyInfo(alias="errorRate")] + """Per-character mistake rate for humantouch typing. -1 uses server default.""" + stealth: bool wpm: int diff --git a/src/mobilerun_sdk/types/devices/proxy_connect_params.py b/src/mobilerun_sdk/types/devices/proxy_connect_params.py index d177e00..5afc504 100644 --- a/src/mobilerun_sdk/types/devices/proxy_connect_params.py +++ b/src/mobilerun_sdk/types/devices/proxy_connect_params.py @@ -13,7 +13,7 @@ class ProxyConnectParams(TypedDict, total=False): host: str name: str - """Proxy name (used for wireguard tunnel name)""" + """Proxy name""" password: str @@ -26,9 +26,6 @@ class ProxyConnectParams(TypedDict, total=False): user: str - wireguard: str - """WireGuard tunnel configuration file content (required for wireguard).""" - x_device_display_id: Annotated[int, PropertyInfo(alias="X-Device-Display-ID")] diff --git a/src/mobilerun_sdk/types/devices/proxy_status_response.py b/src/mobilerun_sdk/types/devices/proxy_status_response.py index b457c9d..668f612 100644 --- a/src/mobilerun_sdk/types/devices/proxy_status_response.py +++ b/src/mobilerun_sdk/types/devices/proxy_status_response.py @@ -16,7 +16,7 @@ class ProxyStatusResponse(BaseModel): """Active proxy name""" protocol: Optional[str] = None - """Active proxy protocol (socks5 or wireguard).""" + """Active proxy protocol (socks5).""" schema_: Optional[str] = FieldInfo(alias="$schema", default=None) """A URL to the JSON Schema for this object.""" diff --git a/src/mobilerun_sdk/types/devices/state_ui_response.py b/src/mobilerun_sdk/types/devices/state_ui_response.py index 7a695f3..bdddd0c 100644 --- a/src/mobilerun_sdk/types/devices/state_ui_response.py +++ b/src/mobilerun_sdk/types/devices/state_ui_response.py @@ -76,3 +76,5 @@ class StateUiResponse(BaseModel): schema_: Optional[str] = FieldInfo(alias="$schema", default=None) """A URL to the JSON Schema for this object.""" + + ime_tree: Optional[object] = None diff --git a/src/mobilerun_sdk/types/shared/__init__.py b/src/mobilerun_sdk/types/shared/__init__.py index 1c5496b..7d0f116 100644 --- a/src/mobilerun_sdk/types/shared/__init__.py +++ b/src/mobilerun_sdk/types/shared/__init__.py @@ -1,6 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from .meta import Meta as Meta +from .socks5 import Socks5 as Socks5 +from .location import Location as Location from .pagination import Pagination as Pagination from .device_spec import DeviceSpec as DeviceSpec from .device_carrier import DeviceCarrier as DeviceCarrier diff --git a/src/mobilerun_sdk/types/shared/device_spec.py b/src/mobilerun_sdk/types/shared/device_spec.py index 33e54cc..64614c9 100644 --- a/src/mobilerun_sdk/types/shared/device_spec.py +++ b/src/mobilerun_sdk/types/shared/device_spec.py @@ -4,21 +4,13 @@ from pydantic import Field as FieldInfo +from .socks5 import Socks5 +from .location import Location from ..._models import BaseModel from .device_carrier import DeviceCarrier from .device_identifiers import DeviceIdentifiers -__all__ = ["DeviceSpec", "Proxy", "ProxySocks5"] - - -class ProxySocks5(BaseModel): - host: str - - password: str - - port: int - - user: str +__all__ = ["DeviceSpec", "Proxy"] class Proxy(BaseModel): @@ -26,23 +18,31 @@ class Proxy(BaseModel): smart_ip: Optional[bool] = FieldInfo(alias="smartIp", default=None) - socks5: Optional[ProxySocks5] = None - - wireguard: Optional[str] = None + socks5: Optional[Socks5] = None class DeviceSpec(BaseModel): schema_: Optional[str] = FieldInfo(alias="$schema", default=None) """A URL to the JSON Schema for this object.""" + android_version: Optional[int] = FieldInfo(alias="androidVersion", default=None) + apps: Optional[List[str]] = None carrier: Optional[DeviceCarrier] = None + country: Optional[str] = None + files: Optional[List[str]] = None identifiers: Optional[DeviceIdentifiers] = None + locale: Optional[str] = None + + location: Optional[Location] = None + name: Optional[str] = None proxy: Optional[Proxy] = None + + timezone: Optional[str] = None diff --git a/src/mobilerun_sdk/types/devices/location_get_response.py b/src/mobilerun_sdk/types/shared/location.py similarity index 83% rename from src/mobilerun_sdk/types/devices/location_get_response.py rename to src/mobilerun_sdk/types/shared/location.py index 27f711a..bf0a32a 100644 --- a/src/mobilerun_sdk/types/devices/location_get_response.py +++ b/src/mobilerun_sdk/types/shared/location.py @@ -6,10 +6,10 @@ from ..._models import BaseModel -__all__ = ["LocationGetResponse"] +__all__ = ["Location"] -class LocationGetResponse(BaseModel): +class Location(BaseModel): latitude: float longitude: float diff --git a/src/mobilerun_sdk/types/shared/socks5.py b/src/mobilerun_sdk/types/shared/socks5.py new file mode 100644 index 0000000..36791bd --- /dev/null +++ b/src/mobilerun_sdk/types/shared/socks5.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["Socks5"] + + +class Socks5(BaseModel): + host: str + + password: str + + port: int + + user: str diff --git a/src/mobilerun_sdk/types/shared_params/__init__.py b/src/mobilerun_sdk/types/shared_params/__init__.py index 28eabc9..0b4dbfb 100644 --- a/src/mobilerun_sdk/types/shared_params/__init__.py +++ b/src/mobilerun_sdk/types/shared_params/__init__.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .socks5 import Socks5 as Socks5 +from .location import Location as Location from .device_spec import DeviceSpec as DeviceSpec from .device_carrier import DeviceCarrier as DeviceCarrier from .device_identifiers import DeviceIdentifiers as DeviceIdentifiers diff --git a/src/mobilerun_sdk/types/shared_params/device_spec.py b/src/mobilerun_sdk/types/shared_params/device_spec.py index 60bd226..7d1a43e 100644 --- a/src/mobilerun_sdk/types/shared_params/device_spec.py +++ b/src/mobilerun_sdk/types/shared_params/device_spec.py @@ -3,24 +3,16 @@ from __future__ import annotations from typing import Optional -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Annotated, TypedDict +from .socks5 import Socks5 from ..._types import SequenceNotStr from ..._utils import PropertyInfo +from .location import Location from .device_carrier import DeviceCarrier from .device_identifiers import DeviceIdentifiers -__all__ = ["DeviceSpec", "Proxy", "ProxySocks5"] - - -class ProxySocks5(TypedDict, total=False): - host: Required[str] - - password: Required[str] - - port: Required[int] - - user: Required[str] +__all__ = ["DeviceSpec", "Proxy"] class Proxy(TypedDict, total=False): @@ -28,20 +20,28 @@ class Proxy(TypedDict, total=False): smart_ip: Annotated[bool, PropertyInfo(alias="smartIp")] - socks5: ProxySocks5 - - wireguard: str + socks5: Socks5 class DeviceSpec(TypedDict, total=False): + android_version: Annotated[int, PropertyInfo(alias="androidVersion")] + apps: Optional[SequenceNotStr[str]] carrier: DeviceCarrier + country: str + files: Optional[SequenceNotStr[str]] identifiers: DeviceIdentifiers + locale: str + + location: Location + name: str proxy: Proxy + + timezone: str diff --git a/src/mobilerun_sdk/types/shared_params/location.py b/src/mobilerun_sdk/types/shared_params/location.py new file mode 100644 index 0000000..49198dc --- /dev/null +++ b/src/mobilerun_sdk/types/shared_params/location.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["Location"] + + +class Location(TypedDict, total=False): + latitude: Required[float] + + longitude: Required[float] diff --git a/src/mobilerun_sdk/types/shared_params/socks5.py b/src/mobilerun_sdk/types/shared_params/socks5.py new file mode 100644 index 0000000..0e56d16 --- /dev/null +++ b/src/mobilerun_sdk/types/shared_params/socks5.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["Socks5"] + + +class Socks5(TypedDict, total=False): + host: Required[str] + + password: Required[str] + + port: Required[int] + + user: Required[str] diff --git a/tests/api_resources/devices/test_keyboard.py b/tests/api_resources/devices/test_keyboard.py index ba20e76..766a63b 100644 --- a/tests/api_resources/devices/test_keyboard.py +++ b/tests/api_resources/devices/test_keyboard.py @@ -138,6 +138,7 @@ def test_method_write_with_all_params(self, client: Mobilerun) -> None: device_id="deviceId", text="text", clear=True, + error_rate=0, stealth=True, wpm=0, x_device_display_id=0, @@ -310,6 +311,7 @@ async def test_method_write_with_all_params(self, async_client: AsyncMobilerun) device_id="deviceId", text="text", clear=True, + error_rate=0, stealth=True, wpm=0, x_device_display_id=0, diff --git a/tests/api_resources/devices/test_location.py b/tests/api_resources/devices/test_location.py index 2d8b007..2ec5cb0 100644 --- a/tests/api_resources/devices/test_location.py +++ b/tests/api_resources/devices/test_location.py @@ -9,7 +9,7 @@ from tests.utils import assert_matches_type from mobilerun_sdk import Mobilerun, AsyncMobilerun -from mobilerun_sdk.types.devices import LocationGetResponse +from mobilerun_sdk.types.shared import Location base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,7 +23,7 @@ def test_method_get(self, client: Mobilerun) -> None: location = client.devices.location.get( device_id="deviceId", ) - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -32,7 +32,7 @@ def test_method_get_with_all_params(self, client: Mobilerun) -> None: device_id="deviceId", x_device_display_id=0, ) - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -44,7 +44,7 @@ def test_raw_response_get(self, client: Mobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" location = response.parse() - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -56,7 +56,7 @@ def test_streaming_response_get(self, client: Mobilerun) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" location = response.parse() - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) assert cast(Any, response.is_closed) is True @@ -141,7 +141,7 @@ async def test_method_get(self, async_client: AsyncMobilerun) -> None: location = await async_client.devices.location.get( device_id="deviceId", ) - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -150,7 +150,7 @@ async def test_method_get_with_all_params(self, async_client: AsyncMobilerun) -> device_id="deviceId", x_device_display_id=0, ) - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -162,7 +162,7 @@ async def test_raw_response_get(self, async_client: AsyncMobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" location = await response.parse() - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -174,7 +174,7 @@ async def test_streaming_response_get(self, async_client: AsyncMobilerun) -> Non assert response.http_request.headers.get("X-Stainless-Lang") == "python" location = await response.parse() - assert_matches_type(LocationGetResponse, location, path=["response"]) + assert_matches_type(Location, location, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/devices/test_proxy.py b/tests/api_resources/devices/test_proxy.py index eb9d960..914b3e3 100644 --- a/tests/api_resources/devices/test_proxy.py +++ b/tests/api_resources/devices/test_proxy.py @@ -42,7 +42,6 @@ def test_method_connect_with_all_params(self, client: Mobilerun) -> None: "user": "user", }, user="user", - wireguard="wireguard", x_device_display_id=0, ) assert proxy is None @@ -214,7 +213,6 @@ async def test_method_connect_with_all_params(self, async_client: AsyncMobilerun "user": "user", }, user="user", - wireguard="wireguard", x_device_display_id=0, ) assert proxy is None diff --git a/tests/api_resources/test_apps.py b/tests/api_resources/test_apps.py index 0015cdc..887a5ee 100644 --- a/tests/api_resources/test_apps.py +++ b/tests/api_resources/test_apps.py @@ -9,7 +9,14 @@ from tests.utils import assert_matches_type from mobilerun_sdk import Mobilerun, AsyncMobilerun -from mobilerun_sdk.types import AppListResponse +from mobilerun_sdk.types import ( + AppListResponse, + AppDeleteResponse, + AppRetrieveResponse, + AppMarkFailedResponse, + AppConfirmUploadResponse, + AppCreateSignedUploadURLResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -17,6 +24,48 @@ class TestApps: 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_retrieve(self, client: Mobilerun) -> None: + app = client.apps.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppRetrieveResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Mobilerun) -> None: + response = client.apps.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppRetrieveResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Mobilerun) -> None: + with client.apps.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppRetrieveResponse, app, 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 `id` but received ''"): + client.apps.with_raw_response.retrieve( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Mobilerun) -> None: @@ -30,9 +79,10 @@ def test_method_list_with_all_params(self, client: Mobilerun) -> None: order="asc", page=1, page_size=1, + platform="all", query="query", sort_by="createdAt", - source="all", + status="all", ) assert_matches_type(AppListResponse, app, path=["response"]) @@ -58,12 +108,268 @@ def test_streaming_response_list(self, client: Mobilerun) -> None: 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: + app = client.apps.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppDeleteResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Mobilerun) -> None: + response = client.apps.with_raw_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppDeleteResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Mobilerun) -> None: + with client.apps.with_streaming_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppDeleteResponse, app, path=["response"]) + + 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 `id` but received ''"): + client.apps.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_confirm_upload(self, client: Mobilerun) -> None: + app = client.apps.confirm_upload( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppConfirmUploadResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_confirm_upload(self, client: Mobilerun) -> None: + response = client.apps.with_raw_response.confirm_upload( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppConfirmUploadResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_confirm_upload(self, client: Mobilerun) -> None: + with client.apps.with_streaming_response.confirm_upload( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppConfirmUploadResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_confirm_upload(self, client: Mobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.apps.with_raw_response.confirm_upload( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_signed_upload_url(self, client: Mobilerun) -> None: + app = client.apps.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + ) + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_signed_upload_url_with_all_params(self, client: Mobilerun) -> None: + app = client.apps.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + country="US", + description="description", + developer_name="developerName", + icon_url="iconURL", + platform="android", + target_sdk=0, + ) + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create_signed_upload_url(self, client: Mobilerun) -> None: + response = client.apps.with_raw_response.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create_signed_upload_url(self, client: Mobilerun) -> None: + with client.apps.with_streaming_response.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_mark_failed(self, client: Mobilerun) -> None: + app = client.apps.mark_failed( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppMarkFailedResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_mark_failed(self, client: Mobilerun) -> None: + response = client.apps.with_raw_response.mark_failed( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = response.parse() + assert_matches_type(AppMarkFailedResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_mark_failed(self, client: Mobilerun) -> None: + with client.apps.with_streaming_response.mark_failed( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = response.parse() + assert_matches_type(AppMarkFailedResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_mark_failed(self, client: Mobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.apps.with_raw_response.mark_failed( + "", + ) + class TestAsyncApps: 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_retrieve(self, async_client: AsyncMobilerun) -> None: + app = await async_client.apps.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppRetrieveResponse, app, 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.apps.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppRetrieveResponse, app, 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.apps.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppRetrieveResponse, app, 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 `id` but received ''"): + await async_client.apps.with_raw_response.retrieve( + "", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncMobilerun) -> None: @@ -77,9 +383,10 @@ async def test_method_list_with_all_params(self, async_client: AsyncMobilerun) - order="asc", page=1, page_size=1, + platform="all", query="query", sort_by="createdAt", - source="all", + status="all", ) assert_matches_type(AppListResponse, app, path=["response"]) @@ -104,3 +411,217 @@ async def test_streaming_response_list(self, async_client: AsyncMobilerun) -> No assert_matches_type(AppListResponse, app, 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: + app = await async_client.apps.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppDeleteResponse, app, path=["response"]) + + @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.apps.with_raw_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppDeleteResponse, app, path=["response"]) + + @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.apps.with_streaming_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppDeleteResponse, app, 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_delete(self, async_client: AsyncMobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_confirm_upload(self, async_client: AsyncMobilerun) -> None: + app = await async_client.apps.confirm_upload( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppConfirmUploadResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_confirm_upload(self, async_client: AsyncMobilerun) -> None: + response = await async_client.apps.with_raw_response.confirm_upload( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppConfirmUploadResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_confirm_upload(self, async_client: AsyncMobilerun) -> None: + async with async_client.apps.with_streaming_response.confirm_upload( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppConfirmUploadResponse, app, 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_confirm_upload(self, async_client: AsyncMobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.with_raw_response.confirm_upload( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_signed_upload_url(self, async_client: AsyncMobilerun) -> None: + app = await async_client.apps.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + ) + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_signed_upload_url_with_all_params(self, async_client: AsyncMobilerun) -> None: + app = await async_client.apps.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + country="US", + description="description", + developer_name="developerName", + icon_url="iconURL", + platform="android", + target_sdk=0, + ) + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create_signed_upload_url(self, async_client: AsyncMobilerun) -> None: + response = await async_client.apps.with_raw_response.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create_signed_upload_url(self, async_client: AsyncMobilerun) -> None: + async with async_client.apps.with_streaming_response.create_signed_upload_url( + bundle_id="x", + display_name="x", + files=[ + { + "content_type": "x", + "file_name": "x", + } + ], + size_bytes=0, + version_code=0, + version_name="x", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppCreateSignedUploadURLResponse, app, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_mark_failed(self, async_client: AsyncMobilerun) -> None: + app = await async_client.apps.mark_failed( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AppMarkFailedResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_mark_failed(self, async_client: AsyncMobilerun) -> None: + response = await async_client.apps.with_raw_response.mark_failed( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + app = await response.parse() + assert_matches_type(AppMarkFailedResponse, app, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_mark_failed(self, async_client: AsyncMobilerun) -> None: + async with async_client.apps.with_streaming_response.mark_failed( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + app = await response.parse() + assert_matches_type(AppMarkFailedResponse, app, 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_mark_failed(self, async_client: AsyncMobilerun) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.apps.with_raw_response.mark_failed( + "", + ) diff --git a/tests/api_resources/test_carriers.py b/tests/api_resources/test_carriers.py index 429c77d..537784a 100644 --- a/tests/api_resources/test_carriers.py +++ b/tests/api_resources/test_carriers.py @@ -10,9 +10,12 @@ from tests.utils import assert_matches_type from mobilerun_sdk import Mobilerun, AsyncMobilerun from mobilerun_sdk.types import ( - Carrier, CarrierListResponse, + CarrierCreateResponse, CarrierDeleteResponse, + CarrierLookupResponse, + CarrierUpdateResponse, + CarrierRetrieveResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -30,7 +33,7 @@ def test_method_create(self, client: Mobilerun) -> None: mnc="x", operator="x", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -53,7 +56,7 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: umts_bands="umts_bands", website="website", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -68,7 +71,7 @@ def test_raw_response_create(self, client: Mobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -83,7 +86,7 @@ def test_streaming_response_create(self, client: Mobilerun) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True @@ -93,7 +96,7 @@ def test_method_retrieve(self, client: Mobilerun) -> None: carrier = client.carriers.retrieve( 1, ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierRetrieveResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -105,7 +108,7 @@ def test_raw_response_retrieve(self, client: Mobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierRetrieveResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -117,7 +120,7 @@ def test_streaming_response_retrieve(self, client: Mobilerun) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierRetrieveResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True @@ -127,7 +130,7 @@ def test_method_update(self, client: Mobilerun) -> None: carrier = client.carriers.update( carrier_id=1, ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -149,7 +152,7 @@ def test_method_update_with_all_params(self, client: Mobilerun) -> None: umts_bands="umts_bands", website="website", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -161,7 +164,7 @@ def test_raw_response_update(self, client: Mobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -173,7 +176,7 @@ def test_streaming_response_update(self, client: Mobilerun) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True @@ -259,7 +262,7 @@ def test_method_lookup(self, client: Mobilerun) -> None: mcc="x", mnc="x", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierLookupResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -272,7 +275,7 @@ def test_raw_response_lookup(self, client: Mobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierLookupResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -285,7 +288,7 @@ def test_streaming_response_lookup(self, client: Mobilerun) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierLookupResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True @@ -304,7 +307,7 @@ async def test_method_create(self, async_client: AsyncMobilerun) -> None: mnc="x", operator="x", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -327,7 +330,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) umts_bands="umts_bands", website="website", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -342,7 +345,7 @@ async def test_raw_response_create(self, async_client: AsyncMobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -357,7 +360,7 @@ async def test_streaming_response_create(self, async_client: AsyncMobilerun) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierCreateResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True @@ -367,7 +370,7 @@ async def test_method_retrieve(self, async_client: AsyncMobilerun) -> None: carrier = await async_client.carriers.retrieve( 1, ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierRetrieveResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -379,7 +382,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncMobilerun) -> None assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierRetrieveResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -391,7 +394,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncMobilerun) - assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierRetrieveResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True @@ -401,7 +404,7 @@ async def test_method_update(self, async_client: AsyncMobilerun) -> None: carrier = await async_client.carriers.update( carrier_id=1, ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -423,7 +426,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncMobilerun) umts_bands="umts_bands", website="website", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -435,7 +438,7 @@ async def test_raw_response_update(self, async_client: AsyncMobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -447,7 +450,7 @@ async def test_streaming_response_update(self, async_client: AsyncMobilerun) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierUpdateResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True @@ -533,7 +536,7 @@ async def test_method_lookup(self, async_client: AsyncMobilerun) -> None: mcc="x", mnc="x", ) - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierLookupResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -546,7 +549,7 @@ async def test_raw_response_lookup(self, async_client: AsyncMobilerun) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierLookupResponse, carrier, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -559,6 +562,6 @@ async def test_streaming_response_lookup(self, async_client: AsyncMobilerun) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" carrier = await response.parse() - assert_matches_type(Carrier, carrier, path=["response"]) + assert_matches_type(CarrierLookupResponse, carrier, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_devices.py b/tests/api_resources/test_devices.py index 975ae92..765eca2 100644 --- a/tests/api_resources/test_devices.py +++ b/tests/api_resources/test_devices.py @@ -32,7 +32,10 @@ def test_method_create(self, client: Mobilerun) -> None: @parametrize def test_method_create_with_all_params(self, client: Mobilerun) -> None: device = client.devices.create( + query_country="country", device_type="dedicated_physical_device", + profile_id="profileId", + android_version=0, apps=["string"], carrier={ "gsm_operator_alpha": "GsmOperatorAlpha", @@ -42,6 +45,7 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: "gsm_sim_operator_numeric": 0, "persist_sys_timezone": "PersistSysTimezone", }, + body_country="country", files=["string"], identifiers={ "bootloader_serial_number": "BootloaderSerialNumber", @@ -60,6 +64,11 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: "identifier_wifi_mac": "IdentifierWifiMAC", "serial_number": "SerialNumber", }, + locale="locale", + location={ + "latitude": 0, + "longitude": 0, + }, name="name", proxy={ "name": "name", @@ -70,8 +79,8 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: "port": 0, "user": "user", }, - "wireguard": "wireguard", }, + timezone="timezone", ) assert_matches_type(Device, device, path=["response"]) @@ -367,7 +376,10 @@ async def test_method_create(self, async_client: AsyncMobilerun) -> None: @parametrize async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) -> None: device = await async_client.devices.create( + query_country="country", device_type="dedicated_physical_device", + profile_id="profileId", + android_version=0, apps=["string"], carrier={ "gsm_operator_alpha": "GsmOperatorAlpha", @@ -377,6 +389,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) "gsm_sim_operator_numeric": 0, "persist_sys_timezone": "PersistSysTimezone", }, + body_country="country", files=["string"], identifiers={ "bootloader_serial_number": "BootloaderSerialNumber", @@ -395,6 +408,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) "identifier_wifi_mac": "IdentifierWifiMAC", "serial_number": "SerialNumber", }, + locale="locale", + location={ + "latitude": 0, + "longitude": 0, + }, name="name", proxy={ "name": "name", @@ -405,8 +423,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) "port": 0, "user": "user", }, - "wireguard": "wireguard", }, + timezone="timezone", ) assert_matches_type(Device, device, path=["response"]) diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py index 6c557de..404e8d8 100644 --- a/tests/api_resources/test_profiles.py +++ b/tests/api_resources/test_profiles.py @@ -36,6 +36,7 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: profile = client.profiles.create( name="x", spec={ + "android_version": 0, "apps": ["string"], "carrier": { "gsm_operator_alpha": "GsmOperatorAlpha", @@ -45,6 +46,7 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: "gsm_sim_operator_numeric": 0, "persist_sys_timezone": "PersistSysTimezone", }, + "country": "country", "files": ["string"], "identifiers": { "bootloader_serial_number": "BootloaderSerialNumber", @@ -63,6 +65,11 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: "identifier_wifi_mac": "IdentifierWifiMAC", "serial_number": "SerialNumber", }, + "locale": "locale", + "location": { + "latitude": 0, + "longitude": 0, + }, "name": "name", "proxy": { "name": "name", @@ -73,8 +80,8 @@ def test_method_create_with_all_params(self, client: Mobilerun) -> None: "port": 0, "user": "user", }, - "wireguard": "wireguard", }, + "timezone": "timezone", }, ) assert_matches_type(Profile, profile, path=["response"]) @@ -166,6 +173,7 @@ def test_method_update_with_all_params(self, client: Mobilerun) -> None: profile_id="profileId", name="x", spec={ + "android_version": 0, "apps": ["string"], "carrier": { "gsm_operator_alpha": "GsmOperatorAlpha", @@ -175,6 +183,7 @@ def test_method_update_with_all_params(self, client: Mobilerun) -> None: "gsm_sim_operator_numeric": 0, "persist_sys_timezone": "PersistSysTimezone", }, + "country": "country", "files": ["string"], "identifiers": { "bootloader_serial_number": "BootloaderSerialNumber", @@ -193,6 +202,11 @@ def test_method_update_with_all_params(self, client: Mobilerun) -> None: "identifier_wifi_mac": "IdentifierWifiMAC", "serial_number": "SerialNumber", }, + "locale": "locale", + "location": { + "latitude": 0, + "longitude": 0, + }, "name": "name", "proxy": { "name": "name", @@ -203,8 +217,8 @@ def test_method_update_with_all_params(self, client: Mobilerun) -> None: "port": 0, "user": "user", }, - "wireguard": "wireguard", }, + "timezone": "timezone", }, ) assert_matches_type(Profile, profile, path=["response"]) @@ -352,6 +366,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) profile = await async_client.profiles.create( name="x", spec={ + "android_version": 0, "apps": ["string"], "carrier": { "gsm_operator_alpha": "GsmOperatorAlpha", @@ -361,6 +376,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) "gsm_sim_operator_numeric": 0, "persist_sys_timezone": "PersistSysTimezone", }, + "country": "country", "files": ["string"], "identifiers": { "bootloader_serial_number": "BootloaderSerialNumber", @@ -379,6 +395,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) "identifier_wifi_mac": "IdentifierWifiMAC", "serial_number": "SerialNumber", }, + "locale": "locale", + "location": { + "latitude": 0, + "longitude": 0, + }, "name": "name", "proxy": { "name": "name", @@ -389,8 +410,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncMobilerun) "port": 0, "user": "user", }, - "wireguard": "wireguard", }, + "timezone": "timezone", }, ) assert_matches_type(Profile, profile, path=["response"]) @@ -482,6 +503,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncMobilerun) profile_id="profileId", name="x", spec={ + "android_version": 0, "apps": ["string"], "carrier": { "gsm_operator_alpha": "GsmOperatorAlpha", @@ -491,6 +513,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncMobilerun) "gsm_sim_operator_numeric": 0, "persist_sys_timezone": "PersistSysTimezone", }, + "country": "country", "files": ["string"], "identifiers": { "bootloader_serial_number": "BootloaderSerialNumber", @@ -509,6 +532,11 @@ async def test_method_update_with_all_params(self, async_client: AsyncMobilerun) "identifier_wifi_mac": "IdentifierWifiMAC", "serial_number": "SerialNumber", }, + "locale": "locale", + "location": { + "latitude": 0, + "longitude": 0, + }, "name": "name", "proxy": { "name": "name", @@ -519,8 +547,8 @@ async def test_method_update_with_all_params(self, async_client: AsyncMobilerun) "port": 0, "user": "user", }, - "wireguard": "wireguard", }, + "timezone": "timezone", }, ) assert_matches_type(Profile, profile, path=["response"]) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index db8149c..0000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from mobilerun_sdk._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 5792e00..bd4e31b 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from mobilerun_sdk._types import FileTypes +from mobilerun_sdk._types import FileTypes, ArrayFormat from mobilerun_sdk._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index 7d4e3e7..97fe070 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from mobilerun_sdk._files import to_httpx_files, async_to_httpx_files +from mobilerun_sdk._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from mobilerun_sdk._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert [entry for _, entry in extracted] == [file1, file2] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } diff --git a/tests/test_models.py b/tests/test_models.py index 36abb13..231e44e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from mobilerun_sdk._utils import PropertyInfo from mobilerun_sdk._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from mobilerun_sdk._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from mobilerun_sdk._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]