Skip to main content

earthranger-cli

A command-line utility for creating and editing EarthRanger event categories, choices, and v2 event types — and posting events — directly against the EarthRanger API, authenticated with a username and password.

You describe what you want in a small YAML spec (no hand-written JSON Schema); er events apply generates the ER v2 schema envelope and the shared Choice records, then idempotently creates what's missing and patches what changed. Nothing is ever deleted — removal means is_active: false.

er-cli-demo

Install

uv pip install -e .

Authenticate

Sessions live on profiles (gcloud-style): create a profile, log in once, and every command run under that profile reuses its cached session.

er profile add myreserve --server myreserve --username me   # prints: export ER_PROFILE=myreserve
export ER_PROFILE=myreserve   # select it in this shell (the wrapper below automates this)
er auth login                 # prompts for your password

Working across sites? Save each as a profile and select one per shell or per command — selection is never global:

er profile add sandbox --server sandbox --username me
er profile add prod --server myreserve --username me   # adding auto-switches (via the wrapper)
er profile use sandbox                 # switch this shell (needs the wrapper below)
er profile set username me2           # edit a property on the selected profile
er --profile sandbox events list categories   # one-off override
er profile list                        # marker shows this shell's selection
er profile current                     # prints it (exit 1 if none) — prompt-friendly

A selected profile supplies the server and default username when you don't pass them; explicit --server/--username flags always win.

er profile use and er profile add print an export ER_PROFILE=... line on stdout (a subprocess can't set its parent shell's env; add's confirmation goes to stderr), so add this wrapper to your zshrc — it evals that line in place, which also makes a newly added profile the shell's selection immediately, and passes every other command through; the optional prompt segment shows the shell's selection:

er() {
  if [[ $1 == profile && ($2 == use || $2 == add) ]]; then
    local out
    out=$(command er "$@") || { [[ -n $out ]] && print -r -- "$out"; return 1; }
    # eval only the switch protocol; anything else (e.g. --help) prints normally
    if [[ $out == "unset ER_PROFILE" || ($out == "export ER_PROFILE="* && $out != *$'\n'*) ]]; then
      eval "$out"
    elif [[ -n $out ]]; then
      print -r -- "$out"
    fi
  else
    command er "$@"
  fi
}
_er_prompt() { [[ -n $ER_PROFILE ]] && print -n "%F{yellow}(er:$ER_PROFILE)%f "; }
setopt PROMPT_SUBST; PROMPT='$(_er_prompt)'"$PROMPT"

auth login requires a selected profile: it verifies your credentials and caches the access and refresh tokens (never your password) in ~/.config/er-events/tokens/<profile>.json (0600). Subsequent commands under that profile just work — expired access tokens are refreshed automatically and the rotated tokens re-cached. er auth status shows the selected profile's session; er auth logout deletes it. The session is bound to the profile's identity: profile set server, setting username to a different user, or profile remove all clear it, and logging in with a different --username updates the profile to match. Profiles pointing at the same server hold independent sessions.

You can always bypass the cache with an explicit credential. A pre-issued OAuth bearer token wins over everything else and needs no username — this is the path for agent sandboxes and CI, where there is no one to type a password:

export ER_SERVER=myreserve
export ER_TOKEN='...'              # single-quote it — tokens can contain $, !, & etc.
er events list categories          # or: er --token '...' events list categories

To keep a token on a profile instead, er auth login --token '...' verifies it against the server, records its owner, and stores it in place of a password session; er auth status then reports static token as <user> (never refreshes) — re-run auth login --token when the token expires.

An explicit password is next in precedence:

export ER_USERNAME=me
export ER_PASSWORD=...              # or --password; omit both to be prompted

Docker: tokens often contain shell-special characters, so -e ER_TOKEN=$TOKEN can corrupt the value. Prefer an env-file (docker run --env-file er.env ... with ER_SERVER=... and ER_TOKEN=... lines) or export ER_TOKEN='...' once and forward it by name with a bare -e ER_TOKEN.

If the cached session's refresh token has expired, commands fail with error: cached session for profile '<name>' expired or invalid — run 'er auth login'. Without a selected profile there is no session cache — bare --server one-offs authenticate with --password/ER_PASSWORD each time. (Upgrading from host-keyed caches: run er auth login once per profile; old tokens/<host>.json files are ignored.)

Walkthrough

  1. Write a spec (start from examples/wildlife_monitoring.yaml):

    category:
      value: wildlife_monitoring
      display: Wildlife Monitoring
    event_types:
      - value: animal_sighting
        display: Animal Sighting
        fields:
          - key: species
            label: Species
            type: select
            options: [elephant, lion]
          - key: count
            label: Number of animals
            type: integer
            min: 0
        required: [species]
    
  2. Preview what would change, then apply:

    er events apply spec.yaml --dry-run
    er events apply spec.yaml
    
  3. Post an event against the new type:

    er events post --event-type animal_sighting \
        --field species=elephant --field count=3 \
        --location -1.286,36.817
    
  4. Edit: change the spec (rename a display, add a field, drop an option), dry-run again, re-apply. apply patches exactly what differs; dropped options are deactivated, never deleted.

  5. Absorb server-side edits into your file: if someone changed a value in ER's UI, er events pull wildlife_monitoring -o spec.yaml rewrites your spec from the live server; apply --dry-run should then report all unchanged. The few constructs the DSL can't express (location fields, headers, condition operators other than is_exactly, non-positional section ids, auto-generate schemas, pattern validation, choice-list or nested sub-fields inside collections) are reported and refused unless you pass --skip-unsupported, which drops them and lists what was skipped in a comment at the top of the file.

Commands

Command What it does
events apply SPEC [--dry-run] Upsert category, choices, and event types from a spec
events post --event-type V --field k=v ... Post one event (--location LAT,LON, --time, --title)
events post --file events.yaml Post a batch; exits 1 if any fail
events list categories List categories (inactive included)
events list event-types [--category V] List event types
events show event-type V Full v2 event-type JSON + its Choice records
events pull CATEGORY [-o FILE] [--skip-unsupported] Reconstruct a DSL spec from the server (reverse of apply)
auth login [--token T]/status/logout Cache (password session or static token), inspect, or clear the selected profile's credential
profile add/use/set/show/list/remove/current Named site profiles; use selects per shell and add auto-switches (via the wrapper); set edits the selected profile; --profile NAME per command

Spec reference

Field types: string, textarea, integer (advisory — ER stores it as number on the wire), number (both take optional min/max), boolean, date, datetime, url, select, multiselect (both take options).

Sub-forms are type: collection fields: repeating groups of scalar sub-fields (item_name labels one entry; optional button_text, item_identifier, columns: 2 with sub-field column: right, min/max item counts, and a required: list of sub-field keys):

- key: sightings
  label: Sightings
  type: collection
  item_name: sighting
  fields:
    - {key: species_note, label: Species note, type: string}
    - {key: count, label: Count, type: integer}
  required: [species_note]

Choice-list or nested-collection sub-fields aren't supported yet and are refused by name.

Every field takes optional active: false (the field is deprecated/hidden on ER but its historical data remains) and description. Scalar and choice fields additionally take hint (ER's placeholder, max 32 chars; not on boolean/date/datetime or collections), and scalar fields take default (string/textarea/url/integer/number/boolean only). string fields take format: url | email | uuid — the builder's "Format Validation" (url is sent as JSON Schema uri).

Options are {value, display} mappings or bare strings (lion → value lion, display Lion); option values are free text (ER stores them as-is). options: [] is valid — it deactivates every option on that field (an all-inactive choice set pulls back to exactly this). Category and event-type values must match [a-z0-9_]+ (they appear in URLs); field keys follow ER's own rule, [a-zA-Z0-9_-]+.

Select/multiselect fields take an optional choices_field naming the exact ER Choice set to use, overriding the derived <event_type>_<field_key> name — needed for stock event types whose choice sets predate this tool. To share one set across fields, declare it once at the top level and reference it — the fields then omit options entirely (inline-declared sharing also works when every sharing field carries identical options):

choices:
  shared_actions:
    - {value: stopped, display: Halted}
    - {value: warned, display: Warned, icon: warn_icon}

event_types:
  - value: t1
    fields:
      - {key: action, label: Action, type: select, choices_field: shared_actions}
  - value: t2
    fields:
      - {key: response, label: Response, type: multiselect, choices_field: shared_actions}

Options may carry an icon, and their spec order is the dropdown order: apply writes ordernum from spec position only when the visible order actually differs (a set whose active options already appear in spec order keeps its existing numbering — stock 10/20/30 gaps and holes left by deactivated records are left alone). A set with missing ordernums gets numbered on first apply.

Per event type an optional layout: {label, columns} (default {label: Details, columns: 1}) controls the form section; with columns: 2, per-field column: right places a field in the right column. Multi-section forms use sections: instead of fields:/ layout: — a list of {label?, columns?, active?, condition?, fields: [...]} mappings, one per form section, in order. active: false hides a section; condition: {field, operator: is_exactly, value} shows it only when another (outside) field has the given value — the only condition operator ER's builder offers that the DSL supports so far; the other operators are refused by name on pull:

- value: entry_alert
  display: Entry Alert
  sections:
    - label: ""
      fields: [...]
    - label: ""
      columns: 2
      fields: [...]

A form-less event type (e.g. an incident collection container) is declared with an explicit fields: [] and, for collections, is_collection: true.

Per event type: value, display, fields (required); optional: required, is_active, icon_id (sent to ER as its writable icon field — the API's own icon_id is a derived, read-only property), default_priority (gray|green|amber|red or 0|100|200|300), default_state (new|active|resolved), readonly (makes the whole event type read-only in ER — v2 schemas have no per-field read-only), geometry_type (point|polygon; whether events record a location point or a drawn polygon), auto_resolve + resolve_time (auto-resolve events after N hours — auto_resolve: true requires resolve_time, since ER silently ignores the flag without it), and ordernum (explicit display rank — a number; ER uses fractional ranks like 0.5 for insert-between — deliberately not derived from spec position, because a spec doesn't own every event type in its category). These are sent only when declared: omitted keys leave the server's values untouched. geometry_type is immutable once an event type exists — apply refuses a spec that declares a different value than the server's, since ER's API would accept the change but events already recorded under the old geometry would be corrupted.

What apply owns

apply only manages what the spec declares: it never touches event types or categories that exist on the server but aren't in the spec (retire one by setting is_active: false in the spec). The exception is choice options within a spec-managed field — there the spec is authoritative, and removed options are deactivated on the server.

Why YAML and not JSON?

Both work: spec files are parsed with a YAML parser, and YAML is a superset of JSON, so a pure-JSON spec file is accepted as-is. YAML is the documented format because spec files are hand-authored and reviewed — comments matter, and block style keeps nested fields readable. Watch YAML's implicit typing (no → false, 1.10 → a float): quote anything ambiguous.

v2 schemas and choices, briefly

ER v2 event types store a {json, ui} schema envelope. Dropdown fields don't embed their options; they reference shared Choice records via $ref: /api/v2.0/schemas/choices.json?field=<name>. This tool derives <name> as <event_type_value>_<field_key> (compressed with a short hash suffix when longer than ER's 40-char field limit) and manages those records for you — creating, re-labelling, deactivating, and reactivating options to mirror your spec.

Release files for earthranger-cli 0.1.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 earthranger-cli 0.1.0
File Size Uploaded
earthranger_cli-0.1.0.tar.gz 184.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for earthranger-cli 0.1.0
File Interpreter ABI Platform
earthranger_cli-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 238.5 kB

Release files / earthranger_cli-0.1.0.tar.gz

Download URL earthranger_cli-0.1.0.tar.gz
Size 184.8 kB
Tags Source
SHA-256 checksum
How to use checksums
c7cf42ebd09dcd86fb651e35daaa5529b28fbcbaa2c15c515f220b40931b4996
BLAKE2b-256 checksum
How to use checksums
8a4623b743ac5e598e67139984f052929c1575e6a5df5f7c19f120b035569e5a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / earthranger_cli-0.1.0-py3-none-any.whl

Download URL earthranger_cli-0.1.0-py3-none-any.whl
Size 53.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b387bea5b3f73afd745b667681562c3763592b120b4ff8ea80ef6b262862a672
BLAKE2b-256 checksum
How to use checksums
40b953fdc974bd655e905fa928930119719b947a115db608c239ea4d03108806
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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