Skip to main content

nomac

NoMac Simulator from Python: iPhone and iPad Simulators in the cloud, for a platform's users and agents. Start a phone, build and run a native app on it, read and tap its screen by label, show it live in your page, save its state, and hear when it is ready or ends.

pip install nomac

Python 3.10 and later, one dependency (httpx), typed. NoMac for sync code, AsyncNoMac for asyncio: the same calls, awaited.

from nomac import NoMac

nomac = NoMac()  # NOMAC_API_KEY from the environment
phone = nomac.phones.start(label="user-42", reuse=True)
phone.wait_until_ready()
root = phone.fetch_git("https://github.com/acme/app", token=github_token)["root"]
print(phone.tool("app_run", path=root, scheme="App").text)
phone.stop()
from nomac import AsyncNoMac

async with AsyncNoMac() as nomac:
    phone = await nomac.phones.start(label="user-42", reuse=True)
    await phone.wait_until_ready()
    print((await phone.tool("ui_look")).text)
    await phone.stop()

A phone costs $0.80 an hour, billed per minute from the account's prepaid credit, with a one-minute minimum. Waiting in line is free.

Phones

nomac.phones.start(device=, label=, reuse=, limits=) starts a phone and answers it at once, usually queued; phone.wait_until_ready() follows it. A phone from the pool is ready in seconds. When every phone is in use, the start waits in line, free, up to 5 minutes.

  • device: "iphone" (iPhone 17 Pro, the default), "ipad" (iPad Pro 11-inch (M5)) or "iphone-pro-max" (iPhone 17 Pro Max), all on iOS 27.
  • label: your own name for the phone, such as your user's id: letters, digits and . _ : @ -, up to 64.
  • reuse: with label, answer the live phone with that label instead of starting another (phone.reused).
  • limits: max_duration_seconds (7,200 by default, 28,800 at most), idle_timeout_seconds (600 by default, 60 to 3,600) and spend_cap_microunits ("5000000", $5, by default).

An account runs two phones at once by default; NoMac Platform runs more. A retry of a start with the same idempotency_key answers the same phone, never a second one.

nomac.phones.list(label=) lists the account's phones, newest first, and nomac.phones.get(id) reads one. phone.session has its state, device, label, cost and deadlines; phone.refresh() reads it again. phone.extend(minutes=) keeps a running phone longer, up to 8 hours in all. phone.stop() ends it: its apps and data go with it, and its workspace (projects and build output) stays for the next phone with the same label, so the next build is incremental.

Tools

phone.tool(name, arguments) runs one of simulator-use's tools on the phone, its arguments as a dict or as keywords, and answers a ToolResult: text says what happened and what to do next, content has its text and images, is_error, and stop_notice in the phone's last 15 minutes. A long build is followed to its end.

Tool
app_run Builds an Xcode project or workspace (path, scheme) with Xcode 27.1, installs it and launches it.
tests_run Runs XCTest and XCUITest, each case's result as it finishes.
ui_look The screen as text, one line per element with its label and position.
ui_act Taps, types, swipes and keys by label: steps=['tap "Sign In"', "fill #email a@b.co"].
app_control Installs, launches and terminates apps, opens links, sets permissions, appearance, locale, time zone and location.
app_logs, screen_record, devices, job_status, job_cancel Logs, video, the phone and Xcodes, long jobs.

Projects reach the phone's workspace with phone.fetch_git(url, ref=, token=) or phone.upload_archive(data, name=) (bytes or a file's path: a .tar.gz or .zip of a project, or of a .app built for the Simulator, up to 2 GiB). Both answer root, the path app_run and tests_run take; only what changed is written.

A private repository on github.com, gitlab.com or bitbucket.org takes its token one of two ways, and it goes only to that host:

  • token: the token itself, sent with this fetch and kept nowhere, not even for the phone's next fetch. Your user's token stays with you, and a short-lived one (GitHub's one-hour tokens) works. A phone's token may send one.
  • secret: the name of one of the account's own secrets that holds it, for your own repositories (the account's key only).

Values your app needs go with the run, as env on app_run ("API_URL=https://staging.example KEY=..."): the phone keeps them for its relaunches until it stops, and they are erased with it.

Live view and your users

phone.links["view"] (watch) and phone.links["control"] (use) are links a person opens with no NoMac sign-in; phone.view_link(control=True, minutes=120) makes one for your user. @nomac/phone embeds it in your page.

phone.token(minutes=) gives a token for that phone only, to hand your own user or agent: NoMac(token=...) reaches that phone's tools, files, state, view and replay until the token expires (2 hours by default, a day at most). It cannot start, list or stop phones, make another token, use the account's secrets, or reach your webhooks. phone.revoke_tokens() revokes every token and link of the phone made so far.

Phones are kept apart by label: a workspace and saved states are shared only by phones with the same label. Give each of your users their own label, and one user's phone, or its token, never reaches another's.

Saved state and replays

phone.save_state("signed-in") keeps the phone's apps, their data, its keychain and permissions for the phones with its label; phone.restore_state("signed-in") puts them back onto any of them of the same model and iOS. phone.list_states() and phone.delete_state(name) manage them. Every phone is recorded: phone.replay() answers its video and its calls (typed text left out) once it has stopped, kept 7 days.

Deleting a user's data

When one of your users deletes their account, nomac.labels.delete("user-42") deletes what NoMac keeps for that label: its saved states, its phones' replays and file copies, and its workspace (the key to its encrypted volume is destroyed at once, so it never opens again; the host deletes the volume on its own sweep). It answers {"deleted": {"states", "workspaces", "replays", "files"}}; a repeat finds nothing left. Stop the label's phones first (label_in_use); for 15 minutes after the last one ends its replay is still being saved (label_busy, details["retry_after_seconds"]). The phones' records (times and cost, under the label) stay for billing, so use your own user id as the label, not an email.

Webhooks

hook = nomac.webhooks.create("https://platform.example/nomac", events=["phone.ready", "phone.ended"])
# Keep hook["secret"]: it is shown once.

Check every delivery before trusting it, with the raw body as received:

from nomac import verify_webhook

event = verify_webhook(request.body, request.headers["nomac-signature"], os.environ["NOMAC_WEBHOOK_SECRET"])
if event["type"] == "phone.ended":
    release(event["data"]["session"]["label"], event["data"]["reason"])

phone.ended carries the label, the time billed and the cost, and why it ended (stopped, idle, time_limit, spend_cap, credit, crashed, no_capacity or start_failed). A delivery can come more than once: key on its id. nomac.webhooks.list(), delete(id) and deliveries(id) manage them.

Errors

A refusal is a NoMacError with NoMac's code (for code to branch on), message (for a person), HTTP status and details, such as session_limit_reached (409) or insufficient_credit (402). A read, a stop and any request with a request key are sent again when NoMac is busy for a moment.

The REST API it calls is at nomac.app/docs/simulator; nomac.request(method, path, body=, idempotency_key=) calls it directly.

MIT licensed.

Metadata

Release files for nomac 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nomac 0.1.0
File Size Uploaded Provenance Actions
nomac-0.1.0.tar.gz 16.9 kB Without provenance Details for nomac-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for nomac 0.1.0
File Interpreter ABI Platform Actions
nomac-0.1.0-py3-none-any.whl Python 3 none any Details for nomac-0.1.0-py3-none-any.whl

Total release size: 37.0 kB

Release files / nomac-0.1.0.tar.gz

Download URL nomac-0.1.0.tar.gz
Size 16.9 kB
Tags Source
SHA-256 checksum
How to use checksums
9c1f66874dcac6579c33faaf1dc1995294f4f19582b63cc4573f26cbdd1b3f91
BLAKE2b-256 checksum
How to use checksums
3f9500f25f259091dd2f712721ca67777dc6820cce096909c7e4181ce32a3275
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / nomac-0.1.0-py3-none-any.whl

Download URL nomac-0.1.0-py3-none-any.whl
Size 20.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
04f9803ced08c3acbf27079a059c25b0b7e2d04abaea364750d4fb7196031a61
BLAKE2b-256 checksum
How to use checksums
ac3fd3eadbfce9ee4c1b50900ca0cc921a3945b6cc5acfe5d24ee9d1bbc3442d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for nomac 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page