├── .gitignore ├── README.md └── demo ├── .vscode └── settings.json ├── auth.py ├── constants.py ├── course.py ├── datasource.py ├── membership.py ├── restdemo.py ├── term.py ├── trusted └── keytool_crt.pem └── user.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.ipynb 3 | 4 | *.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BBDN-REST_DEMO-Python 2 | This project contains sample code for demonstrating the Blackboard Learn REST APIs in Python. 3 | This sample code was built with Python 3.9.1. 4 | 5 | ### Project at a glance: 6 | - Target: Blackboard Learn SaaS Release 2015.11.0-ci.23+a9a4758 minimum 7 | - Source Release: v2.0 8 | - Release Date 2020-12-22 9 | - Author: shurrey 10 | - Tested on Blackboard Learn SaaS Release 3900.2.0-rel.34+4ad580a 11 | - Source Release: v1.0 12 | - Release Date 2016-02-24 13 | - Author: moneil 14 | - Tested on Blackboard Learn SaaS Release 2015.11.0-ci.23+a9a4758 15 | 16 | ### Requirements: 17 | - Python 3.9.1 18 | - Developer account - register at https://developer.blackboard.com 19 | - Test instance 20 | 21 | 22 | ### Setting Up Your Development Environment 23 | #### Python development tools 24 | You will first need to install Python 3.9.1. You can use tools like brew or ports to install on Mac, Powershell and the Marketplace on Windows, or run the installation manually. 25 | 26 | You may also install Python tools for your IDE or use a text editor and terminal to run the python code. 27 | 28 | 29 | ### Included Files 30 | restdemo.py - this is the main script
31 | auth.py - this script contains the code for authenticating the application and managing tokens
32 | datasouce.py - this script contains examples for each of the REST endpoints for managing Data Sources in Learn
33 | term.py - this script contains examples for each of the REST endpoints for managing Terms in Learn
34 | course.py - this script contains examples for each of the REST endpoints for managing Courses in Learn
35 | user.py - this script contains examples for each of the REST endpoints for managing Users in Learn
36 | membership.py - this script contains examples for each of the REST endpoints for managing Memberships in Learn Courses 37 | 38 | 39 | ### What it does 40 | The rest demo script demonstrates authenticating a REST application, management and use of the authorization token, and creating, updating, discovering, and deleting supported Learn objects. 41 | 42 | NOTE: Before running the example code you must register a developer account and application as described on the Developer Community Getting Started with REST and Managing REST Integrations in Learn: The REST Integrations Tool for System Administrators pages. You must also configure the script as outlined in the below Configure the Script section. 43 | 44 | When run with only a target URL the script will in the following order 45 | 1. Authenticate
46 | 2. Create, Read, and Update a Data Source
47 | 3. Create, Read, and Update a Term
48 | 4. Create, Read, and Update a Course
49 | 5. Create, Read, and Update a User
50 | 6. Create, Read, and Update a Membership
51 | 7. Delete created objects in reverse order of create - membership, user, course, term, datasource. 52 | 53 | When run with a specific command on an object only that operation will be run - you are responsible for system cleanup. 54 | 55 | All generated output is sent to the terminal (or IDE output window). 56 | 57 | e.g.: 58 | ``` 59 | $ python restdemo.py -h 60 | restdemo.py -t|--target -c|--command 61 | e.g.:$ restdemo.py -t www.myschool.edu -c create_course 62 | command: _ where is one of the following: 63 | create, read, update, delete 64 | and <object> is one of the following: 65 | datasource, term, course, user, membership 66 | -t is required; No -c args will run demo in predetermined order. 67 | -c commands require a valid datasource PK1 - 68 | a datasource get will be run in these cases, defaulting to create 69 | if the demo datasource does not exist. 70 | ``` 71 | 72 | For example: 73 | ``` 74 | $ python restdemo.py -t localhost:9877 75 | ``` 76 | Runs the full CRUD demo 77 | 78 | ``` 79 | $ python restdemo.py -t localhost:9877 -c create_datasource 80 | ``` 81 | Runs the datasource.py:createDataSource() demo code and does not clean up the remote system after running you must run: 82 | 83 | ``` 84 | $ python restdemo.py -t localhost:9877 -c delete_datasource 85 | ``` 86 | to remove the created data source. 87 |

88 | 89 | ## Running the Demo! 90 | ### Setup Your Test Server 91 | To run the demo if you have not already done so you must as outlined above register the application via the Developer Portal and add the application to your test environment using the REST API Integration tool. 92 | 93 | 94 | ### Configuring the Script 95 | Before executing the script to run against your test server you must configure it with your registered application's Key and Secret. 96 | 97 | Open auth.py and edit lines 37 and 38. 98 | On line 37 replace "insert_your_application_key_here" with the key issued when you registered your application.
99 | On line 38 replace "insert_your_application_secret_here" with the secret issued when you registered your application. 100 | 101 | Once you have setup your test server and changed auth.py to reflect your application's key and secret you may run the command line tools as outlined above or via your IDE. 102 | 103 | 104 | ### Conclusion 105 | For a thorough walkthrough of this code, visit the corresponding Blackboard Developer Community documentation REST Demo Using Python. 106 | -------------------------------------------------------------------------------- /demo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | } -------------------------------------------------------------------------------- /demo/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016, Blackboard Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of Blackboard Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | """ 10 | 11 | """ 12 | PYTHON and Self-signed certificates: 13 | To enable code to recognize the DVM self signed certificate you have to provide Python 14 | with the public Key 15 | 16 | 1. Start the DVM and ssh into it... 17 | $ vagrant up 18 | $ vagrant ssh 19 | 2. Copy the keystore password from the config file 20 | $ cd /usr/local/blackboard/config 21 | $ more /usr/local/blackboard/config/bb-config.properties 22 | find the keystore password and copy it - you will use this in step 3 for 23 | exporting the pem file 24 | 3. Export the cert and keys 25 | $ cd /usr/local/blackboard/config/keystores 26 | $ keytool -exportcert -alias tomcat -keystore tomcat.keystore -rfc -file /vagrant/keytool_crt.pem 27 | 4. Make sure you can read the file: 28 | $ openssl x509 -in /vagrant/keytool_crt.pem -inform pem -noout -text 29 | 4. copy the .pem file to a location your project can access it. 30 | 5. update config.py to point to your .pem file 31 | 6. add dev.bbdn.local to your hosts file and save: 32 | 127.0.0.1 localhost dev.bbdn.local 33 | 7. test access to to your DVM using dev.bbdn.local, if it doesn't work restart and retry 34 | 35 | now for all requests against your DVM the pem will be used to verify the server 36 | NOTE: that for production code you should change 'verify=CERTPATH' to 'verify=False' if 37 | not using the server derived .pem file, or remove 'verify=...' completely if using 38 | commercial certificates. 39 | see http://docs.python-requests.org/en/master/user/advanced/ for more information on 40 | SSL sessions in Python. 41 | """ 42 | 43 | import json 44 | import requests 45 | from requests.adapters import HTTPAdapter 46 | from requests.packages.urllib3.poolmanager import PoolManager 47 | import datetime 48 | import time 49 | #from datetime import now 50 | import ssl 51 | import sys 52 | from constants import * 53 | 54 | #Tls1Adapter allows for connection to sites with non-CA/self-signed 55 | # certificates e.g.: Learn Dev VM 56 | # May be removed if you migrated the cert as outlined in auth.py 57 | class Tls1Adapter(HTTPAdapter): 58 | def init_poolmanager(self, connections, maxsize, block=False): 59 | self.poolmanager = PoolManager(num_pools=connections, 60 | maxsize=maxsize, 61 | block=block, 62 | ssl_version=ssl.PROTOCOL_TLSv1) 63 | 64 | class AuthToken(): 65 | target_url = '' 66 | 67 | def __init__(self, URL): 68 | 69 | self.KEY = "insert_your_application_key_here" 70 | self.SECRET = "insert_your_application_secret_here" 71 | 72 | #self.SECRET = "biExBJNI1IXiBBXpV8g01JJJjmXKHSg7" #Example Only. Change to your secret 73 | #self.KEY = "9cb9384a-3662-410d-9953-fe73cc374b81"#Example Only. Change to your key 74 | 75 | self.CREDENTIALS = 'client_credentials' 76 | self.PAYLOAD = { 77 | 'grant_type':'client_credentials' 78 | } 79 | self.TOKEN = None 80 | self.target_url = URL 81 | self.EXPIRES_AT = '' 82 | self.cert_path='./trusted/keytool_crt.pem' 83 | 84 | def getKey(self): 85 | return self.KEY 86 | 87 | def getSecret(self): 88 | return self.SECRET 89 | 90 | def setToken(self): 91 | oauth_path = '/learn/api/public/v1/oauth2/token' 92 | OAUTH_URL = 'https://' + self.target_url + oauth_path 93 | 94 | if self.TOKEN is None: 95 | session = requests.session() 96 | #session.mount('https://', Tls1Adapter()) # remove for production 97 | 98 | # Authenticate 99 | print("[auth:setToken] POST Request URL: " + OAUTH_URL) 100 | print("[auth:setToken] JSON Payload: \n" + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 101 | #r = requests.post(url, data=data, verify='/path/to/public_key.pem') 102 | r = session.post(OAUTH_URL, data=self.PAYLOAD, auth=(self.KEY, self.SECRET), verify=False) 103 | # r = session.post(OAUTH_URL, data=self.PAYLOAD, auth=(self.KEY, self.SECRET), verify=CERTPATH) 104 | 105 | print("[auth:setToken()] STATUS CODE: " + str(r.status_code) ) 106 | #strip quotes from result for better dumps 107 | res = json.loads(r.text) 108 | print("[auth:setToken()] RESPONSE: \n" + json.dumps(res,indent=4, separators=(',', ': '))) 109 | 110 | if r.status_code == 200: 111 | parsed_json = json.loads(r.text) 112 | self.TOKEN = parsed_json['access_token'] 113 | self.EXPIRES = parsed_json['expires_in'] 114 | m, s = divmod(self.EXPIRES, 60) 115 | #h, m = divmod(m, 60) 116 | #print "%d:%02d:%02d" % (h, m, s) 117 | self.NOW = datetime.datetime.now() 118 | self.EXPIRES_AT = self.NOW + datetime.timedelta(seconds = s, minutes = m) 119 | print ("[auth:setToken()] Token Expires at " + self.EXPIRES_AT.strftime("%H:%M:%S")) 120 | 121 | print ("[auth:setToken()] TOKEN: " + self.TOKEN) 122 | 123 | #there is the possibility the reaquired token may expire 124 | #before we are done so perform expiration sanity check... 125 | if self.isExpired(self.EXPIRES_AT): 126 | self.setToken() 127 | 128 | else: 129 | print("[auth:setToken()] ERROR") 130 | else: 131 | print ("[auth:setToken()] TOKEN set") 132 | 133 | def getToken(self): 134 | #if token time is less than a one second then 135 | # print that we are pausing to clear 136 | # re-auth and return the new token 137 | if self.isExpired(self.EXPIRES_AT): 138 | self.setToken() 139 | 140 | return self.TOKEN 141 | 142 | def revokeToken(self): 143 | revoke_path = '/learn/api/public/v1/oauth2/revoke' 144 | revoke_URL = 'https://' + self.target_url + revoke_path 145 | 146 | print("[auth:revokeToken()] KEY: " + self.KEY) 147 | print("[auth:revokeToken()] SECRET: " + self.SECRET) 148 | print("[auth:revokeToken()] TOKEN: " + self.TOKEN) 149 | print("[auth:revokeToken()] revoke_URL: " + revoke_URL) 150 | self.PAYLOAD = { 151 | 'token':self.TOKEN 152 | } 153 | 154 | if self.TOKEN != '': 155 | print("[auth:revokeToken()] TOKEN not empty...able to revoke") 156 | print("[auth:revokeToken()] POST PAYLOAD: ") 157 | for keys,values in self.PAYLOAD.items(): 158 | print("\t\t\t" + keys + ":" + values) 159 | session = requests.session() 160 | session.mount('https://', Tls1Adapter()) # remove for production 161 | 162 | # revoke token 163 | print("[auth:revokeToken] Request URL: " + revoke_URL) 164 | print("[auth:revokeToken] JSON Payload: \n " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 165 | r = session.post(revoke_URL, data=self.PAYLOAD, auth=(self.KEY, self.SECRET), verify=CERTPATH) 166 | 167 | print("[auth:revokeToken()] STATUS CODE: " + str(r.status_code) ) 168 | print("[auth:revokeToken()] RESPONSE: " + r.text) 169 | 170 | if r.status_code == 200: 171 | print("[auth:revokeToken()] Token Revoked") 172 | else: 173 | print("[auth] ERROR on token revoke") 174 | else: 175 | print ("[auth:revokeToken()] Must have set a token to revoke a token...") 176 | 177 | 178 | def isExpired(self, expiration_datetime): 179 | expired = False 180 | print ("[auth:isExpired()] Token Expires at " + expiration_datetime.strftime("%H:%M:%S")) 181 | 182 | time_left = (expiration_datetime - datetime.datetime.now()).total_seconds() 183 | print ("[auth:isExpired()] Time Left on Token (in seconds): " + str(time_left)) 184 | if time_left < 1: 185 | print ("[auth:isExpired()] Token almost expired retrieving new token in two seconds.") 186 | time.sleep( 1 ) 187 | expired = True 188 | 189 | return expired 190 | -------------------------------------------------------------------------------- /demo/constants.py: -------------------------------------------------------------------------------- 1 | # constants file for BBDN-REST_DEMO-Python 2 | 3 | #IDs for create 4 | DSKEXTERNALID = 'BBDN-PYTHON-REST-DEMO-DSK' 5 | COURSEEXTERNALID = 'BBDN-PYTHON-REST-DEMO-COURSE' 6 | TERMEXTERNALID = 'BBDN-PYTHON-REST-DEMO-TERM' 7 | USEREXTERNALID = 'BBDN-PYTHON-REST-DEMO-USER' 8 | PAGINATIONLIMIT = 5 9 | COURSEGETFIELDS = 'externalId,courseId,name,availability,id' 10 | USERGETFIELDS = 'externalId,name' 11 | TERMSGETFIELDS = 'externalId,name' 12 | DSKSGETFIELDS = 'externalId,name' 13 | CRSMEMBERSHIPGETFIELDS = "externalId,availability" 14 | USRMEMBERSHIPGETFIELDS = "externalId,availability" 15 | CERTPATH = "./trusted/keytool_crt.pem" 16 | 17 | -------------------------------------------------------------------------------- /demo/course.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016, Blackboard Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of Blackboard Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | """ 10 | 11 | import json 12 | import html 13 | import requests 14 | from requests.adapters import HTTPAdapter 15 | from requests.packages.urllib3.poolmanager import PoolManager 16 | import ssl 17 | import sys 18 | from constants import * 19 | 20 | requests.packages.urllib3.disable_warnings() 21 | 22 | #Tls1Adapter allows for connection to sites with non-CA/self-signed 23 | # certificates e.g.: Learn Dev VM 24 | # May be removed if you migrated the cert as outlined in auth.py 25 | class Tls1Adapter(HTTPAdapter): 26 | def init_poolmanager(self, connections, maxsize, block=False): 27 | self.poolmanager = PoolManager(num_pools=connections, 28 | maxsize=maxsize, 29 | block=block, 30 | ssl_version=ssl.PROTOCOL_TLSv1) 31 | 32 | class Course(): 33 | 34 | def __init__(self, target_url, token): 35 | self.target_url = target_url 36 | self.token = token 37 | self.courses_Path = '/learn/api/public/v1/courses' #create(POST)/get(GET) 38 | self.course_Path = '/learn/api/public/v1/courses/externalId:' 39 | self.courses_Path_Params = "?limit=%s&fields=%s" % (PAGINATIONLIMIT, COURSEGETFIELDS) 40 | self.termId = None 41 | 42 | 43 | 44 | 45 | def execute(self, command, dsk, token): 46 | if "create" in command: 47 | print('[Course:execute] : ' + command) 48 | self.createCourse(dsk, token) 49 | elif "read_all" in command: 50 | print('[Course:execute] : ' + command) 51 | self.getCourses(token) 52 | elif "read" in command: 53 | print('[Course:execute] : ' + command) 54 | self.getCourse(token) 55 | elif "update" in command: 56 | print('[Course:execute] : ' + command) 57 | self.updateCourse(dsk, token) 58 | elif "delete" in command: 59 | print('[Course:execute] : ' + command) 60 | self.deleteCourse(token) 61 | 62 | 63 | def getCourses(self, token): 64 | #demo limits returned page count to constants.PAGINATIONLIMIT and 65 | #limits result data fields to constants.COURSEGETFIELDS 66 | print('[Course:getCourses()] token: ' + token) 67 | #"Authorization: Bearer $token" 68 | authStr = 'Bearer ' + token 69 | print('[Course:getCourses()] authStr: ' + authStr) 70 | #session = requests.session() 71 | #session.mount('https://', Tls1Adapter()) # remove for production 72 | 73 | nextPage = True 74 | nextpageURL = None 75 | while nextPage: 76 | print ("[Course:getCourses()] ENTERING WHILE LOOP FOR NEXT PAGE CHECK") 77 | print ("[Course:getCourses()] NEXTPAGE: %s" % nextPage) 78 | print ("[Course:getCourses()] NEXTPAGEURL: %s" % nextpageURL) 79 | if nextpageURL: 80 | print ("[Course:getCourses()] NEXTPAGE: %s, so update URL parameters." % nextPage) 81 | self.courses_Path_Params = nextpageURL.replace(self.courses_Path, '') 82 | print ("[Course:getCourses()] UPDATED URL PARAMS: %s" %self.courses_Path_Params) 83 | print("[Course:getCourses()] GET Request URL: https://" + self.target_url + self.courses_Path + self.courses_Path_Params) 84 | print("[Course:getCourses()] JSON Payload: NONE REQUIRED") 85 | r = requests.get("https://" + self.target_url + self.courses_Path + self.courses_Path_Params, headers={'Authorization':authStr}, verify=False) 86 | 87 | print("[Course:getCourses()] STATUS CODE: " + str(r.status_code) ) 88 | print("[Course:getCourses()] RESPONSE:") 89 | if r.text: 90 | res = json.loads(r.text) 91 | print(json.dumps(res,indent=4, separators=(',', ': '))) 92 | try: 93 | nextpageURL = res['paging']['nextPage'] 94 | nextPage=True 95 | #continue to process records here before retrieving more 96 | except KeyError as err: 97 | nextPage=False 98 | nextpageURL=None 99 | print ("[Course:getCourses()] No (more) records.") 100 | else: 101 | print("NONE") 102 | 103 | 104 | 105 | def createCourse(self, dsk, token): 106 | #"Authorization: Bearer $token" 107 | authStr = 'Bearer ' + token 108 | 109 | self.PAYLOAD = { 110 | "externalId": COURSEEXTERNALID, 111 | "dataSourceId": "externalId:%s" % DSKEXTERNALID, 112 | "courseId": COURSEEXTERNALID, 113 | "name":"Course used for REST demo", 114 | "description":"Course used for REST demo", 115 | "allowGuests":"true", 116 | "readOnly": "false", 117 | "availability": { 118 | "duration":"continuous" 119 | } 120 | } 121 | 122 | #session = requests.session() 123 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 124 | print("[Course:createCourse()] POST Request URL: https://" + self.target_url + self.courses_Path) 125 | print("[Courses:createCourse()] JSON Payload: \n " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 126 | r = requests.post("https://" + self.target_url + self.courses_Path, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 127 | print("[Course:createCourse()] STATUS CODE: " + str(r.status_code) ) 128 | print("[Course:createCourse()] RESPONSE:") 129 | if r.text: 130 | res = json.loads(r.text) 131 | print(json.dumps(res,indent=4, separators=(',', ': '))) 132 | else: 133 | print("NONE") 134 | 135 | def getCourse(self, token): 136 | print('[Course:getCourse()] token: ' + token) 137 | #"Authorization: Bearer $token" 138 | authStr = 'Bearer ' + token 139 | print('[Course:getCourses] authStr: ' + authStr) 140 | #session = requests.session() 141 | #session.mount('https://', Tls1Adapter()) # remove for production 142 | print("[Course:getCourse()] GET Request URL: https://" + self.target_url + self.course_Path + COURSEEXTERNALID) 143 | 144 | r = requests.get("https://" + self.target_url + self.course_Path+COURSEEXTERNALID, headers={'Authorization':authStr}, verify=False) 145 | 146 | print("[Course:getCourse()] STATUS CODE: " + str(r.status_code) ) 147 | print("[Course:getCourse()] RESPONSE:") 148 | if r.text: 149 | res = json.loads(r.text) 150 | print(json.dumps(res,indent=4, separators=(',', ': '))) 151 | else: 152 | print("NONE") 153 | done = False 154 | 155 | def updateCourse(self, dsk, token): 156 | #"Authorization: Bearer $token" 157 | authStr = 'Bearer ' + token 158 | print("[Course:updateCourse()] COURSEEXTERNALID: " + COURSEEXTERNALID) 159 | 160 | self.PAYLOAD = { 161 | "externalId":COURSEEXTERNALID, 162 | "dataSourceId": "externalId:%s" % DSKEXTERNALID, 163 | "courseId":COURSEEXTERNALID, 164 | "name":"Course used for REST Python demo", 165 | "description":"Course used for REST Python demo", 166 | "allowGuests":"true", 167 | "readOnly": "false", 168 | #"termId":constants.TERMEXTERNALID, 169 | "availability": { 170 | "available":"Yes", 171 | "duration":"continuous" 172 | } 173 | } 174 | #session = requests.session() 175 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 176 | print("[Course:updateCourse()] PATCH Request URL: https://" + self.target_url + self.course_Path + COURSEEXTERNALID) 177 | print("[Courses:updateCourse()] Result: \n " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 178 | r = requests.patch("https://" + self.target_url + self.course_Path+COURSEEXTERNALID, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 179 | 180 | print("[Course:updateCourse()] STATUS CODE: " + str(r.status_code) ) 181 | print("[Course:updateCourse()] RESPONSE:") 182 | if r.text: 183 | res = json.loads(r.text) 184 | print(json.dumps(res,indent=4, separators=(',', ': '))) 185 | else: 186 | print("NONE") 187 | 188 | def deleteCourse(self, token): 189 | #"Authorization: Bearer $token" 190 | authStr = 'Bearer ' + token 191 | print("[Course:deleteCourse()] COURSEEXTERNALID: " + COURSEEXTERNALID) 192 | 193 | #session = requests.session() 194 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 195 | print("[Course:deleteCourse()] DELETE Request URL: https://" + self.target_url + self.course_Path + COURSEEXTERNALID) 196 | print("[Courses:deleteCourse()] JSON Payload: NONE REQUIRED") 197 | r = requests.delete("https://" + self.target_url + self.course_Path+COURSEEXTERNALID, headers={'Authorization':authStr}, verify=False) 198 | 199 | print("[Course:deleteCourse()] STATUS CODE: " + str(r.status_code) ) 200 | print("[Course:deleteCourse()] RESPONSE:") 201 | if r.text: 202 | res = json.loads(r.text) 203 | print(json.dumps(res,indent=4, separators=(',', ': '))) 204 | else: 205 | print("NONE") 206 | -------------------------------------------------------------------------------- /demo/datasource.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016, Blackboard Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of Blackboard Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | """ 10 | 11 | from constants import * 12 | import json 13 | import requests 14 | from requests.adapters import HTTPAdapter 15 | from requests.packages.urllib3.poolmanager import PoolManager 16 | import ssl 17 | import sys 18 | from constants import * 19 | 20 | 21 | requests.packages.urllib3.disable_warnings() 22 | 23 | #Tls1Adapter allows for connection to sites with non-CA/self-signed 24 | # certificates e.g.: Learn Dev VM 25 | # May be removed if you migrated the cert as outlined in auth.py 26 | class Tls1Adapter(HTTPAdapter): 27 | def init_poolmanager(self, connections, maxsize, block=False): 28 | self.poolmanager = PoolManager(num_pools=connections, 29 | maxsize=maxsize, 30 | block=block, 31 | ssl_version=ssl.PROTOCOL_TLSv1) 32 | 33 | class DataSource(): 34 | 35 | 36 | def __init__(self, target_url, token): 37 | self.target_url = target_url 38 | self.token = token 39 | self.datasource_PK1 = None 40 | self.DATASOURCES_PATH = '/learn/api/public/v1/dataSources' #create(POST)/get(GET) 41 | self.DATASOURCE_PATH = '/learn/api/public/v1/dataSources/externalId:' 42 | self.DATASOURCES_PATH_Params = "?limit=%s&fields=%s" % (PAGINATIONLIMIT, DSKSGETFIELDS) 43 | 44 | 45 | def execute(self, command, token): 46 | 47 | if "create" in command: 48 | print('[DataSource:execute] : ' + command) 49 | self.createDataSource(token) 50 | elif "read_all" in command: 51 | print('[DataSource:execute] : ' + command) 52 | self.getDataSources(token) 53 | elif "read" in command: 54 | print('[DataSource:execute] : ' + command) 55 | self.getDataSource(token) 56 | elif "update" in command: 57 | print('[DataSource:execute] : ' + command) 58 | self.updateDataSource(token) 59 | elif "delete" in command: 60 | print('[DataSource:execute] : ' + command) 61 | self.deleteDataSource(token) 62 | 63 | 64 | def getDataSources(self, token): 65 | print('[DataSource:getDataSources] token: ' + token) 66 | #"Authorization: Bearer $token" 67 | authStr = 'Bearer ' + token 68 | print('[DataSource:getDataSources] authStr: ' + authStr) 69 | #session = requests.session() 70 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 71 | 72 | nextPage = True 73 | nextpageURL = None 74 | while nextPage: 75 | print ("[DataSource:getDataSources()] ENTERING WHILE LOOP FOR NEXT PAGE CHECK") 76 | print ("[DataSource:getDataSources()] NEXTPAGE: %s" % nextPage) 77 | print ("[DataSource:getDataSources()] NEXTPAGEURL: %s" % nextpageURL) 78 | if nextpageURL: 79 | print ("[DataSource:getDataSources()] NEXTPAGE: %s, so update URL parameters." % nextPage) 80 | self.DATASOURCES_PATH_Params = nextpageURL.replace(self.DATASOURCES_PATH_Params, '') 81 | print ("[DataSource:getDataSources()] UPDATED URL PARAMS: %s" %self.DATASOURCES_PATH_Params) 82 | print("[DataSource:getDataSources()] GET Request URL: https://" + self.target_url + self.DATASOURCES_PATH + self.DATASOURCES_PATH_Params) 83 | print("[DataSource:getDataSources()] JSON Payload: NONE REQUIRED") 84 | r = requests.get("https://" + self.target_url + self.DATASOURCES_PATH + self.DATASOURCES_PATH_Params, headers={'Authorization':authStr}, verify=False) 85 | 86 | print("[DataSource:getDataSources()] STATUS CODE: " + str(r.status_code) ) 87 | print("[DataSource:getDataSources()] RESPONSE:") 88 | if r.text: 89 | res = json.loads(r.text) 90 | print(json.dumps(res,indent=4, separators=(',', ': '))) 91 | try: 92 | nextpageURL = res['paging']['nextPage'] 93 | nextPage=True 94 | #continue to process records here before retrieving more 95 | except KeyError as err: 96 | nextPage=False 97 | nextpageURL=None 98 | print ("[DataSource:getDataSources()] No (more) records.") 99 | else: 100 | print("NONE") 101 | 102 | 103 | def createDataSource(self, token): 104 | #"Authorization: Bearer $token" 105 | authStr = "Bearer " + token 106 | self.PAYLOAD = {"externalId":"%s" % DSKEXTERNALID, "description":"Data Source used for REST demo"} 107 | 108 | #session = requests.session() 109 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 110 | 111 | print("[DataSource:createDataSource()] POST Request URL: https://" + self.target_url + self.DATASOURCES_PATH) 112 | print("[DataSource:createDataSource()] JSON Payload: \n" + json.dumps(self.PAYLOAD,indent=4, separators=(',', ': '))) 113 | 114 | try: 115 | print("1") 116 | r = requests.post("https://" + self.target_url + self.DATASOURCES_PATH, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 117 | print("[DataSource:createDataSource()] STATUS CODE: " + str(r.status_code) ) 118 | print("[DataSource:createDataSource()] RESPONSE:") 119 | if r.text: 120 | print("2") 121 | res = json.loads(r.text) 122 | print(json.dumps(res,indent=4, separators=(',', ': '))) 123 | parsed_json = json.loads(r.text) 124 | self.datasource_PK1 = parsed_json['externalId'] 125 | print ("[DataSource:createDataSource()] datasource_PK1:" + self.datasource_PK1) 126 | print ("[DataSource:createDataSource()] datasource_externalId:" + parsed_json['externalId']) 127 | else: 128 | print("NONE") 129 | 130 | print("3") 131 | 132 | if r.status_code == 429: 133 | print("[datasource:getDataSource] Error 429 Too Many Requests. Exiting.") 134 | sys.exit(2) 135 | except requests.RequestException as re: 136 | print("[datasource:createDataSource()] Error cannot connect to requested server. Exiting.\n" + str(re)) 137 | sys.exit(2) 138 | 139 | def getDataSource(self, token): 140 | #print('[DataSource:getDataSource()] token: ' + token) 141 | #"Authorization: Bearer $token" 142 | authStr = 'Bearer ' + token 143 | print('[DataSource:getDataSource()] authStr: ' + authStr) 144 | #session = requests.session() 145 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 146 | 147 | print("[DataSource:getDataSource()] GET Request URL: https://" + self.target_url + self.DATASOURCE_PATH+DSKEXTERNALID) 148 | print("[DataSource:getDataSource()] JSON Payload: NONE REQUIRED") 149 | r = requests.get("https://" + self.target_url + self.DATASOURCE_PATH+DSKEXTERNALID, headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 150 | 151 | print("[DataSource:getDataSource()] STATUS CODE: " + str(r.status_code) ) 152 | print("[DataSource:getDataSource()] RESPONSE:") 153 | if r.text: 154 | res = json.loads(r.text) 155 | print(json.dumps(res,indent=4, separators=(',', ': '))) 156 | else: 157 | print("NONE") 158 | 159 | if r.status_code == 200: 160 | parsed_json = json.loads(r.text) 161 | self.datasource_PK1 = parsed_json['id'] 162 | print ("[DataSource:getDataSource()] datasource_PK1:" + self.datasource_PK1) 163 | 164 | 165 | 166 | def updateDataSource(self, token): 167 | #"Authorization: Bearer $token" 168 | authStr = 'Bearer ' + token 169 | print("[DataSource:updateDataSource()] DSKEXTERNALID: " + DSKEXTERNALID) 170 | 171 | self.PAYLOAD = {"description":"Demo Data Source used for REST Python Demo"} 172 | 173 | #session = requests.session() 174 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 175 | 176 | print("[DataSource:updateDataSource()] PATCH Request URL: https://" + self.target_url + self.DATASOURCE_PATH + DSKEXTERNALID) 177 | print("[DataSource:updateDataSource()] JSON Payload: \n" + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 178 | r = requests.patch("https://" + self.target_url + self.DATASOURCE_PATH+DSKEXTERNALID, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 179 | 180 | print("[DataSource:updateDataSource()] STATUS CODE: " + str(r.status_code) ) 181 | print("[DataSource:updateDataSource()] RESPONSE:") 182 | if r.text: 183 | res = json.loads(r.text) 184 | print(json.dumps(res,indent=4, separators=(',', ': '))) 185 | else: 186 | print("NONE") 187 | 188 | 189 | def deleteDataSource(self, token): 190 | #"Authorization: Bearer $token" 191 | authStr = 'Bearer ' + token 192 | print("[DataSource:deleteDataSource()] DSKEXTERNALID: " + DSKEXTERNALID) 193 | 194 | #session = requests.session() 195 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 196 | 197 | print("[DataSource:deleteDataSource()] DELETE Request URL: https://" + self.target_url + self.DATASOURCE_PATH + DSKEXTERNALID) 198 | print("[DataSource:deleteDataSource()] JSON Payload: NONE REQUIRED") 199 | r = requests.delete("https://" + self.target_url + self.DATASOURCE_PATH+DSKEXTERNALID, headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 200 | 201 | print("[DataSource:deleteDataSource()] STATUS CODE: " + str(r.status_code) ) 202 | print("[DataSource:deleteDataSource()] RESPONSE:") 203 | if r.text: 204 | res = json.loads(r.text) 205 | print(json.dumps(res,indent=4, separators=(',', ': '))) 206 | else: 207 | print("NONE") 208 | 209 | def checkDataSource(self, token): 210 | self.getDataSource(token) 211 | if not self.datasource_PK1: 212 | print("[datasource::checkDataSource] Data Source %s does not exist, creating." % DSKEXTERNALID) 213 | self.createDataSource(token) 214 | -------------------------------------------------------------------------------- /demo/membership.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016, Blackboard Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | Neither the name of Blackboard Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | """ 14 | 15 | import json 16 | import requests 17 | from requests.adapters import HTTPAdapter 18 | from requests.packages.urllib3.poolmanager import PoolManager 19 | import ssl 20 | import sys 21 | from constants import * 22 | 23 | requests.packages.urllib3.disable_warnings() 24 | 25 | #Tls1Adapter allows for connection to sites with non-CA/self-signed 26 | # certificates e.g.: Learn Dev VM 27 | # May be removed if you migrated the cert as outlined in auth.py 28 | class Tls1Adapter(HTTPAdapter): 29 | def init_poolmanager(self, connections, maxsize, block=False): 30 | self.poolmanager = PoolManager(num_pools=connections, 31 | maxsize=maxsize, 32 | block=block, 33 | ssl_version=ssl.PROTOCOL_TLSv1) 34 | 35 | class Membership(): 36 | 37 | def __init__(self, target_url, token): 38 | self.target_url = target_url 39 | self.token = token 40 | self.memberships_Path = '/learn/api/public/v1/courses/courseId/users' #create(POST)/get(GET) 41 | self.membership_Path = '/learn/api/public/v1/courses/courseId/users/userId' 42 | self.userMembships_Path = '/learn/api/public/v1/users/userId/courses' 43 | self.usermemberships_Path_Params = "?limit=%s&fields=%s" % (PAGINATIONLIMIT, USRMEMBERSHIPGETFIELDS) 44 | self.coursememberships_Path_Params = "?limit=%s&fields=%s" % (PAGINATIONLIMIT, CRSMEMBERSHIPGETFIELDS) 45 | 46 | 47 | def execute(self, command, dsk, token): 48 | if "create" in command: 49 | print('[Membership:execute] : ' + command) 50 | self.createMembership(dsk, token) 51 | elif "read_all_user_memberships" in command: 52 | print ('[Membership:execute] : ' + command) 53 | self.getUserMemberships(token) 54 | elif "read_all_course_memberships" in command: 55 | print('[Membership:execute] : ' + command) 56 | self.getCourseMemberships(token) 57 | elif "read" in command: 58 | print('[Membership:execute] : ' + command) 59 | self.getMembership(token) 60 | elif "update" in command: 61 | print('[Membership:execute] : ' + command) 62 | self.updateMembership(dsk, token) 63 | elif "delete" in command: 64 | print('[Membership:execute] : ' + command) 65 | self.deleteMembership(token) 66 | 67 | 68 | def getCourseMemberships(self, token): 69 | #GET /learn/api/public/v1/courses/{courseId}/users 70 | #prints all the memberships for a specific course 71 | 72 | print('[Membership:getCourseMemberships] token: ' + token) 73 | #"Authorization: Bearer $token" 74 | authStr = 'Bearer ' + token 75 | print('[Membership:getCourseMemberships] authStr: ' + authStr) 76 | #session = requests.session() 77 | #session.mount('https://', Tls1Adapter()) # remove for production 78 | 79 | replacement = "externalId:"+COURSEEXTERNALID 80 | memberships_Path = self.memberships_Path 81 | memberships_Path = memberships_Path.replace("courseId", replacement) 82 | 83 | 84 | nextPage = True 85 | nextpageURL = None 86 | while nextPage: 87 | print ("[Membership:getCourseMemberships()] ENTERING WHILE LOOP FOR NEXT PAGE CHECK") 88 | print ("[Membership:getCourseMemberships()] NEXTPAGE: %s" % nextPage) 89 | print ("[Membership:getCourseMemberships()] NEXTPAGEURL: %s" % nextpageURL) 90 | if nextpageURL: 91 | print ("[Membership:getCourseMemberships()] NEXTPAGE: %s, so update URL parameters." % nextPage) 92 | self.coursememberships_Path_Params = nextpageURL.replace(self.memberships_Path, '') 93 | print ("[Membership:getCourseMemberships()] UPDATED URL PARAMS: %s" %self.coursememberships_Path_Params) 94 | print("[Membership:getCourseMemberships()] GET Request URL: https://" + self.target_url + self.memberships_Path + self.coursememberships_Path_Params) 95 | print("[Membership:getCourseMemberships()] JSON Payload: NONE REQUIRED") 96 | #r = session.get("https://" + self.target_url + self.memberships_Path + self.memberships_Path_Params, headers={'Authorization':authStr}, verify=False) 97 | r = requests.get("https://" + self.target_url + self.memberships_Path + self.coursememberships_Path_Params, headers={'Authorization':authStr}, verify=False) 98 | 99 | print("[Membership:getCourseMemberships()] STATUS CODE: " + str(r.status_code) ) 100 | print("[Membership:getCourseMemberships()] RESPONSE:") 101 | if r.text: 102 | res = json.loads(r.text) 103 | print(json.dumps(res,indent=4, separators=(',', ': '))) 104 | try: 105 | nextpageURL = res['paging']['nextPage'] 106 | nextPage=True 107 | #continue to process records here before retrieving more 108 | except KeyError as err: 109 | nextPage=False 110 | nextpageURL=None 111 | print ("[Membership:getCourseMemberships()] No (more) records.") 112 | else: 113 | print("NONE") 114 | 115 | 116 | 117 | # print("[Membership:getMemberships()] GET Request URL: https://" + self.target_url + memberships_Path) 118 | # print("[Membership:getMemberships()] JSON Payload: NONE REQUIRED") 119 | # r = session.get("https://" + self.target_url + memberships_Path, headers={'Authorization':authStr}, verify=False) 120 | # print("[Membership:getMemberships()] STATUS CODE: " + str(r.status_code) ) 121 | # print("[Membership:getMemberships()] RESPONSE:") 122 | # if r.text: 123 | # res = json.loads(r.text) 124 | # print(json.dumps(res,indent=4, separators=(',', ': '))) 125 | # else: 126 | # print("NONE") 127 | 128 | def getUserMemberships(self, token): 129 | #GET /learn/api/public/v1/users/{userId}/courses 130 | #prints all the memberships for a specific user 131 | 132 | print('[Membership:getCourseMemberships] token: ' + token) 133 | #"Authorization: Bearer $token" 134 | authStr = 'Bearer ' + token 135 | print('[Membership:getCourseMemberships] authStr: ' + authStr) 136 | #session = requests.session() 137 | #session.mount('https://', Tls1Adapter()) # remove for production 138 | 139 | replacement = "externalId:"+USEREXTERNALID 140 | userMembships_Path = self.userMembships_Path 141 | userMembships_Path = userMembships_Path.replace("userId", replacement) 142 | 143 | 144 | 145 | print ("Membership:getUsersMemberships() not yet implemented.") 146 | nextPage = True 147 | nextpageURL = None 148 | while nextPage: 149 | print ("[Membership:getUserMemberships()] ENTERING WHILE LOOP FOR NEXT PAGE CHECK") 150 | print ("[Membership:getUserMemberships()] NEXTPAGE: %s" % nextPage) 151 | print ("[Membership:getUserMemberships()] NEXTPAGEURL: %s" % nextpageURL) 152 | if nextpageURL: 153 | print ("[Membership:getUserMemberships()] NEXTPAGE: %s, so update URL parameters." % nextPage) 154 | self.courses_Path_Params = nextpageURL.replace(self.memberships_Path, '') 155 | print ("[Membership:getUserMemberships()] UPDATED URL PARAMS: %s" %self.usermemberships_Path_Params) 156 | print("[Membership:getUserMemberships()] GET Request URL: https://" + self.target_url + self.memberships_Path + self.usermemberships_Path_Params) 157 | print("[Membership:getUserMemberships()] JSON Payload: NONE REQUIRED") 158 | #r = session.get("https://" + self.target_url + self.memberships_Path + self.memberships_Path_Params, headers={'Authorization':authStr}, verify=False) 159 | r = requests.get("https://" + self.target_url + self.memberships_Path + self.usermemberships_Path_Params, headers={'Authorization':authStr}, verify=False) 160 | 161 | print("[Membership:getUserMemberships()] STATUS CODE: " + str(r.status_code) ) 162 | print("[Membership:getUserMemberships()] RESPONSE:") 163 | if r.text: 164 | res = json.loads(r.text) 165 | print(json.dumps(res,indent=4, separators=(',', ': '))) 166 | try: 167 | nextpageURL = res['paging']['nextPage'] 168 | nextPage=True 169 | #continue to process records here before retrieving more 170 | except KeyError as err: 171 | nextPage=False 172 | nextpageURL=None 173 | print ("[Membership:getUserMemberships()] No (more) records.") 174 | else: 175 | print("NONE") 176 | 177 | def createMembership(self, dsk, token): 178 | #"Authorization: Bearer $token" 179 | authStr = 'Bearer ' + token 180 | 181 | self.PAYLOAD = { 182 | "dataSourceId":"externalId:%s" % DSKEXTERNALID, 183 | "availability": { 184 | "available":"Yes" 185 | }, 186 | "courseRoleId":"Instructor" 187 | } 188 | 189 | #session = requests.session() 190 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 191 | 192 | #self.membership_Path = '/learn/api/public/v1/courses/courseId/users/userId' 193 | replacement = "externalId:"+COURSEEXTERNALID 194 | membership_Path = self.membership_Path 195 | membership_Path = membership_Path.replace("courseId", replacement) 196 | 197 | replacement = "externalId:" + USEREXTERNALID 198 | membership_Path = membership_Path.replace("userId", replacement) 199 | 200 | print("[Membership:getMemberships()] PUT Request URL: https://" + self.target_url + membership_Path) 201 | print("[Membership:getMemberships()] JSON Payload: " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 202 | #r = session.put("https://" + self.target_url + membership_Path, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 203 | r = requests.put("https://" + self.target_url + membership_Path, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 204 | print("[Membership:getMemberships()] STATUS CODE: " + str(r.status_code) ) 205 | print("[Membership:getMemberships()] RESPONSE:") 206 | if r.text: 207 | res = json.loads(r.text) 208 | print(json.dumps(res,indent=4, separators=(',', ': '))) 209 | else: 210 | print("NONE") 211 | 212 | def getMembership(self, token): 213 | #GET /learn/api/public/v1/courses/{courseId}/users/{userId} 214 | print('[Membership:getMemberships] token: ' + token) 215 | #"Authorization: Bearer $token" 216 | authStr = 'Bearer ' + token 217 | print('[Membership:getMemberships] authStr: ' + authStr) 218 | #session = requests.session() 219 | #session.mount('https://', Tls1Adapter()) # remove for production 220 | 221 | replacement = "externalId:"+COURSEEXTERNALID 222 | membership_Path = self.membership_Path 223 | membership_Path = membership_Path.replace("courseId", replacement) 224 | 225 | replacement = "externalId:" + USEREXTERNALID 226 | membership_Path = membership_Path.replace("userId", replacement) 227 | 228 | print("[Membership:getMemberships()] GET Request URL: https://" + self.target_url + membership_Path) 229 | print("[Membership:getMemberships()] JSON Payload: NONE REQUIRED") 230 | r = requests.get("https://" + self.target_url + membership_Path, headers={'Authorization':authStr}, verify=False) 231 | print("[Membership:getMembership()] STATUS CODE: " + str(r.status_code) ) 232 | print("[Membership:getMembership()] RESPONSE:") 233 | if r.text: 234 | res = json.loads(r.text) 235 | print(json.dumps(res,indent=4, separators=(',', ': '))) 236 | else: 237 | print("NONE") 238 | 239 | def readUserMemberships(self, token): 240 | #GET /learn/api/public/v1/users/{userId}/courses 241 | print('[Membership:readUserMemberships] token: ' + token) 242 | #"Authorization: Bearer $token" 243 | authStr = 'Bearer ' + token 244 | print('[Membership:readUserMemberships] authStr: ' + authStr) 245 | #session = requests.session() 246 | #session.mount('https://', Tls1Adapter()) # remove for production 247 | 248 | replacement = "externalId:" + USEREXTERNALID 249 | userMemberships_Path = self.userMembships_Path 250 | userMemberships_Path = userMemberships_Path.replace("userId", replacement) 251 | print("[Membership:readUserMemberships()] GET Request URL: https://" + self.target_url + userMemberships_Path) 252 | print("[Membership:readUserMemberships()] JSON Payload: NONE REQUIRED") 253 | r = requests.get("https://" + self.target_url + userMemberships_Path, headers={'Authorization':authStr}, verify=False) 254 | print("[Membership:readUserMemberships()] STATUS CODE: " + str(r.status_code) ) 255 | print("[Membership:readUserMemberships()] RESPONSE:") 256 | if r.text: 257 | res = json.loads(r.text) 258 | print(json.dumps(res,indent=4, separators=(',', ': '))) 259 | else: 260 | print("NONE") 261 | 262 | 263 | def updateMembership(self, dsk, token): 264 | #"Authorization: Bearer $token" 265 | authStr = 'Bearer ' + token 266 | 267 | self.PAYLOAD = { 268 | "dataSourceId":"externalId:%s" % DSKEXTERNALID, 269 | "availability": { 270 | "available":"No" 271 | }, 272 | "courseRoleId":"Student" 273 | } 274 | 275 | #session = requests.session() 276 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 277 | 278 | replacement = "externalId:"+ COURSEEXTERNALID 279 | membership_Path = self.membership_Path 280 | membership_Path = membership_Path.replace("courseId", replacement) 281 | 282 | replacement = "externalId:" + USEREXTERNALID 283 | membership_Path = membership_Path.replace("userId", replacement) 284 | 285 | print("[Membership:updateMembership()] Request URL: https://" + self.target_url + membership_Path) 286 | print("[Membership:updateMembership()] JSON Payload: " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 287 | #r = session.patch("https://" + self.target_url + membership_Path, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 288 | r = requests.patch("https://" + self.target_url + membership_Path, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 289 | print("[Membership:updateMembership()] STATUS CODE: " + str(r.status_code) ) 290 | print("[Membership:updateMembership()] RESPONSE:") 291 | if r.text: 292 | res = json.loads(r.text) 293 | print(json.dumps(res,indent=4, separators=(',', ': '))) 294 | else: 295 | print("NONE") 296 | 297 | def deleteMembership(self, token): 298 | #"Authorization: Bearer $token" 299 | authStr = 'Bearer ' + token 300 | 301 | #session = requests.session() 302 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 303 | 304 | replacement = "externalId:"+ COURSEEXTERNALID 305 | membership_Path = self.membership_Path 306 | membership_Path = membership_Path.replace("courseId", replacement) 307 | 308 | replacement = "externalId:" + USEREXTERNALID 309 | membership_Path = membership_Path.replace("userId", replacement) 310 | 311 | print("[Membership:deleteMembership()] DELETE Request URL: https://" + self.target_url + membership_Path) 312 | print("[Membership:deleteMembership()] JSON Payload: NONE REQUIRED") 313 | #r = session.delete("https://" + self.target_url + membership_Path, headers={'Authorization':authStr}, verify=False) 314 | r = requests.delete("https://" + self.target_url + membership_Path, headers={'Authorization':authStr}, verify=False) 315 | print("[Membership:deleteMembership()] STATUS CODE: " + str(r.status_code) ) 316 | print("[Membership:deleteMembership()] RESPONSE:") 317 | if r.text: 318 | res = json.loads(r.text) 319 | print(json.dumps(res,indent=4, separators=(',', ': '))) 320 | else: 321 | print("NONE") 322 | -------------------------------------------------------------------------------- /demo/restdemo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016, Blackboard Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of Blackboard Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | """ 10 | 11 | from auth import AuthToken 12 | from datasource import DataSource 13 | from term import Term 14 | from course import Course 15 | from user import User 16 | from membership import Membership 17 | from constants import * 18 | 19 | import sys 20 | import getopt 21 | 22 | 23 | 24 | def main(argv): 25 | target_url = '' 26 | COMMAND = '' 27 | ALL = False 28 | AUTH = False 29 | DATASOURCE = False 30 | TERM = False 31 | COURSE = False 32 | USER = False 33 | MEMBERSHIP = False 34 | CLEANUP = False 35 | 36 | datasource_PK1 = None 37 | datasource_session = None 38 | 39 | usageStr = "\nrestdemo.py -t|--target -c|--command \n" 40 | usageStr += "e.g restdemo.py -t www.myschool.edu -c create_course\n" 41 | usageStr += "command: _ where is one of the following:\n" 42 | usageStr += "\tcreate, read, read_all_, read_all_course_memberships, read_all_user_memberships, update, delete\n" 43 | usageStr += "and is one of the following:\n" 44 | usageStr += "\tdatasource, term, course, user\n" 45 | usageStr += "-t is required; No -c args will run demo in predetermined order.\n" 46 | usageStr += "'-c authorize' demomonstrates the authorization process and does not create objects." 47 | usageStr += "-c commands require a valid datasource PK1 - \n" 48 | usageStr += "\ta datasource get will be run in these cases, defaulting to create\n" 49 | usageStr += "\tif the demo datasource does not exist." 50 | 51 | if len(sys.argv) > 1: #there are command line arguments 52 | try: 53 | opts, args = getopt.getopt(argv,"ht:c:",["target=","command="]) 54 | except getopt.GetoptError: 55 | print (usageStr) 56 | sys.exit(2) 57 | for opt, arg in opts: 58 | if opt == '-h': 59 | print (usageStr) 60 | sys.exit() 61 | elif opt == '-d': 62 | print ("Deleting at end of run.") 63 | CLEANUP = True 64 | elif opt in ("-t", "--target"): 65 | target_url = arg.lstrip() 66 | elif opt in ("-c", "--command"): 67 | COMMAND = arg 68 | else: 69 | COMMAND = "Run All" 70 | print ('[main] Target is:', target_url) 71 | print ('[main] Command is:', COMMAND) 72 | 73 | 74 | else: 75 | print(usageStr) 76 | sys.exit(2) 77 | 78 | 79 | #Set up some booleans for processing flags and order of processing 80 | if "course" in COMMAND: 81 | print("[main] Run course command") 82 | COURSE = True 83 | elif "membership" in COMMAND: 84 | print("[main] Run membership command") 85 | MEMBERSHIP = True 86 | elif "user" in COMMAND: 87 | print("[main] Run user command") 88 | USER = True 89 | elif "term" in COMMAND: 90 | print("[main] Run term command") 91 | TERM = True 92 | elif "datasource" in COMMAND: 93 | print("[main] Run datasource command") 94 | DATASOURCE = True 95 | elif "authorize" in COMMAND: 96 | print("[main] Run authorization command") 97 | AUTH = True 98 | else: 99 | print("[main] Empty Command: Run All\n") 100 | ALL = True 101 | 102 | print ('\n[main] Acquiring auth token...\n') 103 | authorized_session = AuthToken(target_url) 104 | authorized_session.setToken() 105 | print ('\n[main] Returned token: ' + authorized_session.getToken() + '\n') 106 | 107 | if not AUTH: 108 | #run commands in required order if running ALL 109 | if DATASOURCE or ALL: 110 | #process Datasource command 111 | print("\n[main] Run datasource command: " + ('ALL' if ALL else COMMAND) + '...') 112 | datasource_session = DataSource(target_url, authorized_session.getToken()) 113 | if 'datasource' in COMMAND: 114 | datasource_session.execute(COMMAND, authorized_session.getToken()) 115 | else: 116 | if not datasource_PK1 or datasource_PK1 is None: 117 | datasource_session.createDataSource(authorized_session.getToken()) 118 | datasource_session.getDataSource(authorized_session.getToken()) 119 | datasource_session.getDataSources(authorized_session.getToken()) 120 | datasource_session.updateDataSource(authorized_session.getToken()) 121 | 122 | if TERM or ALL: 123 | term_session = Term(target_url, authorized_session.getToken()) 124 | #process term command 125 | print("\n[main] Run term command: " + ('ALL' if ALL else COMMAND) + '...') 126 | if 'term' in COMMAND: 127 | if (('delete' in COMMAND) or ('read' in COMMAND)): 128 | print ("[main] Deleting or getting does not require a datasource.") 129 | else: 130 | if not datasource_PK1: 131 | print("[main] confirm datasource.") 132 | datasource_session = DataSource(target_url, authorized_session.getToken()) 133 | datasource_session.checkDataSource(authorized_session.getToken()) 134 | 135 | term_session.execute(COMMAND, "externalId:"+TERMEXTERNALID, authorized_session.getToken()) 136 | else: 137 | term_session.getTerms(authorized_session.getToken()) 138 | term_session.createTerm("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 139 | term_session.getTerm(authorized_session.getToken()) 140 | term_session.updateTerm("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 141 | 142 | if COURSE or ALL: 143 | course_session = Course(target_url, authorized_session.getToken()) 144 | #process course command 145 | print("\n[main] Run course command: " + ('ALL' if ALL else COMMAND) + '...') 146 | if 'course' in COMMAND: 147 | if (('delete' in COMMAND) or ('read' in COMMAND)): 148 | print ("[main] Deleting or getting does not require a datasource.") 149 | else: 150 | if not datasource_PK1: 151 | print("[main] confirm datasource.") 152 | datasource_session = DataSource(target_url, authorized_session.getToken()) 153 | datasource_session.checkDataSource(authorized_session.getToken()) 154 | 155 | course_session.execute(COMMAND, "externalId:"+DSKEXTERNALID, authorized_session.getToken()) 156 | else: 157 | course_session.getCourses(authorized_session.getToken()) 158 | course_session.createCourse("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 159 | course_session.getCourse(authorized_session.getToken()) 160 | course_session.updateCourse("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 161 | 162 | if USER or ALL: 163 | user_session = User(target_url, authorized_session.getToken()) 164 | #process user command 165 | print("\n[main] Run user command: " + ('ALL' if ALL else COMMAND) + '...') 166 | if 'user' in COMMAND: 167 | if (('delete' in COMMAND) or ('read' in COMMAND)): 168 | print ("[main] Deleting or getting does not require a datasource.") 169 | else: 170 | if not datasource_PK1: 171 | print("[main] confirm datasource.") 172 | datasource_session = DataSource(target_url, authorized_session.getToken()) 173 | datasource_session.checkDataSource(authorized_session.getToken()) 174 | 175 | user_session.execute(COMMAND, "externalId:"+DSKEXTERNALID, authorized_session.getToken()) 176 | else: 177 | user_session.getUsers(authorized_session.getToken()) 178 | user_session.createUser("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 179 | user_session.getUser(authorized_session.getToken()) 180 | user_session.updateUser("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 181 | 182 | if MEMBERSHIP or ALL: 183 | membership_session = Membership(target_url, authorized_session.getToken()) 184 | 185 | #process membership command 186 | print("\n[main] Run membership command: " + ('ALL' if ALL else COMMAND) + '...') 187 | if 'membership' in COMMAND: 188 | if (('delete' in COMMAND) or ('read' in COMMAND)): 189 | print ("[main] Deleting or getting does not require a datasource.") 190 | else: 191 | if not datasource_PK1: 192 | print("[main] confirm datasource.") 193 | datasource_session = DataSource(target_url, authorized_session.getToken()) 194 | datasource_session.checkDataSource(authorized_session.getToken()) 195 | 196 | membership_session.execute(COMMAND, "externalId:"+DSKEXTERNALID, authorized_session.getToken()) 197 | else: 198 | membership_session.getCourseMemberships(authorized_session.getToken()) 199 | membership_session.createMembership("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 200 | membership_session.getUserMemberships(authorized_session.getToken()) 201 | membership_session.updateMembership("externalId:"+DSKEXTERNALID, authorized_session.getToken()) 202 | membership_session.readUserMemberships(authorized_session.getToken()) 203 | #clean up if not using individual commands 204 | if ALL: 205 | print('\n[main] Completing Demo and deleting created objects...') 206 | print ("[main] Deleting membership") 207 | membership_session.deleteMembership(authorized_session.getToken()) 208 | print ("[main] Deleting Course") 209 | user_session.deleteUser(authorized_session.getToken()) 210 | print ("[main] Deleting Course") 211 | course_session.deleteCourse(authorized_session.getToken()) 212 | print ("[main] Deleting Term") 213 | term_session.deleteTerm(authorized_session.getToken()) 214 | print ("[main] Deleting DataSource") 215 | datasource_session.deleteDataSource(authorized_session.getToken()) 216 | else: 217 | print("\nRemember to delete created demo objects!") 218 | 219 | 220 | print("[main] Processing Complete") 221 | 222 | #revoke issued Token 223 | #authorized_session.revokeToken() 224 | 225 | if __name__ == '__main__': 226 | main(sys.argv[1:]) 227 | -------------------------------------------------------------------------------- /demo/term.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016, Blackboard Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of Blackboard Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | """ 10 | 11 | import json 12 | import requests 13 | from requests.adapters import HTTPAdapter 14 | from requests.packages.urllib3.poolmanager import PoolManager 15 | import ssl 16 | import sys 17 | from constants import * 18 | 19 | requests.packages.urllib3.disable_warnings() 20 | 21 | #Tls1Adapter allows for connection to sites with non-CA/self-signed 22 | # certificates e.g.: Learn Dev VM 23 | # May be removed if you migrated the cert as outlined in auth.py 24 | class Tls1Adapter(HTTPAdapter): 25 | def init_poolmanager(self, connections, maxsize, block=False): 26 | self.poolmanager = PoolManager(num_pools=connections, 27 | maxsize=maxsize, 28 | block=block, 29 | ssl_version=ssl.PROTOCOL_TLSv1) 30 | 31 | class Term(): 32 | 33 | def __init__(self, target_url, token): 34 | self.target_url = target_url 35 | self.token = token 36 | self.terms_Path = '/learn/api/public/v1/terms' #create(POST)/get(GET) 37 | self.term_Path = '/learn/api/public/v1/terms/externalId:' 38 | self.terms_Path_Params = "?limit=%s&fields=%s" % (PAGINATIONLIMIT, TERMSGETFIELDS) 39 | 40 | 41 | 42 | def execute(self, command, dsk, token): 43 | if "create" in command: 44 | print('[Term:execute] : ' + command) 45 | self.createTerm(dsk, token) 46 | elif "read_all" in command: 47 | print('[Term:execute] : ' + command) 48 | self.getTerms(token) 49 | elif "read" in command: 50 | print('[Term:execute] : ' + command) 51 | self.getTerm(token) 52 | elif "update" in command: 53 | print('[Term:execute] : ' + command) 54 | self.updateTerm(dsk, token) 55 | elif "delete" in command: 56 | print('[Term:execute] : ' + command) 57 | self.deleteTerm(token) 58 | 59 | 60 | def getTerms(self, token): 61 | print('[Term:getTerms] token: ' + token) 62 | #"Authorization: Bearer $token" 63 | authStr = 'Bearer ' + token 64 | print('[Term:getTerms] authStr: ' + authStr) 65 | #session = requests.session() 66 | #session.mount('https://', Tls1Adapter()) # remove for production 67 | 68 | nextPage = True 69 | nextpageURL = None 70 | while nextPage: 71 | print ("[Term:getTerms()] ENTERING WHILE LOOP FOR NEXT PAGE CHECK") 72 | print ("[Term:getTerms()] NEXTPAGE: %s" % nextPage) 73 | print ("[Term:getTerms()] NEXTPAGEURL: %s" % nextpageURL) 74 | if nextpageURL: 75 | print ("[Term:getTerms()] NEXTPAGE: %s, so update URL parameters." % nextPage) 76 | self.terms_Path_Params = nextpageURL.replace(self.terms_Path, '') 77 | print ("[Term:getTerms()] UPDATED URL PARAMS: %s" %self.terms_Path_Params) 78 | print("[Term:getTerms()] GET Request URL: https://" + self.target_url + self.terms_Path + self.terms_Path_Params) 79 | print("[Term:getTerms()] JSON Payload: NONE REQUIRED") 80 | r = requests.get("https://" + self.target_url + self.terms_Path + self.terms_Path_Params, headers={'Authorization':authStr}, verify=False) 81 | 82 | print("[Term:getTerms()] STATUS CODE: " + str(r.status_code) ) 83 | print("[Term:getTerms()] RESPONSE:") 84 | if r.text: 85 | res = json.loads(r.text) 86 | print(json.dumps(res,indent=4, separators=(',', ': '))) 87 | try: 88 | nextpageURL = res['paging']['nextPage'] 89 | nextPage=True 90 | #continue to process records here before retrieving more 91 | except KeyError as err: 92 | nextPage=False 93 | nextpageURL=None 94 | print ("[Term:getTerms()] No (more) records.") 95 | else: 96 | print("NONE") 97 | 98 | 99 | 100 | 101 | def createTerm(self, dsk, token): 102 | #"Authorization: Bearer $token" 103 | authStr = 'Bearer ' + token 104 | self.PAYLOAD = { 105 | "externalId":TERMEXTERNALID, 106 | "dataSourceId": "externalId:%s" % DSKEXTERNALID, 107 | "name":"REST Demo Term", 108 | "description": "Term used for REST demo", 109 | "availability": { 110 | "duration":"Continuous" 111 | } 112 | } 113 | 114 | #session = requests.session() 115 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 116 | 117 | print("[Term:createTerm()] POST Request URL: https://" + self.target_url + self.terms_Path) 118 | print("[Term:createTerm()] JSON Payload: " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 119 | r = requests.post("https://" + self.target_url + self.terms_Path, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 120 | 121 | print("[Term:createTerm()] STATUS CODE: " + str(r.status_code) ) 122 | print("[Term:createTerm()] RESPONSE:") 123 | if r.text: 124 | res = json.loads(r.text) 125 | print(json.dumps(res,indent=4, separators=(',', ': '))) 126 | else: 127 | print("NONE") 128 | 129 | def getTerm(self, token): 130 | print('[Term:getTerm()] token: ' + token) 131 | #"Authorization: Bearer $token" 132 | authStr = 'Bearer ' + token 133 | print('[Term:getTerm()] authStr: ' + authStr) 134 | #session = requests.session() 135 | #session.mount('https://', Tls1Adapter()) # remove for production 136 | 137 | print("[Term:getTerm()] GET Request URL: https://" + self.target_url + self.term_Path+TERMEXTERNALID) 138 | print("[Term:getTerm()] JSON Payload: NONE REQUIRED") 139 | r = requests.get("https://" + self.target_url + self.term_Path+TERMEXTERNALID, headers={'Authorization':authStr}, verify=False) 140 | 141 | print("[Term:getTerm()] STATUS CODE: " + str(r.status_code) ) 142 | print("[Term:getTerm()] RESPONSE:") 143 | if r.text: 144 | res = json.loads(r.text) 145 | print(json.dumps(res,indent=4, separators=(',', ': '))) 146 | else: 147 | print("NONE") 148 | 149 | def updateTerm(self, dsk, token): 150 | #"Authorization: Bearer $token" 151 | authStr = 'Bearer ' + token 152 | print("[Term:updateTerm()] Term ExternalId: " + TERMEXTERNALID) 153 | 154 | self.PAYLOAD = { 155 | "externalId":TERMEXTERNALID, 156 | "dataSourceId": "externalId:%s" % DSKEXTERNALID, 157 | "name":"REST Python Demo Term", 158 | "description": "Term used for REST Python demo", 159 | "availability": { 160 | "duration":"continuous" 161 | } 162 | } 163 | #session = requests.session() 164 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 165 | 166 | print("[Term:updateTerm()] PATCH Request URL: https://" + self.target_url + self.term_Path+TERMEXTERNALID) 167 | print("[Term:updateTerm()] JSON Payload: " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 168 | r = requests.patch("https://" + self.target_url + self.term_Path+TERMEXTERNALID, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 169 | 170 | print("[Term:updateTerm()] STATUS CODE: " + str(r.status_code) ) 171 | print("[Term:updateTerm()] RESPONSE:") 172 | if r.text: 173 | res = json.loads(r.text) 174 | print(json.dumps(res,indent=4, separators=(',', ': '))) 175 | else: 176 | print("NONE") 177 | 178 | def deleteTerm(self, token): 179 | #"Authorization: Bearer $token" 180 | authStr = 'Bearer ' + token 181 | print("[Term:deleteTerm()] Term ExternalId: " + TERMEXTERNALID) 182 | 183 | #session = requests.session() 184 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 185 | 186 | print("[Term:getTerms()] DELETE Request URL: https://" + self.target_url + self.term_Path+TERMEXTERNALID) 187 | print("[Term:getTerms()] JSON Payload: NONE REQUIRED") 188 | #r = session.delete("https://" + self.target_url + self.term_Path+self.termExternalId, headers={'Authorization':authStr}, verify=False) 189 | r = requests.delete("https://" + self.target_url+self.term_Path+TERMEXTERNALID, headers={'Authorization':authStr}, verify=False) 190 | print("[Term:deleteTerm()] STATUS CODE: " + str(r.status_code) ) 191 | print("[Term:deleteTerm()] RESPONSE:") 192 | if r.text: 193 | res = json.loads(r.text) 194 | print(json.dumps(res,indent=4, separators=(',', ': '))) 195 | else: 196 | print("NONE") 197 | -------------------------------------------------------------------------------- /demo/trusted/keytool_crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsjCCAZqgAwIBAgIIZhffvonH6uowDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAxMOZGV2LmJi 3 | ZG4ubG9jYWwwHhcNMTYwNDE1MDAwMDAwWhcNMTgwNDE1MDAwMDAwWjAZMRcwFQYDVQQDEw5kZXYu 4 | YmJkbi5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMFwUcfsC77Pyyn21Y9S 5 | rZde5zvBY8CA8yUKc/jJj87BXM4gBZLB5lM7ypX8pLjgHuLIAquqf0a5bn1xkxRaK/Cf2v9bEM4m 6 | PHy6ZI5iLq5A90Q0JOGAiQsscBRiijSm8KYFAvpThmpsq7/K2mwS9fQtfyX1mRzrdVKDavQlrqm/ 7 | I7kg6EJAfzSEGTEl0NknyU0M22deiWtNbGwOcibMg9+yM+I863fU8FR0sSGm6RQwF0g/NcZpC94v 8 | hWTFdVdweRq+76PA73bbsaQukam1/+RxbG3r99FFFtN4HAt26Ak5/yCtt+Nn85263a1PEV/D4bCU 9 | 3sbh+OOo+hRxQ29l/xsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH563dPcVzxbGF1gE1PGQdlR+ 10 | Pzuux0EutLT0TE17y9yp5mgffweNJ3lgpcjwrhHrR0FeBbQvz9Pp62QsQgnJF2kHgwqq1MPqsPqe 11 | Mo78BIzTRZ45EtzE/p0oyoLBR1S9EWAyUtJBA8gKfGQiboj9qN5ZsX/KYny27b7ukMRJ8q/mB2XF 12 | c3LO6+T/ubTd3ZHN1nNfnqceecewx7CCwXMC5PblWq1bZ/rU0+Exv7+h7Ff7fkjx+wfmaR3lGf6L 13 | y+ceMVnS+r/VHYeah9kxb4qAPMIDqkmJLPLNXGl7Gc5Ph466y+bATFu6ywnqj6EJK0InxFXN1nvg 14 | gHafgVXjIMG0+g== 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /demo/user.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2016, Blackboard Inc. 3 | All rights reserved. 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | Neither the name of Blackboard Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY BLACKBOARD INC ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLACKBOARD INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | """ 10 | 11 | import json 12 | import requests 13 | from requests.adapters import HTTPAdapter 14 | from requests.packages.urllib3.poolmanager import PoolManager 15 | import ssl 16 | import sys 17 | from constants import * 18 | 19 | requests.packages.urllib3.disable_warnings() 20 | 21 | #Tls1Adapter allows for connection to sites with non-CA/self-signed 22 | # certificates e.g.: Learn Dev VM 23 | # May be removed if you migrated the cert as outlined in auth.py 24 | class Tls1Adapter(HTTPAdapter): 25 | def init_poolmanager(self, connections, maxsize, block=False): 26 | self.poolmanager = PoolManager(num_pools=connections, 27 | maxsize=maxsize, 28 | block=block, 29 | ssl_version=ssl.PROTOCOL_TLSv1) 30 | 31 | class User(): 32 | 33 | def __init__(self, target_url, token): 34 | self.target_url = target_url 35 | self.token = token 36 | self.users_Path = '/learn/api/public/v1/users' #create(POST)/get(GET) 37 | self.user_Path = '/learn/api/public/v1/users/externalId:' 38 | self.users_Path_Params = "?limit=%s&fields=%s" % (PAGINATIONLIMIT, USERGETFIELDS) 39 | 40 | 41 | 42 | 43 | def execute(self, command, dsk, token): 44 | if "create" in command: 45 | print('[User:execute] : ' + command) 46 | self.createUser(dsk, token) 47 | elif "read_all" in command: 48 | print('[User:execute] : ' + command) 49 | self.getUsers(token) 50 | elif "read" in command: 51 | print('[User:execute] : ' + command) 52 | self.getUser(token) 53 | elif "update" in command: 54 | print('[User:execute] : ' + command) 55 | self.updateUser(dsk, token) 56 | elif "delete" in command: 57 | print('[User:execute] : ' + command) 58 | self.deleteUser(token) 59 | 60 | 61 | def getUsers(self, token): 62 | #demo limits returned page count to constants.PAGINATIONLIMIT and 63 | #limits result data fields to constants.USERGETFIELDS 64 | print('[User:getUsers] token: ' + token) 65 | #"Authorization: Bearer $token" 66 | authStr = 'Bearer ' + token 67 | print('[User:getUsers] authStr: ' + authStr) 68 | #session = requests.session() 69 | #session.mount('https://', Tls1Adapter()) # remove for production 70 | 71 | nextPage = True 72 | nextpageURL = None 73 | while nextPage: 74 | print ("[User:getUsers()] ENTERING WHILE LOOP FOR NEXT PAGE CHECK") 75 | print ("[User:getUsers()] NEXTPAGE: %s" % nextPage) 76 | print ("[User:getUsers()] NEXTPAGEURL: %s" % nextpageURL) 77 | if nextpageURL: 78 | print ("[User:getUsers()] NEXTPAGE: %s, so update URL parameters." % nextPage) 79 | self.users_Path_Params = nextpageURL.replace(self.users_Path, '') 80 | print ("[User:getUsers()] UPDATED URL PARAMS: %s" %self.users_Path_Params) 81 | print("[User:getUsers()] GET Request URL: https://" + self.target_url + self.users_Path + self.users_Path_Params) 82 | print("[User:getUsers()] JSON Payload: NONE REQUIRED") 83 | r = requests.get("https://" + self.target_url + self.users_Path + self.users_Path_Params, headers={'Authorization':authStr}, verify=False) 84 | 85 | print("[User:getUsers()] STATUS CODE: " + str(r.status_code) ) 86 | print("[User:getUsers()] RESPONSE:") 87 | if r.text: 88 | res = json.loads(r.text) 89 | print(json.dumps(res,indent=4, separators=(',', ': '))) 90 | try: 91 | nextpageURL = res['paging']['nextPage'] 92 | nextPage=True 93 | except KeyError as err: 94 | nextPage=False 95 | nextpageURL=None 96 | print ("[User:getUsers()] No (more) records.") 97 | else: 98 | print("NONE") 99 | 100 | def createUser(self, dsk, token): 101 | #"Authorization: Bearer $token" 102 | authStr = 'Bearer ' + token 103 | 104 | self.PAYLOAD = { 105 | "externalId":USEREXTERNALID, 106 | "dataSourceId": dsk, #self.dskExternalId, Supported soon. 107 | "userName":"python_demo", 108 | "password":"python61", 109 | "availability": { 110 | "available": "Yes" 111 | }, 112 | "name": { 113 | "given": "Python", 114 | "family": "Demo", 115 | }, 116 | "contact": { 117 | "email": "no.one@ereh.won", 118 | } 119 | } 120 | 121 | #session = requests.session() 122 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 123 | 124 | print("[User:createUser()] POST Request URL: https://" + self.target_url + self.users_Path) 125 | print("[User:createUser()] JSON Payload: " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 126 | r = requests.post("https://" + self.target_url + self.users_Path, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 127 | print("[User:createUser()] STATUS CODE: " + str(r.status_code) ) 128 | print("[User:createUser()] RESPONSE:") 129 | if r.text: 130 | res = json.loads(r.text) 131 | print(json.dumps(res,indent=4, separators=(',', ': '))) 132 | else: 133 | print("NONE") 134 | 135 | def getUser(self, token): 136 | print('[User:getUser()] token: ' + token) 137 | #"Authorization: Bearer $token" 138 | authStr = 'Bearer ' + token 139 | print('[User:getUser()] authStr: ' + authStr) 140 | #session = requests.session() 141 | #session.mount('https://', Tls1Adapter()) # remove for production 142 | 143 | print("[User:getUser()] GET Request URL: https://" + self.target_url + self.user_Path+USEREXTERNALID) 144 | print("[User:getUser()] JSON Payload: NONE REQUIRED") 145 | r = requests.get("https://" + self.target_url + self.user_Path+USEREXTERNALID, headers={'Authorization':authStr}, verify=False) 146 | 147 | print("[User:getUser()] STATUS CODE: " + str(r.status_code) ) 148 | print("[User:getUser()] RESPONSE:") 149 | if r.text: 150 | res = json.loads(r.text) 151 | print(json.dumps(res,indent=4, separators=(',', ': '))) 152 | else: 153 | print("NONE") 154 | 155 | def updateUser(self, dsk, token): 156 | #"Authorization: Bearer $token" 157 | authStr = 'Bearer ' + token 158 | print("[User:updateUser()] USEREXTERNALID: " + USEREXTERNALID) 159 | 160 | self.PAYLOAD = { 161 | "externalId": USEREXTERNALID, 162 | "dataSourceId": dsk, #self.dskExternalId, Supported soon. 163 | "userName":"python_demo", 164 | "password": "python16", 165 | "availability": { 166 | "available": "Yes" 167 | }, 168 | "name": { 169 | "given": "Python", 170 | "family": "BbDN", 171 | "middle": "Demo", 172 | }, 173 | "contact": { 174 | "email": "no.one@ereh.won", 175 | } 176 | } 177 | #session = requests.session() 178 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 179 | 180 | print("[User:updateUser()] PATCH Request URL: https://" + self.target_url + self.user_Path+USEREXTERNALID) 181 | print("[User:updateUser()] JSON Payload: " + json.dumps(self.PAYLOAD, indent=4, separators=(',', ': '))) 182 | r = requests.patch("https://" + self.target_url + self.user_Path+USEREXTERNALID, data=json.dumps(self.PAYLOAD), headers={'Authorization':authStr, 'Content-Type':'application/json'}, verify=False) 183 | 184 | print("[User:updateUser()] STATUS CODE: " + str(r.status_code) ) 185 | print("[User:updateUser()] RESPONSE:") 186 | if r.text: 187 | res = json.loads(r.text) 188 | print(json.dumps(res,indent=4, separators=(',', ': '))) 189 | else: 190 | print("NONE") 191 | 192 | def deleteUser(self, token): 193 | #"Authorization: Bearer $token" 194 | authStr = 'Bearer ' + token 195 | print("[User:deleteUser()] USEREXTERNALID: " + USEREXTERNALID) 196 | 197 | #session = requests.session() 198 | #session.mount('https://', Tls1Adapter()) # remove for production with commercial cert 199 | 200 | print("[User:deleteUser()] DELETE Request URL: https://" + self.target_url + self.user_Path+USEREXTERNALID) 201 | print("[User:deleteUser()] JSON Payload: NONE REQUIRED") 202 | r = requests.delete("https://" + self.target_url + self.user_Path+USEREXTERNALID, headers={'Authorization':authStr}, verify=False) 203 | 204 | print("[User:deleteUser()] STATUS CODE: " + str(r.status_code) ) 205 | print("[User:deleteUser()] RESPONSE:") 206 | if r.text: 207 | res = json.loads(r.text) 208 | print(json.dumps(res,indent=4, separators=(',', ': '))) 209 | else: 210 | print("NONE") 211 | --------------------------------------------------------------------------------