Metadata-Version: 2.4
Name: env-able
Version: 0.8.0
Summary: AI-ready GIS toolkit for energy and subsurface workflows.
License: MIT
License-File: LICENSE
Requires-Python: >=3.8
Requires-Dist: geopandas
Requires-Dist: openpyxl
Requires-Dist: pyogrio
Requires-Dist: xlrd
Provides-Extra: atlas
Requires-Dist: fastapi; extra == 'atlas'
Requires-Dist: python-multipart; extra == 'atlas'
Requires-Dist: uvicorn; extra == 'atlas'
Provides-Extra: databricks
Requires-Dist: databricks-sql-connector; extra == 'databricks'
Provides-Extra: fast
Requires-Dist: duckdb; extra == 'fast'
Requires-Dist: requests; extra == 'fast'
Provides-Extra: fiona
Requires-Dist: fiona; extra == 'fiona'
Description-Content-Type: text/markdown

# 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

```bash
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

```python
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>
```

```python
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.Intersect(input_layer, intersect_layer, output=None)`
Computes geometric intersection of input features against a polygon boundary.
- `input_layer` — point, line, or polygon (file path or GeoDataFrame)
- `intersect_layer` — polygon layer defining the intersection boundary
- `output` — file path to save result (`.gpkg`, `.shp`, etc.) — omit to return a GeoDataFrame

```python
import env_able as env
env.Intersect("wells.shp", "boundary.shp", "result.gpkg")
```

---

### `env.Buffer(input_layer, distance, unit="meters", output=None)`
Buffers input features by a given distance and unit.
- `input_layer` — point, line, or polygon (file path or GeoDataFrame)
- `distance` — numeric buffer distance
- `unit` — `meters`, `km`, `miles`, `feet`, `usfeet`, `nautical miles`
- `output` — file path to save result — omit to return a GeoDataFrame

```python
env.Buffer("wells.shp", 1, "miles", "wells_buffer.gpkg")
```

---

### `env.Clip(input_layer, clip_layer, output=None)`
Clips input features to the extent of a polygon clip boundary.
- `input_layer` — point, line, or polygon (file path or GeoDataFrame)
- `clip_layer` — polygon layer defining the clip boundary
- `output` — file path to save result — omit to return a GeoDataFrame

```python
env.Clip("roads.shp", "county.shp", "roads_clipped.gpkg")
```

---

### `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)

```python
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")
```

**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]`

```python
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 |
| `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 |
| `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, color picker (server-synced), visibility toggle, zoom-to, and remove.

---

## 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]`

```python
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.8.0 — 2026-07-17
- **Multi-file shapefile upload** — select `.shp` + companions together; GDAL finds `.prj`/`.dbf`/`.shx` automatically
- **`SHAPE_RESTORE_SHX`** — missing `.shx` regenerated on upload instead of failing
- **Layer right-click menu** — Rename (inline), Zoom To, Attribute Table, Remove
- **`GET /api/layers/{name}/attributes`** REST endpoint for tabular attribute data
- **Folder drag-and-drop** — drag a shapefile folder from Explorer onto the map
- **File picker "All Files"** — no more "Custom" filter in Windows dialog
- **CRS prompt modal** — manual EPSG/WKT override when CRS cannot be detected
- **CRS fix** — removed false EPSG:3857 detection; only EPSG:4326 (decimal degrees) is inferred automatically
- **ESRI WKT fallback** — `_try_read_prj()` handles non-standard ESRI projection names via pyproj → GDAL osr → regex parameter extraction

### v0.6.0 — 2026-07-17
- Browser file upload (Add Layer button) — gpkg, geojson, zip/shp, csv/xlsx, kml
- Layer drag-and-drop reorder in the panel; MapLibre z-order rebuilds live
- Color picker now syncs back to server on commit; Claude can read user color changes
- `client.upload_layer(path)`, `client.set_layer_color(name, color)`, `client.reorder_layers(names)`
- Bidirectional poll — Claude's `remove_layer()` and `set_layer_color()` now reflect in browser without refresh
- `python-multipart` added to atlas extras

### v0.5.1 — 2026-07-15
- `AtlasClient` — `env.atlas.connect()` returns a programmatic client for Claude to operate Atlas without writing Python files
- `client.add_layer`, `set_viewport`, `save_layout`, `load_layout`, `state` and more
- Browser viewport sync — Claude sets view, browser flies to it; user pan/zoom pushed back to server
- Export ribbon tab — Save PNG and Save PDF
- `serve(block=False)` for non-blocking server startup
- Windows `cp1252` fix, `env.atlas` lazy import fix

### v0.5.0 — 2026-07-15
- `env.stream` — memory-flat streaming pulls into DuckDB; bypasses Databricks row/size caps
- `pull_to_file`, `connector_arrow_frames`, `rest_external_links_frames`, `keyset_frames`, `offset_frames`
- `pip install env-able[fast]` extras group (duckdb + requests)
- Default I/O engine switched to pyogrio

### v0.4.0 — 2026-07-15
- `env.atlas` — interactive MapLibre map server (`env.atlas.serve()`, `add_layer`, `remove_layer`, `clear_layers`)
- ArcGIS Pro-style ribbon UI with basemap switcher (7 options) and layer panel
- `pip install env-able[atlas]` extras group

### v0.3.0 — 2026-07-14
- `pull()` — paginated Databricks table fetch with WKT auto-detection and chunked assembly

### v0.2.0 — 2026-07-14
- Renamed import alias convention from `e` to `env`

### v0.1.0 — 2026-07-14
- Initial release: `Intersect()`, `Buffer()`, `Clip()`, `morph()`
- Smart geometry detection for WKT and lat/lon columns
- Multi-layer GDB/GPKG support
