Metadata-Version: 2.5
Name: certvas
Version: 0.1.0
Summary: Python SDK for Certvas — point-in-time, license-clean African & EM data and signals.
Project-URL: Homepage, https://certvas.com
Project-URL: Documentation, https://certvas.com/developers.html
Project-URL: Source, https://github.com/KingMandase/dataco
Author-email: Certvas <mandase@certvas.com>
License-Expression: MIT
License-File: LICENSE
Keywords: african markets,alternative data,certvas,emerging markets,point-in-time,procurement,quant,signals
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Requires-Dist: requests>=2.25
Provides-Extra: dev
Requires-Dist: pandas>=1.3; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Provides-Extra: pandas
Requires-Dist: pandas>=1.3; extra == 'pandas'
Provides-Extra: shared
Requires-Dist: duckdb>=0.10; extra == 'shared'
Requires-Dist: pandas>=1.3; extra == 'shared'
Description-Content-Type: text/markdown

# certvas — Python SDK

Point-in-time, license-clean African & emerging-market data and signals, in three lines.

```bash
pip install certvas          # add pandas for DataFrames: pip install "certvas[pandas]"
```

```python
import certvas
cv = certvas.Client(api_key="...")                 # free sample key: https://certvas.com/signup.html
panel = cv.signals.procurement_demand(country="ZA", as_of="2025-06-30")
```

`panel` is a pandas DataFrame (or a list of dicts if pandas isn't installed). Every response carries
provenance and a license class; point-in-time endpoints honour `as_of` with **no look-ahead**.

## What you can call

```python
# The flagship signal — government-demand share, buyer concentration, 3-month momentum, point-in-time
cv.signals.procurement_demand(country="ZA", as_of="2025-06-30", category="services")

# Underlying datasets
cv.tenders(country="ZA", from_="2024-01-01")
cv.macro(series="FX.ZAR.USD", as_of="2025-06-30")   # point-in-time macro
cv.macro(country="NGA", indicator="FP.CPI.TOTL.ZG")  # annual macro

# Fundamentals for one issuer, point-in-time (Woolworths Holdings, JSE)
cv.fundamentals("DC4D39BDF1A9471A472810CB32", as_of="2024-02-01", period_type="FY")
cv.fundamentals("DC4D39BDF1A9471A472810CB32", as_reported=True)       # originally filed — use this for a backtest
cv.fundamentals("DC4D39BDF1A9471A472810CB32", all_vintages=True)      # every restatement, for revision analysis
```

### `as_of` on fundamentals is a real no-look-ahead read

It filters on **two** dates, and the second is the one that matters:

| | |
|---|---|
| `period_end <= as_of` | the reporting period had ended |
| `available_at <= as_of` | the number had actually been **published** |

FY results for a December year-end are typically not public for another 60–120 days. Filtering on
the period alone hands you numbers nobody had at the time — the classic look-ahead bias. Where an
announcement date could not be evidenced, `available_at` falls back to the date Certvas captured the
value, which is always *later* than true publication for a backfilled filing. So the fallback can
only ever **exclude** a row from your window, never admit one early: weak coverage costs you recall,
not correctness.

**Three reads, three different questions.** Fundamentals is append-only across restatements, so an
issuer × period × line item can hold several rows:

| call | returns | use it for |
|---|---|---|
| *(default)* | latest vintage available by `as_of` | what the tape said on that date |
| `as_reported=True` | the **originally filed** value | **factor studies and backtests** |
| `all_vintages=True` | every vintage, with `restated` / `vintage_seq` / `superseding_record_ref` | revision analysis |

Reach for `as_reported=True` in any historical study. Running one on latest-value history is
**restatement bias**: those numbers were revised with hindsight and were never on the tape at the
time you are pretending to trade. The same values ship as `fundamentals_as_reported` in the bulk
AF-FUND package.

### Everything else

```python
# Raw escape hatch for any endpoint — returns the full JSON envelope
cv.get("/v1/quality-metrics/af-tender")
```

## Provenance on every value

The DataFrame carries the license notice and query metadata in `df.attrs`:

```python
panel.attrs["_license"]   # the license-clean notice
panel.attrs["as_of"]      # the point-in-time date you queried
panel.attrs["count"]
```

## One shape for every dataset: entity + fact + provenance

`certvas.shared` puts AF-FUND, AF-TENDER and AF-MACRO in one layout: `shared_entity` (issuers, public
buyers and countries), `shared_fact` (one row per value, with `available_at`, the point-in-time column)
and `shared_provenance` (the source and licence of every fact). It runs locally over the Parquet release
you receive, so nothing extra is hosted or billed:

```python
# pip install "certvas[shared]"
import duckdb
from certvas import shared

con = duckdb.connect()
shared.register_release(con, "path/to/release")      # the folder holding gold_*.parquet
shared.build(con)
con.sql("select * from shared_fact where available_at <= date '2025-06-30'").df()
```

The API serves the same rows from the live tables, with provenance on each row:

```python
cv.shared_facts(entity_id="DC4D39BDF1A9471A472810CB32", as_of="2025-06-30")   # an issuer, AF-FUND
cv.shared_facts(entity_id="ZAF")                                              # a country, AF-MACRO
cv.shared_facts(country="KEN", dataset="AF-TENDER", limit=100)                # tender values
cv.shared_facts(entity_id="DC9C8E68C8A893E1142B0D9806", dataset="AF-TENDER")  # awards an issuer won (Fidson Healthcare)
```

From a fundamentals response instead: `shared.from_api_fundamentals(cv.fundamentals("DC...", as_of="2025-06-30"))`.
Every column is described in `shared.SPEC`. Nothing is estimated, a restatement is a new `vintage`,
`available_at` is NULL rather than guessed, and a tender buyer carries a Certvas ID only where it was
resolved.

## Upgrading in-band

Sample-tier keys are capped. Over-ask and the SDK raises `certvas.UpgradeRequired` with the checkout
URL, so an agent or script can upgrade without leaving the flow:

```python
try:
    cv.signals.procurement_demand(limit=100_000)
except certvas.UpgradeRequired as e:
    print("Upgrade:", e.checkout_url)
```

## Errors

- `certvas.AuthError` — missing/invalid key (401)
- `certvas.UpgradeRequired` — this request has a price (402); carries `.checkout_url` and `.docs`.
  Raised when a sample key explicitly asks past its per-request row cap. At API v2 the same
  condition returns `429` and this exception will be raised for that too, so catching
  `UpgradeRequired` is the forward-compatible way to handle it — do not branch on the raw status.
- `certvas.CertvasError` — base class for everything else

Docs: https://certvas.com/developers.html · Signals: https://certvas.com/signals/
