georest.esri.portal¶
ArcGIS / Esri Portal search and service metadata client.
Ported from geeViz’s esriLib.py, with the geeViz-viewer rendering functions (addEsriImageService, addEsriMapService, addEsriFeatureService, addEsriService) removed - this module only fetches data, it does not render into any viewer.
Quick start:
from georest.esri import portal
# Discover data on any ArcGIS Portal
results = portal.searchPortal("naip 2023") # IIPP (default)
results = portal.searchPortal("naip 2023", portal="agol") # ArcGIS Online
results = portal.searchPortal("naip 2023",
portal="https://myagency.gov/portal")
# Available portals
portal.PORTALS.keys() # iipp, agol, usgs, noaa, usfs, nasa
# Inspect any service
meta = portal.getServiceMetadata("https://.../ImageServer")
Secured portals:
# Sign in once (OAuth, works with enterprise SSO); calls without token=
# then use the session wherever it applies. See georest.esri.auth.
from georest.esri import auth
auth.login("https://myorg.maps.arcgis.com", client_id="...")
portal.searchPortal("classified data", portal="https://myorg.maps.arcgis.com")
# An explicit token= still wins; token="" forces an anonymous request.
portal.searchPortal("classified data", token="...")
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
Module Attributes
Module-level dict mapping short names to portal base URLs. |
Functions
|
Fetch and return the JSON metadata for any ArcGIS REST service. |
|
Search any ArcGIS Portal for hosted services. |
- georest.esri.portal.PORTALS: dict[str, str] = {'agol': 'https://www.arcgis.com', 'iipp': 'https://imagery.geoplatform.gov/iipp', 'nasa': 'https://nasa.maps.arcgis.com', 'noaa': 'https://coastalatlas.noaa.gov', 'usfs': 'https://data.fs.usda.gov/geodata', 'usgs': 'https://www.sciencebase.gov/sciencebase'}¶
Module-level dict mapping short names to portal base URLs.
Add your own at runtime:
from portal import PORTALS PORTALS["myagency"] = "https://gis.myagency.gov/portal"
- georest.esri.portal.searchPortal(query: str, portal: str = 'iipp', limit: int = 20, data_only: bool = True, raw_q: str | None = None, token: str | None = None, org_scoped: bool | None = None, **filters: Any) list[dict[str, Any]][source]¶
Search any ArcGIS Portal for hosted services. Defaults to IIPP.
Uses the standard
/sharing/rest/searchendpoint present on ArcGIS Online, IIPP, and any ArcGIS Enterprise install. This is a raw portal search - it has no theme/keyword-alias matching. To search the USFS EDW catalog specifically (a separate, non-portal REST endpoint), usegeorest.esri.edw.search_edw_services(query, theme=...)instead.- Parameters:
query (str) – Free-text search query (e.g.
"naip 2023","fire perimeter").portal (str, optional) – Either a short name from
PORTALS("iipp","agol","usgs","noaa","usfs","nasa") or a full portal base URL. Defaults to"iipp".limit (int, optional) – Maximum results to return (1–100). Defaults to 20.
data_only (bool, optional) – When
True(default), appends a bundled exclusion list that filters out non-data items (styles, web apps, dashboards, etc.) so results are datasets only. Set toFalseto search without restrictions.raw_q (str, optional) – If supplied, overrides the assembled query string entirely - ignores
query,data_only, andfilters. Use for portal query DSL power users.token (str, optional) – ArcGIS token for secured portals. Omit for public services. Obtain via
POST <portal>/sharing/rest/generateToken.org_scoped (bool | None, optional) –
Restrict results to the portal’s own organization by injecting an
orgid:clause. -None(default) - auto. Scopes only for a canonical<org>.maps.arcgis.comURL, and only whenraw_qis not supplied. That includes the built-inportal="nasa"(nasa.maps.arcgis.com), which searches only NASA’s own items; passFalseto search all of ArcGIS Online, asportal="nasa"did in 0.1.0. Global AGOL (portal="agol"), Enterprise portals, and custom vanity domains are NOT auto-scoped; passTruefor those.True- always scope, wrappingraw_qif present.False- never scope (pre-fix behaviour), and the escape hatch if the organization id cannot be resolved.
Scoping costs one extra
/sharing/rest/portals/selfrequest the first time a portal is seen; successful lookups are cached for the process. If the id cannot be resolved this raisesRuntimeErrorrather than silently returning other organizations’ data. When scoping, aquerywith unbalanced parentheses or quotes has those characters dropped (it is searched as plain terms), while araw_qthat could escape theorgid:wrapper raisesValueError.**filters – Extra ArcGIS search filters forwarded as query params (e.g.
sortField="title",sortOrder="asc",bbox="-120,35,-110,42").q,numandfare set by this function and raiseTypeErrorif passed here; useraw_qandlimitinstead.
- Returns:
Parsed portal items. Each dict includes:
id(str): Item ID.title(str): Item title.type(str): Esri item type (e.g."Image Service","Feature Service").snippet(str): Short description.tags(list of str): Associated tags.url(str): Service endpoint URL (may be""if not set).owner(str): Portal username of the owner.created(int): Unix timestamp (ms) of item creation.modified(int): Unix timestamp (ms) of last modification.thumbnail(str or None): Thumbnail URL, orNoneif absent._raw(dict): Full raw portal item dict for advanced access.
An empty list means the search genuinely matched nothing - a rejected search raises rather than returning
[](see below).- Return type:
- Raises:
TypeError – If
filterscontainsq,numorf. Raised before any request is made.ValueError – If the portal answers 200 with an error object - most commonly code 498, an invalid or expired token - or with a body that isn’t JSON. This case used to be swallowed into an empty result list, which read as “no matches”. Also raised when scoping and
raw_qhas unbalanced parentheses or quotes, or a trailing backslash.RuntimeError – If the portal is unreachable or answers with an HTTP error status.
_httpconverts bothurllib.error.HTTPErrorandurllib.error.URLErrorintoRuntimeError, so a dead host and a 500 surface the same way. Also raised when scoping and the organization id cannot be resolved.
Example:
from georest.esri import portal # Search IIPP for NAIP imagery (default portal) results = portal.searchPortal("naip 2023", limit=10) for r in results: print(r["title"], r["type"], r["url"]) # ArcGIS Online results = portal.searchPortal("wildfire perimeter", portal="agol") # Any ArcGIS Online organization or Enterprise portal, by its URL results = portal.searchPortal("active fire", portal="https://nasa.maps.arcgis.com") # Raw portal query DSL (bypasses data_only and filters) results = portal.searchPortal("", raw_q='type:"Feature Service" owner:USGS')
- georest.esri.portal.getServiceMetadata(url: str, token: str | None = None) dict[str, Any][source]¶
Fetch and return the JSON metadata for any ArcGIS REST service.
Appends
?f=jsonto the URL and returns the parsed response. Works for ImageServer, FeatureServer, MapServer, and any sub-layer URL (e.g./FeatureServer/0).- Parameters:
- Returns:
Parsed service metadata. Common keys vary by service type:
name(str): Service name.type(str): Layer geometry type (Feature Services).fields(list): Schema fields (Feature Services).extent(dict): Spatial extent.spatialReference(dict): Spatial reference info.minScale,maxScale(int): Scale range.capabilities(str): Comma-separated capabilities string.
- Return type:
- Raises:
ValueError – If the service answers 200 with an Esri error object - which is how ArcGIS reports a service that does not exist, rather than with an HTTP 404 - or if the response is not valid JSON.
RuntimeError – If the URL is unreachable or answers with an HTTP error status (see
searchPortal()).
Example:
from georest.esri import portal meta = portal.getServiceMetadata( "https://imagery.geoplatform.gov/iipp/rest/services/Vegetation/" "USFS_EDW_LCMS_AnnualChange_CONUS/ImageServer") print(meta["name"]) print(meta["extent"]) # FeatureServer layer 0 meta = portal.getServiceMetadata( "https://sampleserver6.arcgisonline.com/arcgis/rest/services/" "Earthquakes_Since1970/FeatureServer/0") print([f["name"] for f in meta.get("fields", [])])