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"""
     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

def status(): View Source

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

def login(): View Source

    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.

def auth_details(): View Source

    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.

def permission_list(): View Source

    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.

def ldap_sync(): View Source

    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.

def query(payload): View Source

    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.

def date_from_timestamp(date): View Source

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

def timestamp_from_date(date): View Source

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

def timestamp_from_datetime(date): View Source

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

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

    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.

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

    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.

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

    171def delete(endpoint, payload=None):
    172    """HTTP DELETE to the Projectal server."""
    173    return __request("delete", endpoint, payload)

HTTP DELETE to the Projectal server.

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

    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.