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.
@@ -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
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .env
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .DS_Store
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
@@ -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
+ ]