Metadata-Version: 2.4
Name: lyhnidos
Version: 0.2.1
Summary: Python client for Lyhnidos Geo: an admin SDK for the app platform, and (with the [data] extra) Apache Arrow, DuckDB and Ibis integration
Author: Lyhnidos Geo Authors
License-Expression: MIT OR Apache-2.0
Project-URL: Homepage, https://lyhnidos.dev
Project-URL: Documentation, https://lyhnidos.dev/studio
Keywords: gis,geospatial,ogc,arcgis,arrow,duckdb,lyhnidos
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28.0
Provides-Extra: data
Requires-Dist: pyarrow>=14.0.0; extra == "data"
Requires-Dist: duckdb>=1.0.0; extra == "data"
Requires-Dist: ibis-framework>=9.0.0; extra == "data"
Provides-Extra: all
Requires-Dist: pyarrow>=14.0.0; extra == "all"
Requires-Dist: duckdb>=1.0.0; extra == "all"
Requires-Dist: ibis-framework>=9.0.0; extra == "all"
Requires-Dist: polars>=1.0.0; extra == "all"
Requires-Dist: pandas>=2.0.0; extra == "all"
Requires-Dist: geopandas>=1.0.0; extra == "all"
Requires-Dist: shapely>=2.0.0; extra == "all"

# lyhnidos

Python client for **Lyhnidos Geo** — a spatial database platform with OGC API
Features and ArcGIS FeatureServer interfaces, an app platform (end-user
sign-in and row rules), and Apache Arrow streaming.

## Installation

```bash
pip install lyhnidos            # admin SDK + HTTP client (needs only `requests`)
pip install "lyhnidos[data]"    # + Apache Arrow streaming, DuckDB and Ibis
pip install "lyhnidos[all]"     # + Polars, pandas, GeoPandas and Shapely
```

Python 3.10 or newer. Features that need an extra say which one when it is
missing.

## Quickstart

```python
from lyhnidos import LyhnidosClient

client = LyhnidosClient(
    "https://api.lyhnidos.dev",
    database="my_database",
    auth_token="lyhn_xxxxxxxxxxxx",  # an API key from Studio
)

# SQL with the engine's spatial functions
rows = client.query(
    "SELECT name FROM parcels "
    "WHERE ST_DWithin4D(geom, ST_Point4D(144.983, -37.801, 0.0, 0.0), 500, 9e999) = 1"
)
print(rows["rows"])
# A time window of 9e999 (SQLite infinity) disables the time test; 1e12 would
# still drop features stamped in milliseconds. The distance is 3D, so a query
# point at z = 0 sits "below" any elevated feature: use ST_Z(...) of the
# feature, as semantic_search does, to measure along the ground. Queries are
# unindexed scans.

# Import GeoJSON or CSV into a table
with open("parcels.geojson") as f:
    client.import_data(format="geojson", data=f.read(), table_name="parcels")

# Requires the [data] extra: stream a table as Apache Arrow, or into DuckDB
arrow_table = client.fetch_arrow("parcels")
con = client.to_duckdb("parcels")
print(con.execute("SELECT count(*) FROM parcels").fetchall())

# Requires the [all] extra: DataFrames
df = client.to_polars("parcels")
```

`semantic_search(table, search_text, lat, lng, radius_meters=1000)` ranks
features near a point by text similarity, for tables that carry an embedding
column. Distance is horizontal (exact for points and level features;
approximately horizontal for sloped lines or polygons); the search is an unindexed scan.

## App platform admin

`lyhnidos.AppsAdmin` administers a workspace's app platform: the app-user
pool, publishable keys, app users and their sessions, usage, and row rules.
It calls the engine's `/v1/tenants/{id}/...` admin routes with a
workspace-scoped admin key or a fleet key -- never an app's own end-user
surface (`/v1/apps/{workspace}/...`).

```python
from lyhnidos import AppsAdmin, LyhnidosError

admin = AppsAdmin("https://api.lyhnidos.dev", api_key="lyhn_xxxxxxxxxxxx", workspace="acme")

# Mint a publishable key (shown once -- an app's own front end carries this).
key = admin.create_publishable_key("Marketing site")
print(key["key"], key["key_prefix"])

# Invite a user into the pool.
admin.invite_user("new-user@example.com", role="member")

# Let signed-in users read and insert only their own rows of `notes`.
admin.put_table_rules(
    "acme_main",
    "notes",
    enabled=True,
    rules=[
        {"role": "*", "operation": "read", "preset": {"kind": "own_rows"}},
        {"role": "*", "operation": "insert", "preset": {"kind": "own_rows"}},
    ],
)

try:
    admin.get_user("does-not-exist")
except LyhnidosError as error:
    print(error.status, error.code)  # 404 not_found
```

`update_pool(**changes)` reads the pool, merges the given top-level fields
into it, and writes it back -- so changing `site_url` alone never drops
`allowed_origins` or the rest of the pool a plain `put_pool` would otherwise
need restated in full.

The write is conditional: `update_pool` sends the `revision` its read returned as
`If-Match`, so if another administrator (Studio, a second script) changed the pool
in between, the server answers `409 pool_changed` and `update_pool` raises
`LyhnidosError` rather than overwriting their change; read again and retry.
`put_pool(pool)` stays unconditional (last writer wins) unless you pass
`if_match=<revision from get_pool()>`. Against a server that does not report a
`revision` (older than 0.6.13) both are last-writer-wins.

## License

MIT OR Apache-2.0
