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