Skip to content

About

A wrapper written in Python with a focus on using our API.

Topics

Resources

Stars

24 stars

Watchers

2 watching

Forks

Latest commit

 

History

459 Commits

Folders and files

Repository files navigation

Square Cloud Banner

squarecloud-api

The official Python SDK for the Square Cloud API.

PyPI Version License Downloads
  • Zero runtime dependencies: only the standard library (http.client, json, ssl), with one keep-alive connection per thread.
  • Runs on Python 3.11+, sync (SquareCloud) and async (AsyncSquareCloud), fully typed (TypedDict responses, py.typed).
  • Covers all 67 operations of the Square Cloud API, checked against the pinned OpenAPI spec on every CI run.
  • Uploads and snapshot downloads stream; realtime logs and status come as an iterator (or async for).
  • One error type, SquareCloudAPIError, for every API, network and local failure.

Documentation · Releases · Migration guide

Installation

pip install squarecloud-api
uv add squarecloud-api
poetry add squarecloud-api

Requires Python 3.11 or newer.

API key

Create one at squarecloud.app/account/security. A key can be limited to scopes (apps:read, apps:deploy, ...) and to specific apps or databases. A call outside those limits raises SquareCloudAPIError with 403 MISSING_SCOPE or RESOURCE_NOT_ALLOWED, and list endpoints return only the resources the key can see. An unknown, revoked or expired key is 401 ACCESS_DENIED.

Quick start

import os
from squarecloud import SquareCloud

with SquareCloud(os.environ['SQUARECLOUD_API_KEY']) as client:
    me = client.account.me()
    print(me['user']['name'], [app['name'] for app in me['applications']])

    app_id = me['applications'][0]['id']
    client.apps.restart(app_id)
    print(client.apps.logs(app_id))

The same API with await:

import asyncio
import os

from squarecloud import AsyncSquareCloud


async def main() -> None:
    async with AsyncSquareCloud(os.environ['SQUARECLOUD_API_KEY']) as client:
        statuses = await client.apps.status_all()
        print([s['id'] for s in statuses if s['running']])


asyncio.run(main())

More in examples/: apps, realtime, snapshots, databases, workspaces, async client.

Configuration

import os

from squarecloud import BASE_URL, SquareCloud, __version__

client = SquareCloud(
    os.environ['SQUARECLOUD_API_KEY'],
    base_url=BASE_URL,  # the options are keyword-only; these are the defaults
    timeout=30.0,
    max_retries=2,
    transport=None,
    user_agent=f'squarecloud-sdk-py/{__version__}',
)
Option Default Notes
api_key required Sent raw in Authorization. An empty or whitespace-only key raises ValueError.
base_url BASE_URL = https://api.squarecloud.app/v2 A trailing / is stripped.
timeout 30.0 Seconds per socket operation. <= 0 disables every timeout, floors included. See Retries, timeouts and rate limits.
max_retries 2 Negative counts as 0.
transport HTTPTransport(timeout) Plug your own HTTP client (see below).
user_agent squarecloud-sdk-py/<version> Replaces the whole User-Agent header.

AsyncSquareCloud takes the same arguments. Both clients are thread-safe; call close() (or use with / async with) to close their connections. The version is squarecloud.__version__.

Custom transport

transport= accepts any callable transport(method, url, headers, body, timeout, stream) that returns an object with status, read(), readline() and close() (an http.client.HTTPResponse qualifies). Use it for proxies, tracing or tests. body is bytes, an iterable of bytes chunks (with Content-Length already set) or None. timeout is the per-socket-operation timeout (raised for the held calls), or None for uploads, large file writes and the realtime stream, which a custom transport should bound on connect itself. The default HTTPTransport(timeout) still bounds their connect, and asks for gzip on every call except the streamed ones (realtime and snapshot downloads).

Logging

The SDK logs each request at DEBUG on the squarecloud logger (method, path and status; never bodies or keys) and attaches only a NullHandler:

import logging

logging.basicConfig()
logging.getLogger('squarecloud').setLevel(logging.DEBUG)

API

Every app id may also be the composite '<appId>-<workspaceId>' to act on an app shared with you through a workspace. Optional modifiers are keyword-only.

Group Methods
account me(), snapshots(*, scope=None)
service status() (public route: the API does not check the key, but the client still needs a non-empty one)
ai chat(request) (OpenAI-compatible, non-streaming)
apps create(file), get(id), delete(id), status_all(*, workspace_id=None), status(id, *, raw=False), start(id), stop(id), restart(id), logs(id), metrics(id), realtime(id), domains(), load_balancers(), commit(id, file, *, path=None, filename=None)
apps.deploys set_webhook(id, access_token), link_github_app(id, repository, branch), unlink_github_app(id), list(id), current(id)
apps.envs get(id), set(id, envs), replace(id, envs), delete(id, keys)
apps.files list(id, path=None), read(id, path) → bytes, write(id, path, content), move(id, path, to), delete(id, path)
apps.snapshots list(id), create(id), restore(id, name, version_id)
apps.network analytics(id, start, end, **filters), errors(id, start, end, *, include_4xx=False), logs(id, start, end), performance(id, start, end), dns(id), set_domain(id, domain), purge_cache(id)
databases create(name, *, type, version, memory), get(id), update(id, *, name=None, ram=None), delete(id), start(id), stop(id), status(id, *, raw=False), metrics(id), status_all(), certificate(id), reset_credentials(id, 'password' | 'certificate') → the new password, or ''
databases.snapshots list(id), create(id), restore(id, name, version_id)
workspaces create(name), list(), get(id), delete(id), leave(id)
workspaces.members add(workspace_id, code, group), update(workspace_id, member_id, group), remove(workspace_id, member_id), invite_code()
workspaces.apps add(workspace_id, app_id), remove(workspace_id, app_id)
client download_snapshot(url, dest), close()

AsyncSquareCloud has the same groups and methods, each returning a coroutine (it runs the sync call in asyncio.to_thread), except close(), which is sync, and apps.realtime(id), which returns an AsyncRealtime to use with async for. Field names are the API's own (created_at, content_type, include_4xx...), so the API reference applies as-is. Responses are TypedDicts from squarecloud.types: plain dicts, so fields the API adds later are kept. A string result is never None ('' when the API sends nothing). start/end take an ISO 8601 string (sent as is) or a datetime (sent as UTC; a naive datetime is local time). The analytics filters are keyword-only: country, ip, path, status, os, browser, protocol, referer, provider, content_type, bot. provider takes the 'NAME (ASN)' form the providers buckets report (provider='GOOGLE (15169)'); an invalid filter is 400 INVALID_FILTER. metrics() returns up to 24 h of 5-minute points, newest first. status() formats cpu and ram as strings ('120.4MB'); with raw=True they are numbers. Empty, . and .. ids are rejected with INVALID_ID before sending, because they would reach another route. apps.network.analytics, errors and performance return None for a window with no traffic.

Usage

Uploads

apps.create and apps.commit take a path, bytes or a binary file object, and stream it: a path or a seekable file is never loaded into memory. A commit unpacks a .zip at path (the app root by default); any other file lands at path/<filename>. The filename is filename=, else the file's own name, else app.zip on create and commit.zip on commit. A zip over 100 MB fails locally with FILE_TOO_LARGE before anything is sent. Uploads have no timeout.

created = client.apps.create('bot.zip')
client.apps.commit(created['id'], 'patch.zip', path='/src')
client.apps.commit(created['id'], b'print(1)', path='/src', filename='main.py')

Files

client.apps.files.write(app_id, '/config.json', '{"debug": false}')  # str or bytes
data: bytes = client.apps.files.read(app_id, '/config.json')
client.apps.files.move(app_id, '/config.json', '/config.old.json')

A str travels as text; bytes travel base64-encoded (binary-safe, about 1.33x on the wire). Empty content ('' or b'') creates an empty file. Content over 10 MB fails locally with FILE_TOO_LARGE, and content over 1 MiB is sent without a timeout, like an upload; invalid content is 400 INVALID_CONTENT. read always asks for base64 and returns the decoded bytes (at most 10 MB, else 413 FILE_TOO_LARGE). list of a missing directory is 404 FILE_NOT_FOUND, and a blocked path is 403 BLOCKED_PATH. Paths are at most 256 characters.

Snapshots

create returns {'pending': True} while the API is still generating the snapshot (HTTP 202 SNAPSHOT_PROCESSING). It then appears in list on its own, usually within 2 minutes: poll list, and never call create again, which is limited to one per 180 seconds and counts against the plan's daily snapshot quota.

snapshot = client.apps.snapshots.create(app_id)
if not snapshot['pending']:
    client.download_snapshot(snapshot['url'], 'backups/')  # -> backups/<name>.zip

latest = max(client.apps.snapshots.list(app_id), key=lambda s: s['modified'])
client.apps.snapshots.restore(app_id, latest['name'], latest['version_id'])
client.download_snapshot(latest['url'], 'backups/')

Each listed item carries the API's version_id (what restore needs) and a signed download url, passed through as sent. download_snapshot never sends the API key to the snapshot host, streams to a .part file next to the target and renames it when complete, and bounds each read with timeout.

Realtime

with client.apps.realtime(app_id) as stream:
    for event in stream:
        if event['event'] == 'logs':
            print(event['stream'], event['line'])  # stdout | stderr
        elif event['event'] == 'status':
            print(event['status'].get('cpu'))  # always the full, merged state
        else:  # 'system' or 'error'
            print(event['data'])  # a code such as REALTIME_DISCONNECTED

Every event has event, data (the raw frame text) and id. The HTTP status is checked before streaming, so 429 REALTIME_MAX_CONNECTIONS raises. A dropped connection, or the API's REALTIME_RECONNECT hand-off, reopens up to 3 times in a row (a logs or status event resets the count), at most one open per 5.5 s to stay under the API's pace of one per 5 s; past that it raises NETWORK_ERROR. The open is timed until the response headers arrive; the stream itself is not. The loop ends on a clean close (10-minute server limit), on REALTIME_DISCONNECTED, or on stream.close() (safe from any thread, also mid-wait). With AsyncSquareCloud: async with client.apps.realtime(app_id) as stream: async for event in stream: .... Max 5 concurrent streams per account and 30 per app.

GitHub deploys

# A GitHub webhook: returns its URL ('' when removed with '@')
url = client.apps.deploys.set_webhook(app_id, 'ghp_xxx')

# Or the Square Cloud GitHub App
repo = client.apps.deploys.link_github_app(app_id, 'octocat/hello-world', 'main')
print(repo['id'], repo['full_name'], repo['branch'])

current = client.apps.deploys.current(app_id)  # {} when nothing is set
client.apps.deploys.unlink_github_app(app_id)

Linking needs scope apps:deploy and a GitHub App installation on your account (403 GITHUB_NOT_CONNECTED otherwise). The repository must belong to a GitHub App installation your connected GitHub account holds (403 REPOSITORY_NOT_AVAILABLE otherwise), and that account needs write access to it (403 REPOSITORY_PERMISSION_REQUIRED). 502 FAILED_TO_FETCH means GitHub did not confirm the branch; it is safe to retry. A repository and branch can be linked to one app across all accounts (409 REPOSITORY_BRANCH_ALREADY_CONFIGURED; the other app's id appears only in message, and only when that app is yours). Re-linking needs an unlink first (400 GIT_ALREADY_CONFIGURED), and unlinking without a link is 400 GIT_NOT_CONFIGURED. Link and unlink share a limit of 3 calls per 60 s.

Errors

Every API and network failure raises SquareCloudAPIError with status, code, message, method, path ('/v2/apps/...', never the query string) and cause. Local file problems, such as a missing upload path or an unwritable download destination, raise OSError.

from squarecloud import SquareCloudAPIError

try:
    client.apps.start(app_id)
except SquareCloudAPIError as e:
    print(e)  # POST /v2/apps/<id>/start: HTTP 404 APP_NOT_FOUND
    if e.code == 'MISSING_SCOPE':
        ...
  • No response: status 0 with code NETWORK_ERROR (original exception in cause) or TIMEOUT.
  • Local checks, nothing sent: status 0 with FILE_TOO_LARGE, or INVALID_ID for an id that is empty, . or ...
  • message is the server's explanation, or '' when it sent only a code. A body without a code (a proxy page, a failed snapshot download) is UNKNOWN_ERROR, with the server's message when it sent one and HTTP <status> otherwise; a 2xx body that is not JSON is UNKNOWN_ERROR with Invalid JSON in HTTP <status> response.
  • Start, stop and restart refusals are 409 with only a code (message is ''): CONTAINER_ALREADY_STARTED, CONTAINER_ALREADY_STOPPED, CONTAINER_TEMPORARILY_SUSPENDED (apps), CONTAINER_NOT_FOUND, CONTAINER_INSUFFICIENT_DISK_SPACE, CONTAINER_NETWORK_CONFLICT or ACTION_FAILED. They are errors: check e.code if an already started or stopped resource is fine for you. A 2xx reply whose body is {"status": "error"} also raises; a 202 SNAPSHOT_PROCESSING stays {'pending': True}.
  • ai.chat() errors, auth, 429 and 503 included, carry the OpenAI error's lowercase code (access_denied, upgrade_required, rate_limit_exceeded, server_overloaded, database_unavailable, ...), or its type when there is no code.
  • The SquareCloudAPIError docstring lists every known API code. The list grows: treat an unknown code as a generic failure of its HTTP status.
  • str(e) is <METHOD> <path>: HTTP <status> <CODE>: <message>, without HTTP <status> when the status is 0 and without : <message> when it is empty.

Retries, timeouts and rate limits

  • Timeouts: timeout (30 s) applies per socket operation. Calls the server holds open wait at least 120 s (start/stop/restart of apps and databases, databases.create, snapshot create/restore), and so does ai.chat(), whose gateway has one 90 s deadline for the whole request and answers 503 server_overloaded past it; a larger timeout wins. timeout <= 0 disables every timeout, floors included. Uploads and files.write above 1 MiB have no timeout; realtime() is timed only until it opens.
  • Retries: the SDK retries only what is safe: network errors on GET (including a body cut off mid-read, realtime opens and download_snapshot() before the response), and 503 UPLOAD_BUSY/ANALYTICS_BUSY (plus DATABASE_UNAVAILABLE on GET), up to max_retries (2) times with exponential backoff (500 ms·2ⁿ, jitter, max 8 s). Timeouts, 429 and the AI's 503 server_overloaded (safe for you to retry) are never retried. DATABASE_UNAVAILABLE is not retried on other methods because it can fire after a mutation was applied; retrying an idempotent one (a files.write, an envs.replace, a stop) is up to you.
  • Rate limits: every account has a global limit of requests per 60 s, set by its plan (values). Going over it, or over a route's own limit, returns 429 RATE_LIMITED or KEEP_CALM. RATE_LIMITED can block the account, API key or IP for about 30 minutes; it is also the limit of the network endpoints and account.snapshots(). RATE_LIMIT and RATE_LIMIT_EXCEEDED are its deprecated names. The API sends no Retry-After, which is why the SDK never retries a 429.

Development

uv sync
uv run ruff format --check && uv run ruff check && uv run mypy && uv run pytest   # offline
uv build

The offline suites (tests/test_transport.py, tests/test_http.py, tests/test_streams.py, tests/test_conformance.py) use a mock transport or a loopback server and never reach the API. The live suite runs all 67 operations against the real API:

SQUARECLOUD_API_KEY=... uv run pytest -m live -s

Warning: the live suite creates and deletes real resources (an app, a database and a workspace named sdk-live-py-<timestamp>) on the key's account. It is skipped without SQUARECLOUD_API_KEY and never runs in CI. Requests are spaced 2.1 s apart; run the JS, Python and Go live suites one after another, never at the same time.

Contributing

Issues and pull requests are welcome at squarecloudofc/sdk-api-py.

License

MIT, see LICENSE.

Authors

Maintained by Square Cloud.

Contributors:

About

A wrapper written in Python with a focus on using our API.

Topics

Resources

Stars

24 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages