polari-sdk 0.2.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,423 @@
1
+ Metadata-Version: 2.4
2
+ Name: polari-sdk
3
+ Version: 0.2.0
4
+ Summary: Official Python SDK for the Polari API
5
+ Author-email: Polari Technologies <hello@polariapi.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://polariapi.com
8
+ Project-URL: Documentation, https://docs.polariapi.com
9
+ Project-URL: Repository, https://github.com/polariapi/polari-sdk
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: httpx>=0.25.0
19
+ Requires-Dist: pydantic>=2.0.0
20
+ Requires-Dist: python-dateutil>=2.8.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
24
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
25
+ Requires-Dist: black>=23.0.0; extra == "dev"
26
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
28
+
29
+ # Polari Python SDK
30
+
31
+ Official Python SDK for the [Polari API](https://polariapi.com) — semantic news intelligence.
32
+
33
+ ## Requirements
34
+
35
+ - Python 3.10+
36
+ - A Polari API key ([get one at polariapi.com](https://polariapi.com))
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install polari-sdk
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ import asyncio
48
+ from polari import PolariClient, ArticleInput
49
+
50
+ async def main():
51
+ async with PolariClient(api_key="pk_live_...") as client:
52
+ article = ArticleInput(
53
+ title="Fed holds rates steady",
54
+ content="The Federal Reserve held interest rates steady on Wednesday...",
55
+ url="https://reuters.com/fed-rates-2026",
56
+ source="Reuters",
57
+ )
58
+
59
+ result = await client.layer0.analyze(article)
60
+ print(f"Quality: {result.quality_score:.3f} ID: {result.article_id}")
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ## Authentication
66
+
67
+ All requests require a Bearer API key. Pass it at client initialization:
68
+
69
+ ```python
70
+ client = PolariClient(api_key="pk_live_...")
71
+ ```
72
+
73
+ Or via environment variable:
74
+
75
+ ```bash
76
+ export POLARI_API_KEY="pk_live_..."
77
+ ```
78
+
79
+ ```python
80
+ client = PolariClient.from_env()
81
+ ```
82
+
83
+ ## Client configuration
84
+
85
+ All layer URLs default to the correct Polari API endpoints — no configuration needed for standard use. Override only if needed:
86
+
87
+ ```python
88
+ client = PolariClient(
89
+ api_key="pk_live_...",
90
+ timeout=60,
91
+ max_retries=3,
92
+ )
93
+ ```
94
+
95
+ The client is an async context manager. Use `async with` to ensure the HTTP connection is closed cleanly. Alternatively call `await client.close()` manually.
96
+
97
+ ---
98
+
99
+
100
+
101
+ # README addition — paste this section into README.md
102
+ # Suggested placement: after "Authentication" section, before "Quickstart"
103
+ #
104
+ # ─────────────────────────────────────────────────────────────────────────────
105
+
106
+ ## Namespaces & Quality Gate
107
+
108
+ Every API key operates in one of three namespace modes. The mode is set when
109
+ your key is issued — no SDK configuration is required.
110
+
111
+ | Mode | Articles written to | Articles visible | Quality gate |
112
+ |------|--------------------|--------------------|--------------|
113
+ | `public` | Shared pool | Shared pool | Enforced (default floor: 0.53) |
114
+ | `private` | Your namespace only | Your namespace only | Bypassed |
115
+ | `shared` | Your namespace only | Shared pool + yours | Bypassed on write |
116
+
117
+ **Public (default):** Articles enter the shared corpus. During the Layer 0 → Layer 1
118
+ handoff, any article whose quality score falls below the gate floor (0.53 by default)
119
+ is silently dropped and will not appear in Layer 1 or Layer 2 output. This protects
120
+ the shared pool from low-signal content.
121
+
122
+ **Private:** Articles are fully isolated to your key. The quality gate is bypassed
123
+ entirely — all articles proceed to Layer 1 regardless of score. Private namespace
124
+ requires a Professional or Enterprise key. To request one, contact
125
+ [support@polariapi.com](mailto:support@polariapi.com) or select Private Namespace
126
+ during signup.
127
+
128
+ **Shared:** Write-isolated, read-public. Your ingested articles bypass the quality
129
+ gate and land in your own namespace. Read operations (entities, stories, clusters)
130
+ span both your namespace and the shared public pool.
131
+
132
+ ### Diagnosing dropped articles
133
+
134
+ If an article passes Layer 0 but never appears in Layer 1 output, the most likely
135
+ cause is the quality gate. You can check the quality score from the Layer 0 response:
136
+
137
+ ```python
138
+ result = await client.layer0.analyze(article)
139
+ print(result.quality_score) # must be >= 0.53 for public namespace keys
140
+ ```
141
+
142
+ If you need to ingest content that consistently scores below the gate floor (internal
143
+ documents, short-form content, domain-specific text), request a private namespace key.
144
+
145
+
146
+
147
+
148
+
149
+ ## Layer 0 — Token Intelligence
150
+
151
+ Layer 0 processes raw articles into quality-scored, embedded representations.
152
+
153
+ ```python
154
+ from polari import ArticleInput
155
+
156
+ article = ArticleInput(
157
+ title="Fed holds rates steady",
158
+ content="The Federal Reserve held interest rates steady...",
159
+ url="https://reuters.com/fed-rates-2026", # used as deduplication key
160
+ source="Reuters",
161
+ )
162
+
163
+ result = await client.layer0.analyze(article)
164
+
165
+ result.article_id # "art_8f7h2k9s"
166
+ result.quality_score # 0.87 (0.0–1.0; articles below 0.53 are filtered)
167
+ result.token_count # 412
168
+ result.is_duplicate # True if this URL was already processed
169
+ result.semantic_hash # content fingerprint for fast similarity matching
170
+ ```
171
+
172
+ **Quality score tiers:**
173
+
174
+ | Score | Tier |
175
+ |-------|------|
176
+ | 0.8+ | Excellent |
177
+ | 0.65–0.8 | Good |
178
+ | 0.5–0.65 | Medium |
179
+ | 0.3–0.5 | Low |
180
+ | <0.3 | Noise |
181
+
182
+ Articles below `0.53` are not forwarded to Layer 1 or Layer 2.
183
+
184
+ ### Embeddings
185
+
186
+ By default, `embedding` is not returned (saves ~1.5KB per article). Pass `include_embedding=True` to fetch the 384-dimensional vector from ChromaDB:
187
+
188
+ ```python
189
+ result = await client.layer0.analyze(article, include_embedding=True)
190
+ result.embedding # List[float], length 384
191
+ ```
192
+
193
+ ### Batch processing
194
+
195
+ ```python
196
+ articles = [ArticleInput(...), ArticleInput(...), ArticleInput(...)]
197
+ results = await client.layer0.analyze_batch(articles)
198
+ # Submits all at once, polls concurrently — significantly faster than sequential calls
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Layer 1 — Semantic Analysis
204
+
205
+ Layer 1 extracts entities, sentence semantics, and sentiment. **Requires Layer 0 first.**
206
+
207
+ ```python
208
+ result = await client.layer1.process(
209
+ article_id=l0.article_id,
210
+ title=article.title,
211
+ content=article.content,
212
+ url=article.url, # optional
213
+ published_date="2026-05-29T00:00:00", # optional
214
+ )
215
+
216
+ result.stats.sentence_count # 12
217
+ result.stats.entity_count # 8
218
+ result.entities # {"PERSON": ["Jerome Powell"], "ORG": ["Federal Reserve"], "GPE": ["Washington"]}
219
+ result.locations # ["Washington"]
220
+ result.article_embedding # 384-dim article-level embedding
221
+ result.processed_at # datetime
222
+ ```
223
+
224
+ ### Batch processing
225
+
226
+ ```python
227
+ results = await client.layer1.process_batch([
228
+ {"article_id": "art_xxx", "title": "...", "content": "..."},
229
+ {"article_id": "art_yyy", "title": "...", "content": "..."},
230
+ ])
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Layer 2 — Story Clustering
236
+
237
+ Layer 2 groups related articles across sources into unified stories. **Requires Layer 0 and Layer 1 first.**
238
+
239
+ ```python
240
+ result = await client.layer2.cluster(article_id)
241
+
242
+ result.cluster_id # "clus_9x3k2m8f"
243
+ result.confidence # 0.94
244
+ result.is_new # True if this article formed a new cluster
245
+ result.already_clustered # True if this article was previously clustered
246
+ ```
247
+
248
+ ### Batch clustering
249
+
250
+ ```python
251
+ result = await client.layer2.cluster_batch(
252
+ article_ids=["art_xxx", "art_yyy", "art_zzz"],
253
+ similarity_threshold=0.75, # default
254
+ time_window_hours=168, # default (7 days)
255
+ )
256
+
257
+ result.stats.clusters_formed # 2
258
+ result.stats.clustering_rate # 1.0
259
+ result.stats.singleton_articles # 0
260
+
261
+ for cluster in result.clusters:
262
+ cluster.cluster_id # "clus_xxx"
263
+ cluster.title # "Fed holds rates steady"
264
+ cluster.article_count # 3
265
+ cluster.source_count # 2
266
+ cluster.confidence # 0.89
267
+ ```
268
+
269
+ ### Story queries
270
+
271
+ ```python
272
+ stories = await client.layer2.list_stories(limit=20, offset=0)
273
+ detail = await client.layer2.get_story("clus_xxx")
274
+ articles = await client.layer2.get_story_articles("clus_xxx")
275
+ timeline = await client.layer2.get_story_timeline("clus_xxx")
276
+ sources = await client.layer2.get_story_sources("clus_xxx")
277
+ rels = await client.layer2.get_stories_relationships(["clus_xxx", "clus_yyy"])
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Layer 3 — Intelligence Graph
283
+
284
+ Layer 3 maps entity relationships and reveals trends across the information landscape. **Requires Professional tier or higher.**
285
+
286
+ ```python
287
+ # Graph statistics
288
+ stats = await client.layer3.get_stats()
289
+ stats["entity_relationships"] # 5204
290
+ stats["cluster_relationships"] # 48405
291
+ stats["narrative_threads"] # 18
292
+ stats["trending_entities"] # 49
293
+
294
+ # Trending entities
295
+ result = await client.layer3.get_trending_entities(min_velocity=2.0, limit=20)
296
+ for trend in result["trends"]:
297
+ trend["entity"] # "Federal Reserve"
298
+ trend["velocity"] # 7.0
299
+ trend["mentions"] # 51
300
+
301
+ # Cluster relationships
302
+ rels = await client.layer3.get_cluster_relationships("clus_xxx")
303
+
304
+ # Trigger full graph rebuild (admin)
305
+ await client.layer3.build_graph()
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Full pipeline example
311
+
312
+ ```python
313
+ import asyncio
314
+ from polari import PolariClient, ArticleInput
315
+
316
+ async def main():
317
+ async with PolariClient(api_key="pk_live_...") as client:
318
+
319
+ article = ArticleInput(
320
+ title="Fed holds rates steady",
321
+ content="The Federal Reserve held interest rates steady on Wednesday...",
322
+ url="https://reuters.com/fed-rates-2026",
323
+ source="Reuters",
324
+ )
325
+
326
+ # Layer 0 — quality scoring
327
+ l0 = await client.layer0.analyze(article)
328
+ if l0.quality_score < 0.53:
329
+ print("Article below quality threshold, skipping")
330
+ return
331
+
332
+ # Layer 1 — entity extraction
333
+ l1 = await client.layer1.process(
334
+ article_id=l0.article_id,
335
+ title=article.title,
336
+ content=article.content,
337
+ )
338
+ print(f"Entities: {l1.entities}")
339
+
340
+ # Layer 2 — story clustering
341
+ l2 = await client.layer2.cluster(l0.article_id)
342
+ print(f"Cluster: {l2.cluster_id} Confidence: {l2.confidence:.3f}")
343
+
344
+ # Layer 3 — trending entities
345
+ trends = await client.layer3.get_trending_entities(limit=5)
346
+ for t in trends["trends"]:
347
+ print(f" {t['entity']}: velocity={t['velocity']}")
348
+
349
+ asyncio.run(main())
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Error handling
355
+
356
+ ```python
357
+ from polari.exceptions import (
358
+ AuthenticationError, # 401 — invalid or revoked API key
359
+ ValidationError, # 400/422 — bad request parameters
360
+ RateLimitError, # 429 — rate limit exceeded
361
+ ServerError, # 5xx — service error
362
+ NetworkError, # connection failure
363
+ TimeoutError, # request timed out
364
+ ProcessingError, # article processing failed
365
+ RetryExhaustedError, # all retry attempts failed
366
+ )
367
+
368
+ try:
369
+ result = await client.layer0.analyze(article)
370
+ except AuthenticationError:
371
+ print("Invalid API key")
372
+ except RateLimitError:
373
+ print("Rate limit hit — back off and retry")
374
+ except RetryExhaustedError as e:
375
+ print(f"Failed after retries: {e}")
376
+ ```
377
+
378
+ The SDK retries `429`, `5xx`, network errors, and timeouts automatically with exponential backoff (default: 3 retries). `401`, `400`, `422`, and `404` are not retried.
379
+
380
+ ---
381
+
382
+ ## Metrics and cost tracking
383
+
384
+ ```python
385
+ metrics = client.get_metrics()
386
+ metrics.total_requests # 42
387
+ metrics.successful_requests # 41
388
+ metrics.average_latency # 0.34 (seconds)
389
+
390
+ cost = client.get_cost_summary()
391
+ cost.total_calls # 42
392
+ cost.total_cost # 0.063
393
+ cost.cost_by_layer # {"layer0": 0.01, "layer1": 0.02, "layer2": 0.01, "layer3": 0.03}
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Tier access
399
+
400
+ | Tier | Layers | Requests/day | Requests/min |
401
+ |------|--------|-------------|-------------|
402
+ | Starter | 0, 1, 2 | 10,000 | 10 |
403
+ | Professional | 0, 1, 2, 3 | 100,000 | 100 |
404
+ | Enterprise | 0, 1, 2, 3 | Custom | 1,000 |
405
+
406
+ Layer 3 (Intelligence Graph) and Trends endpoints require Professional tier or higher.
407
+ Starter tier keys receive `403` on all Layer 3 and Trends requests.
408
+
409
+ ---
410
+
411
+ ## Known limitations
412
+
413
+ - `Layer0Result.embedding` returns `[0.0] * 384` unless `include_embedding=True` is passed. Embedding retrieval adds latency due to ChromaDB lookup.
414
+ - `Layer1Result.sentences` returns empty for articles already processed by Layer 1 (cached result). Entity counts are still populated.
415
+ - Integration tests reuse article URLs — re-running tests returns cached results for already-processed articles. Append a unique suffix to URLs to force fresh processing.
416
+
417
+ ---
418
+
419
+ ## Support
420
+
421
+ - Docs: [docs.polariapi.com](https://docs.polariapi.com)
422
+ - Email: [support@polariapi.com](mailto:support@polariapi.com)
423
+ - Homepage: [polariapi.com](https://polariapi.com)