projectal API documentation

# projectal

A python client for the Projectal API.

## Getting started

    import projectal
    import os

    # Supply your Projectal server URL and account credentials
    api_base = 'https://yourcompany.projectal.com'
    api_username = os.environ.get('PROJECTAL_USERNAME')
    api_password = os.environ.get('PROJECTAL_PASSWORD')

    # Test communication with server
    status = projectal.status()

    # Test account credentials
    projectal.login()
    details = projectal.auth_details()

## Changelog

### 5.0.0

- Updated `projectal.login()` to use a basic Dict to store the login cookie instead of an instance of the RequestsCookieJar object from the Python Requests library. This fixes an issue where an indefinite loop can occur when the stored cookie is cleared unexpectedly after attempting to login again after a token expired.
- Update the stored cookie whenever a new cookie is returned by a successful request. This takes advantage of an updated Projectal implementation that refreshes the login cookie periodically. This should avoid the re-authentication procedure in most cases for scripts with long execution times.

### 4.3.2

- Added CompanyType.Division enum, matching new system defaults.

### 4.3.1

- Fixed typo in "Getting started" example.
- Fixed typo in 4.3.0 Changelog.

### 4.3.0

Breaking changes:

- Added "PREDECESSOR_TASK" option when fetching projectal.Task with links
- Predecessor tasks will be returned as a list of tasks under the taskList attribute
- This attribute name is different to what you would see when using the REST API directly (planList). This change was necessary to allow for directly manipulating the list of links and then saving the task entity to commit any changes, since the REST API is expecting a different key for linking calls (taskList).
- Existing Predecessor Task linking methods were also updated to match the new linking functionality. They now work as a reverse linker, automatically inverting the link relationship between the task entities. This more closely matches what you would expect to see based on the Web UI. I.e. When previously calling `some_task.link_predecessor_task(another_task)`, some_task would be set as a predecessor for another_task instead of the other way around. Now another_task would be set as the predecessor for some_task.

### 4.2.2

- `projectal.User.current_user_permissions()` fixed incorrect query.
- `projectal.Webhook.list()` default limit increased to 1000.

### 4.2.1

- Added classes allowing for the management of dynamic enums. The user must have the "List Management" permission to update enums.

  New classes:

  - `projectal.CompanyTypes`
  - `projectal.SkillLevels`
  - `projectal.StaffTypes`
  - `projectal.PriorityLevels`
  - `projectal.ComplexityLevels`
  - `projectal.CurrencyList`

  The current enum can be retrieved with get(), and updated with set(). For example, to return the current SkillLevels enum:

      projectal.SkillLevels.get()

  For each enum the entire list of key value pairs must be provided when calling set(), any existing values that are omitted from the dictionary will be removed, and any additional values will be added.

  To update the SkillLevels enum with a new value:

      new_value_added = {
          "Senior": 10,
          "Mid": 20,
          "Junior": 30,
          # new SkillLevel value "Beginner"
          "Beginner": 40,
      }
      projectal.SkillLevels.set(new_value_added)

  To change the name of a value, set a new key name for the original value:

      updated_value_name = {
          # changing "Senior" SkillLevel to "Expert"
          "Expert": 10,
          "Mid": 20,
          "Junior": 30,
      }
      projectal.SkillLevels.set(updated_value_name)

  To remove an existing value, call set on a dictionary with that value removed:

      value_removed = {
          "Senior": 10,
          "Mid": 20,
          # "Junior" SkillLevel removed
      }
      projectal.SkillLevels.set(new_value_added)

  Updating the CurrencyList works differently to the other enums, since the names of values must match the alphabetic currency code and the value must match the numeric currency code. This will cause an exception if you try to change the name for any values.

  Adding a new currency:

      new_currency_added = {
        "AED": 784
        ...
        # rest of the existing currencies
        ...
        # new currency to add with the alphabetic and numeric code
        "ZWL": 932,
      }
      projectal.CurrencyList.set(new_currency_added)

  Removing an existing currency requires you to provide the numeric code for the currency as a negative value.

      currency_removed = {
        # this currency will be removed
        "AED": -784
        ...
        # rest of the existing currencies
        ...
      }
      projectal.CurrencyList.set(currency_removed)

### 4.2.0

Version 4.2.0 accompanies the release of Projectal 4.1.0

- Minimum Projectal version is now 4.1.0.

- Changed order of applying link types when an entity is initialized, prevents a type error with reverse linking in certain situations.

- `projectal.Task.reset_duration()` now supports adjustments with multi day calendar exceptions.

- `projectal.Task.reset_duration()` location working days override base exceptions.

- `projectal.TaskTemplate.list()` fixed incorrect query when using inherited method.

### 4.1.0

- DateLimit.Max enum value changed from "9999-12-31" to "3000-01-01". This reflects changes to the Projectal backend that defines this as the maximum allowable date value. The front end typically considers this value as equivalent with having no end date.

- Updated requirements.txt version for requests package

- Minimum Projectal version is now 4.0.40

### 4.0.3

- When a dict object is passed to the update class method, it will be converted to the corresponding Entity type. Allows for proper handling of keys that require being treated as links.

### 4.0.2

- Booking entity is now fetched with project field and either staff or resource field.

- Added missing link methods for 'Booking' entity (Note, File)

- Added missing link methods for 'Activity' entity (Booking, Note, File, Rebate)

- Reduced maximum number of link methods to 100 for a single batch request to prevent timeouts under heavy load.

### 4.0.1

- Minimum Projectal version is now 4.0.0.

### 4.0.0

Version 4.0.0 accompanies the release of Projectal 4.0.

- Added the `Activity` entity, new in Projectal 4.0.

- Added the `Booking` entity, new in Projectal 4.0.

### 3.1.1

- Link requests generated by 'projectal.Entity.create()' and 'projectal.Entity.update()' are now executed in batches. This is enabled by default with the 'batch_linking=True' parameter and can be disabled to execute each link request individually. It is recommended to leave this parameter enabled as this can greatly reduce the number of network requests.

### 3.1.0

- Minimum Projectal version is now 3.1.5.

- Added `projectal.Webhook.list_events()`. See API doc for details on how to use.

- Added `deleted_at` parameter to `projectal.Entity.get()`. This value should be a UTC timestamp from a webhook delete event.

- Added `projectal.ldap_sync()` to initiate a user sync with the LDAP/AD service configured in the Projectal server settings.

- Enhanced output of `projectal.Entity.changes()` function when reporting link changes. It no longer dumps the entire before-and-after list with the full content of each linked entity. Now reports three lists: `added`, `updated`, `removed`. Entities within the `updated` list follow the same `old` vs `new` dictionary model for the data attributes within them. E.g:

      resourceList: [
          'added': [],
          'updated': [
              {'uuId': '14eb4c31-0f92-49d1-8b4d-507ab939003e', 'resourceLink': {'utilization': {'old': 0.1, 'new': 0.9}}},
          ],
          'removed': []
      ]

  This should result in slimmer logs that are much easier to understand as the changes are clearly indicated.

### 3.0.2

- Added `projectal.Entity.get_link_definitions()`. Exposes entity link definition dictionary. Consumers can inspect which links an Entity knows about and their internal settings. Link definitions that appear here are the links valid for `links=[]` parameters.

### 3.0.1

- Fixed fetching project with links=['task'] not being available.

- Improved Permission.list(). Now returns a dict with the permission name as key with Permission objects as the value (instead of list of uuIds).

- Added a way to use the aliasing feature of the API (new in Projectal 3.0). Set `api_alias = 'uuid'` to the UUID of a User object and all requests made will be done as that user. Restore this value to None to resume normal operation. (Some rules and limitations apply. See API for more details.)

- Added complete support for the Tags entity (including linkers).

### 3.0

Version 3.0 accompanies the release of Projectal 3.0.

Breaking changes:

- The `links` parameter on `Entity` functions now consumes a list of entity names instead of a comma-separated string. For example:

      # Before:
      projectal.Staff.get('<uuid>', links='skill,location')  # No longer valid
      # Now:
      projectal.Staff.get('<uuid>', links=['skill', 'location'])

- The `projectal.enums.SkillLevel` enum has had all values renamed to match the new values used in Projectal (Junior, Mid, Senior). This includes the properties on Skill entities indicating work time for auto-scheduling (now `juniorLevel`, `midLevel`, `seniorLevel`).

Other changes:

- Working with entity links has changed in this release. The previous methods are still available and continue to work as before, but there is no need to interact with the `projectal.linkers` methods yourself anymore.

  You can now modify the list of links within an entity and save the entity directly. The library will automatically determine how the links have been modified and issue the correct linker methods on your behalf. E.g., you can now do:

      staff = projectal.Staff.get('<uuid>', links=['skill'])
      staff['firstName'] = "New name"  # Field update
      staff['skillList'] = [skill1, skill2, skill3]  # Link update
      staff.save()  # Both changes are saved

      task = projectal.Task.get('<uuid>', links=['stage'])
      task['stage'] = stage1  # Uses a single object instead of list
      task.save()

  See `examples/linking.py` for a more complete demonstration of linking capabilities and limitations.

- Linkers (`projectal.linkers`) can now be given a list of Entities (of one type) to link/unlink/relink in bulk. E.g:

      staff.unlink_skill(skill1)  # Before
      staff.unlink_skill([skill1, skill2, skill3])  # This works now too

- Linkers now strip the payload to only the required fields instead of passing on the entire Entity object. This cuts down on network traffic significantly.

- Linkers now also work in reverse. The Projectal server currently only supports linking entities in one direction (e.g., Company to Staff), which often means writing something like:

      staff.link_location(location)
      company.link_staff(staff)

  The change in direction is not very intuitive and would require you to constantly verify which direction is the one available to you in the documentation.

  Reverse linkers hide this from you and figure out the direction of the relationship for you behind the scenes. So now this is possible, even though the API doesn't strictly support it:

      staff.link_location(location)
      staff.link_company(company)

  Caveat: the documentation for Staff will not list Company links. You will still have to look up the Company documentation for the link description.

- Requesting entity links with the `links=` parameter will now always ensure the link field (e.g., `taskList`) exists in the result, even if there are no links. The server may not always return a value, but we can use a default value ([] for lists, None for dicts).

- Added a `Permission` entity to correctly type Permissions in responses.

- Added a `Tag` entity, new in Projectal 3.0.

- Added `links` parameter to `Company.get_primary_company()`

- `Department.tree()`: now consumes a `holder` Entity object instead of a uuId.

- `Department.tree()`: added `generic_staff` parameter, new in Projectal 3.0.

- Don't break on trailing slash in Projectal URL

- When creating tasks, populate the `projectRef` and `parent` fields in the returned Task object.

- Added convenience functions for matching on fields where you only want one result (e.g match_one()) which return the first match found.

- Update the entity `history()` method for Projectal 3.0. Some new parameters allow you to restrict the history to a particular range or to get only the changes for a webhook timestamp.

- Entity objects can call `.history()` on themselves.

- The library now keeps a reference to the User account that is currently logged in and using the API: `api_auth_details`.

Known issues:

- You cannot save changes to Notes or Calendars via their holding entity. You must save the changes on the Note or Calendar directly. To illustrate:

      staff = projectal.Staff.get(<uuid>, links=['calendar'])
      calendar = staff['calendarList'][0]
      calendar['name'] = 'Calendar 2'

      # Cannot do this - will not pick up the changes
      staff.save()

      # You must do this for now
      calendar.save()

This will be resolved in a future release.

- When creating Notes, the `created` and `modified` values may differ by 1ms in the object you have a reference to compared to what is actually stored in the database.

- Duration calculation is not precise yet (mentioned in 2.1.0)

### 2.1.0

**Breaking changes**: - Getting location calendar is now done on an instance instead of class. So `projectal.Location.calendar(uuid)` is now simply `location.calendar()` - The `CompanyType.Master` enum has been replaced with `CompanyType.Primary`. This was a leftover reference to the Master Company which was renamed in Projectal several versions ago. **Other changes**: - Date conversion functions return None when given None or empty string - Added `Task.reset_duration()` as a basic duration calculator for tasks. This is a work-in-progress and will be gradually improved. The duration calculator takes into consideration the location to remove non-work days from the estimate of working duration. It currently does not work for the time component or `isWorking=True` exceptions. - Change detection in `Entity.changes()` now excludes cases where the server has no value and the new value is None. Saving this change has no effect and would always detect a change until a non-None value is set, which is noisy and generates more network activity.

### 2.0.3

- Better support for calendars.
  - Distinguish between calendar containers ("Calendar") and the calendar items within them ("CalendarItem").
  - Allow CalendarItems to be saved directly. E.G item.save()

- Fix 'holder' parameter in contact/staff/location/task_template not permitting object type. Now consumes uuId or object to match rest of the library.
- `Entity.changes()` has been extended with an `old=True` flag. When this flag is true, the set of changes will now return both the original and the new values. E.g.

md5-00b181fcc1619305e4f44557fe69cb6b

- Fixed entity link cache causing errors when deleting a link from an entity which has not been fetched with links (deleting from empty list).

### 2.0.2

- Fixed updating Webhook entities

### 2.0.1

- Fixed application ID not being used correctly.

### 2.0.0

- Version 2.0 accompanies the release of Projectal 2.0. There are no major changes since the previous release.
- Expose `Entity.changes()` function. It returns a list of fields on an entity that have changed since fetching it. These are the changes that will be sent over to the server when an update request is made.
- Added missing 'packaging' dependency to requirements.

### 1.2.0

**Breaking changes**:

- Renamed `request_timestamp` to `response_timestamp` to better reflect its purpose.

- Automatic timestamp conversion into dates (introduced in `1.1.0`) has been reverted. All date fields returned from the server remain as UTC timestamps.

  The reason is that date fields on tasks contain a time component and converting them into date strings was erasing the time, resulting in a value that does not match the database.

  Note: the server supports setting date fields using a date string like `2022-04-05`. You may use this if you prefer but the server will always return a timestamp.

  Note: we provide utility functions for easily converting dates from/to timestamps expected by the Projectal server. See: `projectal.date_from_timestamp()`,`projectal.timestamp_from_date()`, and `projectal.timestamp_from_datetime()`.

**Other changes**: - Implement request chunking - for methods that consume a list of entities, we now automatically batch them up into multiple requests to prevent timeouts on really large request. Values are configurable through `chunk_size_read` and `chunk_size_write`. Default values: Read: 1000 items. Write: 200 items. - Added profile get/set functions on entities for easier use. Now you only need to supply the key and the data. E.g: md5-48915080a3985b61468968b47d64a23c

- Entity link methods now automatically update the entity's cached list of links. E.g: a task fetched with staff links will have `task['staffList'] = [Staff1,Staff2]`. Before, doing a `task.link_staff(staff)` did not modify the list to reflect the addition. Now, it will turn into `[Staff1,Staff2,Staff3]`. The same applies for update and delete.

  This allows you to modify links and continue working with that object without having to fetch it again to obtain the most recent link data. Be aware that if you acquire the object without requesting the link data as well (e.g: `projectal.Task.get(id, links='STAFF')`), these lists will not accurately reflect what's in the database, only the changes made while the object is held.

- Support new `applicationId` property on login. Set with: `api_application_id`. The application ID is sent back to you in webhooks so you know which application was the source of the event (and you can choose to filter them accordingly).

- Added `Entity.set_readonly()` to allow setting values on entities that will not be sent over to the server when updating/saving the entity.

  The main use case for this is to populate cached entities which you have just created with values you already know about. This is mainly a workaround for the limitation of the server not sending the full object back after creating it, resulting in the client needing to fetch the object in full again if it needs some of the fields set by the server after creation.

  Additionally, some read-only fields will generate an error on the server if included in the update request. This method lets you set these values on newly created objects without triggering this error.

  A common example is setting the `projectRef` of a task you just created.

### 1.1.1

- Add support for 'profiles' API. Profiles are a type of key-value storage that target any entity. Not currently documented.
- Fix handling error message parsing in ProjectalException for batch create operation
- Add `Task.update_order()` to set task order
- Return empty list when GETing empty list instead of failing (no request to server)
- Expose the timestamp returned by requests that modify the database. Use `projectal.request_timestamp` to get the value of the most recent request (None if no timestamp in response)

### 1.1.0

- Minimum Projectal version is now 1.9.4.

Breaking changes:

- Entity `list()` now returns a list of UUIDs instead of full objects. You may provide an `expand` parameter to restore the previous behavior: `Entity.list(expand=True)`. This change is made for performance reasons where you may have thousands of tasks and getting them all may time out. For those cases, we suggest writing a query to filter down to only the tasks and fields you need.
- `Company.get_master_company()` has been renamed to `Company.get_primary_company()` to match the server.
- The following date fields are converted into date strings upon fetch: `startTime`, `closeTime`, `scheduleStart`, `scheduleFinish`. These fields are added or updated using date strings (like `2022-03-02`), but the server returns timestamps (e.g: 1646006400000) upon fetch, which is confusing. This change ensures they are always date strings for consistency.

Other changes:

- When updating an entity, only the fields that have changed are sent to the server. When updating a list of entities, unmodified entities are not sent to the server at all. This dramatically reduces the payload size and should speed things up.
- When fetching entities, entity links are now typed as well. E.g. `project['rebateList']` contains a list of `Rebate` instead of `dict`.
- Added `date_from_timestamp()` and `timestamp_from_date()` functions to help with converting to/from dates and Projectal timestamps.
- Entity history now uses `desc` by default (index 0 is newest)
- Added `Project.tasks()` to list all task UUIDs within a project.

### 1.0.3

- Fix another case of automatic JWT refresh not working

### 1.0.2

- Entity instances can `save()` or `delete()` on themselves
- Fix broken `dict` methods (`get()` and `update()`) when called from Entity instances
- Fix automatic JWT refresh only working in some cases

### 1.0.1

- Added `list()` function for all entities
- Added search functions for all entities (match-, search, query)
- Added `Company.get_master_company()`
- Fixed adding template tasks

View Source

      1"""
      2A python client for the [Projectal API](https://projectal.com/docs/latest).
      3
      4## Getting started
      5
      6```
      7import projectal
      8import os
      9
     10# Supply your Projectal server URL and account credentials
     11projectal.api_base = 'https://yourcompany.projectal.com'
     12projectal.api_username = os.environ.get('PROJECTAL_USERNAME')
     13projectal.api_password = os.environ.get('PROJECTAL_PASSWORD')
     14
     15# Test communication with server
     16status = projectal.status()
     17
     18# Test account credentials
     19projectal.login()
     20details = projectal.auth_details()
     21```
     22
     23----
     24
     25## Changelog
     26
     27### 5.0.0
     28- Updated `projectal.login()` to use a basic Dict to store the login cookie instead of an instance
     29  of the RequestsCookieJar object from the Python Requests library. This fixes an issue where an
     30  indefinite loop can occur when the stored cookie is cleared unexpectedly after attempting to login
     31  again after a token expired.
     32- Update the stored cookie whenever a new cookie is returned by a successful request. This takes
     33  advantage of an updated Projectal implementation that refreshes the login cookie periodically.
     34  This should avoid the re-authentication procedure in most cases for scripts with long execution
     35  times.
     36
     37### 4.3.2
     38- Added CompanyType.Division enum, matching new system defaults.
     39
     40### 4.3.1
     41- Fixed typo in "Getting started" example.
     42- Fixed typo in 4.3.0 Changelog.
     43
     44### 4.3.0
     45
     46**Breaking changes:**
     47
     48- Added "PREDECESSOR_TASK" option when fetching projectal.Task with links
     49- Predecessor tasks will be returned as a list of tasks under the taskList attribute
     50- This attribute name is different to what you would see when using the REST API directly
     51  (planList). This change was necessary to allow for directly manipulating
     52  the list of links and then saving the task entity to commit any changes,
     53  since the REST API is expecting a different key for linking calls (taskList).
     54- Existing Predecessor Task linking methods were also updated to match the new linking functionality.
     55  They now work as a reverse linker, automatically inverting the link relationship between
     56  the task entities. This more closely matches what you would expect to see based on the Web UI.
     57  I.e. When previously calling `some_task.link_predecessor_task(another_task)`,
     58  some_task would be set as a predecessor for another_task instead of the other way around.
     59  Now another_task would be set as the predecessor for some_task.
     60
     61### 4.2.2
     62- `projectal.User.current_user_permissions()` fixed incorrect query.
     63- `projectal.Webhook.list()` default limit increased to 1000.
     64
     65### 4.2.1
     66- Added classes allowing for the management of dynamic enums. The user must have the "List Management"
     67  permission to update enums.
     68
     69  New classes:
     70  - `projectal.CompanyTypes`
     71  - `projectal.SkillLevels`
     72  - `projectal.StaffTypes`
     73  - `projectal.PriorityLevels`
     74  - `projectal.ComplexityLevels`
     75  - `projectal.CurrencyList`
     76
     77  The current enum can be retrieved with get(), and updated with set().
     78  For example, to return the current SkillLevels enum:
     79
     80  ```
     81  projectal.SkillLevels.get()
     82  ```
     83
     84  For each enum the entire list of key value pairs must be provided when calling set(),
     85  any existing values that are omitted from the dictionary will be removed,
     86  and any additional values will be added.
     87
     88  To update the SkillLevels enum with a new value:
     89
     90  ```
     91  new_value_added = {
     92      "Senior": 10,
     93      "Mid": 20,
     94      "Junior": 30,
     95      # new SkillLevel value "Beginner"
     96      "Beginner": 40,
     97  }
     98  projectal.SkillLevels.set(new_value_added)
     99  ```
    100
    101  To change the name of a value, set a new key name for the original value:
    102
    103  ```
    104  updated_value_name = {
    105      # changing "Senior" SkillLevel to "Expert"
    106      "Expert": 10,
    107      "Mid": 20,
    108      "Junior": 30,
    109  }
    110  projectal.SkillLevels.set(updated_value_name)
    111  ```
    112
    113  To remove an existing value, call set on a dictionary with that value removed:
    114
    115  ```
    116  value_removed = {
    117      "Senior": 10,
    118      "Mid": 20,
    119      # "Junior" SkillLevel removed
    120  }
    121  projectal.SkillLevels.set(new_value_added)
    122  ```
    123
    124  Updating the CurrencyList works differently to the other enums, since the
    125  names of values must match the alphabetic currency code and the value must
    126  match the numeric currency code.
    127  This will cause an exception if you try to change the name for any values.
    128
    129  Adding a new currency:
    130
    131  ```
    132  new_currency_added = {
    133    "AED": 784
    134    ...
    135    # rest of the existing currencies
    136    ...
    137    # new currency to add with the alphabetic and numeric code
    138    "ZWL": 932,
    139  }
    140  projectal.CurrencyList.set(new_currency_added)
    141  ```
    142
    143  Removing an existing currency requires you to provide the numeric code for
    144  the currency as a negative value.
    145
    146  ```
    147  currency_removed = {
    148    # this currency will be removed
    149    "AED": -784
    150    ...
    151    # rest of the existing currencies
    152    ...
    153  }
    154  projectal.CurrencyList.set(currency_removed)
    155  ```
    156
    157### 4.2.0
    158Version 4.2.0 accompanies the release of Projectal 4.1.0
    159
    160- Minimum Projectal version is now 4.1.0.
    161
    162- Changed order of applying link types when an entity is initialized,
    163prevents a type error with reverse linking in certain situations.
    164
    165- `projectal.Task.reset_duration()` now supports adjustments with multi day calendar exceptions.
    166
    167- `projectal.Task.reset_duration()` location working days override base exceptions.
    168
    169- `projectal.TaskTemplate.list()` fixed incorrect query when using inherited method.
    170
    171### 4.1.0
    172- DateLimit.Max enum value changed from "9999-12-31" to "3000-01-01". This reflects changes to the Projectal
    173backend that defines this as the maximum allowable date value. The front end typically considers this value as
    174equivalent with having no end date.
    175
    176- Updated requirements.txt version for requests package
    177
    178- Minimum Projectal version is now 4.0.40
    179
    180### 4.0.3
    181- When a dict object is passed to the update class method, it will be converted to the corresponding Entity type.
    182  Allows for proper handling of keys that require being treated as links.
    183
    184### 4.0.2
    185- Booking entity is now fetched with project field and either staff or resource field.
    186
    187- Added missing link methods for 'Booking' entity (Note, File)
    188
    189- Added missing link methods for 'Activity' entity (Booking, Note, File, Rebate)
    190
    191- Reduced maximum number of link methods to 100 for a single batch request to prevent timeouts
    192under heavy load.
    193
    194### 4.0.1
    195- Minimum Projectal version is now 4.0.0.
    196
    197### 4.0.0
    198
    199Version 4.0.0 accompanies the release of Projectal 4.0.
    200
    201- Added the `Activity` entity, new in Projectal 4.0.
    202
    203- Added the `Booking` entity, new in Projectal 4.0.
    204
    205### 3.1.1
    206- Link requests generated by 'projectal.Entity.create()' and 'projectal.Entity.update()' are now
    207  executed in batches. This is enabled by default with the 'batch_linking=True' parameter and can
    208  be disabled to execute each link request individually. It is recommended to leave this parameter
    209  enabled as this can greatly reduce the number of network requests.
    210
    211### 3.1.0
    212- Minimum Projectal version is now 3.1.5.
    213
    214- Added `projectal.Webhook.list_events()`. See API doc for details on how to use.
    215
    216- Added `deleted_at` parameter to `projectal.Entity.get()`. This value should be a UTC timestamp
    217  from a webhook delete event.
    218
    219- Added `projectal.ldap_sync()` to initiate a user sync with the LDAP/AD service configured in
    220  the Projectal server settings.
    221
    222- Enhanced output of `projectal.Entity.changes()` function when reporting link changes.
    223  It no longer dumps the entire before-and-after list with the full content of each linked entity.
    224  Now reports three lists: `added`, `updated`, `removed`. Entities within the `updated` list
    225  follow the same `old` vs `new` dictionary model for the data attributes within them. E.g:
    226
    227    ```
    228    resourceList: [
    229        'added': [],
    230        'updated': [
    231            {'uuId': '14eb4c31-0f92-49d1-8b4d-507ab939003e', 'resourceLink': {'utilization': {'old': 0.1, 'new': 0.9}}},
    232        ],
    233        'removed': []
    234    ]
    235    ```
    236  This should result in slimmer logs that are much easier to understand as the changes are
    237  clearly indicated.
    238
    239### 3.0.2
    240- Added `projectal.Entity.get_link_definitions()`. Exposes entity link definition dictionary.
    241  Consumers can inspect which links an Entity knows about and their internal settings.
    242  Link definitions that appear here are the links valid for `links=[]` parameters.
    243
    244### 3.0.1
    245- Fixed fetching project with links=['task'] not being available.
    246
    247- Improved Permission.list(). Now returns a dict with the permission name as
    248  key with Permission objects as the value (instead of list of uuIds).
    249
    250- Added a way to use the aliasing feature of the API (new in Projectal 3.0).
    251Set `projectal.api_alias = 'uuid'` to the UUID of a User object and all
    252requests made will be done as that user. Restore this value to None to resume
    253normal operation. (Some rules and limitations apply. See API for more details.)
    254
    255- Added complete support for the Tags entity (including linkers).
    256
    257### 3.0
    258
    259Version 3.0 accompanies the release of Projectal 3.0.
    260
    261**Breaking changes**:
    262
    263- The `links` parameter on `Entity` functions now consumes a list of entity
    264  names instead of a comma-separated string. For example:
    265
    266    ```
    267    # Before:
    268    projectal.Staff.get('<uuid>', links='skill,location')  # No longer valid
    269    # Now:
    270    projectal.Staff.get('<uuid>', links=['skill', 'location'])
    271    ```
    272
    273- The `projectal.enums.SkillLevel` enum has had all values renamed to match the new values
    274  used in Projectal (Junior, Mid, Senior). This includes the properties on
    275  Skill entities indicating work time for auto-scheduling (now `juniorLevel`,
    276  `midLevel`, `seniorLevel`).
    277
    278**Other changes**:
    279
    280- Working with entity links has changed in this release. The previous methods
    281  are still available and continue to work as before, but there is no need
    282  to interact with the `projectal.linkers` methods yourself anymore.
    283
    284  You can now modify the list of links within an entity and save the entity
    285  directly. The library will automatically determine how the links have been
    286  modified and issue the correct linker methods on your behalf. E.g.,
    287  you can now do:
    288
    289    ```
    290    staff = projectal.Staff.get('<uuid>', links=['skill'])
    291    staff['firstName'] = "New name"  # Field update
    292    staff['skillList'] = [skill1, skill2, skill3]  # Link update
    293    staff.save()  # Both changes are saved
    294
    295    task = projectal.Task.get('<uuid>', links=['stage'])
    296    task['stage'] = stage1  # Uses a single object instead of list
    297    task.save()
    298    ```
    299
    300  See `examples/linking.py` for a more complete demonstration of linking
    301  capabilities and limitations.
    302
    303- Linkers (`projectal.linkers`) can now be given a list of Entities (of one
    304 type) to link/unlink/relink in bulk. E.g:
    305    ```
    306    staff.unlink_skill(skill1)  # Before
    307    staff.unlink_skill([skill1, skill2, skill3])  # This works now too
    308    ```
    309
    310- Linkers now strip the payload to only the required fields instead of passing
    311  on the entire Entity object. This cuts down on network traffic significantly.
    312
    313- Linkers now also work in reverse. The Projectal server currently only supports
    314  linking entities in one direction (e.g., Company to Staff), which often means
    315  writing something like:
    316    ```
    317    staff.link_location(location)
    318    company.link_staff(staff)
    319    ```
    320  The change in direction is not very intuitive and would require you to constantly
    321  verify which direction is the one available to you in the documentation.
    322
    323  Reverse linkers hide this from you and figure out the direction of the relationship
    324  for you behind the scenes. So now this is possible, even though the API doesn't
    325  strictly support it:
    326    ```
    327    staff.link_location(location)
    328    staff.link_company(company)
    329    ```
    330    Caveat: the documentation for Staff will not list Company links. You will still
    331    have to look up the Company documentation for the link description.
    332
    333- Requesting entity links with the `links=` parameter will now always ensure the
    334  link field (e.g., `taskList`) exists in the result, even if there are no links.
    335  The server may not always return a value, but we can use a default value ([] for
    336  lists, None for dicts).
    337
    338- Added a `Permission` entity to correctly type Permissions in responses.
    339
    340- Added a `Tag` entity, new in Projectal 3.0.
    341
    342- Added `links` parameter to `Company.get_primary_company()`
    343
    344- `Department.tree()`: now consumes a `holder` Entity object instead
    345  of a uuId.
    346
    347- `Department.tree()`: added `generic_staff` parameter, new in
    348  Projectal 3.0.
    349
    350- Don't break on trailing slash in Projectal URL
    351
    352- When creating tasks, populate the `projectRef` and `parent` fields in the
    353  returned Task object.
    354
    355- Added convenience functions for matching on fields where you only want
    356  one result (e.g match_one()) which return the first match found.
    357
    358- Update the entity `history()` method for Projectal 3.0. Some new parameters
    359  allow you to restrict the history to a particular range or to get only the
    360  changes for a webhook timestamp.
    361
    362- Entity objects can call `.history()` on themselves.
    363
    364- The library now keeps a reference to the User account that is currently logged
    365  in and using the API: `projectal.api_auth_details`.
    366
    367**Known issues**:
    368- You cannot save changes to Notes or Calendars via their holding entity. You
    369  must save the changes on the Note or Calendar directly. To illustrate:
    370  ```
    371  staff = projectal.Staff.get(<uuid>, links=['calendar'])
    372  calendar = staff['calendarList'][0]
    373  calendar['name'] = 'Calendar 2'
    374
    375  # Cannot do this - will not pick up the changes
    376  staff.save()
    377
    378  # You must do this for now
    379  calendar.save()
    380  ```
    381  This will be resolved in a future release.
    382
    383- When creating Notes, the `created` and `modified` values may differ by
    384  1ms in the object you have a reference to compared to what is actually
    385  stored in the database.
    386
    387- Duration calculation is not precise yet (mentioned in 2.1.0)
    388
    389### 2.1.0
    390**Breaking changes**:
    391- Getting location calendar is now done on an instance instead of class. So
    392  `projectal.Location.calendar(uuid)` is now simply `location.calendar()`
    393- The `CompanyType.Master` enum has been replaced with `CompanyType.Primary`.
    394  This was a leftover reference to the Master Company which was renamed in
    395  Projectal several versions ago.
    396
    397**Other changes**:
    398- Date conversion functions return None when given None or empty string
    399- Added `Task.reset_duration()` as a basic duration calculator for tasks.
    400  This is a work-in-progress and will be gradually improved. The duration
    401  calculator takes into consideration the location to remove non-work
    402  days from the estimate of working duration. It currently does not work
    403  for the time component or `isWorking=True` exceptions.
    404- Change detection in `Entity.changes()` now excludes cases where the
    405  server has no value and the new value is None. Saving this change has
    406  no effect and would always detect a change until a non-None value is
    407  set, which is noisy and generates more network activity.
    408
    409### 2.0.3
    410- Better support for calendars.
    411  - Distinguish between calendar containers ("Calendar") and the
    412    calendar items within them ("CalendarItem").
    413  - Allow CalendarItems to be saved directly. E.G item.save()
    414- Fix 'holder' parameter in contact/staff/location/task_template not
    415  permitting object type. Now consumes uuId or object to match rest of
    416  the library.
    417- `Entity.changes()` has been extended with an `old=True` flag. When
    418  this flag is true, the set of changes will now return both the original
    419  and the new values. E.g.
    420```
    421task.changes()
    422# {'name': 'current'}
    423task.changes(old=True)
    424# {'name': {'old': 'original', 'new': 'current'}}
    425```
    426- Fixed entity link cache causing errors when deleting a link from an entity
    427  which has not been fetched with links (deleting from empty list).
    428
    429### 2.0.2
    430- Fixed updating Webhook entities
    431
    432### 2.0.1
    433- Fixed application ID not being used correctly.
    434
    435### 2.0.0
    436- Version 2.0 accompanies the release of Projectal 2.0. There are no major changes
    437  since the previous release.
    438- Expose `Entity.changes()` function. It returns a list of fields on an entity that
    439  have changed since fetching it. These are the changes that will be sent over to the
    440  server when an update request is made.
    441- Added missing 'packaging' dependency to requirements.
    442
    443### 1.2.0
    444
    445**Breaking changes**:
    446
    447- Renamed `request_timestamp` to `response_timestamp` to better reflect its purpose.
    448- Automatic timestamp conversion into dates (introduced in `1.1.0`) has been reverted.
    449  All date fields returned from the server remain as UTC timestamps.
    450
    451  The reason is that date fields on tasks contain a time component and converting them
    452  into date strings was erasing the time, resulting in a value that does not match
    453  the database.
    454
    455  Note: the server supports setting date fields using a date string like `2022-04-05`.
    456  You may use this if you prefer but the server will always return a timestamp.
    457
    458  Note: we provide utility functions for easily converting dates from/to
    459  timestamps expected by the Projectal server. See:
    460  `projectal.date_from_timestamp()`,`projectal.timestamp_from_date()`, and
    461  `projectal.timestamp_from_datetime()`.
    462
    463**Other changes**:
    464- Implement request chunking - for methods that consume a list of entities, we now
    465  automatically batch them up into multiple requests to prevent timeouts on really
    466  large request. Values are configurable through
    467  `projectal.chunk_size_read` and `projectal.chunk_size_write`.
    468  Default values: Read: 1000 items. Write: 200 items.
    469- Added profile get/set functions on entities for easier use. Now you only need to supply
    470  the key and the data. E.g:
    471
    472```
    473key = 'hr_connector'
    474data = {'staff_source': 'company_z'}
    475task.profile_set(key, data)
    476```
    477
    478- Entity link methods now automatically update the entity's cached list of links. E.g:
    479  a task fetched with staff links will have `task['staffList'] = [Staff1,Staff2]`.
    480  Before, doing a `task.link_staff(staff)` did not modify the list to reflect the
    481  addition. Now, it will turn into `[Staff1,Staff2,Staff3]`. The same applies for update
    482  and delete.
    483
    484  This allows you to modify links and continue working with that object without having
    485  to fetch it again to obtain the most recent link data. Be aware that if you acquire
    486  the object without requesting the link data as well
    487  (e.g: `projectal.Task.get(id, links='STAFF')`),
    488  these lists will not accurately reflect what's in the database, only the changes made
    489  while the object is held.
    490
    491- Support new `applicationId` property on login. Set with: `projectal.api_application_id`.
    492  The application ID is sent back to you in webhooks so you know which application was
    493  the source of the event (and you can choose to filter them accordingly).
    494- Added `Entity.set_readonly()` to allow setting values on entities that will not
    495  be sent over to the server when updating/saving the entity.
    496
    497  The main use case for this is to populate cached entities which you have just created
    498  with values you already know about. This is mainly a workaround for the limitation of
    499  the server not sending the full object back after creating it, resulting in the client
    500  needing to fetch the object in full again if it needs some of the fields set by the
    501  server after creation.
    502
    503  Additionally, some read-only fields will generate an error on the server if
    504  included in the update request. This method lets you set these values on newly
    505  created objects without triggering this error.
    506
    507  A common example is setting the `projectRef` of a task you just created.
    508
    509
    510### 1.1.1
    511- Add support for 'profiles' API. Profiles are a type of key-value storage that target
    512  any entity. Not currently documented.
    513- Fix handling error message parsing in ProjectalException for batch create operation
    514- Add `Task.update_order()` to set task order
    515- Return empty list when GETing empty list instead of failing (no request to server)
    516- Expose the timestamp returned by requests that modify the database. Use
    517  `projectal.request_timestamp` to get the value of the most recent request (None
    518  if no timestamp in response)
    519
    520### 1.1.0
    521- Minimum Projectal version is now 1.9.4.
    522
    523**Breaking changes**:
    524- Entity `list()` now returns a list of UUIDs instead of full objects. You may provide
    525  an `expand` parameter to restore the previous behavior: `Entity.list(expand=True)`.
    526  This change is made for performance reasons where you may have thousands of tasks
    527  and getting them all may time out. For those cases, we suggest writing a query to filter
    528  down to only the tasks and fields you need.
    529- `Company.get_master_company()` has been renamed to `Company.get_primary_company()`
    530  to match the server.
    531- The following date fields are converted into date strings upon fetch:
    532  `startTime`, `closeTime`, `scheduleStart`, `scheduleFinish`.
    533  These fields are added or updated using date strings (like `2022-03-02`), but the
    534  server returns timestamps (e.g: 1646006400000) upon fetch, which is confusing. This
    535  change ensures they are always date strings for consistency.
    536
    537**Other changes**:
    538- When updating an entity, only the fields that have changed are sent to the server. When
    539  updating a list of entities, unmodified entities are not sent to the server at all. This
    540  dramatically reduces the payload size and should speed things up.
    541- When fetching entities, entity links are now typed as well. E.g. `project['rebateList']`
    542  contains a list of `Rebate` instead of `dict`.
    543- Added `date_from_timestamp()` and `timestamp_from_date()` functions to help with
    544  converting to/from dates and Projectal timestamps.
    545- Entity history now uses `desc` by default (index 0 is newest)
    546- Added `Project.tasks()` to list all task UUIDs within a project.
    547
    548### 1.0.3
    549- Fix another case of automatic JWT refresh not working
    550
    551### 1.0.2
    552- Entity instances can `save()` or `delete()` on themselves
    553- Fix broken `dict` methods (`get()` and `update()`) when called from Entity instances
    554- Fix automatic JWT refresh only working in some cases
    555
    556### 1.0.1
    557- Added `list()` function for all entities
    558- Added search functions for all entities (match-, search, query)
    559- Added `Company.get_master_company()`
    560- Fixed adding template tasks
    561
    562"""
    563import logging
    564import os
    565
    566from projectal.entities import *
    567from projectal.dynamic_enums import *
    568from .api import *
    569from . import profile
    570
    571api_base = os.getenv("PROJECTAL_URL")
    572api_username = os.getenv("PROJECTAL_USERNAME")
    573api_password = os.getenv("PROJECTAL_PASSWORD")
    574api_application_id = None
    575api_auth_details = None
    576api_alias = None
    577cookies = None
    578chunk_size_read = 1000
    579chunk_size_write = 200
    580
    581# Records the timestamp generated by the last request (database
    582# event time). These are reported on add or updates; if there is
    583# no timestamp in the response, this is set to None.
    584response_timestamp = None
    585
    586
    587# The minimum version number of the Projectal instance that this
    588# API client targets. Lower versions are not supported and will
    589# raise an exception.
    590MIN_PROJECTAL_VERSION = "4.0.40"
    591
    592__verify = True
    593
    594logging.getLogger("projectal-api-client").addHandler(logging.NullHandler())

api_base = None
api_username = None
api_password = None
api_application_id = None
api_auth_details = None
api_alias = None
cookies = None
chunk_size_read = 1000
chunk_size_write = 200
response_timestamp = None
MIN_PROJECTAL_VERSION = '4.0.40'