Spaces:
Running
Running
File size: 4,671 Bytes
a3b3e34 de2552b 67cc26b a3b3e34 de2552b a3b3e34 6f32c7e 47105f4 a3b3e34 ec0543b a3b3e34 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | """Versioned public API models and opaque cursor contract."""
from __future__ import annotations
import base64,json,re
from typing import Any,Literal
from pydantic import BaseModel,ConfigDict,Field
REVISION=re.compile(r"^[0-9a-f]{40}$")
class StrictModel(BaseModel): model_config=ConfigDict(extra="forbid")
class ErrorBody(StrictModel):
code:str;message:str;details:list[dict[str,Any]]=Field(default_factory=list);dataset_revision:str|None=None
class ErrorResponse(StrictModel): error:ErrorBody
class PageMeta(StrictModel):
limit:int=Field(ge=1,le=100);next_cursor:str|None=None;dataset_revision:str;returned:int=Field(ge=0,le=100)
class CompanySummary(StrictModel):
issuer_id:str;exchange:Literal["ASX","NZX"];ticker:str;name:str|None=None;sector:str|None=None;industry:str|None=None;as_of_date:str;extraction_coverage_status:Literal["current","backfill_in_progress","pending","unknown"]="unknown";extraction_as_of_date:str|None=None;needs_review:bool;basic_shares:int|None=None;fd_ratio:float|None=None;worst_case_dilution_pct:float|None=None;expiry_pct_lt_3m:float|None=None;expiry_pct_lt_6m:float|None=None;expiry_pct_lt_12m:float|None=None;cash_runway_months:float|None=None
class DilutionInstrument(StrictModel):
instrument_id:str;ticker:str;instrument_type:str;quantity:int|None=None;series_name:str|None=None;asx_code:str|None=None;exercise_price_aud:str|None=None;expiry:str|None=None;holder_type:str|None=None;vesting_condition:str|None=None;status:str;as_of_date:str;document_ids:list[str]=Field(default_factory=list)
class CompanySnapshot(StrictModel):
dataset_revision:str;published_at:str|None=None;source_watermark:dict[str,Any]|None=None;dataset_citation:str;found:bool;company:CompanySummary|None=None;instruments:list[DilutionInstrument]=Field(default_factory=list);returned:int=Field(ge=0,le=100)
class DocumentSummary(StrictModel):
document_id:str;exchange:Literal["ASX","NZX"];ticker:str;source_document_id:str;announcement_date:str;announced_at:str|None=None;title:str;doc_type:str|None=None;doc_type_source:str|None=None;doc_type_confidence:float|None=None;raw_artifact_key:str|None=None;raw_size_bytes:int|None=None;markdown_artifact_key:str|None=None;markdown_size_bytes:int|None=None;publication_state:str;source_link:str|None=None
class CompanyPage(StrictModel): items:list[CompanySummary];page:PageMeta
class DocumentPage(StrictModel): items:list[DocumentSummary];page:PageMeta
class SearchHit(StrictModel):
document_id:str;ticker:str;doc_type:str|None=None;announcement_date:str;title:str;source_link:str;snippet:str=Field(max_length=600);score:float
class SearchPage(StrictModel): items:list[SearchHit];page:PageMeta;query:str
class EventItem(StrictModel):
event_id:str;instrument_id:str|None=None;instrument_type:str|None=None;event_type:str|None=None;quantity_delta:int|None=None;effective_date:str|None=None;document_id:str|None=None;doc_type:str|None=None;status:str|None=None
class EventPageMeta(StrictModel):
limit:int=Field(ge=1,le=500);next_cursor:str|None=None;dataset_revision:str;returned:int=Field(ge=0,le=500)
class EventPage(StrictModel): items:list[EventItem];page:EventPageMeta
class DatasetStatus(StrictModel):
service:str;ready:bool;status:str;build_sha:str;dataset_revision:str;schema_version:int;cache_artifacts:int;cache_bytes:int;documents_total:int|None=None;documents_last_24h:int|None=None;companies_total:int|None=None
class CursorError(ValueError):
def __init__(self,code,message):super().__init__(message);self.code=code
def encode_cursor(revision:str,kind:str,values:list[Any])->str:
if not REVISION.fullmatch(revision):raise ValueError("invalid revision")
raw=json.dumps({"v":1,"revision":revision,"kind":kind,"values":values},sort_keys=True,separators=(",", ":")).encode()
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
def decode_cursor(value:str,revision:str,kind:str)->list[Any]:
if not value or len(value)>1024:raise CursorError("invalid_cursor","Cursor is empty or too long")
try:
raw=base64.b64decode(value+"="*(-len(value)%4),altchars=b"-_",validate=True);payload=json.loads(raw)
except Exception as error:raise CursorError("invalid_cursor","Cursor is not valid base64url JSON") from error
if set(payload)!={"v","revision","kind","values"} or payload["v"]!=1 or not isinstance(payload["values"],list):raise CursorError("invalid_cursor","Cursor shape is invalid")
if payload["kind"]!=kind:raise CursorError("invalid_cursor","Cursor belongs to another endpoint")
if payload["revision"]!=revision:raise CursorError("stale_cursor","Cursor Dataset revision is no longer active")
return payload["values"]
|