unipost 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.
- unipost-0.1.0/.github/workflows/publish.yml +19 -0
- unipost-0.1.0/.github/workflows/test.yml +22 -0
- unipost-0.1.0/.gitignore +10 -0
- unipost-0.1.0/PKG-INFO +157 -0
- unipost-0.1.0/README.md +126 -0
- unipost-0.1.0/examples/async_example.py +24 -0
- unipost-0.1.0/examples/per_platform.py +28 -0
- unipost-0.1.0/examples/quickstart.py +23 -0
- unipost-0.1.0/pyproject.toml +35 -0
- unipost-0.1.0/tests/__init__.py +0 -0
- unipost-0.1.0/tests/test_client.py +40 -0
- unipost-0.1.0/tests/test_types.py +30 -0
- unipost-0.1.0/tests/test_webhook.py +18 -0
- unipost-0.1.0/unipost/__init__.py +44 -0
- unipost-0.1.0/unipost/async_client.py +180 -0
- unipost-0.1.0/unipost/client.py +63 -0
- unipost-0.1.0/unipost/errors.py +85 -0
- unipost-0.1.0/unipost/http.py +86 -0
- unipost-0.1.0/unipost/py.typed +0 -0
- unipost-0.1.0/unipost/resources/__init__.py +0 -0
- unipost-0.1.0/unipost/resources/accounts.py +40 -0
- unipost-0.1.0/unipost/resources/analytics.py +35 -0
- unipost-0.1.0/unipost/resources/connect.py +35 -0
- unipost-0.1.0/unipost/resources/media.py +65 -0
- unipost-0.1.0/unipost/resources/posts.py +132 -0
- unipost-0.1.0/unipost/resources/users.py +30 -0
- unipost-0.1.0/unipost/types/__init__.py +129 -0
- unipost-0.1.0/unipost/webhook.py +34 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- run: pip install build
|
|
18
|
+
- run: python -m build
|
|
19
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e ".[dev]"
|
|
21
|
+
- run: pytest tests/
|
|
22
|
+
- run: mypy unipost/ --ignore-missing-imports
|
unipost-0.1.0/.gitignore
ADDED
unipost-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unipost
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official UniPost API client for Python
|
|
5
|
+
Project-URL: Homepage, https://unipost.dev
|
|
6
|
+
Project-URL: Documentation, https://unipost.dev/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/unipost-dev/sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/unipost-dev/sdk-python/issues
|
|
9
|
+
Author-email: UniPost <hello@unipost.dev>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: api,instagram,linkedin,social-media,twitter,unipost
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Provides-Extra: async
|
|
23
|
+
Requires-Dist: httpx>=0.25.0; extra == 'async'
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: httpx>=0.25.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# unipost
|
|
33
|
+
|
|
34
|
+
Official UniPost API client for Python.
|
|
35
|
+
Post to 7 social platforms with one API call.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install unipost
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For async support:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install unipost[async]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from unipost import UniPost
|
|
53
|
+
|
|
54
|
+
# Reads UNIPOST_API_KEY from environment automatically
|
|
55
|
+
client = UniPost()
|
|
56
|
+
|
|
57
|
+
post = client.posts.create(
|
|
58
|
+
caption="Hello from UniPost! 🚀",
|
|
59
|
+
account_ids=["sa_twitter_xxx", "sa_linkedin_xxx"],
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### List Accounts
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
result = client.accounts.list()
|
|
69
|
+
accounts = result["data"]
|
|
70
|
+
|
|
71
|
+
# Filter by platform
|
|
72
|
+
twitter = client.accounts.list(platform="twitter")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Create Posts
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Immediate publish
|
|
79
|
+
post = client.posts.create(
|
|
80
|
+
caption="Hello world!",
|
|
81
|
+
account_ids=["sa_twitter_xxx"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Scheduled
|
|
85
|
+
post = client.posts.create(
|
|
86
|
+
caption="Scheduled post",
|
|
87
|
+
account_ids=["sa_twitter_xxx"],
|
|
88
|
+
scheduled_at="2026-04-28T09:00:00Z",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Per-platform captions
|
|
92
|
+
post = client.posts.create(
|
|
93
|
+
platform_posts=[
|
|
94
|
+
{"account_id": "sa_twitter_xxx", "caption": "Short tweet 🐦"},
|
|
95
|
+
{"account_id": "sa_linkedin_xxx", "caption": "Longer LinkedIn version..."},
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Save as draft
|
|
100
|
+
draft = client.posts.create(
|
|
101
|
+
caption="Work in progress",
|
|
102
|
+
account_ids=["sa_twitter_xxx"],
|
|
103
|
+
status="draft",
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Async
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from unipost import AsyncUniPost
|
|
111
|
+
|
|
112
|
+
async def main():
|
|
113
|
+
client = AsyncUniPost()
|
|
114
|
+
post = await client.posts.create(
|
|
115
|
+
caption="Async post!",
|
|
116
|
+
account_ids=["sa_twitter_xxx"],
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Webhook Verification
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from unipost import verify_webhook_signature
|
|
124
|
+
|
|
125
|
+
is_valid = verify_webhook_signature(
|
|
126
|
+
payload=request.body,
|
|
127
|
+
signature=request.headers["X-UniPost-Signature"],
|
|
128
|
+
secret=os.environ["UNIPOST_WEBHOOK_SECRET"],
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Error Handling
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from unipost import UniPost, AuthError, RateLimitError, UniPostError
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
post = client.posts.create(...)
|
|
139
|
+
except AuthError:
|
|
140
|
+
print("API key invalid")
|
|
141
|
+
except RateLimitError as e:
|
|
142
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
143
|
+
except UniPostError as e:
|
|
144
|
+
print(f"API error: {e.status} {e.code} {e}")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Type Hints
|
|
148
|
+
|
|
149
|
+
Full type annotations included. Works with mypy.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from unipost import Post, SocialAccount
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
unipost-0.1.0/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# unipost
|
|
2
|
+
|
|
3
|
+
Official UniPost API client for Python.
|
|
4
|
+
Post to 7 social platforms with one API call.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install unipost
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For async support:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install unipost[async]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from unipost import UniPost
|
|
22
|
+
|
|
23
|
+
# Reads UNIPOST_API_KEY from environment automatically
|
|
24
|
+
client = UniPost()
|
|
25
|
+
|
|
26
|
+
post = client.posts.create(
|
|
27
|
+
caption="Hello from UniPost! 🚀",
|
|
28
|
+
account_ids=["sa_twitter_xxx", "sa_linkedin_xxx"],
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### List Accounts
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
result = client.accounts.list()
|
|
38
|
+
accounts = result["data"]
|
|
39
|
+
|
|
40
|
+
# Filter by platform
|
|
41
|
+
twitter = client.accounts.list(platform="twitter")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Create Posts
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# Immediate publish
|
|
48
|
+
post = client.posts.create(
|
|
49
|
+
caption="Hello world!",
|
|
50
|
+
account_ids=["sa_twitter_xxx"],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Scheduled
|
|
54
|
+
post = client.posts.create(
|
|
55
|
+
caption="Scheduled post",
|
|
56
|
+
account_ids=["sa_twitter_xxx"],
|
|
57
|
+
scheduled_at="2026-04-28T09:00:00Z",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Per-platform captions
|
|
61
|
+
post = client.posts.create(
|
|
62
|
+
platform_posts=[
|
|
63
|
+
{"account_id": "sa_twitter_xxx", "caption": "Short tweet 🐦"},
|
|
64
|
+
{"account_id": "sa_linkedin_xxx", "caption": "Longer LinkedIn version..."},
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Save as draft
|
|
69
|
+
draft = client.posts.create(
|
|
70
|
+
caption="Work in progress",
|
|
71
|
+
account_ids=["sa_twitter_xxx"],
|
|
72
|
+
status="draft",
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Async
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from unipost import AsyncUniPost
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
client = AsyncUniPost()
|
|
83
|
+
post = await client.posts.create(
|
|
84
|
+
caption="Async post!",
|
|
85
|
+
account_ids=["sa_twitter_xxx"],
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Webhook Verification
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from unipost import verify_webhook_signature
|
|
93
|
+
|
|
94
|
+
is_valid = verify_webhook_signature(
|
|
95
|
+
payload=request.body,
|
|
96
|
+
signature=request.headers["X-UniPost-Signature"],
|
|
97
|
+
secret=os.environ["UNIPOST_WEBHOOK_SECRET"],
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Error Handling
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from unipost import UniPost, AuthError, RateLimitError, UniPostError
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
post = client.posts.create(...)
|
|
108
|
+
except AuthError:
|
|
109
|
+
print("API key invalid")
|
|
110
|
+
except RateLimitError as e:
|
|
111
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
112
|
+
except UniPostError as e:
|
|
113
|
+
print(f"API error: {e.status} {e.code} {e}")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Type Hints
|
|
117
|
+
|
|
118
|
+
Full type annotations included. Works with mypy.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from unipost import Post, SocialAccount
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Async usage with AsyncUniPost (requires httpx)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from unipost import AsyncUniPost
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def main():
|
|
8
|
+
client = AsyncUniPost()
|
|
9
|
+
|
|
10
|
+
# List accounts
|
|
11
|
+
result = await client.accounts.list()
|
|
12
|
+
accounts = result["data"]
|
|
13
|
+
print(f"Found {len(accounts)} accounts")
|
|
14
|
+
|
|
15
|
+
if accounts:
|
|
16
|
+
# Create a post
|
|
17
|
+
post = await client.posts.create(
|
|
18
|
+
caption="Async post from Python! 🐍",
|
|
19
|
+
account_ids=[accounts[0].id],
|
|
20
|
+
)
|
|
21
|
+
print(f"Post: {post.id} ({post.status})")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Per-platform captions: Different copy for each platform."""
|
|
2
|
+
|
|
3
|
+
from unipost import UniPost
|
|
4
|
+
|
|
5
|
+
client = UniPost()
|
|
6
|
+
|
|
7
|
+
post = client.posts.create(
|
|
8
|
+
platform_posts=[
|
|
9
|
+
{
|
|
10
|
+
"account_id": "sa_twitter_xxx",
|
|
11
|
+
"caption": "Short and punchy for Twitter 🐦",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"account_id": "sa_linkedin_xxx",
|
|
15
|
+
"caption": (
|
|
16
|
+
"I'm excited to share that we've shipped the UniPost Python SDK "
|
|
17
|
+
"— a unified API client for posting to 7 social platforms.\n\n"
|
|
18
|
+
"#DevTools #SocialMedia"
|
|
19
|
+
),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"account_id": "sa_bluesky_xxx",
|
|
23
|
+
"caption": "Just shipped unipost Python SDK 🦋",
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
print(f"Multi-platform post: {post.id}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Quick Start: Create your first post with UniPost."""
|
|
2
|
+
|
|
3
|
+
from unipost import UniPost
|
|
4
|
+
|
|
5
|
+
# Reads UNIPOST_API_KEY from environment automatically
|
|
6
|
+
client = UniPost()
|
|
7
|
+
|
|
8
|
+
# List connected accounts
|
|
9
|
+
result = client.accounts.list()
|
|
10
|
+
accounts = result["data"]
|
|
11
|
+
print(f"Found {len(accounts)} connected accounts")
|
|
12
|
+
|
|
13
|
+
if not accounts:
|
|
14
|
+
print("Connect an account first at https://app.unipost.dev")
|
|
15
|
+
exit()
|
|
16
|
+
|
|
17
|
+
# Create a post
|
|
18
|
+
post = client.posts.create(
|
|
19
|
+
caption="Hello from UniPost Python SDK! 🚀",
|
|
20
|
+
account_ids=[a.id for a in accounts],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
print(f"Post created: {post.id} (status: {post.status})")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "unipost"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official UniPost API client for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "UniPost", email = "hello@unipost.dev" }]
|
|
13
|
+
keywords = ["unipost", "social-media", "api", "twitter", "linkedin", "instagram"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
async = ["httpx>=0.25.0"]
|
|
29
|
+
dev = ["pytest", "pytest-asyncio", "httpx>=0.25.0", "mypy", "ruff"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://unipost.dev"
|
|
33
|
+
Documentation = "https://unipost.dev/docs"
|
|
34
|
+
Repository = "https://github.com/unipost-dev/sdk-python"
|
|
35
|
+
Issues = "https://github.com/unipost-dev/sdk-python/issues"
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from unipost import UniPost, AsyncUniPost
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_requires_api_key():
|
|
7
|
+
# Ensure env var is not set
|
|
8
|
+
key = os.environ.pop("UNIPOST_API_KEY", None)
|
|
9
|
+
try:
|
|
10
|
+
with pytest.raises(ValueError, match="API key is required"):
|
|
11
|
+
UniPost()
|
|
12
|
+
finally:
|
|
13
|
+
if key:
|
|
14
|
+
os.environ["UNIPOST_API_KEY"] = key
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_accepts_explicit_api_key():
|
|
18
|
+
client = UniPost(api_key="up_test_xxx")
|
|
19
|
+
assert client.posts is not None
|
|
20
|
+
assert client.accounts is not None
|
|
21
|
+
assert client.media is not None
|
|
22
|
+
assert client.analytics is not None
|
|
23
|
+
assert client.connect is not None
|
|
24
|
+
assert client.users is not None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_async_requires_api_key():
|
|
28
|
+
key = os.environ.pop("UNIPOST_API_KEY", None)
|
|
29
|
+
try:
|
|
30
|
+
with pytest.raises(ValueError, match="API key is required"):
|
|
31
|
+
AsyncUniPost()
|
|
32
|
+
finally:
|
|
33
|
+
if key:
|
|
34
|
+
os.environ["UNIPOST_API_KEY"] = key
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_async_accepts_explicit_api_key():
|
|
38
|
+
client = AsyncUniPost(api_key="up_test_xxx")
|
|
39
|
+
assert client.posts is not None
|
|
40
|
+
assert client.accounts is not None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from unipost.types import SocialAccount, Post, _from_dict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_social_account_from_dict():
|
|
5
|
+
data = {
|
|
6
|
+
"id": "sa_1",
|
|
7
|
+
"platform": "twitter",
|
|
8
|
+
"account_name": "unipost",
|
|
9
|
+
"status": "active",
|
|
10
|
+
"connected_at": "2026-04-09T00:00:00Z",
|
|
11
|
+
"connection_type": "byo",
|
|
12
|
+
"unknown_field": "ignored",
|
|
13
|
+
}
|
|
14
|
+
account = _from_dict(SocialAccount, data)
|
|
15
|
+
assert account.id == "sa_1"
|
|
16
|
+
assert account.platform == "twitter"
|
|
17
|
+
assert account.account_name == "unipost"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_post_from_dict():
|
|
21
|
+
data = {
|
|
22
|
+
"id": "post_1",
|
|
23
|
+
"caption": "Hello",
|
|
24
|
+
"status": "published",
|
|
25
|
+
"created_at": "2026-04-09T00:00:00Z",
|
|
26
|
+
}
|
|
27
|
+
post = _from_dict(Post, data)
|
|
28
|
+
assert post.id == "post_1"
|
|
29
|
+
assert post.caption == "Hello"
|
|
30
|
+
assert post.results == []
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
from unipost.webhook import verify_webhook_signature
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_valid_signature():
|
|
7
|
+
secret = "whsec_test"
|
|
8
|
+
payload = '{"event":"post.published"}'
|
|
9
|
+
sig = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
|
|
10
|
+
assert verify_webhook_signature(payload=payload, signature=sig, secret=secret) is True
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_invalid_signature():
|
|
14
|
+
assert verify_webhook_signature(
|
|
15
|
+
payload='{"event":"post.published"}',
|
|
16
|
+
signature="0" * 64,
|
|
17
|
+
secret="whsec_test",
|
|
18
|
+
) is False
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Official UniPost API client for Python."""
|
|
2
|
+
|
|
3
|
+
from unipost.client import UniPost
|
|
4
|
+
from unipost.async_client import AsyncUniPost
|
|
5
|
+
from unipost.errors import (
|
|
6
|
+
UniPostError,
|
|
7
|
+
AuthError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
PlatformError,
|
|
12
|
+
QuotaError,
|
|
13
|
+
)
|
|
14
|
+
from unipost.webhook import verify_webhook_signature
|
|
15
|
+
from unipost.types import (
|
|
16
|
+
SocialAccount,
|
|
17
|
+
Post,
|
|
18
|
+
PlatformResult,
|
|
19
|
+
ConnectSession,
|
|
20
|
+
ManagedUser,
|
|
21
|
+
PostAnalytics,
|
|
22
|
+
AnalyticsRollup,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
__all__ = [
|
|
27
|
+
"UniPost",
|
|
28
|
+
"AsyncUniPost",
|
|
29
|
+
"UniPostError",
|
|
30
|
+
"AuthError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"ValidationError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
"PlatformError",
|
|
35
|
+
"QuotaError",
|
|
36
|
+
"verify_webhook_signature",
|
|
37
|
+
"SocialAccount",
|
|
38
|
+
"Post",
|
|
39
|
+
"PlatformResult",
|
|
40
|
+
"ConnectSession",
|
|
41
|
+
"ManagedUser",
|
|
42
|
+
"PostAnalytics",
|
|
43
|
+
"AnalyticsRollup",
|
|
44
|
+
]
|