Package: bmi-arcgis-restapi
restapiThis is a Python API for working with ArcGIS REST API, ArcGIS Online, and Portal/ArcGIS Enterprise.  This package has been designed to work witharcpywhen available, or the included open source modulepyshp.  It will try to use arcpy if available for some data conversions, otherwise will use open source options. Also included is a subpackage for administering ArcGIS Server Sites.  This is updated often, so continue checking here for new functionality!Why would you use this package?Esri currently provides theArcGIS API for Pythonwhich provides complete bindings to the ArcGIS REST API.  This package has less coverage of the REST API, but has many convience functions not available in the ArcGIS API for Python and has a strong focus on downloading and querying data.  This package will also support older versions of Python (i.e. 2.7.x) whereas Esri's package only supports 3.x.Release HistoryRelease HistoryInstallationrestapiis supported on Python 2.7 and 3.x. It can be found onGithubandPyPi. To install using pip:pipinstallbmi-arcgis-restapiAfter installation, it should be available to use in Python:importrestapiA note aboutarcpyBy default,restapiwill import Esri'sarcpymodule if available. However, this module is not required to use this package.arcpyis only used when available to write data to disk in esri specific formats (file geodatabase, etc) and working witharcpyGeometries.  Whenarcpyis not availalbe, thepyshpmodule is used to write data (shapefile format only) and work withshapefile.Shapeobjects (geometry).  Also worth noting is that open source version is much faster than usingarcpy.That being said, there may be times when you want to forcerestapito use the open source version, even when you have access toarcpy.  Some example scenarios being when you don't need to write any data in an Esri specific format, you want the script to execute very fast, or you are working in an environment wherearcpymay not play very nicely (Flask,Django, etc.).  To forcerestapito use the open source version, you can simply create an environment variable calledRESTAPI_USE_ARCPYand set it toFALSEor0.  This variable will be checked before attempting to importarcpy.Here is an example on how to force open source at runtime:importosos.environ['RESTAPI_USE_ARCPY']='FALSE'# now import restapiimportrestapirequests.exceptions.SSLErrorIf you are seeingrequests.exceptions.SSLErrorexceptions inrestapi>= 2.0, this is probaly due to a change in handling servers without valid SSL certificates. Because many ArcGIS Server instances are accessed using SSL with a self-signed certificate, or through a MITM proxy like Fiddler,restapi< 2.0 defaulted to ignoring SSL errors by setting therequestclient'sverifyoption toFalse. The new default behavior is to enable certificate verification. If you are receiving this error, you are probably accessing your server with a self-signed certificate, or through a MITM proxy. If that is not the case, you should investigate why you are seeing SSL errors, as there would likely be an issue with the server configuration, or some other security issues.To mimic the previous behavior in the newer versions ofrestapi, there are 2 options - disable certificate verification (less secure), orbuild a custom CA bundlewhich includes any self-signed certificates needed to access your server (more secure). Both of these can be done using the newrestapi.RequestClient()feature.importrestapiimportrequestssession=requests.Session()client=restapi.RequestClient(session)restapi.set_request_client(client)# Disable verificationclient.session.verify=False# -or-# Use custom CA bundleclient.session.verify='/path/to/certfile'Sinceverify = Falseis a commonly used setting when dealing with ArcGIS Server instances, it's also possible to use an environment variable. The variable must be set beforerestapiis imported.os.environ['RESTAPI_VERIFY_CERT']='FALSE'importrestapiConnecting to an ArcGIS Serversamples for this section can be found in theconnecting_to_arcgis_server.pyfile.One of the first things you might do is to connect to a services directory (or catalog):importrestapi# connect to esri's sample server 6rest_url='https://sampleserver6.arcgisonline.com/arcgis/rest/services'# connect to restapi.ArcServer instanceags=restapi.ArcServer(rest_url)>>># get folder and service properties>>>print('Number of folders:{}'.format(len(ags.folders)))>>>print('Number of services:{}\n'.format(len(ags.services)))>>>>>># walk thru directories>>>forroot,servicesinags.walk():>>>print('Folder:{}'.format(root))>>>print('Services:{}\n'.format(services))Numberoffolders:13Numberofservices:60Folder:NoneServices:['911CallsHotspot/GPServer','911CallsHotspot/MapServer','Census/MapServer','CharlotteLAS/ImageServer','CommercialDamageAssessment/FeatureServer','CommercialDamageAssessment/MapServer','CommunityAddressing/FeatureServer','CommunityAddressing/MapServer','<...>','Water_Network/MapServer','Wildfire/FeatureServer','Wildfire/MapServer','WindTurbines/MapServer','World_Street_Map/MapServer','WorldTimeZones/MapServer']Folder:AGPServices:['AGP/Census/MapServer','AGP/Hurricanes/MapServer','AGP/USA/MapServer','AGP/WindTurbines/MapServer']Folder:ElevationServices:['Elevation/earthquakedemoelevation/ImageServer','Elevation/ESRI_Elevation_World/GPServer','Elevation/GlacierBay/MapServer','Elevation/MtBaldy_Elevation/ImageServer','Elevation/WorldElevations/MapServer']# ..etcConnecting to a child servicefrom anArcServerinstance, you can connect to any service within that instance by providing a child path or wildcard search match.# connect to a specific service# using just the service name (at the root)usa=ags.getService('USA')#/USA/MapServer -> restapi.common_types.MapService# using the relative path to a service in a foldercensus=ags.getService('AGP/Census')#/AGP/Census/MapServer -> restapi.common_types.MapService# can also just use the service name, but be weary of possible duplicatesinfastructure=ags.getService('Infrastructure')#/Energy/Infrastructure/FeatureServer -> restapi.common_types.FeatureService# using a wildcard searchcovid_cases=ags.getService('*Covid19Cases*')#/NYTimes_Covid19Cases_USCounties/MapServer -> restapi.common_types.MapServicecheck outputs:>>>forservicein[usa,census,infastructure,covid_cases]:>>>print('name: "{}"'.format(service.name))>>>print('repr: "{}"'.format(repr(service)))>>>print('url:{}\n'.format(service.url))name:"USA"repr:"<MapService: USA/MapServer>"url:https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServername:"Census"repr:"<MapService: AGP/Census/MapServer>"url:https://sampleserver6.arcgisonline.com/arcgis/rest/services/AGP/Census/MapServername:"Infrastructure"repr:"<FeatureService: Energy/Infrastructure/FeatureServer>"url:https://sampleserver6.arcgisonline.com/arcgis/rest/services/Energy/Infrastructure/FeatureServername:"NYTimes_Covid19Cases_USCounties"repr:"<MapService: NYTimes_Covid19Cases_USCounties/MapServer>"url:https://sampleserver6.arcgisonline.com/arcgis/rest/services/NYTimes_Covid19Cases_USCounties/MapServerWorking with layerssamples for this section can be found in theworking_with_layers.pyfile.You can connect toMapServiceLayers orFeatureLayers directly by passing the url, or by accessing from the parentFeatureServiceorMapService.also query the layer and get back arcpy.da Cursor like access# get access to the "Cities" layer from USA Map Servicecities=usa.layer('Cities')# or can use layer id: usa.layer(0)# query the map layer for all cities in California with population > 100000where="st = 'CA' and pop2000 > 100000"# the query operation returns a restapi.FeatureSet or restapi.FeatureCollection depending on the return formatfeatureSet=cities.query(where=where)# get result count, can also use len(featureSet)print('Found{}cities in California with Population > 100K'.format(featureSet.count))# print first feature (restapi.Feature).  The __str__ method is pretty printed JSON# can use an index to fetch via its __getitem__ methodprint(featureSet[0])exceeding themaxRecordCountIn many cases, you may run into issues due to themaxRecordCountset for a service, which by default is1000features.  This limit can be exceeded by using theexceed_limitparameter.  Ifexceed_limitis set toTrue, thequery_in_chunksmethod will be called internally to keep fetching until all queried features are returned.# fetch first 1000first1000=cities.query()print('count without exceed_limit:{}'.format(first1000.count))# can also use len()# fetch all by exceeding limitallCities=cities.query(exceed_limit=True)print('count without exceed_limit:{}'.format(len(allCities)))check outputs:Found57citiesinCaliforniawithPopulation>100K{"type":"Feature","geometry":{"type":"Point","coordinates":[-117.88976896399998,33.836165033999976]},"properties":{"areaname":"Anaheim"}}countwithoutexceed_limit:1000totalrecords:3557countwithexceed_limit:3557using a search cursorA query can also be done by using a search cursor, which behaves very similarly to thearcpy.da.SearchCursor.  While therestapi.Cursordoes support usage of awithstatement, it is not necessary as there is no file on disk that is opened.  When using thecursor()method on aMapServiceLayerorFeatureLayerit will actually make a call to the ArcGIS Server and will return a tuple of values based on thefieldsrequested.  The defaultfieldsvalue is*, which will return all fields.  Like the arcpy cursors, theSHAPE@token can be used to fetch geometry, while theOID@token will fetch the object id.# if you don't want the json/FeatureSet representation, you can use restapi cursors# for the query which are similar to the arcpy.da cursors. The `SearchCursor` will return a tuplecursor=restapi.SearchCursor(cities,fields=['areaname','pop2000','SHAPE@'],where=where)forrowincursor:print(row)check outputs:('Anaheim',328014,<restapi.shapefile.Shapeobjectat0x000002B1DE232400>)('Bakersfield',247057,<restapi.shapefile.Shapeobjectat0x000002B1DE232470>)('Berkeley',102743,<restapi.shapefile.Shapeobjectat0x000002B1DE232400>)('Burbank',100316,<restapi.shapefile.Shapeobjectat0x000002B1DE232470>)('Chula Vista',173556,<restapi.shapefile.Shapeobjectat0x000002B1DE232400>)('Concord',121780,<restapi.shapefile.Shapeobjectat0x000002B1DE232470>)('Corona',124966,<restapi.shapefile.Shapeobjectat0x000002B1DE232400>)# ... etcalso supportswithstatements:withrestapi.SearchCursor(cities,fields=['areaname','pop2000','SHAPE@'],where=where)ascursor:forrowincursor:print(row)In the above examples, when theSearchCursoris instantiated, it actually kicked off a new query for the cursor results.  If you already have aFeatureSetorFeatureCollectionlike we did above, you can also save a network call by constructing therestapi.Cursorfrom the feature set by doing:featureSet=cities.query(where=where)# can pass in fields as second argument to limit resultscursor=restapi.Cursor(featureSet,['areaname','pop2000','SHAPE@'])note: Both theMapServiceLayerandFeatureLayeralso have asearchCursor()convenience method, which will allow you to use the same functionality as shown above but without passing in the layer itself:rows = cities.searchCursor(['areaname', 'pop2000', 'SHAPE@'])exporting features from a layerData can also be easily exported from aFeatureLayerorMapServicelayer.  This can be done by exporting a feature set directly or from the layer itself:# set output folderout_folder=os.path.join(os.path.expanduser('~'),'Documents','restapi_samples')# create output folder if it doesn't existifnotos.path.exists(out_folder):os.makedirs(out_folder)# output shapefileshp=os.path.join(out_folder,'CA_Cities_100K.shp')# export layer from map servicecities.export_layer(shp,where=where)You can also export a feature set directly if you already have one loaded:restapi.exportFeatureSet(featureSet,shp)exporting layers or feature sets also supports afieldsfilter, where you can limit which fields are exported. Other options are supported as well such as an output spatial reference.  One important thing to note, if you do not have access toarcpy, you can only export to shapefile format.  If you do havearcpy, any output format supported by esri will work.  If the output is ageodatabase feature class, you can also choose to include things likedomainsif applicable.# exporting featuresout_folder=os.path.join(os.path.expanduser('~'),'Documents','restapi_samples')ifnotos.path.exists(out_folder):os.makedirs(out_folder)shp=os.path.join(out_folder,'CA_Cities_100K.shp')# export layer to a shapefilecities.export_layer(shp,where=where)# if there is an existing feature set, you can also export that directly# restapi.exportFeatureSet(featureSet, shp)selecting features by geometryBoth theMapServiceLayerandFeatureLayersupport arcgis spatial selections usingarcgis geometryobjects. Theselect_layer_by_locationmethod will return aFeatureSetorFeatureCollection, while theclipmethod will actually export the spatial selection to a shapffile or Feature Class (latter only available witharcpy).thegeometry-helperapplicationThis package now includes ageometry-helperapplication that can be used to quickly create geometries and copy the geometry in eitheresri-jsonorgeojsonformat to form geometry necessary to make queries:to open this application in a web browser, simply call:restapi.open_geometry_helper()select by locationto select a layer by location, first create a geometry object as json (can be esri or geojson format) and make the selection:# select layer by locationuniversities_url='https://services1.arcgis.com/Hp6G80Pky0om7QvQ/arcgis/rest/services/Colleges_and_Universities/FeatureServer/0'# get feature layeruniversities=restapi.FeatureLayer(universities_url)print('universities: ',repr(universities))# form geometry (do not have to cast to restapi.Geometry, this will happen under the hood automatically)geometry=restapi.Geometry({"spatialReference":{"latestWkid":3857,"wkid":102100},"rings":[[[-10423340.4579098,5654465.8453829475],[-10324889.565478457,5654465.8453829475],[-10324889.565478457,5584449.527473665],[-10423340.4579098,5584449.527473665],[-10423340.4579098,5654465.8453829475]]]})# make selectionfeatureCollection=universities.select_by_location(geometry)print('Number of Universities in Twin Cities area:{}'.format(featureCollection.count))# can also export the feature collection directly or call the clip() method (makes a new call to server)universities_shp=os.path.join(out_folder,'TwinCities_Univiersities.shp')universities.clip(geometry,universities_shp)check outputs:universities:<FeatureLayer:"CollegesUniversities"(id:0)>totalrecords:50NumberofUniversitiesinTwinCitiesarea:50totalrecords:50Created:"L:\Users\calebma\Documents\restapi_samples\TwinCities_Univiersities.shp"FetchedallrecordsFeatureLayer Editingsamples for this section can be found in thefeature_layer_editing.pyfile.This package also includes comprehensive support for working withFeatureLayers.  These are editable layers hosted by an ArcGIS Server, Portal, or ArcGIS Online.  AFeatureLayeris an extension of theMapServiceLayerthat supports editing options.add features usingFeatureLayer.addFeatures()# connect to esri's Charlotte Hazards Sample Feature Layer for incidentsurl='https://services.arcgis.com/V6ZHFr6zdgNZuVG0/ArcGIS/rest/services/Hazards_Uptown_Charlotte/FeatureServer/0'# instantiate a FeatureLayerhazards=restapi.FeatureLayer(url)# create a new feature as jsonfeature={"attributes":{"HazardType":"Road Not Passable","Description":"restapi test","SpecialInstructions":"Contact Dispatch","Priority":"High","Status":"Active"},"geometry":create_random_coordinates()}# add json featureadds=hazards.addFeatures([feature])print(adds)check outputs:Added1feature(s){"addResults":[{"globalId":"8B643546-7253-4F0B-80DE-2DE8F1754C03","objectId":11858,"success":true,"uniqueId":11858}]}add an attachment# add attachment to new feature using the OBJECTID of the new featureoid=adds.addResults[0].objectIdimage=os.path.join(os.path.abspath('...'),'docs','images','geometry-helper.png')attRes=hazards.addAttachment(oid,image)print(attRes)# now query attachments for new featureattachments=hazards.attachments(oid)print(attachments)check outputs:Addedattachment'211'forfeatureobjectId{"addAttachmentResult":{"globalId":"f370a4e0-3976-4809-b0d5-f5d1b5990b4b","objectId":211,"success":true}}[<AttachmentID:211(geometry-helper.png)>]update features# update the feature we just added, payload only has to include OBJECTID and any fields to updateupdatePayload=[{"attributes":{"OBJECTID":r.objectId,"Description":"restapi update"}}forrinadds.addResults]updates=hazards.updateFeatures(updatePayload)print(updates)check outputs:Updated1feature(s){"updateResults":[{"globalId":null,"objectId":11858,"success":true,"uniqueId":11858}]}delete features# delete features by passing in a list of objectIdsdeletePayload=[r.objectIdforrinupdates.updateResults]deletes=hazards.deleteFeatures(deletePayload)print(deletes)check outputs:Deleted1feature(s){"deleteResults":[{"globalId":null,"objectId":11858,"success":true,"uniqueId":11858}]}feature editing withrestapicursorsrestapialso supports cursors similar to what you get when usingarcpy.  However, these work directly with the REST API and JSON features while also supportingarcpyandshapefilegeometry types.  See the below example on how to use aninsertCursorto add new records:adding features with anInsertCursorNew records can be added using anInsertCursor.  This can be instantiated using theInsertCursorclass itself, or by calling theinsertCursormethod of aFeatureLayer.  When called asFeatureLayer.insertCursor, the layer itself will not need to be passed in.>>># add 5 new features using an insert cursor>>># using this in a "with" statement will call applyEdits on __exit__>>>fields=["SHAPE@",'HazardType',"Description","Priority"]>>>withrestapi.InsertCursor(hazards,fields)asirows:foriinlist(range(1,6)):desc="restapi insert cursor feature{}".format(i)irows.insertRow([create_random_coordinates(),"Wire Down",desc,"High"])Added5feature(s)Any time an insert or update cursor will save changes, it will print a short message showing how many features were affected.  You can always get at the raw edit information from theFeatureLayerby calling theeditResultsproperty.  This will be an array that stores the results of everyapplyEditsoperation, so the length will reflect how many times edits have been saved.>>># we can always view the results by calling FeatureLayer.editResults which stores>>># an array of edit results for each time applyEdits() is called.>>>print(hazards.editResults)[{"addResults":[{"globalId":"2774C9ED-D8F8-4B71-9F37-B26B40790710","objectId":12093,"success":true,"uniqueId":12093},{"globalId":"18411060-F6FF-49FB-AD91-54DB65C13D06","objectId":12094,"success":true,"uniqueId":12094},{"globalId":"8045C840-F1B9-4BD2-AEDC-72F4D65EB7A6","objectId":12095,"success":true,"uniqueId":12095},{"globalId":"EC9DB6FC-0D34-4B83-8C98-398C7B48666D","objectId":12096,"success":true,"uniqueId":12096},{"globalId":"C709033F-DF3B-43B3-8148-2299E7CEE986","objectId":12097,"success":true,"uniqueId":12097}],"deleteResults":[],"updateResults":[]}]note: when using awithstatement for theInsertCursorandUpdateCursorit will automatically call theapplyEdits()method on__exit__, which is critical to submitting the new, deleted, or updated features to the server.  If not using awithstatement, you will need to callapplyEdits()manually after changes have been made.updating features with anUpdateCursorrecords can be updated or deleted with anupdateCursorand a where clause.  Note that theOBJECTIDfield must be included in the query to indicate which records will be updated.  TheOID@field token can be used to retreive theobjectIdFieldName:>>># now update records with updateCursor for the records we just added. Can use the>>># editResults property of the feature layer to get the oids of our added features>>>addedOids=','.join(map(str,[r.objectIdforrinhazards.editResults[0].addResults]))>>>whereClause="{}in ({})".format(hazards.OIDFieldName,addedOids)>>>withrestapi.UpdateCursor(hazards,["Priority","Description","OID@"],where=whereClause)asrows:forrowinrows:ifnotrow[2]%2:# update rows with even OBJECTID'srow[0]="Low"rows.updateRow(row)else:# delete odd OBJECTID rowsprint('deleting row with odd objectid: ',row[2])rows.deleteRow(row)Updated2feature(s)Deleted3feature(s)>>># now delete the rest of the records we added>>>whereClause="Description like 'restapi%'">>>withrestapi.UpdateCursor(hazards,["Description","Priority","OID@"],where=whereClause)asrows:forrowinrows:rows.deleteRow(row)irows.insertRow([create_random_coordinates(),"Wire Down",desc,"High"])Deleted2feature(s)attachment editingOffline capabilities (Sync)# if sync were enabled, we could create a replica like this:# can pass in layer ID (0) or name ('incidents', not case sensative)replica=fs.createReplica(0,'test_replica',geometry=adds[0]['geometry'],geometryType='esriGeometryPoint',inSR=4326)# now export the replica object to file geodatabase (if arcpy access) or shapefile with hyperlinks (if open source)restapi.exportReplica(replica,folder)Working with Image Servicesurl='http://pca-gis02.pca.state.mn.us/arcgis/rest/services/Elevation/DEM_1m/ImageServer'im=restapi.ImageService(url)# clip DEMgeometry={"rings":[[[240006.00808044084,4954874.19629429],[240157.31010183255,4954868.8053006204],[240154.85966611796,4954800.0316874133],[240003.55764305394,4954805.4226145679],[240006.00808044084,4954874.19629429]]],"spatialReference":{"wkid":26915,"latestWkid":26915}}tif=os.path.join(folder,'dem.tif')im.clip(geometry,tif)# test point identifyx,y=400994.780878,157878.398217elevation=im.pointIdentify(x=x,y=y,sr=103793)print(elevation)Geocoding# hennepin county, MN geocoderhenn='http://gis.hennepin.us/arcgis/rest/services/Locators/HC_COMPOSITE/GeocodeServer'geocoder=restapi.Geocoder(henn)# find target field, use the SingleLine address field by defaultgeoResult=geocoder.findAddressCandidates('353 N 5th St, Minneapolis, MN 55403')# export results to shapefileprint('found{}candidates'.format(len(geoResult))geocoder.exportResults(geoResult,os.path.join(folder,'target_field.shp'))# Esri geocoderesri_url='http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Locators/ESRI_Geocode_USA/GeocodeServer'esri_geocoder=restapi.Geocoder(esri_url)# find candidates using key word arguments (**kwargs) to fill in locator fields, no single line optioncandidates=esri_geocoder.findAddressCandidates(Address='380 New York Street',City='Redlands',State='CA',Zip='92373')print('Number of address candidates:{}'.format(len(candidates)))forcandidateincandidates:print(candidate.location)# export results to shapefileout_shp=os.path.join(folder,'Esri_headquarters.shp')geocoder.exportResults(candidates,out_shp)Geoprocessing Services# test esri's drive time analysis GP Taskgp_url='http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Network/ESRI_DriveTime_US/GPServer/CreateDriveTimePolygons'gp=restapi.GPTask(gp_url)# get a list of gp parameters (so we know what to pass in as kwargs)print('\nGP Task "{}" parameters:\n'.format(gp.name)forpingp.parameters:print('\t',p.name,p.dataType)point={"geometryType":"esriGeometryPoint","features":[{"geometry":{"x":-10603050.16225853,"y":4715351.1473399615,"spatialReference":{"wkid":102100,"latestWkid":3857}}}],"sr":{"wkid":102100,"latestWkid":3857}}# run task, passing in gp parameters as keyword arguments (**kwargs)gp_res=gp.run(Input_Location=str(point),Drive_Times='1 2 3',inSR=102100)# returns a GPResult() object, can get at the first result by indexing (usually only one result)# can test if there are results by __nonzero__()ifgp_res:result=gp_res.results[0]# this returned a GPFeatureRecordSetLayer as an outputParameter, so we can export this to polygonsprint('\nOutput Result: "{}", data type:{}\n'.format(result.paramName,result.dataType))# now export the result value to fc (use the value property of the GPResult object from run())drive_times=os.path.join(folder,'drive_times.shp')restapi.exportFeatureSet(drive_times,gp_res.value)A note about input Geometriesrestapi will try to use arcpy first if you have it, otherwise will defer to open source.  Both
support the reading of shapefiles to return the first feature back as a restapi.Geometry objectIt also supports arcpy Geometries and shapefile.Shape() objects>>>shp=r'C:\TEMP\Polygons.shp'# a shapefile on disk somewhere>>>geom=restapi.Geometry(shp)>>>print(geom.envelope())-121.5,38.3000000007,-121.199999999,38.6000000015Token Based Securityrestapi also supports secured services.  This is also session based, so if you sign in once to an
ArcGIS Server Resource (on the same ArcGIS Site), the token will automatically persist via the
IdentityManager().There are 3 ways to authticate:# **kwargs for all accessing all ArcGIS resources are# usr   -- username# pw    -- password# token -- token (as string or restapi.Token object)# proxy -- url to proxy# secured urlsecured_url='http://some-domain.com/arcgis/rest/services'# 1. username and passwordags=restapi.ArcServer(url,'username','password')# token is generated and persists# 2. a token that has already been requestedags=restapi.ArcServer(url,token=token)# uses a token that is already active# 3. via a proxy (assuming using the standard esri proxy)#   this will forward all subsequent requests through the proxyags=restapi.ArcServer(url,proxy='http://some-domain.com/proxy.ashx')You can even just generate a token and let the IdentityManager handle the rest.  It is even smart enough to handle multiple tokens for different sites:# login to instance 1usr='username'pw='password'# urls to two different ArcGIS Server sitesurl_1='http://some-domain.com/arcserver1/rest/services'url_2='http://domain2.com/arcgis/rest/services'# generate tokenstok1=restapi.generate_token(url_1,usr,pw)tok2=restapi.generate_token(url_2,usr,pw)# now we should be able to access both ArcGIS Server sites via the IdentityManagerarcserver1=restapi.ArcServer(url_1)# tok1 is automatically passed in and handledarcserver2=restapi.ArcServer(url_2)# tok2 is used hereThe admin Subpackagerestapi also contains an administrative subpackage (warning: most functionality has not been tested!).  You can import this module like this:fromrestapiimportadminConnecting to a Portalurl='https://domain.gis.com/portal/home'portal=admin.Portal(url,'username','password')# get serversservers=portal.servers# stop sample cities serviceserver=servers[0]service=server.service('SampleWorldCities.MapServer')service.stop()To connect to an ArcGIS Server instance that you would like to administer you can do the following:# test with your own serversurl='localhost:6080/arcgis/admin/services'#server urlusr='username'pw='password'# connect to ArcGIS Server instancearcserver=admin.ArcServerAdmin(url,usr,pw)To list services within a folder, you can do this:folder=arcserver.folder('SomeFolder')# supply name of folder as argumentforserviceinfolder.iter_services():print(service.serviceName,service.configuredState# can stop a service like this# service.stop()# or start like this# service.start()print('\n'*3)# show all services and configured state (use iter_services to return restapi.admin.Service() object!)forserviceinarcserver.iter_services():print(service.serviceName,service.configuredState)SecurityYou can set security at the folder or service level.  By default, the addPermssion() method used by Folder and Service objects will make the service unavailable to the general public and only those in the administrator role can view the services.  This is done by setting the 'esriEveryone' principal "isAllowed" value to false.  You can also assign permissions based on roles.arcserver.addPermission('SomeFolder')# by default it will make private True# now make it publically avaiable (unsecure)arcserver.addPermission('SomeFolder',private=False)# secure based on role, in this case will not allow assessor group to see utility data#   assessor is name of assessor group role, Watermain is folder to securearcserver.addPermission('Watermain','assessor',False)# note, this can also be done at the folder level:folder=arcserver.folder('Watermain')folder.addPermission('assessor',False)Stopping and Starting ServicesServices can easily be started and stopped with this module.  This can be done from the ArcServerAdmin() or Folder() object:# stop all services in a folderarcserver.stopServices(folderName='SomeFolder')# this can take a few minutes# look thru the folder to check the configured states, should be stoppedforserviceinarcserver.folder('SomeFolder').iter_services():print(service.serviceName,service.configuredState)# now restartarcserver.startServices(folderName='SomeFolder')# this can take a few minutes# look thru folder, services should be startedforserviceinarcserver.folder('SomeFolder').iter_services():print(service.serviceName,service.configuredState)# to do this from a folder, simply get a folder object backfolder=arcserver.folder('SomeFolder')folder.stopServices()forserviceinfolder.iter_services():print(service.serviceName,service.configuredState)Updating Service PropertiesThe admin package can be used to update the service definitions via JSON.  By default, the Service.edit() method will pass in the original service definition as JSON so no changes are made if no arguments are supplied.  The first argument is the service config as JSON, but this method also supports keyword arguments to update single properties (**kwargs).  These represent keys of a the dictionary in Python.# connect to an individual service (by wildcard) - do not need to include full name, just# enough of the name to make it a unique name queryservice=arcserver.service('SampleWorldCities')#provide name of service here# get original service descriptiondescription=service.description# now edit the description only by using description kwarg (must match key exactly to update)service.edit(description='This is an updated service description')# edit description again to set it back to the original descriptionservice.edit(description=description)There are also some helper methods that aren't available out of the box from the ArcGIS REST API such as enabling or disabling extensions:# disable Feature Access and kml downloadsservice.disableExtensions(['FeatureServer','KmlServer'])# you can also list enabled/disabled servicesprint(service.enabledExtensions)# [u'KmlServer', u'WFSServer', u'FeatureServer']service.disabledExtensions# [u'NAServer', u'MobileServer', u'SchematicsServer', u'WCSServer', u'WMSServer']# Edit service extension properites# get an extension and view its propertiesfs_extension=service.getExtension('FeatureServer')print(fs_extension)# will print as pretty jsonFor Service objects, all properties are represented as pretty json.  Below is what the FeatureService Extension looks like:{"allowedUploadFileTypes":"","capabilities":"Query,Create,Update,Delete,Uploads,Editing","enabled":"true","maxUploadFileSize":0,"properties":{"allowGeometryUpdates":"true","allowOthersToDelete":"false","allowOthersToQuery":"true","allowOthersToUpdate":"false","allowTrueCurvesUpdates":"false","creatorPresent":"false","dataInGdb":"true","datasetInspected":"true","editorTrackingRespectsDayLightSavingTime":"false","editorTrackingTimeInUTC":"true","editorTrackingTimeZoneID":"UTC","enableOwnershipBasedAccessControl":"false","enableZDefaults":"false","maxRecordCount":"1000","realm":"","syncEnabled":"false","syncVersionCreationRule":"versionPerDownloadedMap","versionedData":"false","xssPreventionEnabled":"true","zDefaultValue":"0"},"typeName":"FeatureServer"}Setting properties for extensions is also easy:# set properties for an extension using helper method, use **kwargs for setting capabilitiesservice.setExtensionProperties('FeatureServer',capabilities='Query,Update,Delete,Editing')# verify changes were madeprint(fs_extension.capabilities# 'Query,Update,Delete,Editing'# alternatively, you can edit the service json directly and call the edit method# change it back to original settingsfs_extension.capabilities='Query,Create,Update,Delete,Uploads,Editing'service.edit()# verify one more time...print(fs_extension.capabilities)# 'Query,Create,Update,Delete,Uploads,Editing'Access the Data StoreYou can iterate through the data store items easily to read/update/add items:# connect to the server's data storeds=arcserver.dataStore# iterate through all items of data storeforiteminds:print(item.type,item.path# if it is an enterprise database connection, you can get the connection string like thisifitem.type=='egdb':print(item.info.connectionString)# else if a folder, print(server pathelifitem.type=='folder':print(item.info.path)print('\n')User and Role StoresWhen viewing usernames/roles you can limit the number of names returned using the "maxCount" keyword argument.  To view and make changes to Role Store:# connect to role storers=arcserver.roleStore# print rolesforroleinrs:print(role)# find users within rolesforroleinrs:print(role,'Users: ',rs.getUsersWithinRole(role))# add a user to rolers.addUsersToRole('Administrators','your-domain\\someuser')# remove user from rolers.removeUsersFromRole('Administrators','your-domain\\someuser')# remove an entire rolers.removeRole('transportation')To view and make changes to the User Store:# connect to user storeus=arcserver.userStore# get number of usersprint(len(us)# iterate through first 10 usersforuserinus.searchUsers(maxCount=10):print(user)# add new userus.addUser('your-domain\\someuser','password')# assign roles by using comma separated list of role namesus.assignRoles('your-domain\\someuser','Administrators,Publishers')# get privileges from userus.getPrivilegeForUser('your-domain\\someuser')# remove roles from userus.removeRoles('your-domain\\someuser','Administrators,Publishers')Log FilesYou can easily query server log files like this:importrestapiimportdatetime# query log files (within last 3 days), need to convert to millisecondsthreeDaysAgo=restapi.date_to_mil(datetime.datetime.now()-relativedelta(days=3))forloginarcserver.queryLogs(endTime=threeDaysAgo,pageSize=25):print(log.timeformessageinlog:print(message)print('\n')A note about verbosityWhen using the admin subpackage you will likely be making changes to services/permissions etc.  On operations that change a configuration, the @passthrough decorator will report back if the operation is successful and return results like this:{u'status':u'SUCCESS'}The printing of these messages can be shut off by changing the global "VERBOSE" variable so these messages are not reported.  This can be disabled like this:admin.VERBOSE=FalseAdvanced UsageRequestClientFrom version 2.0, it is possible to use a customrequests.Session()instance. This instance can be defined globally for all requests made byrestapi, or it can be passed on each function call as arestapi.RequestClient()object. This can be useful if different parameters are needed to access different servers.Use this functionality to access servers behind HTTP or SOCKS proxies, to disable certificate validation or use custom CA certificates, or if additional authentication is needed. Refer to therequests.Session()documentation for details# Create a restapi.RequestClient() object.custom_session=requests.Session()custom_client=restapi.RequestClient(custom_session)# Customize the clientproxies={“http”:“http://10.10.10.10:8000”,“https”:“http://10.10.10.10:8000”,}custom_client.session.proxies=proxiescustom_client.session.headers['Source-Client']='Custom'# Use the client for an individual callrest_url='https://gis.ngdc.noaa.gov/arcgis/rest/services'arcserver=restapi.ArcServer(rest_url,client=custom_client)# Set a different client as client as restapi's defaultglobal_session=requests.Session()global_client=restapi.RequestClient(global_session)global_client.session.headers['Source-Client']='Global'restapi.set_request_client(global_client)# Now any call made by restapi will use the custom clientarcserver=restapi.ArcServer(rest_url)# The global client can also be accessed directlyrestapi.requestClient.headers['Another-Header']='Header is here'Any session objects which extendrequests.Session()should be supported, for example,pypac.PACSession().ExceptionsThe modulerestapi.exceptionscontains the following custom exceptions, which correspond to the ESRI REST error returns codes specified in theESRI documentation. Other, previously unknown returns codes will raise the genericRestAPIException:codeException Class400RestAPIUnableToCompleteOperationException401RestAPIAuthorizationRequiredException403RestAPITokenValidAccessDeniedException404RestAPINotFoundException413RestAPITooLargeException498RestAPIInvalidTokenException499RestAPITokenRequiredException500RestAPIErrorPerforningOperationException501RestAPINotImplementedException504RestAPIGatewayTimeoutException