From 4e68b664077fb31a33e8ae6ad08de7135558a0ff Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Thu, 15 Oct 2020 10:49:37 +0200 Subject: [PATCH 01/16] Raise NotImplementedError if a function is called which is not implemented --- src/personio_py/client.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 4ecc5f1..cacdd5a 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -269,8 +269,7 @@ def update_employee(self, employee: Employee): """ placeholder; not ready to be used """ - # TODO implement - pass + raise NotImplementedError() def get_attendances(self, employees: Union[int, List[int], Employee, List[Employee]], start_date: datetime = None, end_date: datetime = None) -> List[Attendance]: @@ -297,22 +296,19 @@ def create_attendances(self, attendances: List[Attendance]): """ # attendances can be created individually, but here you can push a huge bunch of items # in a single request, which can be significantly faster - # TODO implement - pass + raise NotImplementedError() def update_attendance(self, attendance_id: int): """ placeholder; not ready to be used """ - # TODO implement - pass + raise NotImplementedError() def delete_attendance(self, attendance_id: int): """ placeholder; not ready to be used """ - # TODO implement - pass + raise NotImplementedError() def get_absence_types(self) -> List[AbsenceType]: """ @@ -350,22 +346,19 @@ def get_absence(self, absence_id: int) -> Absence: """ placeholder; not ready to be used """ - # TODO implement - pass + raise NotImplementedError() def create_absence(self, absence: Absence): """ placeholder; not ready to be used """ - # TODO implement - pass + raise NotImplementedError() def delete_absence(self, absence_id: int): """ placeholder; not ready to be used """ - # TODO implement - pass + raise NotImplementedError() def _get_employee_metadata( self, path: str, resource_cls: Type[PersonioResourceType], From b052660907a0e484583392e38b28e68b4fdc7bb0 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 16 Oct 2020 09:30:09 +0200 Subject: [PATCH 02/16] Always send request bodies as json --- src/personio_py/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index cacdd5a..357b8c5 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -102,7 +102,7 @@ def request(self, path: str, method='GET', params: Dict[str, Any] = None, _headers.update(headers) # make the request url = urljoin(self.base_url, path) - response = requests.request(method, url, headers=_headers, params=params, data=data) + response = requests.request(method, url, headers=_headers, params=params, json=data) # re-new the authorization header authorization = response.headers.get('Authorization') if authorization: @@ -127,8 +127,7 @@ def request_json(self, path: str, method='GET', params: Dict[str, Any] = None, during this request (default: True for json requests) :return: the parsed json response, when the request was successful, or a PersonioApiError """ - headers = {'accept': 'application/json'} - response = self.request(path, method, params, data, headers, auth_rotation=auth_rotation) + response = self.request(path, method, params, data, auth_rotation=auth_rotation) if response.ok: try: return response.json() From 25ef834617f1a0115abc7d34ded0020408a23907 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 16 Oct 2020 09:42:43 +0200 Subject: [PATCH 03/16] Implement create_attendances function --- src/personio_py/client.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 357b8c5..2d624da 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -291,11 +291,25 @@ def get_attendances(self, employees: Union[int, List[int], Employee, List[Employ def create_attendances(self, attendances: List[Attendance]): """ - placeholder; not ready to be used - """ - # attendances can be created individually, but here you can push a huge bunch of items - # in a single request, which can be significantly faster - raise NotImplementedError() + Create all given attendance records. + + Note: Attendances are created sequentially. This function stops on first error. + All attendance records before the error will be created, all records after the error will be skipped. + + :param attendances: A list attendance records to be created. + """ + # Extract and send only the data expected by the personio API + data_to_send = [] + for attendance in attendances: + data_to_send.append({ + "employee": attendance.employee_id, + "date": attendance.date.strftime("%Y-%m-%d"), + "start_time": attendance.start_time, + "end_time": attendance.end_time, + "break": attendance.break_duration, + "comment": attendance.comment}) + response = self.request_json(path='company/attendances', method='POST', data={"attendances": data_to_send}) + return response def update_attendance(self, attendance_id: int): """ From 29232b254dc9bbce77c7395028c777670fafeb4c Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 16 Oct 2020 10:49:19 +0200 Subject: [PATCH 04/16] Implemented update_attendance function --- src/personio_py/client.py | 43 +++++++++++++++++++++++++++++---------- src/personio_py/models.py | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 2d624da..382457a 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -298,24 +298,45 @@ def create_attendances(self, attendances: List[Attendance]): :param attendances: A list attendance records to be created. """ - # Extract and send only the data expected by the personio API data_to_send = [] for attendance in attendances: - data_to_send.append({ - "employee": attendance.employee_id, - "date": attendance.date.strftime("%Y-%m-%d"), - "start_time": attendance.start_time, - "end_time": attendance.end_time, - "break": attendance.break_duration, - "comment": attendance.comment}) + data_to_send.append(attendance.to_body_params(patch_existing_attendance=False)) response = self.request_json(path='company/attendances', method='POST', data={"attendances": data_to_send}) return response - def update_attendance(self, attendance_id: int): + def update_attendance(self, attendance, remote_query_id=False): """ - placeholder; not ready to be used + Update an existing attendance record + + Either an attendance id or o remote query is required. Remote queries are only executed if required. + An Attendance object returned by get_attendances() include the attendance id. DO NOT SET THE ID YOURSELF. + + :param attendance: The Attendance object holding the new data. + :param remote_query_id: Allow a remote query for the id if it is not set within the given Attendance object. + :raises: + ValueError: If a query is required but not allowed or the query does not provide exactly one result. """ - raise NotImplementedError() + if attendance.id_ is not None: + # remote query not necessary + response = self.request_json(path='company/attendances/' + str(attendance.id_), method='PATCH', + data=attendance.to_body_params(patch_existing_attendance=True)) + return response + else: + if remote_query_id: + if attendance.employee_id is None: + raise ValueError("For a remote query an employee_id is required") + if attendance.date is None: + raise ValueError("For a remote query a date is required") + matching_remote_attendances = self.get_attendances(employees=[attendance.employee_id], + start_date=attendance.date, end_date=attendance.date) + if len(matching_remote_attendances) == 0: + raise ValueError("The attendance to patch was not found") + elif len(matching_remote_attendances) > 1: + raise ValueError("More than one attendance found.") + attendance.id_ = matching_remote_attendances[0].id_ + self.update_attendance(attendance) + else: + raise ValueError("You either need to provide the attendance id or allow a remote query.") def delete_attendance(self, attendance_id: int): """ diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 081a879..3c670af 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -660,6 +660,42 @@ def _update(self, client: 'Personio'): def _delete(self, client: 'Personio'): pass + def to_body_params(self, patch_existing_attendance=False): + """ + Return the Attendance object in the representation expected by the Personio API + + For an attendance record to be created all_values_required needs to be True. + For patch operations only the attendance id is required, but it is not + included into the body params. + + :param patch_existing_attendance Get patch body. If False a create body is returned. + """ + if patch_existing_attendance: + if self.id_ is None: + raise ValueError("An attendance id is required") + body_dict = {} + if self.date is not None: + body_dict['date'] = self.date.strftime("%Y-%m-%d") + if self.start_time is not None: + body_dict['start_time'] = self.start_time + if self.end_time is not None: + body_dict['end_time'] = self.end_time + if self.break_duration is not None: + body_dict['break'] = self.break_duration + if self.comment is not None: + body_dict['comment'] = self.comment + return body_dict + else: + return \ + { + "employee": self.employee_id, + "date": self.date.strftime("%Y-%m-%d"), + "start_time": self.start_time, + "end_time": self.end_time, + "break": self.break_duration, + "comment": self.comment + } + class Employee(WritablePersonioResource, LabeledAttributesMixin): From 1d24527ee3b2be4ff3cadb59f164de170bae0763 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 16 Oct 2020 11:04:40 +0200 Subject: [PATCH 05/16] Implemented delete_attendance function --- src/personio_py/client.py | 54 ++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 382457a..6074836 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -304,7 +304,7 @@ def create_attendances(self, attendances: List[Attendance]): response = self.request_json(path='company/attendances', method='POST', data={"attendances": data_to_send}) return response - def update_attendance(self, attendance, remote_query_id=False): + def update_attendance(self, attendance: Attendance, remote_query_id=False): """ Update an existing attendance record @@ -323,26 +323,50 @@ def update_attendance(self, attendance, remote_query_id=False): return response else: if remote_query_id: - if attendance.employee_id is None: - raise ValueError("For a remote query an employee_id is required") - if attendance.date is None: - raise ValueError("For a remote query a date is required") - matching_remote_attendances = self.get_attendances(employees=[attendance.employee_id], - start_date=attendance.date, end_date=attendance.date) - if len(matching_remote_attendances) == 0: - raise ValueError("The attendance to patch was not found") - elif len(matching_remote_attendances) > 1: - raise ValueError("More than one attendance found.") - attendance.id_ = matching_remote_attendances[0].id_ + attendance = self.__add_remote_attendance_id(attendance) self.update_attendance(attendance) else: raise ValueError("You either need to provide the attendance id or allow a remote query.") - def delete_attendance(self, attendance_id: int): + def delete_attendance(self, attendance: Attendance, remote_query_id=False): """ - placeholder; not ready to be used + Delete an existing record + + Either an attendance id or o remote query is required. Remote queries are only executed if required. + An Attendance object returned by get_attendances() include the attendance id. DO NOT SET THE ID YOURSELF. + + :param attendance: The Attendance object holding the new data. + :param remote_query_id: Allow a remote query for the id if it is not set within the given Attendance object. + :raises: + ValueError: If a query is required but not allowed or the query does not provide exactly one result. """ - raise NotImplementedError() + if attendance.id_ is not None: + # remote query not necessary + response = self.request_json(path='company/attendances/' + str(attendance.id_), method='DELETE') + return response + else: + if remote_query_id: + attendance = self.__add_remote_attendance_id(attendance) + self.delete_attendance(attendance) + else: + raise ValueError("You either need to provide the attendance id or allow a remote query.") + + def __add_remote_attendance_id(self, attendance: Attendance) -> Attendance: + """ + Queries the API for an attendance record matching the given Attendance object and adds the remote id. + """ + if attendance.employee_id is None: + raise ValueError("For a remote query an employee_id is required") + if attendance.date is None: + raise ValueError("For a remote query a date is required") + matching_remote_attendances = self.get_attendances(employees=[attendance.employee_id], + start_date=attendance.date, end_date=attendance.date) + if len(matching_remote_attendances) == 0: + raise ValueError("The attendance to patch was not found") + elif len(matching_remote_attendances) > 1: + raise ValueError("More than one attendance found.") + attendance.id_ = matching_remote_attendances[0].id_ + return attendance def get_absence_types(self) -> List[AbsenceType]: """ From 0b0eb378515de0f5771527110f54cfd13a6d8eab Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 19 Oct 2020 15:50:57 +0200 Subject: [PATCH 06/16] Allow to delete an attendance record by record id --- src/personio_py/client.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 6074836..fa183e9 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -298,9 +298,7 @@ def create_attendances(self, attendances: List[Attendance]): :param attendances: A list attendance records to be created. """ - data_to_send = [] - for attendance in attendances: - data_to_send.append(attendance.to_body_params(patch_existing_attendance=False)) + data_to_send = [attendance.to_body_params(patch_existing_attendance=False) for attendance in attendances] response = self.request_json(path='company/attendances', method='POST', data={"attendances": data_to_send}) return response @@ -328,32 +326,39 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): else: raise ValueError("You either need to provide the attendance id or allow a remote query.") - def delete_attendance(self, attendance: Attendance, remote_query_id=False): + def delete_attendance(self, attendance: Attendance or int, remote_query_id=False): """ Delete an existing record Either an attendance id or o remote query is required. Remote queries are only executed if required. An Attendance object returned by get_attendances() include the attendance id. DO NOT SET THE ID YOURSELF. - :param attendance: The Attendance object holding the new data. + :param attendance: The Attendance object holding the new data or an attendance record id to delete. :param remote_query_id: Allow a remote query for the id if it is not set within the given Attendance object. :raises: ValueError: If a query is required but not allowed or the query does not provide exactly one result. """ - if attendance.id_ is not None: - # remote query not necessary - response = self.request_json(path='company/attendances/' + str(attendance.id_), method='DELETE') + if isinstance(attendance, int): + response = self.request_json(path='company/attendances/' + str(attendance), method='DELETE') return response - else: - if remote_query_id: - attendance = self.__add_remote_attendance_id(attendance) - self.delete_attendance(attendance) + elif isinstance(attendance, Attendance): + if attendance.id_ is not None: + return self.delete_attendance(attendance.id_) else: - raise ValueError("You either need to provide the attendance id or allow a remote query.") + if remote_query_id: + attendance = self.__add_remote_attendance_id(attendance) + self.delete_attendance(attendance.id_) + else: + raise ValueError("You either need to provide the attendance id or allow a remote query.") + else: + raise ValueError("attendance must be an Attendance object or an integer") def __add_remote_attendance_id(self, attendance: Attendance) -> Attendance: """ Queries the API for an attendance record matching the given Attendance object and adds the remote id. + + :param attendance: The attendance object to be updated + :return: The attendance object with the attendance_id set """ if attendance.employee_id is None: raise ValueError("For a remote query an employee_id is required") From 38c7ce6699990bcf93bfb6f8f74ec0b86cff1a12 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 19 Oct 2020 16:15:59 +0200 Subject: [PATCH 07/16] Implement get_absence method --- src/personio_py/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index fa183e9..759e2b5 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -407,9 +407,12 @@ def get_absences(self, employees: Union[int, List[int], Employee, List[Employee] def get_absence(self, absence_id: int) -> Absence: """ - placeholder; not ready to be used + Get an absence record from a given id. + + :param absence_id: The absence id to fetch. """ - raise NotImplementedError() + response = self.request_json('company/time-offs/' + str(absence_id)) + return Absence.from_dict(response['data'], self) def create_absence(self, absence: Absence): """ From 57b8463227d33d401c419c8ab0f6d8d068b8382a Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 19 Oct 2020 17:23:50 +0200 Subject: [PATCH 08/16] Set attendance id after successful creation --- src/personio_py/client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 759e2b5..ebc8005 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -289,7 +289,7 @@ def get_attendances(self, employees: Union[int, List[int], Employee, List[Employ return self._get_employee_metadata( 'company/attendances', Attendance, employees, start_date, end_date) - def create_attendances(self, attendances: List[Attendance]): + def create_attendances(self, attendances: List[Attendance]) -> bool: """ Create all given attendance records. @@ -300,7 +300,11 @@ def create_attendances(self, attendances: List[Attendance]): """ data_to_send = [attendance.to_body_params(patch_existing_attendance=False) for attendance in attendances] response = self.request_json(path='company/attendances', method='POST', data={"attendances": data_to_send}) - return response + if response['success']: + for i in range(len(attendances)): + attendances[i].id_ = response['data']['id'][i] + return True + return False def update_attendance(self, attendance: Attendance, remote_query_id=False): """ From 718114e15b8673c48bdf18dd4b4da4e42f17b350 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 19 Oct 2020 17:27:28 +0200 Subject: [PATCH 09/16] Implement create_absence method --- src/personio_py/client.py | 5 ++++- src/personio_py/models.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index ebc8005..2a9efbf 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -422,7 +422,10 @@ def create_absence(self, absence: Absence): """ placeholder; not ready to be used """ - raise NotImplementedError() + + data = absence.to_body_params() + response = self.request_json('company/time-offs', method='POST', data=data) + return response def delete_absence(self, absence_id: int): """ diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 3c670af..9e54078 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -602,6 +602,19 @@ def _create(self, client: 'Personio'): def _delete(self, client: 'Personio'): pass + def to_body_params(self): + data = { + 'empolyee_id': self.employee.id_, + 'time_off_type_id': self.time_off_type.id_, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'half_day_start': self.half_day_start, + 'half_day_end': self.half_day_end + } + if self.comment is not None: + data['comment'] = self.comment + return data + class Attendance(WritablePersonioResource): From 3d949df34be4efa92297511d37ebead3aaa2425b Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 26 Oct 2020 11:20:45 +0100 Subject: [PATCH 10/16] Implement attendance _create, _update, _delete Set the objects client after create operations --- src/personio_py/client.py | 1 + src/personio_py/models.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 2a9efbf..a586e8f 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -303,6 +303,7 @@ def create_attendances(self, attendances: List[Attendance]) -> bool: if response['success']: for i in range(len(attendances)): attendances[i].id_ = response['data']['id'][i] + attendances[i].set_client(self) return True return False diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 9e54078..62fdc59 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -289,6 +289,9 @@ def _check_client(self, client: 'Personio' = None) -> 'Personio': client.authenticate() return client + def set_client(self, client: 'Personio'): + self._client = client + class LabeledAttributesMixin(PersonioResource): """ @@ -665,13 +668,13 @@ def to_dict(self, nested=False) -> Dict[str, Any]: return d def _create(self, client: 'Personio'): - pass + self._client.create_attendances([self]) - def _update(self, client: 'Personio'): - pass + def _update(self, client: 'Personio', allow_remote_query: bool = False): + self._client.update_attendance(self, remote_query_id=allow_remote_query) - def _delete(self, client: 'Personio'): - pass + def _delete(self, client: 'Personio', allow_remote_query: bool = False): + self._client.delete_attendance(self, remote_query_id=allow_remote_query) def to_body_params(self, patch_existing_attendance=False): """ From c1869f7a12602acbc7ef97f9ef376a5818a82656 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 28 Oct 2020 13:45:48 +0100 Subject: [PATCH 11/16] Use get_client function to determine which client to use. Raises error if no client is available --- src/personio_py/client.py | 9 +++++---- src/personio_py/models.py | 19 ++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index a586e8f..8c2e4a1 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -303,7 +303,7 @@ def create_attendances(self, attendances: List[Attendance]) -> bool: if response['success']: for i in range(len(attendances)): attendances[i].id_ = response['data']['id'][i] - attendances[i].set_client(self) + attendances[i].client = self return True return False @@ -421,14 +421,15 @@ def get_absence(self, absence_id: int) -> Absence: def create_absence(self, absence: Absence): """ - placeholder; not ready to be used - """ + Creates an absence record on the Personio servers + :param absence: The absence object to be created + """ data = absence.to_body_params() response = self.request_json('company/time-offs', method='POST', data=data) return response - def delete_absence(self, absence_id: int): + def delete_absence(self, absence_id: int, remote_query_id=False): """ placeholder; not ready to be used """ diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 62fdc59..cf9c30e 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -67,7 +67,7 @@ class PersonioResource: def __init__(self, client: 'Personio' = None, **kwargs): super().__init__() - self._client = client + self.client = client @classmethod def _field_mapping(cls) -> Dict[str, FieldMapping]: @@ -282,16 +282,13 @@ def _delete(self, client: 'Personio'): UnsupportedMethodError('delete', self.__class__) def _check_client(self, client: 'Personio' = None) -> 'Personio': - client = client or self._client + client = client or self.client if not client: raise PersonioError() if not client.authenticated: client.authenticate() return client - def set_client(self, client: 'Personio'): - self._client = client - class LabeledAttributesMixin(PersonioResource): """ @@ -600,10 +597,10 @@ def __init__(self, self.created_at = created_at def _create(self, client: 'Personio'): - pass + get_client(self, client).create_absence(self) - def _delete(self, client: 'Personio'): - pass + def _delete(self, client: 'Personio', allow_remote_query: bool = False): + get_client(self, client).delete_absence(self.id_, remote_query_id=allow_remote_query) def to_body_params(self): data = { @@ -668,13 +665,13 @@ def to_dict(self, nested=False) -> Dict[str, Any]: return d def _create(self, client: 'Personio'): - self._client.create_attendances([self]) + get_client(self, client).create_attendances([self]) def _update(self, client: 'Personio', allow_remote_query: bool = False): - self._client.update_attendance(self, remote_query_id=allow_remote_query) + get_client(self, client).update_attendance(self, remote_query_id=allow_remote_query) def _delete(self, client: 'Personio', allow_remote_query: bool = False): - self._client.delete_attendance(self, remote_query_id=allow_remote_query) + get_client(self, client).delete_attendance(self, remote_query_id=allow_remote_query) def to_body_params(self, patch_existing_attendance=False): """ From 36bd13963a69fca6a62f529228d3270c0802c81f Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Thu, 29 Oct 2020 14:41:21 +0100 Subject: [PATCH 12/16] Implemented delete_absence, create_absence, __add_remote_absence_id Changed Absence definition: half_day_start, half_day_end are now bool Implemented Absence.to_body_params Added test --- src/personio_py/client.py | 57 ++++++++++++++++++++--- src/personio_py/models.py | 32 +++++++------ tests/test_api.py | 98 +++++++++++++++++++++++++++++++++++---- 3 files changed, 158 insertions(+), 29 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 8c2e4a1..cd2cae8 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -255,7 +255,7 @@ def create_employee(self, employee: Employee, refresh=True) -> Employee: 'employee[position]': employee.position, 'employee[department]': employee.department.name, 'employee[hire_date]': employee.hire_date.isoformat()[:10], - 'employee[weekly_hours]': employee.weekly_working_hours, + 'employee[weekly_hours]': employee.weekly_working_hours } response = self.request_json('company/employees', method='POST', data=data) employee.id_ = response['data']['id'] @@ -378,6 +378,28 @@ def __add_remote_attendance_id(self, attendance: Attendance) -> Attendance: attendance.id_ = matching_remote_attendances[0].id_ return attendance + def __add_remote_absence_id(self, absence: Absence) -> Absence: + """ + Queries the API for an absence record matching the given Absence object and adds the remote id. + + :param absence: The absence object to be updated + :return: The absence object with the absence_id set + """ + if absence.employee is None: + raise ValueError("For a remote query an employee_id is required") + if absence.start_date is None: + raise ValueError("For a remote query a start date is required") + if absence.end_date is None: + raise ValueError("For a remote query an end date is required") + matching_remote_absences = self.get_absences(employees=[absence.employee.id_], + start_date=absence.start_date, end_date=absence.end_date) + if len(matching_remote_absences) == 0: + raise ValueError("The absence to patch was not found") + elif len(matching_remote_absences) > 1: + raise ValueError("More than one absence found.") + absence.id_ = matching_remote_absences[0].id_ + return absence + def get_absence_types(self) -> List[AbsenceType]: """ Get a list of all available absence types, e.g. "paid vacation" or "parental leave". @@ -419,7 +441,7 @@ def get_absence(self, absence_id: int) -> Absence: response = self.request_json('company/time-offs/' + str(absence_id)) return Absence.from_dict(response['data'], self) - def create_absence(self, absence: Absence): + def create_absence(self, absence: Absence) -> bool: """ Creates an absence record on the Personio servers @@ -427,13 +449,36 @@ def create_absence(self, absence: Absence): """ data = absence.to_body_params() response = self.request_json('company/time-offs', method='POST', data=data) - return response + if response['success']: + absence.id_ = response['data']['attributes']['id'] + return True + return False - def delete_absence(self, absence_id: int, remote_query_id=False): + def delete_absence(self, absence: Absence or int, remote_query_id=False): """ - placeholder; not ready to be used + Delete an existing record + + Either an absence id or o remote query is required. Remote queries are only executed if required. + + :param absence: The Absence object holding the new data or an absence record id to delete. + :param remote_query_id: Allow a remote query for the id if it is not set within the given Absence object. + :raises: + ValueError: If a query is required but not allowed or the query does not provide exactly one result. """ - raise NotImplementedError() + if isinstance(absence, int): + response = self.request_json(path='company/time-offs/' + str(absence), method='DELETE') + return response + elif isinstance(absence, Absence): + if absence.id_ is not None: + return self.delete_absence(absence.id_) + else: + if remote_query_id: + absence = self.__add_remote_absence_id(absence) + self.delete_absence(absence.id_) + else: + raise ValueError("You either need to provide the absence id or allow a remote query.") + else: + raise ValueError("absence must be an Absence object or an integer") def _get_employee_metadata( self, path: str, resource_cls: Type[PersonioResourceType], diff --git a/src/personio_py/models.py b/src/personio_py/models.py index cf9c30e..361f253 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -573,8 +573,8 @@ def __init__(self, start_date: datetime = None, end_date: datetime = None, days_count: float = None, - half_day_start: int = None, - half_day_end: int = None, + half_day_start: bool = None, + half_day_end: bool = None, time_off_type: AbsenceType = None, employee: ShortEmployee = None, created_by: str = None, @@ -588,28 +588,32 @@ def __init__(self, self.start_date = start_date self.end_date = end_date self.days_count = days_count - self.half_day_start = half_day_start - self.half_day_end = half_day_end + self.half_day_start = False + if half_day_start and half_day_start > 0: + self.half_day_start = True + self.half_day_end = False + if half_day_end and half_day_end > 0: + self.half_day_end = True self.time_off_type = time_off_type self.employee = employee self.created_by = created_by self.certificate = certificate self.created_at = created_at - def _create(self, client: 'Personio'): + def _create(self, client: 'Personio' = None): get_client(self, client).create_absence(self) - def _delete(self, client: 'Personio', allow_remote_query: bool = False): - get_client(self, client).delete_absence(self.id_, remote_query_id=allow_remote_query) + def _delete(self, client: 'Personio' = None, allow_remote_query: bool = False): + get_client(self, client).delete_absence(self, remote_query_id=allow_remote_query) def to_body_params(self): data = { - 'empolyee_id': self.employee.id_, + 'employee_id': self.employee.id_, 'time_off_type_id': self.time_off_type.id_, - 'start_date': self.start_date, - 'end_date': self.end_date, - 'half_day_start': self.half_day_start, - 'half_day_end': self.half_day_end + 'start_date': self.start_date.strftime("%Y-%m-%d"), + 'end_date': self.end_date.strftime("%Y-%m-%d"), + 'half_day_start': self.half_day_start or False, + 'half_day_end': self.half_day_end or False } if self.comment is not None: data['comment'] = self.comment @@ -850,7 +854,7 @@ def log_once(level: int, message: str): def get_client(resource: PersonioResource, client: 'Personio' = None): - if resource._client or client: - return resource._client or client + if resource.client or client: + return resource.client or client raise PersonioError(f"no Personio client reference is available, please provide it to " f"your {type(resource).__name__} or as function parameter") diff --git a/tests/test_api.py b/tests/test_api.py index 5f5b09c..37f90a9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,9 @@ import os -from datetime import datetime +from datetime import datetime, timedelta, date import pytest -from personio_py import Department, Employee, Personio, PersonioError +from personio_py import Department, Employee, ShortEmployee, Personio, PersonioError, Absence # Personio client authentication CLIENT_ID = os.getenv('CLIENT_ID') @@ -18,6 +18,11 @@ can_authenticate = False skip_if_no_auth = pytest.mark.skipif(not can_authenticate, reason="Personio authentication failed") +# This is used to ensure the test check for existing objects +shared_test_data = { + 'absences_to_delete': [] +} + @skip_if_no_auth def test_raw_api_employees(): @@ -46,7 +51,7 @@ def test_raw_api_attendances(): def test_raw_api_absence_types(): params = {"limit": 200, "offset": 0} absence_types = personio.request_json('company/time-off-types', params=params) - assert len(absence_types['data']) > 10 + assert len(absence_types['data']) >= 10 # Personio test accounts know 10 different absence types @skip_if_no_auth @@ -66,17 +71,36 @@ def test_raw_api_absences(): def test_get_employees(): employees = personio.get_employees() assert len(employees) > 0 + test_employee = employees[0] + shared_test_data['test_employee'] = { + 'id': test_employee.id_, + 'first_name': test_employee.first_name, + 'last_name': test_employee.last_name, + 'email': test_employee.email, + 'hire_date': test_employee.hire_date + } @skip_if_no_auth def test_get_employee(): - employee = personio.get_employee(2007207) - assert employee.first_name == 'Sebastian' + test_data = shared_test_data['test_employee'] + employee = personio.get_employee(test_data['id']) + assert employee.first_name == test_data['first_name'] + assert employee.last_name == test_data['last_name'] + assert employee.email == test_data['email'] + assert employee.hire_date == test_data['hire_date'] d = employee.to_dict() assert d - response = personio.request_json(f'company/employees/2007207') + response = personio.request_json('company/employees/' + str(test_data['id'])) api_attr = response['data']['attributes'] - assert d == api_attr + attr = d['attributes'] + for att_name in attr.keys(): + if 'date' not in att_name: + assert attr[att_name] == api_attr[att_name] + else: + att_date = datetime.fromisoformat(attr[att_name]['value']).replace(tzinfo=None) + api_attr_date = datetime.fromisoformat(api_attr[att_name]['value']).replace(tzinfo=None) + assert (att_date - api_attr_date) < timedelta(seconds=1) @skip_if_no_auth @@ -106,13 +130,69 @@ def test_create_employee(): assert ada_created.status == 'active' +@skip_if_no_auth +def test_create_absences_no_halfdays(): + # Prepare data + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + start_date = date(2020, 1, 1) + end_date = date(2020, 1, 10) + + # Ensure there are no left absences + absences = personio.get_absences(test_user.id_) + delete_absences(personio, absences) + + # Start test + absence_to_create = Absence( + start_date=start_date, + end_date=end_date, + time_off_type=personio.get_absence_types()[0], + employee=test_user, + comment="Test" + ) + absence_to_create.create(personio) + assert absence_to_create.id_ + remote_absence = personio.get_absence(absence_id=absence_to_create.id_) + assert remote_absence.half_day_start is False + assert remote_absence.half_day_start is False + assert remote_absence.start_date - start_date < timedelta(seconds=1) + assert remote_absence.end_date - end_date < timedelta(seconds=1) + + @skip_if_no_auth def test_get_absences(): - absences = personio.get_absences(2007207) - assert len(absences) > 0 + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + absences = personio.get_absences(test_user.id_) + #assert len(absences) == len(shared_test_data['absences_to_delete']) + for created_absence in shared_test_data['absences_to_delete']: + #assert len([absence for absence in absences if absence.id_ == created_absence.id_]) == 1 + remote_absence = [absence for absence in absences if absence.id_ == created_absence.id_][0] + #assert created_absence.employee.id_ == remote_absence.employee.id_ + + +@skip_if_no_auth +def test_delete_absences(): + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + absences = personio.get_absences(test_user.id_) + num_absences = len(absences) + #assert len(absences) == len(shared_test_data['absences_to_delete']) + for created_absence in shared_test_data['absences_to_delete'] or absences: + created_absence.delete(client=personio) + absences = personio.get_absences(test_user.id_) + #assert len(absences) == num_absences - 1 + num_absences -= 1 + + @skip_if_no_auth def test_get_attendances(): attendances = personio.get_attendances(2007207) assert len(attendances) > 0 + + +def delete_absences(client: Personio, absences: [int] or [Absence]): + for absence in absences: + client.delete_absence(absence) From cdf897a0b31d5f4318ffae213bc336e17b5d31a7 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 30 Oct 2020 12:31:21 +0100 Subject: [PATCH 13/16] More test for creating absences --- tests/test_api.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 37f90a9..5f40ca9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -71,7 +71,7 @@ def test_raw_api_absences(): def test_get_employees(): employees = personio.get_employees() assert len(employees) > 0 - test_employee = employees[0] + test_employee = [e for e in employees if e.last_name == "Flohr"][0] shared_test_data['test_employee'] = { 'id': test_employee.id_, 'first_name': test_employee.first_name, @@ -118,7 +118,7 @@ def test_create_employee(): email='ada@example.org', gender='female', position='first programmer ever', - department=Department(name='Operations'), + department=None, hire_date=datetime(1835, 2, 1), weekly_working_hours="35", ) @@ -131,12 +131,20 @@ def test_create_employee(): @skip_if_no_auth -def test_create_absences_no_halfdays(): +@pytest.mark.parametrize("half_day_start", [True, False]) +@pytest.mark.parametrize("half_dey_end", [True, False]) +def test_create_absences(half_day_start: bool, half_dey_end: bool): + """ + Test the creation of absence records on the server. + half_day_start and half_day_end are not supported on all absence types. + """ # Prepare data test_data = shared_test_data['test_employee'] test_user = personio.get_employee(test_data['id']) - start_date = date(2020, 1, 1) - end_date = date(2020, 1, 10) + absence_types = personio.get_absence_types() + absence_type = [absence_type for absence_type in absence_types if absence_type.name == "Unpaid vacation"][0] + start_date = date(2021, 1, 1) + end_date = date(2021, 1, 10) # Ensure there are no left absences absences = personio.get_absences(test_user.id_) @@ -146,15 +154,17 @@ def test_create_absences_no_halfdays(): absence_to_create = Absence( start_date=start_date, end_date=end_date, - time_off_type=personio.get_absence_types()[0], + time_off_type=absence_type, employee=test_user, + half_day_start=half_day_start, + half_day_end=half_dey_end, comment="Test" ) absence_to_create.create(personio) assert absence_to_create.id_ remote_absence = personio.get_absence(absence_id=absence_to_create.id_) - assert remote_absence.half_day_start is False - assert remote_absence.half_day_start is False + assert remote_absence.half_day_start is half_day_start + assert remote_absence.half_day_end is half_dey_end assert remote_absence.start_date - start_date < timedelta(seconds=1) assert remote_absence.end_date - end_date < timedelta(seconds=1) From f4476cb1c203bd60d40b192cbdd2e2aca7817495 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 30 Oct 2020 13:41:53 +0100 Subject: [PATCH 14/16] Implemented: allow to delete absence from absence object / remote_id_query Added tests for absences --- src/personio_py/client.py | 21 +++-- tests/test_api.py | 190 ++++++++++++++++++++++++++++++-------- 2 files changed, 169 insertions(+), 42 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index cd2cae8..cd070e2 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -432,14 +432,23 @@ def get_absences(self, employees: Union[int, List[int], Employee, List[Employee] return self._get_employee_metadata( 'company/time-offs', Absence, employees, start_date, end_date) - def get_absence(self, absence_id: int) -> Absence: + def get_absence(self, absence: int or Absence, remote_query_id=False) -> Absence: """ Get an absence record from a given id. - :param absence_id: The absence id to fetch. + :param absence: The absence id to fetch. """ - response = self.request_json('company/time-offs/' + str(absence_id)) - return Absence.from_dict(response['data'], self) + if isinstance(absence, int): + response = self.request_json('company/time-offs/' + str(absence)) + return Absence.from_dict(response['data'], self) + else: + if absence.id_: + return self.get_absence(absence.id_) + elif absence.id_ is None and remote_query_id: + self.__add_remote_absence_id(absence) + return self.get_absence(absence.id_) + else: + raise ValueError("Id is required to get an absence record") def create_absence(self, absence: Absence) -> bool: """ @@ -467,14 +476,14 @@ def delete_absence(self, absence: Absence or int, remote_query_id=False): """ if isinstance(absence, int): response = self.request_json(path='company/time-offs/' + str(absence), method='DELETE') - return response + return response['success'] elif isinstance(absence, Absence): if absence.id_ is not None: return self.delete_absence(absence.id_) else: if remote_query_id: absence = self.__add_remote_absence_id(absence) - self.delete_absence(absence.id_) + return self.delete_absence(absence.id_) else: raise ValueError("You either need to provide the absence id or allow a remote query.") else: diff --git a/tests/test_api.py b/tests/test_api.py index 5f40ca9..135bcfc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,7 +3,7 @@ import pytest -from personio_py import Department, Employee, ShortEmployee, Personio, PersonioError, Absence +from personio_py import Department, Employee, ShortEmployee, Personio, PersonioError, Absence, AbsenceType # Personio client authentication CLIENT_ID = os.getenv('CLIENT_ID') @@ -71,7 +71,7 @@ def test_raw_api_absences(): def test_get_employees(): employees = personio.get_employees() assert len(employees) > 0 - test_employee = [e for e in employees if e.last_name == "Flohr"][0] + test_employee = employees[0] shared_test_data['test_employee'] = { 'id': test_employee.id_, 'first_name': test_employee.first_name, @@ -132,69 +132,137 @@ def test_create_employee(): @skip_if_no_auth @pytest.mark.parametrize("half_day_start", [True, False]) -@pytest.mark.parametrize("half_dey_end", [True, False]) -def test_create_absences(half_day_start: bool, half_dey_end: bool): +@pytest.mark.parametrize("half_day_end", [True, False]) +def test_create_absences(half_day_start: bool, half_day_end: bool): """ Test the creation of absence records on the server. - half_day_start and half_day_end are not supported on all absence types. """ # Prepare data test_data = shared_test_data['test_employee'] test_user = personio.get_employee(test_data['id']) - absence_types = personio.get_absence_types() - absence_type = [absence_type for absence_type in absence_types if absence_type.name == "Unpaid vacation"][0] start_date = date(2021, 1, 1) end_date = date(2021, 1, 10) # Ensure there are no left absences - absences = personio.get_absences(test_user.id_) - delete_absences(personio, absences) + delete_all_absences_of_employee(test_user) # Start test - absence_to_create = Absence( - start_date=start_date, - end_date=end_date, - time_off_type=absence_type, - employee=test_user, - half_day_start=half_day_start, - half_day_end=half_dey_end, - comment="Test" - ) + absence_to_create = create_absence_for_user(test_user, + start_date=start_date, + end_date=end_date, + half_day_start=half_day_start, + half_day_end=half_day_end) + assert absence_to_create.id_ is None absence_to_create.create(personio) assert absence_to_create.id_ - remote_absence = personio.get_absence(absence_id=absence_to_create.id_) + remote_absence = personio.get_absence(absence=absence_to_create) assert remote_absence.half_day_start is half_day_start - assert remote_absence.half_day_end is half_dey_end + assert remote_absence.half_day_end is half_day_end assert remote_absence.start_date - start_date < timedelta(seconds=1) assert remote_absence.end_date - end_date < timedelta(seconds=1) @skip_if_no_auth -def test_get_absences(): +def test_get_absences_from_id(): + user = prepare_test_get_absences() + id = create_absence_for_user(user, create=True).id_ + absence = personio.get_absence(id) + assert absence.id_ == id + + +@skip_if_no_auth +def test_get_absences_from_absence_object(): + user = prepare_test_get_absences() + remote_absence = create_absence_for_user(user, create=True) + absence = personio.get_absence(remote_absence) + assert absence.id_ == remote_absence.id_ + + +@skip_if_no_auth +def test_get_absences_from_absence_object_without_id_remote_query(): + user = prepare_test_get_absences() + remote_absence = create_absence_for_user(user, create=True) + absence_id = remote_absence.id_ + remote_absence.id_ = None + absence = personio.get_absence(remote_absence, remote_query_id=True) + assert absence.id_ == absence_id + + +@skip_if_no_auth +def test_get_absences_from_absence_object_without_id_no_remote_query(): + user = prepare_test_get_absences() + remote_absence = create_absence_for_user(user, create=True) + remote_absence.id_ = None + with pytest.raises(ValueError): + personio.get_absence(remote_absence, remote_query_id=False) + + +@skip_if_no_auth +def test_delete_absences_from_model_no_client(): + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + with pytest.raises(PersonioError): + absence.delete() + + +@skip_if_no_auth +def test_delete_absences_from_model_passed_client(): test_data = shared_test_data['test_employee'] test_user = personio.get_employee(test_data['id']) - absences = personio.get_absences(test_user.id_) - #assert len(absences) == len(shared_test_data['absences_to_delete']) - for created_absence in shared_test_data['absences_to_delete']: - #assert len([absence for absence in absences if absence.id_ == created_absence.id_]) == 1 - remote_absence = [absence for absence in absences if absence.id_ == created_absence.id_][0] - #assert created_absence.employee.id_ == remote_absence.employee.id_ + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + assert absence.delete(client=personio) is True @skip_if_no_auth -def test_delete_absences(): +def test_delete_absences_from_model_with_client(): test_data = shared_test_data['test_employee'] test_user = personio.get_employee(test_data['id']) - absences = personio.get_absences(test_user.id_) - num_absences = len(absences) - #assert len(absences) == len(shared_test_data['absences_to_delete']) - for created_absence in shared_test_data['absences_to_delete'] or absences: - created_absence.delete(client=personio) - absences = personio.get_absences(test_user.id_) - #assert len(absences) == num_absences - 1 - num_absences -= 1 + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + absence.client = personio + assert absence.delete() is True +@skip_if_no_auth +def test_delete_absences_from_client_id(): + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + assert personio.delete_absence(absence.id_) is True + + +@skip_if_no_auth +def test_delete_absences_from_client_object_with_id(): + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + assert personio.delete_absence(absence) is True + + +@skip_if_no_auth +def test_delete_absences_from_client_object_with_no_id_query(): + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + absence.id_ = None + assert personio.delete_absence(absence, remote_query_id=True) is True + + +@skip_if_no_auth +def test_delete_absences_from_client_object_with_no_id_no_query(): + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + delete_all_absences_of_employee(test_user) + absence = create_absence_for_user(test_user, create=True) + absence.id_ = None + with pytest.raises(ValueError): + personio.delete_absence(absence, remote_query_id=False) @skip_if_no_auth @@ -206,3 +274,53 @@ def test_get_attendances(): def delete_absences(client: Personio, absences: [int] or [Absence]): for absence in absences: client.delete_absence(absence) + + +def create_absences(client: Personio, absences: [Absence]): + for absence in absences: + client.create_absence(absence) + + +def delete_all_absences_of_employee(employee: Employee): + absences = personio.get_absences(employee) + delete_absences(personio, absences) + + +def create_absence_for_user(employee: Employee, + time_off_type: AbsenceType = None, + start_date: date = None, + end_date: date = None, + half_day_start: bool = False, + half_day_end: bool = False, + comment: str = None, + create: bool = False) -> Absence: + if not time_off_type: + absence_types = personio.get_absence_types() + time_off_type = [absence_type for absence_type in absence_types if absence_type.name == "Unpaid vacation"][0] + if not start_date: + start_date = date(2021, 1, 1) + if not end_date: + end_date = date(2021, 1, 10) + + absence_to_create = Absence( + start_date=start_date, + end_date=end_date, + time_off_type=time_off_type, + employee=employee, + half_day_start=half_day_start, + half_day_end=half_day_end, + comment=comment + ) + if create: + absence_to_create.create(personio) + return absence_to_create + + +def prepare_test_get_absences() -> Employee: + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + + # Be sure there are no leftover absences + delete_all_absences_of_employee(test_user) + return test_user + From 234d40b33d38d9de58f9f0a4c66a0025112f7758 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 30 Oct 2020 14:48:52 +0100 Subject: [PATCH 15/16] Restructured api tests --- tests/apitest_shared.py | 29 ++++ tests/{test_api.py => test_api_absences.py} | 142 +------------------- tests/test_api_attendances.py | 13 ++ tests/test_api_employees.py | 58 ++++++++ tests/test_api_raw.py | 44 ++++++ 5 files changed, 149 insertions(+), 137 deletions(-) create mode 100644 tests/apitest_shared.py rename tests/{test_api.py => test_api_absences.py} (61%) create mode 100644 tests/test_api_attendances.py create mode 100644 tests/test_api_employees.py create mode 100644 tests/test_api_raw.py diff --git a/tests/apitest_shared.py b/tests/apitest_shared.py new file mode 100644 index 0000000..79c4a10 --- /dev/null +++ b/tests/apitest_shared.py @@ -0,0 +1,29 @@ +import os +import pytest + +from personio_py import Personio, PersonioError + +# Personio client authentication +CLIENT_ID = os.getenv('CLIENT_ID') +CLIENT_SECRET = os.getenv('CLIENT_SECRET') +personio = Personio(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) + +# deactivate all tests that rely on a specific personio instance +try: + personio.authenticate() + can_authenticate = True +except PersonioError: + can_authenticate = False +skip_if_no_auth = pytest.mark.skipif(not can_authenticate, reason="Personio authentication failed") + +# This is used to ensure the test check for existing objects +test_employee = personio.get_employees()[0] +shared_test_data = { + 'test_employee': { + 'id': test_employee.id_, + 'first_name': test_employee.first_name, + 'last_name': test_employee.last_name, + 'email': test_employee.email, + 'hire_date': test_employee.hire_date + } +} diff --git a/tests/test_api.py b/tests/test_api_absences.py similarity index 61% rename from tests/test_api.py rename to tests/test_api_absences.py index 135bcfc..d295a91 100644 --- a/tests/test_api.py +++ b/tests/test_api_absences.py @@ -1,134 +1,8 @@ -import os -from datetime import datetime, timedelta, date - -import pytest +from .apitest_shared import * +from datetime import timedelta, date from personio_py import Department, Employee, ShortEmployee, Personio, PersonioError, Absence, AbsenceType -# Personio client authentication -CLIENT_ID = os.getenv('CLIENT_ID') -CLIENT_SECRET = os.getenv('CLIENT_SECRET') -personio = Personio(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) - -# deactivate all tests that rely on a specific personio instance -try: - personio.authenticate() - can_authenticate = True -except PersonioError: - can_authenticate = False -skip_if_no_auth = pytest.mark.skipif(not can_authenticate, reason="Personio authentication failed") - -# This is used to ensure the test check for existing objects -shared_test_data = { - 'absences_to_delete': [] -} - - -@skip_if_no_auth -def test_raw_api_employees(): - response = personio.request_json('company/employees') - employees = response['data'] - assert len(employees) > 0 - id_0 = employees[0]['attributes']['id']['value'] - employee_0 = personio.request_json(f'company/employees/{id_0}') - assert employee_0 - - -@skip_if_no_auth -def test_raw_api_attendances(): - params = { - "start_date": "2020-01-01", - "end_date": "2020-06-01", - "employees[]": [1142212, 1142211], - "limit": 200, - "offset": 0 - } - attendances = personio.request_json('company/attendances', params=params) - assert attendances - - -@skip_if_no_auth -def test_raw_api_absence_types(): - params = {"limit": 200, "offset": 0} - absence_types = personio.request_json('company/time-off-types', params=params) - assert len(absence_types['data']) >= 10 # Personio test accounts know 10 different absence types - - -@skip_if_no_auth -def test_raw_api_absences(): - params = { - "start_date": "2020-01-01", - "end_date": "2020-06-01", - "employees[]": [1142212], # [2007207, 2007248] - "limit": 200, - "offset": 0 - } - absences = personio.request_json('company/time-offs', params=params) - assert absences - - -@skip_if_no_auth -def test_get_employees(): - employees = personio.get_employees() - assert len(employees) > 0 - test_employee = employees[0] - shared_test_data['test_employee'] = { - 'id': test_employee.id_, - 'first_name': test_employee.first_name, - 'last_name': test_employee.last_name, - 'email': test_employee.email, - 'hire_date': test_employee.hire_date - } - - -@skip_if_no_auth -def test_get_employee(): - test_data = shared_test_data['test_employee'] - employee = personio.get_employee(test_data['id']) - assert employee.first_name == test_data['first_name'] - assert employee.last_name == test_data['last_name'] - assert employee.email == test_data['email'] - assert employee.hire_date == test_data['hire_date'] - d = employee.to_dict() - assert d - response = personio.request_json('company/employees/' + str(test_data['id'])) - api_attr = response['data']['attributes'] - attr = d['attributes'] - for att_name in attr.keys(): - if 'date' not in att_name: - assert attr[att_name] == api_attr[att_name] - else: - att_date = datetime.fromisoformat(attr[att_name]['value']).replace(tzinfo=None) - api_attr_date = datetime.fromisoformat(api_attr[att_name]['value']).replace(tzinfo=None) - assert (att_date - api_attr_date) < timedelta(seconds=1) - - -@skip_if_no_auth -def test_get_employee_picture(): - employee = Employee(client=personio, id_=2007207) - picture = employee.picture() - assert picture - - -@skip_if_no_auth -def test_create_employee(): - ada = Employee( - first_name='Ada', - last_name='Lovelace', - email='ada@example.org', - gender='female', - position='first programmer ever', - department=None, - hire_date=datetime(1835, 2, 1), - weekly_working_hours="35", - ) - ada_created = personio.create_employee(ada, refresh=True) - assert ada.first_name == ada_created.first_name - assert ada.email == ada_created.email - assert ada_created.id_ - assert ada_created.last_modified_at.isoformat()[:10] == datetime.now().isoformat()[:10] - assert ada_created.status == 'active' - @skip_if_no_auth @pytest.mark.parametrize("half_day_start", [True, False]) @@ -165,9 +39,9 @@ def test_create_absences(half_day_start: bool, half_day_end: bool): @skip_if_no_auth def test_get_absences_from_id(): user = prepare_test_get_absences() - id = create_absence_for_user(user, create=True).id_ - absence = personio.get_absence(id) - assert absence.id_ == id + absence_id = create_absence_for_user(user, create=True).id_ + absence = personio.get_absence(absence_id) + assert absence.id_ == absence_id @skip_if_no_auth @@ -265,12 +139,6 @@ def test_delete_absences_from_client_object_with_no_id_no_query(): personio.delete_absence(absence, remote_query_id=False) -@skip_if_no_auth -def test_get_attendances(): - attendances = personio.get_attendances(2007207) - assert len(attendances) > 0 - - def delete_absences(client: Personio, absences: [int] or [Absence]): for absence in absences: client.delete_absence(absence) diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py new file mode 100644 index 0000000..2fcc8f6 --- /dev/null +++ b/tests/test_api_attendances.py @@ -0,0 +1,13 @@ +from .apitest_shared import * + + +@skip_if_no_auth +def test_create_attendances(): + attendances = personio.get_attendances(2007207) + assert len(attendances) > 0 + + +@skip_if_no_auth +def test_get_attendances(): + attendances = personio.get_attendances(2007207) + assert len(attendances) > 0 diff --git a/tests/test_api_employees.py b/tests/test_api_employees.py new file mode 100644 index 0000000..38efbc7 --- /dev/null +++ b/tests/test_api_employees.py @@ -0,0 +1,58 @@ +from .apitest_shared import * +from datetime import datetime, timedelta +from personio_py import Employee + + +@skip_if_no_auth +def test_get_employees(): + employees = personio.get_employees() + assert len(employees) > 0 + + +@skip_if_no_auth +def test_get_employee(): + test_data = shared_test_data['test_employee'] + employee = personio.get_employee(test_data['id']) + assert employee.first_name == test_data['first_name'] + assert employee.last_name == test_data['last_name'] + assert employee.email == test_data['email'] + assert employee.hire_date == test_data['hire_date'] + d = employee.to_dict() + assert d + response = personio.request_json('company/employees/' + str(test_data['id'])) + api_attr = response['data']['attributes'] + attr = d['attributes'] + for att_name in attr.keys(): + if 'date' not in att_name: + assert attr[att_name] == api_attr[att_name] + else: + att_date = datetime.fromisoformat(attr[att_name]['value']).replace(tzinfo=None) + api_attr_date = datetime.fromisoformat(api_attr[att_name]['value']).replace(tzinfo=None) + assert (att_date - api_attr_date) < timedelta(seconds=1) + + +@skip_if_no_auth +def test_get_employee_picture(): + employee = Employee(client=personio, id_=2007207) + picture = employee.picture() + assert picture + + +@skip_if_no_auth +def test_create_employee(): + ada = Employee( + first_name='Ada', + last_name='Lovelace', + email='ada@example.org', + gender='female', + position='first programmer ever', + department=None, + hire_date=datetime(1835, 2, 1), + weekly_working_hours="35", + ) + ada_created = personio.create_employee(ada, refresh=True) + assert ada.first_name == ada_created.first_name + assert ada.email == ada_created.email + assert ada_created.id_ + assert ada_created.last_modified_at.isoformat()[:10] == datetime.now().isoformat()[:10] + assert ada_created.status == 'active' diff --git a/tests/test_api_raw.py b/tests/test_api_raw.py new file mode 100644 index 0000000..4adf618 --- /dev/null +++ b/tests/test_api_raw.py @@ -0,0 +1,44 @@ +from .apitest_shared import * + + +@skip_if_no_auth +def test_raw_api_employees(): + response = personio.request_json('company/employees') + employees = response['data'] + assert len(employees) > 0 + id_0 = employees[0]['attributes']['id']['value'] + employee_0 = personio.request_json(f'company/employees/{id_0}') + assert employee_0 + + +@skip_if_no_auth +def test_raw_api_attendances(): + params = { + "start_date": "2020-01-01", + "end_date": "2020-06-01", + "employees[]": [1142212, 1142211], + "limit": 200, + "offset": 0 + } + attendances = personio.request_json('company/attendances', params=params) + assert attendances + + +@skip_if_no_auth +def test_raw_api_absence_types(): + params = {"limit": 200, "offset": 0} + absence_types = personio.request_json('company/time-off-types', params=params) + assert len(absence_types['data']) >= 10 # Personio test accounts know 10 different absence types + + +@skip_if_no_auth +def test_raw_api_absences(): + params = { + "start_date": "2020-01-01", + "end_date": "2020-06-01", + "employees[]": [1142212], # [2007207, 2007248] + "limit": 200, + "offset": 0 + } + absences = personio.request_json('company/time-offs', params=params) + assert absences From f7953b41881d8000ff0d6a1d11c3a2ad180b6680 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Fri, 30 Oct 2020 14:54:53 +0100 Subject: [PATCH 16/16] Fix: pytest: Only query for test users is credentials are available and authentication successful --- tests/apitest_shared.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/apitest_shared.py b/tests/apitest_shared.py index 79c4a10..5fd742b 100644 --- a/tests/apitest_shared.py +++ b/tests/apitest_shared.py @@ -8,22 +8,25 @@ CLIENT_SECRET = os.getenv('CLIENT_SECRET') personio = Personio(client_id=CLIENT_ID, client_secret=CLIENT_SECRET) +shared_test_data = {} + # deactivate all tests that rely on a specific personio instance try: personio.authenticate() can_authenticate = True + # This is used to ensure the test check for existing objects + test_employee = personio.get_employees()[0] + shared_test_data = { + 'test_employee': { + 'id': test_employee.id_, + 'first_name': test_employee.first_name, + 'last_name': test_employee.last_name, + 'email': test_employee.email, + 'hire_date': test_employee.hire_date + } + } except PersonioError: can_authenticate = False skip_if_no_auth = pytest.mark.skipif(not can_authenticate, reason="Personio authentication failed") -# This is used to ensure the test check for existing objects -test_employee = personio.get_employees()[0] -shared_test_data = { - 'test_employee': { - 'id': test_employee.id_, - 'first_name': test_employee.first_name, - 'last_name': test_employee.last_name, - 'email': test_employee.email, - 'hire_date': test_employee.hire_date - } -} +