Metadata-Version: 2.5
Name: maton-ai
Version: 0.2.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.connection.create(app="gmail")
connection_id = conn["id"]

gmail = maton.google_mail(connection=connection_id)
messages = gmail.message.list(q="is:unread", max_results=10)
gmail.message.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.message.send(channel="#general", text="deploy finished ✅")

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

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

hubspot = maton.hubspot(connection=hubspot_conn_id)
hubspot.contact.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.message.list(q="is:unread")

maton.google_mail.message.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,
)
```

## Triggers

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

```python
trigger = maton.trigger.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.trigger.list(source="github", status="ENABLED")
maton.trigger.update(trigger_id, status="DISABLED")

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

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

for event in maton.trigger.event.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`, and the parsed `body`:

| 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.message.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)
```
