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 = {
     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    """Returns 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

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 = {
    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.

def auth_details(): View Source

    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.

def permission_list(): View Source

    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.

def ldap_sync(): View Source

    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.

def query(payload): View Source

    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.

def date_from_timestamp(date): View Source

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

def timestamp_from_date(date): View Source

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

def timestamp_from_datetime(date): View Source

    145def timestamp_from_datetime(date):
    146    """Returns 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)

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

    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.

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

    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.

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

    163def delete(endpoint, payload=None):
    164    """HTTP DELETE to the Projectal server."""
    165    return __request('delete', endpoint, payload)

HTTP DELETE to the Projectal server.

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

    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.