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 = {"username": projectal.api_username, "password": projectal.api_password} 86 if projectal.api_application_id: 87 payload["applicationId"] = projectal.api_application_id 88 response = requests.post( 89 _build_url("/auth/login"), json=payload, verify=projectal.__verify 90 ) 91 # Handle errors here 92 if response.status_code == 200 and response.json()["status"] == "OK": 93 projectal.cookies = response.cookies 94 projectal.api_auth_details = auth_details() 95 return True 96 raise LoginException("Check the API URL and your credentials") 97 98 99def auth_details(): 100 """ 101 Returns some details about the currently logged-in user account, 102 including all permissions available to it. 103 """ 104 return projectal.get("/api/user/details") 105 106 107def permission_list(): 108 """ 109 Returns a list of all permissions that exist in Projectal. 110 """ 111 return projectal.get("/api/permission/list") 112 113 114def ldap_sync(): 115 """Initiate an on-demand user sync with the LDAP/AD server configured in your 116 Projectal server settings. If not configured, returns a HTTP 405 error.""" 117 return projectal.post("/api/ldap/sync", None) 118 119 120def query(payload): 121 """ 122 Executes a query and returns the result. See the 123 [Query API](https://projectal.com/docs/latest#tag/Query) for details. 124 """ 125 return projectal.post("/api/query/match", payload) 126 127 128def date_from_timestamp(date): 129 """Returns a date string from a timestamp. 130 E.g., `1647561600000` returns `2022-03-18`.""" 131 if not date: 132 return None 133 return str(datetime.utcfromtimestamp(int(date) / 1000).date()) 134 135 136def timestamp_from_date(date): 137 """Returns a timestamp from a date string. 138 E.g., `2022-03-18` returns `1647561600000`.""" 139 if not date: 140 return None 141 return int( 142 datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() 143 * 1000 144 ) 145 146 147def timestamp_from_datetime(date): 148 """Returns a timestamp from a datetime string. 149 E.g. `2022-03-18 17:00` returns `1647622800000`.""" 150 if not date: 151 return None 152 return int( 153 datetime.strptime(date, "%Y-%m-%d %H:%M") 154 .replace(tzinfo=timezone.utc) 155 .timestamp() 156 * 1000 157 ) 158 159 160def post(endpoint, payload=None, file=None, is_json=True): 161 """HTTP POST to the Projectal server.""" 162 return __request("post", endpoint, payload, file=file, is_json=is_json) 163 164 165def get(endpoint, payload=None, is_json=True): 166 """HTTP GET to the Projectal server.""" 167 return __request("get", endpoint, payload, is_json=is_json) 168 169 170def delete(endpoint, payload=None): 171 """HTTP DELETE to the Projectal server.""" 172 return __request("delete", endpoint, payload) 173 174 175def put(endpoint, payload=None, file=None, form=False): 176 """HTTP PUT to the Projectal server.""" 177 return __request("put", endpoint, payload, file=file, form=form) 178 179 180def __request(method, endpoint, payload=None, file=None, form=False, is_json=True): 181 """ 182 Make an API request. If this is the first request made in the module, 183 this function will issue a login API call first. 184 185 Additionally, if the response claims an expired JWT, the function 186 will issue a login API call and try the request again (max 1 try). 187 """ 188 if not projectal.cookies: 189 projectal.login() 190 fun = getattr(requests, method) 191 kwargs = {} 192 if file: 193 kwargs["files"] = file 194 kwargs["data"] = payload 195 elif form: 196 kwargs["data"] = payload 197 else: 198 kwargs["json"] = payload 199 response = fun( 200 _build_url(endpoint), 201 cookies=projectal.cookies, 202 verify=projectal.__verify, 203 **kwargs 204 ) 205 206 try: 207 # Raise error for non-200 response 208 response.raise_for_status() 209 except HTTPError: 210 # If the error is from an expired JWT we can retry it by 211 # clearing the cookie. (Login happens on next call). 212 try: 213 r = response.json() 214 if ( 215 r.get("status", None) == "UNAUTHORIZED" 216 or r.get("message", None) == "anonymousUser" 217 or r.get("error", None) == "Unauthorized" 218 ): 219 projectal.cookies = None 220 return __request(method, endpoint, payload, file) 221 except JSONDecodeError: 222 pass 223 raise ProjectalException(response) from None 224 225 # We will treat a partial success as failure - we cannot silently 226 # ignore some errors 227 if response.status_code == 207: 228 raise ProjectalException(response) 229 230 if not is_json: 231 return response 232 try: 233 payload = response.json() 234 # Fail if the status code in the response body (not the HTTP code!) 235 # does not match what we expect for the API endpoint. 236 __maybe_fail_status(response, payload) 237 # If we have a timestamp, record it for whoever is interested 238 if "timestamp" in payload: 239 projectal.response_timestamp = payload["timestamp"] 240 else: 241 projectal.response_timestamp = None 242 243 # If we have a 'jobCase', return the data it points to, which is 244 # what the caller is after (saves them having to do it every time). 245 if "jobCase" in payload: 246 return payload[payload["jobCase"]] 247 return payload 248 except JSONDecodeError: 249 # API always responds with JSON. If not, it's an error 250 raise ProjectalException(response) from None 251 252 253def __maybe_fail_status(response, payload): 254 """ 255 Check the status code in the body of the response. Raise 256 a `ProjectalException` if it does not match the "good" 257 status for that request. 258 259 The code is "OK" for everything, but /create returns "CREATED". 260 Luckily for us, /create also returns a 201, so we know which 261 codes to match up. 262 263 Requests with no 'status' key are assumed to be good. 264 """ 265 expected = "OK" 266 if response.status_code == 201: 267 expected = "CREATED" 268 269 got = payload.get("status", expected) 270 if expected == got: 271 return True 272 m = "Unexpected response calling {}. Expected status: {}. Got: {}".format( 273 response.url, expected, got 274 ) 275 raise ProjectalException(response, m) 276 277 278def _build_url(endpoint): 279 req = PreparedRequest() 280 url = projectal.api_base.rstrip("/") + endpoint 281 params = {"alias": projectal.api_alias} 282 req.prepare_url(url, params) 283 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 = {"username": projectal.api_username, "password": projectal.api_password} 87 if projectal.api_application_id: 88 payload["applicationId"] = projectal.api_application_id 89 response = requests.post( 90 _build_url("/auth/login"), json=payload, verify=projectal.__verify 91 ) 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")
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.
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")
Returns some details about the currently logged-in user account, including all permissions available to it.
108def permission_list(): 109 """ 110 Returns a list of all permissions that exist in Projectal. 111 """ 112 return projectal.get("/api/permission/list")
Returns a list of all permissions that exist in Projectal.
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)
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.
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)
Executes a query and returns the result. See the Query API for details.
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())
Returns a date string from a timestamp.
E.g., 1647561600000
returns 2022-03-18
.
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( 143 datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp() 144 * 1000 145 )
Returns a timestamp from a date string.
E.g., 2022-03-18
returns 1647561600000
.
148def timestamp_from_datetime(date): 149 """Returns a timestamp from a datetime string. 150 E.g. `2022-03-18 17:00` returns `1647622800000`.""" 151 if not date: 152 return None 153 return int( 154 datetime.strptime(date, "%Y-%m-%d %H:%M") 155 .replace(tzinfo=timezone.utc) 156 .timestamp() 157 * 1000 158 )
Returns a timestamp from a datetime string.
E.g. 2022-03-18 17:00
returns 1647622800000
.
161def post(endpoint, payload=None, file=None, is_json=True): 162 """HTTP POST to the Projectal server.""" 163 return __request("post", endpoint, payload, file=file, is_json=is_json)
HTTP POST to the Projectal server.
166def get(endpoint, payload=None, is_json=True): 167 """HTTP GET to the Projectal server.""" 168 return __request("get", endpoint, payload, is_json=is_json)
HTTP GET to the Projectal server.
171def delete(endpoint, payload=None): 172 """HTTP DELETE to the Projectal server.""" 173 return __request("delete", endpoint, payload)
HTTP DELETE to the Projectal server.
176def put(endpoint, payload=None, file=None, form=False): 177 """HTTP PUT to the Projectal server.""" 178 return __request("put", endpoint, payload, file=file, form=form)
HTTP PUT to the Projectal server.