Metadata-Version: 2.4
Name: commovision
Version: 0.1.0
Summary: Python client and command line for the CommoVision oil and gas data API
License-Expression: MIT
Project-URL: Documentation, https://api.commovision.com/docs
Project-URL: Homepage, https://commovision.com
Keywords: oil,gas,energy,wells,production,data,api
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Provides-Extra: polars
Requires-Dist: polars>=1.0; extra == "polars"
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == "pandas"
Dynamic: license-file

# commovision

The Python client and command line for the CommoVision oil and gas data API.
It wraps the REST contract at `api.commovision.com`: keyed requests, cursor
pagination, CSV and DataFrame loading, bulk extracts, and the change feed.
The full endpoint reference lives at https://api.commovision.com/docs.

## Install

```bash
pip install commovision
```

DataFrame loading is optional: `pip install "commovision[polars]"` or
`pip install "commovision[pandas]"`.

## Quickstart

Three steps from a key to data.

1. Create a key in the portal at `app.commovision.com` under Account &
   settings, API keys, and put it in the `COMMOVISION_API_KEY` environment
   variable. The key is shown once: store it in a secrets manager or an
   environment variable, never in code.

2. Confirm the key works and read what it can access. A result confirms the
   key; `entitlements` lists what your plan grants:

   ```python
   import commovision

   client = commovision.Client()
   print(client.me()["entitlements"])
   ```

3. Make your first data call. `rows` walks pages for you; filters pass
   through exactly as the reference spells them:

   ```python
   for well in client.rows("wells", state_province="Texas", limit=10):
       print(well["well_id"], well["well_name"])
   ```

`limit` is the most rows you want IN TOTAL, across every page. Leave it unset
and the walk reads the whole selection, which on a wide filter is a large pull:
one request per page, and every row counts against your plan's budget.
`page_size` sets how many rows one request asks for; leave it unset and the
server chooses.

## DataFrames

```python
wells = client.frame(
    "wells",
    state_province="Texas",
    basin="Permian Basin",
    trajectory="horizontal",
    limit=5000,
)  # polars; client.df(...) returns pandas
```

One well's monthly series, by its opaque `well_id`:

```python
series = client.frame(f"wells/{wells['well_id'][0]}/production", month="2020-01..")
```

Treat `well_id` and every other id as an opaque string.

## Whole-selection CSV

Register routes stream your whole filtered selection as one download, without
paging. The server refuses up front when the selection does not fit your
remaining budget, so a download is never cut short for budget:

```python
client.export_csv("permits", "tx_permits.csv", state_province="Texas")
```

A download writes a temporary file beside its destination and renames over it,
so the destination is replaced rather than written through. A path that exists
and is not a regular file is refused for that reason: `-o /dev/null` would
replace the null device rather than discard the bytes. Name a file, or a path
that does not exist yet.

One consequence of that, worth knowing before it surprises you: a destination
that is a symlink to a regular file is replaced by a regular file, so the file
it pointed at is left untouched and stops being updated. If you keep something
like `latest.parquet` as a link into a shared directory, download to the real
path and update the link yourself. Resolving the link and writing through it
would be the worse behaviour, because a partly written file would then be
visible to everything reading the shared copy.

## Bulk extracts and the change feed

For a whole table, download a prebuilt extract, then keep it current from the
change feed instead of re-downloading:

```python
catalog = client.extracts()
record = client.download_extract("tx", "well_production", "tx_production.parquet")

feed = client.changes("tx", "well_production", from_version=record["version"])
for page in feed:
    apply_upserts_and_deletes(page.rows)  # your upsert by key, delete by key
    store(feed.cursor)                    # the resume point
```

A stored cursor resumes for 60 days. One past that raises
`commovision.CursorExpired`, whose `resync_extract` names the extract to
reload before starting the feed again from its version.

## Command line

```bash
commovision wells --state-province Texas --limit 1000 --csv > tx_wells.csv
commovision wells --state-province Texas --all -o tx_wells.csv
commovision me
commovision extract tx well_production -o tx_production.parquet
commovision changes tx well_production --from-version 3 --state-file tx.cursor
commovision get /oil-gas/v1/freshness
```

Filter flags pass through as query parameters, so the command line serves
every filter the API serves; an unknown one is refused by the server with the
parameter named. `--limit` is the most rows in total and `--page-size` the rows
per request; `--all` writes the whole selection and takes neither. Pass the key
in `COMMOVISION_API_KEY` rather than `--key`, which is visible to every user of
the machine and lands in shell history and CI logs.

Exit codes a script can branch on:

| Code | Means |
|---|---|
| 0 | done |
| 2 | a usage error, or a request the server refused |
| 3 | the change cursor expired: reload the extract named on stderr |
| 4 | over quota |

## Pacing and errors

Your plan states a rolling 24-hour row ceiling, and on some plans a monthly
allowance; every data response reports both in its headers. The client sleeps
out a short `429` or `503` wait on its own and retries. A wait longer than
`max_retry_wait` (120 seconds by default) is not slept: the client raises
instead, so a monthly refusal surfaces as an exception rather than a
weeks-long sleep.

Every failure the API itself reports raises `commovision.APIError`, carrying
the API's error body:
branch on `error.code` (a stable identifier such as `invalid-parameter`), read
`error.retry_after` where the answer named one, and quote `error.request_id`
when you write to us at https://commovision.com/contact/. Two subclasses mark
the cases a script usually handles on its own:

| Exception | Raised for |
|---|---|
| `commovision.QuotaExceeded` | a `429`: a row quota, or the per-account in-flight bound |
| `commovision.CursorExpired` | a `410` on the change feed; `resync_extract` names the extract to reload |
| `commovision.ChecksumMismatch` | a downloaded extract that does not match the sha256 its issuance stated: download it again rather than retrying the issuance |
| `commovision.CommoVisionError` | the client's own refusals: no key, a missing optional extra, a parameter a walk sets itself |

The first two are `APIError`; all four are `commovision.CommoVisionError`, so
one `except commovision.CommoVisionError` catches everything this package
raises.

One name to know when you set `page_size`: it rides as the request's own
`limit`, so a value above a route's maximum comes back as a `400` naming
`limit`. Leaving `page_size` unset lets the server choose, which is the right
default; set it when you would rather make fewer, larger requests.

## Versioning

This package follows the API's own compatibility promise rather than an
internal one, because people depend on it from outside:

- Additive change ships freely in minor and patch releases: new methods, new
  optional parameters, new fields in returned data. Write your code to ignore
  fields it does not recognize.
- A breaking change to this package ships only as a new major version,
  announced in the changelog below.
- The API itself versions by namespace: a breaking API change ships as a new
  namespace and the prior one keeps working for at least six months after the
  announcement (reference, "Versioning").

## License

MIT. Keep the copyright notice with the code and there is nothing else to
observe; the full text ships in the package as `LICENSE`.

## Changelog

- **0.1.0**: first release. Keyed requests with pacing, cursor pagination,
  CSV and DataFrame loading, whole-selection export, bulk extract download
  with checksum verification, the change feed, and the command line.
