fopost-fastapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .mypy_cache/
13
+ .coverage
14
+ htmlcov/
15
+ uv.lock
16
+ .idea/
17
+ .vscode/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Porter Bridge, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.5
2
+ Name: fopost-fastapi
3
+ Version: 0.1.0
4
+ Summary: Official FastAPI integration for the FoPost API. Dependency injection, settings, webhooks, and error handling on top of the fopost SDK.
5
+ Project-URL: Homepage, https://fopost.com
6
+ Project-URL: Documentation, https://fopost.com/docs
7
+ Project-URL: Repository, https://github.com/fopost/fopost-fastapi
8
+ Project-URL: Issues, https://github.com/fopost/fopost-fastapi/issues
9
+ Project-URL: Support, https://fopost.com/contact
10
+ Author: FoPost, Porter Bridge, LLC
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,fastapi,fopost,publishing,scheduling,sdk,social-media,webhooks
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: fastapi>=0.110
26
+ Requires-Dist: fopost<1.0,>=0.1
27
+ Requires-Dist: pydantic-settings>=2
28
+ Requires-Dist: pydantic>=2
29
+ Provides-Extra: dev
30
+ Requires-Dist: httpx>=0.27; extra == 'dev'
31
+ Requires-Dist: mypy>=1.11; extra == 'dev'
32
+ Requires-Dist: pytest>=8; extra == 'dev'
33
+ Requires-Dist: ruff>=0.6; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # fopost-fastapi
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/fopost-fastapi.svg)](https://pypi.org/project/fopost-fastapi/)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/fopost-fastapi.svg)](https://pypi.org/project/fopost-fastapi/)
40
+ [![CI](https://github.com/fopost/fopost-fastapi/actions/workflows/ci.yml/badge.svg)](https://github.com/fopost/fopost-fastapi/actions/workflows/ci.yml)
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
42
+
43
+ Official FastAPI integration for the [FoPost](https://fopost.com) API. Schedule and publish to
44
+ +30 social platforms from your code.
45
+
46
+ This is a **thin wrapper**. Every request, model, retry, and error type lives in the
47
+ [`fopost`](https://pypi.org/project/fopost/) SDK — this package wires that client into FastAPI's
48
+ idioms: settings, dependency injection, a webhook receiver, and an exception handler.
49
+
50
+ ```bash
51
+ pip install fopost-fastapi
52
+ ```
53
+
54
+ Requires Python 3.10 or newer, FastAPI 0.110 or newer, and pydantic v2.
55
+
56
+ > **0.x release.** The public API is still settling and minor versions may contain breaking
57
+ > changes. Pin an exact version if that matters to you.
58
+
59
+ ## Quick start
60
+
61
+ ```python
62
+ from fastapi import FastAPI
63
+
64
+ from fopost_fastapi import FoPostDep, install_exception_handlers, setup_fopost
65
+
66
+ app = FastAPI()
67
+ setup_fopost(app) # one client, created at startup, closed at shutdown
68
+ install_exception_handlers(app)
69
+
70
+
71
+ @app.get("/workspaces")
72
+ def workspaces(fopost: FoPostDep):
73
+ return fopost.workspaces.list()
74
+ ```
75
+
76
+ `FoPostDep` is `Annotated[Fopost, Depends(get_client)]`. The client is built **once** when the
77
+ app starts and shared by every request — the dependency looks it up, it never constructs one.
78
+
79
+ If your app already has its own lifespan, `setup_fopost` wraps it rather than replacing it.
80
+ To wire FoPost through the lifespan directly instead:
81
+
82
+ ```python
83
+ from fopost_fastapi import fopost_lifespan
84
+
85
+ app = FastAPI(lifespan=fopost_lifespan())
86
+ ```
87
+
88
+ ## Settings
89
+
90
+ `FoPostSettings` is a `pydantic-settings` model reading `FOPOST_`-prefixed environment variables
91
+ (and a `.env` file, if present).
92
+
93
+ | Field | Environment variable | Default |
94
+ | :--- | :--- | :--- |
95
+ | `api_key` | `FOPOST_API_KEY` | — (required) |
96
+ | `base_url` | `FOPOST_BASE_URL` | `https://api.fopost.com/v1` |
97
+ | `timeout` | `FOPOST_TIMEOUT` | `30.0` seconds |
98
+ | `max_retries` | `FOPOST_MAX_RETRIES` | `3` attempts |
99
+ | `default_workspace_id` | `FOPOST_DEFAULT_WORKSPACE_ID` | — |
100
+ | `webhook_secret` | `FOPOST_WEBHOOK_SECRET` | — |
101
+
102
+ Create an API key at <https://app.fopost.com/api-keys>. It is sent as `X-API-Key`.
103
+
104
+ Pass settings explicitly when you would rather not read the environment:
105
+
106
+ ```python
107
+ setup_fopost(app, FoPostSettings(api_key="fp_...", base_url="https://api.fopost.com/v1"))
108
+ ```
109
+
110
+ `FoPostSettingsDep` injects the resolved settings into a route, which is how you reach
111
+ `default_workspace_id`.
112
+
113
+ ## Sync or async? The SDK is synchronous
114
+
115
+ The `fopost` package ships a **blocking** client only — there is no async variant, and this
116
+ package deliberately does not write one. That leaves two shapes:
117
+
118
+ ```python
119
+ # A `def` route — FastAPI already runs it in a worker thread. Call the SDK directly.
120
+ @app.get("/accounts")
121
+ def accounts(fopost: FoPostDep, settings: FoPostSettingsDep):
122
+ return fopost.accounts.list(workspace_id=settings.default_workspace_id)
123
+
124
+
125
+ # An `async def` route — the call must leave the event loop, or it stalls the server.
126
+ from fopost_fastapi import run_fopost
127
+
128
+ @app.post("/posts")
129
+ async def create(fopost: FoPostDep, settings: FoPostSettingsDep):
130
+ return await run_fopost(
131
+ fopost.posts.create,
132
+ workspace_id=settings.default_workspace_id,
133
+ content="Hello from FastAPI",
134
+ accounts=["<account id>"],
135
+ )
136
+ ```
137
+
138
+ `run_fopost` is a thin wrapper over `fastapi.concurrency.run_in_threadpool`. Never call the SDK
139
+ straight from an `async def` route: a 30-second timeout would block every other request.
140
+
141
+ ## Receiving webhooks
142
+
143
+ ```python
144
+ from fopost_fastapi import WebhookEvent, on_event, webhook_router
145
+
146
+ app.include_router(webhook_router, prefix="/fopost") # POST /fopost/webhooks
147
+
148
+
149
+ @on_event("post.published")
150
+ async def published(event: WebhookEvent) -> None:
151
+ print(event.data["id"], event.timestamp)
152
+
153
+
154
+ @on_event("post.failed")
155
+ def failed(event: WebhookEvent) -> None: # a `def` handler runs in a worker thread
156
+ ...
157
+ ```
158
+
159
+ Point a FoPost webhook at `https://<your host>/fopost/webhooks` and put its secret in
160
+ `FOPOST_WEBHOOK_SECRET`.
161
+
162
+ FoPost signs each delivery with HMAC-SHA256 over the **raw** request body using that webhook's
163
+ secret, and sends it as `X-FoPost-Signature: sha256=<hex>` alongside `X-FoPost-Event` and
164
+ `X-FoPost-Delivery`. The router reads the raw bytes before any parsing, compares with
165
+ `hmac.compare_digest`, and answers **401** on a mismatch or a missing header — no handler runs.
166
+ With no secret configured at all it answers 500 rather than accepting unverifiable traffic.
167
+
168
+ Events: `post.published`, `post.failed`, `post.partially_failed`, `delivery.published`,
169
+ `delivery.failed`, `delivery.delayed`, `account.health_changed`. `@on_event()` with no argument
170
+ subscribes to all of them.
171
+
172
+ Run several receivers, or keep the secret out of the environment, by building your own router:
173
+
174
+ ```python
175
+ from fopost_fastapi import FoPostWebhookRouter
176
+
177
+ router = FoPostWebhookRouter(secret="whsec_...", path="/callbacks")
178
+ app.include_router(router, prefix="/fopost")
179
+ ```
180
+
181
+ `sign_payload(body, secret)` and `verify_webhook_signature(body, header, secret)` are exported
182
+ if you need to verify a delivery somewhere else.
183
+
184
+ ## Error handling
185
+
186
+ `install_exception_handlers(app)` turns an SDK exception into the status the FoPost API actually
187
+ answered with, instead of a 500 and a stack trace.
188
+
189
+ | SDK error | Response |
190
+ | :--- | :--- |
191
+ | `AuthenticationError` (401), `PermissionDeniedError` (403), `NotFoundError` (404) | the same status |
192
+ | `PaymentRequiredError` (402) | 402, body keeps `upgrade_url` |
193
+ | `RateLimitError` (429) | 429 with a `Retry-After` header |
194
+ | any 4xx | the same status |
195
+ | any 5xx or transport failure | 502 — your app is fine, its dependency is not |
196
+
197
+ The body is the API's own envelope: `{"error": "<machine code>", "message": "<human text>"}`.
198
+
199
+ Retries are the SDK's job, not this package's: a 429 is retried up to `max_retries` attempts,
200
+ honouring `Retry-After`, before the error ever reaches the handler.
201
+
202
+ ## The rest of the API
203
+
204
+ Everything you can call on the injected client — `posts`, `accounts`, `workspaces`, `labels`,
205
+ `ai`, and the `request()` escape hatch for endpoints the SDK does not wrap — is documented in the
206
+ [`fopost` SDK](https://github.com/fopost/fopost-python). This package adds no resources of its own
207
+ and stores nothing.
208
+
209
+ ## Example
210
+
211
+ [`examples/main.py`](examples/main.py) is a complete app: settings from the environment, the
212
+ injected client in both a `def` and an `async def` route, and the webhook receiver.
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ python -m venv .venv && source .venv/bin/activate
218
+ pip install -e '.[dev]'
219
+ pytest
220
+ ruff check . && ruff format --check .
221
+ mypy
222
+ ```
223
+
224
+ The suite is fully offline — it stubs the SDK's HTTP transport and never reaches the network.
225
+
226
+ ## Links
227
+
228
+ - Documentation — <https://fopost.com/docs>
229
+ - Python SDK — <https://github.com/fopost/fopost-python>
230
+ - Issues — <https://github.com/fopost/fopost-fastapi/issues>
231
+ - Support — <https://fopost.com/contact>
232
+
233
+ MIT licensed. Copyright (c) 2026 Porter Bridge, LLC.
@@ -0,0 +1,198 @@
1
+ # fopost-fastapi
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/fopost-fastapi.svg)](https://pypi.org/project/fopost-fastapi/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/fopost-fastapi.svg)](https://pypi.org/project/fopost-fastapi/)
5
+ [![CI](https://github.com/fopost/fopost-fastapi/actions/workflows/ci.yml/badge.svg)](https://github.com/fopost/fopost-fastapi/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ Official FastAPI integration for the [FoPost](https://fopost.com) API. Schedule and publish to
9
+ +30 social platforms from your code.
10
+
11
+ This is a **thin wrapper**. Every request, model, retry, and error type lives in the
12
+ [`fopost`](https://pypi.org/project/fopost/) SDK — this package wires that client into FastAPI's
13
+ idioms: settings, dependency injection, a webhook receiver, and an exception handler.
14
+
15
+ ```bash
16
+ pip install fopost-fastapi
17
+ ```
18
+
19
+ Requires Python 3.10 or newer, FastAPI 0.110 or newer, and pydantic v2.
20
+
21
+ > **0.x release.** The public API is still settling and minor versions may contain breaking
22
+ > changes. Pin an exact version if that matters to you.
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ from fastapi import FastAPI
28
+
29
+ from fopost_fastapi import FoPostDep, install_exception_handlers, setup_fopost
30
+
31
+ app = FastAPI()
32
+ setup_fopost(app) # one client, created at startup, closed at shutdown
33
+ install_exception_handlers(app)
34
+
35
+
36
+ @app.get("/workspaces")
37
+ def workspaces(fopost: FoPostDep):
38
+ return fopost.workspaces.list()
39
+ ```
40
+
41
+ `FoPostDep` is `Annotated[Fopost, Depends(get_client)]`. The client is built **once** when the
42
+ app starts and shared by every request — the dependency looks it up, it never constructs one.
43
+
44
+ If your app already has its own lifespan, `setup_fopost` wraps it rather than replacing it.
45
+ To wire FoPost through the lifespan directly instead:
46
+
47
+ ```python
48
+ from fopost_fastapi import fopost_lifespan
49
+
50
+ app = FastAPI(lifespan=fopost_lifespan())
51
+ ```
52
+
53
+ ## Settings
54
+
55
+ `FoPostSettings` is a `pydantic-settings` model reading `FOPOST_`-prefixed environment variables
56
+ (and a `.env` file, if present).
57
+
58
+ | Field | Environment variable | Default |
59
+ | :--- | :--- | :--- |
60
+ | `api_key` | `FOPOST_API_KEY` | — (required) |
61
+ | `base_url` | `FOPOST_BASE_URL` | `https://api.fopost.com/v1` |
62
+ | `timeout` | `FOPOST_TIMEOUT` | `30.0` seconds |
63
+ | `max_retries` | `FOPOST_MAX_RETRIES` | `3` attempts |
64
+ | `default_workspace_id` | `FOPOST_DEFAULT_WORKSPACE_ID` | — |
65
+ | `webhook_secret` | `FOPOST_WEBHOOK_SECRET` | — |
66
+
67
+ Create an API key at <https://app.fopost.com/api-keys>. It is sent as `X-API-Key`.
68
+
69
+ Pass settings explicitly when you would rather not read the environment:
70
+
71
+ ```python
72
+ setup_fopost(app, FoPostSettings(api_key="fp_...", base_url="https://api.fopost.com/v1"))
73
+ ```
74
+
75
+ `FoPostSettingsDep` injects the resolved settings into a route, which is how you reach
76
+ `default_workspace_id`.
77
+
78
+ ## Sync or async? The SDK is synchronous
79
+
80
+ The `fopost` package ships a **blocking** client only — there is no async variant, and this
81
+ package deliberately does not write one. That leaves two shapes:
82
+
83
+ ```python
84
+ # A `def` route — FastAPI already runs it in a worker thread. Call the SDK directly.
85
+ @app.get("/accounts")
86
+ def accounts(fopost: FoPostDep, settings: FoPostSettingsDep):
87
+ return fopost.accounts.list(workspace_id=settings.default_workspace_id)
88
+
89
+
90
+ # An `async def` route — the call must leave the event loop, or it stalls the server.
91
+ from fopost_fastapi import run_fopost
92
+
93
+ @app.post("/posts")
94
+ async def create(fopost: FoPostDep, settings: FoPostSettingsDep):
95
+ return await run_fopost(
96
+ fopost.posts.create,
97
+ workspace_id=settings.default_workspace_id,
98
+ content="Hello from FastAPI",
99
+ accounts=["<account id>"],
100
+ )
101
+ ```
102
+
103
+ `run_fopost` is a thin wrapper over `fastapi.concurrency.run_in_threadpool`. Never call the SDK
104
+ straight from an `async def` route: a 30-second timeout would block every other request.
105
+
106
+ ## Receiving webhooks
107
+
108
+ ```python
109
+ from fopost_fastapi import WebhookEvent, on_event, webhook_router
110
+
111
+ app.include_router(webhook_router, prefix="/fopost") # POST /fopost/webhooks
112
+
113
+
114
+ @on_event("post.published")
115
+ async def published(event: WebhookEvent) -> None:
116
+ print(event.data["id"], event.timestamp)
117
+
118
+
119
+ @on_event("post.failed")
120
+ def failed(event: WebhookEvent) -> None: # a `def` handler runs in a worker thread
121
+ ...
122
+ ```
123
+
124
+ Point a FoPost webhook at `https://<your host>/fopost/webhooks` and put its secret in
125
+ `FOPOST_WEBHOOK_SECRET`.
126
+
127
+ FoPost signs each delivery with HMAC-SHA256 over the **raw** request body using that webhook's
128
+ secret, and sends it as `X-FoPost-Signature: sha256=<hex>` alongside `X-FoPost-Event` and
129
+ `X-FoPost-Delivery`. The router reads the raw bytes before any parsing, compares with
130
+ `hmac.compare_digest`, and answers **401** on a mismatch or a missing header — no handler runs.
131
+ With no secret configured at all it answers 500 rather than accepting unverifiable traffic.
132
+
133
+ Events: `post.published`, `post.failed`, `post.partially_failed`, `delivery.published`,
134
+ `delivery.failed`, `delivery.delayed`, `account.health_changed`. `@on_event()` with no argument
135
+ subscribes to all of them.
136
+
137
+ Run several receivers, or keep the secret out of the environment, by building your own router:
138
+
139
+ ```python
140
+ from fopost_fastapi import FoPostWebhookRouter
141
+
142
+ router = FoPostWebhookRouter(secret="whsec_...", path="/callbacks")
143
+ app.include_router(router, prefix="/fopost")
144
+ ```
145
+
146
+ `sign_payload(body, secret)` and `verify_webhook_signature(body, header, secret)` are exported
147
+ if you need to verify a delivery somewhere else.
148
+
149
+ ## Error handling
150
+
151
+ `install_exception_handlers(app)` turns an SDK exception into the status the FoPost API actually
152
+ answered with, instead of a 500 and a stack trace.
153
+
154
+ | SDK error | Response |
155
+ | :--- | :--- |
156
+ | `AuthenticationError` (401), `PermissionDeniedError` (403), `NotFoundError` (404) | the same status |
157
+ | `PaymentRequiredError` (402) | 402, body keeps `upgrade_url` |
158
+ | `RateLimitError` (429) | 429 with a `Retry-After` header |
159
+ | any 4xx | the same status |
160
+ | any 5xx or transport failure | 502 — your app is fine, its dependency is not |
161
+
162
+ The body is the API's own envelope: `{"error": "<machine code>", "message": "<human text>"}`.
163
+
164
+ Retries are the SDK's job, not this package's: a 429 is retried up to `max_retries` attempts,
165
+ honouring `Retry-After`, before the error ever reaches the handler.
166
+
167
+ ## The rest of the API
168
+
169
+ Everything you can call on the injected client — `posts`, `accounts`, `workspaces`, `labels`,
170
+ `ai`, and the `request()` escape hatch for endpoints the SDK does not wrap — is documented in the
171
+ [`fopost` SDK](https://github.com/fopost/fopost-python). This package adds no resources of its own
172
+ and stores nothing.
173
+
174
+ ## Example
175
+
176
+ [`examples/main.py`](examples/main.py) is a complete app: settings from the environment, the
177
+ injected client in both a `def` and an `async def` route, and the webhook receiver.
178
+
179
+ ## Development
180
+
181
+ ```bash
182
+ python -m venv .venv && source .venv/bin/activate
183
+ pip install -e '.[dev]'
184
+ pytest
185
+ ruff check . && ruff format --check .
186
+ mypy
187
+ ```
188
+
189
+ The suite is fully offline — it stubs the SDK's HTTP transport and never reaches the network.
190
+
191
+ ## Links
192
+
193
+ - Documentation — <https://fopost.com/docs>
194
+ - Python SDK — <https://github.com/fopost/fopost-python>
195
+ - Issues — <https://github.com/fopost/fopost-fastapi/issues>
196
+ - Support — <https://fopost.com/contact>
197
+
198
+ MIT licensed. Copyright (c) 2026 Porter Bridge, LLC.
@@ -0,0 +1,22 @@
1
+ # Example
2
+
3
+ `main.py` is a complete FastAPI app: settings from the environment, the injected client in
4
+ both a `def` and an `async def` route, and the webhook receiver.
5
+
6
+ ```bash
7
+ pip install fopost-fastapi uvicorn
8
+ export FOPOST_API_KEY=fp_...
9
+ export FOPOST_DEFAULT_WORKSPACE_ID=9b2f6c1e-...
10
+ export FOPOST_WEBHOOK_SECRET=...
11
+ uvicorn examples.main:app --reload
12
+ ```
13
+
14
+ ```bash
15
+ curl localhost:8000/accounts
16
+ curl -X POST localhost:8000/posts \
17
+ -H 'content-type: application/json' \
18
+ -d '{"text": "Hello from FastAPI", "accounts": ["<account id>"], "publish": true}'
19
+ ```
20
+
21
+ Create the webhook in FoPost (or with the SDK) pointing at `https://<your host>/fopost/webhooks`,
22
+ and put its secret in `FOPOST_WEBHOOK_SECRET`.
@@ -0,0 +1,82 @@
1
+ """A runnable FastAPI app: create a post, publish it, and receive the webhook.
2
+
3
+ pip install fopost-fastapi uvicorn
4
+ export FOPOST_API_KEY=fp_...
5
+ export FOPOST_DEFAULT_WORKSPACE_ID=9b2f6c1e-...
6
+ export FOPOST_WEBHOOK_SECRET=... # the secret of your FoPost webhook
7
+ uvicorn examples.main:app --reload
8
+
9
+ Then point a FoPost webhook at ``https://<your host>/fopost/webhooks``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import Any
16
+
17
+ from fastapi import FastAPI, HTTPException
18
+ from pydantic import BaseModel
19
+
20
+ from fopost_fastapi import (
21
+ FoPostDep,
22
+ FoPostSettingsDep,
23
+ WebhookEvent,
24
+ install_exception_handlers,
25
+ on_event,
26
+ run_fopost,
27
+ setup_fopost,
28
+ webhook_router,
29
+ )
30
+
31
+ log = logging.getLogger("example")
32
+
33
+ app = FastAPI(title="FoPost example")
34
+ setup_fopost(app)
35
+ install_exception_handlers(app)
36
+ app.include_router(webhook_router, prefix="/fopost")
37
+
38
+
39
+ class NewPost(BaseModel):
40
+ text: str
41
+ accounts: list[str]
42
+ publish: bool = False
43
+
44
+
45
+ @app.get("/accounts")
46
+ def list_accounts(fopost: FoPostDep, settings: FoPostSettingsDep) -> list[dict[str, Any]]:
47
+ """A ``def`` route: FastAPI already runs it in a worker thread, so call the SDK directly."""
48
+ if not settings.default_workspace_id:
49
+ raise HTTPException(400, "set FOPOST_DEFAULT_WORKSPACE_ID")
50
+ accounts = fopost.accounts.list(workspace_id=settings.default_workspace_id)
51
+ return [{"id": a.id, "platform": a.platform, "username": a.username} for a in accounts]
52
+
53
+
54
+ @app.post("/posts")
55
+ async def create_post(
56
+ body: NewPost, fopost: FoPostDep, settings: FoPostSettingsDep
57
+ ) -> dict[str, Any]:
58
+ """An ``async def`` route: the blocking SDK goes through ``run_fopost``."""
59
+ if not settings.default_workspace_id:
60
+ raise HTTPException(400, "set FOPOST_DEFAULT_WORKSPACE_ID")
61
+
62
+ post = await run_fopost(
63
+ fopost.posts.create,
64
+ workspace_id=settings.default_workspace_id,
65
+ content=body.text,
66
+ accounts=body.accounts,
67
+ )
68
+ if body.publish:
69
+ # Returns once delivery is queued, not once the post is live.
70
+ await run_fopost(fopost.posts.publish, post.id)
71
+
72
+ return {"id": post.id, "status": post.status}
73
+
74
+
75
+ @on_event("post.published")
76
+ async def post_published(event: WebhookEvent) -> None:
77
+ log.info("published: %s", event.data.get("id"))
78
+
79
+
80
+ @on_event("post.failed")
81
+ async def post_failed(event: WebhookEvent) -> None:
82
+ log.warning("failed: %s", event.data)
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "fopost-fastapi"
7
+ version = "0.1.0"
8
+ description = "Official FastAPI integration for the FoPost API. Dependency injection, settings, webhooks, and error handling on top of the fopost SDK."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "FoPost, Porter Bridge, LLC" }]
14
+ keywords = [
15
+ "fopost",
16
+ "fastapi",
17
+ "social-media",
18
+ "publishing",
19
+ "scheduling",
20
+ "webhooks",
21
+ "api",
22
+ "sdk",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Framework :: FastAPI",
27
+ "Intended Audience :: Developers",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Topic :: Internet :: WWW/HTTP",
34
+ "Typing :: Typed",
35
+ ]
36
+ dependencies = [
37
+ "fopost>=0.1,<1.0",
38
+ "fastapi>=0.110",
39
+ "pydantic>=2",
40
+ "pydantic-settings>=2",
41
+ ]
42
+
43
+ [project.optional-dependencies]
44
+ dev = [
45
+ "pytest>=8",
46
+ "httpx>=0.27",
47
+ "ruff>=0.6",
48
+ "mypy>=1.11",
49
+ ]
50
+
51
+ [project.urls]
52
+ Homepage = "https://fopost.com"
53
+ Documentation = "https://fopost.com/docs"
54
+ Repository = "https://github.com/fopost/fopost-fastapi"
55
+ Issues = "https://github.com/fopost/fopost-fastapi/issues"
56
+ Support = "https://fopost.com/contact"
57
+
58
+ [tool.hatch.build.targets.wheel]
59
+ packages = ["src/fopost_fastapi"]
60
+
61
+ [tool.hatch.build.targets.sdist]
62
+ include = ["src/fopost_fastapi", "tests", "examples", "README.md", "LICENSE"]
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ addopts = "-q"
67
+
68
+ [tool.ruff]
69
+ line-length = 100
70
+ src = ["src", "tests"]
71
+ # Ruff is the Python formatter here; snippets in the docs are not CI's business.
72
+ extend-exclude = ["*.md"]
73
+
74
+ [tool.ruff.lint]
75
+ select = ["E", "F", "I", "UP", "B"]
76
+
77
+ [tool.ruff.lint.isort]
78
+ known-first-party = ["fopost_fastapi", "tests"]
79
+
80
+ [tool.mypy]
81
+ python_version = "3.10"
82
+ strict = true
83
+ files = ["src/fopost_fastapi"]