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 completeSELECTqueryoutput— 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. DefaultEPSG: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— ifTrue, 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 distanceunit—meters,km,miles,feet,usfeet,nautical milesoutput— output file pathwhere— optional SQL attribute filterpreview— ifTrue, 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 againstoutput— output file pathwhere— optional SQL attribute filter on the input layerpreview— ifTrue, 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 geodatabaseoutput_path— destination file. Extension sets the format. Use trailing/for directory output (one file per layer). Use dot notation for named layers:roads.parcels.gpkgx_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.14.0 — 2026-08-06
Mostly one feature and its consequences: Atlas can browse a Databricks workspace and pull spatial layers onto the map without the data crossing a language model's context. Connecting it to a real warehouse then disproved much of what had been written from naming conventions, so several fixes below correct this same feature rather than older ones.
Two changes affect maps you have already built: polygon fills are now as opaque as the slider says (they were capped at half), and Databricks layers cache their features on disk, so reopening a project reads the cache instead of re-querying.
Added
-
Data Explorer — a pane that browses a Databricks workspace's spatial layers and clicks them onto the map. Every table with a geometry column, grouped by catalogue and schema, plus a search box and a curated shortlist of everyday layers. Right-click for Add visible extent / Add all features / Row count / Copy table path.
- Discovery is one query per catalogue against
information_schema.columns, which finds the spatial tables and their geometry column in a single round trip - Search reads a cached manifest in the browser, so typing never hits the warehouse
- Row counts are fetched on demand, and shown before a big table is drawn
- Adding defaults to the visible extent; a table that cannot take a spatial predicate falls back to a row limit and says so
- Discovery is one query per catalogue against
-
Claude can fill the pane over MCP, so browsing needs no token. Ask it to list the workspace's spatial layers and it runs discovery through its own Databricks connection, then posts the manifest to Atlas (
client.databricks_set_layers()). The pane reads "Browsing only" and says a token is needed only to draw features. This works because a manifest is metadata — a few hundred short rows, well inside the MCP's ~4096-row cap, where the features are not- Atlas cannot borrow Claude's MCP credential: it lives in claude.ai's infrastructure and the model never sees it. So adding without a token is refused before any request goes out, with a link to the workspace's token page rather than a dead end
GET /api/databricks/discovery_sqlhands out the exact query, so the MCP route and the warehouse route return the same shape
-
Pulled features are cached on disk under a key covering table, geometry column, extent, limit and geometry type — an identical pull becomes a file read. Measured on 2,000 pipeline features: 6.6s → 0.1s
- the extent is rounded to ~10 m, so panning a pixel reuses the cache
- nothing expires on a timer; staleness is reported instead (
cached,cache_age, and "from local cache (1h old)" in the toast), with Re-pull fresh to force the warehouse - a cached layer opens with no token at all, so clearing a token strands nothing
- capped at 512 MB, oldest first;
GET/DELETE /api/databricks/cache
-
Pull Extent / Pull All at the top of the Data pane — chosen before you click, because it changes what clicking means. It used to be implicit (always the extent), which read as Atlas ignoring the request. The active mode is named in the footer as well
-
A progress bar at the bottom of the pane with the current stage and an elapsed clock. Indeterminate deliberately: one warehouse query yields no percentage, and a fake one that stalls at 90% is worse than an honest bar plus a number you can act on
-
Binary geometry columns work. Verified against a live warehouse: the
BINARYgeometry out there is PostGIS EWKB, and Databricks' ownST_GeomFromWKBrejects its SRID flag — so no server-sideST_call is possible. Atlas selects it raw, reads it withshapely.wkb(bytes or hex, drivers differ) and clips the extent after the rows land, saying so innotebecause the row limit then applies to the whole table. Without this, US states, US counties and the PLSS grids are unreachable — they are all binary -
Column-name matching is case-insensitive, so a manifest saying
wktworks against a column declaredWKT -
gdb_geomattr_data,geometrysource,blockgeometrylinkandgeom_gmlare no longer offered as geometry — 32 columns thatLIKE '%geom%'was catching and that cannot be drawn -
status()no longer reports an empty catalogue list next to a full layer count when the manifest came from Claude over MCP -
A Databricks layer's source is its query, so a project re-runs it on open rather than caching the features
-
Somewhere to paste a Databricks token. The Data pane pointed at File › Settings and no such field existed. Settings now has a Databricks group — host, warehouse HTTP path, token, row limit, common-layers file — and the token is treated as a secret on every route out:
- per OS account in
%LOCALAPPDATA%\env-able\settings.json(chmod 600on POSIX, where the default644would have exposed it to other local accounts); never inside the repo or the wheel, so it cannot travel with anything shipped - write-only over the API:
GETreturns bullets, and sending bullets back does not overwrite the stored value. The input istype=passwordand is emptied once saved - scrubbed from every error message —
run_sqlis the single choke point, which matters because the bug recorder captures failed-response bodies - unreachable from
project.py, so no project or export can embed one - a Clear button to remove it
- pasted values are normalised: scheme and trailing slash off the host, leading slash onto the warehouse path, quotes and whitespace off the token
- per OS account in
-
A "Find token" button that opens the workspace's token page, so generating one and pasting it back is two clicks. The URL comes from the new
databricksTokenPagesetting or the configured host — a bare workspace URL is completed for you, and anything that is not a web address is refused, since this value opens a browser tab -
Settings has General / Databricks tabs, and Databricks is on the start page — the connection fields used to exist only inside a map, so you could not set up before opening one. Includes Find token and Clear, with the same write-only token handling
-
databricks-sql-connectoris a core dependency, not an optional extra — a plain install used to give you a Data Explorer that browsed, searched, then failed at the one step that matters. Markedpython_version >= '3.10', because the connector requires 3.10 while this package supports 3.8, and an unconditional dependency would silently backtrack to a 2.x release from years ago. On 3.8/3.9 the error says so instead of suggesting an impossible install.env-able[databricks]still works as an alias -
client.databricks_status()/databricks_layers()/databricks_count()/databricks_add()— the data goes warehouse → Atlas → map without crossing a language model's context, so the MCP row and byte caps no longer apply -
Connection comes from Settings or
DATABRICKS_HOST/DATABRICKS_HTTP_PATH/DATABRICKS_TOKEN, so existingenv.pullusers need no setup. The token is write-only over the API and never returned byGET /api/settings -
client.settings()/set_setting()/set_settings()— referenced in the skill's instructions but missing until now, so following them failed -
No Fill in the colour picker — outline-only polygons, for showing a boundary over imagery or stacking two areas without either hiding the other. Fill tab, polygons only (an outline of "none" leaves nothing; on a point it erases the point)
- it is a colour value, not a flag, so it survives projects, reloads and the legend without every one of those needing to know about it
- choosing it hands the old fill colour to the outline, so the polygon becomes an outline rather than vanishing
- the opacity slider and later colour changes no longer silently re-fill it
- the layer chip goes hollow, outlined in its own colour — a blank chip would read as a broken colour
-
Outline thickness in points, at the top of the picker's Outline tab — previously only in the Symbology ribbon. Stored as
symbology.size, the field the render path already used
Fixed
- Opacity now means opacity. Every polygon fill was multiplied by a hardcoded
0.5(0.6graduated), so 100% still showed the basemap through it and no slider position could make it solid. This changes existing maps: polygons that were half-transparent at 100% are now opaque — drag the slider back, or use No Fill if you wanted the outline all along - The Layout map frame no longer escapes the page. The layout maths rebuilt its view
rectangle from window size and the ribbon height while CSS also subtracted the dock width
and the 26px view bar, so the page was centred in the whole window inside a narrower
overflow:hiddenbox — and theposition:fixedmap wrapper was never clipped by the dock. It measures the container now, verified across eight window sizes × both dock states × every template - Editing a query-backed layer no longer stages edits with nowhere to save them. A Databricks layer has no file, so cells looked editable and changes staged into limbo. Editing waits for the file, and clicking a locked cell offers Save As instead of doing nothing
- Layers-panel text was too faint. The Unique Values field heading and "…and N more" measured 2.5:1 (dark) / 2.7:1 (light) against a 4.5:1 requirement; a hidden layer's name was 1.9:1. Both fixed, with hidden layers still visibly dimmer
- Light-theme fix: the selected option in a switch stayed dark-themed.
.set-sw button.onhardcoded navy-on-pale-blue, visible on the new Pull Extent / Pull All toggle and every Settings switch. Now on the--accent-bg/--accent-txpair, along with eight other rules carrying the same hardcoded values (Join and Geoprocessing run buttons, the project Go button, pane tabs, and four low-contrast pale-blue text rules) - An ESRI housekeeping blob is no longer pulled with land-grid layers. All 22
land_gridtables carrygdb_geomattr_data; inlandgrid_texas_blocksit is up to 98 KB a row across 5,836 rows — ~500 MB of nothing, and unrepresentable in GeoJSON. Discovery records that the column exists so the pull canEXCEPTit by name (necessary becauseEXCEPTerrors on absent columns). 25 rows: 1.2s instead of 3.4s. Any binary attribute that slips through anyway is dropped and named innote
v0.13.5 — 2026-08-05
Fixed
- Geometry was stored simplified, not just displayed that way — above 10,000 features the simplified copy was the only one Atlas held, so exports, the GeoPackage deliverable and Layout view all carried approximated geometry. Full resolution is stored now; simplification happens on the way out, and Layout view asks for full
- Adding a layer whose name was taken silently replaced it — names resolve to
Wells_1,Wells_2on every ingest path, and the response says which name was used. Project open, repoint and restore still replace by name on purpose - A spreadsheet with a title row above the header read as empty — the header row is detected now (a row of numbers or dates is not mistaken for one) and an empty unnamed column A is dropped
- Left-clicking a layer opened its right-click menu and left the row highlighted, so applying symbology looked like the features had been selected
- Context menu text was unreadable in the light theme — menus, the query picker and the attribute modal used hard-coded light greys
Changed
- The query builder opens with a clause ready instead of needing + Add Clause first
v0.13.4 — 2026-08-05
UI fixes, mostly in the light theme and the attribute table.
Fixed
- The light theme only reached half the UI — the layer pane, File menu, right-click menus, Morph, Symbology, Geoprocessing, Join, Query, the attribute table header hover, the modals and the badges were all literal dark hex. Elevated surfaces are theme tokens now, defined for both themes together
- The basemap follows the theme — light defaults to Light Gray, dark to Dark Gray, but only when still on the other theme's default, so a deliberate pick is respected
- File → Start page bounced straight back to the map — the redirect that keeps a programmatic map request from being stranded now knows a deliberate visit from a landing
Changed
- Renaming a field happens in the header cell, not a browser prompt
- Changing a type is a submenu, with the length box only for text
- The Fields dialog edits names and types inline — Rename and Type buttons are gone
- Blank map uses a
+icon rather than an empty square
v0.13.3 — 2026-08-05
Fixed
- A new field did not appear in the attribute table — the only refresh after the layer's data changed was nested inside the poll's filter-changed branch, so the column showed up only if a filter changed too. The table now refreshes whenever its data does, and a schema change pulls the layer immediately instead of waiting for the next poll
Changed
- Editing a cell highlights the row instead of selecting the text — select-all meant the first keystroke silently replaced the value. The table is no longer text-selectable; the cell editor still is
- New Atlas mark — three nested contour lines reading as an A, replacing the stacked plates. Sized so all three intervals still separate at a 16 px favicon
v0.13.2 — 2026-08-05
Bug fixes and refinements from a session of real map-making.
Fixed
- Labels drew nothing at all — the glyph server serves Noto Sans and Atlas asked for Open Sans, which 404s. A symbol layer whose font cannot be fetched draws no text and reports no error. Fonts are now validated against what the server has, numeric label fields are coerced to text, and the debug recorder no longer filters glyph 404s — that filter is why the reports kept saying "no failed request"
- The dock and its tab strip did not line up — both read one width variable now
- A Short field would accept a value it cannot store — ranges are enforced at entry
Added
- ArcGIS field types — Text, Short, Long, Double, Date. Older names still open
- Excel-style attribute columns — drag to resize, double-click the edge to autofit, right-click to sort / freeze / fit / rename / retype / set a domain / delete
- ArcGIS Pro-style label placement — position, weight, and collision avoidance on by default; lines label along the line
Changed
- Saving attribute edits no longer asks first — the backup, not the prompt, was always the protection. The toast names the backup
- Attribute tables open in edit mode; a read-only source still opens disarmed
- The refresh interval setting is gone and the map refreshes at its fastest
- The attribute header no longer shows the file format
v0.13.1 — 2026-08-04
Bug fixes and refinements to the panes and ribbon introduced in 0.13.0.
Fixed
- Opening the File menu blanked the ribbon — File is a menu, not a view, but it shared the tab-switching path, so General / Tools / Symbology vanished. They persist now
- Geoprocessing opened with no tool and every parameter showing — the pane tab strip opened panels directly and skipped their initialisation. It now routes through each panel's real opener, so Symbology and Query also get their target layer
- Recent projects appeared not to persist — test fixtures written into the system temp directory had filled the capped list and evicted the real entries. Temp-directory projects are no longer remembered, and the list holds 30 rather than 20
Added
- Remove a project from the Recent list — an × on each start-page card, plus Remove
missing. Forgetting never deletes.
client.forget_recent()/clear_recent() - An Atlas logo, and a favicon on both pages where there was none
Changed
- Join sits in the Geoprocessing ribbon group with Buffer, Clip, Intersect and Spatial Join
- Add a map view or layout from the Catalog, beside the lists of what they create
- The Catalog opens with a project
v0.13.0 — 2026-08-04
Atlas becomes a project-based application rather than a session that vanishes when you close it. Everything here is additive — no existing call changes behaviour.
Added
- Projects — save the whole scene to one
.atlas.jsonand reopen it rendering identically. Declarative specs, no geometry, so it stays a few kilobytes; layers with no file of their own are cached into aMyProject.data/sidecar. The round-trip is proven, not assumed:client.fingerprint()digests everything that affects what the map draws, and the tests save, clear, reopen and assert it is unchanged - A broken data reference shows as a repointable layer, never a silently dropped one
— it still registers, keeps its symbology, carries a readable reason, and stays in
every view it belonged to.
client.repoint_layer(name, path) - Start page with recent projects, data-free templates and settings. Programmatic
launches (
serve(landing=False)) go straight to the map - Settings, with a light theme — dark stays default. A real theme via CSS custom properties, not an inversion; brand colours and the layout paper stay fixed
- Multiple map views — a bottom tab strip, renameable and closeable. Each view owns its layers, basemap and viewport, and returns to where you left it. Symbology lives on the layer, so two views can style one source differently
- Layouts bound to a map view, with their own tabs. Per-layout template, title and text styling persist. A layout whose view is gone opens unbound and rebindable rather than quietly drawing the wrong map
- Catalog pane listing data sources, views and layouts with the view each draws
- Attribute editing, staged — double-click a cell; the map and table update at once and the file is untouched until Save edits, which copies it aside with a timestamp before writing. Discard returns the layer to how it loaded (the source is byte-identical), Undo steps back one change, and Restore puts a committed file back from its backup
- What each format accepts is shown up front — GeoPackage/GeoJSON full, Shapefile with its limits reported per field, CSV/XLSX attributes only, File Geodatabase read-only, no-file layers via Save As
- Schema editing — add with type/length/default, rename, retype, remove. A retype that would lose a value is refused naming the row; a removal keeps its values so Undo restores the data, not just the column
- Field domains (coded list or numeric range) enforced by Atlas whatever the format underneath, and stored in the project — the only place they can persist
- Session recorder (Ctrl+Shift+D files a bug report) and Atlas as a desktop window
Changed
- The four tool panels share one right-hand dock, one open at a time; the map shrinks rather than being covered
- Project format is version 2 — layer specs are project-level and views list names, so symbology appears exactly once in the file. Version 1 projects still open
save_project(map_name=...)is optional; omitted keeps the view's current name
Fixed
- GDAL_DATA / PROJ_LIB self-configure at import — PyCharm and bare
python script.pyskip conda's activation scripts, causingWarning 3: Cannot find tms_*.jsonand quietly degraded CRS lookups - An inferred text length was enforced as a constraint, refusing to lengthen
"CHEVRON"to"CHEVRON USA" - A layer inside a
.gdbwas not recognised as one, since the directory carries the extension rather than the path
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
.prjresolves its own CRS — dragging a lone.shpgives no.prjto 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.prjalways 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/uploadrejected 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 onintersects/within/contains/crosses/overlaps/touches/closest; one-to-one (withstats={"FIELD": "sum"}aggregation) or one-to-many;keep_allfor left vs inner join; addsJoin_Count, renames colliding fields, andclosestaddsdist_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 recorder —
serve(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/metaplus per-layer/geojsonwith ETag caching - GDAL/PROJ auto-configuration —
GDAL_DATAandPROJ_LIBresolved 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
infattribute 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/dropEffectmeant the drop event never fired - Shapefile export was unusable — only the
.shpwas 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.unitin km/meters/miles/feet/nm,max_distancenulls 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 window —
env.atlas.app()orserve(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>andconnect()all unchanged - Atlas: SVG export upgrades — new
bgargument (nonetransparent default,light,dark, or hex; was hardcoded navy) and layer groups now carry real names viaid/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 layer —
client.set_symbology(name, "unique", field=...)with novalueColorsmatched 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.htmlcache 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 withupload_layer+set_viewportfor 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.zipto 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
Clip—ogr2ogr -clipsrc; streaming, no full memory loadBuffer— OGR Python API with auto-UTM zone selection; result in original CRSIntersect—OGR Layer.Intersection(); attributes from both layers in result
SpatialResult— rich return type: counts, CRS, timing, warnings,.gdf,.preview(),.to()- Async variants —
clip_async(),buffer_async(),intersect_async()via thread-pool; GDAL releases GIL for true parallelism - Fluent pipeline —
env.load(source).buffer(...).clip(...).to(...).preview(...).run()and.run_async() where=filter — SQL attribute filter on all three operationsmorphspatial→spatial via ogr2ogr — file-to-file conversions no longer load full dataset into memorymorph_async()— async variant ofenv.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; optionalfieldslist to limit what's addedclient.validate_join()— preview match stats without modifying any layerclient.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.shxregenerated 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file env_able-0.14.0.tar.gz.
File metadata
- Download URL: env_able-0.14.0.tar.gz
- Upload date:
- Size: 1.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad30318cc1db643a3111c2740571006437faf435b258a6cb7da211e5b07f7446
|
|
| MD5 |
27472042bff9682e5b6ce41594771b69
|
|
| BLAKE2b-256 |
bd767e03498df1eaf6652527a37a7905e857e79c16d57c160f90959677261abe
|
File details
Details for the file env_able-0.14.0-py3-none-any.whl.
File metadata
- Download URL: env_able-0.14.0-py3-none-any.whl
- Upload date:
- Size: 1.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c10bac182db0ca71ff3445a64881732480058b94922a10a623e9a3b473accd8a
|
|
| MD5 |
df2ff7d9f407b7ceee9350c5e83a0751
|
|
| BLAKE2b-256 |
112a23456dd49a385aba1b35021b62ce3c5d4c8206dd3043db7adbb03c5313ce
|