Skip to content

Commit d38ea70

Browse files
authored
Merge pull request #63 from fedi-libs/feat/new-client
feat: Unified Client
2 parents ac8667a + 5335710 commit d38ea70

35 files changed

Lines changed: 1709 additions & 892 deletions

.gemini/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"contextFileName": "AGENTS.md"
3+
}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,5 @@ __marimo__/
208208

209209
private_key*.pem
210210

211-
devel/
211+
devel/
212+
data/

.pre-commit-config.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,3 @@ repos:
1414
language: system
1515
types_or: [python, pyi]
1616
require_serial: true
17-
18-
- id: pyrefly-check
19-
name: Pyrefly (type checking)
20-
entry: pyrefly check
21-
language: system
22-
pass_filenames: false

AGENTS.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Agent Guidelines for apkit
2+
3+
This file provides guidelines for AI agents working on the apkit codebase.
4+
5+
## Project Overview
6+
7+
apkit is a modern, fast toolkit for building ActivityPub-based applications with Python. It uses FastAPI for the server, supports async HTTP clients, and handles ActivityPub models, HTTP signatures, and Fediverse protocols.
8+
9+
## Build, Test, and Lint Commands
10+
11+
### Package Manager
12+
This project uses `uv` as the package manager.
13+
14+
### Running Tests
15+
```bash
16+
# Run all tests
17+
uv run pytest
18+
19+
# Run tests with coverage
20+
uv run pytest --cov=src/apkit
21+
22+
# Run a single test
23+
uv run pytest tests/path/to/test_file.py::test_function_name
24+
25+
# Run tests for a specific module
26+
uv run pytest tests/client/
27+
```
28+
29+
### Linting and Formatting
30+
```bash
31+
# Check all files with ruff
32+
uv run ruff check .
33+
34+
# Check and auto-fix issues
35+
uv run ruff check --fix .
36+
37+
# Format all files
38+
uv run ruff format .
39+
40+
# Type checking with pyrefly
41+
uv run pyrefly check .
42+
```
43+
44+
### Pre-commit Hooks
45+
```bash
46+
# Install pre-commit hooks
47+
pre-commit install
48+
49+
# Run all hooks manually
50+
pre-commit run --all-files
51+
```
52+
53+
## Code Style Guidelines
54+
55+
### Imports
56+
1. **Standard library** imports first (e.g., `import json`, `from typing import ...`)
57+
2. **Third-party** imports second (e.g., `import apmodel`, `from fastapi import ...`)
58+
3. **Local/apkit** imports last (e.g., `from ..types import ActorKey`)
59+
4. Use **absolute imports** for external dependencies, **relative imports** for internal modules
60+
5. Sort imports with `collections.abc` before `typing`
61+
62+
Example:
63+
```python
64+
import json
65+
import re
66+
from collections.abc import Iterable, Mapping
67+
from typing import Any, Dict, List, Optional, TypeVar
68+
69+
import aiohttp
70+
import httpx
71+
from apmodel.types import ActivityPubModel
72+
73+
from ..types import ActorKey
74+
from .models import Resource
75+
```
76+
77+
### Formatting
78+
- **Line length**: 88 characters (Black-compatible)
79+
- **Indent**: 4 spaces
80+
- **Quotes**: Double quotes for strings
81+
- Follow **ruff** configuration in `pyproject.toml`
82+
83+
### Type Hints
84+
- **Always use type hints** for function parameters and return types
85+
- Use `from typing import ...` imports at the top
86+
- Use `ParamSpec` and `TypeVar` for generic types
87+
- For Python 3.10+, use `X | Y` syntax instead of `Optional` or `Union` where appropriate
88+
89+
Example:
90+
```python
91+
from typing import Optional, TypeVar
92+
93+
T = TypeVar("T")
94+
95+
def fetch(url: str, headers: Optional[dict] = None) -> dict | None:
96+
...
97+
```
98+
99+
### Naming Conventions
100+
- **Classes**: `PascalCase` (e.g., `ActivityPubClient`, `WebfingerResult`)
101+
- **Functions/Methods**: `snake_case` (e.g., `fetch_actor`, `build_webfinger_url`)
102+
- **Constants**: `SCREAMING_SNAKE_CASE`
103+
- **Private methods/vars**: Prefix with underscore (e.g., `__fetch_actor`, `_client`)
104+
- **Type variables**: Single uppercase letter (e.g., `T`, `P`, `R`)
105+
106+
### Data Classes
107+
- Use `@dataclass(frozen=True)` for immutable models
108+
- Use regular `@dataclass` for mutable response wrappers
109+
- Document classes and methods with docstrings
110+
111+
Example:
112+
```python
113+
@dataclass(frozen=True)
114+
class Link:
115+
"""Represents a link in a WebFinger response."""
116+
rel: str
117+
type: str | None
118+
href: str | None
119+
```
120+
121+
### Error Handling
122+
- Use **specific exceptions** (e.g., `ValueError`, `TypeError`)
123+
- Raise with descriptive messages
124+
- Use custom exceptions in `exceptions.py` for domain-specific errors
125+
- Use `match` statements for pattern matching (Python 3.11+)
126+
127+
Example:
128+
```python
129+
match headers:
130+
case Mapping() as m:
131+
items = m.items()
132+
case None:
133+
items = []
134+
case _:
135+
raise TypeError(f"Unsupported header type: {type(headers)}")
136+
```
137+
138+
### Testing
139+
- Use **pytest** for testing
140+
- Write **descriptive test names** (e.g., `test_build_webfinger_url`)
141+
- Use pytest classes for grouping related tests (e.g., `class TestResource:`)
142+
- Mock external dependencies when appropriate
143+
144+
### Project Structure
145+
```
146+
src/apkit/
147+
├── __init__.py # Package exports
148+
├── _version.py # Version info (auto-generated)
149+
├── abc/ # Abstract base classes
150+
├── cache.py # Caching utilities
151+
├── client/ # HTTP client implementation
152+
│ ├── __init__.py
153+
│ ├── base/ # Base context managers
154+
│ ├── client.py # Main ActivityPubClient
155+
│ ├── exceptions.py # Client exceptions
156+
│ ├── models.py # Data models
157+
│ └── types.py # Type definitions
158+
├── config.py # Configuration
159+
├── helper/ # Helper utilities
160+
├── kv/ # Key-value store implementations
161+
├── models/ # ActivityPub model exports
162+
├── nodeinfo/ # NodeInfo implementation
163+
├── server/ # FastAPI server components
164+
│ ├── app.py # ActivityPubServer
165+
│ ├── routes/ # Route handlers
166+
│ ├── responses.py # Response classes
167+
│ └── types.py # Server types
168+
└── types.py # Common types
169+
```
170+
171+
## Important Notes
172+
173+
- Python **3.11+** is required
174+
- **Type hints are mandatory** for all new code
175+
- Follow the **KISS principle** - Keep It Simple, Stupid
176+
- **Conventional Commits** for commit messages (e.g., `feat:`, `fix:`, `docs:`)
177+
- The codebase is **not stable** - API changes may break backward compatibility
178+
179+
## Dependencies
180+
181+
Key external dependencies:
182+
- `apmodel>=0.5.1` - ActivityPub models
183+
- `apsig>=0.6.0` - HTTP signatures
184+
- `fastapi>=0.116.1` - Web framework (optional, server extra)
185+
- `aiohttp>=3.13.3` - Async HTTP client
186+
- `httpx>=0.28.1` - Sync HTTP client
187+
188+
## Before Submitting
189+
190+
1. Run `uv run ruff check --fix .` to auto-fix linting issues
191+
2. Run `uv run ruff format .` to format code
192+
3. Run `uv run pyrefly check .` to verify type hints
193+
4. Run `uv run pytest` to ensure all tests pass
194+
5. Ensure imports are organized correctly

FEDERATION.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server)
66
- [WebFinger](https://webfinger.net/)
7-
- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
7+
- [draft-cavage-http-signatures-12](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
88
- [Linked Data Signatures 1.0](https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/)
99
- [NodeInfo](https://nodeinfo.diaspora.software/)
10+
- [RFC 9421: HTTP Message Signatures](https://datatracker.ietf.org/doc/html/rfc9421)
1011

1112
## Supported FEPs
1213

examples/send_message.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,9 @@ async def send_note(recepient: str) -> None:
117117
cc=["https://www.w3.org/ns/activitystreams#Public"],
118118
tag=[
119119
Mention(
120-
href=target_actor.url,
121-
name=f"@{target_actor.preferred_username}"
120+
href=target_actor.url, name=f"@{target_actor.preferred_username}"
122121
)
123-
]
122+
],
124123
)
125124

126125
# Create activity

pyproject.toml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ server = [
3131
"fastapi>=0.116.1",
3232
"uvicorn>=0.35.0",
3333
]
34+
speed = [
35+
"google-re2>=1.1.20251105",
36+
"lxml>=6.0.2"
37+
]
3438

3539
[tool.uv]
3640
default-groups = "all"
@@ -51,13 +55,16 @@ version-file = "src/apkit/_version.py"
5155

5256
[dependency-groups]
5357
dev = [
58+
"aioresponses>=0.7.8",
5459
"coverage>=7.10.7",
5560
"pytest>=8.4.1",
5661
"pytest-asyncio>=1.3.0",
5762
"pytest-cov>=7.0.0",
63+
"respx>=0.22.0",
5864
"pyrefly>=0.46.0",
5965
"ruff>=0.14.10",
60-
"prek>=0.3.3"
66+
"prek>=0.3.3",
67+
"pre-commit>=4.5.1",
6168
]
6269
docs = [
6370
"mkdocs>=1.6.1",
@@ -113,12 +120,17 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
113120

114121
[tool.pyrefly]
115122
project-includes = [
116-
"src/**/*.py*",
117-
"tests/**/*.py*",
123+
"src/**/*.py",
124+
"tests/**/*.py",
118125
]
119126
project-excludes = [
120-
"scripts/**/*.py"
127+
"scripts/**/*.py",
128+
"**/.*",
129+
"**/*venv/**",
121130
]
131+
search-path = ["src"]
132+
ignore-errors-in-generated-code = true
133+
122134

123135
[tool.ruff.format]
124136
quote-style = "double"

src/apkit/_version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
commit_id: COMMIT_ID
2929
__commit_id__: COMMIT_ID
3030

31-
__version__ = version = '0.3.8.post1.dev10+g892881c3c'
32-
__version_tuple__ = version_tuple = (0, 3, 8, 'post1', 'dev10', 'g892881c3c')
31+
__version__ = version = '0.3.8.post1.dev52+gd0f42cdc1'
32+
__version_tuple__ = version_tuple = (0, 3, 8, 'post1', 'dev52', 'gd0f42cdc1')
3333

3434
__commit_id__ = commit_id = None

src/apkit/client/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
from .models import WebfingerResult
2-
from .models import Resource as WebfingerResource
1+
from .client import ActivityPubClient
32
from .models import Link as WebfingerLink
3+
from .models import Resource as WebfingerResource
4+
from .models import WebfingerResult
45

5-
__all__ = ["WebfingerResult", "WebfingerResource", "WebfingerLink"]
6+
__all__ = [
7+
"ActivityPubClient",
8+
"WebfingerResult",
9+
"WebfingerResource",
10+
"WebfingerLink",
11+
]

0 commit comments

Comments
 (0)