projectal.api
Core API functions to communicate with the Projectal server.
Get the status()
of the Projectal server, run a query()
,
or make custom HTTP requests to any Projectal API method.
Verb functions (GET, POST, etc.)
The HTTP verb functions provided here are used internally by this library; in general, you should not need to use these functions directly unless this library's implementation of an API method is insufficient for your needs.
The response is validated automatically for all verbs. A
projectal.errors.ProjectalException
is thrown if the response
fails, otherwise you get a dict
containing the JSON response.
Login and session state
This module handles logins and session state for the library.
It's done for you automatically by the module when you make the
first authenticated request. See login()
for details.
1""" 2Core API functions to communicate with the Projectal server. 3 4Get the `status()` of the Projectal server, run a `query()`, 5or make custom HTTP requests to any Projectal API method. 6 7**Verb functions (GET, POST, etc.)** 8 9The HTTP verb functions provided here are used internally by 10this library; in general, you should not need to use these 11functions directly unless this library's implementation of 12an API method is insufficient for your needs. 13 14The response is validated automatically for all verbs. A 15`projectal.errors.ProjectalException` is thrown if the response 16fails, otherwise you get a `dict` containing the JSON response. 17 18**Login and session state** 19 20This module handles logins and session state for the library. 21It's done for you automatically by the module when you make the 22first authenticated request. See `login()` for details. 23""" 24from datetime import timezone, datetime 25 26import requests 27from packaging import version 28from requests import PreparedRequest 29 30try: 31 from simplejson.errors import JSONDecodeError 32except ImportError: 33 from json.decoder import JSONDecodeError 34 35from .errors import * 36import projectal 37 38 39def status(): 40 """Get runtime details of the Projectal server (with version number).""" 41 _check_creds_or_fail() 42 response = requests.get(_build_url('/management/status'), verify=projectal.__verify) 43 return response.json() 44 45 46def _check_creds_or_fail(): 47 """Correctness check: can't proceed if no API details supplied.""" 48 if not projectal.api_base: 49 raise LoginException('Projectal URL (projectal.api_base) is not set') 50 if not projectal.api_username or not projectal.api_password: 51 raise LoginException('API credentials are missing') 52 53 54def _check_version_or_fail(): 55 """ 56 Check the version number of the Projectal instance. If the 57 version number is below the minimum supported version number 58 of this API client, raise a ProjectalVersionException. 59 """ 60 status = projectal.status() 61 if status['status'] != 'UP': 62 raise LoginException('Projectal server status check failed') 63 v = projectal.status()['version'] 64 min = projectal.MIN_PROJECTAL_VERSION 65 if version.parse(v) >= version.parse(min): 66 return True 67 m = "Minimum supported Projectal version: {}. Got: {}".format(min, v) 68 raise ProjectalVersionException(m) 69 70 71def login(): 72 """ 73 Log in using the credentials supplied to the module. If successful, 74 stores the cookie in memory for reuse in future requests. 75 76 **You do not need to manually call this method** to use this library. 77 The library will automatically log in before the first request is 78 made or if the previous session has expired. 79 80 This method can be used to check if the account credentials are 81 working correctly. 82 """ 83 _check_version_or_fail() 84 85 payload = { 86 'username': projectal.api_username, 87 'password': projectal.api_password 88 } 89 if projectal.api_application_id: 90 payload['applicationId'] = projectal.api_application_id 91 response = requests.post(_build_url('/auth/login'), json=payload, verify=projectal.__verify) 92 # Handle errors here 93 if response.status_code == 200 and response.json()['status'] == 'OK': 94 projectal.cookies = response.cookies 95 projectal.api_auth_details = auth_details() 96 return True 97 raise LoginException('Check the API URL and your credentials') 98 99 100def auth_details(): 101 """ 102 Returns some details about the currently logged-in user account, 103 including all permissions available to it. 104 """ 105 return projectal.get('/api/user/details') 106 107 108def permission_list(): 109 """ 110 Returns a list of all permissions that exist in Projectal. 111 """ 112 return projectal.get('/api/permission/list') 113 114 115def ldap_sync(): 116 """Initiate an on-demand user sync with the LDAP/AD server configured in your 117 Projectal server settings. If not configured, returns a HTTP 405 error.""" 118 return projectal.post('/api/ldap/sync', None) 119 120 121def query(payload): 122 """ 123 Executes a query and returns the result. See the 124 [Query API](https://projectal.com/docs/latest#tag/Query) for details. 125 """ 126 return projectal.post('/api/query/match', payload) 127 128 129def date_from_timestamp(date): 130 """Returns a date string from a timestamp. 131 E.g., `1647561600000` returns `2022-03-18`.""" 132 if not date: 133 return None 134 return str(datetime.utcfromtimestamp(int(date)/1000).date()) 135 136 137def timestamp_from_date(date): 138 """Returns a timestamp from a date string. 139 E.g., `2022-03-18` returns `1647561600000`.""" 140 if not date: 141 return None 142 return int(datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() * 1000) 143 144def timestamp_from_datetime(date): 145 """Retuns a timestamp from a datetime string. 146 E.g. `2022-03-18 17:00` returns `1647622800000`.""" 147 if not date: 148 return None 149 return int(datetime.strptime(date, "%Y-%m-%d %H:%M") 150 .replace(tzinfo=timezone.utc).timestamp() * 1000) 151 152def post(endpoint, payload=None, file=None, is_json=True): 153 """HTTP POST to the Projectal server.""" 154 return __request('post', endpoint, payload, file=file, is_json=is_json) 155 156 157def get(endpoint, payload=None, is_json=True): 158 """HTTP GET to the Projectal server.""" 159 return __request('get', endpoint, payload, is_json=is_json) 160 161 162def delete(endpoint, payload=None): 163 """HTTP DELETE to the Projectal server.""" 164 return __request('delete', endpoint, payload) 165 166 167def put(endpoint, payload=None, file=None, form=False): 168 """HTTP PUT to the Projectal server.""" 169 return __request( 170 'put', endpoint, payload, file=file, form=form) 171 172 173def __request(method, endpoint, payload=None, file=None, form=False, is_json=True): 174 """ 175 Make an API request. If this is the first request made in the module, 176 this function will issue a login API call first. 177 178 Additionally, if the response claims an expired JWT, the function 179 will issue a login API call and try the request again (max 1 try). 180 """ 181 if not projectal.cookies: 182 projectal.login() 183 fun = getattr(requests, method) 184 kwargs = {} 185 if file: 186 kwargs['files'] = file 187 kwargs['data'] = payload 188 elif form: 189 kwargs['data'] = payload 190 else: 191 kwargs['json'] = payload 192 response = fun(_build_url(endpoint), cookies=projectal.cookies, verify=projectal.__verify, **kwargs) 193 194 try: 195 # Raise error for non-200 response 196 response.raise_for_status() 197 except HTTPError: 198 # If the error is from an expired JWT we can retry it by 199 # clearing the cookie. (Login happens on next call). 200 try: 201 r = response.json() 202 if (r.get('status', None) == 'UNAUTHORIZED' 203 or r.get('message', None) == 'anonymousUser' 204 or r.get('error', None) == 'Unauthorized'): 205 projectal.cookies = None 206 return __request(method, endpoint, payload, file) 207 except JSONDecodeError: 208 pass 209 raise ProjectalException(response) from None 210 211 # We will treat a partial success as failure - we cannot silently 212 # ignore some errors 213 if response.status_code == 207: 214 raise ProjectalException(response) 215 216 if not is_json: 217 return response 218 try: 219 payload = response.json() 220 # Fail if the status code in the response body (not the HTTP code!) 221 # does not match what we expect for the API endpoint. 222 __maybe_fail_status(response, payload) 223 # If we have a timestamp, record it for whoever is interested 224 if 'timestamp' in payload: 225 projectal.response_timestamp = payload['timestamp'] 226 else: 227 projectal.response_timestamp = None 228 229 # If we have a 'jobCase', return the data it points to, which is 230 # what the caller is after (saves them having to do it every time). 231 if 'jobCase' in payload: 232 return payload[payload['jobCase']] 233 return payload 234 except JSONDecodeError: 235 # API always responds with JSON. If not, it's an error 236 raise ProjectalException(response) from None 237 238 239def __maybe_fail_status(response, payload): 240 """ 241 Check the status code in the body of the response. Raise 242 a `ProjectalException` if it does not match the "good" 243 status for that request. 244 245 The code is "OK" for everything, but /create returns "CREATED". 246 Luckily for us, /create also returns a 201, so we know which 247 codes to match up. 248 249 Requests with no 'status' key are assumed to be good. 250 """ 251 expected = "OK" 252 if response.status_code == 201: 253 expected = "CREATED" 254 255 got = payload.get('status', expected) 256 if expected == got: 257 return True 258 m = "Unexpected response calling {}. Expected status: {}. Got: {}". \ 259 format(response.url, expected, got) 260 raise ProjectalException(response, m) 261 262 263def _build_url(endpoint): 264 req = PreparedRequest() 265 url = projectal.api_base.rstrip('/') + endpoint 266 params = {'alias': projectal.api_alias} 267 req.prepare_url(url, params) 268 return req.url
40def status(): 41 """Get runtime details of the Projectal server (with version number).""" 42 _check_creds_or_fail() 43 response = requests.get(_build_url('/management/status'), verify=projectal.__verify) 44 return response.json()
Get runtime details of the Projectal server (with version number).
72def login(): 73 """ 74 Log in using the credentials supplied to the module. If successful, 75 stores the cookie in memory for reuse in future requests. 76 77 **You do not need to manually call this method** to use this library. 78 The library will automatically log in before the first request is 79 made or if the previous session has expired. 80 81 This method can be used to check if the account credentials are 82 working correctly. 83 """ 84 _check_version_or_fail() 85 86 payload = { 87 'username': projectal.api_username, 88 'password': projectal.api_password 89 } 90 if projectal.api_application_id: 91 payload['applicationId'] = projectal.api_application_id 92 response = requests.post(_build_url('/auth/login'), json=payload, verify=projectal.__verify) 93 # Handle errors here 94 if response.status_code == 200 and response.json()['status'] == 'OK': 95 projectal.cookies = response.cookies 96 projectal.api_auth_details = auth_details() 97 return True 98 raise LoginException('Check the API URL and your credentials')
Log in using the credentials supplied to the module. If successful, stores the cookie in memory for reuse in future requests.
You do not need to manually call this method to use this library. The library will automatically log in before the first request is made or if the previous session has expired.
This method can be used to check if the account credentials are working correctly.
101def auth_details(): 102 """ 103 Returns some details about the currently logged-in user account, 104 including all permissions available to it. 105 """ 106 return projectal.get('/api/user/details')
Returns some details about the currently logged-in user account, including all permissions available to it.
109def permission_list(): 110 """ 111 Returns a list of all permissions that exist in Projectal. 112 """ 113 return projectal.get('/api/permission/list')
Returns a list of all permissions that exist in Projectal.
116def ldap_sync(): 117 """Initiate an on-demand user sync with the LDAP/AD server configured in your 118 Projectal server settings. If not configured, returns a HTTP 405 error.""" 119 return projectal.post('/api/ldap/sync', None)
Initiate an on-demand user sync with the LDAP/AD server configured in your Projectal server settings. If not configured, returns a HTTP 405 error.
122def query(payload): 123 """ 124 Executes a query and returns the result. See the 125 [Query API](https://projectal.com/docs/latest#tag/Query) for details. 126 """ 127 return projectal.post('/api/query/match', payload)
Executes a query and returns the result. See the Query API for details.
130def date_from_timestamp(date): 131 """Returns a date string from a timestamp. 132 E.g., `1647561600000` returns `2022-03-18`.""" 133 if not date: 134 return None 135 return str(datetime.utcfromtimestamp(int(date)/1000).date())
Returns a date string from a timestamp.
E.g., 1647561600000
returns 2022-03-18
.
138def timestamp_from_date(date): 139 """Returns a timestamp from a date string. 140 E.g., `2022-03-18` returns `1647561600000`.""" 141 if not date: 142 return None 143 return int(datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() * 1000)
Returns a timestamp from a date string.
E.g., 2022-03-18
returns 1647561600000
.
145def timestamp_from_datetime(date): 146 """Retuns a timestamp from a datetime string. 147 E.g. `2022-03-18 17:00` returns `1647622800000`.""" 148 if not date: 149 return None 150 return int(datetime.strptime(date, "%Y-%m-%d %H:%M") 151 .replace(tzinfo=timezone.utc).timestamp() * 1000)
Retuns a timestamp from a datetime string.
E.g. 2022-03-18 17:00
returns 1647622800000
.
153def post(endpoint, payload=None, file=None, is_json=True): 154 """HTTP POST to the Projectal server.""" 155 return __request('post', endpoint, payload, file=file, is_json=is_json)
HTTP POST to the Projectal server.
158def get(endpoint, payload=None, is_json=True): 159 """HTTP GET to the Projectal server.""" 160 return __request('get', endpoint, payload, is_json=is_json)
HTTP GET to the Projectal server.
163def delete(endpoint, payload=None): 164 """HTTP DELETE to the Projectal server.""" 165 return __request('delete', endpoint, payload)
HTTP DELETE to the Projectal server.
168def put(endpoint, payload=None, file=None, form=False): 169 """HTTP PUT to the Projectal server.""" 170 return __request( 171 'put', endpoint, payload, file=file, form=form)
HTTP PUT to the Projectal server.