priorish-sdk 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,22 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .env
10
+ .env.local
11
+ .venv/
12
+ venv/
13
+ neo4j_data/
14
+ neo4j_logs/
15
+ .ruff_cache/
16
+ .pytest_cache/
17
+ .mypy_cache/
18
+ *.ipynb_checkpoints/
19
+ .gstack/
20
+ .claude/
21
+ _index.sqlite
22
+ rust-preprocessor/target/
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: priorish-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Priorish temporal search API
5
+ Requires-Python: <4,>=3.11
6
+ Requires-Dist: httpx>=0.28.0
7
+ Requires-Dist: pydantic>=2.11.0
@@ -0,0 +1,67 @@
1
+ """Priorish Python SDK.
2
+
3
+ Usage:
4
+ from priorish_sdk import PriorishClient
5
+
6
+ client = PriorishClient(api_key="your-key")
7
+ results = client.search("nonfarm payrolls", as_of="2026-03-01T00:00:00Z")
8
+ diff = client.diff("nonfarm payrolls", t1="2026-03-01", t2="2026-04-01")
9
+ """
10
+
11
+ from priorish_sdk.client import PriorishClient
12
+ from priorish_sdk.models import (
13
+ BaseRateResponse,
14
+ BatchSearchResponse,
15
+ BatchSearchResultSet,
16
+ DiffChange,
17
+ DiffResponse,
18
+ EdgeDetail,
19
+ EntityDiffResponse,
20
+ EpisodeDetail,
21
+ FactProvenance,
22
+ GraphNeighborsResponse,
23
+ NeighborEntity,
24
+ Provenance,
25
+ RelatedFact,
26
+ RevisionHistoryResponse,
27
+ SagaEpisode,
28
+ SagaGroup,
29
+ SagaListResponse,
30
+ SagaSummary,
31
+ SagaTimelineResponse,
32
+ SearchResponse,
33
+ SearchResult,
34
+ SourceStatus,
35
+ StatusResponse,
36
+ TimelineEvent,
37
+ TimelineResponse,
38
+ )
39
+
40
+ __all__ = [
41
+ "PriorishClient",
42
+ "BaseRateResponse",
43
+ "BatchSearchResponse",
44
+ "BatchSearchResultSet",
45
+ "DiffChange",
46
+ "DiffResponse",
47
+ "EdgeDetail",
48
+ "EntityDiffResponse",
49
+ "EpisodeDetail",
50
+ "FactProvenance",
51
+ "GraphNeighborsResponse",
52
+ "NeighborEntity",
53
+ "Provenance",
54
+ "RelatedFact",
55
+ "RevisionHistoryResponse",
56
+ "SagaEpisode",
57
+ "SagaGroup",
58
+ "SagaListResponse",
59
+ "SagaSummary",
60
+ "SagaTimelineResponse",
61
+ "SearchResponse",
62
+ "SearchResult",
63
+ "SourceStatus",
64
+ "StatusResponse",
65
+ "TimelineEvent",
66
+ "TimelineResponse",
67
+ ]
@@ -0,0 +1,310 @@
1
+ """Priorish Python SDK client."""
2
+
3
+ from datetime import datetime
4
+
5
+ import httpx
6
+
7
+ from priorish_sdk.models import (
8
+ BaseRateResponse,
9
+ BatchSearchResponse,
10
+ CoverageResponse,
11
+ DiffResponse,
12
+ EntityDiffResponse,
13
+ FactProvenance,
14
+ GraphNeighborsResponse,
15
+ RevisionHistoryResponse,
16
+ SagaListResponse,
17
+ SagaTimelineResponse,
18
+ SearchResponse,
19
+ SeriesResponse,
20
+ StatusResponse,
21
+ TimelineResponse,
22
+ )
23
+
24
+
25
+ class PriorishClient:
26
+ """Client for the Priorish API.
27
+
28
+ Usage:
29
+ client = PriorishClient(api_key="your-key")
30
+ results = client.search("nonfarm payrolls")
31
+ diff = client.diff("nonfarm payrolls", t1="2026-03-01", t2="2026-04-01")
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ api_key: str,
37
+ base_url: str = "https://api.priori.sh",
38
+ timeout: float = 30.0,
39
+ ):
40
+ self._client = httpx.Client(
41
+ base_url=base_url,
42
+ headers={"X-API-Key": api_key},
43
+ timeout=timeout,
44
+ )
45
+
46
+ def search(
47
+ self,
48
+ query: str,
49
+ as_of: str | datetime | None = None,
50
+ start_date: str | datetime | None = None,
51
+ end_date: str | datetime | None = None,
52
+ sources: list[str] | None = None,
53
+ limit: int = 10,
54
+ temperature: float = 0.0,
55
+ natural_language: bool = False,
56
+ ) -> SearchResponse:
57
+ """Temporal search: what was knowable at a specific point in time."""
58
+ params: dict = {
59
+ "query": query,
60
+ "limit": limit,
61
+ }
62
+ if natural_language:
63
+ params["natural_language"] = "true"
64
+ if temperature > 0.0:
65
+ params["temperature"] = temperature
66
+ if as_of:
67
+ params["as_of"] = str(as_of) if isinstance(as_of, datetime) else as_of
68
+ if start_date:
69
+ params["start_date"] = (
70
+ str(start_date) if isinstance(start_date, datetime) else start_date
71
+ )
72
+ if end_date:
73
+ params["end_date"] = str(end_date) if isinstance(end_date, datetime) else end_date
74
+ if sources:
75
+ params["sources"] = ",".join(sources)
76
+
77
+ resp = self._client.get("/search", params=params)
78
+ resp.raise_for_status()
79
+ return SearchResponse(**resp.json())
80
+
81
+ def diff(
82
+ self,
83
+ query: str,
84
+ t1: str | datetime,
85
+ t2: str | datetime,
86
+ sources: list[str] | None = None,
87
+ ) -> DiffResponse:
88
+ """Knowledge diff: what changed between two timestamps."""
89
+ params = {
90
+ "query": query,
91
+ "t1": str(t1) if isinstance(t1, datetime) else t1,
92
+ "t2": str(t2) if isinstance(t2, datetime) else t2,
93
+ }
94
+ if sources:
95
+ params["sources"] = ",".join(sources)
96
+
97
+ resp = self._client.get("/diff", params=params)
98
+ resp.raise_for_status()
99
+ return DiffResponse(**resp.json())
100
+
101
+ def status(self) -> StatusResponse:
102
+ """Get source freshness and system status."""
103
+ resp = self._client.get("/status")
104
+ resp.raise_for_status()
105
+ return StatusResponse(**resp.json())
106
+
107
+ def sagas(self, source: str | None = None) -> SagaListResponse:
108
+ """List all sagas with metadata."""
109
+ params = {}
110
+ if source:
111
+ params["source"] = source
112
+ resp = self._client.get("/sagas", params=params)
113
+ resp.raise_for_status()
114
+ return SagaListResponse(**resp.json())
115
+
116
+ def saga_timeline(
117
+ self,
118
+ name: str,
119
+ limit: int = 50,
120
+ offset: int = 0,
121
+ ) -> SagaTimelineResponse:
122
+ """Get a saga's timeline: episodes in chronological order."""
123
+ resp = self._client.get(
124
+ f"/sagas/{name}",
125
+ params={"limit": limit, "offset": offset},
126
+ )
127
+ resp.raise_for_status()
128
+ return SagaTimelineResponse(**resp.json())
129
+
130
+ def saga_diff(
131
+ self,
132
+ name: str,
133
+ episode_1: str | None = None,
134
+ episode_2: str | None = None,
135
+ ) -> DiffResponse:
136
+ """Diff two episodes within a saga. Defaults to two most recent."""
137
+ params = {}
138
+ if episode_1:
139
+ params["episode_1"] = episode_1
140
+ if episode_2:
141
+ params["episode_2"] = episode_2
142
+ resp = self._client.get(f"/sagas/{name}/diff", params=params)
143
+ resp.raise_for_status()
144
+ return DiffResponse(**resp.json())
145
+
146
+ def search_batch(
147
+ self,
148
+ query: str,
149
+ timestamps: list[str | datetime],
150
+ sources: list[str] | None = None,
151
+ limit: int = 10,
152
+ ) -> BatchSearchResponse:
153
+ """Run the same query at multiple timestamps for backtesting."""
154
+ ts_strs = [str(t) if isinstance(t, datetime) else t for t in timestamps]
155
+ params: dict = {
156
+ "query": query,
157
+ "timestamps": ",".join(ts_strs),
158
+ "limit": limit,
159
+ }
160
+ if sources:
161
+ params["sources"] = ",".join(sources)
162
+ resp = self._client.get("/search/batch", params=params)
163
+ resp.raise_for_status()
164
+ return BatchSearchResponse(**resp.json())
165
+
166
+ def timeline(
167
+ self,
168
+ entity: str | None = None,
169
+ saga: str | None = None,
170
+ as_of: str | datetime | None = None,
171
+ sources: list[str] | None = None,
172
+ limit: int = 50,
173
+ offset: int = 0,
174
+ ) -> TimelineResponse:
175
+ """Temporal evolution: how an entity's facts changed over time."""
176
+ params: dict = {"limit": limit, "offset": offset}
177
+ if entity:
178
+ params["entity"] = entity
179
+ if saga:
180
+ params["saga"] = saga
181
+ if as_of:
182
+ params["as_of"] = str(as_of) if isinstance(as_of, datetime) else as_of
183
+ if sources:
184
+ params["sources"] = ",".join(sources)
185
+ resp = self._client.get("/timeline", params=params)
186
+ resp.raise_for_status()
187
+ return TimelineResponse(**resp.json())
188
+
189
+ def provenance(self, fact_uuid: str) -> FactProvenance:
190
+ """Full provenance for a fact: source episodes, raw content, related facts."""
191
+ resp = self._client.get(f"/provenance/{fact_uuid}")
192
+ resp.raise_for_status()
193
+ return FactProvenance(**resp.json())
194
+
195
+ def entity_diff(
196
+ self,
197
+ entity_name: str,
198
+ t1: str | datetime,
199
+ t2: str | datetime,
200
+ ) -> EntityDiffResponse:
201
+ """Entity-centric diff: what changed for an entity between t1 and t2."""
202
+ params = {
203
+ "t1": str(t1) if isinstance(t1, datetime) else t1,
204
+ "t2": str(t2) if isinstance(t2, datetime) else t2,
205
+ }
206
+ resp = self._client.get(f"/entities/{entity_name}/diff", params=params)
207
+ resp.raise_for_status()
208
+ return EntityDiffResponse(**resp.json())
209
+
210
+ def entity_revisions(
211
+ self,
212
+ entity_name: str,
213
+ as_of: str | datetime | None = None,
214
+ ) -> RevisionHistoryResponse:
215
+ """Revision/contradiction history for an entity."""
216
+ params: dict = {}
217
+ if as_of:
218
+ params["as_of"] = str(as_of) if isinstance(as_of, datetime) else as_of
219
+ resp = self._client.get(f"/entities/{entity_name}/revisions", params=params)
220
+ resp.raise_for_status()
221
+ return RevisionHistoryResponse(**resp.json())
222
+
223
+ def entity_neighbors(
224
+ self,
225
+ entity_name: str,
226
+ relation_types: list[str] | None = None,
227
+ as_of: str | datetime | None = None,
228
+ limit: int = 100,
229
+ ) -> GraphNeighborsResponse:
230
+ """Graph neighbors: entities connected via edges."""
231
+ params: dict = {"limit": limit}
232
+ if relation_types:
233
+ params["relation_types"] = ",".join(relation_types)
234
+ if as_of:
235
+ params["as_of"] = str(as_of) if isinstance(as_of, datetime) else as_of
236
+ resp = self._client.get(f"/entities/{entity_name}/neighbors", params=params)
237
+ resp.raise_for_status()
238
+ return GraphNeighborsResponse(**resp.json())
239
+
240
+ def base_rates(
241
+ self,
242
+ relation_type: str,
243
+ entity_type: str | None = None,
244
+ ) -> BaseRateResponse:
245
+ """Base rate query: all edges of a given type for quantitative analysis."""
246
+ params: dict = {}
247
+ if entity_type:
248
+ params["entity_type"] = entity_type
249
+ resp = self._client.get(f"/base-rates/{relation_type}", params=params)
250
+ resp.raise_for_status()
251
+ return BaseRateResponse(**resp.json())
252
+
253
+ def relation_types(
254
+ self,
255
+ entity_type: str | None = None,
256
+ ) -> list[dict]:
257
+ """Discover available relation types and their counts."""
258
+ params: dict = {}
259
+ if entity_type:
260
+ params["entity_type"] = entity_type
261
+ resp = self._client.get("/relation-types", params=params)
262
+ resp.raise_for_status()
263
+ return resp.json()
264
+
265
+ def series(
266
+ self,
267
+ entity: str,
268
+ relation_type: str,
269
+ format: str = "json",
270
+ start_date: str | datetime | None = None,
271
+ end_date: str | datetime | None = None,
272
+ ) -> SeriesResponse:
273
+ """Time series data for an entity + relation type."""
274
+ params: dict = {"format": format}
275
+ if start_date:
276
+ params["start_date"] = (
277
+ str(start_date) if isinstance(start_date, datetime) else start_date
278
+ )
279
+ if end_date:
280
+ params["end_date"] = str(end_date) if isinstance(end_date, datetime) else end_date
281
+ resp = self._client.get(f"/series/{entity}/{relation_type}", params=params)
282
+ resp.raise_for_status()
283
+ return SeriesResponse(**resp.json())
284
+
285
+ def coverage(self) -> CoverageResponse:
286
+ """Coverage per source, entity type, and relation type."""
287
+ resp = self._client.get("/coverage")
288
+ resp.raise_for_status()
289
+ return CoverageResponse(**resp.json())
290
+
291
+ def provenance_chain(self, fact_uuid: str) -> dict:
292
+ """Extended provenance with content hashes."""
293
+ resp = self._client.get(f"/provenance/{fact_uuid}/chain")
294
+ resp.raise_for_status()
295
+ return resp.json()
296
+
297
+ def provenance_pack(self, fact_uuid: str) -> dict:
298
+ """Evidence bundle from R2 artifacts."""
299
+ resp = self._client.get(f"/provenance/{fact_uuid}/pack")
300
+ resp.raise_for_status()
301
+ return resp.json()
302
+
303
+ def close(self):
304
+ self._client.close()
305
+
306
+ def __enter__(self):
307
+ return self
308
+
309
+ def __exit__(self, *args):
310
+ self.close()
@@ -0,0 +1,287 @@
1
+ """Priorish SDK response models."""
2
+
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Provenance(BaseModel):
9
+ origin_url: str
10
+ source_type: str
11
+ indexed_at: datetime
12
+ content_hash: str
13
+ valid_from: datetime
14
+ valid_to: datetime | None = None
15
+ superseded_by: str | None = None
16
+
17
+
18
+ class Evidence(BaseModel):
19
+ """Structured evidentiary weight for LLM consumption."""
20
+
21
+ basis: str = Field(
22
+ description="'primary' (from source doc) or 'derived' (computed from other facts)"
23
+ )
24
+ status: str = Field(description="'current' or 'superseded'")
25
+ corroboration: int = Field(description="Number of independent source episodes")
26
+ quantitative: bool = Field(description="Contains numeric/dollar/percent values")
27
+
28
+
29
+ class SearchResult(BaseModel):
30
+ id: str
31
+ content: str
32
+ score: float
33
+ confidence_score: float = Field(
34
+ default=0.0,
35
+ description="Deprecated. Use evidence object instead.",
36
+ )
37
+ evidence: Evidence | None = None
38
+ provenance: Provenance
39
+ fact_type: str = Field(default="extracted", description="'extracted' or 'derived'")
40
+ saga_name: str | None = None
41
+ saga_label: str | None = None
42
+ cluster_size: int = Field(
43
+ default=1, description="Number of results folded into this one by temperature."
44
+ )
45
+
46
+
47
+ class SagaGroup(BaseModel):
48
+ saga_name: str
49
+ saga_label: str
50
+ source_type: str
51
+ results: list[SearchResult]
52
+
53
+
54
+ class SearchResponse(BaseModel):
55
+ query: str
56
+ as_of: datetime
57
+ results: list[SearchResult]
58
+ total_count: int = Field(description="Total results available before pagination.")
59
+ offset: int = 0
60
+ temperature: float = 0.0
61
+ source_freshness: dict[str, datetime | None] = Field(default_factory=dict)
62
+ saga_groups: list[SagaGroup] = []
63
+ ungrouped: list[SearchResult] = []
64
+
65
+
66
+ class DiffChange(BaseModel):
67
+ type: str # "revised", "added", "retracted"
68
+ fact_id: str | None = None
69
+ source_type: str | None = None
70
+ document_at_t1: str | None = None
71
+ document_at_t2: str | None = None
72
+ summary: str
73
+
74
+
75
+ class DiffResponse(BaseModel):
76
+ query: str
77
+ t1: datetime
78
+ t2: datetime
79
+ results_at_t1: list[SearchResult]
80
+ results_at_t2: list[SearchResult]
81
+ changes: list[DiffChange]
82
+
83
+
84
+ class EpisodeDetail(BaseModel):
85
+ """An ingested source document."""
86
+
87
+ uuid: str
88
+ name: str
89
+ content: str
90
+ source_type: str
91
+ origin_url: str
92
+ content_hash: str
93
+ ingested_at: datetime
94
+ valid_at: datetime
95
+ chunks: list[dict] | None = None
96
+
97
+
98
+ class SagaSummary(BaseModel):
99
+ name: str
100
+ source_type: str
101
+ label: str
102
+ episode_count: int
103
+ earliest: datetime | None = None
104
+ latest: datetime | None = None
105
+
106
+
107
+ class SagaListResponse(BaseModel):
108
+ sagas: list[SagaSummary]
109
+
110
+
111
+ class SagaEpisode(BaseModel):
112
+ uuid: str
113
+ name: str
114
+ valid_at: datetime
115
+ created_at: datetime
116
+ source_type: str
117
+ origin_url: str
118
+ content_preview: str
119
+ fact_count: int
120
+
121
+
122
+ class SagaTimelineResponse(BaseModel):
123
+ saga_name: str
124
+ as_of: datetime
125
+ source_type: str
126
+ label: str
127
+ episodes: list[SagaEpisode]
128
+ total_count: int
129
+ offset: int = 0
130
+
131
+
132
+ class RelatedFact(BaseModel):
133
+ """A related fact with the same structure as the main fact."""
134
+
135
+ fact_uuid: str
136
+ fact: str
137
+ source_node: str
138
+ target_node: str
139
+ valid_at: datetime | None
140
+ invalid_at: datetime | None
141
+
142
+
143
+ class FactProvenance(BaseModel):
144
+ """Full provenance for a single fact (edge)."""
145
+
146
+ fact_uuid: str
147
+ fact: str
148
+ source_node: str
149
+ target_node: str
150
+ valid_at: datetime | None
151
+ invalid_at: datetime | None
152
+ created_at: datetime
153
+ fact_type: str = "extracted"
154
+ source_chunk_index: int | None = None
155
+ episodes: list[EpisodeDetail]
156
+ related_facts: list[RelatedFact]
157
+ saga_name: str | None = None
158
+ saga_label: str | None = None
159
+
160
+
161
+ class BatchSearchResultSet(BaseModel):
162
+ as_of: datetime
163
+ results: list[SearchResult]
164
+
165
+
166
+ class BatchSearchResponse(BaseModel):
167
+ query: str
168
+ result_sets: list[BatchSearchResultSet]
169
+
170
+
171
+ class TimelineEvent(BaseModel):
172
+ """A single temporal event in an entity's history."""
173
+
174
+ fact_uuid: str
175
+ fact: str
176
+ source_node: str
177
+ target_node: str
178
+ valid_at: datetime | None
179
+ invalid_at: datetime | None
180
+ created_at: datetime
181
+ source_type: str
182
+ origin_url: str
183
+
184
+
185
+ class TimelineResponse(BaseModel):
186
+ entity: str
187
+ as_of: datetime
188
+ events: list[TimelineEvent]
189
+ total_count: int = Field(description="Total events available before pagination.")
190
+ offset: int = 0
191
+
192
+
193
+ class SourceStatus(BaseModel):
194
+ source: str
195
+ last_successful_ingest: datetime | None = None
196
+ status: str # "green", "yellow", "red"
197
+ document_count: int = 0
198
+
199
+
200
+ class StatusResponse(BaseModel):
201
+ sources: list[SourceStatus]
202
+ total_documents: int = 0
203
+ total_entities: int = 0
204
+
205
+
206
+ class EdgeDetail(BaseModel):
207
+ """An extracted fact/edge with full metadata."""
208
+
209
+ id: str
210
+ fact: str
211
+ relation_type: str
212
+ source_entity: str
213
+ target_entity: str
214
+ valid_at: datetime | None
215
+ invalid_at: datetime | None
216
+ created_at: datetime
217
+ expired_at: datetime | None = None
218
+ superseded_by: str | None = None
219
+ structured_data: dict | None = None
220
+ origin_url: str = ""
221
+ source_type: str = ""
222
+
223
+
224
+ class EntityDiffResponse(BaseModel):
225
+ entity: str
226
+ t1: datetime
227
+ t2: datetime
228
+ appeared: list[EdgeDetail]
229
+ invalidated: list[EdgeDetail]
230
+
231
+
232
+ class RevisionHistoryResponse(BaseModel):
233
+ entity: str
234
+ revisions: list[EdgeDetail]
235
+ total_count: int
236
+
237
+
238
+ class NeighborEntity(BaseModel):
239
+ entity_id: str
240
+ name: str
241
+ entity_type: str
242
+ relation_type: str
243
+ fact: str
244
+ valid_at: datetime | None
245
+
246
+
247
+ class GraphNeighborsResponse(BaseModel):
248
+ entity: str
249
+ neighbors: list[NeighborEntity]
250
+ total_count: int
251
+
252
+
253
+ class BaseRateResponse(BaseModel):
254
+ relation_type: str
255
+ entity_type: str | None
256
+ total_count: int
257
+ edges: list[EdgeDetail]
258
+
259
+
260
+ class SeriesPoint(BaseModel):
261
+ valid_at: str | None = None
262
+ fact: str = ""
263
+ value: float | str | None = None
264
+ unit: str | None = None
265
+ period: str | None = None
266
+
267
+
268
+ class SeriesResponse(BaseModel):
269
+ entity: str
270
+ relation_type: str
271
+ count: int
272
+ data: list[dict]
273
+
274
+
275
+ class CoverageSource(BaseModel):
276
+ name: str
277
+ doc_count: int = 0
278
+ entity_count: int = 0
279
+ edge_count: int = 0
280
+ earliest: str | None = None
281
+ latest: str | None = None
282
+
283
+
284
+ class CoverageResponse(BaseModel):
285
+ sources: list[CoverageSource] = []
286
+ entity_types: list[dict] = []
287
+ relation_types: list[dict] = []
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "priorish-sdk"
3
+ description = "Python SDK for the Priorish temporal search API"
4
+ version = "0.1.0"
5
+ requires-python = ">=3.11,<4"
6
+ dependencies = [
7
+ "httpx>=0.28.0",
8
+ "pydantic>=2.11.0",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["priorish_sdk"]
17
+
18
+ [tool.ruff]
19
+ line-length = 100
20
+
21
+ [tool.ruff.lint]
22
+ select = ["E", "F", "UP", "B", "SIM", "I"]
23
+ ignore = ["E501", "B008"]
24
+
25
+ [tool.ruff.format]
26
+ quote-style = "double"
27
+ indent-style = "space"