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