fopost 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.
- fopost-0.1.0/.gitignore +15 -0
- fopost-0.1.0/LICENSE +21 -0
- fopost-0.1.0/PKG-INFO +250 -0
- fopost-0.1.0/README.md +223 -0
- fopost-0.1.0/examples/create_post.py +83 -0
- fopost-0.1.0/pyproject.toml +71 -0
- fopost-0.1.0/src/fopost/__init__.py +87 -0
- fopost-0.1.0/src/fopost/_http.py +185 -0
- fopost-0.1.0/src/fopost/client.py +94 -0
- fopost-0.1.0/src/fopost/errors.py +122 -0
- fopost-0.1.0/src/fopost/models.py +346 -0
- fopost-0.1.0/src/fopost/py.typed +0 -0
- fopost-0.1.0/src/fopost/resources/__init__.py +13 -0
- fopost-0.1.0/src/fopost/resources/_base.py +42 -0
- fopost-0.1.0/src/fopost/resources/accounts.py +29 -0
- fopost-0.1.0/src/fopost/resources/ai.py +86 -0
- fopost-0.1.0/src/fopost/resources/labels.py +14 -0
- fopost-0.1.0/src/fopost/resources/posts.py +255 -0
- fopost-0.1.0/src/fopost/resources/workspaces.py +17 -0
- fopost-0.1.0/tests/__init__.py +0 -0
- fopost-0.1.0/tests/conftest.py +91 -0
- fopost-0.1.0/tests/test_accounts.py +59 -0
- fopost-0.1.0/tests/test_ai.py +138 -0
- fopost-0.1.0/tests/test_client.py +86 -0
- fopost-0.1.0/tests/test_errors.py +99 -0
- fopost-0.1.0/tests/test_labels.py +33 -0
- fopost-0.1.0/tests/test_models.py +31 -0
- fopost-0.1.0/tests/test_posts.py +234 -0
- fopost-0.1.0/tests/test_retry.py +115 -0
- fopost-0.1.0/tests/test_workspaces.py +39 -0
fopost-0.1.0/.gitignore
ADDED
fopost-0.1.0/LICENSE
ADDED
|
@@ -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.
|
fopost-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fopost
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the FoPost API. Schedule and publish to 31 social platforms from your code.
|
|
5
|
+
Project-URL: Homepage, https://fopost.com
|
|
6
|
+
Project-URL: Documentation, https://fopost.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/fopost/fopost-python
|
|
8
|
+
Project-URL: Issues, https://github.com/fopost/fopost-python/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,fopost,instagram,linkedin,publishing,scheduling,sdk,social-media,twitter
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pydantic>=2.7
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# fopost
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/fopost/)
|
|
31
|
+
[](https://pypi.org/project/fopost/)
|
|
32
|
+
[](https://github.com/fopost/fopost-python/actions/workflows/ci.yml)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
Official Python SDK for the [FoPost](https://fopost.com) API. Schedule and publish to 31 social platforms from your code.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install fopost
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10 or newer. Built on `httpx` and `pydantic` v2, fully typed.
|
|
42
|
+
|
|
43
|
+
> **0.x release.** The public API is still settling and minor versions may
|
|
44
|
+
> contain breaking changes. Pin an exact version if that matters to you.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from fopost import Fopost
|
|
50
|
+
|
|
51
|
+
client = Fopost(api_key="osk_...") # or set FOPOST_API_KEY
|
|
52
|
+
|
|
53
|
+
workspace = client.workspaces.list()[0]
|
|
54
|
+
accounts = client.accounts.list(workspace_id=workspace.id)
|
|
55
|
+
|
|
56
|
+
post = client.posts.create(
|
|
57
|
+
workspace_id=workspace.id,
|
|
58
|
+
content="Hello from Python",
|
|
59
|
+
accounts=[a.id for a in accounts],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
client.posts.publish(post.id)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`content` takes a string for a single block, or a list for a thread:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
client.posts.create(
|
|
69
|
+
workspace_id=workspace.id,
|
|
70
|
+
content=[
|
|
71
|
+
"First post in the thread",
|
|
72
|
+
{
|
|
73
|
+
"text": "Second one, with an image",
|
|
74
|
+
"media": [
|
|
75
|
+
{"type": "image", "name": "chart.png", "url": "https://.../chart.png"},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
accounts=[a.id for a in accounts],
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Scheduling
|
|
84
|
+
|
|
85
|
+
`status` is `"draft"` or `"scheduled"`; a scheduled post needs `schedule_at`. To send something out now, create it and call `publish`.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from datetime import datetime, timezone
|
|
89
|
+
|
|
90
|
+
client.posts.create(
|
|
91
|
+
workspace_id=workspace.id,
|
|
92
|
+
status="scheduled",
|
|
93
|
+
schedule_at=datetime(2026, 9, 1, 10, 0, tzinfo=timezone.utc),
|
|
94
|
+
content="Scheduled with the SDK",
|
|
95
|
+
accounts=[accounts[0].id],
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Pagination
|
|
100
|
+
|
|
101
|
+
`posts.list` returns one page and iterates over its items directly. `posts.iter` walks every page for you:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
page = client.posts.list(workspace_id=workspace.id, status="published", per_page=50)
|
|
105
|
+
print(f"{page.meta.total} published posts")
|
|
106
|
+
for post in page:
|
|
107
|
+
print(post.id, post.status)
|
|
108
|
+
|
|
109
|
+
# Every post, one page fetched at a time
|
|
110
|
+
for post in client.posts.iter(workspace_id=workspace.id):
|
|
111
|
+
print(post.id)
|
|
112
|
+
|
|
113
|
+
# Or page by page, when you want the meta
|
|
114
|
+
for page in client.posts.iter_pages(workspace_id=workspace.id):
|
|
115
|
+
print(page.meta.current_page, len(page))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## AI features
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
balance = client.ai.credits()
|
|
122
|
+
print(f"{balance.credits_remaining} of {balance.credits_total} credits left")
|
|
123
|
+
|
|
124
|
+
result = client.ai.generate_caption(
|
|
125
|
+
current_caption="shipping a new feature",
|
|
126
|
+
platforms=["twitter", "linkedin"],
|
|
127
|
+
)
|
|
128
|
+
print(result.caption)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`rewrite` and `repurpose_url` are wired the same way:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
rewrites = client.ai.rewrite(
|
|
135
|
+
content="Long article-style draft...",
|
|
136
|
+
platforms=["twitter", "linkedin", "bluesky"],
|
|
137
|
+
)
|
|
138
|
+
for variant in rewrites.results:
|
|
139
|
+
print(variant.platform, variant.content)
|
|
140
|
+
|
|
141
|
+
repurposed = client.ai.repurpose_url(
|
|
142
|
+
url="https://example.com/blog/post",
|
|
143
|
+
platforms=["twitter", "linkedin", "bluesky", "threads"],
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
> **API keys reach `credits` and `generate_caption`.** `rewrite` and
|
|
148
|
+
> `repurpose_url` currently require a signed-in dashboard session and answer
|
|
149
|
+
> `401` to an API key. They are here so the surface is complete once the server
|
|
150
|
+
> opens them up.
|
|
151
|
+
|
|
152
|
+
## Configuration
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
Fopost(
|
|
156
|
+
api_key="osk_...", # or FOPOST_API_KEY
|
|
157
|
+
base_url="https://api.fopost.com/api/v1", # override for a dev server
|
|
158
|
+
timeout=30.0, # seconds, or an httpx.Timeout
|
|
159
|
+
max_retries=3, # total attempts on a 429
|
|
160
|
+
http_client=my_httpx_client, # bring your own transport
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| Env var | Used for |
|
|
165
|
+
| ------------------ | -------------------------------------------- |
|
|
166
|
+
| `FOPOST_API_KEY` | API key, when not passed to the constructor |
|
|
167
|
+
|
|
168
|
+
The client is a context manager, and closes its transport on exit:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
with Fopost() as client:
|
|
172
|
+
client.posts.list(workspace_id=workspace.id)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A `429` is retried automatically, waiting for the interval the API asks for in
|
|
176
|
+
`Retry-After` (delta-seconds or an HTTP date, capped at 60s). `max_retries`
|
|
177
|
+
counts total attempts, so the default of 3 means two retries.
|
|
178
|
+
|
|
179
|
+
## Error handling
|
|
180
|
+
|
|
181
|
+
Every non-2xx response raises `FopostError` or one of its subclasses, carrying
|
|
182
|
+
the API's `status`, `code`, and `message`.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from fopost import Fopost, FopostError, PaymentRequiredError, RateLimitError
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
client.posts.publish("9b2f6c1e-...")
|
|
189
|
+
except PaymentRequiredError as err:
|
|
190
|
+
print(f"Out of credits — upgrade at {err.upgrade_url}")
|
|
191
|
+
except RateLimitError as err:
|
|
192
|
+
print(f"Rate limited, retry in {err.retry_after}s")
|
|
193
|
+
except FopostError as err:
|
|
194
|
+
print(f"API {err.status} ({err.code}): {err.message}")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
| Status | Exception |
|
|
198
|
+
| ------ | ----------------------- |
|
|
199
|
+
| 401 | `AuthenticationError` |
|
|
200
|
+
| 402 | `PaymentRequiredError` |
|
|
201
|
+
| 403 | `PermissionDeniedError` |
|
|
202
|
+
| 404 | `NotFoundError` |
|
|
203
|
+
| 429 | `RateLimitError` |
|
|
204
|
+
| other | `FopostError` |
|
|
205
|
+
|
|
206
|
+
## Resources
|
|
207
|
+
|
|
208
|
+
| Namespace | Methods |
|
|
209
|
+
| ------------ | -------------------------------------------------------------------------------------- |
|
|
210
|
+
| `posts` | `list`, `iter`, `iter_pages`, `get`, `create`, `update`, `delete`, `publish`, `cancel`, `retry`, `preflight`, `deliveries` |
|
|
211
|
+
| `accounts` | `list`, `get`, `health` |
|
|
212
|
+
| `workspaces` | `list`, `get` |
|
|
213
|
+
| `labels` | `list` |
|
|
214
|
+
| `ai` | `credits`, `generate_caption`, `rewrite`, `repurpose_url` |
|
|
215
|
+
|
|
216
|
+
For an endpoint the SDK does not wrap yet, `client.request` sends an
|
|
217
|
+
authenticated call and hands back the decoded body:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
client.request("GET", "/analytics/summary", params={"workspace_id": workspace.id})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Example
|
|
224
|
+
|
|
225
|
+
[`examples/create_post.py`](examples/create_post.py) creates a post against a
|
|
226
|
+
running API:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
export FOPOST_API_KEY=osk_...
|
|
230
|
+
export FOPOST_BASE_URL=http://localhost:8080/api/v1
|
|
231
|
+
python examples/create_post.py "Hello from the Python SDK" --publish
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Contributing
|
|
235
|
+
|
|
236
|
+
Issues and pull requests are welcome at
|
|
237
|
+
[fopost/fopost-python](https://github.com/fopost/fopost-python).
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
uv sync --group dev
|
|
241
|
+
uv run pytest
|
|
242
|
+
uv run ruff check .
|
|
243
|
+
uv run mypy
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
249
|
+
|
|
250
|
+
Questions or a problem: [fopost.com/contact](https://fopost.com/contact).
|
fopost-0.1.0/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# fopost
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fopost/)
|
|
4
|
+
[](https://pypi.org/project/fopost/)
|
|
5
|
+
[](https://github.com/fopost/fopost-python/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Official Python SDK for the [FoPost](https://fopost.com) API. Schedule and publish to 31 social platforms from your code.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install fopost
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Python 3.10 or newer. Built on `httpx` and `pydantic` v2, fully typed.
|
|
15
|
+
|
|
16
|
+
> **0.x release.** The public API is still settling and minor versions may
|
|
17
|
+
> contain breaking changes. Pin an exact version if that matters to you.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from fopost import Fopost
|
|
23
|
+
|
|
24
|
+
client = Fopost(api_key="osk_...") # or set FOPOST_API_KEY
|
|
25
|
+
|
|
26
|
+
workspace = client.workspaces.list()[0]
|
|
27
|
+
accounts = client.accounts.list(workspace_id=workspace.id)
|
|
28
|
+
|
|
29
|
+
post = client.posts.create(
|
|
30
|
+
workspace_id=workspace.id,
|
|
31
|
+
content="Hello from Python",
|
|
32
|
+
accounts=[a.id for a in accounts],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
client.posts.publish(post.id)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`content` takes a string for a single block, or a list for a thread:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
client.posts.create(
|
|
42
|
+
workspace_id=workspace.id,
|
|
43
|
+
content=[
|
|
44
|
+
"First post in the thread",
|
|
45
|
+
{
|
|
46
|
+
"text": "Second one, with an image",
|
|
47
|
+
"media": [
|
|
48
|
+
{"type": "image", "name": "chart.png", "url": "https://.../chart.png"},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
accounts=[a.id for a in accounts],
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Scheduling
|
|
57
|
+
|
|
58
|
+
`status` is `"draft"` or `"scheduled"`; a scheduled post needs `schedule_at`. To send something out now, create it and call `publish`.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from datetime import datetime, timezone
|
|
62
|
+
|
|
63
|
+
client.posts.create(
|
|
64
|
+
workspace_id=workspace.id,
|
|
65
|
+
status="scheduled",
|
|
66
|
+
schedule_at=datetime(2026, 9, 1, 10, 0, tzinfo=timezone.utc),
|
|
67
|
+
content="Scheduled with the SDK",
|
|
68
|
+
accounts=[accounts[0].id],
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Pagination
|
|
73
|
+
|
|
74
|
+
`posts.list` returns one page and iterates over its items directly. `posts.iter` walks every page for you:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
page = client.posts.list(workspace_id=workspace.id, status="published", per_page=50)
|
|
78
|
+
print(f"{page.meta.total} published posts")
|
|
79
|
+
for post in page:
|
|
80
|
+
print(post.id, post.status)
|
|
81
|
+
|
|
82
|
+
# Every post, one page fetched at a time
|
|
83
|
+
for post in client.posts.iter(workspace_id=workspace.id):
|
|
84
|
+
print(post.id)
|
|
85
|
+
|
|
86
|
+
# Or page by page, when you want the meta
|
|
87
|
+
for page in client.posts.iter_pages(workspace_id=workspace.id):
|
|
88
|
+
print(page.meta.current_page, len(page))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## AI features
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
balance = client.ai.credits()
|
|
95
|
+
print(f"{balance.credits_remaining} of {balance.credits_total} credits left")
|
|
96
|
+
|
|
97
|
+
result = client.ai.generate_caption(
|
|
98
|
+
current_caption="shipping a new feature",
|
|
99
|
+
platforms=["twitter", "linkedin"],
|
|
100
|
+
)
|
|
101
|
+
print(result.caption)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`rewrite` and `repurpose_url` are wired the same way:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
rewrites = client.ai.rewrite(
|
|
108
|
+
content="Long article-style draft...",
|
|
109
|
+
platforms=["twitter", "linkedin", "bluesky"],
|
|
110
|
+
)
|
|
111
|
+
for variant in rewrites.results:
|
|
112
|
+
print(variant.platform, variant.content)
|
|
113
|
+
|
|
114
|
+
repurposed = client.ai.repurpose_url(
|
|
115
|
+
url="https://example.com/blog/post",
|
|
116
|
+
platforms=["twitter", "linkedin", "bluesky", "threads"],
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> **API keys reach `credits` and `generate_caption`.** `rewrite` and
|
|
121
|
+
> `repurpose_url` currently require a signed-in dashboard session and answer
|
|
122
|
+
> `401` to an API key. They are here so the surface is complete once the server
|
|
123
|
+
> opens them up.
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
Fopost(
|
|
129
|
+
api_key="osk_...", # or FOPOST_API_KEY
|
|
130
|
+
base_url="https://api.fopost.com/api/v1", # override for a dev server
|
|
131
|
+
timeout=30.0, # seconds, or an httpx.Timeout
|
|
132
|
+
max_retries=3, # total attempts on a 429
|
|
133
|
+
http_client=my_httpx_client, # bring your own transport
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Env var | Used for |
|
|
138
|
+
| ------------------ | -------------------------------------------- |
|
|
139
|
+
| `FOPOST_API_KEY` | API key, when not passed to the constructor |
|
|
140
|
+
|
|
141
|
+
The client is a context manager, and closes its transport on exit:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
with Fopost() as client:
|
|
145
|
+
client.posts.list(workspace_id=workspace.id)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A `429` is retried automatically, waiting for the interval the API asks for in
|
|
149
|
+
`Retry-After` (delta-seconds or an HTTP date, capped at 60s). `max_retries`
|
|
150
|
+
counts total attempts, so the default of 3 means two retries.
|
|
151
|
+
|
|
152
|
+
## Error handling
|
|
153
|
+
|
|
154
|
+
Every non-2xx response raises `FopostError` or one of its subclasses, carrying
|
|
155
|
+
the API's `status`, `code`, and `message`.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from fopost import Fopost, FopostError, PaymentRequiredError, RateLimitError
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
client.posts.publish("9b2f6c1e-...")
|
|
162
|
+
except PaymentRequiredError as err:
|
|
163
|
+
print(f"Out of credits — upgrade at {err.upgrade_url}")
|
|
164
|
+
except RateLimitError as err:
|
|
165
|
+
print(f"Rate limited, retry in {err.retry_after}s")
|
|
166
|
+
except FopostError as err:
|
|
167
|
+
print(f"API {err.status} ({err.code}): {err.message}")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| Status | Exception |
|
|
171
|
+
| ------ | ----------------------- |
|
|
172
|
+
| 401 | `AuthenticationError` |
|
|
173
|
+
| 402 | `PaymentRequiredError` |
|
|
174
|
+
| 403 | `PermissionDeniedError` |
|
|
175
|
+
| 404 | `NotFoundError` |
|
|
176
|
+
| 429 | `RateLimitError` |
|
|
177
|
+
| other | `FopostError` |
|
|
178
|
+
|
|
179
|
+
## Resources
|
|
180
|
+
|
|
181
|
+
| Namespace | Methods |
|
|
182
|
+
| ------------ | -------------------------------------------------------------------------------------- |
|
|
183
|
+
| `posts` | `list`, `iter`, `iter_pages`, `get`, `create`, `update`, `delete`, `publish`, `cancel`, `retry`, `preflight`, `deliveries` |
|
|
184
|
+
| `accounts` | `list`, `get`, `health` |
|
|
185
|
+
| `workspaces` | `list`, `get` |
|
|
186
|
+
| `labels` | `list` |
|
|
187
|
+
| `ai` | `credits`, `generate_caption`, `rewrite`, `repurpose_url` |
|
|
188
|
+
|
|
189
|
+
For an endpoint the SDK does not wrap yet, `client.request` sends an
|
|
190
|
+
authenticated call and hands back the decoded body:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
client.request("GET", "/analytics/summary", params={"workspace_id": workspace.id})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Example
|
|
197
|
+
|
|
198
|
+
[`examples/create_post.py`](examples/create_post.py) creates a post against a
|
|
199
|
+
running API:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
export FOPOST_API_KEY=osk_...
|
|
203
|
+
export FOPOST_BASE_URL=http://localhost:8080/api/v1
|
|
204
|
+
python examples/create_post.py "Hello from the Python SDK" --publish
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Contributing
|
|
208
|
+
|
|
209
|
+
Issues and pull requests are welcome at
|
|
210
|
+
[fopost/fopost-python](https://github.com/fopost/fopost-python).
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
uv sync --group dev
|
|
214
|
+
uv run pytest
|
|
215
|
+
uv run ruff check .
|
|
216
|
+
uv run mypy
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
222
|
+
|
|
223
|
+
Questions or a problem: [fopost.com/contact](https://fopost.com/contact).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Create a post and publish it.
|
|
2
|
+
|
|
3
|
+
Point it at a local dev API:
|
|
4
|
+
|
|
5
|
+
export FOPOST_API_KEY=osk_...
|
|
6
|
+
export FOPOST_BASE_URL=http://localhost:8080/api/v1
|
|
7
|
+
python examples/create_post.py "Hello from the Python SDK"
|
|
8
|
+
|
|
9
|
+
Without --publish it stops at a draft, so you can run it against a real
|
|
10
|
+
workspace without anything going out.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from fopost import DEFAULT_BASE_URL, Fopost, FopostError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> int:
|
|
23
|
+
parser = argparse.ArgumentParser(description="Create a post with the FoPost SDK.")
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"text",
|
|
26
|
+
nargs="?",
|
|
27
|
+
default="Hello from the FoPost Python SDK",
|
|
28
|
+
help="post body",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("--workspace", help="workspace id (defaults to the first one)")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--publish",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="publish immediately instead of leaving a draft",
|
|
35
|
+
)
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
api_key = os.environ.get("FOPOST_API_KEY")
|
|
39
|
+
if not api_key:
|
|
40
|
+
print("Set FOPOST_API_KEY first.", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
base_url = os.environ.get("FOPOST_BASE_URL", DEFAULT_BASE_URL)
|
|
44
|
+
|
|
45
|
+
with Fopost(api_key=api_key, base_url=base_url) as client:
|
|
46
|
+
try:
|
|
47
|
+
workspace_id = args.workspace
|
|
48
|
+
if not workspace_id:
|
|
49
|
+
workspaces = client.workspaces.list()
|
|
50
|
+
if not workspaces:
|
|
51
|
+
print("No workspaces on this key.", file=sys.stderr)
|
|
52
|
+
return 1
|
|
53
|
+
workspace_id = workspaces[0].id
|
|
54
|
+
print(f"Workspace: {workspaces[0].name} ({workspace_id})")
|
|
55
|
+
|
|
56
|
+
accounts = client.accounts.list(workspace_id=workspace_id)
|
|
57
|
+
if not accounts:
|
|
58
|
+
print("No connected accounts in this workspace.", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
for account in accounts:
|
|
61
|
+
print(f" · {account.platform}: @{account.username}")
|
|
62
|
+
|
|
63
|
+
post = client.posts.create(
|
|
64
|
+
workspace_id=workspace_id,
|
|
65
|
+
content=args.text,
|
|
66
|
+
accounts=[a.id for a in accounts],
|
|
67
|
+
)
|
|
68
|
+
print(f"Created post {post.id} ({post.status})")
|
|
69
|
+
|
|
70
|
+
if args.publish:
|
|
71
|
+
client.posts.publish(post.id)
|
|
72
|
+
for delivery in client.posts.deliveries(post.id):
|
|
73
|
+
print(f" · {delivery.platform}: {delivery.status}")
|
|
74
|
+
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
except FopostError as err:
|
|
78
|
+
print(f"API error: {err}", file=sys.stderr)
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fopost"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the FoPost API. Schedule and publish to 31 social platforms from your code."
|
|
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
|
+
"social-media",
|
|
17
|
+
"publishing",
|
|
18
|
+
"scheduling",
|
|
19
|
+
"twitter",
|
|
20
|
+
"linkedin",
|
|
21
|
+
"instagram",
|
|
22
|
+
"api",
|
|
23
|
+
"sdk",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
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 = ["httpx>=0.27", "pydantic>=2.7"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://fopost.com"
|
|
40
|
+
Documentation = "https://fopost.com/docs"
|
|
41
|
+
Repository = "https://github.com/fopost/fopost-python"
|
|
42
|
+
Issues = "https://github.com/fopost/fopost-python/issues"
|
|
43
|
+
Support = "https://fopost.com/contact"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = ["pytest>=8", "respx>=0.21", "ruff>=0.6", "mypy>=1.11"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/fopost"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src/fopost", "tests", "examples", "README.md", "LICENSE"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "-q"
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
src = ["src", "tests"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.isort]
|
|
66
|
+
known-first-party = ["fopost", "tests"]
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.10"
|
|
70
|
+
strict = true
|
|
71
|
+
files = ["src/fopost"]
|