Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4e50de9
build!: migrate from Hatch to uv package manager
marclove Sep 9, 2025
a509ca3
chore: add llmc.toml to .gitignore
marclove Sep 9, 2025
7edfbe2
feat!: improve error handling and Python compatibility
marclove Sep 9, 2025
987a6d9
refactor!: split API module into separate files for better organization
marclove Sep 9, 2025
3705b54
ci: add Python matrix testing across versions 3.8-3.12
marclove Sep 9, 2025
d6bacd1
feat(api): add configurable timeout and base_url parameters
marclove Sep 9, 2025
6e9aa5c
feat(api): add retry mechanism with exponential backoff
marclove Sep 9, 2025
2964482
refactor(api): introduce Transport and AccountsService layers
marclove Sep 9, 2025
4b1228f
refactor: extract threads functionality into dedicated service
marclove Sep 9, 2025
9908bd0
refactor: extract media operations into dedicated MediaService
marclove Sep 9, 2025
a70608e
refactor(api): extract insights and moderation logic into dedicated s…
marclove Sep 9, 2025
32343b1
feat(api): add type annotations and documentation for endpoints
marclove Sep 9, 2025
35326f6
feat: add async iterators for paginated API responses
marclove Sep 9, 2025
8233f60
feat(api): add request_options parameter to all API methods
marclove Sep 9, 2025
23b5f1b
feat: add optional Pydantic v2 models for response validation
marclove Sep 9, 2025
86066e3
docs: add docstrings to API client methods and create insights model …
marclove Sep 9, 2025
e35f971
docs: add doctest support and examples
marclove Sep 9, 2025
0c94c16
feat: add centralized config management and improve type safety
marclove Sep 9, 2025
d476fc3
ci: update uv sync command and standardize quotes
marclove Sep 9, 2025
44aad46
refactor!: replace generic dict with RequestOptions type
marclove Sep 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Setup uv
uses: astral-sh/setup-uv@v4
- name: Pin Python
run: uv python pin ${{ matrix.python-version }}
- name: Sync dependencies (dev + extras)
run: uv sync --extra dev
- name: Lint (ruff)
run: uv run ruff check --fix .
- name: Type check (pyright)
run: uv run pyright .
- name: Run tests (exclude smoke)
env:
CI: "1"
run: uv run pytest -m "not smoke"
# - name: Coverage HTML (optional)
# run: uv run coverage html
12 changes: 7 additions & 5 deletions .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ jobs:
uses: actions/configure-pages@v5
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Hatch
uses: pypa/hatch@install
python-version: "3.12"
- name: Setup uv
uses: astral-sh/setup-uv@v4
- name: Sync dependencies (dev + extras)
run: uv sync --extra dev
- name: Build Sphinx docs
run: |
hatch run docs:build
uv run sphinx-build -b html docs/source docs/build/html
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './docs/build/html'
path: "./docs/build/html"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
39 changes: 0 additions & 39 deletions .github/workflows/hatch.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,4 @@ flask_session/

# PEM files
*.pem
/llmc.toml
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.371
hooks:
- id: pyright
1 change: 1 addition & 0 deletions .prototools
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uv = "0.8.15"
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
38 changes: 38 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Repository Guidelines

## Project Structure & Module Organization
- Source code lives in `src/pythreads/` (e.g., `api.py`, `threads.py`, `credentials.py`, `configuration.py`). Public imports resolve from the `pythreads` package.
- Tests are under `tests/` and named `test_*.py`. Networked smoke tests are marked with `@pytest.mark.smoke`.
- Documentation uses Sphinx in `docs/` and builds to `docs/build/html`.
- Example environment variables live in `.env.template`.

## Build, Test, and Development Commands
- Tooling: use `uv` (fast package manager). First time: `uv sync --dev`.
- Lint and types: `uv run ruff check --fix .` and `uv run pyright .`.
- Unit tests (no network): `CI=1 uv run pytest -m "not smoke"`.
- Smoke tests (real API): `uv run pytest -m "smoke"` (requires `THREADS_SMOKE_TEST_USER_ID` and `THREADS_SMOKE_TEST_TOKEN`).
- Coverage: `uv run pytest -m "not smoke" --cov=src/pythreads` then `uv run coverage html`.
- Docs: `uv run sphinx-build -b html docs/source docs/build/html`.

## Coding Style & Naming Conventions
- Python 3.8+ with type hints; use `dataclasses` for value objects and `Enum/StrEnum` for constants where appropriate.
- Indentation is 4 spaces. Follow PEP 8: modules/functions `snake_case`, classes `PascalCase`, constants `UPPER_CASE`.
- Run ruff and pyright locally before committing (see commands above).

## Testing Guidelines
- Framework: `pytest` with markers configured in `pyproject.toml`. Default test runs exclude `smoke`.
- Name tests `tests/test_*.py`. Prefer small, isolated tests; avoid network except in `@pytest.mark.smoke`.
- Aim to maintain high coverage (~90%+). Add tests alongside new features and bug fixes.
- For smoke tests, see `tests/server.py` and configure env from `.env.template`.

## Commit & Pull Request Guidelines
- Commits: short, imperative subject; optional scope prefix (e.g., `api:`, `pip:`). Add a body explaining the why when non-trivial.
- PRs: include a clear description, linked issues, and notes on tests/docs. Update docs when changing public APIs. CI must pass (lint + tests).

## Security & Configuration Tips
- Do not commit real tokens, certs, or keys. Use `.env.template` as a reference and local env vars for secrets.
- Local OAuth requires SSL: set `THREADS_SSL_CERT_FILEPATH` and `THREADS_SSL_KEY_FILEPATH` for flows used by `threads.py`.

## Agent-Specific Instructions
- Keep changes minimal and targeted; avoid unrelated refactors.
- If you change API signatures or behavior, update tests and docs in the same PR.
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

PyThreads uses **uv** for package management and development:

- **Setup**: `uv sync --dev` - Install all dependencies including dev extras
- **Lint**: `uv run ruff check --fix .` - Run linter with auto-fix
- **Type checking**: `uv run pyright .` - Run type checking
- **Unit tests (no network)**: `CI=1 uv run pytest -m "not smoke"` - Run offline tests only
- **All tests**: `uv run pytest` - Run all tests including network-dependent smoke tests
- **Documentation**: `uv run sphinx-build -b html docs/source docs/build/html` - Build docs
- **Coverage**: `uv run pytest --cov` - Run tests with coverage report

## Architecture

PyThreads is a Python wrapper for Meta's Threads API with a layered architecture:

### Core Authentication Layer (`src/pythreads/`)
- **`threads.py`**: Main `Threads` class for OAuth2 flow - handles authorization URL generation, token exchange, and refresh
- **`credentials.py`**: `Credentials` class for token management with expiration checking
- **`configuration.py`**: `Configuration` class for app settings (app_id, secret, redirect_uri, scopes)

### API Client Layer (`src/pythreads/api/`)
- **`client.py`**: Main `API` class - async HTTP client with session management, retries, and service orchestration
- **`transport.py`**: `Transport` class - low-level HTTP transport with URL building and request handling
- **Service endpoints** in `endpoints/`: Modular services for different API domains:
- `accounts.py`: User profile and publishing limits
- `threads.py`: Thread/post listing with async iterators for pagination
- `media.py`: Container creation, status checking, and publishing
- `insights.py`: Analytics and metrics
- `moderation.py`: Reply management (hide/unhide)

### Type System (`src/pythreads/api/`)
- **`types.py`**: Enums, dataclasses, and constants for API parameters and responses
- **`models.py`**: Optional Pydantic v2 models for response validation (requires `[models]` extra)
- **`errors.py`**: Custom exception classes for API and HTTP errors

### Key Design Patterns
- **Async context manager**: `API` object manages aiohttp sessions automatically
- **Service delegation**: Main `API` class delegates to specialized service classes
- **Optional session management**: Users can provide their own aiohttp session or let the library manage it
- **Retry logic**: Built-in exponential backoff for transient HTTP errors
- **Paginated iterators**: Async generators for seamless multi-page data fetching

### Publishing Workflow
Threads API requires a two/three-step publishing process:
1. Create container(s) for media/text
2. Wait for video processing (if applicable)
3. Publish container

For carousels: Create individual media containers → Create carousel container → Publish carousel

## Environment Variables
Required for API functionality:
- `THREADS_APP_ID`: Meta app ID
- `THREADS_API_SECRET`: Meta app secret
- `THREADS_REDIRECT_URI`: OAuth2 redirect URI

Optional:
- `THREADS_GRAPH_API_VERSION`: API version (defaults to latest)
- `THREADS_SSL_CERT_FILEPATH` / `THREADS_SSL_KEY_FILEPATH`: SSL client certificates

## Testing Strategy
- **Unit tests**: Mock-based tests in `tests/` directory
- **Smoke tests**: Real API integration tests (marked with `@pytest.mark.smoke`)
- **CI environment**: Set `CI=1` to skip network-dependent smoke tests
- **Coverage**: Maintained at 93%+ as shown in README badge

## Optional Dependencies
- **`[models]`**: Pydantic v2 models for response validation
- **`[dev]`**: All development tools (pytest, ruff, pyright, sphinx, etc.)
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ follows [Semantic Versioning](https://semver.org/).
- [Authentication & Authorization](#authentication--authorization)
- [Making Requests](#making-requests)
- [API Methods](#api-methods)
- [Development](#development)
- [Roadmap](#roadmap)
- [License](#license)

Expand Down Expand Up @@ -248,6 +249,20 @@ async with API(self.credentials) as api:
# carousel_id == result_id
```

### Iterating over pages
Use the built-in async iterators to page through results without manually handling cursors:

```python
async with API(credentials) as api:
# Iterate a user's threads (2 pages of 50 items)
async for t in api.threads_iter(per_page=50, page_limit=2):
print(t["id"], t.get("text"))

# Iterate replies for a thread
async for r in api.replies_iter(thread_id="1234567890", per_page=25):
print(r["id"], r.get("text"))
```

A few key things to point out above:

1. Creating media containers requires you to put the image or video at a
Expand All @@ -272,3 +287,37 @@ process:
## License

`pythreads` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.

## Development

Use uv for a fast, reproducible workflow:

- Setup: `uv sync --dev`
- Lint and types: `uv run ruff check --fix .` and `uv run pyright .`
- Unit tests (no network): `CI=1 uv run pytest -m "not smoke"`
- Docs: `uv run sphinx-build -b html docs/source docs/build/html`

Notes:
- The `API` client uses a default 30s timeout and raises `ThreadsHTTPError` on non-2xx HTTP responses.
- You can override defaults via `API(credentials, timeout=10, base_url="https://graph.threads.net/")`.
- Per-call overrides are also supported via `request_options`:

```python
async with API(credentials) as api:
# Retry this specific call up to 3 times with a 10s timeout
resp = await api.threads(request_options={"retries": 3, "timeout": 10.0})
```

Using Pydantic models (optional)
- Install with extras: `uv pip install ".[models]"` or `pip install pythreads[models]`
- Validate responses with Pydantic v2 models:

```python
from pythreads.api.models import InsightsResponseModel

async with API(credentials) as api:
raw = await api.insights("someid")
model = InsightsResponseModel.model_validate(raw)
for item in (model.data or []):
print(item.name, item.period)
```
31 changes: 31 additions & 0 deletions docs/source/architecture.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Architecture Overview
=====================

Package Layout
--------------

- ``pythreads.api.client``: public API facade. Owns credentials, session
management, and wires endpoint services. Backwards-compatible surface.
- ``pythreads.api.transport``: HTTP layer. Builds URLs (via
``Threads.build_graph_api_url``), validates access tokens, applies timeouts,
retries with backoff, and normalizes JSON/text error handling.
- ``pythreads.api.endpoints.*``: endpoint services grouped by responsibility
(accounts, threads, media, insights, moderation). Services build params and
delegate HTTP to ``Transport``.
- ``pythreads.api.types``: enums, dataclasses, constants, and lightweight
TypedDicts for common responses.
- ``pythreads.api.utils``: small helpers for timestamp and parameter casting.
- ``pythreads.threads``: OAuth and URL helpers for the Threads Graph API.

Design Principles
-----------------

- Single Responsibility: each service focuses on one domain area and is easy
to test. The facade composes services; it doesn’t duplicate their logic.
- Consistent Transport: all HTTP goes through ``Transport`` for timeouts,
retries, and error handling.
- Stronger Types: narrow return types where practical using TypedDicts;
preserve dict-based flexibility where the schema is broad.
- Progressive Enhancement: iterators and options (timeouts, base_url, retries)
are opt-in and non-breaking.

1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"myst_parser",
"sphinx.ext.napoleon",
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
]

templates_path = ["_templates"]
Expand Down
28 changes: 28 additions & 0 deletions docs/source/doctests.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Doctests
========

These quick doctests validate small, self-contained pieces of functionality.

Utils
-----

.. doctest::

>>> from datetime import datetime, timezone
>>> from pythreads.api.utils import ts_to_str, iso_date_or_str
>>> ts_to_str(datetime(1970, 1, 1, tzinfo=timezone.utc))
'0'
>>> iso_date_or_str('2024-01-01')
'2024-01-01'

Pydantic Models
---------------

.. doctest::

>>> from pythreads.api.models import InsightsResponseModel
>>> raw = {"data": [{"name": "likes", "period": "day", "values": [{"value": 1}]}]}
>>> model = InsightsResponseModel.model_validate(raw)
>>> model.data[0].name
'likes'

2 changes: 2 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Welcome to PyThreads' documentation!
:caption: Contents:

pythreads
doctests
architecture

.. include:: ../../README.md
:parser: myst_parser.sphinx_
Expand Down
Loading
Loading