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.
- polari_sdk-0.2.0/PKG-INFO +423 -0
- polari_sdk-0.2.0/README.md +395 -0
- polari_sdk-0.2.0/polari/__init__.py +95 -0
- polari_sdk-0.2.0/polari/client.py +190 -0
- polari_sdk-0.2.0/polari/config.py +112 -0
- polari_sdk-0.2.0/polari/exceptions.py +90 -0
- polari_sdk-0.2.0/polari/layers/__init__.py +5 -0
- polari_sdk-0.2.0/polari/layers/base.py +144 -0
- polari_sdk-0.2.0/polari/layers/layer0.py +306 -0
- polari_sdk-0.2.0/polari/layers/layer1.py +270 -0
- polari_sdk-0.2.0/polari/layers/layer2.py +263 -0
- polari_sdk-0.2.0/polari/layers/layer3.py +139 -0
- polari_sdk-0.2.0/polari/types.py +202 -0
- polari_sdk-0.2.0/polari/utils/__init__.py +7 -0
- polari_sdk-0.2.0/polari/utils/cost.py +86 -0
- polari_sdk-0.2.0/polari/utils/metrics.py +95 -0
- polari_sdk-0.2.0/polari/utils/retry.py +109 -0
- polari_sdk-0.2.0/polari_sdk.egg-info/PKG-INFO +423 -0
- polari_sdk-0.2.0/polari_sdk.egg-info/SOURCES.txt +22 -0
- polari_sdk-0.2.0/polari_sdk.egg-info/dependency_links.txt +1 -0
- polari_sdk-0.2.0/polari_sdk.egg-info/requires.txt +11 -0
- polari_sdk-0.2.0/polari_sdk.egg-info/top_level.txt +1 -0
- polari_sdk-0.2.0/pyproject.toml +65 -0
- polari_sdk-0.2.0/setup.cfg +4 -0
|
@@ -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)
|