projectal.api API documentation

# 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.

View Source

      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

def status(): View Source

    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).

def login(): View Source

     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.

def auth_details(): View Source

    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.

def permission_list(): View Source

    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.

def ldap_sync(): View Source

    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.

def query(payload): View Source

    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.

def date_from_timestamp(date): View Source

    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`.

def timestamp_from_date(date): View Source

    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`.

def timestamp_from_datetime(date): View Source

    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`.

def post(endpoint, payload=None, file=None, is_json=True): View Source

    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.

def get(endpoint, payload=None, is_json=True): View Source

    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.

def delete(endpoint, payload=None): View Source

    174def delete(endpoint, payload=None):
    175    """HTTP DELETE to the Projectal server."""
    176    return __request("delete", endpoint, payload)

HTTP DELETE to the Projectal server.

def put(endpoint, payload=None, file=None, form=False): View Source

    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.