├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── agoTools ├── __init__.py ├── admin.py └── utilities.py ├── license.txt ├── samples ├── AGO Tools.tbx ├── AGOLCat.py ├── MakeLab_CreateGroupAndSequentialUsers.py ├── MakeLab_CreateGroupAndUsersFromCsv.py ├── addNewUsersToGroups.py ├── clearFolder.py ├── clearGroup.py ├── createUserListCSV.py ├── createUserListWithGroups.py ├── deleteItems.py ├── findItemsContainingUrl.py ├── flagAttachments.py ├── listUsersByGroup.py ├── migrateAccount.py ├── moveItemsReassignGroups.py ├── populateBookmarks.py ├── registerItems.py ├── searchAllUserItems.py ├── searchExamples.py ├── searchTopViewedItems.py ├── shareItems.py ├── updateMapServiceUrlsInWebMaps.py ├── updateRegisteredUrlForServiceOrApp.py ├── updateServiceItemsThumbnail.py ├── updateUserRoles.py └── updateWebMapVersionAGX.py ├── search-cheat-sheet.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | *.pyc 3 | build/ 4 | 5 | # Installer logs 6 | pip-log.txt 7 | pip-delete-this-directory.txt -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/esri/contributing). 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ago-tools 2 | 3 | A Python package to assist with administering ArcGIS Online Organizations. 4 | 5 | ## Project Status 6 | >This project is no longer being actively developed. In the short term, the intent is to tidy up any outstanding bugs and small fixes. Longer term, I'd like to rewrite all of the examples from this project to work against the new [ArcGIS API for Python](https://developers.arcgis.com/python/) which will be more robust and ultimately much better maintained than this library. It is actively being developed and you can [join the conversation in the beta](https://earlyadopter.esri.com/callout/?callid={C9060EB8-F000-4611-88F8-4C2D34B28A36}) today. 7 | 8 | >I will continue to accept new PRs in the meantime if anybody is feeling particularly ambitious or wants to help out with the cleanup. 9 | 10 | > @ecaldwell July 7, 2016 11 | 12 | ## Features 13 | 14 | * Create a spreadsheet of all users in the org 15 | * Update map service urls in webmaps 16 | * Search for new users and add them to a list of groups 17 | * Move (migrate) all items between accounts (single or batch) 18 | * Search a portal 19 | * Update sharing of content in groups 20 | * Remove items from an organization 21 | * Automate registration of items via spreadsheet 22 | * Calculate presence of attachments for features 23 | 24 | ## Installation 25 | 26 | 1. Download or clone the project. 27 | 2. Run `python setup.py install` from the command line **--OR--** add the `agoTools` folder to your script directory. 28 | 29 | ## Samples 30 | ### Admin Class 31 | 32 | * [Create a spreadsheet of all users in the organization](samples/createUserListCSV.py) 33 | * [Add new users to a set of groups](samples/addNewUsersToGroups.py) 34 | * Migrate items and group ownership/membership between user accounts: 35 | * [Move all items from one account to another, reassign ownership of all groups, and/or add user to another user's groups](samples/moveItemsReassignGroups.py) 36 | * [Migrate account(s)](samples/migrateAccount.py) 37 | * [Generate a CSV listing the items in the organization](samples/AGOLCat.py) 38 | * [Register items listed in an input CSV to the organization](samples/registerItems.py) 39 | * [Remove (delete) all items from a designated folder under My Content](samples/clearFolder.py) 40 | * [Remove (unshare) all items from a designated group in the organization](samples/clearGroup.py) 41 | * [Update any missing thumbnails for items under My Content with a default](samples/updateServiceItemsThumbnail.py) 42 | * [Assign roles to Username and Role from input CSV](samples/updateUserRoles.py) 43 | * [Delete items listed in a CSV from an organization](samples/deleteItems.py) 44 | * [Find items containing reference of specific path](samples/findItemsContainingUrl.py) 45 | * [Insert bookmarks into webmap from feature extents](samples/populateBookmarks.py) 46 | * [Calculate status of attachments for features in layer](samples/flagAttachments.py) 47 | 48 | 49 | ### Utilities Class 50 | 51 | * [Update map service URLs in web maps](samples/updateMapServiceUrlsInWebMaps.py) 52 | * [Update the URL for registered services or web applications](samples/updateRegisteredUrlForServiceOrApp.py) 53 | * Search Examples ([search cheat sheet](search-cheat-sheet.md)): 54 | * [Search for the top 10 most viewed public items in the organization](samples/searchTopViewedItems.py) 55 | * [Search for all content owned by a specific user (admin view)](samples/searchAllUserItems.py) 56 | 57 | 58 | ## Requirements 59 | 60 | * Python 61 | * Notepad or your favorite Python editor 62 | 63 | ## Resources 64 | 65 | * [Python for ArcGIS Resource Center](http://resources.arcgis.com/en/communities/python/) 66 | * [ArcGIS Blog](http://blogs.esri.com/esri/arcgis/) 67 | * [twitter@esri](http://twitter.com/esri) 68 | 69 | ## Issues 70 | 71 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 72 | 73 | ## Contributing 74 | 75 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/esri/contributing). 76 | 77 | Please use the following style guides when contributing to this particular repo: 78 | 79 | * 4 spaces for indentation 80 | * `'singleQuotes'` instead of `"doubleQuotes"` 81 | * `publicFunction()` vs ` `\_\_internalFunction\_\_()` 82 | * `# Place comments on their own line preceding the code they explain.` 83 | 84 | 85 | ## Licensing 86 | Copyright 2013 Esri 87 | 88 | Licensed under the Apache License, Version 2.0 (the "License"); 89 | you may not use this file except in compliance with the License. 90 | You may obtain a copy of the License at 91 | 92 | http://www.apache.org/licenses/LICENSE-2.0 93 | 94 | Unless required by applicable law or agreed to in writing, software 95 | distributed under the License is distributed on an "AS IS" BASIS, 96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | See the License for the specific language governing permissions and 98 | limitations under the License. 99 | 100 | A copy of the license is available in the repository's [license.txt](license.txt) file. 101 | 102 | [](Esri Tags: ArcGIS-Online Python Tools Library) 103 | [](Esri Language: Python) 104 | -------------------------------------------------------------------------------- /agoTools/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import urllib 4 | import json 5 | import getpass 6 | 7 | class User: 8 | 9 | def __init__(self, username, portal=None, password=None): 10 | if portal == None: 11 | self.portalUrl = 'https://www.arcgis.com' 12 | else: 13 | self.portalUrl = portal 14 | self.username = username 15 | if password == None: 16 | self.password = getpass.getpass() 17 | else: 18 | self.password = password 19 | self.token = self.__getToken__(self.portalUrl, self.username, self.password) 20 | 21 | def __getToken__(self, url, username, password): 22 | '''Retrieves a token to be used with future requests.''' 23 | parameters = urllib.urlencode({'username' : username, 24 | 'password' : password, 25 | 'client' : 'referer', 26 | 'referer': url, 27 | 'expiration': 60, 28 | 'f' : 'json'}) 29 | response = urllib.urlopen(url + '/sharing/rest/generateToken?', parameters).read() 30 | token = json.loads(response)['token'] 31 | return token 32 | 33 | def __portalId__(self): 34 | '''Gets the ID for the organization/portal.''' 35 | parameters = urllib.urlencode({'token' : self.token, 36 | 'f' : 'json'}) 37 | response = urllib.urlopen(self.portalUrl + '/sharing/rest/portals/self?' + parameters).read() 38 | portalId = json.loads(response)['id'] 39 | return portalId 40 | -------------------------------------------------------------------------------- /agoTools/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import urllib,urllib2 4 | import json 5 | import csv 6 | import time 7 | 8 | 9 | from datetime import date, timedelta 10 | from collections import namedtuple 11 | 12 | class Admin: 13 | '''A class of tools for administering AGO Orgs or Portals''' 14 | def __init__(self, username, portal=None, password=None): 15 | from . import User 16 | self.user = User(username, portal, password) 17 | 18 | def __users__(self, start=0): 19 | '''Retrieve a single page of users.''' 20 | parameters = urllib.urlencode({'token' : self.user.token, 21 | 'f' : 'json', 22 | 'start' : start, 23 | 'num' : 100}) 24 | portalId = self.user.__portalId__() 25 | response = urllib.urlopen(self.user.portalUrl + '/sharing/rest/portals/' + portalId + '/users?' + parameters).read() 26 | users = json.loads(response) 27 | return users 28 | def __roles__(self,start=0): 29 | parameters = urllib.urlencode({'token' : self.user.token, 30 | 'f' : 'json', 31 | 'start' : start, 32 | 'num' : 100}) 33 | portalId = self.user.__portalId__() 34 | response = urllib.urlopen(self.user.portalUrl + '/sharing/rest/portals/' + portalId + '/roles?' + parameters).read() 35 | roles = json.loads(response) 36 | return roles 37 | def __groups__(self,start=0): 38 | parameters = urllib.urlencode({'token' : self.user.token, 39 | 'q':'orgid:'+ self._getOrgID(), 40 | 'f' : 'json', 41 | 'start' : start, 42 | 'num' : 100}) 43 | portalId = self.user.__portalId__() 44 | response = urllib.urlopen(self.user.portalUrl + '/sharing/rest/community/groups?' + parameters).read() 45 | groups = json.loads(response) 46 | return groups 47 | def getRoles(self): 48 | ''' 49 | Returns a list of roles defined in the organization. 50 | This is helpful for custom roles because the User's role property simply returns the ID of the role. 51 | THIS DOES NOT INCLUDE THE STANDARD ARCGIS ONLINE ROLES OF ['org_admin', 'org_publisher', 'org_author', 'org_viewer'] 52 | ''' 53 | allRoles = [] 54 | roles = self.__roles__() 55 | for role in roles['roles']: 56 | allRoles.append(role) 57 | while roles['nextStart'] > 0: 58 | roles=self.__roles__(roles['nextStart']) 59 | for role in roles['roles']: 60 | allRoles.append(role) 61 | return allRoles 62 | def getGroups(self): 63 | ''' 64 | Returns a list of groups defined in the organization. 65 | ''' 66 | allGroups = [] 67 | groups = self.__groups__() 68 | for group in groups['results']: 69 | allGroups.append(group) 70 | while groups['nextStart'] > 0: 71 | groups = self.__groups__(groups['nextStart']) 72 | for group in groups['results']: 73 | allGroups.append(group) 74 | return allGroups 75 | def findGroup(self,title): 76 | ''' 77 | Gets a group object by its title. 78 | ''' 79 | parameters = urllib.urlencode({'token' : self.user.token, 80 | 'q':'title:'+title, 81 | 'f' : 'json'}) 82 | portalId = self.user.__portalId__() 83 | response = urllib.urlopen(self.user.portalUrl + '/sharing/rest/community/groups?' + parameters).read() 84 | groupUsers = json.loads(response) 85 | if "results" in groupUsers and len(groupUsers["results"]) > 0: 86 | return groupUsers["results"][0] 87 | else: 88 | return None 89 | def getUsersInGroup(self,groupID): 90 | ''' 91 | Returns a list of users in a group 92 | ''' 93 | parameters = urllib.urlencode({'token' : self.user.token, 94 | 'f' : 'json'}) 95 | portalId = self.user.__portalId__() 96 | response = urllib.urlopen(self.user.portalUrl + '/sharing/rest/community/groups/'+groupID+'/users?' + parameters).read() 97 | groupUsers = json.loads(response) 98 | return groupUsers 99 | def getUsers(self, roles=None, daysToCheck=10000): 100 | ''' 101 | Returns a list of all users in the organization (requires admin access). 102 | Optionally provide a list of roles to filter the results (e.g. ['org_publisher']). 103 | Optionally provide a number to include only accounts created in the last x number of days. 104 | ''' 105 | #if not roles: 106 | # roles = ['org_admin', 'org_publisher', 'org_user'] 107 | #roles = ['org_admin', 'org_publisher', 'org_author', 'org_viewer'] # new roles to support Dec 2013 update 108 | #the role property of a user is either one of the standard roles or a custom role ID. Loop through and build a list of ids from the queried roles. 109 | if roles: 110 | standardRoles = ['org_admin', 'org_publisher', 'org_author', 'org_viewer'] 111 | queryRoleIDs=[] 112 | #if it's a standard role, go ahead and add it. 113 | for roleName in roles: 114 | if roleName in standardRoles: 115 | queryRoleIDs.append(roleName) 116 | #if it's not a standard role, we'll have to look it to return the ID. 117 | allRoles = self.getRoles() 118 | for role in allRoles: 119 | for roleName in roles: 120 | if roleName == role["name"]: 121 | queryRoleIDs.append(role["id"]) 122 | allUsers = [] 123 | users = self.__users__() 124 | for user in users['users']: 125 | if roles: 126 | if not user['role'] in queryRoleIDs: 127 | continue 128 | if date.fromtimestamp(float(user['created'])/1000) > date.today()-timedelta(days=daysToCheck): 129 | allUsers.append(user) 130 | while users['nextStart'] > 0: 131 | users = self.__users__(users['nextStart']) 132 | for user in users['users']: 133 | if roles: 134 | if not user['role'] in queryRoleIDs: 135 | continue 136 | if date.fromtimestamp(float(user['created'])/1000) > date.today()-timedelta(days=daysToCheck): 137 | allUsers.append(user) 138 | return allUsers 139 | def createGroup(self,title,snippet=None,description=None,tags=None,access="org",isViewOnly=False,viewOnly=False,inviteOnly=True,thumbnail=None): 140 | ''' 141 | Creates a new group 142 | ''' 143 | portalId = self.user.__portalId__() 144 | uri = self.user.portalUrl + '/sharing/rest/community/createGroup' 145 | parameters ={'token' : self.user.token, 146 | 'f' : 'json', 147 | 'title' : title, 148 | 'description':description, 149 | 'snippet':snippet, 150 | 'tags':tags, 151 | 'access':access, 152 | 'isInvitationOnly':inviteOnly, 153 | 'isViewOnly':viewOnly, 154 | 'thumbnail':thumbnail} 155 | 156 | parameters = urllib.urlencode(parameters) 157 | req = urllib2.Request(uri,parameters) 158 | response = urllib2.urlopen(req) 159 | result = response.read() 160 | return json.loads(result) 161 | def createUser(self,username,password,firstName,lastName,email,description,role,provider): 162 | ''' 163 | Creates a new user WITHOUT sending an invitation 164 | ''' 165 | invitations = [{"username":str(username), 166 | "password":str(password), 167 | "firstname":str(firstName), 168 | "lastname":str(lastName), 169 | "fullname":str(firstName) + " " + str(lastName), 170 | "email":str(email), 171 | "role":str(role)}] 172 | parameters ={'token' : self.user.token, 173 | 'f' : 'json', 174 | 'subject':'Welcome to the portal', 175 | 'html':"blah", 176 | 'invitationList':{'invitations':invitations}} 177 | 178 | parameters = urllib.urlencode(parameters) 179 | portalId = self.user.__portalId__() 180 | 181 | uri = self.user.portalUrl + '/sharing/rest/portals/' + portalId + '/invite' 182 | req = urllib2.Request(uri,parameters) 183 | response = urllib2.urlopen(req) 184 | 185 | result = response.read() 186 | return json.loads(result) 187 | 188 | def addUsersToGroups(self, users, groups): 189 | ''' 190 | REQUIRES ADMIN ACCESS 191 | Add organization users to multiple groups and return a list of the status 192 | ''' 193 | # Provide one or more usernames in a list. 194 | # e.g. ['user_1', 'user_2'] 195 | # Provide one or more group IDs in a list. 196 | # e.g. ['d93aabd856f8459a8905a5bd434d4d4a', 'f84c841a3dfc4591b1ff83281ea5025f'] 197 | 198 | toolSummary = [] 199 | 200 | # Assign users to the specified group(s). 201 | parameters = urllib.urlencode({'token': self.user.token, 'f': 'json'}) 202 | for group in groups: 203 | # Add Users - REQUIRES POST method (undocumented operation as of 2013-11-12). 204 | response = urllib.urlopen(self.user.portalUrl + '/sharing/rest/community/groups/' + group + '/addUsers?', 'users=' + ','.join(users) + "&" + parameters).read() 205 | # Users not added will be reported back with each group. 206 | toolSummary.append({group: json.loads(response)}) 207 | 208 | return toolSummary 209 | 210 | def removeUsersFromGroups(self, users, groups): 211 | ''' 212 | REQUIRES ADMIN ACCESS 213 | Remove organization users from multiple groups and return a list of the status 214 | ''' 215 | # Provide one or more usernames in a list. 216 | # e.g. ['user_1', 'user_2'] 217 | # Provide one or more group IDs in a list. 218 | # e.g. ['d93aabd856f8459a8905a5bd434d4d4a', 'f84c841a3dfc4591b1ff83281ea5025f'] 219 | 220 | toolSummary = [] 221 | 222 | # Assign users to the specified group(s). 223 | parameters = urllib.urlencode({'token': self.user.token, 'f': 'json'}) 224 | for group in groups: 225 | # Add Users - REQUIRES POST method (undocumented operation as of 2013-11-12). 226 | response = urllib.urlopen(self.user.portalUrl + '/sharing/rest/community/groups/' + group + '/removeUsers?', 'users=' + ','.join(users) + "&" + parameters).read() 227 | # Users not removed will be reported back with each group. 228 | toolSummary.append({group: json.loads(response)}) 229 | 230 | return toolSummary 231 | 232 | def reassignAllUser1ItemsToUser2(self, userFrom, userTo): 233 | ''' 234 | REQUIRES ADMIN ACCESS 235 | Transfers ownership of all items in userFrom/User1's account to userTo/User2's account, keeping same folder names. 236 | - Does not check for existing folders in userTo's account. 237 | - Does not delete content from userFrom's account. 238 | ''' 239 | 240 | # request user content for userFrom 241 | # response contains list of items in root folder and list of all folders 242 | parameters = urllib.urlencode({'token': self.user.token, 'f': 'json'}) 243 | request = self.user.portalUrl + '/sharing/rest/content/users/' + userFrom + '?' + parameters 244 | userContent = json.loads(urllib.urlopen(request).read()) 245 | 246 | # create same folders in userTo's account like those in userFrom's account 247 | for folder in userContent['folders']: 248 | parameters2 = urllib.urlencode({'title' : folder['title'], 'token': self.user.token, 'f': 'json'}) 249 | request2 = self.user.portalUrl + '/sharing/rest/content/users/' + userTo + '/createFolder?' 250 | response2 = urllib.urlopen(request2, parameters2).read() # requires POST 251 | 252 | # keep track of items and folders 253 | numberOfItems = 0 254 | numberOfFolders = 1 255 | 256 | # change ownership of items in ROOT folder 257 | for item in userContent['items']: 258 | parameters3 = urllib.urlencode({'targetUsername' : userTo, 'targetFoldername' : '/', 'token': self.user.token, 'f': 'json'}) 259 | request3 = self.user.portalUrl + '/sharing/rest/content/users/' + userFrom + '/items/' + item['id'] + '/reassign?' 260 | response3 = urllib.urlopen(request3, parameters3).read() # requires POST 261 | if 'success' in response3: 262 | numberOfItems += 1 263 | 264 | ### change ownership of items in SUBFOLDERS (nested loop) 265 | # request content in current folder 266 | for folder in userContent['folders']: 267 | parameters4 = urllib.urlencode({'token': self.user.token, 'f': 'json'}) 268 | request4 = self.user.portalUrl + '/sharing/rest/content/users/' + userFrom + '/' + folder['id'] + '?' + parameters4 269 | folderContent = json.loads(urllib.urlopen(request4).read()) 270 | numberOfFolders += 1 271 | 272 | # change ownership of items in CURRENT folder to userTo and put in correct folder 273 | for item in folderContent['items']: 274 | parameters5 = urllib.urlencode({'targetUsername' : userTo, 'targetFoldername' : folder['title'], 'token': self.user.token, 'f': 'pjson'}) 275 | request5 = self.user.portalUrl + '/sharing/rest/content/users/' + userFrom + '/' + folder['id'] + '/items/' + item['id'] + '/reassign?' 276 | response5 = urllib.urlopen(request5, parameters5).read() # requires POST 277 | numberOfItems += 1 278 | 279 | # summarize results 280 | print ' ' + str(numberOfItems) + ' ITEMS in ' + str(numberOfFolders) + ' FOLDERS (incl. Home folder) copied' 281 | print ' from USER ' + userFrom + ' to USER ' + userTo 282 | 283 | return 284 | def reassignGroupOwnership(self,groupId,userTo): 285 | parameters ={'token' : self.user.token, 286 | 'f' : 'json', 287 | 'targetUsername':userTo} 288 | 289 | parameters = urllib.urlencode(parameters) 290 | portalId = self.user.__portalId__() 291 | 292 | uri = self.user.portalUrl + '/sharing/rest/community/groups/'+groupId+'/reassign' 293 | req = urllib2.Request(uri,parameters) 294 | response = urllib2.urlopen(req) 295 | 296 | result = response.read() 297 | return json.loads(result) 298 | def reassignAllGroupOwnership(self, userFrom, userTo): 299 | ''' 300 | REQUIRES ADMIN ACCESS 301 | Reassigns ownership of all groups between a pair of accounts. 302 | ''' 303 | groups = 0 304 | groupsReassigned = 0 305 | 306 | # Get list of userFrom's groups 307 | print 'Requesting ' + userFrom + "'s group info from ArcGIS Online...", 308 | parameters = urllib.urlencode({'token': self.user.token, 'f': 'pjson'}) 309 | request = self.user.portalUrl + '/sharing/rest/community/users/' + userFrom + '?' + parameters 310 | response = urllib.urlopen(request).read() 311 | userFromContent = json.loads(response) 312 | print 'RECEIVED!' 313 | 314 | # Determine if userFrom is group owner and, if so, transfer ownership to userTo 315 | print 'Checking groups...', 316 | for group in userFromContent['groups']: 317 | print '.', 318 | groups += 1 319 | if group['owner'] == userFrom: 320 | parameters = urllib.urlencode({'targetUsername' : userTo, 'token': self.user.token, 'f': 'pjson'}) 321 | request = self.user.portalUrl + '/sharing/rest/community/groups/' + group['id'] + '/reassign?' 322 | response = urllib.urlopen(request, parameters).read() # requires POST 323 | if 'success' in response: 324 | groupsReassigned += 1 325 | 326 | # Report results 327 | print 328 | print ' CHECKED ' + str(groups) + ' groups ASSOCIATED with ' + userFrom + '.' 329 | print ' REASSIGNED ' + str(groupsReassigned) + ' groups OWNED by ' + userFrom + ' to ' + userTo + '.' 330 | 331 | return 332 | 333 | def addUser2ToAllUser1Groups(self, userFrom, userTo): 334 | ''' 335 | REQUIRES ADMIN ACCESS 336 | Adds userTo/User2 to all groups that userFrom/User1 is a member 337 | ''' 338 | 339 | groups = 0 340 | groupsOwned = 0 341 | groupsAdded = 0 342 | 343 | # Get list of userFrom's groups 344 | parameters = urllib.urlencode({'token': self.user.token, 'f': 'pjson'}) 345 | request = self.user.portalUrl + '/sharing/rest/community/users/' + userFrom + '?' + parameters 346 | response = urllib.urlopen(request).read() 347 | userFromContent = json.loads(response) 348 | 349 | # Add userTo to each group that userFrom's is a member, but not an owner 350 | for group in userFromContent['groups']: 351 | groups += 1 352 | if group['owner'] == userFrom: 353 | groupsOwned += 1 354 | else: 355 | parameters = urllib.urlencode({'users' : userTo, 'token': self.user.token, 'f': 'pjson'}) 356 | request = self.user.portalUrl + '/sharing/rest/community/groups/' + group['id'] + '/addUsers?' 357 | response = urllib.urlopen(request, parameters).read() # requires POST 358 | if '[]' in response: # This currently undocumented operation does not correctly return "success" 359 | groupsAdded += 1 360 | 361 | print ' CHECKED ' + str(groups) + ' groups associated with ' + userFrom + ':' 362 | print ' ' + userFrom + ' OWNS ' + str(groupsOwned) + ' groups (' + userTo + ' NOT added).' 363 | print ' ' + userTo + ' is already a MEMBER of ' + str(groups-groupsOwned-groupsAdded) + ' groups.' 364 | print ' ' + userTo + ' was ADDED to ' + str(groupsAdded) + ' groups.' 365 | 366 | return 367 | 368 | def migrateAccount(self, userFrom, userTo): 369 | ''' 370 | REQUIRES ADMIN ACCESS 371 | Reassigns ownership of all content items and groups from userFrom to userTo. 372 | Also adds userTo to all groups which userFrom is a member. 373 | ''' 374 | 375 | print 'Copying all items from ' + userFrom + ' to ' + userTo + '...' 376 | self.reassignAllUser1ItemsToUser2(userFrom, userTo) 377 | print 378 | 379 | print 'Reassigning groups owned by ' + userFrom + ' to ' + userTo + '...' 380 | self.reassignAllGroupOwnership(userFrom, userTo) 381 | print 382 | 383 | print 'Adding ' + userTo + ' as a member of ' + userFrom + "'s groups..." 384 | self.addUser2ToAllUser1Groups(userFrom, userTo) 385 | return 386 | 387 | def migrateAccounts(self, pathUserMappingCSV): 388 | ''' 389 | REQUIRES ADMIN ACCESS 390 | Reassigns ownership of all content items and groups between pairs of accounts specified in a CSV file. 391 | Also adds userTo to all groups which userFrom is a member. 392 | This function batches migrateAccount using a CSV to feed in the accounts to migrate from/to, 393 | the CSV should have two columns (no column headers/labels): col1=userFrom, col2=userTo) 394 | ''' 395 | 396 | with open(pathUserMappingCSV, 'rb') as userMappingCSV: 397 | userMapping = csv.reader(userMappingCSV) 398 | for user in userMapping: 399 | userFrom = user[0] 400 | userTo = user[1] 401 | 402 | print '==========' 403 | print 'Copying all items from ' + userFrom + ' to ' + userTo + '...' 404 | self.reassignAllUser1ItemsToUser2(self, userFrom, userTo) 405 | print 406 | 407 | print 'Reassigning groups owned by ' + userFrom + ' to ' + userTo + '...' 408 | self.reassignAllGroupOwnership(self, userFrom, userTo) 409 | print 410 | 411 | print 'Adding ' + userTo + ' as a member of ' + userFrom + "'s groups..." 412 | self.addUser2ToAllUser1Groups(self, userFrom, userTo) 413 | print '==========' 414 | return 415 | 416 | def updateServiceItemsThumbnail(self, folder=None): 417 | ''' 418 | Fetches catalog of items in portal. If there is no thumbnail, assigns the default. 419 | ''' 420 | if(folder!=None): 421 | catalog = self.AGOLUserCatalog(folder,False) 422 | else: 423 | catalog=self.AGOLCatalog(None) 424 | 425 | for r in catalog: 426 | if(r.thumbnail==None): 427 | parameters = urllib.urlencode({'thumbnailURL' : 'http://static.arcgis.com/images/desktopapp.png', 'token' : self.user.token, 'f' : 'json'}) 428 | 429 | requestToUpdate = self.user.portalUrl + '/sharing/rest/content/users/' + self.user.username + '/items/' +r.id + '/update' 430 | 431 | try: 432 | print ("updating " + r.title + " with thumbnail.") 433 | response = urllib.urlopen(requestToUpdate, parameters ).read() 434 | 435 | jresult = json.loads(response) 436 | except: 437 | e=1 438 | 439 | return None 440 | 441 | #calculateAttachmentCount(args.layerURL,args.flagField) 442 | def calculateAttachmentCount(self,layerURL, flagField): 443 | 444 | #get objectIDs 445 | #http://services.arcgis.com/XWaQZrOGjgrsZ6Cu/arcgis/rest/services/CambridgeAssetInspections/FeatureServer/0/query?where=0%3D0&returnIdsOnly=true&f=json 446 | parameters = urllib.urlencode({'token' : self.user.token}) 447 | query = "/query?where={}&returnIdsOnly=true&f=json".format("0=0") 448 | 449 | requestString = layerURL + query 450 | 451 | try: 452 | print ("retrieving OBJECTIDs...") 453 | responseOID = urllib.urlopen(requestString,parameters ).read() 454 | 455 | jresult = json.loads(responseOID) 456 | oidList=jresult["objectIds"] 457 | 458 | #iterate through features 459 | for oid in oidList: 460 | aQuery=layerURL + "/"+str(oid) + "/attachments?f=json" 461 | 462 | #determine attachment count 463 | responseAttachments = urllib.urlopen(aQuery,parameters ).read() 464 | print "reading attachments for feature " + str(oid) 465 | 466 | jAttachresult = json.loads(responseAttachments) 467 | aCount = len(jAttachresult["attachmentInfos"]) 468 | bHasAttachments=False 469 | if(aCount>0): 470 | bHasAttachments=True 471 | 472 | #write attachment status 473 | sPost = '[{"attributes":{"OBJECTID":' + str(oid) + ',"' + flagField + '":"' + str(bHasAttachments) +'"}}]' 474 | updateFeaturesRequest=layerURL + "/updateFeatures" 475 | 476 | parametersUpdate = urllib.urlencode({'f':'json','token' : self.user.token,'features':sPost}) 477 | 478 | print "writing " + str(aCount) + " attachments status for feature " + str(oid) 479 | 480 | responseUpdate = urllib.urlopen(updateFeaturesRequest,parametersUpdate ).read() 481 | a=responseUpdate 482 | 483 | except: 484 | e=1 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | return None 493 | 494 | def shareItems (self, items,groupid): 495 | ''' 496 | http://www.arcgis.com/sharing/rest/content/users/jsmith/shareItems 497 | everyone: false 498 | items=93b54c8930ae46d9a00a7820cb3ebbd1,bb8e3d443ab44878ab8315b00b0612ca 499 | groups=4774c1c2b79046f285b2e86e5a20319e,cc5f73ab367544d6b954d82cc9c6dab7 500 | 501 | http://www.arcgis.com/sharing/rest/content/items/af01df44bf36437fa8daed01407138ab/share 502 | 503 | ''' 504 | 505 | #requestToShare= self.user.portalUrl + '/sharing/rest/content/users/' + self.user.username + '/shareItems' 506 | #requestToShare= self.user.portalUrl + '/sharing/rest/content/items/shareItems' 507 | 508 | ''' 509 | parameters = urllib.urlencode({'token' : self.user.token, 510 | 'items' : sWebMapIDs, 511 | 'everyone' : False, 512 | 'groups' : groupid, 513 | 'f' : 'json'}) 514 | 515 | response = urllib.urlopen(requestToShare, parameters ).read() 516 | 517 | jresult = json.loads(response) 518 | ''' 519 | 520 | sWebMapIDs='' 521 | i=0 522 | for v in items: 523 | i = i +1 524 | sWebMapIDs = sWebMapIDs + v.id 525 | if (i < len(items)): 526 | sWebMapIDs = sWebMapIDs + "," 527 | 528 | requestToShare= self.user.portalUrl + '/sharing/rest/content/items/' + v.id + '/share' 529 | parameters = urllib.urlencode({'token' : self.user.token, 530 | 'everyone':False, 531 | 'account':False, 532 | 'groups' : groupid, 533 | 'f' : 'json'}) 534 | 535 | response = urllib.urlopen(requestToShare, parameters ).read() 536 | 537 | jresult = json.loads(response) 538 | 539 | l = len(jresult['notSharedWith']) 540 | bSuccess=True 541 | if(l==0): 542 | bSuccess=True 543 | else: 544 | bSuccess=False 545 | 546 | if(bSuccess): 547 | print str(i) + ') ' + v.title + " (" + jresult["itemId"] + ") was shared." 548 | else: 549 | print str(i) + ') ' + v.title + " (" + jresult["itemId"] + ") could not be shared, or was already shared with this group." 550 | 551 | return 552 | 553 | def deleteItems (self, items): 554 | 555 | sWebMapIDs='' 556 | i=0 557 | for v in items: 558 | i = i +1 559 | sWebMapIDs = sWebMapIDs + v.id 560 | if (i < len(items)): 561 | sWebMapIDs = sWebMapIDs + "," 562 | 563 | if not hasattr(v, 'title'): 564 | v.title="Item " 565 | 566 | #have to get ownerFolder 567 | parameters = urllib.urlencode({'token' : self.user.token, 'f': 'json'}) 568 | requestForInfo = self.user.portalUrl + '/sharing/rest/content/items/' + v.id 569 | 570 | response = urllib.urlopen(requestForInfo, parameters ).read() 571 | 572 | jResult = json.loads(response) 573 | 574 | if('error' in jResult): 575 | print str(i) + ') ' + v.title + " (" + v.id + ") was not found and will be skipped." 576 | else: 577 | 578 | if jResult['ownerFolder']: 579 | folderID = str(jResult['ownerFolder']) 580 | else: 581 | folderID = '' 582 | 583 | requestToDelete = self.user.portalUrl + '/sharing/rest/content/users/' + v.owner + '/' + folderID + '/items/' + v.id + '/delete' 584 | parameters = urllib.urlencode({'token' : self.user.token, 'f': 'json','items':v.id}) 585 | 586 | response = urllib.urlopen(requestToDelete, parameters ).read() 587 | 588 | jResult = json.loads(response) 589 | 590 | bSuccess=False 591 | try: 592 | if('success' in jResult): 593 | bSuccess=True 594 | except: 595 | bSuccess=False 596 | 597 | if(bSuccess): 598 | print str(i) + ') ' + v.title + " (" + v.id + ") was deleted." 599 | else: 600 | print str(i) + ') ' + v.title + " (" + v.id + ") could not be deleted, or was already unaccessible." 601 | 602 | 603 | def registerItems (self, mapservices, folder=''): 604 | ''' 605 | Given a set of AGOL items, register them to the portal, 606 | optionally to a specific folder. 607 | ''' 608 | self.servicesToRegister=mapservices 609 | 610 | if folder==None: 611 | folder='' 612 | 613 | icount=0 614 | i=0 615 | for ms in self.servicesToRegister.service_list: 616 | i = i +1 617 | 618 | sURL=ms.url 619 | sTitle=ms.title 620 | if ms.thumbnail==None: 621 | sThumbnail ='http://static.arcgis.com/images/desktopapp.png' 622 | elif ms.id !=None: 623 | sThumbnail ="http://www.arcgis.com/sharing/content/items/" + ms.id + "/info/" + ms.thumbnail 624 | else: 625 | sThumbnail='http://static.arcgis.com/images/desktopapp.png' 626 | 627 | #todo, handle map service exports 628 | 629 | sTags = 'mapping' if ms.tags==None else ms.tags 630 | sType= 'Map Service' if ms.type==None else ms.type 631 | sDescription = '' if ms.description==None else ms.description 632 | sSnippet = '' if ms.snippet ==None else ms.snippet 633 | sExtent = '' if ms.extent==None else ms.extent 634 | sSpatialReference='' if ms.spatialReference==None else ms.spatialReference 635 | sAccessInfo='' if ms.accessInformation==None else ms.accessInformation 636 | sLicenseInfo='' if ms.licenseInfo==None else ms.licenseInfo 637 | sCulture='' if ms.culture == None else ms.culture 638 | 639 | parameters = urllib.urlencode({'URL' : sURL, 640 | 'title' : sTitle, 641 | 'thumbnailURL' : sThumbnail, 642 | 'tags' : sTags, 643 | 'description' : sDescription, 644 | 'snippet': sSnippet, 645 | 'extent':sExtent, 646 | 'spatialReference':sSpatialReference, 647 | 'accessInformation': sAccessInfo, 648 | 'licenseInfo': sLicenseInfo, 649 | 'culture': sCulture, 650 | 'type' : sType, 651 | 'token' : self.user.token, 652 | 'f' : 'json'}) 653 | #todo- use export map on map service items for thumbnail 654 | 655 | requestToAdd = self.user.portalUrl + '/sharing/rest/content/users/' + self.user.username + folder + '/addItem' 656 | 657 | try: 658 | if(sType.find('Service')>=0 or sType.find('Web Mapping Application')>=0): 659 | response = urllib.urlopen(requestToAdd, parameters ).read() 660 | 661 | jresult = json.loads(response) 662 | print str(i) + ") " + ms.title + ": success= " + str(jresult["success"]) + "," + ms.url + ", " + "(" + jresult["id"] + ")" 663 | 664 | if jresult["success"]: 665 | icount=icount+1 666 | 667 | except: 668 | print str(i) + ") " + ms.title + ':error!' 669 | 670 | print str(icount) + " item(s) added." 671 | 672 | def getFolderID(self, folderName): 673 | ''' 674 | Return the ID of the folder with the given name. 675 | ''' 676 | folders = self._getUserFolders() 677 | 678 | for f in folders: 679 | if str(f['title']) == folderName: 680 | return str(f['id']) 681 | 682 | return '' 683 | 684 | def _getUserFolders(self): 685 | ''' 686 | Return all folder objects. 687 | ''' 688 | requestToAdd = self.user.portalUrl + '/sharing/rest/content/users/' + self.user.username + '?f=json&token=' + self.user.token; 689 | response = urllib.urlopen(requestToAdd).read() 690 | 691 | jresult = json.loads(response) 692 | return jresult["folders"] 693 | def deleteGroup(self,groupid): 694 | ''' 695 | Deletes group 696 | ''' 697 | portalId = self.user.__portalId__() 698 | uri = self.user.portalUrl + '/sharing/rest/community/groups/'+groupid+'/delete' 699 | parameters ={'token' : self.user.token, 700 | 'f' : 'json'} 701 | 702 | parameters = urllib.urlencode(parameters) 703 | req = urllib2.Request(uri,parameters) 704 | response = urllib2.urlopen(req) 705 | result = response.read() 706 | return json.loads(result) 707 | def clearGroup(self, groupid): 708 | ''' 709 | Unshare all content from the specified group. 710 | CAUTION 711 | ''' 712 | groupcatalog = self.AGOLGroupCatalog(groupid) 713 | 714 | sItems='' 715 | for f in groupcatalog: 716 | requestToDelete = self.user.portalUrl + '/sharing/rest/content/items/' + f.id + "/unshare?groups=" + groupid 717 | 718 | parameters = urllib.urlencode({ 719 | 'token' : self.user.token, 720 | 'f' : 'json'}) 721 | print "Unsharing " + f.title 722 | 723 | response = urllib.urlopen(requestToDelete,parameters).read() 724 | 725 | jresult = json.loads(response) 726 | 727 | print "Complete." 728 | return None 729 | 730 | def clearFolder(self, folderid): 731 | ''' 732 | Delete all content from the specified folder. 733 | CAUTION 734 | ''' 735 | foldercatalog = self.AGOLUserCatalog(folderid) 736 | sItems='' 737 | for f in foldercatalog: 738 | sItems+= f.id + "," 739 | 740 | if len(sItems)>0: sItems=sItems[:-1] 741 | 742 | requestToDelete = self.user.portalUrl + '/sharing/rest/content/users/' + self.user.username + "/deleteItems" 743 | 744 | parameters = urllib.urlencode({'items':sItems, 745 | 'token' : self.user.token, 746 | 'f' : 'json'}) 747 | 748 | print "Deleting " + str(len(foldercatalog)) + " items..." 749 | response = urllib.urlopen(requestToDelete,parameters).read() 750 | 751 | jresult = json.loads(response) 752 | print "Complete." 753 | return None 754 | 755 | def AGOLGroupCatalog(self, groupid): 756 | ''' 757 | Return the catalog of items in desiginated group. 758 | ''' 759 | sCatalogURL=self.user.portalUrl + "/sharing/rest/search?q=%20group%3A" + groupid + "%20-type:%22Code%20Attachment%22%20-type:%22Featured%20Items%22%20-type:%22Symbol%20Set%22%20-type:%22Color%20Set%22%20-type:%22Windows%20Viewer%20Add%20In%22%20-type:%22Windows%20Viewer%20Configuration%22%20%20-type:%22Code%20Attachment%22%20-type:%22Featured%20Items%22%20-type:%22Symbol%20Set%22%20-type:%22Color%20Set%22%20-type:%22Windows%20Viewer%20Add%20In%22%20-type:%22Windows%20Viewer%20Configuration%22%20&num=100&sortField=title&sortOrder=asc" 760 | 761 | return self.AGOLCatalog(None,None,sCatalogURL) 762 | 763 | def findWebmapService(self, webmapId, oldUrl, folderID=None): 764 | try: 765 | params = urllib.urlencode({'token' : self.user.token, 766 | 'f' : 'json'}) 767 | #print 'Getting Info for: ' + webmapId 768 | #Get the item data 769 | reqUrl = self.user.portalUrl + '/sharing/content/items/' + webmapId + '/data?' + params 770 | itemDataReq = urllib.urlopen(reqUrl).read() 771 | itemString = str(itemDataReq) 772 | 773 | #See if it needs to be updated 774 | if itemString.find(oldUrl) > -1: 775 | return True 776 | else: 777 | #print 'Didn\'t find any services for ' + oldUrl 778 | return False 779 | 780 | except ValueError as e: 781 | print 'Error - no web maps specified' 782 | except AGOPostError as e: 783 | print 'Error updating web map ' + e.webmap + ": " + e.msg 784 | 785 | def findItemUrl(self, item, oldUrl, folderID=None): 786 | ''' 787 | Use this to find the URL for items such as Map Services. 788 | This can also find part of a URL. The text of oldUrl is replaced with the text of newUrl. For example you could change just the host name of your URLs. 789 | ''' 790 | try: 791 | 792 | if item.url == None: 793 | #print item.title + ' doesn\'t have a url property' 794 | return False 795 | 796 | # Double check that the existing URL matches the provided URL 797 | if item.url.find(oldUrl) > -1: 798 | return True 799 | else: 800 | #print 'Didn\'t find the specified old URL: ' + oldUrl 801 | return False 802 | except ValueError as e: 803 | print e 804 | return False 805 | 806 | def __decode_dict__(self, dct): 807 | newdict = {} 808 | for k, v in dct.iteritems(): 809 | k = self.__safeValue__(k) 810 | v = self.__safeValue__(v) 811 | newdict[k] = v 812 | return newdict 813 | 814 | def __safeValue__(self, inVal): 815 | outVal = inVal 816 | if isinstance(inVal, unicode): 817 | outVal = inVal.encode('utf-8') 818 | elif isinstance(inVal, list): 819 | outVal = self.__decode_list__(inVal) 820 | return outVal 821 | 822 | def __decode_list__(self, lst): 823 | newList = [] 824 | for i in lst: 825 | i = self.__safeValue__(i) 826 | newList.append(i) 827 | return newList 828 | 829 | def json_to_obj(self,s): 830 | def h2o(x): 831 | if isinstance(x, dict): 832 | return type('jo', (), {k: h2o(v) for k, v in x.iteritems()}) 833 | else: 834 | return x 835 | return h2o(json.loads(s)) 836 | 837 | 838 | 839 | def addBookmarksToWebMap(self,pBookmarks,webmapId): 840 | ''' 841 | update the designated webmap with the list of bookmark objects 842 | ''' 843 | 844 | #read webmap json 845 | try: 846 | params = urllib.urlencode({'token' : self.user.token, 847 | 'f' : 'json'}) 848 | 849 | print 'Getting Info for: ' + webmapId 850 | #Get the item data 851 | reqUrl = self.user.portalUrl + '/sharing/content/items/' + webmapId + '/data?' + params 852 | itemDataReq = urllib.urlopen(reqUrl).read() 853 | itemData = json.loads(itemDataReq, object_hook=self.__decode_dict__) 854 | 855 | #add bookmarks into object list 856 | itemData['bookmarks']=pBookmarks 857 | 858 | #convert the updated json object back to string 859 | sItemDataText=json.dumps(itemData, separators=(',',':')) 860 | 861 | #get original item definition for update 862 | itemInfoReq = urllib.urlopen(self.user.portalUrl + '/sharing/content/items/' + webmapId + '?' + params) 863 | itemInfo = json.loads(itemInfoReq.read(), object_hook=self.__decode_dict__) 864 | 865 | #write original back as test 866 | outParamObj = { 867 | 'extent' : ', '.join([str(itemInfo['extent'][0][0]), str(itemInfo['extent'][0][1]), str(itemInfo['extent'][1][0]), str(itemInfo['extent'][1][1])]), 868 | 'type' : itemInfo['type'], 869 | 'item' : itemInfo['item'], 870 | 'title' : itemInfo['title'], 871 | 'overwrite' : 'true', 872 | 'tags' : ','.join(itemInfo['tags']), 873 | 'text' : sItemDataText 874 | } 875 | # Get the item folder. 876 | if itemInfo['ownerFolder']: 877 | folderID = itemInfo['ownerFolder'] 878 | else: 879 | folderID = '' 880 | 881 | nBookmarksCount=str(len(pBookmarks)) 882 | print "Updating Webmap with " + nBookmarksCount + " bookmarks..." 883 | 884 | #Post back the changes overwriting the old map 885 | modRequest = urllib.urlopen(self.user.portalUrl + '/sharing/content/users/' + self.user.username + '/' + folderID + '/addItem?' + params , urllib.urlencode(outParamObj)) 886 | 887 | #Evaluate the results to make sure it happened 888 | modResponse = json.loads(modRequest.read()) 889 | if modResponse["success"]!=True: 890 | print "The update was NOT successful." 891 | else: 892 | print "The update WAS successful." 893 | 894 | return None 895 | 896 | 897 | except ValueError as e: 898 | print 'Error:'+ e.message 899 | 900 | 901 | return None 902 | 903 | def readBookmarksFromFeatureClass(self,path,labelfield): 904 | 905 | import arcpy 906 | 907 | bmarks=[] 908 | fieldnames = [labelfield,"SHAPE@"] 909 | wkid = "4326" 910 | myCursor = arcpy.da.SearchCursor(path,fieldnames,"",wkid) 911 | for row in myCursor: 912 | bm = bookmark() 913 | extent = row[1].extent 914 | bm.extent.xmin = extent.lowerLeft.X 915 | bm.extent.ymin = extent.lowerLeft.Y 916 | bm.extent.xmax = extent.upperRight.X 917 | bm.extent.ymax = extent.upperRight.Y 918 | bm.extent.SpatialReference.wkid = wkid 919 | bm.name=row[0].title() 920 | 921 | bmarks.append(bm.to_JSON2()) 922 | 923 | return bmarks 924 | 925 | def createBookmarksFromLayer(self, url,labelfield): 926 | 927 | import arcpy 928 | 929 | bmarks=[] 930 | try: 931 | 932 | where = '1=1' 933 | fields ='*' 934 | 935 | token = '' 936 | #todo when to use token? 937 | if(url.find("arcgis.com")>0): 938 | token = self.user.token 939 | 940 | #The above variables construct the query 941 | query = "/query?where={}&outFields={}&returnGeometry=true&f=json&token={}".format(where, fields, token) 942 | 943 | fsURL = url + query 944 | 945 | fs = arcpy.FeatureSet() 946 | fs.load(fsURL) 947 | fieldnames = [labelfield,"SHAPE@"] 948 | 949 | wkid = "4326" #use geographic for bookmarks, easier to confirm 950 | myCursor = arcpy.da.SearchCursor(fs,fieldnames,"",wkid) 951 | 952 | for row in myCursor: 953 | bm = bookmark() 954 | extent = row[1].extent 955 | 956 | #handle points 957 | if extent.lowerLeft.X == extent.upperRight.X: 958 | myX=extent.lowerLeft.X 959 | myY=extent.lowerLeft.Y 960 | nTol=.05 961 | myLL = arcpy.Point(myX-nTol,myY-nTol) 962 | myUR = arcpy.Point(myX+nTol,myY+nTol) 963 | 964 | bm.extent.xmin = myLL.X 965 | bm.extent.ymin = myLL.Y 966 | bm.extent.xmax = myUR.X 967 | bm.extent.ymax = myUR.Y 968 | 969 | else: 970 | bm.extent.xmin = extent.lowerLeft.X 971 | bm.extent.ymin = extent.lowerLeft.Y 972 | bm.extent.xmax = extent.upperRight.X 973 | bm.extent.ymax = extent.upperRight.Y 974 | 975 | bm.extent.SpatialReference.wkid = wkid 976 | bm.name=row[0].title() 977 | 978 | bmarks.append(bm.to_JSON2()) 979 | 980 | 981 | except ValueError as e: 982 | print 'Error: ' +e.message 983 | 984 | return bmarks 985 | 986 | 987 | def readBookmarksFromFile(self,inputfile): 988 | with open(inputfile) as input: 989 | data = json.load(input) 990 | return data["bookmarks"] 991 | 992 | def getLayerURL(self, layerId): 993 | sURL=None 994 | 995 | params = urllib.urlencode({'token' : self.user.token, 996 | 'f' : 'json'}) 997 | #print 'Getting Info for: ' + webmapId 998 | #Get the item data 999 | 1000 | reqUrl = self.user.portalUrl + '/sharing/rest/content/items/' + layerId + "?" + params 1001 | itemDataReq = urllib.urlopen(reqUrl).read() 1002 | itemString = str(itemDataReq) 1003 | 1004 | pp=json.loads(itemString) 1005 | 1006 | sURL = pp["url"] 1007 | l=sURL[len(sURL)-1] 1008 | 1009 | if (l.isnumeric()== False): 1010 | sURL = sURL + "/0" 1011 | 1012 | return sURL 1013 | 1014 | def readBookmarksFromFeatureCollection(self,fcId,labelfield): 1015 | pBookmarks=[] 1016 | 1017 | # def readBookmarksFromFeatureClass(self,path,labelfield): 1018 | #bmarks=[] 1019 | #fieldnames = [labelfield,"SHAPE@"] 1020 | #wkid = "4326" 1021 | #myCursor = arcpy.da.SearchCursor(path,fieldnames,"",wkid) 1022 | #for row in myCursor: 1023 | # bm = bookmark() 1024 | # extent = row[1].extent 1025 | # bm.extent.xmin = extent.lowerLeft.X 1026 | # bm.extent.ymin = extent.lowerLeft.Y 1027 | # bm.extent.xmax = extent.upperRight.X 1028 | # bm.extent.ymax = extent.upperRight.Y 1029 | # bm.extent.SpatialReference.wkid = wkid 1030 | # bm.name=row[0].title() 1031 | 1032 | # bmarks.append(bm.to_JSON2()) 1033 | 1034 | 1035 | params = urllib.urlencode({'token' : self.user.token, 1036 | 'f' : 'json'}) 1037 | #print 'Getting Info for: ' + webmapId 1038 | #Get the item data 1039 | reqUrl = self.user.portalUrl + '/sharing/content/items/' + fcId + '/data?' + params 1040 | itemDataReq = urllib.urlopen(reqUrl).read() 1041 | itemString = str(itemDataReq) 1042 | 1043 | pp=json.loads(itemString) 1044 | 1045 | for f in pp["layers"][0]["featureSet"]["features"]: 1046 | 1047 | x=f["geometry"]["x"]; 1048 | y=f["geometry"]["y"]; 1049 | bmark=bookmark() 1050 | bmark.extent.xmin=x-10000 1051 | bmark.extent.xmax=x+10000 1052 | bmark.extent.ymin=y-10000 1053 | bmark.extent.ymax=y+10000 1054 | bmark.name=f["attributes"][labelfield] 1055 | 1056 | pBookmarks.append(bmark.to_JSON3("102100")) 1057 | 1058 | return pBookmarks 1059 | 1060 | def findItemsWithURLs(self, oldUrl,folder): 1061 | 1062 | if(folder!=None): 1063 | catalog = self.AGOLUserCatalog(folder,False) 1064 | else: 1065 | catalog=self.AGOLCatalog(None) 1066 | 1067 | allResults = [] 1068 | 1069 | ''' 1070 | Find the URL or URL part for all URLs in a list of AGOL items. 1071 | This works for all item types that store a URL. (e.g. web maps, map services, applications, etc.) 1072 | oldUrl -- All or part of a URL to search for. 1073 | newUrl -- The text that will be used to replace the current "oldUrl" text. 1074 | ''' 1075 | countWebMaps=0 1076 | countServicesOrApps=0 1077 | i=0 1078 | iLength=len(catalog) 1079 | for item in catalog: 1080 | i=i+1 1081 | print str(i) + '/' + str(iLength) 1082 | if item.type == 'Web Map': 1083 | v=self.findWebmapService(item.id, oldUrl) 1084 | if v==True: 1085 | dosomething=True 1086 | countWebMaps=countWebMaps+1 1087 | allResults.append(item) 1088 | else: 1089 | if not item.url == None: 1090 | v=self.findItemUrl(item, oldUrl) 1091 | if v==True: 1092 | dosomething=True 1093 | countServicesOrApps=countServicesOrApps+1 1094 | allResults.append(item) 1095 | 1096 | count=countServicesOrApps + countWebMaps 1097 | return allResults 1098 | 1099 | 1100 | def AGOLUserCatalog(self, folder, includeSize=False): 1101 | ''' 1102 | Return the catalog of CURRENT USER's items from portal, optionally from only a folder. 1103 | ''' 1104 | sCatalogURL = self.user.portalUrl + "/sharing/rest/content/users/" + self.user.username + folder 1105 | return self.AGOLCatalog(None,None,sCatalogURL) 1106 | 1107 | def AGOLCatalog(self, query=None, includeSize=False, sCatalogURL=None): 1108 | ''' 1109 | Return all items from all users in a portal, optionally matching a 1110 | specified query. 1111 | optionally make the additional requests for SIZE. 1112 | sCatalogURL can be specified to use a specific folder 1113 | ''' 1114 | 1115 | resultCount = 0 1116 | searchURL = "" 1117 | viewURL = "" 1118 | orgID = "" 1119 | self.sFullSearch = "" 1120 | self.bIncludeSize=includeSize 1121 | 1122 | self.orgID = self._getOrgID() 1123 | 1124 | self.catalogURL=sCatalogURL #for cataloging folders 1125 | 1126 | if self.user.portalUrl != None: 1127 | self.searchURL = self.user.portalUrl + "/sharing/rest" 1128 | self.viewURL = self.user.portalUrl + "/home/item.html?id=" 1129 | 1130 | self.query = query 1131 | 1132 | pList=[] 1133 | allResults = [] 1134 | 1135 | sQuery=self._getCatalogQuery(1,100)#get first batch 1136 | 1137 | print("fetching records 1-100...") 1138 | 1139 | response = urllib.urlopen(sQuery).read() 1140 | jresult=json.loads(response) 1141 | 1142 | nextRecord = jresult['nextStart'] 1143 | totalRecords = jresult['total'] 1144 | num = jresult['num'] 1145 | start =jresult['start'] 1146 | 1147 | #if this is a folder catalog, use items, not results 1148 | sItemsProperty = 'results' 1149 | if self.catalogURL!=None and str(self.catalogURL).find("/sharing/rest/content/users/")>0: sItemsProperty='items' 1150 | 1151 | pList = AGOLItems( jresult[sItemsProperty]) 1152 | 1153 | for r in pList.AGOLItems_list: 1154 | r.itemURL = self.viewURL + r.id 1155 | r.created = time.strftime("%Y-%m-%d",time.gmtime(r.created/1000)) 1156 | r.modified = time.strftime("%Y-%m-%d",time.gmtime(r.modified/1000)) 1157 | if r.size== -1: 1158 | r.size=0 1159 | r.size = self._getSize(r) 1160 | r.myRowID = len(allResults) + 1; 1161 | allResults.append(r) 1162 | 1163 | if (nextRecord>0): 1164 | while(nextRecord>0): 1165 | sQuery = self._getCatalogQuery(nextRecord, 100) 1166 | print("fetching records " + str(nextRecord) + "-" + str(nextRecord+100) + "...") 1167 | 1168 | response = urllib.urlopen(sQuery).read() 1169 | jresult=json.loads(response) 1170 | 1171 | nextRecord = jresult['nextStart'] 1172 | totalRecords = jresult['total'] 1173 | num = jresult['num'] 1174 | start =jresult['start'] 1175 | 1176 | pList = AGOLItems( jresult['results']) 1177 | for r in pList.AGOLItems_list: 1178 | r.itemURL = self.viewURL + r.id 1179 | r.created = time.strftime("%Y-%m-%d",time.gmtime(r.created/1000)) 1180 | r.modified = time.strftime("%Y-%m-%d",time.gmtime(r.modified/1000)) 1181 | if r.size== -1: 1182 | r.size=0 1183 | r.size = self._getSize(r) 1184 | r.myRowID = len(allResults) + 1; 1185 | 1186 | allResults.append(r) 1187 | 1188 | return allResults 1189 | 1190 | def _getSize(self, r): 1191 | ''' 1192 | Issue query for item size. 1193 | ''' 1194 | if(self.bIncludeSize != True): 1195 | return 0 1196 | 1197 | print ("fetching size for " + r.title + " (" + r.type + ")") 1198 | 1199 | result=0 1200 | sURL = self.searchURL + "/content/items/" + str(r.id) + "?f=json&token=" + self.user.token; 1201 | 1202 | response = urllib.urlopen(sURL).read() 1203 | result = json.loads(response)['size'] 1204 | if(result>0): 1205 | result = result/1024 1206 | else: 1207 | result=0 1208 | 1209 | return result 1210 | 1211 | def _getOrgID(self): 1212 | ''' 1213 | Return the organization's ID. 1214 | ''' 1215 | sURL = self.user.portalUrl + "/sharing/rest/portals/self?f=json&token=" + self.user.token 1216 | 1217 | response = urllib.urlopen(sURL).read() 1218 | 1219 | return str(json.loads(response)['id']) 1220 | 1221 | def _getCatalogQuery(self, start, num): 1222 | ''' 1223 | Format a content query from specified start and number of records. 1224 | ''' 1225 | sQuery=None 1226 | if self.query != None: 1227 | sQuery = self.query 1228 | else: 1229 | sQuery = self.sFullSearch 1230 | 1231 | if(self.catalogURL==None): 1232 | sCatalogQuery = self.searchURL + "/search?q=" + sQuery 1233 | if self.orgID != None: 1234 | sCatalogQuery += " orgid:" + self.orgID 1235 | else: 1236 | #check to ensure ? vs & 1237 | if(str(self.catalogURL).find('?')<0): 1238 | char="?" 1239 | else: 1240 | char="&" 1241 | 1242 | sCatalogQuery = self.catalogURL + char + "ts=1" 1243 | 1244 | sCatalogQuery += "&f=json&num="+ str(num) + "&start=" + str(start) 1245 | sCatalogQuery += "&token=" + self.user.token 1246 | 1247 | return sCatalogQuery 1248 | 1249 | def updateUserRoles(self, users): 1250 | self.usersToUpdate=users 1251 | 1252 | requestToUpdate= self.user.portalUrl + '/sharing/rest/portals/self/updateuserrole' 1253 | 1254 | for u in self.usersToUpdate.user_list: 1255 | parameters = urllib.urlencode({'user':u.Username, 1256 | 'role':u.Role, 1257 | 'token' : self.user.token, 1258 | 'f' : 'json'}) 1259 | 1260 | print "Updating Role for " + u.Username + " to " + u.Role + "..." 1261 | response = urllib.urlopen(requestToUpdate,parameters).read() 1262 | jresult = json.loads(response) 1263 | success= str(jresult["success"]) 1264 | print "Success: " + success 1265 | 1266 | print "Complete." 1267 | return None 1268 | 1269 | 1270 | #collection of AGOLItem 1271 | class AGOLItems: 1272 | def __init__ (self, item_list): 1273 | self.AGOLItems_list=[] 1274 | for item in item_list: 1275 | self.AGOLItems_list.append(AGOLItem(item)) 1276 | 1277 | #AGOL item 1278 | class AGOLItem: 1279 | def __init__(self, item_attributes): 1280 | for k, v in item_attributes.items(): 1281 | setattr(self, k, v) 1282 | 1283 | #collection of Map Services 1284 | class MapServices: 1285 | def __init__ (self, import_list): 1286 | self.service_list=[] 1287 | for service in import_list: 1288 | self.service_list.append(MapService(service)) 1289 | 1290 | #Map Service 1291 | class MapService: 1292 | def __init__(self, service_attributes): 1293 | for k, v in service_attributes.items(): 1294 | setattr(self, k, v) 1295 | 1296 | #Collection of Usernames and roles 1297 | class UsersAttributes: 1298 | def __init__ (self, import_list): 1299 | self.user_list=[] 1300 | for user in import_list: 1301 | self.user_list.append(UserAttributes(user)) 1302 | 1303 | class UserAttributes: 1304 | def __init__(self, user_attributes): 1305 | for k, v in user_attributes.items(): 1306 | setattr(self, k, v) 1307 | 1308 | 1309 | 1310 | class bookmark(object): 1311 | 1312 | def __init__(self): 1313 | self.name = None 1314 | self.extent = self.bb() 1315 | 1316 | def to_JSON(self): 1317 | return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=2,separators=(',',':')) 1318 | 1319 | def to_JSON2(self): 1320 | # extent = row[1].extent 1321 | #bm.extent.xmin = extent.lowerLeft.X 1322 | #bm.extent.ymin = extent.lowerLeft.Y 1323 | #bm.extent.xmax = extent.upperRight.X 1324 | #bm.extent.ymax = extent.upperRight.Y 1325 | #bm.extent.SpatialReference.wkid = wkid 1326 | #bm.name=row[0].title() 1327 | #s='{"extent":{"SpatialReference":{"wkid":"4326"},"xmax":-77.58890542503474,"xmin":-77.66083839947551,"ymax":42.58041413631198,"ymin":42.549020481314585},"name":"Wayland 200"}'#.format(self.extent.xmin,self.extent.xmax,self.extent.ymin,self.extent.ymax,self.name) 1328 | s='{"extent":{"SpatialReference":{"wkid":"4326"},"xmax":' + str(self.extent.xmax) +',"xmin":' + str(self.extent.xmin) + ',"ymax":' + str(self.extent.ymax) +',"ymin":' + str(self.extent.ymin) + '},"name":"' + self.name + '"}' 1329 | #.format(self.extent.xmin,self.extent.xmax,self.extent.ymin,self.extent.ymax,self.name) 1330 | 1331 | return json.loads(s) 1332 | 1333 | def to_JSON3(self,wkid): 1334 | # extent = row[1].extent 1335 | #bm.extent.xmin = extent.lowerLeft.X 1336 | #bm.extent.ymin = extent.lowerLeft.Y 1337 | #bm.extent.xmax = extent.upperRight.X 1338 | #bm.extent.ymax = extent.upperRight.Y 1339 | #bm.extent.SpatialReference.wkid = wkid 1340 | #bm.name=row[0].title() 1341 | #s='{"extent":{"SpatialReference":{"wkid":"4326"},"xmax":-77.58890542503474,"xmin":-77.66083839947551,"ymax":42.58041413631198,"ymin":42.549020481314585},"name":"Wayland 200"}'#.format(self.extent.xmin,self.extent.xmax,self.extent.ymin,self.extent.ymax,self.name) 1342 | #SBTEST s='{"extent":{"SpatialReference":{"wkid":' + wkid + '},"xmax":' + str(self.extent.xmax) +',"xmin":' + str(self.extent.xmin) + ',"ymax":' + str(self.extent.ymax) +',"ymin":' + str(self.extent.ymin) + '},"name":"' + self.name + '"}' 1343 | s='{"extent":{"SpatialReference":{"wkid":' + wkid + '},"xmin":' + str(self.extent.xmin) +',"ymin":' + str(self.extent.ymin) + ',"xmax":' + str(self.extent.xmax) +',"ymax":' + str(self.extent.ymax) + '},"name":"' + self.name + '"}' 1344 | 1345 | #.format(self.extent.xmin,self.extent.xmax,self.extent.ymin,self.extent.ymax,self.name) 1346 | 1347 | return json.loads(s) 1348 | 1349 | class bb(object): 1350 | def __init__(self): 1351 | self.SpatialReference = self.sr() 1352 | self.xmax = 0.0 1353 | self.ymax = 0.0 1354 | self.xmin = 0.0 1355 | self.ymin = 0.0 1356 | 1357 | class sr(object): 1358 | def __init__(self): 1359 | self.wkid = "" 1360 | -------------------------------------------------------------------------------- /agoTools/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import urllib 4 | import json 5 | 6 | def searchPortal(portal, query=None, totalResults=None, sortField='numviews', sortOrder='desc', token=None): 7 | ''' 8 | Search the portal using the specified query and search parameters. 9 | Optionally provide a token to return results visible to that user. 10 | More examples on advanced search operators here: 11 | https://github.com/esri/ago-tools/blob/master/search-cheat-sheet.md 12 | ''' 13 | # Default results are returned by highest number of views in descending order. 14 | allResults = [] 15 | if not totalResults or totalResults > 100: 16 | numResults = 100 17 | else: 18 | numResults = totalResults 19 | results = __search__(portal, query, numResults, sortField, sortOrder, 0, token) 20 | 21 | if not 'error' in results.keys(): 22 | if not totalResults: 23 | totalResults = results['total'] # Return all of the results. 24 | allResults.extend(results['results']) 25 | while results['nextStart'] > 0 and results['nextStart'] < totalResults: 26 | # Do some math to ensure it only returns the total results requested. 27 | numResults = min(totalResults - results['nextStart'] + 1, 100) 28 | results = __search__(portal=portal, query=query, numResults=numResults, sortField=sortField, 29 | sortOrder=sortOrder, token=token, start=results['nextStart']) 30 | allResults.extend(results['results']) 31 | return allResults 32 | else: 33 | print results['error']['message'] 34 | return results 35 | 36 | def __search__(portal, query=None, numResults=100, sortField='numviews', sortOrder='desc', start=0, token=None): 37 | '''Retrieve a single page of search results.''' 38 | params = { 39 | 'q': query, 40 | 'num': numResults, 41 | 'sortField': sortField, 42 | 'sortOrder': sortOrder, 43 | 'f': 'json', 44 | 'start': start 45 | } 46 | if token: 47 | params['token'] = token # Adding a token provides an authenticated search. 48 | request = portal + '/sharing/rest/search?' + urllib.urlencode(params) 49 | results = json.loads(urllib.urlopen(request).read()) 50 | return results 51 | 52 | class Utilities: 53 | '''A class of tools for working with content in an AGO account''' 54 | def __init__(self, username, portal=None, password=None): 55 | from . import User 56 | self.user = User(username, portal, password) 57 | 58 | def updateWebmapService(self, webmapId, oldUrl, newUrl, folderID=None): 59 | try: 60 | params = urllib.urlencode({'token' : self.user.token, 61 | 'f' : 'json'}) 62 | print 'Getting Info for: ' + webmapId 63 | #Get the item data 64 | reqUrl = self.user.portalUrl + '/sharing/content/items/' + webmapId + '/data?' + params 65 | itemDataReq = urllib.urlopen(reqUrl).read() 66 | itemString = str(itemDataReq) 67 | 68 | #See if it needs to be updated 69 | if itemString.find(oldUrl) > -1: 70 | #Update the map 71 | newString = itemString.replace(oldUrl, newUrl) 72 | #Get the item's info for the addItem parameters 73 | itemInfoReq = urllib.urlopen(self.user.portalUrl + '/sharing/content/items/' + webmapId + '?' + params) 74 | itemInfo = json.loads(itemInfoReq.read(), object_hook=self.__decode_dict__) 75 | print 'Updating ' + itemInfo['title'] 76 | 77 | #Set up the addItem parameters 78 | outParamObj = { 79 | 'extent' : ', '.join([str(itemInfo['extent'][0][0]), str(itemInfo['extent'][0][1]), str(itemInfo['extent'][1][0]), str(itemInfo['extent'][1][1])]), 80 | 'type' : itemInfo['type'], 81 | 'item' : itemInfo['item'], 82 | 'title' : itemInfo['title'], 83 | 'overwrite' : 'true', 84 | 'tags' : ','.join(itemInfo['tags']), 85 | 'text' : newString 86 | } 87 | # Get the item folder. 88 | if itemInfo['ownerFolder']: 89 | folderID = itemInfo['ownerFolder'] 90 | else: 91 | folderID = '' 92 | #Post back the changes overwriting the old map 93 | modRequest = urllib.urlopen(self.user.portalUrl + '/sharing/content/users/' + self.user.username + '/' + folderID + '/addItem?' + params , urllib.urlencode(outParamObj)) 94 | #Evaluate the results to make sure it happened 95 | modResponse = json.loads(modRequest.read()) 96 | if modResponse.has_key('error'): 97 | raise AGOPostError(webmapId, modResponse['error']['message']) 98 | else: 99 | print "Successfully updated the urls" 100 | else: 101 | print 'Didn\'t find any services for ' + oldUrl 102 | except ValueError as e: 103 | print 'Error - no web maps specified' 104 | except AGOPostError as e: 105 | print 'Error updating web map ' + e.webmap + ": " + e.msg 106 | 107 | def updateItemUrl(self, itemId, oldUrl, newUrl, folderID=None): 108 | ''' 109 | Use this to update the URL for items such as Map Services. 110 | The oldUrl parameter is required as a check to ensure you are not 111 | accidentally changing the wrong item or url. 112 | This can also replace part of a URL. The text of oldUrl is replaced with the text of newUrl. For example you could change just the host name of your URLs. 113 | ''' 114 | try: 115 | params = urllib.urlencode({'token' : self.user.token, 116 | 'f' : 'json'}) 117 | print 'Getting Info for: ' + itemId 118 | # Get the item data 119 | reqUrl = self.user.portalUrl + '/sharing/rest/content/items/' + itemId + '?' + params 120 | itemReq = urllib.urlopen(reqUrl).read() 121 | itemString = str(itemReq) 122 | itemInfo = json.loads(itemString) 123 | 124 | if not itemInfo.has_key('url'): 125 | print itemInfo['title'] + ' doesn\'t have a url property' 126 | return 127 | print 'Updating ' + itemInfo['title'] 128 | existingURL = itemInfo['url'] 129 | 130 | # Double check that the existing URL matches the provided URL 131 | if itemString.find(oldUrl) > -1: 132 | # Get the item folder. 133 | if itemInfo['ownerFolder']: 134 | folderID = itemInfo['ownerFolder'] 135 | else: 136 | folderID = '' 137 | # Update the item URL 138 | updatedURL = existingURL.replace(oldUrl, newUrl) 139 | updateParams = urllib.urlencode({'url' : updatedURL}) 140 | updateUrl = self.user.portalUrl + '/sharing/rest/content/users/' + self.user.username + '/' + folderID + '/items/' + itemId + '/update?' + params 141 | updateReq = urllib.urlopen(updateUrl, updateParams).read() 142 | modResponse = json.loads(updateReq) 143 | if modResponse.has_key('success'): 144 | print "Successfully updated the url." 145 | else: 146 | raise AGOPostError(itemId, modResponse['error']['message']) 147 | else: 148 | print 'Didn\'t find the specified old URL: ' + oldUrl 149 | except ValueError as e: 150 | print e 151 | except AGOPostError as e: 152 | print 'Error updating item: ' + e.msg 153 | 154 | def updatewebmapversionAGX(self, webmapId, folderID=None): 155 | '''Update the web map version from 1.9x to 1.7x so that the new web maps can be opened in ArcGIS Explorer Online.''' 156 | try: 157 | params = urllib.urlencode({'token' : self.user.token, 158 | 'f' : 'json'}) 159 | print 'Getting Info for: ' + webmapId 160 | #Get the item data 161 | reqUrl = self.user.portalUrl + '/sharing/content/items/' + webmapId + '/data?' + params 162 | itemDataReq = urllib.urlopen(reqUrl).read() 163 | itemString = str(itemDataReq) 164 | 165 | itemString = itemString.replace('1.9', '1.7') 166 | 167 | itemInfoReq = urllib.urlopen(self.user.portalUrl + '/sharing/content/items/' + webmapId + '?' + params) 168 | itemInfo = json.loads(itemInfoReq.read(), object_hook=self.__decode_dict__) 169 | print 'Updating ' + itemInfo['title'] 170 | 171 | #Set up the addItem parameters 172 | outParamObj = { 173 | 'extent' : ', '.join([str(itemInfo['extent'][0][0]), str(itemInfo['extent'][0][1]), str(itemInfo['extent'][1][0]), str(itemInfo['extent'][1][1])]), 174 | 'type' : itemInfo['type'], 175 | 'item' : itemInfo['item'], 176 | 'title' : itemInfo['title'], 177 | 'overwrite' : 'true', 178 | 'tags' : ','.join(itemInfo['tags']), 179 | 'text' : itemString 180 | } 181 | # Get the item folder. 182 | if itemInfo['ownerFolder']: 183 | folderID = itemInfo['ownerFolder'] 184 | else: 185 | folderID = '' 186 | 187 | #Post back the changes overwriting the old map 188 | modRequest = urllib.urlopen(self.user.portalUrl + '/sharing/content/users/' + self.user.username + '/' + folderID + '/addItem?' + params , urllib.urlencode(outParamObj)) 189 | #Evaluate the results to make sure it happened 190 | modResponse = json.loads(modRequest.read()) 191 | if modResponse.has_key('error'): 192 | raise AGOPostError(webmapId, modResponse['error']['message']) 193 | else: 194 | print "Successfully updated the version" 195 | 196 | except ValueError as e: 197 | print 'Error - no web maps specified' 198 | except AGOPostError as e: 199 | print 'Error updating web map ' + e.webmap + ": " + e.msg 200 | 201 | def getFolderItems(self, folderId, userName=None): 202 | ''' 203 | Returns all items (list of dictionaries) for an AGOL folder using the folder ID. 204 | folderID -- The unique id for the folder. Use getFolderID to find the folder ID for a folder name. 205 | userName -- The user who owns the folder. If not specified, the user initialized with this object is used. 206 | ''' 207 | if userName == None: 208 | userName = self.user.username 209 | params = urllib.urlencode({'token': self.user.token, 'f': 'json'}) 210 | request = self.user.portalUrl + '/sharing/rest/content/users/' + userName + '/' + folderId + '?' + params 211 | folderContent = json.loads(urllib.urlopen(request).read()) 212 | return folderContent['items'] 213 | 214 | def getUserFolders(self, userName=None): 215 | ''' 216 | Returns all folders (list of dictionaries) for an AGOL user. 217 | userName -- The user who owns the folder. If not specified, the user initialized with this object is used. 218 | ''' 219 | if userName == None: 220 | userName = self.user.username 221 | parameters = urllib.urlencode({'token': self.user.token, 'f': 'json'}) 222 | request = self.user.portalUrl + '/sharing/rest/content/users/' + userName + '?' + parameters 223 | userContent = json.loads(urllib.urlopen(request).read()) 224 | return userContent['folders'] 225 | 226 | def getUserGroups(self, username=None): 227 | ''' 228 | Returns all groups for the specified user.''' 229 | if username == None: 230 | username = self.user.username 231 | parameters = urllib.urlencode({'token': self.user.token, 'f': 'json'}) 232 | request = self.user.portalUrl + '/sharing/rest/community/users/' + username + '?' + parameters 233 | response = json.loads(urllib.urlopen(request).read()) 234 | return response['groups'] 235 | 236 | def getFolderID(self, folderTitle, userName=None): 237 | ''' 238 | Returns the folder ID given a case insensitive folder title. 239 | folderTitle -- The title (name) of a folder. 240 | userName -- The user who owns the folder. If not specified, the user initialized with this object is used. 241 | ''' 242 | if userName == None: 243 | userName = self.user.username 244 | folders = self.getUserFolders(userName) 245 | for folder in folders: 246 | if folder['title'].upper() == folderTitle.upper(): 247 | return folder['id'] 248 | break 249 | 250 | def updateURLs(self, oldUrl, newUrl, items, folderID=None): 251 | ''' 252 | Updates the URL or URL part for all URLs in a list of AGOL items. 253 | This works for all item types that store a URL. (e.g. web maps, map services, applications, etc.) 254 | oldUrl -- All or part of a URL to search for. 255 | newUrl -- The text that will be used to replace the current "oldUrl" text. 256 | ''' 257 | for item in items: 258 | if item['type'] == 'Web Map': 259 | self.updateWebmapService(item['id'], oldUrl, newUrl, folderID) 260 | else: 261 | if item.has_key('url') and not item['url'] == None: 262 | self.updateItemUrl(item['id'], oldUrl, newUrl, folderID) 263 | 264 | def __decode_dict__(self, dct): 265 | newdict = {} 266 | for k, v in dct.iteritems(): 267 | k = self.__safeValue__(k) 268 | v = self.__safeValue__(v) 269 | newdict[k] = v 270 | return newdict 271 | 272 | def __safeValue__(self, inVal): 273 | outVal = inVal 274 | if isinstance(inVal, unicode): 275 | outVal = inVal.encode('utf-8') 276 | elif isinstance(inVal, list): 277 | outVal = self.__decode_list__(inVal) 278 | return outVal 279 | 280 | def __decode_list__(self, lst): 281 | newList = [] 282 | for i in lst: 283 | i = self.__safeValue__(i) 284 | newList.append(i) 285 | return newList 286 | 287 | def __getItemFolder__(self, itemId): 288 | '''Finds the folder id for a particular item.''' 289 | parameters = urllib.urlencode({'token' : self.user.token, 290 | 'f' : 'json'}) 291 | request = '{}/sharing/rest/content/items/{}?{}'.format(self.user.portalUrl, itemId, parameters) 292 | response = json.loads(urllib.urlopen(request).read()) 293 | return response['ownerFolder'] 294 | 295 | def __getFolderContent__(self, folderId): 296 | '''Lists all of the items in a folder.''' 297 | parameters = urllib.urlencode({'token' : self.user.token, 298 | 'f' : 'json'}) 299 | response = json.loads(urllib.urlopen(self.user.portalUrl + '/sharing/rest/content/users/' + self.user.username + '/' + folderId + '?' + parameters).read()) 300 | return response 301 | 302 | class AGOPostError(Exception): 303 | def __init__(self, webmap, msg): 304 | self.webmap = webmap 305 | self.msg = msg 306 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Apache License - 2.0 2 | 3 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 4 | 5 | 1. Definitions. 6 | 7 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 8 | 9 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 10 | 11 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control 12 | with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management 13 | of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 14 | ownership of such entity. 15 | 16 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 17 | 18 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, 19 | and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to 22 | compiled object code, generated documentation, and conversions to other media types. 23 | 24 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice 25 | that is included in or attached to the work (an example is provided in the Appendix below). 26 | 27 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the 28 | editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes 29 | of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, 30 | the Work and Derivative Works thereof. 31 | 32 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work 33 | or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual 34 | or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of 35 | electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on 36 | electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for 37 | the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing 38 | by the copyright owner as "Not a Contribution." 39 | 40 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and 41 | subsequently incorporated within the Work. 42 | 43 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, 44 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, 45 | publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 46 | 47 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, 48 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, 49 | sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are 50 | necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was 51 | submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work 52 | or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You 53 | under this License for that Work shall terminate as of the date such litigation is filed. 54 | 55 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, 56 | and in Source or Object form, provided that You meet the following conditions: 57 | 58 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 59 | 60 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 61 | 62 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices 63 | from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 64 | 65 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a 66 | readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the 67 | Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the 68 | Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever 69 | such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. 70 | You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, 71 | provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to 72 | Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your 73 | modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with 74 | the conditions stated in this License. 75 | 76 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You 77 | to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, 78 | nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 79 | 80 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except 81 | as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 82 | 83 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides 84 | its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, 85 | any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for 86 | determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under 87 | this License. 88 | 89 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required 90 | by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, 91 | including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the 92 | use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or 93 | any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 94 | 95 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a 96 | fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting 97 | such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree 98 | to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your 99 | accepting any such warranty or additional liability. 100 | 101 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /samples/AGO Tools.tbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/ago-tools/e029308af4636d420d55210ad81e07b099a18780/samples/AGO Tools.tbx -------------------------------------------------------------------------------- /samples/AGOLCat.py: -------------------------------------------------------------------------------- 1 | #### Generate a CSV listing the items in the organization 2 | #### Optionally include a query using ArcGIS Portal API syntax (http://resources.arcgis.com/en/help/arcgis-rest-api/02r3/02r3000000mn000000.htm) 3 | #### Optionally return the size of the item (requires additional API request for each item, default is False) 4 | #### The results will include every item accessible by the credentials provided 5 | #### Example: 6 | #### AGOLCat.py -u myuser -p mypassword -size False -portal https://esri.maps.arcgis.com -file c:\temp\agol.csv 7 | 8 | 9 | import csv 10 | import argparse 11 | import sys 12 | 13 | from agoTools.admin import Admin 14 | 15 | def _raw_input(prompt=None, stream=None, input=None): 16 | # A raw_input() replacement that doesn't save the string in the 17 | # GNU readline history. 18 | if not stream: 19 | stream = sys.stderr 20 | if not input: 21 | input = sys.stdin 22 | prompt = str(prompt) 23 | if prompt: 24 | stream.write(prompt) 25 | stream.flush() 26 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 27 | line = input.readline() 28 | if not line: 29 | raise EOFError 30 | if line[-1] == '\n': 31 | line = line[:-1] 32 | return line 33 | 34 | # return value with quotes around it always 35 | def getResultValueWithQuotes(s): 36 | if (s==None): 37 | return '' 38 | try: 39 | sResult = str(s) 40 | if (sResult.find("\"")>0): 41 | sResult = sResult.replace("\"","\"\"") 42 | return "\"" + str(sResult) + "\"" 43 | 44 | except: 45 | return '' 46 | 47 | # return value with quotes if needed 48 | def getResultValue(s): 49 | if (s==None): 50 | return '' 51 | try: 52 | sResult = str(s) 53 | if(sResult.find(",")>0 or sResult.find("\r\n")>0): 54 | sResult = sResult.replace("\"", "\"\"") 55 | return "\"" + str(sResult) + "\"" 56 | else: 57 | return str(sResult) 58 | except: 59 | return '' 60 | 61 | parser = argparse.ArgumentParser() 62 | parser.add_argument('-u', '--user') 63 | parser.add_argument('-p', '--password') 64 | parser.add_argument('-q', '--query') 65 | parser.add_argument('-file', '--file') 66 | parser.add_argument('-portal', '--portal') 67 | parser.add_argument('-size', '--bIncludeSize') 68 | 69 | args = parser.parse_args() 70 | 71 | if args.file == None: 72 | args.file = _raw_input("CSV path: ") 73 | 74 | if args.user == None: 75 | args.user = _raw_input("Username:") 76 | 77 | if args.portal == None: 78 | args.portal = _raw_input("Portal: ") 79 | 80 | args.portal = str(args.portal).replace("http://","https://") 81 | 82 | bIncludeSize=False 83 | 84 | if (args.bIncludeSize != None) and (args.bIncludeSize.upper() == "TRUE"): 85 | bIncludeSize=True 86 | 87 | agoAdmin = Admin(args.user,args.portal,args.password) 88 | 89 | catalog= agoAdmin.AGOLCatalog(args.query,bIncludeSize) 90 | 91 | with open(args.file, 'wb') as output: 92 | # Write header row. 93 | output.write("id,owner,created,modified,name,title,type,typeKeywords,description,tags,snippet,thumbnail,extent,spatialReference,accessInformation,licenseInfo,culture,url,access,size,listed,numComments,numRatings,avgRatings,numViews,itemURL\n") 94 | # Write item data. 95 | for r in catalog: 96 | s='' 97 | s += getResultValue(r.id) + "," 98 | s += getResultValue(r.owner) + "," 99 | s += getResultValue(r.created) + "," 100 | s += getResultValue(r.modified) + "," 101 | s += getResultValueWithQuotes(r.name) + "," 102 | s += getResultValueWithQuotes(r.title) + "," 103 | s += getResultValue(r.type) + "," 104 | 105 | sKeyWords = "" 106 | for sKW in r.typeKeywords: 107 | sKeyWords += sKW + "," 108 | 109 | if (len(sKeyWords)> 0 and sKeyWords.endswith(",")): 110 | sKeyWords = sKeyWords[:-1] 111 | 112 | s += getResultValue(sKeyWords) + "," 113 | s += getResultValueWithQuotes(r.description) + "," 114 | 115 | sTags = "" 116 | for sKW in r.tags: 117 | sTags += sKW + "," 118 | 119 | if (len(sTags)> 0 and sTags.endswith(",")): 120 | sTags = sTags[:-1] 121 | 122 | s += getResultValue(sTags) + "," 123 | s += getResultValueWithQuotes(r.snippet) + "," 124 | s += getResultValue(r.thumbnail) + "," 125 | s += "" + "," 126 | 127 | s += getResultValue(r.spatialReference) + "," 128 | s += getResultValue(r.accessInformation) + "," 129 | s += getResultValue(r.licenseInfo) + "," 130 | s += getResultValue(r.culture) + "," 131 | 132 | s += getResultValue(r.url) + "," 133 | s += getResultValue(r.access) + "," 134 | s += getResultValue(r.size) + "," 135 | 136 | s += getResultValue(r.listed) + "," 137 | s += getResultValue(r.numComments) + "," 138 | s += getResultValue(r.numRatings) + "," 139 | s += getResultValue(r.avgRating) + "," 140 | s += getResultValue(r.numViews) + "," 141 | 142 | 143 | s += getResultValue(r.itemURL); 144 | s+="\n" 145 | 146 | output.writelines(s) 147 | 148 | print (args.file + " written.") 149 | a=1 150 | -------------------------------------------------------------------------------- /samples/MakeLab_CreateGroupAndSequentialUsers.py: -------------------------------------------------------------------------------- 1 | # Requires admin role. 2 | import csv, time,arcpy 3 | from agoTools.admin import Admin 4 | adminAccount = arcpy.GetParameterAsText(0) 5 | adminPassword =arcpy.GetParameterAsText(1) 6 | className = arcpy.GetParameterAsText(2) 7 | classSnippet = arcpy.GetParameterAsText(3) 8 | accountNum = arcpy.GetParameterAsText(4) 9 | userPrefix = arcpy.GetParameterAsText(5) 10 | userPassword = arcpy.GetParameterAsText(6) 11 | userEmail = arcpy.GetParameterAsText(7) 12 | userFirstName = arcpy.GetParameterAsText(8) 13 | userLastName = arcpy.GetParameterAsText(9) 14 | userRole = arcpy.GetParameterAsText(10) 15 | instructorAccount = arcpy.GetParameterAsText(11) 16 | provider = "arcgis" 17 | 18 | if not adminAccount: 19 | adminAccount = "your ago account" 20 | if not adminPassword: 21 | adminPassword = "your ago password" 22 | if not className: 23 | className = "Sample Class" 24 | if not classSnippet: 25 | classSnippet = "Snippet goes here" 26 | if not accountNum: 27 | accountNum = 10 28 | accountNum = int(accountNum) 29 | if not userPrefix: 30 | userPrefix = "labUser_" 31 | if not userPassword: 32 | userPassword = "password1" 33 | if not userEmail: 34 | userEmail = "youremail@email.com" 35 | if not userFirstName: 36 | userFirstName = "lab" 37 | if not userLastName: 38 | userLastName = "user" 39 | if not userRole: 40 | userRole = "account_user" 41 | if not provider: 42 | provider = arcgis 43 | 44 | ##Unicode is not encoding properly so convert all arcpy params 45 | adminAccount = str(adminAccount) 46 | adminPassword = str(adminPassword) 47 | className = str(className) 48 | classSnippet = str(classSnippet) 49 | userPrefix = str(userPrefix) 50 | userPassword = str(userPassword) 51 | userEmail = str(userEmail) 52 | userFirstName = str(userFirstName) 53 | userLastName = str(userLastName) 54 | userRole = str(userRole) 55 | provider = str(provider) 56 | 57 | arcpy.AddMessage("Logging in...") 58 | try: 59 | agoAdmin = Admin(adminAccount,password=adminPassword) 60 | except: 61 | arcpy.AddError("Login failed. Please re-enter your admin username and password.") 62 | sys.exit() 63 | 64 | ##Get roles from the portal so we can translate the user-entered name to the role id that the api needs. 65 | ##Also confirm that the user-entered role is valid. 66 | allRoles = agoAdmin.getRoles() 67 | ##getRoles doesn't return predefined system roles, so we'll add those 68 | roles = {'Administrator':'org_admin', 'Publisher':'org_publisher', 'Author':'org_author', 'User':'org_viewer'} 69 | for role in allRoles: 70 | roles[role["name"]] = role["id"] 71 | if not userRole in roles.keys(): 72 | arcpy.AddError(userRole + " is not a valid role.") 73 | sys.exit() 74 | 75 | roleId =roles[userRole] 76 | 77 | arcpy.AddMessage("Creating Group...") 78 | print "Creating Group..." 79 | group = agoAdmin.createGroup(className,classSnippet) 80 | description = "Lab account for " + className 81 | 82 | if "group" in group: 83 | groupId = group["group"]["id"] 84 | arcpy.AddMessage("Creating Users...") 85 | print "Creating Users..." 86 | i = 1 87 | users = [] 88 | 89 | while i <= accountNum: 90 | username =userPrefix + str(i) 91 | arcpy.AddMessage("creating " + username + "...") 92 | print "creating " + username + "..." 93 | agoAdmin.createUser(username,userPassword,userFirstName,userLastName,userEmail,description,roleId,provider) 94 | users.append(username) 95 | i+=1 96 | arcpy.AddMessage("Adding New Users to Group...") 97 | print "Adding Users to Group..." 98 | agoAdmin.addUsersToGroups(users,[groupId]) 99 | if instructorAccount: 100 | arcpy.AddMessage("Reassigning group ownership to " + instructorAccount + "...") 101 | print "Reassigning group ownership to " + instructorAccount + "..." 102 | agoAdmin.reassignGroupOwnership(groupId,instructorAccount) 103 | print "Done" 104 | else: 105 | arcpy.AddError("Failed to create group") 106 | arcpy.AddError(group["error"]["details"]) 107 | print "Failed to create group: " + group["error"]["details"] 108 | 109 | -------------------------------------------------------------------------------- /samples/MakeLab_CreateGroupAndUsersFromCsv.py: -------------------------------------------------------------------------------- 1 | # Requires admin role. 2 | import csv, time,sys,arcpy 3 | from agoTools.admin import Admin 4 | adminAccount = arcpy.GetParameterAsText(0) 5 | adminPassword =arcpy.GetParameterAsText(1) 6 | className = arcpy.GetParameterAsText(2) 7 | classSnippet = arcpy.GetParameterAsText(3) 8 | csvFile = arcpy.GetParameterAsText(4) 9 | userPrefix = arcpy.GetParameterAsText(5) 10 | userPassword = arcpy.GetParameterAsText(6) 11 | userRole = arcpy.GetParameterAsText(7) 12 | instructorAccount = arcpy.GetParameterAsText(8) 13 | provider = "arcgis" 14 | 15 | if not adminAccount: 16 | adminAccount = "your ago account" 17 | if not adminPassword: 18 | adminPassword = "your ago password" 19 | if not className: 20 | className = "Sample Class" 21 | if not classSnippet: 22 | classSnippet = "Snippet goes here" 23 | if not csvFile: 24 | csvFile = r"C:\students.csv" 25 | if not userPrefix: 26 | userPrefix = "labUser_" 27 | if not userPassword: 28 | userPassword = "password1" 29 | if not userRole: 30 | userRole = "account_user" 31 | if not provider: 32 | provider = "arcgis" 33 | 34 | ##Unicode is not encoding properly so convert all arcpy params 35 | adminAccount = str(adminAccount) 36 | adminPassword = str(adminPassword) 37 | className = str(className) 38 | classSnippet = str(classSnippet) 39 | userPrefix = str(userPrefix) 40 | userPassword = str(userPassword) 41 | userRole = str(userRole) 42 | provider = str(provider) 43 | 44 | arcpy.AddMessage("Logging in...") 45 | try: 46 | agoAdmin = Admin(adminAccount,password=adminPassword) 47 | except: 48 | arcpy.AddError("Login failed. Please re-enter your admin username and password.") 49 | sys.exit() 50 | 51 | 52 | ##Get roles from the portal so we can translate the user-entered name to the role id that the api needs. 53 | ##Also confirm that the user-entered role is valid. 54 | allRoles = agoAdmin.getRoles() 55 | ##getRoles doesn't return predefined system roles, so we'll add those 56 | roles = {'Administrator':'org_admin', 'Publisher':'org_publisher', 'Author':'org_author', 'User':'org_viewer'} 57 | for role in allRoles: 58 | roles[role["name"]] = role["id"] 59 | if not userRole in roles.keys(): 60 | arcpy.AddError(userRole + " is not a valid role.") 61 | sys.exit() 62 | 63 | roleId =roles[userRole] 64 | 65 | arcpy.AddMessage("Creating Group...") 66 | print "Creating Group..." 67 | group = agoAdmin.createGroup(className,classSnippet) 68 | description = "Lab account for " + className 69 | 70 | if "group" in group: 71 | groupId = group["group"]["id"] 72 | arcpy.AddMessage("Creating Users...") 73 | print "Creating Users..." 74 | i = 1 75 | users = [] 76 | sameNameCounter = 1 77 | 78 | with open(csvFile,"rb") as userFile: 79 | rows = csv.reader(userFile) 80 | for row in rows: 81 | userFirstName = row[0] 82 | userLastName = row[1] 83 | userEmail = row[2] 84 | username = userPrefix + "_" + userLastName 85 | 86 | if username in users: 87 | username += "_" + str(sameNameCounter) 88 | sameNameCounter +=1 89 | arcpy.AddMessage("creating " + username + "...") 90 | print "creating " + username + "..." 91 | agoAdmin.createUser(username,userPassword,userFirstName,userLastName,userEmail,description,roleId,provider) 92 | users.append(username) 93 | 94 | arcpy.AddMessage("Adding New Users to Group...") 95 | print "Adding Users to Group..." 96 | agoAdmin.addUsersToGroups(users,[groupId]) 97 | if instructorAccount: 98 | arcpy.AddMessage("Reassigning group ownership to " + instructorAccount + "...") 99 | print "Reassigning group ownership to " + instructorAccount + "..." 100 | agoAdmin.reassignGroupOwnership(groupId,instructorAccount) 101 | print "Done" 102 | else: 103 | arcpy.AddError("Failed to create group") 104 | arcpy.AddError(group["error"]["details"]) 105 | print "Failed to create group: " + group["error"]["details"] 106 | 107 | -------------------------------------------------------------------------------- /samples/addNewUsersToGroups.py: -------------------------------------------------------------------------------- 1 | # Requires admin role. 2 | import csv, time, datetime 3 | from agoTools.admin import Admin 4 | 5 | # User parameters: 6 | agoAdmin = Admin() # Replace with your admin username. 7 | daysToCheck = 7 # Replace with number of days to check...1 checks past day, 7 checks past week, etc. 8 | groups = [, , ...] # Enter of groups to which you want to add new users 9 | 10 | # Find the group ID with this tool: http://developers.arcgis.com/en/javascript/samples/portal_getgroupamd/ 11 | outputDir = 'c:/temp/' # Replace with path for report file 12 | 13 | outputDate = datetime.datetime.now().strftime("%Y%m%d") # Current date prefixed to filename. 14 | outputFile = outputDir + outputDate + '_AddNewUsers2Groups.csv' 15 | 16 | newUsers = agoAdmin.getUsers(daysToCheck=daysToCheck) 17 | groupUsers = [] 18 | for user in newUsers: 19 | groupUsers.append(user['username']) 20 | 21 | userSummary = agoAdmin.addUsersToGroups(groupUsers, groups) 22 | 23 | # print userSummary # Uncomment this line to see a summary of the group additions. 24 | # Reports false-negatives as of Nov 5, 2013. 25 | 26 | with open(outputFile, 'wb') as output: 27 | dataWriter = csv.writer(output, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) 28 | # Write header row. 29 | dataWriter.writerow(['Full Name', 'Email', 'Username', 'Role', 'Date Created']) 30 | # Write user data. 31 | for user in newUsers: 32 | dataWriter.writerow([user['fullName'], user['email'], user['username'], user['role'], time.strftime("%Y-%m-%d", time.gmtime(user['created']/1000))]) -------------------------------------------------------------------------------- /samples/clearFolder.py: -------------------------------------------------------------------------------- 1 | #### Remove (delete) all items from a designated folder under My Content 2 | #### Example: 3 | #### clearFolder.py -u myuser -p mypassword -folder DemoMaps -portal https://esri.maps.arcgis.com 4 | 5 | import argparse 6 | import sys 7 | 8 | from agoTools.admin import Admin 9 | 10 | def _raw_input(prompt=None, stream=None, input=None): 11 | # A raw_input() replacement that doesn't save the string in the 12 | # GNU readline history. 13 | if not stream: 14 | stream = sys.stderr 15 | if not input: 16 | input = sys.stdin 17 | prompt = str(prompt) 18 | if prompt: 19 | stream.write(prompt) 20 | stream.flush() 21 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 22 | line = input.readline() 23 | if not line: 24 | raise EOFError 25 | if line[-1] == '\n': 26 | line = line[:-1] 27 | return line 28 | 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument('-u', '--user') 31 | parser.add_argument('-p', '--password') 32 | parser.add_argument('-portal', '--portal') 33 | parser.add_argument('-folder', '--folder') 34 | 35 | args = parser.parse_args() 36 | 37 | if args.user == None: 38 | args.user = _raw_input("Username:") 39 | 40 | if args.portal == None: 41 | args.portal = _raw_input("Portal: ") 42 | 43 | if args.folder == None: 44 | args.folder = _raw_input("Folder: ") 45 | 46 | args.portal = str(args.portal).replace("http://","https://") 47 | 48 | agoAdmin = Admin(args.user,args.portal,args.password) 49 | 50 | if args.folder!= None: 51 | fid = agoAdmin.getFolderID(args.folder) 52 | args.folder=fid 53 | folderid = '/' + args.folder 54 | 55 | agoAdmin.clearFolder(folderid) 56 | 57 | 58 | -------------------------------------------------------------------------------- /samples/clearGroup.py: -------------------------------------------------------------------------------- 1 | #### Remove (unshare) all items from a designated group in the organization 2 | #### Group IDs can be discovered using this utility: http://developers.arcgis.com/javascript/samples/portal_getgroupamd/ 3 | #### Example: 4 | #### clearGroup.py -u myuser -p mypassword -group a23455GROUPID334323434 -portal https://esri.maps.arcgis.com 5 | 6 | import argparse 7 | import sys 8 | 9 | from agoTools.admin import Admin 10 | 11 | def _raw_input(prompt=None, stream=None, input=None): 12 | # A raw_input() replacement that doesn't save the string in the 13 | # GNU readline history. 14 | if not stream: 15 | stream = sys.stderr 16 | if not input: 17 | input = sys.stdin 18 | prompt = str(prompt) 19 | if prompt: 20 | stream.write(prompt) 21 | stream.flush() 22 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 23 | line = input.readline() 24 | if not line: 25 | raise EOFError 26 | if line[-1] == '\n': 27 | line = line[:-1] 28 | return line 29 | 30 | parser = argparse.ArgumentParser() 31 | parser.add_argument('-u', '--user') 32 | parser.add_argument('-p', '--password') 33 | parser.add_argument('-portal', '--portal') 34 | parser.add_argument('-group', '--group') 35 | 36 | args = parser.parse_args() 37 | 38 | if args.user == None: 39 | args.user = _raw_input("Username:") 40 | 41 | if args.portal == None: 42 | args.portal = _raw_input("Portal: ") 43 | 44 | if args.group == None: 45 | args.group = _raw_input("Group ID: ") 46 | 47 | args.portal = str(args.portal).replace("http://","https://") 48 | 49 | agoAdmin = Admin(args.user,args.portal,args.password) 50 | 51 | agoAdmin.clearGroup(args.group) 52 | 53 | 54 | -------------------------------------------------------------------------------- /samples/createUserListCSV.py: -------------------------------------------------------------------------------- 1 | # Requires admin role. 2 | import csv, time 3 | from agoTools.admin import Admin 4 | 5 | agoAdmin = Admin('') # Replace with your admin username. 6 | outputFile = 'c:/temp/users.csv' 7 | 8 | users = agoAdmin.getUsers() 9 | roles = agoAdmin.getRoles() 10 | 11 | #Make a dictionary of the roles so we can convert custom roles from their ID to their associated name. 12 | roleLookup = {} 13 | for role in roles: 14 | roleLookup[role["id"]] = role["name"] 15 | 16 | with open(outputFile, 'wb') as output: 17 | dataWriter = csv.writer(output, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) 18 | # Write header row. 19 | dataWriter.writerow(['Full Name', 20 | 'Email', 21 | 'Username', 22 | 'Role', 23 | 'Date Created']) 24 | # Write user data. 25 | for user in users: 26 | #get role name from the id. If it's not in the roles, it's one of the standard roles so just use it. 27 | roleID = user['role'] 28 | roleName = roleLookup.get(roleID,roleID) 29 | dataWriter.writerow([user['fullName'].encode('utf-8'), 30 | user['email'].encode('utf-8'), 31 | user['username'].encode('utf-8'), 32 | roleName, 33 | time.strftime("%Y-%m-%d",time.gmtime(user['created']/1000))]) 34 | 35 | print('Finished writing spreadsheet {}'.format(outputFile)) 36 | -------------------------------------------------------------------------------- /samples/createUserListWithGroups.py: -------------------------------------------------------------------------------- 1 | ### This script creates a spreadsheet with key informationa about each user. 2 | ### It expands on createUserListCSV.py by adding a field detailing the groups 3 | ### each user is a member of. 4 | 5 | # Requires admin role. 6 | import csv, time 7 | from agoTools.admin import Admin 8 | from agoTools.utilities import Utilities 9 | 10 | username = '' 11 | password = '' 12 | outputFile = 'c:/temp/users.csv' 13 | 14 | agoAdmin = Admin(username=username, password=password) 15 | agoUtilities = Utilities(username=username, password=password) 16 | 17 | print('Getting users.') 18 | users = agoAdmin.getUsers() 19 | roles = agoAdmin.getRoles() 20 | # Make a dictionary of the roles so we can convert custom roles from their ID to their associated name. 21 | roleLookup = {} 22 | for role in roles: 23 | roleLookup[role["id"]] = role['name'] 24 | 25 | # Get the users' groups. 26 | print('Getting each user\'s groups.') 27 | for user in users: 28 | user['groups'] = [] 29 | groups = agoUtilities.getUserGroups(user['username']) 30 | for group in groups: 31 | user['groups'].append(group['title'].encode('utf-8')) 32 | 33 | print('Writing the results.') 34 | with open(outputFile, 'wb') as output: 35 | dataWriter = csv.writer(output, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) 36 | # Write header row. 37 | dataWriter.writerow(['Full Name', 38 | 'Email', 39 | 'Username', 40 | 'Role', 41 | 'Date Created', 42 | 'Last Login', 43 | 'Groups']) 44 | 45 | # Write user data. 46 | for user in users: 47 | try: 48 | # Get role name from the id. If it's not in the roles, it's one of the standard roles so just use it. 49 | roleID = user['role'] 50 | roleName = roleLookup.get(roleID, roleID) 51 | if user['lastLogin'] > 0: 52 | lastLogin = time.strftime("%Y-%m-%d", time.gmtime(user['lastLogin']/1000)) 53 | else: 54 | lastLogin = 'Never' 55 | dataWriter.writerow([user['fullName'].encode('utf-8'), 56 | user['email'].encode('utf-8'), 57 | user['username'].encode('utf-8'), 58 | roleName, 59 | time.strftime("%Y-%m-%d", time.gmtime(user['created']/1000)), 60 | lastLogin, 61 | ','.join(user['groups'])]) 62 | except: 63 | print('Problem writing data for {}'.format(user['username'])) 64 | 65 | print('Finished writing spreadsheet {}'.format(outputFile)) 66 | -------------------------------------------------------------------------------- /samples/deleteItems.py: -------------------------------------------------------------------------------- 1 | #### Delete items listed in an input CSV from the organization 2 | #### The only required fields in the CSV are: 3 | #### id,owner 4 | #### Example: 5 | #### deleteItems.py -u myuser -p mypassword -folder MyNewItems -portal https://esri.maps.arcgis.com -file c:\temp\agolinput.csv 6 | 7 | import csv 8 | import argparse 9 | import sys 10 | 11 | from agoTools.admin import Admin 12 | from agoTools.admin import AGOLItems 13 | 14 | def _raw_input(prompt=None, stream=None, input=None): 15 | # A raw_input() replacement that doesn't save the string in the 16 | # GNU readline history. 17 | if not stream: 18 | stream = sys.stderr 19 | if not input: 20 | input = sys.stdin 21 | prompt = str(prompt) 22 | if prompt: 23 | stream.write(prompt) 24 | stream.flush() 25 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 26 | line = input.readline() 27 | if not line: 28 | raise EOFError 29 | if line[-1] == '\n': 30 | line = line[:-1] 31 | return line 32 | 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument('-u', '--user') 35 | parser.add_argument('-p', '--password') 36 | parser.add_argument('-file', '--file') 37 | parser.add_argument('-portal', '--portal') 38 | 39 | args = parser.parse_args() 40 | inputFile = '' 41 | 42 | if args.file == None: 43 | args.file = _raw_input("CSV path: ") 44 | 45 | if args.user == None: 46 | args.user = _raw_input("Username:") 47 | 48 | if args.portal == None: 49 | args.portal = _raw_input("Portal: ") 50 | 51 | args.portal = str(args.portal).replace("http://","https://") 52 | 53 | agoAdmin = Admin(args.user,args.portal,args.password) 54 | 55 | if args.file != None: 56 | inputFile=args.file 57 | 58 | with open(inputFile) as input: 59 | dataReader = csv.DictReader(input) 60 | items=AGOLItems(dataReader) 61 | 62 | agoAdmin.deleteItems(items.AGOLItems_list) 63 | -------------------------------------------------------------------------------- /samples/findItemsContainingUrl.py: -------------------------------------------------------------------------------- 1 | 2 | #### Find Items Containing parts of URL, optionally using a folder ID 3 | #### Example: 4 | #### findItemsContainingUrl.py -u myuser -p mypassword -url http://myserver.com -portal https://esri.maps.arcgis.com -folder demo 5 | 6 | import csv 7 | import argparse 8 | import sys 9 | 10 | from agoTools.admin import Admin 11 | 12 | def _raw_input(prompt=None, stream=None, input=None): 13 | # A raw_input() replacement that doesn't save the string in the 14 | # GNU readline history. 15 | if not stream: 16 | stream = sys.stderr 17 | if not input: 18 | input = sys.stdin 19 | prompt = str(prompt) 20 | if prompt: 21 | stream.write(prompt) 22 | stream.flush() 23 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 24 | line = input.readline() 25 | if not line: 26 | raise EOFError 27 | if line[-1] == '\n': 28 | line = line[:-1] 29 | return line 30 | 31 | # return value with quotes around it always 32 | def getResultValueWithQuotes(s): 33 | if (s==None): 34 | return '' 35 | try: 36 | sResult = str(s) 37 | if (sResult.find("\"")>0): 38 | sResult = sResult.replace("\"","\"\"") 39 | return "\"" + str(sResult) + "\"" 40 | 41 | except: 42 | return '' 43 | 44 | # return value with quotes if needed 45 | def getResultValue(s): 46 | if (s==None): 47 | return '' 48 | try: 49 | sResult = str(s) 50 | if(sResult.find(",")>0 or sResult.find("\r\n")>0): 51 | sResult = sResult.replace("\"", "\"\"") 52 | return "\"" + str(sResult) + "\"" 53 | else: 54 | return str(sResult) 55 | except: 56 | return '' 57 | 58 | parser = argparse.ArgumentParser() 59 | parser.add_argument('-u', '--user') 60 | parser.add_argument('-p', '--password') 61 | parser.add_argument('-url', '--url') 62 | parser.add_argument('-file', '--file') 63 | parser.add_argument('-portal', '--portal') 64 | parser.add_argument('-folder', '--folder') 65 | 66 | args = parser.parse_args() 67 | 68 | if args.user == None: 69 | args.user = _raw_input("Username:") 70 | 71 | if args.portal == None: 72 | args.portal = _raw_input("Portal: ") 73 | 74 | args.portal = str(args.portal).replace("http://","https://") 75 | 76 | if args.folder == None: 77 | args.folder = _raw_input("Folder (optional): ") 78 | 79 | agoAdmin = Admin(args.user,args.portal,args.password) 80 | 81 | if args.url == None: 82 | args.url= _raw_input("Query URL: ") 83 | 84 | if args.file == None: 85 | args.file = _raw_input("Output CSV: ") 86 | 87 | folderid = None 88 | if args.folder!= None and args.folder!='': 89 | fid = agoAdmin.getFolderID(args.folder) 90 | args.folder=fid 91 | folderid = '/' + args.folder 92 | 93 | catalog = agoAdmin.findItemsWithURLs(args.url,folderid) 94 | 95 | with open(args.file, 'wb') as output: 96 | # Write header row. 97 | output.write("id,owner,created,modified,name,title,type,typeKeywords,description,tags,snippet,thumbnail,extent,spatialReference,accessInformation,licenseInfo,culture,url,access,size,listed,numComments,numRatings,avgRatings,numViews,itemURL\n") 98 | # Write item data. 99 | for r in catalog: 100 | s='' 101 | s += getResultValue(r.id) + "," 102 | s += getResultValue(r.owner) + "," 103 | s += getResultValue(r.created) + "," 104 | s += getResultValue(r.modified) + "," 105 | s += getResultValueWithQuotes(r.name) + "," 106 | s += getResultValueWithQuotes(r.title) + "," 107 | s += getResultValue(r.type) + "," 108 | 109 | sKeyWords = "" 110 | for sKW in r.typeKeywords: 111 | sKeyWords += sKW + "," 112 | 113 | if (len(sKeyWords)> 0 and sKeyWords.endswith(",")): 114 | sKeyWords = sKeyWords[:-1] 115 | 116 | s += getResultValue(sKeyWords) + "," 117 | s += getResultValueWithQuotes(r.description) + "," 118 | 119 | sTags = "" 120 | for sKW in r.tags: 121 | sTags += sKW + "," 122 | 123 | if (len(sTags)> 0 and sTags.endswith(",")): 124 | sTags = sTags[:-1] 125 | 126 | s += getResultValue(sTags) + "," 127 | s += getResultValueWithQuotes(r.snippet) + "," 128 | s += getResultValue(r.thumbnail) + "," 129 | s += "" + "," 130 | 131 | s += getResultValue(r.spatialReference) + "," 132 | s += getResultValue(r.accessInformation) + "," 133 | s += getResultValue(r.licenseInfo) + "," 134 | s += getResultValue(r.culture) + "," 135 | 136 | s += getResultValue(r.url) + "," 137 | s += getResultValue(r.access) + "," 138 | s += getResultValue(r.size) + "," 139 | 140 | s += getResultValue(r.listed) + "," 141 | s += getResultValue(r.numComments) + "," 142 | s += getResultValue(r.numRatings) + "," 143 | s += getResultValue(r.avgRating) + "," 144 | s += getResultValue(r.numViews) + "," 145 | 146 | 147 | s += getResultValue(r.itemURL); 148 | s+="\n" 149 | 150 | output.writelines(s) 151 | 152 | print (args.file + " written.") 153 | 154 | -------------------------------------------------------------------------------- /samples/flagAttachments.py: -------------------------------------------------------------------------------- 1 | #### calculate a field for feature layer indicating presence of attachments 2 | 3 | #### example: 4 | #### flagAttachment.py -u -p -flagField HASATTACHMENTS -layerURL http://services.arcgis.com/XWaQZrOGjgrsZ6Cu/arcgis/rest/services/Towns/FeatureServer/0 -portal http://yourorg.maps.arcgis.com 5 | 6 | import csv 7 | import argparse 8 | import sys 9 | import json 10 | from agoTools.admin import Admin 11 | 12 | def _raw_input(prompt=None, stream=None, input=None): 13 | # A raw_input() replacement that doesn't save the string in the 14 | # GNU readline history. 15 | if not stream: 16 | stream = sys.stderr 17 | if not input: 18 | input = sys.stdin 19 | prompt = str(prompt) 20 | if prompt: 21 | stream.write(prompt) 22 | stream.flush() 23 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 24 | line = input.readline() 25 | if not line: 26 | raise EOFError 27 | if line[-1] == '\n': 28 | line = line[:-1] 29 | return line 30 | 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument('-u', '--user') 33 | parser.add_argument('-p', '--password') 34 | parser.add_argument('-layerURL', '--layerURL') 35 | parser.add_argument('-layerID', '--layerID') 36 | parser.add_argument('-flagField', '--flagField') 37 | parser.add_argument('-portal', '--portal') 38 | 39 | args = parser.parse_args() 40 | 41 | if args.user == None: 42 | args.user = _raw_input("Username:") 43 | 44 | if args.portal == None: 45 | args.portal = _raw_input("Portal: ") 46 | 47 | args.portal = str(args.portal).replace("http://","https://") 48 | 49 | agoAdmin = Admin(args.user,args.portal,args.password) 50 | 51 | if (args.layerID==None and args.layerURL==None): 52 | args.layerID = _raw_input("layerID: ") 53 | 54 | if args.layerID!=None: 55 | args.layerURL=agoAdmin.getLayerURL(args.layerID) 56 | 57 | if args.flagField == None: 58 | args.flagField=_raw_input("field to update: ") 59 | 60 | agoAdmin.calculateAttachmentCount(args.layerURL,args.flagField) 61 | 62 | -------------------------------------------------------------------------------- /samples/listUsersByGroup.py: -------------------------------------------------------------------------------- 1 | ### Lists out the admins and members of the first group found by the 2 | ### specified search term. 3 | 4 | from agoTools.admin import Admin 5 | 6 | username = '' 7 | password = '' 8 | groupSearch = 'Mission Operations' 9 | 10 | myAdmin = Admin(username=username, password=password) 11 | groupInfo = myAdmin.findGroup(groupSearch) 12 | groupMembers = myAdmin.getUsersInGroup(groupInfo['id']) 13 | 14 | print('Users in {}'.format(groupInfo['title'])) 15 | for admin in groupMembers['admins']: 16 | print(' {} - Admin'.format(admin)) 17 | for user in groupMembers['users']: 18 | print(' {} - Member'.format(user)) -------------------------------------------------------------------------------- /samples/migrateAccount.py: -------------------------------------------------------------------------------- 1 | #### Migrate person to a new account within the same Org 2 | 3 | # Requires admin role 4 | # Useful when migrating to Enterprise Logins. 5 | # Reassigns all items/groups to new owner and 6 | # adds userTo to all groups which userFrom is a member.''' 7 | 8 | from agoTools.admin import Admin 9 | myAgol = Admin('') # Replace your ADMIN account 10 | 11 | # for migrating a single account... 12 | myAgol.migrateAccount('', '') # Replace with usernames between which you are moving items 13 | 14 | # for migrating a batch of accounts... 15 | myAgol.migrateAccounts() # Replace with path to CSV file with col1=userFrom, col2=userTo 16 | -------------------------------------------------------------------------------- /samples/moveItemsReassignGroups.py: -------------------------------------------------------------------------------- 1 | #### Move all items from one account to another, reassign ownership of all groups, or add user to another user's groups 2 | 3 | # Requires admin role 4 | # If you want to do all three tasks at once, see migrateAccount or migrateAccounts functions 5 | 6 | from agoTools.admin import Admin 7 | agoAdmin = Admin() # Replace with your admin username 8 | 9 | agoAdmin.reassignAllUser1ItemsToUser2(agoAdmin, , ) #Replace with your current and new account usernames 10 | agoAdmin.reassignAllGroupOwnership(agoAdmin, , ) 11 | agoAdmin.addUser2ToAllUser1Groups(agoAdmin, , ) -------------------------------------------------------------------------------- /samples/populateBookmarks.py: -------------------------------------------------------------------------------- 1 | #### Update a webmap with a collection of bookmarks derived from: 2 | #### Input json file format, feature extents from service (via URL or ArcGIS Online ID), feature extents from local feature layer 3 | 4 | #### Input json file format (from webmap spec): 5 | #### 6 | #### 7 | #### 8 | #{"bookmarks": [ 9 | #{ 10 | # "extent":{ 11 | # "SpatialReference":{ 12 | # "wkid":"4326" 13 | # }, 14 | # "xmax":-77.58890542503474, 15 | # "xmin":-77.66083839947551, 16 | # "ymax":42.58041413631198, 17 | # "ymin":42.549020481314585 18 | # }, 19 | # "name":"Area of Interest 1" 20 | #}, 21 | #{ 22 | # "extent":{ 23 | # "SpatialReference":{ 24 | # "wkid":"4326" 25 | # }, 26 | # "xmax":-77.53970779419222, 27 | # "xmin":-77.59446878554364, 28 | # "ymax":42.57963402941389, 29 | # "ymin":42.51842215653264 30 | # }, 31 | # "name":"Area of Intest 2" 32 | #} 33 | #} 34 | #### examples: 35 | #### populateBookmarks.py -u -p -jsonfile c:/temp/matownsbookmarks.json -labelfield NAME -itemid ff2251d13c094cbc857ae0787900355b -portal http://yourorg.maps.arcgis.com 36 | #### populateBookmarks.py -u -p -layerURL http://services.arcgis.com/XWaQZrOGjgrsZ6Cu/arcgis/rest/services/Towns/FeatureServer/0 -labelfield NAME -itemid ff2251d13c094cbc857ae0787900355b -portal http://.maps.arcgis.com 37 | #### populateBookmarks.py -u -p -fc D:/data/SteubenCounty/Data.gdb/Districts -labelfield NAME -itemid ff2251d13c094cbc857ae0787900355b -portal http://.maps.arcgis.com 38 | #### populateBookmarks.py -u -p -layerID -labelfield NAME -itemid ff2251d13c094cbc857ae0787900355b -portal http://.maps.arcgis.com 39 | 40 | import csv 41 | import argparse 42 | import sys 43 | import json 44 | from agoTools.admin import Admin 45 | 46 | def _raw_input(prompt=None, stream=None, input=None): 47 | # A raw_input() replacement that doesn't save the string in the 48 | # GNU readline history. 49 | if not stream: 50 | stream = sys.stderr 51 | if not input: 52 | input = sys.stdin 53 | prompt = str(prompt) 54 | if prompt: 55 | stream.write(prompt) 56 | stream.flush() 57 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 58 | line = input.readline() 59 | if not line: 60 | raise EOFError 61 | if line[-1] == '\n': 62 | line = line[:-1] 63 | return line 64 | 65 | # return value with quotes around it always 66 | def getResultValueWithQuotes(s): 67 | if (s==None): 68 | return '' 69 | try: 70 | sResult = str(s) 71 | if (sResult.find("\"")>0): 72 | sResult = sResult.replace("\"","\"\"") 73 | return "\"" + str(sResult) + "\"" 74 | 75 | except: 76 | return '' 77 | 78 | # return value with quotes if needed 79 | def getResultValue(s): 80 | if (s==None): 81 | return '' 82 | try: 83 | sResult = str(s) 84 | if(sResult.find(",")>0 or sResult.find("\r\n")>0): 85 | sResult = sResult.replace("\"", "\"\"") 86 | return "\"" + str(sResult) + "\"" 87 | else: 88 | return str(sResult) 89 | except: 90 | return '' 91 | 92 | parser = argparse.ArgumentParser() 93 | parser.add_argument('-u', '--user') 94 | parser.add_argument('-p', '--password') 95 | parser.add_argument('-itemid', '--itemid') 96 | parser.add_argument('-layerID', '--layerID') 97 | parser.add_argument('-fcID', '--fcID') 98 | parser.add_argument('-layerURL', '--layerURL') 99 | parser.add_argument('-labelfield', '--labelfield') 100 | parser.add_argument('-jsonfile', '--jsonfile') 101 | parser.add_argument('-portal', '--portal') 102 | parser.add_argument('-fc','--fc') 103 | 104 | args = parser.parse_args() 105 | 106 | if args.user == None: 107 | args.user = _raw_input("Username:") 108 | 109 | if args.portal == None: 110 | args.portal = _raw_input("Portal: ") 111 | 112 | args.portal = str(args.portal).replace("http://","https://") 113 | 114 | agoAdmin = Admin(args.user,args.portal,args.password) 115 | 116 | if args.itemid == None: 117 | args.itemid = _raw_input("WebMap Id: ") 118 | 119 | if args.layerID!=None: 120 | args.layerURL=agoAdmin.getLayerURL(args.layerID) 121 | 122 | if args.labelfield == None: 123 | args.labelfield=_raw_input("label field: ") 124 | 125 | if args.layerURL!= None: 126 | pBookmarks =agoAdmin.createBookmarksFromLayer(args.layerURL,args.labelfield) 127 | elif args.fc !=None: 128 | pBookmarks = agoAdmin.readBookmarksFromFeatureClass(args.fc,args.labelfield) 129 | elif args.jsonfile != None: 130 | pBookmarks= agoAdmin.readBookmarksFromFile(args.jsonfile) 131 | elif args.fcID !=None: 132 | pBookmarks = agoAdmin.readBookmarksFromFeatureCollection(args.fcID,args.labelfield) 133 | else: 134 | args.layerID = _raw_input("item ID of feature layer: ") 135 | args.layerURL=agoAdmin.getLayerURL(args.layerID) 136 | pBookmarks =agoAdmin.createBookmarksFromLayer(args.layerURL,args.labelfield) 137 | 138 | if pBookmarks!=None: 139 | agoAdmin.addBookmarksToWebMap(pBookmarks,args.itemid); 140 | else: 141 | print "No Bookmarks were found." 142 | -------------------------------------------------------------------------------- /samples/registerItems.py: -------------------------------------------------------------------------------- 1 | #### Register items listed in an input CSV to the organization 2 | #### Optionally place the items into a specific folder under My Content 3 | #### The required fields in the CSV are: 4 | #### title, url, tags, type, id (empty placeholder) 5 | #### The following fields will be honored if present, otherwise default values will be used: 6 | #### thumbnail, description, snipppet, extent, spatialReference, accessInformation, licenseInfo, culture 7 | #### Note that 'tags' if not specified will be Mapping and 'type' if not specified will be 'Map Service' 8 | #### Example: 9 | #### registerItems.py -u myuser -p mypassword -folder MyNewItems -portal https://esri.maps.arcgis.com -file c:\temp\agolinput.csv 10 | 11 | import csv 12 | import argparse 13 | import sys 14 | 15 | from agoTools.admin import Admin 16 | from agoTools.admin import MapService 17 | from agoTools.admin import MapServices 18 | 19 | def _raw_input(prompt=None, stream=None, input=None): 20 | # A raw_input() replacement that doesn't save the string in the 21 | # GNU readline history. 22 | if not stream: 23 | stream = sys.stderr 24 | if not input: 25 | input = sys.stdin 26 | prompt = str(prompt) 27 | if prompt: 28 | stream.write(prompt) 29 | stream.flush() 30 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 31 | line = input.readline() 32 | if not line: 33 | raise EOFError 34 | if line[-1] == '\n': 35 | line = line[:-1] 36 | return line 37 | 38 | parser = argparse.ArgumentParser() 39 | parser.add_argument('-u', '--user') 40 | parser.add_argument('-p', '--password') 41 | parser.add_argument('-folder', '--folder') 42 | parser.add_argument('-file', '--file') 43 | parser.add_argument('-portal', '--portal') 44 | 45 | args = parser.parse_args() 46 | inputFile = '' 47 | 48 | if args.file == None: 49 | args.file = _raw_input("CSV path: ") 50 | 51 | if args.user == None: 52 | args.user = _raw_input("Username:") 53 | 54 | if args.portal == None: 55 | args.portal = _raw_input("Portal: ") 56 | 57 | if args.folder == None: 58 | args.folder = _raw_input("Folder (optional): ") 59 | 60 | args.portal = str(args.portal).replace("http://","https://") 61 | 62 | agoAdmin = Admin(args.user,args.portal,args.password) 63 | 64 | folderid=None 65 | if args.folder!= None: 66 | fid = agoAdmin.getFolderID(args.folder) 67 | args.folder=fid 68 | folderid = '/' + args.folder 69 | 70 | if args.file != None: 71 | inputFile=args.file 72 | 73 | with open(inputFile) as input: 74 | dataReader = csv.DictReader(input) 75 | mapServices=MapServices(dataReader) 76 | 77 | agoAdmin.registerItems(mapServices,folderid) 78 | -------------------------------------------------------------------------------- /samples/searchAllUserItems.py: -------------------------------------------------------------------------------- 1 | #### Search for all content owned by a specific user (admin view) 2 | 3 | from agoTools.admin import Admin 4 | import agoTools.utilities 5 | 6 | myAgo = Admin(username=, password=) # replace with your username and password 7 | search = agoTools.utilities.searchPortal(myAgo.user.portalUrl, query='owner: username', token=myAgo.user.token) 8 | 9 | print search 10 | -------------------------------------------------------------------------------- /samples/searchExamples.py: -------------------------------------------------------------------------------- 1 | #### Search for the top 10 most viewed public items in my organization. 2 | 3 | from agoTools import User 4 | import agoTools.utilities 5 | 6 | myAgo = User(username=, password=) # replace with your username and password 7 | searchQuery = 'orgid: ' + myAgo.__portalId__() 8 | search = agoTools.utilities.searchPortal(myAgo.portalUrl, query=searchQuery, totalResults=10) 9 | 10 | print search 11 | 12 | 13 | #### Search for all content owned by a specific user (admin view) 14 | 15 | from agoTools.admin import Admin 16 | import agoTools.utilities 17 | 18 | myAgo = Admin(username=, password=) # replace with your username and password 19 | search = agoTools.utilities.searchPortal(myAgo.user.portalUrl, query='owner: username', token=myAgo.user.token) 20 | 21 | print search 22 | -------------------------------------------------------------------------------- /samples/searchTopViewedItems.py: -------------------------------------------------------------------------------- 1 | #### Search for the top 10 most viewed public items in my organization. 2 | 3 | from agoTools import User 4 | import agoTools.utilities 5 | 6 | myAgo = User(username=, password=) # replace with your username and password 7 | searchQuery = 'orgid: ' + myAgo.__portalId__() 8 | search = agoTools.utilities.searchPortal(myAgo.portalUrl, query=searchQuery, totalResults=10) 9 | 10 | print search 11 | -------------------------------------------------------------------------------- /samples/shareItems.py: -------------------------------------------------------------------------------- 1 | 2 | import csv 3 | import argparse 4 | import sys 5 | 6 | from agoTools.admin import Admin 7 | from agoTools.admin import AGOLItems 8 | 9 | def _raw_input(prompt=None, stream=None, input=None): 10 | # A raw_input() replacement that doesn't save the string in the 11 | # GNU readline history. 12 | if not stream: 13 | stream = sys.stderr 14 | if not input: 15 | input = sys.stdin 16 | prompt = str(prompt) 17 | if prompt: 18 | stream.write(prompt) 19 | stream.flush() 20 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 21 | line = input.readline() 22 | if not line: 23 | raise EOFError 24 | if line[-1] == '\n': 25 | line = line[:-1] 26 | return line 27 | 28 | parser = argparse.ArgumentParser() 29 | parser.add_argument('-u', '--user') 30 | parser.add_argument('-p', '--password') 31 | parser.add_argument('-group', '--groupID') 32 | parser.add_argument('-file', '--file') 33 | parser.add_argument('-portal', '--portal') 34 | 35 | args = parser.parse_args() 36 | inputFile = '' 37 | 38 | if args.file == None: 39 | args.file = _raw_input("CSV path: ") 40 | 41 | if args.user == None: 42 | args.user = _raw_input("Username:") 43 | 44 | if args.groupID == None: 45 | args.groupID = _raw_input("Group ID:") 46 | 47 | if args.portal == None: 48 | args.portal = _raw_input("Portal: ") 49 | 50 | args.portal = str(args.portal).replace("http://","https://") 51 | 52 | agoAdmin = Admin(args.user,args.portal,args.password) 53 | 54 | if args.file != None: 55 | inputFile=args.file 56 | 57 | with open(inputFile) as input: 58 | dataReader = csv.DictReader(input) 59 | items=AGOLItems(dataReader) 60 | 61 | agoAdmin.shareItems(items.AGOLItems_list,args.groupID) 62 | -------------------------------------------------------------------------------- /samples/updateMapServiceUrlsInWebMaps.py: -------------------------------------------------------------------------------- 1 | #### Update map service urls in webmaps 2 | 3 | from agoTools.utilities import Utilities 4 | agoUtilities = Utilities() # Replace with your username. 5 | 6 | webmapId = '' # Replace with web map ID 7 | oldUrl = '' # Replace with old map service URL 8 | newUrl = '' # Replace with new map service URL 9 | 10 | agoUtilities.updateWebmapService(webmapId, oldUrl, newUrl) -------------------------------------------------------------------------------- /samples/updateRegisteredUrlForServiceOrApp.py: -------------------------------------------------------------------------------- 1 | #### Update the URL for registered map services or web applications 2 | 3 | from agoTools.utilities import Utilities 4 | 5 | agoUtilities = Utilities() # Replace with your username. 6 | 7 | itemId = '' # Replace with item ID 8 | oldUrl = '' # Replace with old URL 9 | newUrl = '' # Replace with new URL 10 | 11 | agoUtilities.updateItemUrl(itemId, oldUrl, newUrl) -------------------------------------------------------------------------------- /samples/updateServiceItemsThumbnail.py: -------------------------------------------------------------------------------- 1 | #### Update any missing thumbnails for items under My Content with a default 2 | #### Optionally specify a particular folder to search from 3 | #### Example: 4 | #### updateServiceItemsThumbnail.py -u myuser -p mypassword -portal https://esri.maps.arcgis.com 5 | 6 | import csv 7 | import argparse 8 | import sys 9 | 10 | from agoTools.admin import Admin 11 | 12 | 13 | def _raw_input(prompt=None, stream=None, input=None): 14 | # A raw_input() replacement that doesn't save the string in the 15 | # GNU readline history. 16 | if not stream: 17 | stream = sys.stderr 18 | if not input: 19 | input = sys.stdin 20 | prompt = str(prompt) 21 | if prompt: 22 | stream.write(prompt) 23 | stream.flush() 24 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 25 | line = input.readline() 26 | if not line: 27 | raise EOFError 28 | if line[-1] == '\n': 29 | line = line[:-1] 30 | return line 31 | 32 | parser = argparse.ArgumentParser() 33 | parser.add_argument('-u', '--user') 34 | parser.add_argument('-p', '--password') 35 | parser.add_argument('-portal', '--portal') 36 | parser.add_argument('-folder', '--folder') 37 | 38 | args = parser.parse_args() 39 | 40 | if args.user == None: 41 | args.user = _raw_input("Username:") 42 | 43 | if args.portal == None: 44 | args.portal = _raw_input("Portal: ") 45 | 46 | if args.folder == None: 47 | args.folder = _raw_input("Folder (optional): ") 48 | 49 | args.portal = str(args.portal).replace("http://","https://") 50 | 51 | agoAdmin = Admin(args.user,args.portal,args.password) 52 | 53 | if args.folder!= None: 54 | fid = agoAdmin.getFolderID(args.folder) 55 | args.folder=fid 56 | folderid = '/' + args.folder 57 | 58 | agoAdmin.updateServiceItemsThumbnail(folderid) -------------------------------------------------------------------------------- /samples/updateUserRoles.py: -------------------------------------------------------------------------------- 1 | #### Update roles for users in CSV 2 | #### Example: 3 | #### updateUserRoles.py -u myuser -p mypassword -portal https://esri.maps.arcgis.com -file c:\temp\agolinput.csv 4 | 5 | import csv 6 | import argparse 7 | import sys 8 | 9 | from agoTools.admin import Admin 10 | from agoTools.admin import UsersAttributes 11 | from agoTools.admin import UserAttributes 12 | 13 | def _raw_input(prompt=None, stream=None, input=None): 14 | # A raw_input() replacement that doesn't save the string in the 15 | # GNU readline history. 16 | if not stream: 17 | stream = sys.stderr 18 | if not input: 19 | input = sys.stdin 20 | prompt = str(prompt) 21 | if prompt: 22 | stream.write(prompt) 23 | stream.flush() 24 | # NOTE: The Python C API calls flockfile() (and unlock) during readline. 25 | line = input.readline() 26 | if not line: 27 | raise EOFError 28 | if line[-1] == '\n': 29 | line = line[:-1] 30 | return line 31 | 32 | parser = argparse.ArgumentParser() 33 | parser.add_argument('-u', '--user') 34 | parser.add_argument('-p', '--password') 35 | parser.add_argument('-file', '--file') 36 | parser.add_argument('-portal', '--portal') 37 | 38 | args = parser.parse_args() 39 | inputFile = '' 40 | 41 | if args.file == None: 42 | args.file = _raw_input("CSV path: ") 43 | 44 | if args.user == None: 45 | args.user = _raw_input("Username:") 46 | 47 | if args.portal == None: 48 | args.portal = _raw_input("Portal: ") 49 | 50 | args.portal = str(args.portal).replace("http://","https://") 51 | 52 | agoAdmin = Admin(args.user,args.portal,args.password) 53 | 54 | if args.file != None: 55 | inputFile=args.file 56 | 57 | with open(inputFile) as input: 58 | dataReader = csv.DictReader(input) 59 | users=UsersAttributes(dataReader) 60 | 61 | agoAdmin.updateUserRoles(users) 62 | -------------------------------------------------------------------------------- /samples/updateWebMapVersionAGX.py: -------------------------------------------------------------------------------- 1 | #### Update web map version to 1.7x 2 | #### Allows for opening new web maps in ArcGIS Explorer 3 | #### Intended use is if you need to use AGX to create queries for iOS devices 4 | 5 | import sys 6 | sys.path.append('c:/scripts') # Directory where utilities.py is located. Change as needed 7 | 8 | from agoTools.utilities import Utilities 9 | 10 | agoUtilities = Utilities('') # Replace with your username. 11 | 12 | webmapId = '' # Replace with web map ID 13 | 14 | agoUtilities.updatewebmapversionAGX(webmapId) 15 | -------------------------------------------------------------------------------- /search-cheat-sheet.md: -------------------------------------------------------------------------------- 1 | # Advanced Search Cheat Sheet 2 | 3 | ## Contents 4 | * [Defaults](search-cheat-sheet.md#defaults) 5 | * [Modifiers](search-cheat-sheet.md#modifiers) 6 | * [Operators](search-cheat-sheet.md#operators) 7 | * [Search Fields](search-cheat-sheet.md#search-fields) 8 | * [Examples](search-cheat-sheet.md#examples) 9 | 10 | ## Defaults 11 | The default search looks across several fields for items and groups. The best match is always returned. 12 | #### Items 13 | The default fields are title, tags, snippet, description, accessinformation, spatialreference, type, and typekeywords. 14 | #### Groups 15 | The default fields are id, title, description, snippet, tags, and owner. 16 | 17 | ## Modifiers 18 | #### Wildcards 19 | Single and multiple character wildcard searches within single terms (not within phrase queries) are supported. The single character wildcard and the multiple character wildcard cannot be used in the same search. 20 | 21 | * Single character wildcard search: use the `?` symbol. 22 | * Multiple character wildcard search: use the `*` symbol. 23 | 24 | #### Ranges 25 | Range searches allow you to match a single field or multiple field values between the lower and upper bound. Range queries can be inclusive or exclusive of the upper and lower bounds. Inclusive range queries are denoted by square brackets. Exclusive range queries are denoted by curly brackets. 26 | 27 | #### Boosting a Term 28 | Boosting allows you to control the relevance of an item by boosting its term. To boost a term, use the `^` symbol with a boost factor (a number) at the end of the term you are searching. The higher the boost factor, the more relevant the term will be. 29 | 30 | ## Operators 31 | #### AND 32 | The `AND` operator performs matching where both terms exist in either the given field or the default fields. 33 | #### OR 34 | The `OR` operator links two terms and finds a match if either term exists. 35 | #### + 36 | Requires that the term after the `+` symbol exist somewhere in the given field or the default fields. 37 | #### NOT 38 | Excludes items that contain the term after `NOT`. This is equivalent to a difference using sets. 39 | ####\- 40 | Excludes items that contain the term after the `-` symbol. 41 | 42 | ## Search Fields 43 | ####id 44 | ID of the item, for example, `id:4e770315ad9049e7950b552aa1e40869` returns the item for that ID. 45 | ####itemtype 46 | Item type can be `url`, `text`, or `file`. 47 | ####owner 48 | Owner of the item, for example, `owner:esri` returns all content published by esri. 49 | ####uploaded 50 | The date uploaded, for example `uploaded: [0000001249084800000 TO 0000001249548000000]` finds all items published between August 1, 2009, 12:00AM to August 6, 2009 08:40AM. 51 | ####title 52 | Item title, for example, `title:"Southern California"` returns items with Southern California in the title. 53 | ####type 54 | Type returns the type of item and is a predefined field. See [Items and item types](http://resources.arcgis.com/en/help/arcgis-rest-api/02r3/02r3000000ms000000.htm) for a listing of the different types. For example, type:map returns items with map as the type, such as map documents and map services. 55 | ####typekeywords 56 | Type keywords, for example, typekeywords:tool returns items with the tool type keyword such as Network Analysis or geoprocessing services. See [Items and item types](http://resources.arcgis.com/en/help/arcgis-rest-api/02r3/02r3000000ms000000.htm) for a listing of the different types. 57 | ####description 58 | Item description, for example, `description:California` finds all items with the term "California" in the description. 59 | ####tags 60 | The tag field, for example, `tags:"San Francisco"` returns items tagged with the term "San Francisco". 61 | ####snippet 62 | Snippet or summary of the item, for example, `snippet:"natural resources"` returns items with "natural resources" in the snippet. 63 | ####extent 64 | The bounding rectangle of the item. For example, `extent: [-114.3458, 21.7518] - [-73.125, 44.0658]` returns items within that extent. 65 | ####spatialreference 66 | Spatial reference, for example, `spatialreference:102100` returns items in the Web Mercator Auxiliary Sphere projection. 67 | ####accessinformation 68 | Access information, for example, `accessinformation:esri` returns items with "esri" as the source credit. 69 | ####access 70 | The access field, for example, `access:public` returns public items. This field is predefined, and the options are `public`, `private`, `org`, or `shared`. You will only see private or shared items that you can access. 71 | ####group 72 | The ID of the group, for example, `group:1652a410f59c4d8f98fb87b25e0a2669` returns items within the given group. 73 | ####numratings 74 | Number of ratings, for example, `numratings:6` returns items with six ratings. 75 | ####numcomments 76 | Number of comments, for example, `numcomments:[1 TO 3]` returns items that have one to three comments. 77 | ####avgrating 78 | Average rating, for example, `avgrating:3.5` returns items with 3.5 as the average rating. 79 | ####culture 80 | Culture, for example, `culture:en-US`, returns the locale of the item. The search engine treats the two parts of the culture code as two different terms, and searches for individual languages can be done. For example, `culture:en` returns all records that have an "en" in their culture code. There may be overlaps between the codes used for language and the codes used for country, for instance `fr-FR`, but if the client needs to target a code with this problem, they can pass in the complete code. 81 | 82 | ## Examples 83 | * Find all items created between December 1, 2009, and December 9, 2009: `created: [0000001259692864000 TO 0000001260384065000]` 84 | * Find all items from the owners between arcgis_explorer and esri, not including arcgis_explorer and esri: `owner:{arcgis_explorer TO esri}` 85 | * Search for "recent fires" and make "fires" be more relevant: `recent fires^5` 86 | * Search for an item that contains the terms "recent" and "fires": `recent AND fires` 87 | * Search for an item that contains the terms "recent fires" or "fires": `"recent fires" OR fires` 88 | * Search for items that must contain "fires" and may contain "recent": `recent +fires` 89 | * Search for items that contain "California" but not "Imagery": `California NOT Imagery` 90 | 91 | [Source](http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#//02r3000000mn000000) 92 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | setup( 3 | name='agoTools', 4 | description="Administer ArcGIS Online Organizations with Python", 5 | url='www.github.com/Esri/ago-tools', 6 | packages=['agoTools'], 7 | package_dir={'':''}, 8 | license='Apache' 9 | ) --------------------------------------------------------------------------------