omnisocials 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,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OmniSocials
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,397 @@
1
+ Metadata-Version: 2.4
2
+ Name: omnisocials
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the OmniSocials Public API - schedule and publish social media posts, upload media, and read analytics across 11 platforms.
5
+ Project-URL: Homepage, https://omnisocials.com
6
+ Project-URL: Documentation, https://docs.omnisocials.com
7
+ Project-URL: Repository, https://github.com/OmniSocials/omnisocials-python
8
+ Author-email: OmniSocials <hello@omnisocials.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,facebook,instagram,linkedin,omnisocials,scheduling,sdk,social media,tiktok,twitter,youtube
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
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: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: httpx<1,>=0.23.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # OmniSocials Python SDK
28
+
29
+ The official Python client for the [OmniSocials](https://omnisocials.com) Public API. Schedule and publish posts, upload media, and read analytics across Instagram, Facebook, LinkedIn, YouTube, TikTok, X, Pinterest, Bluesky, Threads, Mastodon, and Google Business.
30
+
31
+ Full API reference: [https://docs.omnisocials.com](https://docs.omnisocials.com)
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install omnisocials
37
+ ```
38
+
39
+ Requires Python 3.9+. The only dependency is [httpx](https://www.python-httpx.org/).
40
+
41
+ ## Quickstart
42
+
43
+ ```python
44
+ from omnisocials import OmniSocials
45
+
46
+ client = OmniSocials() # reads OMNISOCIALS_API_KEY from the environment
47
+ post = client.posts.create(content="Hello from Python", channels=["instagram", "linkedin"])
48
+ print(post["data"]["id"])
49
+ ```
50
+
51
+ ## Authentication
52
+
53
+ Create an API key in the OmniSocials app under **Settings -> API Keys**. Keys look like `omsk_live_...` (or `omsk_test_...` for test keys) and are bound to one workspace.
54
+
55
+ Pass the key explicitly or set the `OMNISOCIALS_API_KEY` environment variable:
56
+
57
+ ```python
58
+ from omnisocials import OmniSocials
59
+
60
+ client = OmniSocials(api_key="omsk_live_...")
61
+ # or: export OMNISOCIALS_API_KEY=omsk_live_... and just OmniSocials()
62
+ ```
63
+
64
+ The client is a context manager; use `with` (or call `client.close()`) to release connections:
65
+
66
+ ```python
67
+ with OmniSocials() as client:
68
+ accounts = client.accounts.list()
69
+ ```
70
+
71
+ ## Async client
72
+
73
+ Every resource and method exists on `AsyncOmniSocials` with identical names:
74
+
75
+ ```python
76
+ import asyncio
77
+ from omnisocials import AsyncOmniSocials
78
+
79
+ async def main():
80
+ async with AsyncOmniSocials() as client:
81
+ posts = await client.posts.list(status="scheduled")
82
+ print(len(posts["data"]))
83
+
84
+ asyncio.run(main())
85
+ ```
86
+
87
+ ## Posts
88
+
89
+ ### Schedule a post
90
+
91
+ ```python
92
+ post = client.posts.create(
93
+ content="Big announcement coming Friday",
94
+ channels=["instagram", "facebook", "linkedin"],
95
+ scheduled_at="2026-08-01T09:00:00Z",
96
+ media_urls=["https://example.com/teaser.jpg"],
97
+ )
98
+ ```
99
+
100
+ Omit `scheduled_at` to create a draft, or use `create_and_publish` to publish immediately:
101
+
102
+ ```python
103
+ post = client.posts.create_and_publish(
104
+ content="We are live!",
105
+ channels=["x", "bluesky"],
106
+ )
107
+ ```
108
+
109
+ ### Post to specific platforms with platform options
110
+
111
+ `content` can be a per-platform mapping (with `default` as the fallback), and each platform has its own options object:
112
+
113
+ ```python
114
+ post = client.posts.create(
115
+ content={
116
+ "default": "New feature: best times to post",
117
+ "linkedin": "We just shipped best times to post. Here is how it works.",
118
+ },
119
+ channels=["linkedin", "instagram", "youtube"],
120
+ media_urls=["https://example.com/demo.mp4"],
121
+ scheduled_at="2026-08-01T09:00:00Z",
122
+ instagram={"share_to_feed": True},
123
+ youtube={"title": "Best times to post, explained", "privacy_status": "public"},
124
+ )
125
+ ```
126
+
127
+ Note that `linkedin` targets a personal LinkedIn profile and `linkedin_page` targets a LinkedIn company page. Both can be connected to the same workspace and posted to independently.
128
+
129
+ ### X thread example
130
+
131
+ Pass 2 to 25 `thread_parts` to publish a chained thread instead of a single tweet (each part is at most 280 characters). Bluesky and Mastodon support the same `thread_parts` shape:
132
+
133
+ ```python
134
+ post = client.posts.create(
135
+ content="How we cut our API response times by 80%", # fallback text
136
+ channels=["x"],
137
+ scheduled_at="2026-08-01T15:00:00Z",
138
+ x={
139
+ "thread_parts": [
140
+ {"text": "How we cut our API response times by 80%. A thread:"},
141
+ {"text": "1/ We started by profiling every endpoint under real load."},
142
+ {"text": "2/ The biggest win: batching analytics reads into one query."},
143
+ {"text": "3/ Full write-up on the blog. Thanks for reading!"},
144
+ ]
145
+ },
146
+ )
147
+ ```
148
+
149
+ On update, passing `x={"thread_parts": None}` clears the thread and reverts the post to single-tweet mode. Only top-level `None` values are dropped from request bodies, so nested `None` values like this one are sent as JSON `null`.
150
+
151
+ ### Other post operations
152
+
153
+ ```python
154
+ posts = client.posts.list(status="scheduled", limit=50, offset=0)
155
+ post = client.posts.get("123")
156
+ client.posts.update("123", scheduled_at="2026-08-02T10:00:00Z")
157
+ client.posts.publish("123") # publish a draft or scheduled post now
158
+ client.posts.delete("123") # returns None (204)
159
+
160
+ # Recent posts fetched live from the connected platforms (including content
161
+ # published outside OmniSocials). Requires the analytics:read scope.
162
+ recent = client.posts.recent_platform(limit=10, platforms=["instagram", "tiktok"])
163
+ ```
164
+
165
+ ## Media
166
+
167
+ ### Upload from a URL (recommended, up to 1GB)
168
+
169
+ ```python
170
+ media = client.media.upload_from_url(
171
+ url="https://example.com/promo.mp4",
172
+ name="promo-august",
173
+ folder="Campaigns",
174
+ )
175
+ media_id = media["data"]["id"]
176
+ ```
177
+
178
+ Files over 100MB are processed in the background: the response has `data["status"] == "processing"`; poll `client.media.get(media_id)` until it is `"ready"` before attaching it to a post.
179
+
180
+ ### Upload a local file (multipart, max 100MB)
181
+
182
+ ```python
183
+ media = client.media.upload(file="/path/to/image.jpg", name="hero-shot")
184
+ # file can also be raw bytes or a binary file-like object
185
+ ```
186
+
187
+ Uploading a PDF splits it into image slides (max 20 pages) and returns `slides` plus a `media_ids` array. Pass all of `media_ids`, in order, to `posts.create` to publish the deck as a carousel (a native swipeable document on LinkedIn, an image carousel elsewhere).
188
+
189
+ Every upload response also includes a `compatibility` block listing any connected platforms that would reject the file.
190
+
191
+ ### Other media operations
192
+
193
+ ```python
194
+ files = client.media.list(search="promo", folder_id="42")
195
+ item = client.media.get("1001")
196
+ client.media.update("1001", name="renamed", folder_id="42")
197
+ client.media.update("1001", folder_id=None) # move to the root ("All media")
198
+ client.media.delete("1001")
199
+
200
+ # Base64 upload (handy when you have bytes but no URL)
201
+ client.media.upload_from_base64(data=b64_string, mime_type="image/png")
202
+
203
+ # Preflight compatibility BEFORE uploading
204
+ result = client.media.check(url="https://example.com/huge.mp4")
205
+ if not result["compatible"]:
206
+ print(result["summary"])
207
+ ```
208
+
209
+ ### Large file uploads with a presigned URL
210
+
211
+ `create_upload_url` mints a one-time upload URL so any environment (a CI job, a sandbox, a script without the SDK) can upload a file without auth headers:
212
+
213
+ ```python
214
+ import httpx
215
+
216
+ grant = client.media.create_upload_url()
217
+ # grant contains: upload_url, upload_token, expires_in_seconds (600),
218
+ # method ("POST"), content_type ("multipart/form-data")
219
+
220
+ with open("/path/to/video.mp4", "rb") as f:
221
+ response = httpx.post(grant["upload_url"], files={"file": f})
222
+ media_id = response.json()["data"]["id"]
223
+ ```
224
+
225
+ The token is single-use and expires after 10 minutes. The response JSON contains the created media item with its `id` (or a `media_ids` array for a PDF).
226
+
227
+ ## Folders
228
+
229
+ ```python
230
+ folders = client.folders.list()
231
+ folder = client.folders.create(name="Campaigns")
232
+ sub = client.folders.create(name="August", parent_id=folder["data"]["id"])
233
+ client.folders.update(sub["data"]["id"], name="August 2026")
234
+ client.folders.update(sub["data"]["id"], parent_id=None) # move to top level
235
+ client.folders.delete(folder["data"]["id"]) # files move to root, subfolders move up
236
+ ```
237
+
238
+ ## Accounts
239
+
240
+ ```python
241
+ accounts = client.accounts.list()
242
+ for account in accounts["data"]:
243
+ print(account["platform"], account["username"])
244
+
245
+ account = client.accounts.get("instagram")
246
+ ```
247
+
248
+ ## Analytics
249
+
250
+ ```python
251
+ # One post
252
+ stats = client.analytics.post("123")
253
+
254
+ # Up to 100 posts in one request (avoids rate limit trouble when syncing)
255
+ bulk = client.analytics.posts(["123", "124", "125"])
256
+
257
+ # Workspace overview
258
+ overview = client.analytics.overview(period="30d")
259
+
260
+ # Account-level stats (followers etc.)
261
+ accounts = client.analytics.accounts(platform="instagram")
262
+ ```
263
+
264
+ ### Best times to post
265
+
266
+ ```python
267
+ best = client.analytics.best_times(platform="instagram", timezone="Europe/Amsterdam")
268
+ for slot in best["data"]["best_times"]:
269
+ print(slot)
270
+ ```
271
+
272
+ ## Locations (Instagram place tagging)
273
+
274
+ ```python
275
+ results = client.locations.search("Blue Bottle Coffee Oakland")
276
+ location_id = results["data"][0]["id"]
277
+ client.locations.validate(location_id)
278
+
279
+ client.posts.create(
280
+ content="Great coffee here",
281
+ channels=["instagram"],
282
+ media_urls=["https://example.com/latte.jpg"],
283
+ location_id=location_id,
284
+ )
285
+ ```
286
+
287
+ ## Webhooks
288
+
289
+ Subscribe to `post.scheduled`, `post.published`, and `post.failed` events:
290
+
291
+ ```python
292
+ webhook = client.webhooks.create(
293
+ url="https://example.com/hooks/omnisocials",
294
+ events=["post.published", "post.failed"],
295
+ )
296
+ secret = webhook["data"]["secret"] # only shown once, store it
297
+
298
+ client.webhooks.list()
299
+ client.webhooks.get(webhook["data"]["id"])
300
+ client.webhooks.update(webhook["data"]["id"], is_active=False)
301
+ client.webhooks.rotate_secret(webhook["data"]["id"]) # new secret, shown once
302
+ client.webhooks.delete(webhook["data"]["id"])
303
+ ```
304
+
305
+ ### Verifying webhook signatures (FastAPI example)
306
+
307
+ Every delivery is signed with HMAC-SHA256. The `X-OmniSocials-Signature` header has the form `t=<unix>,v1=<hex>`, signed over `"{timestamp}.{raw_body}"`. Use `verify_webhook_signature` with the raw request body (do not parse it first):
308
+
309
+ ```python
310
+ from fastapi import FastAPI, Header, HTTPException, Request
311
+ from omnisocials import WebhookVerificationError, verify_webhook_signature
312
+
313
+ app = FastAPI()
314
+ WEBHOOK_SECRET = "whsec_from_webhook_creation"
315
+
316
+ @app.post("/hooks/omnisocials")
317
+ async def omnisocials_webhook(
318
+ request: Request,
319
+ x_omnisocials_signature: str = Header(None),
320
+ ):
321
+ payload = await request.body() # raw bytes, exactly as received
322
+ try:
323
+ event = verify_webhook_signature(
324
+ payload, x_omnisocials_signature, WEBHOOK_SECRET, tolerance=300
325
+ )
326
+ except WebhookVerificationError:
327
+ raise HTTPException(status_code=400, detail="Invalid signature")
328
+
329
+ if event["type"] == "post.published":
330
+ for target in event["data"]["targets"]:
331
+ print(target["platform"], target["status"], target.get("native_post_id"))
332
+ return {"ok": True}
333
+ ```
334
+
335
+ `verify_webhook_signature` uses a constant-time comparison, rejects timestamps older than `tolerance` seconds (default 300) to block replays, and returns the parsed event dict on success. Respond with any 2xx within 10 seconds to acknowledge; deliveries are retried up to 3 times.
336
+
337
+ ## Error handling
338
+
339
+ Non-2xx responses raise typed exceptions with `status`, `code`, `message`, and the parsed `body`:
340
+
341
+ ```python
342
+ from omnisocials import (
343
+ OmniSocials,
344
+ APIError,
345
+ APIConnectionError,
346
+ AuthenticationError,
347
+ NotFoundError,
348
+ RateLimitError,
349
+ ValidationError,
350
+ )
351
+
352
+ client = OmniSocials()
353
+
354
+ try:
355
+ client.posts.get("does-not-exist")
356
+ except NotFoundError as exc:
357
+ print("Gone:", exc.code, exc.message)
358
+ except RateLimitError as exc:
359
+ print(f"Slow down, retry in {exc.retry_after}s")
360
+ except ValidationError as exc:
361
+ print("Bad request:", exc.body)
362
+ except AuthenticationError:
363
+ print("Check your API key")
364
+ except APIConnectionError:
365
+ print("Network problem")
366
+ except APIError as exc:
367
+ print("API error:", exc.status, exc.message)
368
+ ```
369
+
370
+ The full hierarchy: `OmniSocialsError` is the base; `APIError` covers all HTTP errors with subclasses `AuthenticationError` (401), `PermissionDeniedError` (403), `NotFoundError` (404), `ValidationError` (400/422), `RateLimitError` (429, with `.retry_after`), and `ServerError` (5xx); `APIConnectionError` covers network failures; `WebhookVerificationError` covers signature failures.
371
+
372
+ ## Rate limits
373
+
374
+ The API allows **100 requests per minute** per API key. Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. On 429 the SDK automatically waits for the `Retry-After` value and retries. When syncing analytics for many posts, prefer the bulk `client.analytics.posts([...])` endpoint (100 posts per request) over per-post calls.
375
+
376
+ ## Retries and timeouts
377
+
378
+ The SDK automatically retries on 429, 5xx, and connection errors with exponential backoff (0.5s, 1s, 2s, with jitter), honoring the `Retry-After` header when present. Other 4xx errors are never retried. Defaults: 2 retries, 30 second timeout. Both are configurable:
379
+
380
+ ```python
381
+ client = OmniSocials(
382
+ api_key="omsk_live_...",
383
+ timeout=60.0, # seconds
384
+ max_retries=5, # retries after the first attempt
385
+ base_url="https://api.omnisocials.com/v1", # override for testing
386
+ )
387
+ ```
388
+
389
+ ## More
390
+
391
+ - Full API reference and guides: [https://docs.omnisocials.com](https://docs.omnisocials.com)
392
+ - MCP server for AI agents: `npx -y @omnisocials/mcp-server`
393
+ - Agent skills CLI: `npx skills add OmniSocials/omnisocials-agent-skills`
394
+
395
+ ## License
396
+
397
+ MIT