georest.esri.services¶
ArcGIS / Esri service query helpers: tile URL construction for Image/Map Services, and GeoJSON feature fetching for Feature Services.
Ported from geeViz’s esriLib.py, with the geeViz-viewer rendering calls (gv.Map.addTileLayer / gv.Map.addLayer) removed - these functions return data (a tile URL template, a GeoJSON dict) instead of rendering it anywhere.
Quick start:
from georest.esri import services
# Image/Map Service tile URL template
tile_url = services.getImageServiceTileUrl("https://.../ImageServer")
# Feature Service query (pre-flight count + fetch), like edw.query_features
# but for arbitrary (non-EDW) ArcGIS Feature Services
geojson = services.queryFeatureService(
"https://services.arcgis.com/.../FeatureServer/0",
where="STATE='UT'",
max_features=2000,
)
Copyright 2026 Ryan Rock and Ian Housman
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
Functions
|
Compute per-band pixel statistics and a value histogram over an area of an ArcGIS Image Service. |
|
Export a rendered image from an ArcGIS Image Service. |
|
Build the |
|
Get detailed metadata for a single service layer: fields, geometry type, and capabilities. |
|
Sample pixel values at multiple points on an ArcGIS Image Service. |
|
List every REST operation actually exposed by a service or layer. |
|
Identify the pixel value at a single point on an ArcGIS Image Service. |
|
Get the true coverage boundary of an ArcGIS Image Service. |
|
Fetch features from an ArcGIS Feature Service, Map Service, or mosaic layer as GeoJSON, optionally filtered by spatial intersection. |
|
Return the number of features matching where (and an optional spatial filter) on a Feature Service, Map Service, or mosaic layer. |
- georest.esri.services.getImageServiceTileUrl(url_or_result: str | dict, token: str | None = None) str[source]¶
Build the
{z}/{y}/{x}tile URL template for an ArcGIS Image Service or cached Map Service.Note
ArcGIS tile URLs use
{z}/{y}/{x}order (y before x), not the XYZ standard{z}/{x}/{y}.Warning
This is pure string construction - no request is made, and nothing is validated. In particular it does not check that the service is actually tiled, and it cannot: a service with no tile cache yields a well-formed template whose tiles all answer HTTP 404 (verified against a live, uncached Image Service). Only cached services serve
/tile. Confirm with?f=jsonfirst - a tiled service reportstileInfo(and a Map Service also reportssingleFusedMapCache: true); an uncached Image Service reports neither and must be rendered throughexportImage()instead.- Parameters:
- Returns:
Tile URL template, e.g.
"https://.../ImageServer/tile/{z}/{y}/{x}".- Return type:
- Raises:
TypeError – If url_or_result is neither a string nor a dict.
ValueError – If a result dict carries no
urlkey.
- georest.esri.services.queryFeatureServiceCount(url_or_result: str | dict, where: str = '1=1', geometry: dict | str | None = None, geometry_type: str = 'esriGeometryEnvelope', spatial_rel: str = 'esriSpatialRelIntersects', in_sr: int = 4326, token: str | None = None, timeout: int | None = None) int[source]¶
Return the number of features matching where (and an optional spatial filter) on a Feature Service, Map Service, or mosaic layer.
- Parameters:
url_or_result (str or dict) – Layer URL (e.g.
".../FeatureServer/0",".../MapServer/160"), or aportal.searchPortal()result dict.where (str, optional) – SQL WHERE clause. Defaults to
"1=1".geometry – Spatial filter geometry. Can be a GeoJSON geometry dict, an Esri JSON geometry dict/string, a bbox string
"xmin,ymin,xmax,ymax", orNonefor no spatial filter.geometry_type (str, optional) – Esri geometry type, e.g.
esriGeometryEnvelope,esriGeometryPolygon.spatial_rel (str, optional) – Spatial relationship. Defaults to
esriSpatialRelIntersects.in_sr (int, optional) – Spatial reference WKID of geometry. Defaults to 4326 (WGS84).
token (str, optional) – ArcGIS token for secured services.
timeout (int, optional) – Request timeout in seconds. Defaults to 60 - some services with large mosaic catalogs can genuinely take longer than that to respond (verified against a live service); pass a larger value rather than assuming a hang.
- Returns:
The number of matching features.
0if the service answers without acountkey at all.- Return type:
- Raises:
RuntimeError – If the service is unreachable or answers with an HTTP error status. Note this is the transport-level failure mode for every function in this module -
_httpconverts bothurllib.error.HTTPErrorandurllib.error.URLErrorintoRuntimeError, so a dead host and a 500 surface the same way.ValueError – If the service answers 200 with an Esri error object in the body (e.g. an invalid where clause), or with a body that isn’t JSON at all (what a degraded HTML error page looks like).
Example:
from georest.esri import services quakes = "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Earthquakes_Since1970/FeatureServer/0" services.queryFeatureServiceCount(quakes, where="magnitude >= 8") # how many before fetching
- georest.esri.services.queryFeatureService(url_or_result: str | dict, where: str = '1=1', geometry: dict | str | None = None, geometry_type: str = 'esriGeometryEnvelope', spatial_rel: str = 'esriSpatialRelIntersects', out_fields: str = '*', max_features: int = 1000, out_sr: int = 4326, token: str | None = None, timeout: int | None = None) dict[str, Any][source]¶
Fetch features from an ArcGIS Feature Service, Map Service, or mosaic layer as GeoJSON, optionally filtered by spatial intersection.
Performs a
returnCountOnly=truepre-flight (honoring the same spatial filter) before fetching geometry. If the result count exceeds max_features, aValueErroris raised with a remediation message instead of fetching a huge payload.- Parameters:
url_or_result (str or dict) – Layer URL (e.g.
".../FeatureServer/0",".../MapServer/160"for a mosaic layer), or aportal.searchPortal()result dict. If the URL points to the FeatureServer root rather than a specific layer,/0is appended automatically.where (str, optional) – SQL WHERE clause sent to the service for server-side filtering. Defaults to
"1=1"(all features).geometry – Spatial filter geometry. Can be a GeoJSON geometry dict, an Esri JSON geometry dict/string, a bbox string
"xmin,ymin,xmax,ymax", orNonefor no spatial filter.geometry_type (str, optional) – Esri geometry type, e.g.
esriGeometryEnvelope,esriGeometryPolygon.spatial_rel (str, optional) – Spatial relationship. Defaults to
esriSpatialRelIntersects.out_fields (str, optional) – Comma-separated field names or
"*".max_features (int, optional) – Hard cap on feature count. Defaults to 1000.
out_sr (int, optional) – Output spatial reference WKID. Also used as the input spatial reference for geometry. Defaults to 4326 (WGS84).
token (str, optional) – ArcGIS token for secured services.
timeout (int, optional) – Request timeout in seconds. Defaults to 60 - some services with large mosaic catalogs can genuinely take longer than that to respond; pass a larger value rather than assuming a hang.
- Returns:
GeoJSON
FeatureCollection.Note
Feature
idvalues differ depending on which of the two response paths served the request - verified against live services. On the normalf=geojsonpath the ids are whatever the server emitted (typically the layer’s integer OBJECTIDs, e.g.20), and property names are passed through untouched. On thef=jsonfallback path (see below) ids are filled in client-side from the feature’s object-id attribute (OBJECTID,FID…), falling back to its position only when it has none, and properties whose names contain a dot (e.g.SHAPE.LEN) are dropped, since many consumers reject dotted keys. Polygons on that path are split by ring winding, as onf=geojson: a multipart polygon comes back as aMultiPolygonand holes stay holes.- Return type:
- Raises:
ValueError – If the feature count exceeds max_features, or the service answers 200 with an Esri error object in the body, or with a body that isn’t JSON at all.
RuntimeError – If the service is unreachable or answers with an HTTP error status (see
queryFeatureServiceCount()).
- georest.esri.services.getLayerInfo(url_or_result: str | dict, token: str | None = None, timeout: int | None = None) dict[str, Any][source]¶
Get detailed metadata for a single service layer: fields, geometry type, and capabilities.
Works for any ArcGIS layer URL - a FeatureServer/MapServer sub-layer, or an Image Service mosaic layer (e.g.
.../MapServer/160).- Parameters:
- Returns:
Keys:
name,geometryType,description,fields(list of{name, type, alias}),extent,maxRecordCount,capabilities(list of supported operations, e.g.["Map", "Query"]),advancedQueryCapabilities,supportedQueryFormats.An ImageServer root URL is a valid target and returns the mosaic catalog’s own fields;
geometryTypeis empty for it, since the service root has no single geometry type of its own.- Return type:
- Raises:
ValueError – If url points at a container rather than a queryable layer - either a group layer or a MapServer/FeatureServer service root - in which case the message names the sub-layers to use instead. Also raised if the service answers with an Esri error object, or with a body that isn’t JSON.
RuntimeError – If the URL is unreachable or answers with an HTTP error status (see
queryFeatureServiceCount()).
Example:
from georest.esri import services quakes = "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Earthquakes_Since1970/FeatureServer/0" info = services.getLayerInfo(quakes) info["geometryType"], [f["name"] for f in info["fields"]]
- georest.esri.services.exportImage(url_or_result: str | dict, bbox: str | dict | list | tuple, bbox_sr: int = 4326, size: tuple[int, int] = (512, 512), out_sr: int | None = None, image_format: str = 'tiff', mosaic_rule: dict | None = None, rendering_rule: dict | None = None, pixel_type: str | None = None, no_data: float | list[float] | None = None, interpolation: str = 'RSP_BilinearInterpolation', out_path: str | None = None, token: str | None = None, timeout: int | None = None) bytes | str[source]¶
Export a rendered image from an ArcGIS Image Service.
- Parameters:
url_or_result (str or dict) – ImageServer URL (e.g.
".../ImageServer"), or aportal.searchPortal()result dict.bbox – Export extent, as a
"xmin,ymin,xmax,ymax"string, a 4-item list/tuple, or a dict with xmin/ymin/xmax/ymax keys.bbox_sr (int, optional) – Spatial reference WKID of bbox. Defaults to 4326 (WGS84).
size (tuple of int, optional) – Output image size in pixels as
(width, height). Defaults to(512, 512)- pass an explicit size for large bboxes rather than relying on the server’s own default, which can be very large.out_sr (int, optional) – Output spatial reference WKID. Defaults to bbox_sr if not given.
image_format (str, optional) – Output image format, e.g.
"tiff","png","png32","jpg","lerc". Defaults to"tiff".mosaic_rule (dict, optional) – Esri mosaic rule JSON, to select which raster(s) in a mosaic dataset contribute to the export (e.g. by attribute or lock a specific raster).
rendering_rule (dict, optional) – Esri raster function JSON, applied server-side before export (e.g. a colormap or band math function).
pixel_type (str, optional) – Output pixel type, e.g.
"U8","F32".no_data (float or list of float, optional) – NoData value(s), one per band.
interpolation (str, optional) – Resampling method, e.g.
"RSP_BilinearInterpolation","RSP_NearestNeighbor","RSP_CubicConvolution". Defaults to"RSP_BilinearInterpolation".out_path (str, optional) – If given, write the image bytes to this path and return the path instead of the raw bytes.
token (str, optional) – ArcGIS token for secured services.
timeout (int, optional) – Request timeout in seconds. Defaults to 60.
- Returns:
The raw image bytes, or the value of out_path if given.
- Return type:
- Raises:
ValueError – If the service returns an error instead of an image - including the 200-with-a-JSON-error-body case described below - or if bbox is malformed.
RuntimeError – If the service is unreachable or answers with an HTTP error status (see
queryFeatureServiceCount()). Note an over-large size is reported this way, as a server-side 500, rather than as a clean Esri error object.
Example:
from georest.esri import services lcms = "https://imagery.geoplatform.gov/iipp/rest/services/Vegetation/USFS_EDW_LCMS_AnnualChange_CONUS/ImageServer" png = services.exportImage(lcms, bbox=(-105.95, 40.5, -105.55, 40.75), size=(600, 400), image_format="png") # In a notebook: from IPython.display import Image; Image(png)
- georest.esri.services.getSupportedOperations(url_or_result: str | dict, token: str | None = None, timeout: int | None = None) list[dict[str, str]][source]¶
List every REST operation actually exposed by a service or layer.
ArcGIS Server does not expose the full operations list as structured JSON - the
?f=jsonrepresentation only carries a coarsecapabilitiesstring (e.g."Image,Metadata,Catalog,Mensuration"), which is not a reliable stand-in: a service can expose operations (computeStatisticsHistograms,getSamples, etc.) with no corresponding capability flag present at all. The authoritative list is only rendered on the plain HTML browse page (?f=html), as a “Supported Operations” section of links - this function fetches that page and parses those links out.- Parameters:
- Returns:
Each entry has
name(human-readable, e.g."Export Image"),operation(URL segment, e.g."exportImage"), andurl(full operation URL).- Return type:
- Raises:
RuntimeError – If the URL is unreachable or answers with an HTTP error status - including the 404 served for a service that does not exist (see
queryFeatureServiceCount()).
Example:
from georest.esri import services lcms = "https://imagery.geoplatform.gov/iipp/rest/services/Vegetation/USFS_EDW_LCMS_AnnualChange_CONUS/ImageServer" [op["name"] for op in services.getSupportedOperations(lcms)]
- georest.esri.services.computeStatisticsHistograms(url_or_result: str | dict, geometry: dict | str, geometry_type: str = 'esriGeometryPolygon', in_sr: int = 4326, mosaic_rule: dict | None = None, rendering_rule: dict | None = None, pixel_size: dict | None = None, token: str | None = None, timeout: int | None = None) list[dict[str, Any]][source]¶
Compute per-band pixel statistics and a value histogram over an area of an ArcGIS Image Service.
Uses the
computeStatisticsHistogramsoperation rather than the narrowercomputeHistograms- it returns both statistics and a histogram together for the same single request, so there’s no reason to call the histogram-only operation separately.- Parameters:
url_or_result (str or dict) – ImageServer URL (e.g.
".../ImageServer"), or aportal.searchPortal()result dict.geometry – Area to compute over. Can be a GeoJSON geometry dict, an Esri JSON geometry dict/string, a bbox string
"xmin,ymin,xmax,ymax", or an Esri JSON polygon/envelope.geometry_type (str, optional) – Esri geometry type, e.g.
esriGeometryPolygon,esriGeometryEnvelope. Defaults toesriGeometryPolygon.in_sr (int, optional) – Spatial reference WKID of geometry. Defaults to 4326 (WGS84).
mosaic_rule (dict, optional) – Esri mosaic rule JSON, to select which raster(s) in a mosaic dataset contribute (e.g. by attribute or lock a specific raster).
rendering_rule (dict, optional) – Esri raster function JSON, applied server-side before computing statistics.
pixel_size (dict, optional) –
{"x", "y", "spatialReference"}to compute at a coarser/finer resolution than native. Omit to use the service default.token (str, optional) – ArcGIS token for secured services.
timeout (int, optional) – Request timeout in seconds. Defaults to 60. At the native resolution of a high-resolution service (e.g. sub-meter imagery), this operation over any non-trivial area can exceed a server-side pixel-count limit outright - pass a coarser pixel_size to avoid that. Even with a reasonable pixel_size, a large area or a service with a big/complex mosaic catalog can legitimately take 30-45+ seconds to respond (verified against live services) - raise this rather than assuming a hang.
- Returns:
One entry per band, each with
band(int index),min,max,mean,stddev,sum,median,mode,count, andhistogram(a dict withmin,max,size,counts).Returns an empty list - not an error - when the service computed nothing for the request. The most common cause is a pixel_size that is absurdly coarse for its own spatial reference: it is interpreted in the units of the
spatialReferenceinside pixel_size, not the raster’s native units, so{"x": 90, "y": 90, "spatialReference": {"wkid": 4326}}asks for 90-degree pixels rather than 90-metre ones, and the service silently returns nothing (verified against a live service). Check for an empty result rather than assuming at least one band.- Return type:
- Raises:
ValueError – If geometry cannot be converted to Esri JSON, or the service answers with an Esri error object or a non-JSON body.
RuntimeError – If the service is unreachable or answers with an HTTP error status (see
queryFeatureServiceCount()).
Example:
from georest.esri import services lcms = "https://imagery.geoplatform.gov/iipp/rest/services/Vegetation/USFS_EDW_LCMS_AnnualChange_CONUS/ImageServer" box = {"rings": [[[-105.9, 40.55], [-105.6, 40.55], [-105.6, 40.7], [-105.9, 40.7], [-105.9, 40.55]]], "spatialReference": {"wkid": 4326}} stats = services.computeStatisticsHistograms(lcms, box) stats[0]["histogram"]["counts"] # pixel count per class value
- georest.esri.services.identifyPixelValue(url_or_result: str | dict, x: float, y: float, in_sr: int = 4326, mosaic_rule: dict | None = None, rendering_rule: dict | None = None, return_catalog_items: bool = False, token: str | None = None, timeout: int | None = None) dict[str, Any][source]¶
Identify the pixel value at a single point on an ArcGIS Image Service.
This is inherently a point operation: the underlying
identifyoperation accepts other geometry types (e.g. polygon), but silently collapses them to their centroid rather than sampling the area (verified against a live service), so this function only takes a single x/y rather than pretending to support area input. For area-wide pixel-value distribution, usecomputeStatisticsHistograms()instead.- Parameters:
url_or_result (str or dict) – ImageServer URL (e.g.
".../ImageServer"), or aportal.searchPortal()result dict.x (float) – X coordinate (longitude, if in_sr is 4326).
y (float) – Y coordinate (latitude, if in_sr is 4326).
in_sr (int, optional) – Spatial reference WKID of x/y. Defaults to 4326 (WGS84).
mosaic_rule (dict, optional) – Esri mosaic rule JSON, to select which raster(s) in a mosaic dataset contribute (e.g. by attribute or lock a specific raster).
rendering_rule (dict, optional) – Esri raster function JSON, applied server-side before identifying the value.
return_catalog_items (bool, optional) – If True, also return the footprint attributes of the raster(s) covering this point (e.g. which mosaic source/year contributed) in the
catalog_itemsresult key. Defaults to False. Note: this does not control whetherband_valuesis populated - see below.token (str, optional) – ArcGIS token for secured services.
timeout (int, optional) – Request timeout in seconds. Defaults to 60 - a service with a large/complex mosaic catalog (many overlapping contributing rasters at a given point) can genuinely take 30-45+ seconds to respond (verified against a live service); pass a larger value rather than assuming a hang.
- Returns:
value(str, the composited pixel value, or"NoData"),band_values(list of str, one per band/raster contributing at this point - empty where the point is NoData),location, andcatalog_items(list of raster footprint{"attributes", "geometry"}dicts if return_catalog_items is True, elseNone).locationis{"x", "y", "spatialReference"}as returned by the service, reprojected into the service’s native spatial reference - not echoed back in in_sr. Querying a WGS84 point on a Web Mercator service returns Web Mercator metres with{"wkid": 102100}(verified against a live service), so read thespatialReferencerather than assuming the coordinates come back in the units you sent.- Return type:
- Raises:
ValueError – If the service answers with an Esri error object or a non-JSON body.
RuntimeError – If the service is unreachable or answers with an HTTP error status (see
queryFeatureServiceCount()).
Example:
from georest.esri import services lcms = "https://imagery.geoplatform.gov/iipp/rest/services/Vegetation/USFS_EDW_LCMS_AnnualChange_CONUS/ImageServer" services.identifyPixelValue(lcms, x=-105.75, y=40.62)["value"] # the class at one point
- georest.esri.services.getSamples(url_or_result: str | dict, points: list[tuple[float, float]], in_sr: int = 4326, mosaic_rule: dict | None = None, pixel_size: dict | None = None, token: str | None = None, timeout: int | None = None) list[dict[str, Any]][source]¶
Sample pixel values at multiple points on an ArcGIS Image Service.
Tries the batch
getSamplesoperation first (one request for every point). If that fails, falls back to callingidentifyPixelValue()once per point instead.This fallback isn’t defensive boilerplate - verified against a live service,
getSamplescan fail outright (HTTP 400, sometimes even 500) for a perfectly valid, correctly-formatted request at specific locations (e.g. a point landing on a NoData pixel), whileidentifyhandles the identical location cleanly. Since a single bad point would otherwise cost every other point’s result too, the fallback keeps results for whichever points can succeed.- Parameters:
url_or_result (str or dict) – ImageServer URL (e.g.
".../ImageServer"), or aportal.searchPortal()result dict.in_sr (int, optional) – Spatial reference WKID of points. Defaults to 4326 (WGS84).
mosaic_rule (dict, optional) – Esri mosaic rule JSON, to select which raster(s) in a mosaic dataset contribute.
pixel_size (dict, optional) –
{"x", "y", "spatialReference"}to sample at a coarser/finer resolution than native.token (str, optional) – ArcGIS token for secured services.
timeout (int, optional) – Request timeout in seconds, applied to both the batch call and any per-point identify fallback. Defaults to 60.
- Returns:
One entry per input point, in the same order as points, each with
x,y,value(str, orNoneif both the batch call and the fallback failed for this point), andsource("getSamples","identify", or an"error: ..."string if this specific point could not be sampled at all).This function does not raise for a point it cannot sample - a failing point is reported in-band via
sourceso the remaining points still return their values.x/yare in in_sr on both paths, but they do not come from the same place: on thegetSamplespath they are echoed back by the service, and on theidentifyfallback they are the input coordinates, copied through unchanged. (The fallback deliberately does not surfaceidentifyPixelValue()’s ownlocation, which the service returns in its native spatial reference rather than in in_sr - mixing the two would put different coordinate systems in one result list.)- Return type:
Note
pixel_size applies only to the batch
getSamplescall; theidentifyfallback has no equivalent parameter, so a point resolved by the fallback is sampled at the service’s native resolution regardless.Example:
from georest.esri import services lcms = "https://imagery.geoplatform.gov/iipp/rest/services/Vegetation/USFS_EDW_LCMS_AnnualChange_CONUS/ImageServer" points = [(-105.75, 40.62), (-105.70, 40.60), (-105.65, 40.58)] [s["value"] for s in services.getSamples(lcms, points)]
- georest.esri.services.queryBoundary(url_or_result: str | dict, mosaic_rule: dict | None = None, out_sr: int = 4326, token: str | None = None, timeout: int | None = None) dict[str, Any][source]¶
Get the true coverage boundary of an ArcGIS Image Service.
Unlike the rectangular
extentreturned bygetLayerInfo()/portal.getServiceMetadata(), this is the actual coverage shape - useful as a pre-flight check for whether a service actually covers an AOI before spending anexportImage()/computeStatisticsHistograms()call on it.Unlike
identify/computeStatisticsHistograms/getSamples, this operation does respect a plainoutSRparam directly (verified against a live service) - there’s no geometry input here to embed a spatial reference into in the first place.- Parameters:
url_or_result (str or dict) – ImageServer URL (e.g.
".../ImageServer"), or aportal.searchPortal()result dict.mosaic_rule (dict, optional) – Esri mosaic rule JSON, to scope the boundary to a subset of the mosaic (e.g. one raster/year). Note: not every mosaic rule type actually changes this operation’s result - verified against a live service, a
where-clause rule that should lock to a single raster returned an identical boundary to no rule at all, and the operation doesn’t validate the rule (a garbage rule is silently ignored rather than erroring).out_sr (int, optional) – Output spatial reference WKID. Defaults to 4326 (WGS84).
token (str, optional) – ArcGIS token for secured services.
timeout (int, optional) – Request timeout in seconds. Defaults to 60 - a service with a large/complex mosaic catalog can genuinely take 30-45+ seconds to respond even for this operation’s simple result (verified against a live service); pass a larger value rather than assuming a hang.
- Returns:
GeoJSON-shaped
{"type": "Polygon", "coordinates": [...], "area": <float>}. Note: unlike the coordinates,areadoes not vary with out_sr - verified against a live service, it’s always reported in the service’s native storage spatial reference (e.g. square meters in Web Mercator), regardless of what SR the geometry itself was requested in.- Return type:
- Raises:
ValueError – If the service answers with an Esri error object or a non-JSON body.
RuntimeError – If the service is unreachable or answers with an HTTP error status (see
queryFeatureServiceCount()).
Example:
from georest.esri import services lcms = "https://imagery.geoplatform.gov/iipp/rest/services/Vegetation/USFS_EDW_LCMS_AnnualChange_CONUS/ImageServer" boundary = services.queryBoundary(lcms) # the area the imagery actually covers