Skip to main content

env-able

An open source spatial analysis library built for AI-driven GIS workflows. Designed to give AI systems like Claude reliable, hallucination-free tools for spatial operations in energy and subsurface contexts.

Install

pip install env-able

# with Databricks support
pip install env-able[databricks]

# with interactive map server (Atlas)
pip install env-able[atlas]

# with large-scale streaming pulls (DuckDB)
pip install env-able[fast]

Usage

import env_able as env

Functions

env.pull(table, output=None, wkt_col=None, chunk_size=None, crs="EPSG:4326")

Pull a Databricks table or query to a local file, chunking around the row/byte limit automatically.

Databricks caps result sets (~4 096 rows for narrow tables, fewer when WKT columns are present). pull() paginates with LIMIT/OFFSET, assembles the complete dataset in memory, and writes it to disk in the requested format.

Parameters

  • table — fully-qualified table name (catalog.schema.table) or a complete SELECT query
  • output — destination file path; format inferred from extension (.gpkg, .geojson, .shp, .parquet, .csv, .xlsx). Omit to return a GeoDataFrame or DataFrame.
  • wkt_col — column containing WKT geometry strings. Auto-detected if omitted.
  • chunk_size — rows per Databricks query. Defaults to 512 (WKT) or 4 096 (tabular). Reduce if you hit payload errors on wide tables.
  • crs — CRS to assign geometry. Default EPSG:4326.

Environment variables required

DATABRICKS_HOST       # https://adb-<workspace-id>.azuredatabricks.net
DATABRICKS_TOKEN      # personal access token
DATABRICKS_HTTP_PATH  # /sql/1.0/warehouses/<warehouse-id>
import env_able as env

# pull a full table to GeoPackage
env.pull("catalog.schema.wells", "wells.gpkg")

# pull with a filter query
env.pull("SELECT * FROM catalog.schema.wells WHERE state = 'TX'", "wells_tx.gpkg")

# pull tabular (no geometry)
env.pull("catalog.schema.formations", "formations.csv")

# return in-memory without writing
gdf = env.pull("catalog.schema.leases", wkt_col="geom_wkt", crs="EPSG:4269")

env.Clip(input_layer, clip_layer, output=None, where=None, preview=False)

Clips input features to the extent of a polygon boundary (ogr2ogr -clipsrc engine — streaming, handles files larger than RAM). Returns a SpatialResult.

  • input_layer — point, line, or polygon (file path or GeoDataFrame)
  • clip_layer — polygon clip boundary (file path or GeoDataFrame)
  • output — output file path (.gpkg, .shp, .geojson, etc.)
  • where — optional SQL attribute filter applied before clipping, e.g. "STATE = 'TX'"
  • preview — if True, pushes result to a running Atlas server on completion
result = env.Clip("wells.shp", "texas.gpkg", output="wells_tx.gpkg")
result = env.Clip(gdf, "counties.gpkg", where="STATE = 'TX'")
result.preview("Texas Wells", color="#F68D2E")   # push to Atlas
result.to("wells_tx.geojson")                    # write extra format
print(result)                                    # stats: input, output, dropped, CRS, time
gdf = result.gdf                                 # access as GeoDataFrame

env.Buffer(input_layer, distance, unit="meters", output=None, where=None, preview=False)

Buffers input features by a given distance (OGR Python API engine). Auto-selects the best UTM zone for accurate metric distances; result is returned in the original CRS. Returns a SpatialResult.

  • input_layer — point, line, or polygon (file path or GeoDataFrame)
  • distance — numeric buffer distance
  • unitmeters, km, miles, feet, usfeet, nautical miles
  • output — output file path
  • where — optional SQL attribute filter
  • preview — if True, pushes result to Atlas on completion
result = env.Buffer("wells.shp", 1, "miles", output="wells_1mi.gpkg")
result = env.Buffer(gdf, 500)                    # 500 m, no file written
print(result)                                    # SpatialResult stats

env.Intersect(input_layer, intersect_layer, output=None, where=None, preview=False)

Geometric intersection of two layers (OGR Layer.Intersection() engine). Input geometries are trimmed to their overlap; attributes from both layers appear in the result. Returns a SpatialResult.

  • input_layer — point, line, or polygon (file path or GeoDataFrame)
  • intersect_layer — polygon boundary to intersect against
  • output — output file path
  • where — optional SQL attribute filter on the input layer
  • preview — if True, pushes result to Atlas on completion
result = env.Intersect("wells.shp", "permits.gpkg", output="wells_permits.gpkg")
result.preview("Permitted Wells")

env.load(source) — fluent spatial pipeline

Create a chainable SpatialPipeline from a source layer. Add operations with .buffer(), .clip(), .intersect(), set outputs with .to(), push to Atlas with .preview(), then execute with .run() or await .run_async(). Intermediate temp files are cleaned up automatically.

result = (
    env.load("wells.shp")
        .buffer(1, "miles")
        .clip("texas.gpkg")
        .to("wells_1mi_tx.gpkg")
        .preview("Wells in TX", color="#F68D2E")
        .run()
)

# Fan-out multiple pipelines in parallel
import asyncio
results = await asyncio.gather(
    env.load("wells.shp").clip("texas.gpkg").run_async(),
    env.load("wells.shp").clip("new_mexico.gpkg").run_async(),
)

Async variants

All three operations have async counterparts that run in a thread-pool executor. GDAL releases the GIL so multiple calls run truly in parallel via asyncio.gather().

import asyncio

# Single async call
result = await env.clip_async("wells.shp", "texas.gpkg")

# Fan-out in parallel
r1, r2, r3 = await asyncio.gather(
    env.clip_async("wells.shp", "texas.gpkg"),
    env.buffer_async("wells.shp", 1, "miles"),
    env.intersect_async("wells.shp", "permits.gpkg"),
)

SpatialResult

Returned by Clip, Buffer, Intersect, and the pipeline .run(). Exposes operation stats and chainable output methods.

Attribute / Method Description
input_count Feature count of the input layer
output_count Feature count of the result
dropped input_count - output_count
slivers Polygon features with suspiciously small area (intersection artifacts)
crs CRS of the output (e.g. EPSG:4326)
elapsed_s Wall-clock seconds for the operation
warnings List of non-fatal warnings (CRS reprojection, empty result, etc.)
.gdf Load result as a GeoDataFrame (lazy, cached)
.preview(name, color) Push to a running Atlas server
.to(*paths) Write to one or more additional file formats

env.morph(input_path, output_path, **kwargs)

Universal format translation. Converts between shp, gpkg, gdb, csv, xlsx, xls, dbf, geojson, json with automatic CRS handling, field name fixes, and multi-layer support.

  • input_path — source file or geodatabase
  • output_path — destination file. Extension sets the format. Use trailing / for directory output (one file per layer). Use dot notation for named layers: roads.parcels.gpkg
  • x_col, y_col — column names for X/Y coordinates (auto-detected if not provided)
  • wkt_col — column containing WKT geometry (auto-detected if not provided)
  • crs — coordinate reference system e.g. EPSG:4326 (required for tabular → spatial)
env.morph("roads.shp", "roads.gpkg")
env.morph("county.gdb", "county.gpkg")
env.morph("county.gdb", "output_folder/")
env.morph("owners.csv", "owners.geojson", crs="EPSG:4269")
env.morph("owners.csv", "owners.shp", x_col="LONGITUDE", y_col="LATITUDE", crs="EPSG:4269")
env.morph("roads.gpkg", "roads.parcels.gpkg")
env.morph("data.json", "data.gpkg")

# async variant
await env.morph_async("roads.shp", "roads.gpkg")

Smart behavior:

  • GDB / GPKG with multiple layers → detects all layers automatically
  • CRS mismatch → auto-reprojects
  • Shapefile field name limit (10 chars) → auto-truncates with warnings
  • Invalid output path → plain English error
  • Empty layers → skipped with a warning, not a crash

env.atlas — interactive map server

env.atlas launches a browser-based interactive map (MapLibre GL JS) that Claude can load data into and control programmatically. The user opens it in their browser and fine-tunes from there.

Requires pip install env-able[atlas]

import env_able as env

# Start the server (non-blocking — runs in background thread)
env.atlas.serve(block=False)

# Connect and operate
client = env.atlas.connect()
client.add_layer(gdf, "Wells", color="#f5a623")   # push a GeoDataFrame
client.set_viewport([-103.0, 32.0], zoom=7)       # frame the view
print(client.state())                              # check what's on the map
client.save_layout("wells_map.atlas.json")         # save for later

AtlasClient methods:

Method Description
add_layer(data, name, color) Push GeoDataFrame or file path; serializes inline, no temp file
upload_layer(path, name, color) Load any format (gpkg, geojson, csv, xlsx, zip/shp); converts via morph
remove_layer(name) Remove a layer by name
clear() Remove all layers
set_layer_color(name, color) Change a layer's color; browser updates on next poll
reorder_layers(names) Set rendering order — first name = top of map
validate_join(input_layer, input_field, join_source, join_field) Preview join match stats without modifying any layer
join_field(input_layer, input_field, join_source, join_field, fields=None) Left-join attributes from a loaded layer or table into another layer; fields limits which columns are added
set_viewport(center, zoom) Set map view — browser flies there within ~2 s
get_viewport() Read current viewport (reflects user pan/zoom)
state() Layer count, names, colors, feature counts, current viewport
save_layout(path) Write full map state to .atlas.json
load_layout(path) Restore a saved layout
export_svg(path) Export all data layers as a vector SVG (no basemap)
is_running() Health check

The browser UI includes an ArcGIS Pro-style ribbon with basemap switching, file upload, and PNG/PDF export, plus a layer panel with drag-and-drop reorder, independent fill and outline color pickers, per-layer opacity (0–100% in 10% steps), visibility toggle, zoom-to, and remove. The Layout tab provides an ArcGIS Pro-style Layout View with 8 A4 templates, north arrow, dual scale bars (map scale 1:N + RF), and title text formatting.


env.stream — large-scale Databricks pulls

env.stream pages arbitrarily large tables through DuckDB without holding them in RAM, writing directly to GPKG, GeoJSON, Parquet, or CSV. Bypasses the ~4096-row / ~2 MB Databricks response cap.

Requires pip install env-able[fast]

from env_able.stream import pull_to_file, connector_arrow_frames

# Stream a full table to GeoPackage via Arrow (no row cap)
frames = connector_arrow_frames(
    "SELECT * FROM catalog.schema.wells",
    host="https://adb-xxxx.azuredatabricks.net",
    http_path="/sql/1.0/warehouses/xxxx",
    token="dapixxxx"
)
rows = pull_to_file(frames, "wells.gpkg", wkt_col="geom_wkt")
print(f"{rows:,} rows written")

Transports:

Function Method Cap
connector_arrow_frames Databricks SQL connector Arrow batches None
rest_external_links_frames Statement Execution API, Cloud Fetch None
keyset_frames Seek/keyset pagination Configurable page size
offset_frames LIMIT/OFFSET pagination Configurable page size

Changelog

v0.12.2 — 2026-08-04

Bug-fix pass driven by Atlas session recordings. Two of these were producing silently wrong output rather than visible errors.

Added

  • Layer colours from the Enverus palette — new layers take the next unused brand colour instead of all arriving blue. An explicit colour still wins
  • A shapefile dropped without its .prj resolves its own CRS — dragging a lone .shp gives no .prj to read, and coordinates alone cannot identify a UTM zone. Atlas now tests candidate systems and keeps whichever places the data closest to the layers already on the map (or the viewport if the map is empty), logging the choice with a graded confidence. A real .prj always takes precedence
  • Dropping a folder loads every dataset in it — multiple shapefiles, GeoPackages, GeoJSON, KML and File Geodatabases, each as its own layer. Sidecars are not mistaken for datasets; tabular files park as tables rather than having columns sniffed for coordinates
  • Per-value colour editing in the layer legend for Unique Values, including <all other values>
  • Eye icon for layer visibility, slashed and dimmed when hidden

Changed

  • The layer colour chip hides when a layer is classified by Unique Values — the legend already enumerates every colour. It reappears for Single, Graduated and Heatmap

Fixed

  • Exporting a filtered layer exported the whole dataset — the filter lived in the browser as a MapLibre expression and the export never consulted the stored clauses. Export now evaluates it server-side and reports (2 of 5 features)
  • Geoprocessing ignored the active filter too — buffering a layer filtered to three features buffered all of them. Buffer/Clip/Intersect/Spatial Join now honour it, matching ArcGIS Pro definition-query behaviour
  • The Symbology ribbon showed the wrong layer's fields — a removed layer left a dangling reference (producing 404s against a name the server had dropped), and switching layers quickly raced two field requests so the slower one won. Both fixed
  • A large layer could land on the map with no row in the layer panel — so it could not be toggled or removed. Building its extent spread one argument per coordinate (~476,000 for a layer of well laterals) and overflowed the call stack partway through registration. The extent is now computed in a single pass, the panel row is built before the zoom, and a failed registration rolls back so the next poll retries
  • Hiding a layer left its labels on the map
  • /api/upload rejected an omitted colour with HTTP 422

v0.12.1 — 2026-07-30

Mostly fixes, plus a few capabilities that missed the 0.12.0 cut.

Added

  • env.SpatialJoin() / spatial_join_async() — attribute transfer by spatial relationship, ArcGIS Pro semantics. Match on intersects / within / contains / crosses / overlaps / touches / closest; one-to-one (with stats={"FIELD": "sum"} aggregation) or one-to-many; keep_all for left vs inner join; adds Join_Count, renames colliding fields, and closest adds dist_m
  • Atlas: geoprocessing in the ribbon — Buffer, Clip, Intersect and Spatial Join under a new Analysis group, plus POST /api/geoprocess. Results land as new map layers
  • Atlas: session recorderserve(debug_dir=...) arms a REC control. Idle until pressed; then it captures clicks, panel activity, errors and failed requests, and writes a self-contained markdown bug report on demand
  • Atlas: incremental layer loading/api/layers/meta plus per-layer /geojson with ETag caching
  • GDAL/PROJ auto-configurationGDAL_DATA and PROJ_LIB resolved at import, so IDE and bare-script runs stop warning and stop degrading CRS lookups
  • Atlas: Symbology pane auto-classifies on field change, matching the ribbon

Fixed

  • A single inf attribute blanked the whole map — it broke the poll endpoint, so no layers loaded at all. All ingest paths now sanitize non-finite values
  • Severe lag with large layers — the poll re-sent every layer's full GeoJSON every 2 s. Poll payload dropped 537 KB → 204 bytes on a 4,000-feature layer
  • Drag-to-export did nothing for layers — incompatible effectAllowed/dropEffect meant the drop event never fired
  • Shapefile export was unusable — only the .shp was sent, without .dbf/.shx/.prj. Now bundled as <name>.shp.zip
  • Layer → GeoJSON and table → CSV exports returned HTTP 500 — scratch file collided with the output path
  • Query filters matched nothing on numeric fields — the map compared type-strictly while the attribute table compared as strings, so the table showed matches the map did not
  • Other Atlas fixes: poll fallback for older servers, deterministic layer ordering, recorder log corruption and missing report context, debug controls hidden in Layout view, basemap tile noise

v0.12.0 — 2026-07-29

  • env.Near() / near_async() — add a distance-to-nearest-feature column to any layer; measured in auto-UTM, returned in the source CRS, every input feature kept. unit in km/meters/miles/feet/nm, max_distance nulls beyond a cutoff, and calls chain so several proximity features land on one table
  • Atlas: Classification legend in the layer pane — every classified layer lists its colors and values under the row (always visible, no expander). Unique Values gets a field header, a row per value and an <all other values> fallback; Graduated gets per-class ranges; Heatmap gets a High→Low bar; Single Symbol adds nothing. Swatches follow geometry (circle / bar / square); caps at 100 rows and scrolls
  • Atlas: Layer chip reflects classification — multi-color band for Unique Values, ramp gradient for Graduated/Heatmap, solid square for Single Symbol
  • Atlas: Desktop windowenv.atlas.app() or serve(window=True) opens the map as an app window instead of a browser tab; pywebview native window if installed, otherwise a chromeless Chrome/Edge window, otherwise the browser. Server, localhost:<port> and connect() all unchanged
  • Atlas: SVG export upgrades — new bg argument (none transparent default, light, dark, or hex; was hardcoded navy) and layer groups now carry real names via id / inkscape:label / <title>, so Illustrator and Inkscape show named layers instead of a flat mass of shapes
  • Atlas: Symbology ribbon — full symbology editing from the ribbon, auto-classifies the moment a field is picked (no Apply step), stays in sync with the slide-in pane, and offers 8 discrete schemes plus 18 continuous ramps. Classifying a field with 100+ distinct values now asks first
  • Atlas fix: sparse symbology no longer blanks a layerclient.set_symbology(name, "unique", field=...) with no valueColors matched no render branch and silently dropped the layer off the map; missing pieces are now derived from the layer's own data, with a clean downgrade to single symbol when a field is missing or non-numeric
  • Other Atlas fixes: poll re-render loop, labels not rendering (glyph font), index.html cache headers, invalid SVG from layer names containing & or <, legend text overflow, ribbon dropdown clipping

v0.11.0 — 2026-07-28

  • Atlas: Morph (Format Translator) — drag a layer or table onto the map canvas to export it; "Drop to Export" overlay + sliding panel; format selector; download streams instantly; exported layer auto-adds to the map scene
  • Atlas: Fill / Outline color tabs — polygon color picker shows Fill and Outline tabs; switching tabs changes which color the swatch grid edits; points and lines show Fill only
  • Atlas: client.set_map_title(title) — set the Layout View title from Python; browser picks it up in ~2 s; pairs with upload_layer + set_viewport for a one-block map delivery
  • Atlas: Selection geometry filters — selection highlight layers now filter by geometry type (fill→polygon, circle→point, line→line+polygon outline)
  • Atlas: Ctrl+drag deselects — hold Ctrl while rubber-band selecting to remove features from the active selection
  • Atlas: GDB export — File Geodatabase output zipped to .gdb.zip to avoid Windows permission errors on directory-format writes
  • SKILL.md — full 9-color Enverus brand palette documented; fast map request playbook (inline python -c, Layout pane as deliverable, <60 s target)
  • Various Atlas fixes: layer row click target, single color swatch, Morph button rename, poll() auto-add after export, defensive morph import

v0.10.1 — 2026-07-24

  • Atlas: Query Builder — ArcGIS Pro-style WHERE clause builder (side panel, 14 operators, AND/OR multi-clause, unique-value picker, MapLibre filter integration); cl ient.query_layer() / client.clear_filter() for Python-driven filtering
  • Atlas: Attribute Table — bottom drawer with sortable columns, filter highlighting, and row count status; works for layers and tables
  • Atlas: Query button — General ribbon tab opens Query Builder with layer/table picker
  • Atlas: Layer row context menu (Visibility, Zoom, Query, Attributes, Opacity, Rename, Remove); visibility button kept inline; scale display rounds to 3 si gnificant figures; ribbon label clipping fixed

v0.10.0 — 2026-07-24

  • Atlas: Fill + outline color pickers — independent per-layer fill and outline color controls in the layer panel
  • Atlas: Layer opacity — 0–100% in 10% steps; applies across all geometry types
  • Atlas: None basemap fix — layers reliably reappear after switching to the blank basemap (isStyleLoaded polling replaces fragile style.load event)
  • Atlas: Title text controls — bold, italic, and color now apply correctly in Layout View, including Full Bleed float titles
  • Atlas: Map scale — populates on page load; no longer drifts on pan (zoom-only updates)
  • Atlas: North arrow — ~1.5× larger; bounding box removed

v0.9.0 — 2026-07-21

Breaking: Clip, Buffer, Intersect now return SpatialResult instead of GeoDataFrame. Use .gdf to get the underlying GeoDataFrame.

  • GDAL/OGR spatial engine — operations rewritten as true GDAL calls, not geopandas wrappers
    • Clipogr2ogr -clipsrc; streaming, no full memory load
    • Buffer — OGR Python API with auto-UTM zone selection; result in original CRS
    • IntersectOGR Layer.Intersection(); attributes from both layers in result
  • SpatialResult — rich return type: counts, CRS, timing, warnings, .gdf, .preview(), .to()
  • Async variantsclip_async(), buffer_async(), intersect_async() via thread-pool; GDAL releases GIL for true parallelism
  • Fluent pipelineenv.load(source).buffer(...).clip(...).to(...).preview(...).run() and .run_async()
  • where= filter — SQL attribute filter on all three operations
  • morph spatial→spatial via ogr2ogr — file-to-file conversions no longer load full dataset into memory
  • morph_async() — async variant of env.morph

v0.8.4 — 2026-07-21

  • client.join_field() — programmatic left-join: carry attributes from any loaded layer or table into another layer in-place; optional fields list to limit what's added
  • client.validate_join() — preview match stats without modifying any layer
  • client.export_svg(path) — export all data layers as a vector SVG; browser Export ribbon SVG button
  • Join Field UI — field checkboxes with Select All / Deselect All; ArcGIS Pro-style two-section validation stats; many-to-one join handling

v0.8.0 — 2026-07-17

  • Multi-file shapefile upload — select .shp + companions together; missing .shx regenerated automatically
  • Layer right-click menu — Rename, Zoom To, Attribute Table, Remove
  • Folder drag-and-drop — drag a shapefile folder from Explorer onto the map
  • CRS prompt modal — manual EPSG/WKT override when CRS cannot be detected
  • ESRI WKT fallback — handles non-standard projection names via pyproj → GDAL → regex parameter extraction

Full history in CHANGELOG.md

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

env_able-0.12.2.tar.gz (1.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

env_able-0.12.2-py3-none-any.whl (1.5 MB view details)

Uploaded Python 3

File details

Details for the file env_able-0.12.2.tar.gz.

File metadata

  • Download URL: env_able-0.12.2.tar.gz
  • Upload date:
  • Size: 1.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for env_able-0.12.2.tar.gz
Algorithm Hash digest
SHA256 f99e126708bf3af9cfea06c4f8cd01b51c85bd8c9a7ce40f2134ee750ccbcf0b
MD5 487286c0e0586b769447a6b77ae9cadd
BLAKE2b-256 a8a1275822e0d5f760477080370e6eae496f8c705a9939fd4a70c98d00760f81

See more details on using hashes here.

File details

Details for the file env_able-0.12.2-py3-none-any.whl.

File metadata

  • Download URL: env_able-0.12.2-py3-none-any.whl
  • Upload date:
  • Size: 1.5 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for env_able-0.12.2-py3-none-any.whl
Algorithm Hash digest
SHA256 dcda986d099a6ed8f19d9a7ebcde0a489fab9716a3c3cedebb4fd8b08c78fc9c
MD5 fe3b66492caafce1a7a328e7c90cf980
BLAKE2b-256 295095d9577475e0bd32ac8545b7ccbca3e458a7ed779b0cbbcb6e45d7557e24

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page