Skip to main content

geodb-client

The Python client for the geoDB Open Exploration Protocol — pull a mining-exploration project's drill, assay, geophysics, raster, and document data as pandas DataFrames and files, over one authenticated, project-scoped API.

pip install "geodb-client>=0.2,<0.3"     # + geopandas extra: pip install "geodb-client[geo]>=0.2,<0.3"

Integrating for the first time? Read AGENTS.md in the protocol repo first. It names the handful of things in this domain that give you silently wrong answers — starting with the coordinate rule below — and carries the sync loop, the error codes and runnable examples in four languages.

20 lines to DataFrames

import geodb

gx = geodb.Client(token="gdbg_...", base_url="https://api.geodb.io")
gx.project()                                         # the companies + projects it reads
P = 12                                               # name the project on every read

# Records lane — relational data straight to pandas
collars       = gx.collars(project=P).to_dataframe()
samples       = gx.samples(project=P).to_dataframe()
results       = gx.assay_results(project=P).to_dataframe()   # flat: sample × element × method
methods       = gx.methods(project=P).to_dataframe()         # join on method_id
lithology     = gx.lithology(project=P, set="all").to_dataframe()  # or set=<id or name>
surveys       = gx.surveys(project=P).to_dataframe() # geophysical surveys + footprints

# Assets lane — walk the STAC catalog, download a Cloud-Optimized GeoTIFF
for item in gx.stac().items("rasters"):
    cog = item.asset("cog")
    if cog:
        cog.download("grid.tif")                     # short-SAS redirect, streamed
        break

# Bulk lane — GeoParquet export (zero-data-loss geometry + native coordinates)
job = gx.export("drill_samples", format="geoparquet", project=P)
job.wait().download("samples.parquet")

That's it — a vendor integrates a customer's project in an afternoon.

Auth

A project owner mints a revocable access grant in geoDB (Project Settings → API Access Grants) and shares the token; it reads the project(s) it was given, and every pull is logged for the owner to see. The client sends it as Authorization: Grant <token>.

geoDB keeps no current project. When the key reads several projects, name one on every read (project=<id or exact name>); otherwise the server answers geodb.ProjectRequired, whose .choices lists the projects by company. Company-level tables (methods, laboratories, standards, QC types) take company=; scope="company", company=<id> reads every project of one company. An interval table over a project with several sets raises geodb.SetChoiceRequired (.sets lists them) until you pass set=<id or name> or set="all".

Every refusal is a typed exception carrying the server's reason_code and remedy: ProjectRequired, CompanyRequired, SetChoiceRequired (InvalidRequest for any other 400), AuthError (401) / PermissionDenied (403), NotFoundError, Conflict, Throttled (.retry_after). Match on reason_code; act on remedy.

Client and server versions are paired. geodb-client is versioned in step with the protocol: 0.2.x speaks protocol 0.2. It sends the version it speaks (X-GeoDB-Protocol-Version) and checks the server's on the first response; a different major.minor raises geodb.ProtocolVersionMismatch, whose .install is the line that fetches the matching client (e.g. pip install "geodb-client>=0.3,<0.4"). Install with the range the server gives you — pip install "geodb-client>=0.2,<0.3" for protocol 0.2 — never unpinned. (check_protocol=False turns the check off.)

First-party callers can use a Knox token instead:

gx = geodb.Client(token="<knox>", base_url="https://api.geodb.io", auth_scheme="Token")

Behind the api. subdomain rewrite, pass api_prefix="/v2".

API

Call Returns
Records lane — see the table list below a Paginated iterator — iterate rows, or .to_dataframe(), or .count() (the list's own count; no extra request once read). Takes project=, company=, scope= (+ set= on interval tables) and filter kwargs (e.g. gx.surveys(project=P, method="magnetics")).
gx.assay_results(project=P) · gx.methods(company=C) · gx.laboratories(company=C) the flat assay table and the lookup tables it names by id.
gx.stac() a StacCatalog: .landing(), .collections(), .items(collection_id) (yields StacItem), .item(cid, iid). StacItem.asset(key).download(path).
gx.export(model, format="geoparquet", include_assays=True, project=…, set=…) an ExportJob of ONE project's table: .wait() then .download(path). Formats: geoparquet, parquet, csv. Models: the export() column below.
gx.project() the credential's context: its companies and projects (with set counts), whether it may write, its limits.
gx.describe · gx.write · gx.validate · gx.retract · gx.restore · gx.undo · gx.writes the write half — see Writing below.

Tables

The drilling tables are the same ones geoDB serves to Leapfrog and Vulcan over ODBC — one definition of a project's drilling data, whichever door you come through.

Records lane export() model Sets What it is
gx.collars(project=…) drill_collars — Collar location, orientation, total depth (/drill-collars/)
gx.drill_surveys(project=…) drill_surveys — Downhole survey stations (depth, azimuth, dip) (/drill-surveys/)
gx.lithology(project=…) drill_lithology set= Downhole lithology intervals (/drill-lithologies/)
gx.alteration(project=…) drill_alteration set= Downhole alteration intervals (/drill-alterations/)
gx.samples(project=…) drill_samples set= Drill samples (+ merged assays) (/drill-samples/)
gx.structures(project=…) drill_structure_point, drill_structure_zone — Structural measurements; point (a depth) vs zone (an interval) (/drill-structures/)
gx.mineralization(project=…) drill_mineralization set= Mineralization intervals + mineral percentages (/drill-mineralizations/)
gx.veins(project=…) drill_veins set= Vein intervals (type, width, mineral contents) (/drill-veins/)
gx.rqd(project=…) drill_rqd set= Geotech: core recovery + rock mass (/drill-rqds/)
gx.spectral(project=…) drill_spectral set= Spectral intervals (geounit abundances) (/drill-spectral-intervals/)
gx.custom_intervals(project=…) drill_custom_intervals set= User-defined intervals (TYPE IS DATA) (/drill-custom-intervals/)
gx.point_samples(project=…) point_samples — Surface / soil / rock-chip samples (/point-samples/)
gx.qc_samples(project=…) qc_samples — QA/QC (standards, blanks, duplicates) (/qc-samples/)
gx.assays(project=…) — — Assay results (merged into the sample exports; flat: assay_results()) (/assays/)
gx.surveys(project=…) geophysical_surveys — Geophysical surveys (metadata + WGS84 footprint) (/geophysical-surveys/)

Asking to export() a model your project does not expose returns HTTP 400 with the list it does — e.g. the two drill_rqd_* halves appear only when the project has the geotech tab split enabled, exactly as over ODBC.

What an export inherits

An export() is not a raw table dump. It comes back the way the project is configured, the same as the project's own CSV/XLSX exports and its ODBC feed:

  • Assay merge settings — the sample exports carry merged assay columns per the project's merge strategy and unit conversions (include_assays=True).
  • Coordinate reference system — coordinates honour the project's configured output CRS (odbc_output_crs), not a forced WGS84.
  • Column configuration — visibility, ordering and display-name overrides.
  • Custom fields — as cf_* columns.
  • Depth units — metres or feet, per the project.

Coordinate contract

GeoParquet exports carry a WGS84 geometry column plus the native/original coordinate columns (latitude/longitude/epsg) — zero-data-loss travels with the data, so you never lose the surveyor's original CRS.

⚠️ On a UTM-imported project the latitude/longitude columns hold northing/easting in that project's CRS, not degrees — that is what "native" means, and epsg tells you how to read them. The geometry column is always WGS84. Read geometry for maps; read the natives when you need exactly what the surveyor recorded.

The same rule applies to every record the API returns, not only exports. Each row also carries source_coordinate ({x, y, epsg} — the same values under names that cannot be mistaken for degrees) and geometry_geojson (WGS84 as a parsed GeoJSON Point). Prefer those two: decide by epsg, never by the field name.

Writing (a key that may write records)

geoDB's write half is not yet served by its production servers; these calls answer once it is. gx.project() says whether your key may write (read_only: false).

Every write goes through ONE endpoint, POST /api/v2/records/, and is answered per row: a bad row comes back with its own reason_code and remedy while the rest of the batch lands. Only a refusal of the whole request raises geodb.WriteRefused (with .reason_code and .remedy).

gx.describe("DrillSample")                    # fields, identity, sets, coordinate rule

rows = [{"bhid": "DH-1", "name": "DH-1-001", "depth_from": 0, "depth_to": 2}]   # or a DataFrame
check = gx.validate("DrillSample", rows, logging_set="pXRF 2026")   # dry run: nothing written
result = gx.write("DrillSample", rows, logging_set={"name": "pXRF 2026", "create": True},
                  idempotency_key="my-batch-0001")                    # a retry replays
result.summary, result.refused                # per-row outcomes; act on each remedy

gx.write("DrillSample", [{"id": 123, "notes": "re-logged"}], intent="update")
gx.retract("DrillSample", [{"id": 124}], dry_run=True)    # show the user what goes
gx.retract("DrillSample", [{"id": 124}], confirm=True)    # only after their yes
gx.undo(result.write_id)                       # reverse any write; .complete says if all of it
gx.restore(retract_write_id)                   # bring a retract back
for w in gx.writes(undone=False): ...          # what this key wrote, newest first
Call Does
gx.describe(model) the live write contract for model
gx.write(model, rows, intent="create", logging_set=, idempotency_key=, dry_run=, acknowledge=) create never overwrites (a differing record is skipped, both values named) · upsert · update (existing records by identity or id; never creates). Returns a WriteResult
gx.validate(model, rows, …) the same call with dry_run=True
gx.retract(model, rows, confirm=False, dry_run=False) to the Trash with everything that belongs to them; needs confirm=True
gx.restore(write_id) / gx.restore(audit_batch_id=…) brings a removed batch back
gx.undo(write_id, dry_run=False) reverses one write; rows changed since are left and named (undo_stale)
gx.writes(write_id=None, model=, intent=, undone=) the key's write log (or one write, with its rows)

logging_set= is the body's "set", for every set-aware family (sample sets too). undo() and restore() take idempotency_key= like write(). result.raise_for_refusals() raises geodb.RowsRefused (status 200, .rows) when rows were refused. make_default_set and qaqc_verdict have no methods on purpose — each is a person's own act on their explicit request (a vendor key is refused); send them through gx.write(...) with dry_run=True, then with confirm= after the user's yes (e.g. gx.write(model, [], intent="make_default_set", logging_set="<set>", dry_run=True)). qc_reconnect (model "QCSample") goes through gx.write(...) too.

The rules the server enforces (and an AI writing for a person must respect): coordinates carry their own epsg (never pre-convert; a row without it is refused missing_crs) · interval and sample rows name their set — when the user has not said which, ask (set_required lists the choices) · a vendor key writes only into sets it created or was given (set_not_owned) · update, upsert, retract and restore change what exists: dry-run, show the user, send after their yes. Hard delete never crosses the API.

Development

pip install -e ".[dev,geo]"
pytest                     # unit tests (mocked transport)
GEODB_TEST_BASE_URL=http://localhost:8001 GEODB_TEST_TOKEN=gdbg_... pytest tests/test_integration.py

Apache-2.0. Version 0.2.0 (protocol 0.2).

Metadata

Release files for geodb-client 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for geodb-client 0.2.0
File Size Uploaded
geodb_client-0.2.0.tar.gz 40.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for geodb-client 0.2.0
File Interpreter ABI Platform
geodb_client-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 65.9 kB

Release files / geodb_client-0.2.0.tar.gz

Download URL geodb_client-0.2.0.tar.gz
Size 40.7 kB
Tags Source
SHA-256 checksum
How to use checksums
42fef9a8adf38982193d76d0b3c183dfc27fd3a9a6d45b83b7419719c8daa14d
BLAKE2b-256 checksum
How to use checksums
5d487bb22b55f1ea05b674a71a7acbd04da60ec63c0eec7354f1dcf192ac6053
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.13

Release files / geodb_client-0.2.0-py3-none-any.whl

Download URL geodb_client-0.2.0-py3-none-any.whl
Size 25.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
537be60eb40f00a5ccbe35cca6cac3a7081f077c05a85af87d79984074be42d9
BLAKE2b-256 checksum
How to use checksums
1ee243ec9ad26538e705f6c9cbb2694c01c6fb95c5c128274375d2aa8d341d7b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.13

Release history Release notifications | RSS feed

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page