postproxy-sdk 1.0.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,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ *.egg
9
+ .venv/
@@ -0,0 +1,263 @@
1
+ Metadata-Version: 2.4
2
+ Name: postproxy-sdk
3
+ Version: 1.0.0
4
+ Summary: Async Python client for the PostProxy API
5
+ Project-URL: Homepage, https://postproxy.dev
6
+ Project-URL: Documentation, https://postproxy.dev/getting-started/overview/
7
+ Project-URL: Repository, https://github.com/postproxy/postproxy-python
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.25.0
11
+ Requires-Dist: pydantic>=2.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: mypy>=1.19; extra == 'dev'
14
+ Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
15
+ Requires-Dist: pytest>=9.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # PostProxy Python SDK
19
+
20
+ Async Python client for the [PostProxy API](https://postproxy.dev). Fully typed with Pydantic v2 models and async/await via httpx.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install postproxy-sdk
26
+ ```
27
+
28
+ Requires Python 3.10+.
29
+
30
+ ## Quick start
31
+
32
+ ```python
33
+ import asyncio
34
+ from postproxy import PostProxy
35
+
36
+ async def main():
37
+ async with PostProxy("your-api-key", profile_group_id="pg-abc") as client:
38
+ # List profiles
39
+ profiles = await client.profiles.list()
40
+
41
+ # Create a post
42
+ post = await client.posts.create(
43
+ "Hello from PostProxy!",
44
+ profiles=[profiles[0].id],
45
+ )
46
+ print(post.id, post.status)
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### Client
54
+
55
+ ```python
56
+ from postproxy import PostProxy
57
+
58
+ # Basic
59
+ client = PostProxy("your-api-key")
60
+
61
+ # With a default profile group (applied to all requests)
62
+ client = PostProxy("your-api-key", profile_group_id="pg-abc")
63
+
64
+ # With a custom httpx client
65
+ import httpx
66
+ client = PostProxy("your-api-key", httpx_client=httpx.AsyncClient(timeout=30))
67
+
68
+ # As a context manager (auto-closes the HTTP client)
69
+ async with PostProxy("your-api-key") as client:
70
+ ...
71
+
72
+ # Manual cleanup
73
+ await client.close()
74
+ ```
75
+
76
+ ### Posts
77
+
78
+ ```python
79
+ # List posts (paginated)
80
+ page = await client.posts.list(page=0, per_page=10, status="draft")
81
+ print(page.total, page.data)
82
+
83
+ # Filter by platform and schedule
84
+ from datetime import datetime
85
+ page = await client.posts.list(
86
+ platforms=["instagram", "tiktok"],
87
+ scheduled_after=datetime(2025, 6, 1),
88
+ )
89
+
90
+ # Get a single post
91
+ post = await client.posts.get("post-id")
92
+
93
+ # Create a post
94
+ post = await client.posts.create(
95
+ "Check out our new product!",
96
+ profiles=["profile-id-1", "profile-id-2"],
97
+ )
98
+
99
+ # Create a draft
100
+ post = await client.posts.create(
101
+ "Draft content",
102
+ profiles=["profile-id"],
103
+ draft=True,
104
+ )
105
+
106
+ # Create with media URLs
107
+ post = await client.posts.create(
108
+ "Photo post",
109
+ profiles=["profile-id"],
110
+ media=["https://example.com/image.jpg"],
111
+ )
112
+
113
+ # Create with local file uploads
114
+ post = await client.posts.create(
115
+ "Posted with a local file!",
116
+ profiles=["profile-id"],
117
+ media_files=["./photo.jpg", "./video.mp4"],
118
+ )
119
+
120
+ # Mix media URLs and local files
121
+ post = await client.posts.create(
122
+ "Mixed media",
123
+ profiles=["profile-id"],
124
+ media=["https://example.com/image.jpg"],
125
+ media_files=["./local-photo.jpg"],
126
+ )
127
+
128
+ # Create with platform-specific params
129
+ from postproxy import PlatformParams, InstagramParams, TikTokParams
130
+
131
+ post = await client.posts.create(
132
+ "Cross-platform post",
133
+ profiles=["ig-profile", "tt-profile"],
134
+ platforms=PlatformParams(
135
+ instagram=InstagramParams(format="reel", collaborators=["@friend"]),
136
+ tiktok=TikTokParams(format="video", privacy_status="PUBLIC_TO_EVERYONE"),
137
+ ),
138
+ )
139
+
140
+ # Schedule a post
141
+ post = await client.posts.create(
142
+ "Scheduled post",
143
+ profiles=["profile-id"],
144
+ scheduled_at="2025-12-25T09:00:00Z",
145
+ )
146
+
147
+ # Publish a draft
148
+ post = await client.posts.publish_draft("post-id")
149
+
150
+ # Delete a post
151
+ result = await client.posts.delete("post-id")
152
+ print(result.deleted) # True
153
+ ```
154
+
155
+ ### Profiles
156
+
157
+ ```python
158
+ # List all profiles
159
+ profiles = await client.profiles.list()
160
+
161
+ # List profiles in a specific group (overrides client default)
162
+ profiles = await client.profiles.list(profile_group_id="pg-other")
163
+
164
+ # Get a single profile
165
+ profile = await client.profiles.get("profile-id")
166
+ print(profile.name, profile.platform, profile.status)
167
+
168
+ # Get available placements for a profile
169
+ placements = await client.profiles.placements("profile-id")
170
+ for p in placements:
171
+ print(p.id, p.name)
172
+
173
+ # Delete a profile
174
+ result = await client.profiles.delete("profile-id")
175
+ print(result.success) # True
176
+ ```
177
+
178
+ ### Profile Groups
179
+
180
+ ```python
181
+ # List all groups
182
+ groups = await client.profile_groups.list()
183
+
184
+ # Get a single group
185
+ group = await client.profile_groups.get("pg-id")
186
+ print(group.name, group.profiles_count)
187
+
188
+ # Create a group
189
+ group = await client.profile_groups.create("My New Group")
190
+
191
+ # Delete a group (must have no profiles)
192
+ result = await client.profile_groups.delete("pg-id")
193
+ print(result.deleted) # True
194
+
195
+ # Initialize a social platform connection
196
+ conn = await client.profile_groups.initialize_connection(
197
+ "pg-id",
198
+ platform="instagram",
199
+ redirect_url="https://yourapp.com/callback",
200
+ )
201
+ print(conn.url) # Redirect the user to this URL
202
+ ```
203
+
204
+ ## Error handling
205
+
206
+ All errors extend `PostProxyError`, which includes the HTTP status code and raw response body:
207
+
208
+ ```python
209
+ from postproxy import (
210
+ PostProxyError,
211
+ AuthenticationError, # 401
212
+ BadRequestError, # 400
213
+ NotFoundError, # 404
214
+ ValidationError, # 422
215
+ )
216
+
217
+ try:
218
+ await client.posts.get("nonexistent")
219
+ except NotFoundError as e:
220
+ print(e.status_code) # 404
221
+ print(e.response) # {"error": "Not found"}
222
+ except PostProxyError as e:
223
+ print(f"API error {e.status_code}: {e}")
224
+ ```
225
+
226
+ ## Types
227
+
228
+ All responses are parsed into Pydantic v2 models. Key types:
229
+
230
+ | Model | Fields |
231
+ |---|---|
232
+ | `Post` | id, body, status, scheduled_at, created_at, platforms |
233
+ | `Profile` | id, name, status, platform, profile_group_id, expires_at, post_count |
234
+ | `ProfileGroup` | id, name, profiles_count |
235
+ | `PlatformResult` | platform, status, params, error, attempted_at, insights |
236
+ | `PaginatedResponse[T]` | total, page, per_page, data |
237
+
238
+ ### Platform parameter models
239
+
240
+ | Model | Platform |
241
+ |---|---|
242
+ | `FacebookParams` | format (`post`, `story`), first_comment, page_id |
243
+ | `InstagramParams` | format (`post`, `reel`, `story`), first_comment, collaborators, cover_url, audio_name, trial_strategy, thumb_offset |
244
+ | `TikTokParams` | format (`video`, `image`), privacy_status, photo_cover_index, auto_add_music, made_with_ai, disable_comment, disable_duet, disable_stitch, brand_content_toggle, brand_organic_toggle |
245
+ | `LinkedInParams` | format (`post`), organization_id |
246
+ | `YouTubeParams` | format (`post`), title, privacy_status, cover_url |
247
+ | `PinterestParams` | format (`pin`), title, board_id, destination_link, cover_url, thumb_offset |
248
+ | `ThreadsParams` | format (`post`) |
249
+ | `TwitterParams` | format (`post`) |
250
+
251
+ Wrap them in `PlatformParams` when passing to `posts.create()`.
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ pip install -e ".[dev]"
257
+ pytest
258
+ mypy postproxy/
259
+ ```
260
+
261
+ ## License
262
+
263
+ MIT
@@ -0,0 +1,246 @@
1
+ # PostProxy Python SDK
2
+
3
+ Async Python client for the [PostProxy API](https://postproxy.dev). Fully typed with Pydantic v2 models and async/await via httpx.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install postproxy-sdk
9
+ ```
10
+
11
+ Requires Python 3.10+.
12
+
13
+ ## Quick start
14
+
15
+ ```python
16
+ import asyncio
17
+ from postproxy import PostProxy
18
+
19
+ async def main():
20
+ async with PostProxy("your-api-key", profile_group_id="pg-abc") as client:
21
+ # List profiles
22
+ profiles = await client.profiles.list()
23
+
24
+ # Create a post
25
+ post = await client.posts.create(
26
+ "Hello from PostProxy!",
27
+ profiles=[profiles[0].id],
28
+ )
29
+ print(post.id, post.status)
30
+
31
+ asyncio.run(main())
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Client
37
+
38
+ ```python
39
+ from postproxy import PostProxy
40
+
41
+ # Basic
42
+ client = PostProxy("your-api-key")
43
+
44
+ # With a default profile group (applied to all requests)
45
+ client = PostProxy("your-api-key", profile_group_id="pg-abc")
46
+
47
+ # With a custom httpx client
48
+ import httpx
49
+ client = PostProxy("your-api-key", httpx_client=httpx.AsyncClient(timeout=30))
50
+
51
+ # As a context manager (auto-closes the HTTP client)
52
+ async with PostProxy("your-api-key") as client:
53
+ ...
54
+
55
+ # Manual cleanup
56
+ await client.close()
57
+ ```
58
+
59
+ ### Posts
60
+
61
+ ```python
62
+ # List posts (paginated)
63
+ page = await client.posts.list(page=0, per_page=10, status="draft")
64
+ print(page.total, page.data)
65
+
66
+ # Filter by platform and schedule
67
+ from datetime import datetime
68
+ page = await client.posts.list(
69
+ platforms=["instagram", "tiktok"],
70
+ scheduled_after=datetime(2025, 6, 1),
71
+ )
72
+
73
+ # Get a single post
74
+ post = await client.posts.get("post-id")
75
+
76
+ # Create a post
77
+ post = await client.posts.create(
78
+ "Check out our new product!",
79
+ profiles=["profile-id-1", "profile-id-2"],
80
+ )
81
+
82
+ # Create a draft
83
+ post = await client.posts.create(
84
+ "Draft content",
85
+ profiles=["profile-id"],
86
+ draft=True,
87
+ )
88
+
89
+ # Create with media URLs
90
+ post = await client.posts.create(
91
+ "Photo post",
92
+ profiles=["profile-id"],
93
+ media=["https://example.com/image.jpg"],
94
+ )
95
+
96
+ # Create with local file uploads
97
+ post = await client.posts.create(
98
+ "Posted with a local file!",
99
+ profiles=["profile-id"],
100
+ media_files=["./photo.jpg", "./video.mp4"],
101
+ )
102
+
103
+ # Mix media URLs and local files
104
+ post = await client.posts.create(
105
+ "Mixed media",
106
+ profiles=["profile-id"],
107
+ media=["https://example.com/image.jpg"],
108
+ media_files=["./local-photo.jpg"],
109
+ )
110
+
111
+ # Create with platform-specific params
112
+ from postproxy import PlatformParams, InstagramParams, TikTokParams
113
+
114
+ post = await client.posts.create(
115
+ "Cross-platform post",
116
+ profiles=["ig-profile", "tt-profile"],
117
+ platforms=PlatformParams(
118
+ instagram=InstagramParams(format="reel", collaborators=["@friend"]),
119
+ tiktok=TikTokParams(format="video", privacy_status="PUBLIC_TO_EVERYONE"),
120
+ ),
121
+ )
122
+
123
+ # Schedule a post
124
+ post = await client.posts.create(
125
+ "Scheduled post",
126
+ profiles=["profile-id"],
127
+ scheduled_at="2025-12-25T09:00:00Z",
128
+ )
129
+
130
+ # Publish a draft
131
+ post = await client.posts.publish_draft("post-id")
132
+
133
+ # Delete a post
134
+ result = await client.posts.delete("post-id")
135
+ print(result.deleted) # True
136
+ ```
137
+
138
+ ### Profiles
139
+
140
+ ```python
141
+ # List all profiles
142
+ profiles = await client.profiles.list()
143
+
144
+ # List profiles in a specific group (overrides client default)
145
+ profiles = await client.profiles.list(profile_group_id="pg-other")
146
+
147
+ # Get a single profile
148
+ profile = await client.profiles.get("profile-id")
149
+ print(profile.name, profile.platform, profile.status)
150
+
151
+ # Get available placements for a profile
152
+ placements = await client.profiles.placements("profile-id")
153
+ for p in placements:
154
+ print(p.id, p.name)
155
+
156
+ # Delete a profile
157
+ result = await client.profiles.delete("profile-id")
158
+ print(result.success) # True
159
+ ```
160
+
161
+ ### Profile Groups
162
+
163
+ ```python
164
+ # List all groups
165
+ groups = await client.profile_groups.list()
166
+
167
+ # Get a single group
168
+ group = await client.profile_groups.get("pg-id")
169
+ print(group.name, group.profiles_count)
170
+
171
+ # Create a group
172
+ group = await client.profile_groups.create("My New Group")
173
+
174
+ # Delete a group (must have no profiles)
175
+ result = await client.profile_groups.delete("pg-id")
176
+ print(result.deleted) # True
177
+
178
+ # Initialize a social platform connection
179
+ conn = await client.profile_groups.initialize_connection(
180
+ "pg-id",
181
+ platform="instagram",
182
+ redirect_url="https://yourapp.com/callback",
183
+ )
184
+ print(conn.url) # Redirect the user to this URL
185
+ ```
186
+
187
+ ## Error handling
188
+
189
+ All errors extend `PostProxyError`, which includes the HTTP status code and raw response body:
190
+
191
+ ```python
192
+ from postproxy import (
193
+ PostProxyError,
194
+ AuthenticationError, # 401
195
+ BadRequestError, # 400
196
+ NotFoundError, # 404
197
+ ValidationError, # 422
198
+ )
199
+
200
+ try:
201
+ await client.posts.get("nonexistent")
202
+ except NotFoundError as e:
203
+ print(e.status_code) # 404
204
+ print(e.response) # {"error": "Not found"}
205
+ except PostProxyError as e:
206
+ print(f"API error {e.status_code}: {e}")
207
+ ```
208
+
209
+ ## Types
210
+
211
+ All responses are parsed into Pydantic v2 models. Key types:
212
+
213
+ | Model | Fields |
214
+ |---|---|
215
+ | `Post` | id, body, status, scheduled_at, created_at, platforms |
216
+ | `Profile` | id, name, status, platform, profile_group_id, expires_at, post_count |
217
+ | `ProfileGroup` | id, name, profiles_count |
218
+ | `PlatformResult` | platform, status, params, error, attempted_at, insights |
219
+ | `PaginatedResponse[T]` | total, page, per_page, data |
220
+
221
+ ### Platform parameter models
222
+
223
+ | Model | Platform |
224
+ |---|---|
225
+ | `FacebookParams` | format (`post`, `story`), first_comment, page_id |
226
+ | `InstagramParams` | format (`post`, `reel`, `story`), first_comment, collaborators, cover_url, audio_name, trial_strategy, thumb_offset |
227
+ | `TikTokParams` | format (`video`, `image`), privacy_status, photo_cover_index, auto_add_music, made_with_ai, disable_comment, disable_duet, disable_stitch, brand_content_toggle, brand_organic_toggle |
228
+ | `LinkedInParams` | format (`post`), organization_id |
229
+ | `YouTubeParams` | format (`post`), title, privacy_status, cover_url |
230
+ | `PinterestParams` | format (`pin`), title, board_id, destination_link, cover_url, thumb_offset |
231
+ | `ThreadsParams` | format (`post`) |
232
+ | `TwitterParams` | format (`post`) |
233
+
234
+ Wrap them in `PlatformParams` when passing to `posts.create()`.
235
+
236
+ ## Development
237
+
238
+ ```bash
239
+ pip install -e ".[dev]"
240
+ pytest
241
+ mypy postproxy/
242
+ ```
243
+
244
+ ## License
245
+
246
+ MIT
@@ -0,0 +1,162 @@
1
+ """Examples of creating posts with the PostProxy SDK."""
2
+
3
+ import asyncio
4
+ import os
5
+
6
+ from postproxy import (
7
+ PostProxy,
8
+ PlatformParams,
9
+ FacebookParams,
10
+ InstagramParams,
11
+ TikTokParams,
12
+ YouTubeParams,
13
+ PinterestParams,
14
+ LinkedInParams,
15
+ ThreadsParams,
16
+ TwitterParams,
17
+ )
18
+
19
+ API_KEY = os.environ["POSTPROXY_API_KEY"]
20
+ PROFILE_GROUP_ID = os.environ.get("POSTPROXY_PROFILE_GROUP_ID")
21
+
22
+
23
+ async def simple_post():
24
+ """Create a simple text post."""
25
+ async with PostProxy(API_KEY, profile_group_id=PROFILE_GROUP_ID) as client:
26
+ profiles = await client.profiles.list()
27
+ post = await client.posts.create(
28
+ "Hello from PostProxy Python SDK!",
29
+ profiles=[profiles[0].id],
30
+ )
31
+ print(f"Created post {post.id} — status: {post.status}")
32
+
33
+
34
+ async def post_with_media():
35
+ """Create a post with media URLs."""
36
+ async with PostProxy(API_KEY, profile_group_id=PROFILE_GROUP_ID) as client:
37
+ profiles = await client.profiles.list()
38
+ facebook_profile = next(
39
+ (p for p in profiles if p.platform == "facebook"), None
40
+ )
41
+ if facebook_profile is None:
42
+ raise ValueError("No Facebook profile found")
43
+
44
+ placements = await client.profiles.placements(id=facebook_profile.id)
45
+
46
+ post = await client.posts.create(
47
+ "Check this out!",
48
+ profiles=[facebook_profile.id],
49
+ draft=True,
50
+ media=[
51
+ "https://example.com/photo.jpg",
52
+ ],
53
+ platforms=PlatformParams(
54
+ facebook=FacebookParams(
55
+ format="post",
56
+ page_id=placements[0].id,
57
+ first_comment="First!",
58
+ ),
59
+ ),
60
+ )
61
+ print(f"Created post {post.id} with media — status: {post.status}")
62
+
63
+
64
+ async def post_with_local_file():
65
+ """Create a post with a local file upload."""
66
+ async with PostProxy(API_KEY, profile_group_id=PROFILE_GROUP_ID) as client:
67
+ profiles = await client.profiles.list()
68
+ post = await client.posts.create(
69
+ "Posted with a local file!",
70
+ profiles=[profiles[0].id],
71
+ draft=True,
72
+ media_files=["./photo.jpg"],
73
+ )
74
+ print(f"Created post {post.id} with file upload — status: {post.status}")
75
+
76
+
77
+ async def draft_then_publish():
78
+ """Create a draft post, then publish it."""
79
+ async with PostProxy(API_KEY, profile_group_id=PROFILE_GROUP_ID) as client:
80
+ profiles = await client.profiles.list()
81
+
82
+ # Create as draft
83
+ post = await client.posts.create(
84
+ "This starts as a draft",
85
+ profiles=[profiles[0].id],
86
+ draft=True,
87
+ )
88
+ print(f"Created draft {post.id} — status: {post.status}")
89
+
90
+ # Publish the draft
91
+ post = await client.posts.publish_draft(post.id)
92
+ print(f"Published {post.id} — status: {post.status}")
93
+
94
+
95
+ async def scheduled_post():
96
+ """Schedule a post for the future."""
97
+ async with PostProxy(API_KEY, profile_group_id=PROFILE_GROUP_ID) as client:
98
+ profiles = await client.profiles.list()
99
+ post = await client.posts.create(
100
+ "This post is scheduled!",
101
+ profiles=[profiles[0].id],
102
+ scheduled_at="2025-12-25T09:00:00Z",
103
+ )
104
+ print(f"Scheduled post {post.id} for {post.scheduled_at}")
105
+
106
+
107
+ async def cross_platform_post():
108
+ """Create a post with platform-specific parameters."""
109
+ async with PostProxy(API_KEY, profile_group_id=PROFILE_GROUP_ID) as client:
110
+ profiles = await client.profiles.list()
111
+
112
+ profile_ids = [p.id for p in profiles]
113
+
114
+ post = await client.posts.create(
115
+ "Cross-platform post with custom settings per network",
116
+ profiles=profile_ids,
117
+ draft=True,
118
+ media=["https://example.com/video.mp4"],
119
+ platforms=PlatformParams(
120
+ facebook=FacebookParams(
121
+ format="post",
122
+ first_comment="First!",
123
+ ),
124
+ instagram=InstagramParams(
125
+ format="reel",
126
+ collaborators=["@collab_account"],
127
+ ),
128
+ tiktok=TikTokParams(
129
+ format="video",
130
+ privacy_status="PUBLIC_TO_EVERYONE",
131
+ auto_add_music=True,
132
+ ),
133
+ linkedin=LinkedInParams(
134
+ format="post",
135
+ organization_id="org-123",
136
+ ),
137
+ youtube=YouTubeParams(
138
+ format="post",
139
+ title="My Video Title",
140
+ privacy_status="public",
141
+ ),
142
+ pinterest=PinterestParams(
143
+ format="pin",
144
+ title="Pin Title",
145
+ board_id="board-abc",
146
+ destination_link="https://example.com",
147
+ ),
148
+ threads=ThreadsParams(
149
+ format="post",
150
+ ),
151
+ twitter=TwitterParams(
152
+ format="post",
153
+ ),
154
+ ),
155
+ )
156
+ print(f"Created cross-platform post {post.id}")
157
+ for p in post.platforms:
158
+ print(f" {p.platform}: {p.status}")
159
+
160
+
161
+ if __name__ == "__main__":
162
+ asyncio.run(simple_post())