Metadata-Version: 2.3
Name: amasto
Version: 0.2.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.12.5
Requires-Dist: semver>=3.0.4
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# amasto

Fully async, type-safe Python client for the [Mastodon API](https://docs.joinmastodon.org/api/).

> [!WARNING]
> This project contains code generated by LLMs (Large Language Models). A significant portion of the codebase has not been fully reviewed or tested. Use at your own risk.

## Features

- **Async-first** — All I/O uses `async`/`await` via [httpx](https://www.python-httpx.org/)
- **Type-safe** — Typed endpoint descriptors and [Pydantic](https://docs.pydantic.dev/) response models; ships with `py.typed`
- **Version-aware** — Automatic server version detection via NodeInfo; models mark field availability with `since()` / `Unsupported`
- **Minimal surface area** — Small, deliberate public API

## Requirements

- Python **≥ 3.14**

## Installation

```bash
pip install amasto
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv add amasto
```

## Quick start

```python
from amasto import Amasto

async def main() -> None:
    client = Amasto("https://mastodon.social", "YOUR_ACCESS_TOKEN")

    # Post a status
    status = await client.api.v1.statuses.post(body={"status": "Hello from amasto!"})

    # Read a single status by ID
    status = await client.api.v1.statuses["123456"].get()

    # List accounts the user is following
    accounts = await client.api.v1.accounts["123456"].following.get()
```

## Client

`Amasto` is the main entry point. It wraps an `httpx.AsyncClient` and automatically discovers the server's Mastodon version via the NodeInfo protocol on first use.

```python
from semver import Version

client = Amasto(
    "https://mastodon.social",   # base URL
    "YOUR_ACCESS_TOKEN",         # API key (Bearer token)
    mastodon_version=Version(4, 3, 0),  # optional: skip auto-detection
)
```

## Resources

API access uses a resource-based pattern where every endpoint is reachable as a chain of attribute accesses on the client:

```
client.api.v1.<resource>.<method>(params=..., body=...)
client.api.v1.<resource>["id"].<sub_resource>.<method>(...)
```

Each leaf node is an `HttpMethod` instance that is:
- **Async-callable** — `await method(params=..., body=...)` executes the HTTP request and returns a validated response.
- **Introspectable** — `.method`, `.path`, and `.requires` expose the HTTP verb, URL path, and minimum server version.
- **Test-friendly** — `.parse(data)` validates data against the response type without making HTTP calls.

### API v1 (`client.api.v1`)

| Resource | Access pattern |
|---|---|
| `accounts` | `.get`, `.post`, `.verify_credentials.get`, `["id"].get`, `["id"].follow.post`, … |
| `announcements` | `.get`, `["id"].dismiss.post`, `["id"].reactions["name"].put` |
| `apps` | `.post`, `.verify_credentials.get` |
| `blocks` | `.get` |
| `bookmarks` | `.get` |
| `conversations` | `.get`, `["id"].delete`, `["id"].read.post` |
| `custom_emojis` | `.get` |
| `directory` | `.get` |
| `domain_blocks` | `.get`, `.post`, `.delete` |
| `emails` | `.confirmations.post` |
| `endorsements` | `.get` |
| `favourites` | `.get` |
| `featured_tags` | `.get`, `.post`, `.suggestions.get`, `["id"].delete` |
| `follow_requests` | `.get`, `["id"].authorize.post`, `["id"].reject.post` |
| `followed_tags` | `.get` |
| `instance` | `.get`, `.peers.get`, `.activity.get`, `.rules.get`, `.domain_blocks.get`, … |
| `lists` | `.get`, `.post`, `["id"].get`, `["id"].put`, `["id"].delete`, `["id"].accounts.get` |
| `markers` | `.get`, `.post` |
| `media` | `["id"].get`, `["id"].put`, `["id"].delete` |
| `mutes` | `.get` |
| `notifications` | `.get`, `.clear.post`, `.unread_count.get`, `.requests.*`, `["id"].get`, `["id"].dismiss.post` |
| `polls` | `["id"].get`, `["id"].votes.post` |
| `preferences` | `.get` |
| `profile` | `.avatar.delete`, `.header.delete` |
| `push` | `.subscription.get`, `.subscription.post`, `.subscription.put`, `.subscription.delete` |
| `reports` | `.post` |
| `scheduled_statuses` | `.get`, `["id"].get`, `["id"].put`, `["id"].delete` |
| `search` | `.get` |
| `statuses` | `.get`, `.post`, `["id"].get`, `["id"].put`, `["id"].delete`, `["id"].context.get`, `["id"].favourite.post`, … |
| `suggestions` | `.get`, `["id"].delete` |
| `tags` | `["key"].get`, `["key"].follow.post`, `["key"].unfollow.post` |
| `timelines` | `.public.get`, `.home.get`, `.link.get`, `.direct.get`, `.tag["hashtag"].get`, `.list["id"].get` |
| `trends` | `.tags.get`, `.statuses.get`, `.links.get` |

### API v2 (`client.api.v2`)

| Resource | Access pattern |
|---|---|
| `filters` | `.get`, `.post`, `["id"].get/put/delete`, `["id"].keywords.get/post`, `.keywords["id"].get/put/delete` |
| `instance` | `.get` |
| `media` | `.post` |
| `notifications` | `.get`, `.unread_count.get`, `.policy.get/patch`, `["group_key"].dismiss.post`, `["group_key"].accounts.get` |
| `search` | `.get` |
| `suggestions` | `.get` |

### OEmbed (`client.api.oembed`)

| Access | Description |
|---|---|
| `.get` | Fetch oEmbed data for a status URL |

### OAuth (`client.oauth`)

| Resource | Access |
|---|---|
| `authorize` | `.get` — Authorization URL |
| `token` | `.post` — Obtain a token |
| `revoke` | `.post` — Revoke a token |
| `userinfo` | `.get` — Authenticated user info |

### Health (`client.health`)

| Access | Description |
|---|---|
| `.get` | Server health check |

## Models

Response models live under `amasto.models` and are re-exported from `amasto.models.v1` and `amasto.models.v2`. All models are frozen Pydantic `BaseModel` subclasses.

<details>
<summary>V1 models</summary>

`Account`, `AccountRole`, `AccountSource`, `AccountWarning`, `Announcement`, `AnnouncementAccount`, `AnnouncementStatus`, `Appeal`, `Application`, `AsyncRefresh`, `Context`, `Conversation`, `CredentialAccount`, `CredentialApplication`, `CustomEmoji`, `DomainBlock`, `EncryptedMessage`, `Error`, `ExtendedDescription`, `FamiliarFollowers`, `FeaturedTag`, `Field`, `IdentityProof`, `InstanceStats`, `InstanceUrls`, `List`, `Marker`, `MediaAttachment`, `MutedAccount`, `Notification`, `NotificationRequest`, `Poll`, `PollOption`, `Preferences`, `PreviewCard`, `PreviewCardAuthor`, `PrivacyPolicy`, `Quote`, `QuoteApproval`, `Reaction`, `Relationship`, `RelationshipSeveranceEvent`, `Report`, `Role`, `Rule`, `ScheduledStatus`, `ScheduledStatusParams`, `ScheduledStatusParamsPoll`, `Search`, `ShallowQuote`, `Status`, `StatusEdit`, `StatusEditPoll`, `StatusEditPollOption`, `StatusMention`, `StatusSource`, `StatusTag`, `Suggestion`, `Tag`, `TagHistory`, `TermsOfService`, `Token`, `Translation`, `TranslationAttachment`, `TranslationPoll`, `TranslationPollOption`, `TrendsLink`, `WebPushAlerts`, `WebPushSubscription`

</details>

<details>
<summary>V2 models</summary>

`Filter`, `FilterKeyword`, `FilterResult`, `FilterStatus`, `Instance`, `InstanceConfiguration`, `InstanceConfigurationAccounts`, `InstanceConfigurationMediaAttachments`, `InstanceConfigurationPolls`, `InstanceConfigurationStatuses`, `InstanceConfigurationTimelinesAccess`, `InstanceConfigurationTimelinesFeedAccess`, `InstanceConfigurationTranslation`, `InstanceConfigurationUrls`, `InstanceConfigurationVapid`, `InstanceContact`, `InstanceIcon`, `InstanceRegistrations`, `InstanceThumbnail`, `InstanceThumbnailVersions`, `InstanceUsage`, `InstanceUsageUsers`, `NotificationPolicy`, `NotificationPolicySummary`

</details>

## Version awareness

Model fields annotated with `since("x.y.z")` resolve to `Unsupported` when the connected server is older than the specified version, so your code can safely handle missing data:

```python
from amasto.models import Status
from amasto._version import Unsupported

if not isinstance(status.poll, Unsupported):
    print(status.poll)
```

Endpoints can also declare `requires="x.y.z"` to indicate the minimum server version.

## Dependencies

| Package | Purpose |
|---|---|
| [httpx](https://www.python-httpx.org/) ≥ 0.28.1 | Async HTTP client |
| [pydantic](https://docs.pydantic.dev/) ≥ 2.12.5 | Response validation and models |
| [semver](https://python-semver.readthedocs.io/) ≥ 3.0.4 | Server version parsing |

## License

See [LICENSE](LICENSE) for details.
