Metadata-Version: 2.5
Name: maton-ai
Version: 0.3.0
Summary: Official Python SDK for Maton — connect and automate 150+ apps.
Project-URL: Homepage, https://maton.ai
Project-URL: Documentation, https://github.com/maton-ai/maton-py#readme
Project-URL: Repository, https://github.com/maton-ai/maton-py
Project-URL: Issues, https://github.com/maton-ai/maton-py/issues
Project-URL: Changelog, https://github.com/maton-ai/maton-py/blob/main/CHANGELOG.md
Author: Maton
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Keywords: ai-agents,api-gateway,integrations,maton,sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: typing-extensions>=4.7.0
Requires-Dist: tzdata>=2024.1; sys_platform == 'win32'
Provides-Extra: dev
Requires-Dist: keyring>=24; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pre-commit>=3.5; extra == 'dev'
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: keyring
Requires-Dist: keyring>=24; extra == 'keyring'
Description-Content-Type: text/markdown

# Maton Python SDK

Official Python SDK for [Maton](https://maton.ai) — connect and automate 150+
apps (Gmail, Slack, GitHub, Notion, HubSpot, Airtable, and more) from Python.

## Install

```bash
pip install maton-ai
# or
uv add maton-ai
# or
poetry add maton-ai
```

## Quickstart

Sign in once through the browser, then construct the client with no arguments:

```python
import maton_ai

maton_ai.login()  # opens a browser; stores the session locally
```

```python
from maton_ai import Maton

maton = Maton()

conn = maton.connections.create(app="gmail")
connection_id = conn["id"]

gmail = maton.google_mail(connection=connection_id)
messages = gmail.messages.list(q="is:unread", max_results=10)
gmail.messages.send(to="alice@example.com", subject="hi", body="hello")
```

## Authentication

`maton_ai.login()` runs an OAuth flow in your browser and stores the session
locally. Nothing else is required: the SDK signs in, renews, and signs out on its
own, with no other Maton tool involved.

```python
maton_ai.login()  # sign in, and make it the active session
maton_ai.login(make_active=False)  # add a session without switching to it
maton_ai.logout()  # revoke and clear the active session
maton_ai.logout("alice@example.com")  # sign one account out
```

When the authorization server refuses, `login()` raises `maton_ai.OAuthError` — a
`MatonError` subclass that carries the server's own `error` code as `.code`, so
`access_denied` (the user declined) is distinguishable from a misconfigured client.
Everything else that can go wrong on the way — discovery, a timeout waiting for the
browser — raises `MatonError`. `logout()` clears the local session even when the
revocation call fails, so a machine can always be signed out.

Credentials resolve in this order:

1. `Maton(profile="...")`
2. `Maton(api_key="...")`
3. the `MATON_API_KEY` environment variable
4. `MATON_PROFILE`
5. the active stored session, or the sole one when none is marked active

Explicit credentials always win over ambient machine state. Passing both
`profile=` and `api_key=` is rejected as ambiguous, and a named profile that
cannot be resolved raises rather than quietly authenticating as another account.

> **One deliberate difference from the Maton CLI.** In the CLI, `MATON_API_KEY` is
> a global override that outranks even `-p/--profile`. In this SDK an explicit
> `profile=` argument outranks `MATON_API_KEY`, because an argument written in code
> is a stronger signal of intent than an exported variable, and because a
> multi-tenant process must be able to select an account on a machine that happens
> to have `MATON_API_KEY` set. `MATON_PROFILE` does *not* outrank
> `MATON_API_KEY` — between two ambient sources, the CLI's order is kept.

```python
maton = Maton(profile="alice@example.com")  # a specific account
```

An API key remains the right choice where a browser sign-in is impossible, such
as CI:

```python
maton = Maton(api_key=os.environ["MATON_API_KEY"])
```

### Where the session is stored

Session metadata goes to the `python` section of `credentials.json` in
`~/.config/maton` on macOS and Linux, or `%AppData%\Maton` on Windows. The file
is shared with the other Maton SDKs, while each SDK keeps independent profiles
and an independent active-profile selection. `$MATON_CONFIG_DIR` overrides the
directory outright, and `$XDG_CONFIG_HOME/maton` takes precedence over the
defaults. The tokens themselves go to the OS keyring when the optional extra is
installed:

```bash
pip install "maton-ai[keyring]"
```

Without it, tokens are written to `credentials.json` in plaintext at mode `0600`, and
`login()` warns that it did so. Installing the extra is recommended on any
machine where that file is backed up or synced.

Access tokens are short-lived; the SDK renews them in-process from the stored
refresh token, so a long-running client keeps working without re-authenticating.

The same pattern works for every supported app:

```python
slack = maton.slack(connection=slack_conn_id)
slack.messages.send(channel="#general", text="deploy finished ✅")

gh = maton.github(connection=gh_conn_id)
gh.issues.create(repo="maton-ai/maton-py", title="bug: ...", body="...")

notion = maton.notion(connection=notion_conn_id)
notion.data_sources.query(data_source_id="...", filter={"property": "Status", "status": {"equals": "Done"}})

hubspot = maton.hubspot(connection=hubspot_conn_id)
hubspot.contacts.list(limit=25)
```

There are three places to select a connection, in order of precedence
(per-call beats accessor beats constructor):

```python
maton = Maton(api_key=..., connection=connection_id)

gmail = maton.google_mail(connection=connection_id)
gmail.messages.list(q="is:unread")

maton.google_mail.messages.list(q="is:unread", connection=connection_id)
```

Generic passthrough:

```python
maton.api.post(
    "google-mail",
    "/gmail/v1/users/me/messages/send",
    json={"raw": "..."},
    connection=connection_id,
)
```

## Functions

A function is a Python or Node handler that Maton hosts. Code lives in
immutable versions; the active version answers requests on the function's own
host, at the `url` that `get` returns. Manage them through `maton.functions`,
with `maton.functions.versions`, `maton.functions.env`, and `maton.functions.runs`
sub-resources:

```python
fn = maton.functions.create(
    name="greet",
    runtime="python3.12",
    files={"main.py": "def handler(event):\n    return {'hello': 'ada'}\n"},
)
function_id = fn["function_id"]

# ``MATON_API_KEY`` is runtime-provided.
maton.functions.env.create(
    function_id,
    env=[{"key": "API_BASE", "value": "https://example.com", "type": "PLAIN"}],
)
maton.functions.env.update(function_id, "API_BASE", "https://example.org")

url = maton.functions.get(function_id)["url"]
try:
    resp = maton.api.with_raw_response.post(url, json={"name": "ada"})
    print(resp.function_run_id, resp.json())
except MatonError as exc:
    for event in maton.functions.runs.logs.tail(function_id, exc.function_run_id):
        print(event["message"], end="")

maton.functions.versions.list(function_id)
maton.functions.update(function_id, version=1)
maton.functions.update(
    function_id,
    files={
        "main.py": "import json\ndef handler(event):\n    body = json.loads(event.get('body') or '{}')\n    return {'hi': body.get('name')}\n"
    },
)
maton.functions.code.download(function_id)
maton.functions.search('"hello"', context=2)
```

### Handler

The runtime calls the handler with `event` and an optional `context`, and turns its return value into an HTTP response.

#### Event

```json
{
  "version": 1,
  "rawPath": "/",
  "rawQueryString": "a=1",
  "cookies": ["k=v"],
  "headers": { "host": "greet-a1b2c3.maton.app" },
  "queryStringParameters": { "a": "1" },
  "requestContext": {
    "accountId": "...",
    "domainName": "greet-a1b2c3.maton.app",
    "domainPrefix": "greet-a1b2c3",
    "http": {
      "method": "POST",
      "path": "/",
      "protocol": "HTTP/1.1",
      "sourceIp": "...",
      "userAgent": "..."
    },
    "runId": "...",
    "time": "30/Aug/2026:17:24:03 +0000",
    "timeEpoch": 1788000000000
  },
  "body": "{\"name\":\"ada\"}",
  "isBase64Encoded": false
}
```

#### Context (optional)

**Python**

```python
context.run_id  # "..."
context.function_name  # "greet"
context.function_version  # "1"
context.function_id  # "..."
context.account_id  # "..."
context.memory_limit_in_mb  # 128
```

**Node**

```jsonc
{
  "runId": "...",
  "functionName": "greet",
  "functionVersion": "1",
  "functionId": "...",
  "accountId": "...",
  "memoryLimitInMB": "128"
}
```

#### Response

Anything the handler returns that is not a dict carrying a `statusCode` key is
sent as the response body with a `200`. A returned string is JSON-encoded, so
`return "hello"` comes back as `"hello"` with the quotes. To set the status or
headers, return an envelope carrying `statusCode` instead:

```python
def handler(event):
    return {
        "statusCode": 201,
        "headers": {"content-type": "text/plain"},
        "body": "created",
    }
```

## Triggers

Triggers register an event source (e.g. GitHub `pull_request.opened`) and fan
matching events out to webhook destinations. Manage them through
`maton.triggers`, with `maton.triggers.destinations` and `maton.triggers.events`
sub-resources:

```python
trigger = maton.triggers.create(
    source="github",
    event_type="pull_request.opened",
    connection_id=gh_conn_id,
    parameters={"repo": "maton-ai/cli"},
    destinations=[{"url": "https://example.com/hook"}],
)
trigger_id = trigger["trigger"]["trigger_id"]

maton.triggers.list(source="github", status="ENABLED")
maton.triggers.update(trigger_id, status="DISABLED")

dst = maton.triggers.destinations.create(trigger_id, url="https://example.com/hook")
maton.triggers.destinations.rotate_secret(trigger_id, dst["destination"]["destination_id"])

events = maton.triggers.events.list(trigger_id, limit=20)
maton.triggers.events.replay(trigger_id, events["events"][0]["event_id"])

for event in maton.triggers.events.watch(trigger_id):
    handle(event)
```

## Supported apps

| App | Accessor | Highlights |
|---|---|---|
| Asana | `maton.asana` | projects, tasks, workspaces |
| GitHub | `maton.github` | repos, issues, PRs, releases, labels |
| Google Ads | `maton.google_ads` | accounts, campaigns, ad groups, ads, keywords |
| Google Calendar | `maton.google_calendar` | calendars, events, ACL, freebusy |
| Google Docs | `maton.google_docs` | documents (create / get / write) |
| Google Drive | `maton.google_drive` | files, drives, permissions, comments, revisions |
| Google Mail | `maton.google_mail` | drafts, labels, messages, threads |
| Google Sheets | `maton.google_sheets` | spreadsheets, sheets, values |
| Google Tasks | `maton.google_tasks` | tasklists, tasks |
| HubSpot | `maton.hubspot` | contacts, companies, deals, associations |
| Jira | `maton.jira` | issues, projects, transitions, comments, users |
| Linear | `maton.linear` | issues, projects, cycles, teams (GraphQL) |
| Microsoft Teams | `maton.microsoft_teams` | teams, channels, chats, messages, meetings |
| Notion | `maton.notion` | pages, databases, data sources, blocks, search |
| OneDrive | `maton.one_drive` | drives, items (upload, share, move) |
| Outlook | `maton.outlook` | messages, events, contacts, folders |
| Salesforce | `maton.salesforce` | records, query, search, composites |
| Slack | `maton.slack` | channels, messages, files, reactions, schedules |
| Stripe | `maton.stripe` | customers, charges, invoices, subscriptions |
| Trello | `maton.trello` | boards, cards, lists, checklists, labels |
| YouTube | `maton.youtube` | channels, videos, playlists, comments, search |

## Reliability

Every call goes through an `httpx`-based client with automatic retries. The
defaults are configurable on the constructor:

```python
maton = Maton(
    api_key=...,
    timeout=30.0,  # per-request timeout, seconds
    max_retries=2,  # retry attempts on transient failures
    max_backoff=20.0,  # cap on a single backoff sleep, seconds
)
```

Retries fire on connection errors and on `429 / 500 / 502 / 503 / 504`; other
4xx/5xx surface immediately. Backoff is truncated exponential with full jitter
(botocore standard mode), and honors a server-provided `Retry-After` header.

## Errors

All failures raise a subclass of `MatonError`, each carrying `status_code`,
`request_id`, the parsed `body`, and `function_run_id` — set only when the failing
response carried `X-Function-Run-Id`, in practice a function invocation:

| Exception | When |
|---|---|
| `AccessDeniedError` | 401/403 — bad/missing API key or insufficient scope |
| `ResourceNotFoundError` | 404 |
| `TooManyRequestsError` | 429 — also exposes `.retry_after` |
| `ValidationError` | other 4xx (incl. 412 when an action needs a connection) |
| `InternalServerError` | 5xx or unexpected upstream response |
| `APIConnectionError` | network/transport failure (couldn't reach the gateway) |

A handful of apps wrap their vendor-specific error envelopes (e.g. a GraphQL
`errors` array or a Slack `ok: false` payload) in a dedicated subclass, so you
can catch them by app while still falling back to `MatonError`:

| Exception | App |
|---|---|
| `GitHubError` | GitHub GraphQL/API errors |
| `GoogleDriveError` | Google Drive API errors |
| `LinearError` | Linear GraphQL/API errors |
| `SlackError` | Slack API errors |
| `StripeError` | Stripe API errors |

```python
from maton_ai import MatonError, TooManyRequestsError

try:
    maton.google_mail.messages.list(q="is:unread", connection=connection_id)
except TooManyRequestsError as exc:
    print("slow down; retry after", exc.retry_after)
except MatonError as exc:
    print(exc.status_code, exc.request_id, exc.body)
```
