From 20942b83f84282d651e3bb5ab1a7fb658091e242 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Tue, 24 Nov 2020 15:25:08 +0100 Subject: [PATCH 01/35] Place authentication in importable file for use with test cases placed in multiple files --- tests/apitest_shared.py | 32 +++++++++++ tests/test_api.py | 118 ---------------------------------------- tests/test_api_raw.py | 44 +++++++++++++++ 3 files changed, 76 insertions(+), 118 deletions(-) create mode 100644 tests/apitest_shared.py delete mode 100644 tests/test_api.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..5fd742b --- /dev/null +++ b/tests/apitest_shared.py @@ -0,0 +1,32 @@ +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) + +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") + + diff --git a/tests/test_api.py b/tests/test_api.py deleted file mode 100644 index 5f5b09c..0000000 --- a/tests/test_api.py +++ /dev/null @@ -1,118 +0,0 @@ -import os -from datetime import datetime - -import pytest - -from personio_py import Department, Employee, 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") - - -@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 - - -@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 - - -@skip_if_no_auth -def test_get_employee(): - employee = personio.get_employee(2007207) - assert employee.first_name == 'Sebastian' - d = employee.to_dict() - assert d - response = personio.request_json(f'company/employees/2007207') - api_attr = response['data']['attributes'] - assert d == api_attr - - -@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=Department(name='Operations'), - 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 -def test_get_absences(): - absences = personio.get_absences(2007207) - assert len(absences) > 0 - - -@skip_if_no_auth -def test_get_attendances(): - attendances = personio.get_attendances(2007207) - assert len(attendances) > 0 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 c6b777087e641d3545fae6d65fd20cac7b1227ae Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Thu, 26 Nov 2020 16:37:49 +0100 Subject: [PATCH 02/35] Readd existing tests --- tests/test_api.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_api.py diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..2215493 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,58 @@ +from personio_py import Employee, Department +from tests.apitest_shared import * +from datetime import datetime + +@skip_if_no_auth +def test_get_employees(): + employees = personio.get_employees() + assert len(employees) > 0 + + +@skip_if_no_auth +def test_get_employee(): + employee = personio.get_employee(2007207) + assert employee.first_name == 'Sebastian' + d = employee.to_dict() + assert d + response = personio.request_json(f'company/employees/2007207') + api_attr = response['data']['attributes'] + assert d == api_attr + + +@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=Department(name='Operations'), + 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 +def test_get_absences(): + absences = personio.get_absences(2007207) + assert len(absences) > 0 + + +@skip_if_no_auth +def test_get_attendances(): + attendances = personio.get_attendances(2007207) + assert len(attendances) > 0 From 9cc99ff2ef3de98c25741db4cba35b088654d3b6 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 30 Nov 2020 12:22:14 +0100 Subject: [PATCH 03/35] Implement attendance functions. Add basic raw api test --- src/personio_py/client.py | 91 ++++++++++++++++++++++++++++++----- src/personio_py/models.py | 46 ++++++++++++++++-- tests/test_api_attendances.py | 66 +++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 tests/test_api_attendances.py diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 357b8c5..259aad3 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -289,25 +289,74 @@ 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: """ - placeholder; not ready to be used + 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. """ - # 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() + 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}) + if response['success']: + for i in range(len(attendances)): + attendances[i].id_ = response['data']['id'][i] + attendances[i].client = self + return True + return False - def update_attendance(self, attendance_id: int): + def update_attendance(self, attendance: 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: + 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 or int, 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 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. """ - raise NotImplementedError() + if isinstance(attendance, int): + response = self.request_json(path='company/attendances/' + str(attendance), method='DELETE') + return response + elif isinstance(attendance, Attendance): + if attendance.id_ is not None: + return self.delete_attendance(attendance.id_) + else: + 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 get_absence_types(self) -> List[AbsenceType]: """ @@ -408,3 +457,23 @@ def _normalize_timeframe_params( employees = [employees] employee_ids = [(e.id_ if isinstance(e, Employee) else e) for e in employees] return employee_ids, start_date, end_date + + 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") + 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 diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 081a879..9639ede 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -652,13 +652,49 @@ def to_dict(self, nested=False) -> Dict[str, Any]: return d def _create(self, client: 'Personio'): - pass + get_client(self, client).create_attendances([self]) - def _update(self, client: 'Personio'): - pass + def _update(self, client: 'Personio', allow_remote_query: bool = False): + get_client(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): + get_client(self, client).delete_attendance(self, remote_query_id=allow_remote_query) + + 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 or 0, + "comment": self.comment or "" + } class Employee(WritablePersonioResource, LabeledAttributesMixin): diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py new file mode 100644 index 0000000..d21297b --- /dev/null +++ b/tests/test_api_attendances.py @@ -0,0 +1,66 @@ +from tests.apitest_shared import * + +from personio_py import Employee, Attendance + +from datetime import datetime + + +@skip_if_no_auth +def test_create_attendances(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendances = personio.get_attendances([employee_id]) + assert len(attendances) == 0 + create_attendance_for_user(employee_id, create=True) + attendances = personio.get_attendances([employee_id]) + assert len(attendances) == 1 + + +def delete_all_attendances_for_employee(employee: Employee, date: datetime = None): + if not date: + date = datetime(2020, 1, 1) + attendances = personio.get_attendances([employee.id_], start_date=date, end_date=date) + for attendance in attendances: + personio.delete_attendance(attendance) + + +def prepare_test_get_attendances() -> Employee: + test_data = shared_test_data['test_employee'] + test_user = personio.get_employee(test_data['id']) + + # Be sure there are no leftover attendances + delete_all_attendances_for_employee(test_user) + return test_user + + +def create_attendance_for_user( + employee_id: int, + date: datetime = None, + start_time: str = None, + end_time: str = None, + break_duration: int = None, + comment: str = None, + is_holiday: bool = None, + is_on_time_off: bool = None, + create: bool = False): + if not date: + date = datetime(2021, 1, 1) + if not start_time: + start_time = "08:00" + if not end_time: + end_time = "17:00" + + attendance_to_create = Attendance( + employee_id=employee_id, + date=date, + start_time=start_time, + end_time=end_time, + break_duration=break_duration, + comment=comment, + is_holiday=is_holiday, + is_on_time_off=is_on_time_off + ) + if create: + attendance_to_create.create(personio) + return attendance_to_create From b29afe4cfff1159720097046697599d6871ff445 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 30 Nov 2020 14:32:39 +0100 Subject: [PATCH 04/35] Add api tests for querying remote attendance ids --- tests/test_api_attendances.py | 42 ++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py index d21297b..f094534 100644 --- a/tests/test_api_attendances.py +++ b/tests/test_api_attendances.py @@ -17,12 +17,44 @@ def test_create_attendances(): assert len(attendances) == 1 -def delete_all_attendances_for_employee(employee: Employee, date: datetime = None): - if not date: - date = datetime(2020, 1, 1) - attendances = personio.get_attendances([employee.id_], start_date=date, end_date=date) +@skip_if_no_auth +def test_add_attendance_id(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + attendance_id = attendance.id_ + attendance_date = attendance.date + attendance.id_ = None + assert attendance.id_ is None + personio._Personio__add_remote_attendance_id(attendance) + assert attendance.id_ == attendance_id + + # Test error conditions + attendance.id_ = None + attendance.employee_id = None + with pytest.raises(ValueError): + personio._Personio__add_remote_attendance_id(attendance) + attendance.employee_id = employee_id + attendance.date = None + with pytest.raises(ValueError): + personio._Personio__add_remote_attendance_id(attendance) + attendance.date = attendance_date + attendance.id_ = attendance_id + attendance.delete(personio) + with pytest.raises(ValueError): + personio._Personio__add_remote_attendance_id(attendance) + attendance_1 = create_attendance_for_user(employee_id, start_time="08:00", end_time="12:00", create=True) + attendance_2 = create_attendance_for_user(employee_id, start_time="13:00", end_time="17:00", create=True) + attendance_1.id_ = None + with pytest.raises(ValueError): + personio._Personio__add_remote_attendance_id(attendance_1) + + +def delete_all_attendances_for_employee(employee: Employee): + attendances = personio.get_attendances([employee.id_]) for attendance in attendances: - personio.delete_attendance(attendance) + attendance.delete(personio) def prepare_test_get_attendances() -> Employee: From 18bc5244ba160ef8f0534817aee69c96908cfc17 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 30 Nov 2020 15:22:50 +0100 Subject: [PATCH 05/35] Make client public accessible. Add api test for deleting attendances. --- src/personio_py/models.py | 2 +- tests/test_api_attendances.py | 83 ++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 9639ede..1ea1279 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -282,7 +282,7 @@ 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: diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py index f094534..9a50859 100644 --- a/tests/test_api_attendances.py +++ b/tests/test_api_attendances.py @@ -1,6 +1,6 @@ from tests.apitest_shared import * -from personio_py import Employee, Attendance +from personio_py import Employee, Attendance, PersonioApiError from datetime import datetime @@ -17,6 +17,87 @@ def test_create_attendances(): assert len(attendances) == 1 +@skip_if_no_auth +def test_delete_attendance_from_client_id(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + personio.delete_attendance(attendance.id_) + assert len(personio.get_attendances([employee_id])) == 0 + + +@skip_if_no_auth +def test_delete_attendance_from_client_object_with_id(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + personio.delete_attendance(attendance) + assert len(personio.get_attendances([employee_id])) == 0 + + +@skip_if_no_auth +def test_delete_attendance_from_client_object_no_id_query(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.id_ = None + personio.delete_attendance(attendance, remote_query_id=True) + assert len(personio.get_attendances([employee_id])) == 0 + + +@skip_if_no_auth +def test_delete_attendance_from_client_object_no_id_no_query(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.id_ = None + with pytest.raises(ValueError): + personio.delete_attendance(attendance, remote_query_id=False) + + +@skip_if_no_auth +def test_delete_attendance_from_model_no_client(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.delete() + with pytest.raises(PersonioApiError): + personio.delete_attendance(attendance, remote_query_id=False) + + +@skip_if_no_auth +def test_delete_attendance_from_model_passed_client(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.delete(client=personio) + assert len(personio.get_attendances([employee_id])) == 0 + + +@skip_if_no_auth +def test_delete_attendance_from_model_with_client(): + employee_id = shared_test_data['test_employee']['id'] + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.client = personio + attendance.delete() + assert len(personio.get_attendances([employee_id])) == 0 + + @skip_if_no_auth def test_add_attendance_id(): employee_id = shared_test_data['test_employee']['id'] From 9de540ff31e27c7965da521b81181638ed19faf7 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 30 Nov 2020 17:32:20 +0100 Subject: [PATCH 06/35] Move attendances mock tests / test data to new files to keep tests readable once more tests are added --- tests/mock_data.py | 48 +----------------------- tests/test_mock_api.py | 31 +--------------- tests/test_mock_api_attendances.py | 37 +++++++++++++++++++ tests/test_mock_api_attendances_data.py | 49 +++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 77 deletions(-) create mode 100644 tests/test_mock_api_attendances.py create mode 100644 tests/test_mock_api_attendances_data.py diff --git a/tests/mock_data.py b/tests/mock_data.py index 3591c25..45f0c6e 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -888,50 +888,4 @@ """ json_dict_empty_response = json.loads(json_string_empty_response) -json_string_attendance_rms = """ -{ - "success": true, - "data": [{ - "id": 33479712, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-20", - "start_time": "11:00", - "end_time": "12:30", - "break": 60, - "comment": "release day! GNU Emacs Version 13 is available as free software now *yay*", - "is_holiday": false, - "is_on_time_off": false - } - }, { - "id": 33479612, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-19", - "start_time": "10:30", - "end_time": "22:00", - "break": 120, - "comment": "just a couple more parentheses...", - "is_holiday": false, - "is_on_time_off": false - } - }, { - "id": 33479602, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-18", - "start_time": "10:00", - "end_time": "20:00", - "break": 90, - "comment": "working on GNU Emacs", - "is_holiday": false, - "is_on_time_off": false - } - } - ] -} -""" -json_dict_attendance_rms = json.loads(json_string_attendance_rms) + diff --git a/tests/test_mock_api.py b/tests/test_mock_api.py index c983d0f..d9691dd 100644 --- a/tests/test_mock_api.py +++ b/tests/test_mock_api.py @@ -1,5 +1,5 @@ import re -from datetime import date, timedelta +from datetime import date from typing import Any, Dict import pytest @@ -147,35 +147,6 @@ def test_get_absence_types(): assert source_dict == target_dict -@responses.activate -def test_get_attendance(): - # mock the get absences endpoint (with different array offsets) - responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=0.*'), - status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) - responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=3.*'), - status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) - # configure personio & get absences for alan - personio = mock_personio() - attendances = personio.get_attendances(2116366) - # validate - assert len(attendances) == 3 - selection = [a for a in attendances if "release" in a.comment.lower()] - assert len(selection) == 1 - release = selection[0] - assert "free software" in release.comment - assert release.date == date(1985, 3, 20) - assert release.start_time == timedelta(seconds=11*60*60) - assert release.end_time == timedelta(seconds=12.5*60*60) - assert release.break_duration == 60 - assert release.employee_id == 2116366 - # validate serialization - source_dict = json_dict_attendance_rms['data'][0] - target_dict = release.to_dict() - compare_labeled_attributes(source_dict, target_dict) - - def mock_personio(): # mock the authentication endpoint, or all no requests will get through resp_json = {'success': True, 'data': {'token': 'dummy_token'}} diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py new file mode 100644 index 0000000..b398228 --- /dev/null +++ b/tests/test_mock_api_attendances.py @@ -0,0 +1,37 @@ +import responses +import re + +from datetime import timedelta, date + +from tests.mock_data import json_dict_empty_response +from tests.test_mock_api import compare_labeled_attributes, mock_personio +from tests.test_mock_api_attendances_data import * + + +@responses.activate +def test_get_attendance(): + # mock the get absences endpoint (with different array offsets) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=0.*'), + status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=3.*'), + status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) + # configure personio & get absences for alan + personio = mock_personio() + attendances = personio.get_attendances(2116366) + # validate + assert len(attendances) == 3 + selection = [a for a in attendances if "release" in a.comment.lower()] + assert len(selection) == 1 + release = selection[0] + assert "free software" in release.comment + assert release.date == date(1985, 3, 20) + assert release.start_time == timedelta(seconds=11*60*60) + assert release.end_time == timedelta(seconds=12.5*60*60) + assert release.break_duration == 60 + assert release.employee_id == 2116366 + # validate serialization + source_dict = json_dict_attendance_rms['data'][0] + target_dict = release.to_dict() + compare_labeled_attributes(source_dict, target_dict) diff --git a/tests/test_mock_api_attendances_data.py b/tests/test_mock_api_attendances_data.py new file mode 100644 index 0000000..7cd209e --- /dev/null +++ b/tests/test_mock_api_attendances_data.py @@ -0,0 +1,49 @@ +import json + +json_string_attendance_rms = """ +{ + "success": true, + "data": [{ + "id": 33479712, + "type": "AttendancePeriod", + "attributes": { + "employee": 2116366, + "date": "1985-03-20", + "start_time": "11:00", + "end_time": "12:30", + "break": 60, + "comment": "release day! GNU Emacs Version 13 is available as free software now *yay*", + "is_holiday": false, + "is_on_time_off": false + } + }, { + "id": 33479612, + "type": "AttendancePeriod", + "attributes": { + "employee": 2116366, + "date": "1985-03-19", + "start_time": "10:30", + "end_time": "22:00", + "break": 120, + "comment": "just a couple more parentheses...", + "is_holiday": false, + "is_on_time_off": false + } + }, { + "id": 33479602, + "type": "AttendancePeriod", + "attributes": { + "employee": 2116366, + "date": "1985-03-18", + "start_time": "10:00", + "end_time": "20:00", + "break": 90, + "comment": "working on GNU Emacs", + "is_holiday": false, + "is_on_time_off": false + } + } + ] +} +""" +json_dict_attendance_rms = json.loads(json_string_attendance_rms) From aa801a75171c69c2b14e39f011ef379c5b8a09f6 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 13 Jan 2021 13:00:25 +0100 Subject: [PATCH 07/35] Fix: duplicated entries returned by API if pagination offset > total number of elements --- src/personio_py/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 259aad3..22f4bf7 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -169,7 +169,10 @@ def request_paginated( resp_data = response['data'] if resp_data: data_acc.extend(resp_data) - params['offset'] += len(resp_data) + if response['metadata']['current_page'] == response['metadata']['total_pages']: + break + else: + params['offset'] += len(resp_data) else: break # return the accumulated data From c03e1d1e12c830e707b1485b74fb7e04ab478c4f Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Thu, 3 Dec 2020 17:40:19 +0100 Subject: [PATCH 08/35] Place authentication in importable file for use with test cases placed in multiple files (#11) * Place authentication in importable file for use with test cases placed in multiple files * Readd existing tests * Update import statement * Don't use shared_test_data dict, cache retrieval of valid online user * Don't require python3.8 to run the tests * cleanup Co-authored-by: Sebastian Straub --- tests/apitest_shared.py | 19 ++++++------------- tests/test_api_raw.py | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/tests/apitest_shared.py b/tests/apitest_shared.py index 5fd742b..3840109 100644 --- a/tests/apitest_shared.py +++ b/tests/apitest_shared.py @@ -1,4 +1,7 @@ import os + +from functools import lru_cache + import pytest from personio_py import Personio, PersonioError @@ -8,25 +11,15 @@ 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") - +@lru_cache(maxsize=1) +def get_test_employee(): + return personio.get_employees()[0] diff --git a/tests/test_api_raw.py b/tests/test_api_raw.py index 4adf618..557bce0 100644 --- a/tests/test_api_raw.py +++ b/tests/test_api_raw.py @@ -1,4 +1,4 @@ -from .apitest_shared import * +from tests.apitest_shared import * @skip_if_no_auth From 3ca6223b3e2aeda5821c3c763aff7c10a2be1485 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Thu, 21 Jan 2021 17:27:09 +0100 Subject: [PATCH 09/35] Adapt test to user retrieval function, fix missing underscore in check_client , set attendance client upon retrieval --- src/personio_py/client.py | 6 ++++-- src/personio_py/models.py | 2 +- tests/test_api_attendances.py | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 22f4bf7..00d5f1f 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -289,8 +289,10 @@ def get_attendances(self, employees: Union[int, List[int], Employee, List[Employ :param end_date: only return attendance records up to this date (inclusive, optional) :return: list of ``Attendance`` records for the specified employees """ - return self._get_employee_metadata( - 'company/attendances', Attendance, employees, start_date, end_date) + attendances = self._get_employee_metadata('company/attendances', Attendance, employees, start_date, end_date) + for attendance in attendances: + attendance._client = self + return attendances def create_attendances(self, attendances: List[Attendance]) -> bool: """ diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 1ea1279..9639ede 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -282,7 +282,7 @@ 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: diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py index 9a50859..a297142 100644 --- a/tests/test_api_attendances.py +++ b/tests/test_api_attendances.py @@ -7,7 +7,7 @@ @skip_if_no_auth def test_create_attendances(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendances = personio.get_attendances([employee_id]) @@ -19,7 +19,7 @@ def test_create_attendances(): @skip_if_no_auth def test_delete_attendance_from_client_id(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -30,7 +30,7 @@ def test_delete_attendance_from_client_id(): @skip_if_no_auth def test_delete_attendance_from_client_object_with_id(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -41,7 +41,7 @@ def test_delete_attendance_from_client_object_with_id(): @skip_if_no_auth def test_delete_attendance_from_client_object_no_id_query(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -53,7 +53,7 @@ def test_delete_attendance_from_client_object_no_id_query(): @skip_if_no_auth def test_delete_attendance_from_client_object_no_id_no_query(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -65,7 +65,7 @@ def test_delete_attendance_from_client_object_no_id_no_query(): @skip_if_no_auth def test_delete_attendance_from_model_no_client(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -77,7 +77,7 @@ def test_delete_attendance_from_model_no_client(): @skip_if_no_auth def test_delete_attendance_from_model_passed_client(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -88,7 +88,7 @@ def test_delete_attendance_from_model_passed_client(): @skip_if_no_auth def test_delete_attendance_from_model_with_client(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -100,7 +100,7 @@ def test_delete_attendance_from_model_with_client(): @skip_if_no_auth def test_add_attendance_id(): - employee_id = shared_test_data['test_employee']['id'] + employee_id = get_test_employee().id_ employee = personio.get_employee(employee_id) delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) @@ -139,7 +139,7 @@ def delete_all_attendances_for_employee(employee: Employee): def prepare_test_get_attendances() -> Employee: - test_data = shared_test_data['test_employee'] + test_data = get_test_employee() test_user = personio.get_employee(test_data['id']) # Be sure there are no leftover attendances From f27f9b12a1a6f327fa366ce8171cd239af69ae93 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 08:21:19 +0100 Subject: [PATCH 10/35] Remove duplicated absence testing code. Move the mock absences data to correct place --- tests/mock_data.py | 48 +++++++++++++++ tests/test_mock_api.py | 81 ------------------------- tests/test_mock_api_attendances.py | 3 +- tests/test_mock_api_attendances_data.py | 49 --------------- 4 files changed, 49 insertions(+), 132 deletions(-) delete mode 100644 tests/test_mock_api_attendances_data.py diff --git a/tests/mock_data.py b/tests/mock_data.py index 57df9c1..b3727e9 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -1134,3 +1134,51 @@ } }""" json_dict_get_absence = json.loads(json_string_get_absence) + +json_string_attendance_rms = """ +{ + "success": true, + "data": [{ + "id": 33479712, + "type": "AttendancePeriod", + "attributes": { + "employee": 2116366, + "date": "1985-03-20", + "start_time": "11:00", + "end_time": "12:30", + "break": 60, + "comment": "release day! GNU Emacs Version 13 is available as free software now *yay*", + "is_holiday": false, + "is_on_time_off": false + } + }, { + "id": 33479612, + "type": "AttendancePeriod", + "attributes": { + "employee": 2116366, + "date": "1985-03-19", + "start_time": "10:30", + "end_time": "22:00", + "break": 120, + "comment": "just a couple more parentheses...", + "is_holiday": false, + "is_on_time_off": false + } + }, { + "id": 33479602, + "type": "AttendancePeriod", + "attributes": { + "employee": 2116366, + "date": "1985-03-18", + "start_time": "10:00", + "end_time": "20:00", + "break": 90, + "comment": "working on GNU Emacs", + "is_holiday": false, + "is_on_time_off": false + } + } + ] +} +""" +json_dict_attendance_rms = json.loads(json_string_attendance_rms) diff --git a/tests/test_mock_api.py b/tests/test_mock_api.py index 03bab91..8ed996f 100644 --- a/tests/test_mock_api.py +++ b/tests/test_mock_api.py @@ -93,87 +93,6 @@ def test_auth_rotation_fail(): assert "authorization header" in str(e.value).lower() -@responses.activate -def test_get_absences(): - # configure personio & get absences for alan - mock_absences() - personio = mock_personio() - absences = personio.get_absences(2116365) - # validate - assert len(absences) == 3 - selection = [a for a in absences if "marathon" in a.comment.lower()] - assert len(selection) == 1 - marathon = selection[0] - assert marathon.start_date == date(1944, 9, 1) - assert marathon.half_day_start == 0 - assert marathon.half_day_end == 1 - assert marathon.status == 'approved' - # validate serialization - source_dict = json_dict_absence_alan['data'][0] - target_dict = marathon.to_dict() - compare_labeled_attributes(source_dict, target_dict) - - -@responses.activate -def test_get_absences_from_employee_objects(): - # mock endpoints & get absences for all employees - mock_employees() - mock_absences() - personio = mock_personio() - employees = personio.get_employees() - assert employees - absences = personio.get_absences(employees) - # the response is not important (it does not match the input), but the function should accept - # a list of Employee objects as parameter and return a result - assert absences - - -@responses.activate -def test_get_absence_types(): - # mock the get absence types endpoint - responses.add( - responses.GET, 'https://api.personio.de/v1/company/time-off-types', status=200, - json=json_dict_absence_types, adding_headers={'Authorization': 'Bearer foo'}) - # configure personio & get absences for alan - personio = mock_personio() - absence_types = personio.get_absence_types() - # non-empty contents - assert len(absence_types) == 3 - for at in absence_types: - assert at.id_ > 0 - assert isinstance(at.name, str) - # serialization matches input - for source_dict, at in zip(json_dict_absence_types['data'], absence_types): - target_dict = at.to_dict() - assert source_dict == target_dict - - -@responses.activate -def test_get_attendance(): - # mock the get absences endpoint (with different array offsets) - responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=1.*'), - status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) - # configure personio & get absences for alan - personio = mock_personio() - attendances = personio.get_attendances(2116366) - # validate - assert len(attendances) == 3 - selection = [a for a in attendances if "release" in a.comment.lower()] - assert len(selection) == 1 - release = selection[0] - assert "free software" in release.comment - assert release.date == date(1985, 3, 20) - assert release.start_time == timedelta(seconds=11*60*60) - assert release.end_time == timedelta(seconds=12.5*60*60) - assert release.break_duration == 60 - assert release.employee_id == 2116366 - # validate serialization - source_dict = json_dict_attendance_rms['data'][0] - target_dict = release.to_dict() - compare_labeled_attributes(source_dict, target_dict) - - def mock_personio(): # mock the authentication endpoint, or all no requests will get through resp_json = {'success': True, 'data': {'token': 'dummy_token'}} diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py index b398228..d53f164 100644 --- a/tests/test_mock_api_attendances.py +++ b/tests/test_mock_api_attendances.py @@ -3,9 +3,8 @@ from datetime import timedelta, date -from tests.mock_data import json_dict_empty_response +from tests.mock_data import json_dict_empty_response, json_dict_attendance_rms from tests.test_mock_api import compare_labeled_attributes, mock_personio -from tests.test_mock_api_attendances_data import * @responses.activate diff --git a/tests/test_mock_api_attendances_data.py b/tests/test_mock_api_attendances_data.py deleted file mode 100644 index 7cd209e..0000000 --- a/tests/test_mock_api_attendances_data.py +++ /dev/null @@ -1,49 +0,0 @@ -import json - -json_string_attendance_rms = """ -{ - "success": true, - "data": [{ - "id": 33479712, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-20", - "start_time": "11:00", - "end_time": "12:30", - "break": 60, - "comment": "release day! GNU Emacs Version 13 is available as free software now *yay*", - "is_holiday": false, - "is_on_time_off": false - } - }, { - "id": 33479612, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-19", - "start_time": "10:30", - "end_time": "22:00", - "break": 120, - "comment": "just a couple more parentheses...", - "is_holiday": false, - "is_on_time_off": false - } - }, { - "id": 33479602, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-18", - "start_time": "10:00", - "end_time": "20:00", - "break": 90, - "comment": "working on GNU Emacs", - "is_holiday": false, - "is_on_time_off": false - } - } - ] -} -""" -json_dict_attendance_rms = json.loads(json_string_attendance_rms) From 11f7657ab94ad5aaeba31c8d552dbf1470176d8f Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 08:58:02 +0100 Subject: [PATCH 11/35] Fix attendance test --- tests/mock_data.py | 4 ++++ tests/test_mock_api_attendances.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/mock_data.py b/tests/mock_data.py index b3727e9..a0e28bc 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -1138,6 +1138,10 @@ json_string_attendance_rms = """ { "success": true, + "metadata":{ + "current_page":1, + "total_pages":1 + }, "data": [{ "id": 33479712, "type": "AttendancePeriod", diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py index d53f164..fd7d087 100644 --- a/tests/test_mock_api_attendances.py +++ b/tests/test_mock_api_attendances.py @@ -11,7 +11,7 @@ def test_get_attendance(): # mock the get absences endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=0.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*'), status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) responses.add( responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=3.*'), From 31b655db88eabc556736a149018b3dabbb9007fe Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 10:24:52 +0100 Subject: [PATCH 12/35] Update function description --- src/personio_py/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 4012522..814181b 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -300,10 +300,10 @@ def create_attendances(self, attendances: List[Attendance]) -> bool: """ 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. + Note: If one or more attendances can not be created, other attendances will be created but + their corresponding objects passed as attendances will not be updated. - :param attendances: A list attendance records to be created. + :param attendances: A list of attendance records to be created. """ 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}) From 174f427488fe474556be220ac069ecacdc31c469 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 13:47:09 +0100 Subject: [PATCH 13/35] Convert timedelta to str when updating an attendance --- src/personio_py/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 0bd485d..ba36a3d 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -690,9 +690,9 @@ def to_body_params(self, patch_existing_attendance=False): 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 + body_dict['start_time'] = str(self.start_time)[:-3] if self.end_time is not None: - body_dict['end_time'] = self.end_time + body_dict['end_time'] = str(self.end_time)[:-3] if self.break_duration is not None: body_dict['break'] = self.break_duration if self.comment is not None: From f6a18e8bc0d0bf702ac3dddad147bc2e23305600 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 13:47:19 +0100 Subject: [PATCH 14/35] Add more mock tests --- tests/mock_data.py | 21 +++++++++++ tests/test_mock_api_attendances.py | 60 ++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/tests/mock_data.py b/tests/mock_data.py index a0e28bc..d48a761 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -1186,3 +1186,24 @@ } """ json_dict_attendance_rms = json.loads(json_string_attendance_rms) + +json_string_attendance_create_no_break = """ +{ + "success":true, + "data":{ + "id": [83648700], + "message": "success" + } +} +""" +json_dict_attendance_create_no_break = json.loads(json_string_attendance_create_no_break) + +json_string_attendance_patch = """ +{ + "success":true, + "data":{ + "message": "success" + } +} +""" +json_dict_attendance_patch = json.loads(json_string_attendance_patch) \ No newline at end of file diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py index fd7d087..e242dd2 100644 --- a/tests/test_mock_api_attendances.py +++ b/tests/test_mock_api_attendances.py @@ -3,19 +3,35 @@ from datetime import timedelta, date -from tests.mock_data import json_dict_empty_response, json_dict_attendance_rms +from personio_py import Attendance, Employee + +from tests.mock_data import json_dict_attendance_create_no_break, \ + json_dict_attendance_rms, json_dict_attendance_patch from tests.test_mock_api import compare_labeled_attributes, mock_personio +@responses.activate +def test_create_attendance(): + mock_create_attendance() + personio = mock_personio() + employee = Employee( + first_name="Alan", + last_name='Turing', + email='alan.turing@cetitec.com' + ) + attendance = Attendance( + client=personio, + employee=employee, + date=date(2020, 1, 10), + start_time="09:00", + end_time="17:00", + break_duration=0 + ) + attendance.create() + assert attendance.id_ @responses.activate def test_get_attendance(): - # mock the get absences endpoint (with different array offsets) - responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*'), - status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) - responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=3.*'), - status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) + mock_attendances() # configure personio & get absences for alan personio = mock_personio() attendances = personio.get_attendances(2116366) @@ -34,3 +50,31 @@ def test_get_attendance(): source_dict = json_dict_attendance_rms['data'][0] target_dict = release.to_dict() compare_labeled_attributes(source_dict, target_dict) + +@responses.activate +def test_patch_attendances(): + mock_attendances() + mock_patch_attendance() + personio = mock_personio() + attendances = personio.get_attendances(2116366) + attendance_to_patch = attendances[0] + attendance_to_patch.break_duration = 1 + personio.update_attendance(attendance_to_patch) + + + +def mock_attendances(): + # mock the get absences endpoint (with different array offsets) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*'), + status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) + +def mock_create_attendance(): + responses.add( + responses.POST, 'https://api.personio.de/v1/company/attendances', + status=200, json=json_dict_attendance_create_no_break, adding_headers={'Authorization': 'Bearer bar'}) + +def mock_patch_attendance(): + responses.add( + responses.PATCH, 'https://api.personio.de/v1/company/attendances/33479712', + status=200, json=json_dict_attendance_patch, adding_headers={'Authorization': 'Bearer bar'}) \ No newline at end of file From 34f5791e030d757303dbb439941917818fb2f619 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 14:14:12 +0100 Subject: [PATCH 15/35] add mock test for attendance delete --- tests/mock_data.py | 12 +++++++++++- tests/test_mock_api_attendances.py | 19 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/mock_data.py b/tests/mock_data.py index d48a761..05a32fb 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -1206,4 +1206,14 @@ } } """ -json_dict_attendance_patch = json.loads(json_string_attendance_patch) \ No newline at end of file +json_dict_attendance_patch = json.loads(json_string_attendance_patch) + +json_string_attendance_delete = """ +{ + "success":true, + "data":{ + "message": "success" + } +} +""" +json_dict_attendance_delete = json.loads(json_string_attendance_delete) \ No newline at end of file diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py index e242dd2..9a07392 100644 --- a/tests/test_mock_api_attendances.py +++ b/tests/test_mock_api_attendances.py @@ -6,7 +6,7 @@ from personio_py import Attendance, Employee from tests.mock_data import json_dict_attendance_create_no_break, \ - json_dict_attendance_rms, json_dict_attendance_patch + json_dict_attendance_rms, json_dict_attendance_patch, json_dict_attendance_delete from tests.test_mock_api import compare_labeled_attributes, mock_personio @responses.activate @@ -60,6 +60,16 @@ def test_patch_attendances(): attendance_to_patch = attendances[0] attendance_to_patch.break_duration = 1 personio.update_attendance(attendance_to_patch) + + +@responses.activate +def test_delete_attendances(): + mock_attendances() + mock_delete_attendance() + personio = mock_personio() + attendances = personio.get_attendances(2116366) + attendance_to_delete = attendances[0] + personio.delete_attendance(attendance_to_delete) @@ -77,4 +87,9 @@ def mock_create_attendance(): def mock_patch_attendance(): responses.add( responses.PATCH, 'https://api.personio.de/v1/company/attendances/33479712', - status=200, json=json_dict_attendance_patch, adding_headers={'Authorization': 'Bearer bar'}) \ No newline at end of file + status=200, json=json_dict_attendance_patch, adding_headers={'Authorization': 'Bearer bar'}) + +def mock_delete_attendance(): + responses.add( + responses.DELETE, 'https://api.personio.de/v1/company/attendances/33479712', + status=200, json=json_dict_attendance_delete, adding_headers={'Authorization': 'Bearer bar'}) \ No newline at end of file From 4a1a9e5987001d90e625d83bd4f011961bb22b98 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 16:20:44 +0100 Subject: [PATCH 16/35] Move attandence POST/PATCH/DELETE from WIP to available --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aac4a0d..32b866f 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ Available * [`GET /company/employees/{id}`](https://developer.personio.de/reference#get_company-employees-employee-id): get the employee with the specified ID * [`GET /company/employees/{id}/profile-picture/{width}`](https://developer.personio.de/reference#get_company-employees-employee-id-profile-picture-width): get the profile picture of the specified employee * [`GET /company/attendances`](https://developer.personio.de/reference#get_company-attendances): fetch attendance data for the company employees +* [`POST /company/attendances`](https://developer.personio.de/reference#post_company-attendances): add attendance data for the company employees +* [`DELETE /company/attendances/{id}`](https://developer.personio.de/reference#delete_company-attendances-id): delete the attendance entry with the specified ID +* [`PATCH /company/attendances/{id}`](https://developer.personio.de/reference#patch_company-attendances-id): update the attendance entry with the specified ID * [`GET /company/time-off-types`](https://developer.personio.de/reference#get_company-time-off-types): get a list of available absences types * [`GET /company/time-offs`](https://developer.personio.de/reference#get_company-time-offs): fetch absence data for the company employees * [`POST /company/time-offs`](https://developer.personio.de/reference#post_company-time-offs): add absence data for the company employees @@ -101,9 +104,6 @@ Work in Progress * [`POST /company/employees`](https://developer.personio.de/reference#post_company-employees): create a new employee * [`PATCH /company/employees/{id}`](https://developer.personio.de/reference#patch_company-employees-employee-id): update an existing employee entry -* [`POST /company/attendances`](https://developer.personio.de/reference#post_company-attendances): add attendance data for the company employees -* [`DELETE /company/attendances/{id}`](https://developer.personio.de/reference#delete_company-attendances-id): delete the attendance entry with the specified ID -* [`PATCH /company/attendances/{id}`](https://developer.personio.de/reference#patch_company-attendances-id): update the attendance entry with the specified ID ## Contact From cbb539232a5a0c8d4c12b0e3324690afa90568eb Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 21:23:08 +0100 Subject: [PATCH 17/35] Fix dependency issue by pinning mistune package to 0.8.4 --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 70d4d42..1e420c0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,6 +13,7 @@ bandit>=1.6.0 sphinx~=3.1.2 sphinx_rtd_theme~=0.5.0 recommonmark~=0.6.0 +mistune==0.8.4 m2r~=0.2.1 # publishing From 901f06f0d7fc2c74a9ff92c0b0bbee718f9fbb14 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Wed, 19 Jan 2022 21:29:33 +0100 Subject: [PATCH 18/35] Install mistune before sphinx --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1e420c0..ebb0a21 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,10 +10,10 @@ flake8>=3.0 bandit>=1.6.0 # documentation +mistune==0.8.4 sphinx~=3.1.2 sphinx_rtd_theme~=0.5.0 recommonmark~=0.6.0 -mistune==0.8.4 m2r~=0.2.1 # publishing From 5851de25c3ad194b627245540e93db90f53940f0 Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Thu, 3 Feb 2022 22:59:36 +0100 Subject: [PATCH 19/35] adding suggested changes to requirements and github action --- .github/workflows/docs-pr.yml | 2 +- requirements-dev.txt | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 7040607..b951350 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -21,7 +21,7 @@ jobs: - uses: ammaraskar/sphinx-action@master name: Generate Sphinx Documentation with: - pre-build-command: "pip install sphinx_rtd_theme~=0.4.3 recommonmark~=0.6.0 m2r~=0.2.1" + pre-build-command: "pip install -r requirements-dev.txt" build-command: "make html" docs-folder: "docs/" - uses: actions/upload-artifact@v2 diff --git a/requirements-dev.txt b/requirements-dev.txt index ebb0a21..987c1fd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,11 +10,12 @@ flake8>=3.0 bandit>=1.6.0 # documentation +sphinx==4.3.2 +sphinx-rtd-theme==1.0.0 +recommonmark==0.7.1 +docutils==0.16 mistune==0.8.4 -sphinx~=3.1.2 -sphinx_rtd_theme~=0.5.0 -recommonmark~=0.6.0 -m2r~=0.2.1 +m2r==0.2.1 # publishing wheel From 6519bc1b7285b59d9b7c274aa7e400e08db486aa Mon Sep 17 00:00:00 2001 From: Philip Flohr Date: Mon, 7 Mar 2022 15:04:01 +0100 Subject: [PATCH 20/35] Code cleanup --- src/personio_py/client.py | 57 +++++++++++++++++++++++++++------------ src/personio_py/models.py | 8 +++--- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 814181b..9899b58 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -291,7 +291,8 @@ def get_attendances(self, employees: Union[int, List[int], Employee, List[Employ :param end_date: only return attendance records up to this date (inclusive, optional) :return: list of ``Attendance`` records for the specified employees """ - attendances = self._get_employee_metadata('company/attendances', Attendance, employees, start_date, end_date) + attendances = self._get_employee_metadata( + 'company/attendances', Attendance, employees, start_date, end_date) for attendance in attendances: attendance._client = self return attendances @@ -305,8 +306,14 @@ def create_attendances(self, attendances: List[Attendance]) -> bool: :param attendances: A list of attendance records to be created. """ - 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}) + 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} + ) if response['success']: for i in range(len(attendances)): attendances[i].id_ = response['data']['id'][i] @@ -318,40 +325,53 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): """ 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. + 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. + :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. + 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='PATCH', - data=attendance.to_body_params(patch_existing_attendance=True)) + response = self.request_json( + path=f'company/attendances/{attendance.id_}', + method='PATCH', + data=attendance.to_body_params(patch_existing_attendance=True) + ) return response else: if remote_query_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.") + raise ValueError( + "You either need to provide the attendance id" + + "or allow a remote query." + ) 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. + 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 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. + :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. + ValueError: If a query is required but not allowed or the query does not provide + exactly one result. """ if isinstance(attendance, int): - response = self.request_json(path='company/attendances/' + str(attendance), method='DELETE') + response = self.request_json(path=f'company/attendances/{attendance}', method='DELETE') return response elif isinstance(attendance, Attendance): if attendance.id_ is not None: @@ -361,7 +381,10 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False 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.") + 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") diff --git a/src/personio_py/models.py b/src/personio_py/models.py index ba36a3d..76cbd3b 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -690,9 +690,9 @@ def to_body_params(self, patch_existing_attendance=False): 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'] = str(self.start_time)[:-3] + body_dict['start_time'] = DurationFieldMapping.serialize(self.start_time) if self.end_time is not None: - body_dict['end_time'] = str(self.end_time)[:-3] + body_dict['end_time'] = DurationFieldMapping.serialize(self.end_time) if self.break_duration is not None: body_dict['break'] = self.break_duration if self.comment is not None: @@ -703,8 +703,8 @@ def to_body_params(self, patch_existing_attendance=False): { "employee": self.employee_id, "date": self.date.strftime("%Y-%m-%d"), - "start_time": self.start_time, - "end_time": self.end_time, + "start_time": DurationFieldMapping.serialize(self.start_time), + "end_time": DurationFieldMapping.serialize(self.end_time), "break": self.break_duration or 0, "comment": self.comment or "" } From 6bc21fd55538606a84da506d0de5b17745727b3b Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Tue, 7 Feb 2023 14:04:51 +0100 Subject: [PATCH 21/35] Add attendance functions --- src/personio_py/client.py | 114 ++++++++++++++--- src/personio_py/models.py | 45 ++++++- tests/apitest_shared.py | 4 + tests/mock_data.py | 33 ++++- tests/test_api_attendances.py | 189 +++++++++++++++++++++++++++++ tests/test_mock_api_attendances.py | 95 +++++++++++++++ 6 files changed, 457 insertions(+), 23 deletions(-) create mode 100644 tests/test_api_attendances.py create mode 100644 tests/test_mock_api_attendances.py diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 5ee9ed6..c9e70fb 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -163,7 +163,7 @@ def request_paginated( if params is None: params = {} params['limit'] = limit - params['offset'] = 1 + params['offset'] = 0 # continue making requests until no more data is returned data_acc = [] while True: @@ -171,7 +171,7 @@ def request_paginated( resp_data = response['data'] if resp_data: data_acc.extend(resp_data) - if response['metadata']['current_page'] == response['metadata']['total_pages']: + if response['metadata']['current_page'] == response['metadata']['total_pages'] - 1: break else: params['offset'] += 1 @@ -291,28 +291,108 @@ def get_attendances(self, employees: Union[int, List[int], Employee, List[Employ :param end_date: only return attendance records up to this date (inclusive, optional) :return: list of ``Attendance`` records for the specified employees """ - return self._get_employee_metadata( + attendances = self._get_employee_metadata( 'company/attendances', Attendance, employees, start_date, end_date) + for attendance in attendances: + attendance._client = self + return attendances - def create_attendances(self, attendances: List[Attendance]): - """ - placeholder; not ready to be used + def create_attendances(self, attendances: List[Attendance]) -> bool: """ - # 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. - def update_attendance(self, attendance_id: int): - """ - placeholder; not ready to be used - """ - raise NotImplementedError() + Note: If one or more attendances can not be created, other attendances will be created but + their corresponding objects passed as attendances will not be updated. - def delete_attendance(self, attendance_id: int): + :param attendances: A list of attendance records to be created. """ - placeholder; not ready to be used + 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} + ) + if response['success']: + for i in range(len(attendances)): + attendances[i].id_ = response['data']['id'][i] + attendances[i].client = self + return True + return False + + def update_attendance(self, attendance: Attendance, remote_query_id=False): + """ + 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. + """ + if attendance.id_ is not None: + # remote query not necessary + response = self.request_json( + path=f'company/attendances/{attendance.id_}', + method='PATCH', + data=attendance.to_body_params(patch_existing_attendance=True) + ) + return response + else: + # if remote_query_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." + # ) + raise ValueError( + "You need to provide the attendance id" + ) + + def delete_attendance(self, attendance: Attendance or int, remote_query_id=False): """ - raise NotImplementedError() + 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 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 isinstance(attendance, int): + response = self.request_json(path=f'company/attendances/{attendance}', method='DELETE') + return response + elif isinstance(attendance, Attendance): + if attendance.id_ is not None: + return self.delete_attendance(attendance.id_) + else: + # 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." + # ) + raise ValueError( + "You need to provide the attendance id" + ) + else: + raise ValueError("attendance must be an Attendance object or an integer") def get_absence_types(self) -> List[AbsenceType]: """ diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 8b92b8c..82638b1 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -670,14 +670,49 @@ def to_dict(self, nested=False) -> Dict[str, Any]: return d def _create(self, client: 'Personio'): - pass + get_client(self, client).create_attendances([self]) - def _update(self, client: 'Personio'): - pass + def _update(self, client: 'Personio', allow_remote_query: bool = False): + get_client(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): + get_client(self, client).delete_attendance(self, remote_query_id=allow_remote_query) + 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 or 0, + "comment": self.comment or "" + } class Employee(WritablePersonioResource, LabeledAttributesMixin): diff --git a/tests/apitest_shared.py b/tests/apitest_shared.py index 2a980c9..4a5aa76 100644 --- a/tests/apitest_shared.py +++ b/tests/apitest_shared.py @@ -27,3 +27,7 @@ @lru_cache(maxsize=1) def get_test_employee(): return personio.get_employees()[0] + +@lru_cache(maxsize=1) +def get_test_employee_for_attendances(): + return personio.get_employee(13603465) diff --git a/tests/mock_data.py b/tests/mock_data.py index 5a74db6..5928023 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -721,7 +721,7 @@ { "success": true, "metadata":{ - "current_page":1, + "current_page":0, "total_pages":1 }, "data": [{ @@ -1147,3 +1147,34 @@ } }""" json_dict_get_absence = json.loads(json_string_get_absence) + +json_string_attendance_create_no_break = """ +{ + "success":true, + "data":{ + "id": [83648700], + "message": "success" + } +} +""" +json_dict_attendance_create_no_break = json.loads(json_string_attendance_create_no_break) + +json_string_attendance_patch = """ +{ + "success":true, + "data":{ + "message": "success" + } +} +""" +json_dict_attendance_patch = json.loads(json_string_attendance_patch) + +json_string_attendance_delete = """ +{ + "success":true, + "data":{ + "message": "success" + } +} +""" +json_dict_attendance_delete = json.loads(json_string_attendance_delete) \ No newline at end of file diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py new file mode 100644 index 0000000..eaddb30 --- /dev/null +++ b/tests/test_api_attendances.py @@ -0,0 +1,189 @@ +from tests.apitest_shared import * + +from personio_py import Employee, Attendance, PersonioApiError + +from datetime import datetime + + +@skip_if_no_auth +def test_get_attendances(): + employee = get_test_employee_for_attendances() + attendances = personio.get_attendances(employee) + assert len(attendances) == 11 + + + +@skip_if_no_auth +def test_create_attendances(): + employee_id = get_test_employee().id_ + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendances = personio.get_attendances([employee_id]) + assert len(attendances) == 0 + create_attendance_for_user(employee_id, create=True) + attendances = personio.get_attendances([employee_id]) + assert len(attendances) == 1 + + +@skip_if_no_auth +def test_delete_attendance_from_client_id(): + employee_id = get_test_employee().id_ + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + personio.delete_attendance(attendance.id_) + assert len(personio.get_attendances([employee_id])) == 0 + + +@skip_if_no_auth +def test_delete_attendance_from_client_object_with_id(): + employee_id = get_test_employee().id_ + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + personio.delete_attendance(attendance) + assert len(personio.get_attendances([employee_id])) == 0 + + +# @skip_if_no_auth +# def test_delete_attendance_from_client_object_no_id_query(): +# employee_id = get_test_employee().id_ +# employee = personio.get_employee(employee_id) +# delete_all_attendances_for_employee(employee) +# attendance = create_attendance_for_user(employee_id, create=True) +# assert len(personio.get_attendances([employee_id])) == 1 +# attendance.id_ = None +# personio.delete_attendance(attendance, remote_query_id=True) +# assert len(personio.get_attendances([employee_id])) == 0 + + +@skip_if_no_auth +def test_delete_attendance_from_client_object_no_id_no_query(): + employee_id = get_test_employee().id_ + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.id_ = None + with pytest.raises(ValueError): + personio.delete_attendance(attendance, remote_query_id=False) + + +@skip_if_no_auth +def test_delete_attendance_from_model_no_client(): + employee_id = get_test_employee().id_ + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.delete() + with pytest.raises(PersonioApiError): + personio.delete_attendance(attendance, remote_query_id=False) + + +@skip_if_no_auth +def test_delete_attendance_from_model_passed_client(): + employee_id = get_test_employee().id_ + employee = personio.get_employee(employee_id) + delete_all_attendances_for_employee(employee) + attendance = create_attendance_for_user(employee_id, create=True) + assert len(personio.get_attendances([employee_id])) == 1 + attendance.delete(client=personio) + assert len(personio.get_attendances([employee_id])) == 0 + + +#@skip_if_no_auth +# def test_delete_attendance_from_model_with_client(): +# employee_id = get_test_employee().id_ +# employee = personio.get_employee(employee_id) +# delete_all_attendances_for_employee(employee) +# attendance = create_attendance_for_user(employee_id, create=True) +# assert len(personio.get_attendances([employee_id])) == 1 +# attendance.client = personio +# # it fails: attendance.client = personio but attendance._client = None, didn't understand the reason +# attendance.delete() +# assert len(personio.get_attendances([employee_id])) == 0 + + +# @skip_if_no_auth +# def test_add_attendance_id(): +# # _Personio__add_remote_attendance_id is not available +# employee_id = get_test_employee().id_ +# employee = personio.get_employee(employee_id) +# delete_all_attendances_for_employee(employee) +# attendance = create_attendance_for_user(employee_id, create=True) +# attendance_id = attendance.id_ +# attendance_date = attendance.date +# attendance.id_ = None +# assert attendance.id_ is None +# personio._Personio__add_remote_attendance_id(attendance) +# assert attendance.id_ == attendance_id + +# # Test error conditions +# attendance.id_ = None +# attendance.employee_id = None +# with pytest.raises(ValueError): +# personio._Personio__add_remote_attendance_id(attendance) +# attendance.employee_id = employee_id +# attendance.date = None +# with pytest.raises(ValueError): +# personio._Personio__add_remote_attendance_id(attendance) +# attendance.date = attendance_date +# attendance.id_ = attendance_id +# attendance.delete(personio) +# with pytest.raises(ValueError): +# personio._Personio__add_remote_attendance_id(attendance) +# attendance_1 = create_attendance_for_user(employee_id, start_time="08:00", end_time="12:00", create=True) +# attendance_2 = create_attendance_for_user(employee_id, start_time="13:00", end_time="17:00", create=True) +# attendance_1.id_ = None +# with pytest.raises(ValueError): +# personio._Personio__add_remote_attendance_id(attendance_1) + + +def delete_all_attendances_for_employee(employee: Employee): + attendances = personio.get_attendances([employee.id_]) + for attendance in attendances: + attendance.delete(personio) + + +def prepare_test_get_attendances() -> Employee: + test_data = get_test_employee() + test_user = personio.get_employee(test_data['id']) + + # Be sure there are no leftover attendances + delete_all_attendances_for_employee(test_user) + return test_user + + +def create_attendance_for_user( + employee_id: int, + date: datetime = None, + start_time: str = None, + end_time: str = None, + break_duration: int = None, + comment: str = None, + is_holiday: bool = None, + is_on_time_off: bool = None, + create: bool = False): + if not date: + date = datetime(2021, 1, 1) + if not start_time: + start_time = "08:00" + if not end_time: + end_time = "17:00" + + attendance_to_create = Attendance( + employee_id=employee_id, + date=date, + start_time=start_time, + end_time=end_time, + break_duration=break_duration, + comment=comment, + is_holiday=is_holiday, + is_on_time_off=is_on_time_off + ) + if create: + attendance_to_create.create(personio) + return attendance_to_create diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py new file mode 100644 index 0000000..9a07392 --- /dev/null +++ b/tests/test_mock_api_attendances.py @@ -0,0 +1,95 @@ +import responses +import re + +from datetime import timedelta, date + +from personio_py import Attendance, Employee + +from tests.mock_data import json_dict_attendance_create_no_break, \ + json_dict_attendance_rms, json_dict_attendance_patch, json_dict_attendance_delete +from tests.test_mock_api import compare_labeled_attributes, mock_personio + +@responses.activate +def test_create_attendance(): + mock_create_attendance() + personio = mock_personio() + employee = Employee( + first_name="Alan", + last_name='Turing', + email='alan.turing@cetitec.com' + ) + attendance = Attendance( + client=personio, + employee=employee, + date=date(2020, 1, 10), + start_time="09:00", + end_time="17:00", + break_duration=0 + ) + attendance.create() + assert attendance.id_ + +@responses.activate +def test_get_attendance(): + mock_attendances() + # configure personio & get absences for alan + personio = mock_personio() + attendances = personio.get_attendances(2116366) + # validate + assert len(attendances) == 3 + selection = [a for a in attendances if "release" in a.comment.lower()] + assert len(selection) == 1 + release = selection[0] + assert "free software" in release.comment + assert release.date == date(1985, 3, 20) + assert release.start_time == timedelta(seconds=11*60*60) + assert release.end_time == timedelta(seconds=12.5*60*60) + assert release.break_duration == 60 + assert release.employee_id == 2116366 + # validate serialization + source_dict = json_dict_attendance_rms['data'][0] + target_dict = release.to_dict() + compare_labeled_attributes(source_dict, target_dict) + +@responses.activate +def test_patch_attendances(): + mock_attendances() + mock_patch_attendance() + personio = mock_personio() + attendances = personio.get_attendances(2116366) + attendance_to_patch = attendances[0] + attendance_to_patch.break_duration = 1 + personio.update_attendance(attendance_to_patch) + + +@responses.activate +def test_delete_attendances(): + mock_attendances() + mock_delete_attendance() + personio = mock_personio() + attendances = personio.get_attendances(2116366) + attendance_to_delete = attendances[0] + personio.delete_attendance(attendance_to_delete) + + + +def mock_attendances(): + # mock the get absences endpoint (with different array offsets) + responses.add( + responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*'), + status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) + +def mock_create_attendance(): + responses.add( + responses.POST, 'https://api.personio.de/v1/company/attendances', + status=200, json=json_dict_attendance_create_no_break, adding_headers={'Authorization': 'Bearer bar'}) + +def mock_patch_attendance(): + responses.add( + responses.PATCH, 'https://api.personio.de/v1/company/attendances/33479712', + status=200, json=json_dict_attendance_patch, adding_headers={'Authorization': 'Bearer bar'}) + +def mock_delete_attendance(): + responses.add( + responses.DELETE, 'https://api.personio.de/v1/company/attendances/33479712', + status=200, json=json_dict_attendance_delete, adding_headers={'Authorization': 'Bearer bar'}) \ No newline at end of file From 01bfbe96eb91e829e744230d4152212867f41908 Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 12:30:52 +0100 Subject: [PATCH 22/35] Adjust mock tests and pagination --- src/personio_py/client.py | 101 ++++++++++++++++++++++++++++---- src/personio_py/models.py | 4 +- tests/mock_data.py | 14 +++-- tests/test_mock_api.py | 6 +- tests/test_mock_api_absences.py | 6 +- 5 files changed, 107 insertions(+), 24 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index c9e70fb..0576708 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -35,6 +35,8 @@ class Personio: BASE_URL = "https://api.personio.de/v1/" """base URL of the Personio HTTP API""" + ATTENDANCE_URL = 'company/attendances' + ABSENCE_URL = 'company/time-offs' def __init__(self, base_url: str = None, client_id: str = None, client_secret: str = None, dynamic_fields: List[DynamicMapping] = None): @@ -157,13 +159,46 @@ def request_paginated( :param limit: the max. number of items to return in response to a single request. A higher limit means fewer requests will be made (though there is an upper bound that is enforced on the server side) + :param offset: Pagination attribute to identify which page number you are requesting + starts from 1 for absences. + :return: the parsed json response, when the request was successful, or a PersonioApiError + """ + if self.ATTENDANCE_URL == path: + return self.request_paginated_attendance(path, method, params, + data, auth_rotation, limit, offset=0) + elif self.ABSENCE_URL == path: + return self.request_paginated_absence(path, method, params, + data, auth_rotation, limit, offset=1) + + + def request_paginated_absence( + self, path: str, method='GET', params: Dict[str, Any] = None, + data: Dict[str, Any] = None, auth_rotation=True, limit=200, offset=1) -> Dict[str, Any]: + """ + Make a request against the Personio API, expecting a json response that may be paginated, + i.e. not all results might have been returned after the first request. Will continue + to make requests until no more results are provided by the Personio API. + Returns the parsed json response as dictionary. Will raise a PersonioApiError if the + request fails. + + :param path: the URL path for this request (relative to the Personio API base URL) + :param method: the HTTP request method (default: GET) + :param params: dictionary of URL parameters (optional) + :param data: dictionary of data to send in the request body (optional) + :param auth_rotation: set to True, if authentication keys should be rotated + during this request (default: True for json requests) + :param limit: the max. number of items to return in response to a single request. + A higher limit means fewer requests will be made (though there is an upper bound + that is enforced on the server side) + :param offset: Pagination attribute to identify which page number you are requesting + starts from 1 for absences. :return: the parsed json response, when the request was successful, or a PersonioApiError """ # prepare the params dict (need limit and offset as parameters) if params is None: params = {} params['limit'] = limit - params['offset'] = 0 + params['offset'] = offset # continue making requests until no more data is returned data_acc = [] while True: @@ -171,7 +206,7 @@ def request_paginated( resp_data = response['data'] if resp_data: data_acc.extend(resp_data) - if response['metadata']['current_page'] == response['metadata']['total_pages'] - 1: + if response['metadata']['current_page'] == response['metadata']['total_pages']: break else: params['offset'] += 1 @@ -180,6 +215,52 @@ def request_paginated( # return the accumulated data response['data'] = data_acc return response + + def request_paginated_attendance( + self, path: str, method='GET', params: Dict[str, Any] = None, + data: Dict[str, Any] = None, auth_rotation=True, limit=200, offset=0) -> Dict[str, Any]: + """ + Make a request against the Personio API, expecting a json response that may be paginated, + i.e. not all results might have been returned after the first request. Will continue + to make requests until no more results are provided by the Personio API. + Returns the parsed json response as dictionary. Will raise a PersonioApiError if the + request fails. + + :param path: the URL path for this request (relative to the Personio API base URL) + :param method: the HTTP request method (default: GET) + :param params: dictionary of URL parameters (optional) + :param data: dictionary of data to send in the request body (optional) + :param auth_rotation: set to True, if authentication keys should be rotated + during this request (default: True for json requests) + :param limit: the max. number of items to return in response to a single request. + A higher limit means fewer requests will be made (though there is an upper bound + that is enforced on the server side) + :param offset: For attendences it's an offset from the first record that + would be returned and starts from 0. + :return: the parsed json response, when the request was successful, or a PersonioApiError + """ + # prepare the params dict (need limit and offset as parameters) + if params is None: + params = {} + params['limit'] = limit + params['offset'] = offset + # continue making requests until no more data is returned + data_acc = [] + while True: + response = self.request_json(path, method, params, data, auth_rotation=auth_rotation) + resp_data = response['data'] + if resp_data: + # if response['metadata']['current_page'] == response['metadata']['total_pages']: + if params['offset'] >= response['metadata']['total_elements']: + break + else: + data_acc.extend(resp_data) + params['offset'] += limit + else: + break + # return the accumulated data + response['data'] = data_acc + return response def request_image(self, path: str, method='GET', params: Dict[str, Any] = None, auth_rotation=False) -> Optional[bytes]: @@ -292,7 +373,7 @@ def get_attendances(self, employees: Union[int, List[int], Employee, List[Employ :return: list of ``Attendance`` records for the specified employees """ attendances = self._get_employee_metadata( - 'company/attendances', Attendance, employees, start_date, end_date) + self.ATTENDANCE_URL, Attendance, employees, start_date, end_date) for attendance in attendances: attendance._client = self return attendances @@ -310,7 +391,7 @@ def create_attendances(self, attendances: List[Attendance]) -> bool: attendance.to_body_params(patch_existing_attendance=False) for attendance in attendances ] response = self.request_json( - path='company/attendances', + path=self.ATTENDANCE_URL, method='POST', data={"attendances": data_to_send} ) @@ -339,7 +420,7 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): if attendance.id_ is not None: # remote query not necessary response = self.request_json( - path=f'company/attendances/{attendance.id_}', + path=f'{self.ATTENDANCE_URL}/{attendance.id_}', method='PATCH', data=attendance.to_body_params(patch_existing_attendance=True) ) @@ -374,7 +455,7 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False exactly one result. """ if isinstance(attendance, int): - response = self.request_json(path=f'company/attendances/{attendance}', method='DELETE') + response = self.request_json(path=f'{self.ATTENDANCE_URL}/{attendance}', method='DELETE') return response elif isinstance(attendance, Attendance): if attendance.id_ is not None: @@ -424,7 +505,7 @@ def get_absences(self, employees: Union[int, List[int], Employee, List[Employee] :return: list of ``Absence`` records for the specified employees """ return self._get_employee_metadata( - 'company/time-offs', Absence, employees, start_date, end_date) + self.ABSENCE_URL, Absence, employees, start_date, end_date) def get_absence(self, absence: Union[Absence, int]) -> Absence: """ @@ -433,7 +514,7 @@ def get_absence(self, absence: Union[Absence, int]) -> Absence: :param absence: The absence id to fetch. """ if isinstance(absence, int): - response = self.request_json(f'company/time-offs/{absence}') + response = self.request_json(f'{self.ABSENCE_URL}/{absence}') return Absence.from_dict(response['data'], self) else: if absence.id_: @@ -450,7 +531,7 @@ def create_absence(self, absence: Absence) -> Absence: :raises PersonioError: If the absence could not be created on the Personio servers """ data = absence.to_body_params() - response = self.request_json('company/time-offs', method='POST', data=data) + response = self.request_json(self.ABSENCE_URL, method='POST', data=data) if response['success']: absence.id_ = response['data']['attributes']['id'] return absence @@ -468,7 +549,7 @@ def delete_absence(self, absence: Union[Absence, int]): or the query does not provide exactly one result. """ if isinstance(absence, int): - response = self.request_json(path=f'company/time-offs/{absence}', method='DELETE') + response = self.request_json(path=f'{self.ABSENCE_URL}/{absence}', method='DELETE') return response['success'] elif isinstance(absence, Absence): if absence.id_ is not None: diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 82638b1..6a7c34b 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -695,9 +695,9 @@ def to_body_params(self, patch_existing_attendance=False): 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 + body_dict['start_time'] = str(self.start_time) if self.end_time is not None: - body_dict['end_time'] = self.end_time + body_dict['end_time'] = str(self.end_time) if self.break_duration is not None: body_dict['break'] = self.break_duration if self.comment is not None: diff --git a/tests/mock_data.py b/tests/mock_data.py index 5928023..45e2bc8 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -721,6 +721,7 @@ { "success": true, "metadata":{ + "total_elements": 3, "current_page":0, "total_pages":1 }, @@ -773,7 +774,8 @@ { "success": true, "metadata":{ - "current_page":1, + "total_elements": 3, + "current_page":0, "total_pages":1 }, "data": [{ @@ -927,7 +929,7 @@ { "success": true, "metadata":{ - "current_page":1, + "current_page":0, "total_pages":1 }, "data": [{ @@ -987,7 +989,7 @@ { "success": true, "metadata":{ - "current_page":1, + "current_page":0, "total_pages":1 }, "data": [{ @@ -1021,7 +1023,7 @@ { "success": true, "metadata":{ - "current_page":1, + "current_page":0, "total_pages":1 }, "data": { @@ -1035,7 +1037,7 @@ { "success":true, "metadata":{ - "current_page":1, + "current_page":0, "total_pages":1 }, "data":{ @@ -1094,7 +1096,7 @@ { "success":true, "metadata":{ - "current_page":1, + "current_page":0, "total_pages":1 }, "data":{ diff --git a/tests/test_mock_api.py b/tests/test_mock_api.py index edc9580..7562049 100644 --- a/tests/test_mock_api.py +++ b/tests/test_mock_api.py @@ -95,11 +95,11 @@ def test_auth_rotation_fail(): @responses.activate def test_get_attendance(): - # mock the get absences endpoint (with different array offsets) + # mock the get attendances endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=1.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*offset=*'), status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) - # configure personio & get absences for alan + # configure personio & get attendances for alan personio = mock_personio() attendances = personio.get_attendances(2116366) # validate diff --git a/tests/test_mock_api_absences.py b/tests/test_mock_api_absences.py index 6e36865..0a3ae90 100644 --- a/tests/test_mock_api_absences.py +++ b/tests/test_mock_api_absences.py @@ -159,21 +159,21 @@ def mock_absence_types(): def mock_absences(): # mock the get absences endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), status=200, json=json_dict_absence_alan, adding_headers={'Authorization': 'Bearer foo'}) def mock_single_absences(): # mock the get absences endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), status=200, json=json_dict_absence_alan_first, adding_headers={'Authorization': 'Bearer foo'}) def mock_no_absences(): # mock the get absences endpoint responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) From 792848c50d0bca00a75f4ee27b1fc380050f4c4d Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 12:59:25 +0100 Subject: [PATCH 23/35] Revert absence mock data --- tests/mock_data.py | 13 ++++++------- tests/test_mock_api_absences.py | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/mock_data.py b/tests/mock_data.py index 45e2bc8..7c9cfb2 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -774,8 +774,7 @@ { "success": true, "metadata":{ - "total_elements": 3, - "current_page":0, + "current_page":1, "total_pages":1 }, "data": [{ @@ -929,7 +928,7 @@ { "success": true, "metadata":{ - "current_page":0, + "current_page":1, "total_pages":1 }, "data": [{ @@ -989,7 +988,7 @@ { "success": true, "metadata":{ - "current_page":0, + "current_page":1, "total_pages":1 }, "data": [{ @@ -1023,7 +1022,7 @@ { "success": true, "metadata":{ - "current_page":0, + "current_page":1, "total_pages":1 }, "data": { @@ -1037,7 +1036,7 @@ { "success":true, "metadata":{ - "current_page":0, + "current_page":1, "total_pages":1 }, "data":{ @@ -1096,7 +1095,7 @@ { "success":true, "metadata":{ - "current_page":0, + "current_page":1, "total_pages":1 }, "data":{ diff --git a/tests/test_mock_api_absences.py b/tests/test_mock_api_absences.py index 0a3ae90..9940def 100644 --- a/tests/test_mock_api_absences.py +++ b/tests/test_mock_api_absences.py @@ -159,21 +159,21 @@ def mock_absence_types(): def mock_absences(): # mock the get absences endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), status=200, json=json_dict_absence_alan, adding_headers={'Authorization': 'Bearer foo'}) def mock_single_absences(): # mock the get absences endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), status=200, json=json_dict_absence_alan_first, adding_headers={'Authorization': 'Bearer foo'}) def mock_no_absences(): # mock the get absences endpoint responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=0.*'), + responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) @@ -193,4 +193,4 @@ def mock_create_absence_no_halfdays(): def mock_get_absence(): responses.add( responses.GET, re.compile('https://api.personio.de/v1/company/time-offs/.*'), - status=200, json=json_dict_get_absence, adding_headers={'Authorization': 'Bearer bar'}) + status=200, json=json_dict_get_absence, adding_headers={'Authorization': 'Bearer bar'}) \ No newline at end of file From 0daf34cb7da1f44dc1ca8d81a9a2a7efad84412d Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 14:45:47 +0100 Subject: [PATCH 24/35] Cleanup code style --- src/personio_py/client.py | 34 +++++++++++++++------------------- src/personio_py/models.py | 1 + 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 0576708..4b8a457 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -160,17 +160,16 @@ def request_paginated( A higher limit means fewer requests will be made (though there is an upper bound that is enforced on the server side) :param offset: Pagination attribute to identify which page number you are requesting - starts from 1 for absences. + starts from 1 for absences. :return: the parsed json response, when the request was successful, or a PersonioApiError """ if self.ATTENDANCE_URL == path: return self.request_paginated_attendance(path, method, params, - data, auth_rotation, limit, offset=0) + data, auth_rotation, limit, offset=0) elif self.ABSENCE_URL == path: return self.request_paginated_absence(path, method, params, - data, auth_rotation, limit, offset=1) - - + data, auth_rotation, limit, offset=1) + def request_paginated_absence( self, path: str, method='GET', params: Dict[str, Any] = None, data: Dict[str, Any] = None, auth_rotation=True, limit=200, offset=1) -> Dict[str, Any]: @@ -191,14 +190,14 @@ def request_paginated_absence( A higher limit means fewer requests will be made (though there is an upper bound that is enforced on the server side) :param offset: Pagination attribute to identify which page number you are requesting - starts from 1 for absences. + starts from 1 for absences. :return: the parsed json response, when the request was successful, or a PersonioApiError """ # prepare the params dict (need limit and offset as parameters) if params is None: params = {} params['limit'] = limit - params['offset'] = offset + params['offset'] = offset # continue making requests until no more data is returned data_acc = [] while True: @@ -215,7 +214,7 @@ def request_paginated_absence( # return the accumulated data response['data'] = data_acc return response - + def request_paginated_attendance( self, path: str, method='GET', params: Dict[str, Any] = None, data: Dict[str, Any] = None, auth_rotation=True, limit=200, offset=0) -> Dict[str, Any]: @@ -249,7 +248,7 @@ def request_paginated_attendance( while True: response = self.request_json(path, method, params, data, auth_rotation=auth_rotation) resp_data = response['data'] - if resp_data: + if resp_data: # if response['metadata']['current_page'] == response['metadata']['total_pages']: if params['offset'] >= response['metadata']['total_elements']: break @@ -396,9 +395,9 @@ def create_attendances(self, attendances: List[Attendance]) -> bool: data={"attendances": data_to_send} ) if response['success']: - for i in range(len(attendances)): - attendances[i].id_ = response['data']['id'][i] - attendances[i].client = self + for attendance, response_id in zip(attendances, response['data']['id']): + attendance.id_ = response_id + attendance.client = self return True return False @@ -434,9 +433,7 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): # "You either need to provide the attendance id" + # "or allow a remote query." # ) - raise ValueError( - "You need to provide the attendance id" - ) + raise ValueError("You need to provide the attendance id") def delete_attendance(self, attendance: Attendance or int, remote_query_id=False): """ @@ -455,7 +452,8 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False exactly one result. """ if isinstance(attendance, int): - response = self.request_json(path=f'{self.ATTENDANCE_URL}/{attendance}', method='DELETE') + response = self.request_json(path=f'{self.ATTENDANCE_URL}/{attendance}', + method='DELETE') return response elif isinstance(attendance, Attendance): if attendance.id_ is not None: @@ -469,9 +467,7 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False # "You either need to provide the attendance" + # "id or allow a remote query." # ) - raise ValueError( - "You need to provide the attendance id" - ) + raise ValueError("You need to provide the attendance id") else: raise ValueError("attendance must be an Attendance object or an integer") diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 6a7c34b..30c1fec 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -714,6 +714,7 @@ def to_body_params(self, patch_existing_attendance=False): "comment": self.comment or "" } + class Employee(WritablePersonioResource, LabeledAttributesMixin): _api_type_name = "Employee" From 8cd2e102d9b58a58922eff0eaedc32d188433478 Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 15:39:43 +0100 Subject: [PATCH 25/35] Remove white space --- tests/test_mock_api_attendances.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py index 9a07392..2f57bb7 100644 --- a/tests/test_mock_api_attendances.py +++ b/tests/test_mock_api_attendances.py @@ -50,7 +50,7 @@ def test_get_attendance(): source_dict = json_dict_attendance_rms['data'][0] target_dict = release.to_dict() compare_labeled_attributes(source_dict, target_dict) - + @responses.activate def test_patch_attendances(): mock_attendances() @@ -60,8 +60,7 @@ def test_patch_attendances(): attendance_to_patch = attendances[0] attendance_to_patch.break_duration = 1 personio.update_attendance(attendance_to_patch) - - + @responses.activate def test_delete_attendances(): mock_attendances() @@ -71,8 +70,6 @@ def test_delete_attendances(): attendance_to_delete = attendances[0] personio.delete_attendance(attendance_to_delete) - - def mock_attendances(): # mock the get absences endpoint (with different array offsets) responses.add( From 88720af3b6738a0905d328753705c975b7c23a30 Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 16:38:29 +0100 Subject: [PATCH 26/35] Add version --- CHANGELOG.md | 5 +++++ src/personio_py/client.py | 7 ++++--- src/personio_py/version.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c28df46..6e8f076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.3](https://github.com/at-gmbh/personio-py/tree/v0.2.3) - 2023-02-08 + +* debug attendance feature +* add support for attendance paginated API requests + ## [Unreleased](https://github.com/at-gmbh/personio-py/compare/v0.2.2...HEAD) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 4b8a457..f13c413 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -249,7 +249,6 @@ def request_paginated_attendance( response = self.request_json(path, method, params, data, auth_rotation=auth_rotation) resp_data = response['data'] if resp_data: - # if response['metadata']['current_page'] == response['metadata']['total_pages']: if params['offset'] >= response['metadata']['total_elements']: break else: @@ -291,7 +290,8 @@ def request_image(self, path: str, method='GET', params: Dict[str, Any] = None, def get_employees(self) -> List[Employee]: """ Get a list of all employee records in your account. - + Does not involve pagination. + :return: list of ``Employee`` instances """ response = self.request_json('company/employees') @@ -301,7 +301,8 @@ def get_employees(self) -> List[Employee]: def get_employee(self, employee_id: int) -> Employee: """ Get a single employee with the specified ID. - + Does not involve pagination. + :param employee_id: the Personio ID of the employee to fetch :return: an ``Employee`` instance or a PersonioApiError, if the employee does not exist """ diff --git a/src/personio_py/version.py b/src/personio_py/version.py index 020ed73..d93b5b2 100644 --- a/src/personio_py/version.py +++ b/src/personio_py/version.py @@ -1 +1 @@ -__version__ = '0.2.2' +__version__ = '0.2.3' From 5c7f7d149fc6feb2e36235e244f0eb12ac10c634 Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 16:54:32 +0100 Subject: [PATCH 27/35] Remove white space --- src/personio_py/client.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index f13c413..6a30cd9 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -140,9 +140,9 @@ def request_json(self, path: str, method='GET', params: Dict[str, Any] = None, else: raise PersonioApiError.from_response(response) - def request_paginated( - self, path: str, method='GET', params: Dict[str, Any] = None, - data: Dict[str, Any] = None, auth_rotation=True, limit=200) -> Dict[str, Any]: + def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = None, + data: Dict[str, Any] = None, auth_rotation=True, limit=200 + ) -> Dict[str, Any]: """ Make a request against the Personio API, expecting a json response that may be paginated, i.e. not all results might have been returned after the first request. Will continue @@ -170,9 +170,9 @@ def request_paginated( return self.request_paginated_absence(path, method, params, data, auth_rotation, limit, offset=1) - def request_paginated_absence( - self, path: str, method='GET', params: Dict[str, Any] = None, - data: Dict[str, Any] = None, auth_rotation=True, limit=200, offset=1) -> Dict[str, Any]: + def request_paginated_absence(self, path: str, method='GET', params: Dict[str, Any] = None, + data: Dict[str, Any] = None, auth_rotation=True, limit=200, + offset=1) -> Dict[str, Any]: """ Make a request against the Personio API, expecting a json response that may be paginated, i.e. not all results might have been returned after the first request. Will continue @@ -215,9 +215,9 @@ def request_paginated_absence( response['data'] = data_acc return response - def request_paginated_attendance( - self, path: str, method='GET', params: Dict[str, Any] = None, - data: Dict[str, Any] = None, auth_rotation=True, limit=200, offset=0) -> Dict[str, Any]: + def request_paginated_attendance(self, path: str, method='GET', params: Dict[str, Any] = None, + data: Dict[str, Any] = None, auth_rotation=True, limit=200, + offset=0) -> Dict[str, Any]: """ Make a request against the Personio API, expecting a json response that may be paginated, i.e. not all results might have been returned after the first request. Will continue @@ -291,7 +291,7 @@ def get_employees(self) -> List[Employee]: """ Get a list of all employee records in your account. Does not involve pagination. - + :return: list of ``Employee`` instances """ response = self.request_json('company/employees') @@ -302,7 +302,7 @@ def get_employee(self, employee_id: int) -> Employee: """ Get a single employee with the specified ID. Does not involve pagination. - + :param employee_id: the Personio ID of the employee to fetch :return: an ``Employee`` instance or a PersonioApiError, if the employee does not exist """ @@ -414,8 +414,8 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): :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. + 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 @@ -449,8 +449,8 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False :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. + ValueError: If a query is required but not allowed or the query does not provide exactly + one result. """ if isinstance(attendance, int): response = self.request_json(path=f'{self.ATTENDANCE_URL}/{attendance}', From 14bf7e1b27a113b8bf16d6d82f1f5678f1fd45ce Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 17:07:11 +0100 Subject: [PATCH 28/35] Modify docstrings --- src/personio_py/client.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 6a30cd9..edb1562 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -159,8 +159,6 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No :param limit: the max. number of items to return in response to a single request. A higher limit means fewer requests will be made (though there is an upper bound that is enforced on the server side) - :param offset: Pagination attribute to identify which page number you are requesting - starts from 1 for absences. :return: the parsed json response, when the request was successful, or a PersonioApiError """ if self.ATTENDANCE_URL == path: @@ -190,7 +188,7 @@ def request_paginated_absence(self, path: str, method='GET', params: Dict[str, A A higher limit means fewer requests will be made (though there is an upper bound that is enforced on the server side) :param offset: Pagination attribute to identify which page number you are requesting - starts from 1 for absences. + starts from 1 for absences. :return: the parsed json response, when the request was successful, or a PersonioApiError """ # prepare the params dict (need limit and offset as parameters) @@ -235,7 +233,7 @@ def request_paginated_attendance(self, path: str, method='GET', params: Dict[str A higher limit means fewer requests will be made (though there is an upper bound that is enforced on the server side) :param offset: For attendences it's an offset from the first record that - would be returned and starts from 0. + would be returned and starts from 0. :return: the parsed json response, when the request was successful, or a PersonioApiError """ # prepare the params dict (need limit and offset as parameters) @@ -413,9 +411,8 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): :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. + :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 @@ -448,9 +445,8 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False 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. + :raises ValueError: If a query is required but not allowed + or the query does not provide exactly one result. """ if isinstance(attendance, int): response = self.request_json(path=f'{self.ATTENDANCE_URL}/{attendance}', @@ -541,9 +537,9 @@ def delete_absence(self, absence: Union[Absence, int]): An absence id is required. :param absence: The Absence object holding - the new data or an absence record id to delete. + the new data or an absence record id to delete. :raises ValueError: If a query is required but not allowed - or the query does not provide exactly one result. + or the query does not provide exactly one result. """ if isinstance(absence, int): response = self.request_json(path=f'{self.ABSENCE_URL}/{absence}', method='DELETE') From f0b937e048dce43c8dc190299f7eee9bf60bc9e3 Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 8 Feb 2023 17:19:59 +0100 Subject: [PATCH 29/35] Modify docstrings --- src/personio_py/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index edb1562..d8a482d 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -404,13 +404,13 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): """ Update an existing attendance record - Either an attendance id or o remote query is required. Remote queries are only executed + Either an attendance id or a 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. + Attendance object. :raises ValueError: If a query is required but not allowed or the query does not provide exactly one result. """ @@ -442,9 +442,9 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False attendance id. DO NOT SET THE ID YOURSELF. :param attendance: The Attendance object holding the new data or an attendance record id to - delete. + delete. :param remote_query_id: Allow a remote query for the id if it is not set within the given - Attendance object. + Attendance object. :raises ValueError: If a query is required but not allowed or the query does not provide exactly one result. """ From f37a522134ac749ad9e710fbdc80d01b7621babf Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Fri, 3 Mar 2023 11:54:29 +0100 Subject: [PATCH 30/35] refactor request paginated --- src/personio_py/client.py | 105 ++++++++------------------------------ 1 file changed, 22 insertions(+), 83 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index d8a482d..c96ea72 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -161,103 +161,42 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No that is enforced on the server side) :return: the parsed json response, when the request was successful, or a PersonioApiError """ - if self.ATTENDANCE_URL == path: - return self.request_paginated_attendance(path, method, params, - data, auth_rotation, limit, offset=0) - elif self.ABSENCE_URL == path: - return self.request_paginated_absence(path, method, params, - data, auth_rotation, limit, offset=1) - - def request_paginated_absence(self, path: str, method='GET', params: Dict[str, Any] = None, - data: Dict[str, Any] = None, auth_rotation=True, limit=200, - offset=1) -> Dict[str, Any]: - """ - Make a request against the Personio API, expecting a json response that may be paginated, - i.e. not all results might have been returned after the first request. Will continue - to make requests until no more results are provided by the Personio API. - Returns the parsed json response as dictionary. Will raise a PersonioApiError if the - request fails. - - :param path: the URL path for this request (relative to the Personio API base URL) - :param method: the HTTP request method (default: GET) - :param params: dictionary of URL parameters (optional) - :param data: dictionary of data to send in the request body (optional) - :param auth_rotation: set to True, if authentication keys should be rotated - during this request (default: True for json requests) - :param limit: the max. number of items to return in response to a single request. - A higher limit means fewer requests will be made (though there is an upper bound - that is enforced on the server side) - :param offset: Pagination attribute to identify which page number you are requesting - starts from 1 for absences. - :return: the parsed json response, when the request was successful, or a PersonioApiError - """ - # prepare the params dict (need limit and offset as parameters) - if params is None: - params = {} - params['limit'] = limit - params['offset'] = offset - # continue making requests until no more data is returned - data_acc = [] - while True: - response = self.request_json(path, method, params, data, auth_rotation=auth_rotation) - resp_data = response['data'] - if resp_data: - data_acc.extend(resp_data) - if response['metadata']['current_page'] == response['metadata']['total_pages']: - break - else: - params['offset'] += 1 - else: - break - # return the accumulated data - response['data'] = data_acc - return response - - def request_paginated_attendance(self, path: str, method='GET', params: Dict[str, Any] = None, - data: Dict[str, Any] = None, auth_rotation=True, limit=200, - offset=0) -> Dict[str, Any]: - """ - Make a request against the Personio API, expecting a json response that may be paginated, - i.e. not all results might have been returned after the first request. Will continue - to make requests until no more results are provided by the Personio API. - Returns the parsed json response as dictionary. Will raise a PersonioApiError if the - request fails. - - :param path: the URL path for this request (relative to the Personio API base URL) - :param method: the HTTP request method (default: GET) - :param params: dictionary of URL parameters (optional) - :param data: dictionary of data to send in the request body (optional) - :param auth_rotation: set to True, if authentication keys should be rotated - during this request (default: True for json requests) - :param limit: the max. number of items to return in response to a single request. - A higher limit means fewer requests will be made (though there is an upper bound - that is enforced on the server side) - :param offset: For attendences it's an offset from the first record that - would be returned and starts from 0. - :return: the parsed json response, when the request was successful, or a PersonioApiError - """ - # prepare the params dict (need limit and offset as parameters) + if self.ABSENCE_URL == path: + offset = 1 + url_type = 'absence' + elif self.ATTENDANCE_URL == path: + offset = 0 + url_type = 'attendance' + else: + raise ValueError(f"Invalid path: {path}") + if params is None: params = {} params['limit'] = limit params['offset'] = offset - # continue making requests until no more data is returned data_acc = [] while True: response = self.request_json(path, method, params, data, auth_rotation=auth_rotation) - resp_data = response['data'] + resp_data = response.get('data') if resp_data: - if params['offset'] >= response['metadata']['total_elements']: - break - else: + if url_type == 'absence': data_acc.extend(resp_data) - params['offset'] += limit + if response['metadata']['current_page'] == response['metadata']['total_pages']: + break + else: + params['offset'] += 1 + elif url_type == 'attendance': + if params['offset'] >= response['metadata']['total_elements']: + break + else: + data_acc.extend(resp_data) + params['offset'] += limit else: break # return the accumulated data response['data'] = data_acc return response - + def request_image(self, path: str, method='GET', params: Dict[str, Any] = None, auth_rotation=False) -> Optional[bytes]: """ From 4b4a33d0f95fbe827396f7ff5237285d457bceaa Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Fri, 3 Mar 2023 14:46:18 +0100 Subject: [PATCH 31/35] remove white space --- src/personio_py/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index c96ea72..0178dbd 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -166,10 +166,10 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No url_type = 'absence' elif self.ATTENDANCE_URL == path: offset = 0 - url_type = 'attendance' + url_type = 'attendance' else: raise ValueError(f"Invalid path: {path}") - + if params is None: params = {} params['limit'] = limit @@ -185,7 +185,7 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No break else: params['offset'] += 1 - elif url_type == 'attendance': + elif url_type == 'attendance': if params['offset'] >= response['metadata']['total_elements']: break else: @@ -196,7 +196,7 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No # return the accumulated data response['data'] = data_acc return response - + def request_image(self, path: str, method='GET', params: Dict[str, Any] = None, auth_rotation=False) -> Optional[bytes]: """ From 08cda7ccd8d193e656edddc3a8152a49e06f8493 Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Fri, 3 Mar 2023 14:54:33 +0100 Subject: [PATCH 32/35] remove white space --- src/personio_py/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 0178dbd..ce5e666 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -169,7 +169,7 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No url_type = 'attendance' else: raise ValueError(f"Invalid path: {path}") - + if params is None: params = {} params['limit'] = limit @@ -196,7 +196,7 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No # return the accumulated data response['data'] = data_acc return response - + def request_image(self, path: str, method='GET', params: Dict[str, Any] = None, auth_rotation=False) -> Optional[bytes]: """ From a91de6da00c2f186d33271b8b2b1adb78462f08d Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Wed, 15 Mar 2023 18:19:46 +0100 Subject: [PATCH 33/35] Clean up merge and remove remote query id --- src/personio_py/client.py | 47 ++---- src/personio_py/models.py | 15 +- tests/mock_data.py | 52 ------- tests/test_api_attendances.py | 261 +--------------------------------- tests/test_mock_api.py | 2 +- 5 files changed, 29 insertions(+), 348 deletions(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index 4400de3..f4c1cde 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -169,7 +169,7 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No url_type = 'attendance' else: raise ValueError(f"Invalid path: {path}") - + if params is None: params = {} params['limit'] = limit @@ -196,7 +196,7 @@ def request_paginated(self, path: str, method='GET', params: Dict[str, Any] = No # return the accumulated data response['data'] = data_acc return response - + def request_image(self, path: str, method='GET', params: Dict[str, Any] = None, auth_rotation=False) -> Optional[bytes]: """ @@ -293,8 +293,9 @@ def update_employee(self, employee: Employee): """ raise NotImplementedError() - def get_attendances(self, employees: Union[int, List[int], Employee, List[Employee]], - start_date: datetime = None, end_date: datetime = None) -> List[Attendance]: + def get_attendances( + self, employees: Union[int, List[int], Employee, List[Employee]], + start_date: datetime = None, end_date: datetime = None) -> List[Attendance]: """ Get a list of all attendance records for the employees with the specified IDs @@ -339,8 +340,7 @@ def create_attendances(self, attendances: List[Attendance]) -> bool: return True return False - def update_attendance(self, attendance: Attendance, remote_query_id=False): - def update_attendance(self, attendance: Attendance, remote_query_id=False): + def update_attendance(self, attendance: Attendance): """ Update an existing attendance record @@ -349,8 +349,6 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): 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. @@ -358,23 +356,15 @@ def update_attendance(self, attendance: Attendance, remote_query_id=False): if attendance.id_ is not None: # remote query not necessary response = self.request_json( - path=f'company/attendances/{attendance.id_}', + path=f'{self.ATTENDANCE_URL}/{attendance.id_}', method='PATCH', data=attendance.to_body_params(patch_existing_attendance=True) ) return response else: - if remote_query_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." - ) + raise ValueError("You need to provide the attendance id") - def delete_attendance(self, attendance: Attendance or int, remote_query_id=False): - def delete_attendance(self, attendance: Attendance or int, remote_query_id=False): + def delete_attendance(self, attendance: Attendance or int): """ Delete an existing record @@ -384,27 +374,19 @@ def delete_attendance(self, attendance: Attendance or int, remote_query_id=False :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 isinstance(attendance, int): - response = self.request_json(path=f'company/attendances/{attendance}', method='DELETE') + response = self.request_json(path=f'{self.ATTENDANCE_URL}/{attendance}', + method='DELETE') return response elif isinstance(attendance, Attendance): if attendance.id_ is not None: return self.delete_attendance(attendance.id_) else: - 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." - ) + raise ValueError("You need to provide the attendance") else: raise ValueError("attendance must be an Attendance object or an integer") @@ -421,8 +403,9 @@ def get_absence_types(self) -> List[AbsenceType]: absence_types = [AbsenceType.from_dict(d, self) for d in response['data']] return absence_types - def get_absences(self, employees: Union[int, List[int], Employee, List[Employee]], - start_date: datetime = None, end_date: datetime = None) -> List[Absence]: + def get_absences( + self, employees: Union[int, List[int], Employee, List[Employee]], + start_date: datetime = None, end_date: datetime = None) -> List[Absence]: """ Get a list of all absence records for the employees with the specified IDs. diff --git a/src/personio_py/models.py b/src/personio_py/models.py index 30c1fec..adca7f9 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -672,11 +672,11 @@ def to_dict(self, nested=False) -> Dict[str, Any]: def _create(self, client: 'Personio'): get_client(self, client).create_attendances([self]) - def _update(self, client: 'Personio', allow_remote_query: bool = False): - get_client(self, client).update_attendance(self, remote_query_id=allow_remote_query) + def _update(self, client: 'Personio'): + get_client(self, client).update_attendance(self) - def _delete(self, client: 'Personio', allow_remote_query: bool = False): - get_client(self, client).delete_attendance(self, remote_query_id=allow_remote_query) + def _delete(self, client: 'Personio'): + get_client(self, client).delete_attendance(self) def to_body_params(self, patch_existing_attendance=False): """ @@ -704,15 +704,12 @@ def to_body_params(self, patch_existing_attendance=False): body_dict['comment'] = self.comment return body_dict else: - return \ - { - "employee": self.employee_id, + 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 or 0, - "comment": self.comment or "" - } + "comment": self.comment or ""} class Employee(WritablePersonioResource, LabeledAttributesMixin): diff --git a/tests/mock_data.py b/tests/mock_data.py index 3ca39db..8cc877f 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -1149,58 +1149,6 @@ }""" json_dict_get_absence = json.loads(json_string_get_absence) -json_string_attendance_rms = """ -{ - "success": true, - "metadata":{ - "current_page":1, - "total_pages":1 - }, - "data": [{ - "id": 33479712, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-20", - "start_time": "11:00", - "end_time": "12:30", - "break": 60, - "comment": "release day! GNU Emacs Version 13 is available as free software now *yay*", - "is_holiday": false, - "is_on_time_off": false - } - }, { - "id": 33479612, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-19", - "start_time": "10:30", - "end_time": "22:00", - "break": 120, - "comment": "just a couple more parentheses...", - "is_holiday": false, - "is_on_time_off": false - } - }, { - "id": 33479602, - "type": "AttendancePeriod", - "attributes": { - "employee": 2116366, - "date": "1985-03-18", - "start_time": "10:00", - "end_time": "20:00", - "break": 90, - "comment": "working on GNU Emacs", - "is_holiday": false, - "is_on_time_off": false - } - } - ] -} -""" -json_dict_attendance_rms = json.loads(json_string_attendance_rms) - json_string_attendance_create_no_break = """ { "success":true, diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py index 7b7df4c..4593c24 100644 --- a/tests/test_api_attendances.py +++ b/tests/test_api_attendances.py @@ -10,9 +10,12 @@ def test_get_attendances(): employee = get_test_employee_for_attendances() attendances = personio.get_attendances(employee) assert len(attendances) == 11 + assert isinstance(attendances[0], Attendance) + assert attendances[0].id_ == 162804610 + assert attendances[0].employee_id == employee.id_ + assert attendances[0]._client == personio - @skip_if_no_auth def test_create_attendances(): employee_id = get_test_employee().id_ @@ -20,9 +23,10 @@ def test_create_attendances(): delete_all_attendances_for_employee(employee) attendances = personio.get_attendances([employee_id]) assert len(attendances) == 0 - create_attendance_for_user(employee_id, create=True) + created_attendance = create_attendance_for_user(employee_id, create=True) attendances = personio.get_attendances([employee_id]) assert len(attendances) == 1 + personio.delete_attendance(created_attendance.id_) @skip_if_no_auth @@ -47,223 +51,6 @@ def test_delete_attendance_from_client_object_with_id(): assert len(personio.get_attendances([employee_id])) == 0 -# @skip_if_no_auth -# def test_delete_attendance_from_client_object_no_id_query(): -# employee_id = get_test_employee().id_ -# employee = personio.get_employee(employee_id) -# delete_all_attendances_for_employee(employee) -# attendance = create_attendance_for_user(employee_id, create=True) -# assert len(personio.get_attendances([employee_id])) == 1 -# attendance.id_ = None -# personio.delete_attendance(attendance, remote_query_id=True) -# assert len(personio.get_attendances([employee_id])) == 0 - - -@skip_if_no_auth -def test_delete_attendance_from_client_object_no_id_no_query(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - attendance.id_ = None - with pytest.raises(ValueError): - personio.delete_attendance(attendance, remote_query_id=False) - - -@skip_if_no_auth -def test_delete_attendance_from_model_no_client(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - attendance.delete() - with pytest.raises(PersonioApiError): - personio.delete_attendance(attendance, remote_query_id=False) - - -@skip_if_no_auth -def test_delete_attendance_from_model_passed_client(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - attendance.delete(client=personio) - assert len(personio.get_attendances([employee_id])) == 0 - - -#@skip_if_no_auth -# def test_delete_attendance_from_model_with_client(): -# employee_id = get_test_employee().id_ -# employee = personio.get_employee(employee_id) -# delete_all_attendances_for_employee(employee) -# attendance = create_attendance_for_user(employee_id, create=True) -# assert len(personio.get_attendances([employee_id])) == 1 -# attendance.client = personio -# # it fails: attendance.client = personio but attendance._client = None, didn't understand the reason -# attendance.delete() -# assert len(personio.get_attendances([employee_id])) == 0 - - -# @skip_if_no_auth -# def test_add_attendance_id(): -# # _Personio__add_remote_attendance_id is not available -# employee_id = get_test_employee().id_ -# employee = personio.get_employee(employee_id) -# delete_all_attendances_for_employee(employee) -# attendance = create_attendance_for_user(employee_id, create=True) -# attendance_id = attendance.id_ -# attendance_date = attendance.date -# attendance.id_ = None -# assert attendance.id_ is None -# personio._Personio__add_remote_attendance_id(attendance) -# assert attendance.id_ == attendance_id - -# # Test error conditions -# attendance.id_ = None -# attendance.employee_id = None -# with pytest.raises(ValueError): -# personio._Personio__add_remote_attendance_id(attendance) -# attendance.employee_id = employee_id -# attendance.date = None -# with pytest.raises(ValueError): -# personio._Personio__add_remote_attendance_id(attendance) -# attendance.date = attendance_date -# attendance.id_ = attendance_id -# attendance.delete(personio) -# with pytest.raises(ValueError): -# personio._Personio__add_remote_attendance_id(attendance) -# attendance_1 = create_attendance_for_user(employee_id, start_time="08:00", end_time="12:00", create=True) -# attendance_2 = create_attendance_for_user(employee_id, start_time="13:00", end_time="17:00", create=True) -# attendance_1.id_ = None -# with pytest.raises(ValueError): -# personio._Personio__add_remote_attendance_id(attendance_1) - - -def delete_all_attendances_for_employee(employee: Employee): - attendances = personio.get_attendances([employee.id_]) - for attendance in attendances: - attendance.delete(personio) - - -def prepare_test_get_attendances() -> Employee: - test_data = get_test_employee() - test_user = personio.get_employee(test_data['id']) - - # Be sure there are no leftover attendances - delete_all_attendances_for_employee(test_user) - return test_user - - -def create_attendance_for_user( - employee_id: int, - date: datetime = None, - start_time: str = None, - end_time: str = None, - break_duration: int = None, - comment: str = None, - is_holiday: bool = None, - is_on_time_off: bool = None, - create: bool = False): - if not date: - date = datetime(2021, 1, 1) - if not start_time: - start_time = "08:00" - if not end_time: - end_time = "17:00" - - attendance_to_create = Attendance( - employee_id=employee_id, - date=date, - start_time=start_time, - end_time=end_time, - break_duration=break_duration, - comment=comment, - is_holiday=is_holiday, - is_on_time_off=is_on_time_off - ) - if create: - attendance_to_create.create(personio) - return attendance_to_create -from tests.apitest_shared import * - -from personio_py import Employee, Attendance, PersonioApiError - -from datetime import datetime - - -@skip_if_no_auth -def test_create_attendances(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendances = personio.get_attendances([employee_id]) - assert len(attendances) == 0 - create_attendance_for_user(employee_id, create=True) - attendances = personio.get_attendances([employee_id]) - assert len(attendances) == 1 - - -@skip_if_no_auth -def test_delete_attendance_from_client_id(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - personio.delete_attendance(attendance.id_) - assert len(personio.get_attendances([employee_id])) == 0 - - -@skip_if_no_auth -def test_delete_attendance_from_client_object_with_id(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - personio.delete_attendance(attendance) - assert len(personio.get_attendances([employee_id])) == 0 - - -@skip_if_no_auth -def test_delete_attendance_from_client_object_no_id_query(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - attendance.id_ = None - personio.delete_attendance(attendance, remote_query_id=True) - assert len(personio.get_attendances([employee_id])) == 0 - - -@skip_if_no_auth -def test_delete_attendance_from_client_object_no_id_no_query(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - attendance.id_ = None - with pytest.raises(ValueError): - personio.delete_attendance(attendance, remote_query_id=False) - - -@skip_if_no_auth -def test_delete_attendance_from_model_no_client(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - assert len(personio.get_attendances([employee_id])) == 1 - attendance.delete() - with pytest.raises(PersonioApiError): - personio.delete_attendance(attendance, remote_query_id=False) - - @skip_if_no_auth def test_delete_attendance_from_model_passed_client(): employee_id = get_test_employee().id_ @@ -282,45 +69,11 @@ def test_delete_attendance_from_model_with_client(): delete_all_attendances_for_employee(employee) attendance = create_attendance_for_user(employee_id, create=True) assert len(personio.get_attendances([employee_id])) == 1 - attendance.client = personio + attendance._client = personio attendance.delete() assert len(personio.get_attendances([employee_id])) == 0 -@skip_if_no_auth -def test_add_attendance_id(): - employee_id = get_test_employee().id_ - employee = personio.get_employee(employee_id) - delete_all_attendances_for_employee(employee) - attendance = create_attendance_for_user(employee_id, create=True) - attendance_id = attendance.id_ - attendance_date = attendance.date - attendance.id_ = None - assert attendance.id_ is None - personio._Personio__add_remote_attendance_id(attendance) - assert attendance.id_ == attendance_id - - # Test error conditions - attendance.id_ = None - attendance.employee_id = None - with pytest.raises(ValueError): - personio._Personio__add_remote_attendance_id(attendance) - attendance.employee_id = employee_id - attendance.date = None - with pytest.raises(ValueError): - personio._Personio__add_remote_attendance_id(attendance) - attendance.date = attendance_date - attendance.id_ = attendance_id - attendance.delete(personio) - with pytest.raises(ValueError): - personio._Personio__add_remote_attendance_id(attendance) - attendance_1 = create_attendance_for_user(employee_id, start_time="08:00", end_time="12:00", create=True) - attendance_2 = create_attendance_for_user(employee_id, start_time="13:00", end_time="17:00", create=True) - attendance_1.id_ = None - with pytest.raises(ValueError): - personio._Personio__add_remote_attendance_id(attendance_1) - - def delete_all_attendances_for_employee(employee: Employee): attendances = personio.get_attendances([employee.id_]) for attendance in attendances: diff --git a/tests/test_mock_api.py b/tests/test_mock_api.py index 91140c3..7562049 100644 --- a/tests/test_mock_api.py +++ b/tests/test_mock_api.py @@ -1,5 +1,5 @@ import re -from datetime import date +from datetime import date, timedelta from typing import Any, Dict import pytest From 66d21b451ccdfb170061657ad24111cca26e9883 Mon Sep 17 00:00:00 2001 From: Fateme Tardasti Date: Thu, 16 Mar 2023 17:22:37 +0100 Subject: [PATCH 34/35] Add indentation --- src/personio_py/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/personio_py/client.py b/src/personio_py/client.py index f4c1cde..a81d879 100644 --- a/src/personio_py/client.py +++ b/src/personio_py/client.py @@ -373,7 +373,7 @@ def delete_attendance(self, attendance: Attendance or int): attendance id. DO NOT SET THE ID YOURSELF. :param attendance: The Attendance object holding the new data or an attendance record id to - delete. + delete. :raises: ValueError: If a query is required but not allowed or the query does not provide exactly one result. From 29e60bdc90a19faa7532768013809c9d629701f7 Mon Sep 17 00:00:00 2001 From: Sebastian Straub Date: Fri, 24 Mar 2023 16:39:58 +0100 Subject: [PATCH 35/35] fix a couple of code style thingies --- .pre-commit-config.yaml | 2 +- setup.py | 2 +- src/personio_py/models.py | 6 ++- tests/apitest_shared.py | 5 ++- tests/test_api.py | 4 +- tests/test_api_absences.py | 11 ++++-- tests/test_api_attendances.py | 7 ++-- tests/test_api_raw.py | 3 +- tests/test_mock_api_absences.py | 62 ++++++++++++++++++++++-------- tests/test_mock_api_attendances.py | 20 +++++----- 10 files changed, 80 insertions(+), 42 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 976a7c4..ec84427 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,4 +19,4 @@ repos: rev: '6.0.0' hooks: - id: flake8 - args: [ '--max-complexity=10', '--max-line-length=100', '--ignore=F401,W504', '--exclude=tests/*' ] + args: [ '--max-complexity=10', '--max-line-length=100', '--ignore=F401,W504', '--exclude=tests/*,setup.py' ] diff --git a/setup.py b/setup.py index c739b46..8c974d7 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(fname): """Utility function to read the README file.""" - return open(os.path.join(os.path.dirname(__file__), fname), 'r', encoding='utf-8').read() + return open(os.path.join(os.path.dirname(__file__), fname), encoding='utf-8').read() class DistCommand(Command): diff --git a/src/personio_py/models.py b/src/personio_py/models.py index adca7f9..fc60fd5 100644 --- a/src/personio_py/models.py +++ b/src/personio_py/models.py @@ -9,9 +9,11 @@ from typing import Any, Dict, List, NamedTuple, Optional, TYPE_CHECKING, Tuple, Type, TypeVar from personio_py import PersonioError, UnsupportedMethodError -from personio_py.mapping import BooleanFieldMapping, DateFieldMapping, DateTimeFieldMapping, \ - DurationFieldMapping, DynamicMapping, FieldMapping, ListFieldMapping, NumericFieldMapping, \ +from personio_py.mapping import ( + BooleanFieldMapping, DateFieldMapping, DateTimeFieldMapping, + DurationFieldMapping, DynamicMapping, FieldMapping, ListFieldMapping, NumericFieldMapping, ObjectFieldMapping +) if TYPE_CHECKING: # only type checkers may import Personio, otherwise we get an evil circular import error diff --git a/tests/apitest_shared.py b/tests/apitest_shared.py index 4a5aa76..fc14809 100644 --- a/tests/apitest_shared.py +++ b/tests/apitest_shared.py @@ -1,6 +1,6 @@ import os -from functools import lru_cache from datetime import date +from functools import lru_cache import pytest @@ -15,7 +15,7 @@ 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 +# deactivate all tests that rely on a specific Personio instance try: personio.authenticate() can_authenticate = True @@ -28,6 +28,7 @@ def get_test_employee(): return personio.get_employees()[0] + @lru_cache(maxsize=1) def get_test_employee_for_attendances(): return personio.get_employee(13603465) diff --git a/tests/test_api.py b/tests/test_api.py index d6c6a45..431397c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,8 @@ +from datetime import datetime + from personio_py import Employee, Department from tests.apitest_shared import * -from datetime import datetime + @skip_if_no_auth def test_get_employees(): diff --git a/tests/test_api_absences.py b/tests/test_api_absences.py index d7362e3..4418401 100644 --- a/tests/test_api_absences.py +++ b/tests/test_api_absences.py @@ -1,7 +1,7 @@ -from .apitest_shared import * -from datetime import timedelta, date +from datetime import timedelta -from personio_py import Employee, ShortEmployee, Personio, PersonioError, Absence, AbsenceType +from personio_py import Employee, Absence, AbsenceType +from tests.apitest_shared import * @skip_if_no_auth @@ -138,7 +138,10 @@ def create_absence_for_user(employee: Employee, 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] + 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(year=2022, month=1, day=1) if not end_date: diff --git a/tests/test_api_attendances.py b/tests/test_api_attendances.py index 4593c24..1237c60 100644 --- a/tests/test_api_attendances.py +++ b/tests/test_api_attendances.py @@ -1,9 +1,8 @@ -from tests.apitest_shared import * - -from personio_py import Employee, Attendance, PersonioApiError - from datetime import datetime +from personio_py import Employee, Attendance +from tests.apitest_shared import * + @skip_if_no_auth def test_get_attendances(): diff --git a/tests/test_api_raw.py b/tests/test_api_raw.py index 557bce0..ff3d975 100644 --- a/tests/test_api_raw.py +++ b/tests/test_api_raw.py @@ -28,7 +28,8 @@ 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 # Personio test accounts know 10 different absence types + # Personio test accounts know 10 different absence types + assert len(absence_types['data']) >= 10 @skip_if_no_auth diff --git a/tests/test_mock_api_absences.py b/tests/test_mock_api_absences.py index 9940def..f334d9f 100644 --- a/tests/test_mock_api_absences.py +++ b/tests/test_mock_api_absences.py @@ -1,13 +1,20 @@ +import re from datetime import date import pytest import responses -import re from personio_py import PersonioError, Absence, Employee +from tests.mock_data import ( + json_dict_absence_alan, + json_dict_absence_types, + json_dict_empty_response, + json_dict_delete_absence, + json_dict_absence_alan_first, + json_dict_absence_create_no_halfdays, + json_dict_get_absence +) from tests.test_mock_api import mock_personio, compare_labeled_attributes, mock_employees -from tests.mock_data import json_dict_absence_alan, json_dict_absence_types, json_dict_empty_response,\ - json_dict_delete_absence, json_dict_absence_alan_first, json_dict_absence_create_no_halfdays, json_dict_get_absence @responses.activate @@ -152,45 +159,66 @@ def test_get_absence_types(): def mock_absence_types(): # mock the get absence types endpoint responses.add( - responses.GET, 'https://api.personio.de/v1/company/time-off-types', status=200, - json=json_dict_absence_types, adding_headers={'Authorization': 'Bearer foo'}) + responses.GET, + 'https://api.personio.de/v1/company/time-off-types', + status=200, + json=json_dict_absence_types, + adding_headers={'Authorization': 'Bearer foo'}) def mock_absences(): # mock the get absences endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), - status=200, json=json_dict_absence_alan, adding_headers={'Authorization': 'Bearer foo'}) + responses.GET, + re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), + status=200, + json=json_dict_absence_alan, + adding_headers={'Authorization': 'Bearer foo'}) def mock_single_absences(): # mock the get absences endpoint (with different array offsets) responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), - status=200, json=json_dict_absence_alan_first, adding_headers={'Authorization': 'Bearer foo'}) + responses.GET, + re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), + status=200, + json=json_dict_absence_alan_first, + adding_headers={'Authorization': 'Bearer foo'}) def mock_no_absences(): # mock the get absences endpoint responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), - status=200, json=json_dict_empty_response, adding_headers={'Authorization': 'Bearer bar'}) + responses.GET, + re.compile('https://api.personio.de/v1/company/time-offs?.*offset=1.*'), + status=200, + json=json_dict_empty_response, + adding_headers={'Authorization': 'Bearer bar'}) def mock_delete_absence(): # mock the delete endpoint responses.add( - responses.DELETE, re.compile('https://api.personio.de/v1/company/time-offs/*'), - status=200, json=json_dict_delete_absence, adding_headers={'Authorization': 'Bearer bar'}) + responses.DELETE, + re.compile('https://api.personio.de/v1/company/time-offs/*'), + status=200, + json=json_dict_delete_absence, + adding_headers={'Authorization': 'Bearer bar'}) def mock_create_absence_no_halfdays(): responses.add( - responses.POST, 'https://api.personio.de/v1/company/time-offs', - status=200, json=json_dict_absence_create_no_halfdays, adding_headers={'Authorization': 'Bearer bar'}) + responses.POST, + 'https://api.personio.de/v1/company/time-offs', + status=200, + json=json_dict_absence_create_no_halfdays, + adding_headers={'Authorization': 'Bearer bar'}) def mock_get_absence(): responses.add( - responses.GET, re.compile('https://api.personio.de/v1/company/time-offs/.*'), - status=200, json=json_dict_get_absence, adding_headers={'Authorization': 'Bearer bar'}) \ No newline at end of file + responses.GET, + re.compile('https://api.personio.de/v1/company/time-offs/.*'), + status=200, + json=json_dict_get_absence, + adding_headers={'Authorization': 'Bearer bar'}) diff --git a/tests/test_mock_api_attendances.py b/tests/test_mock_api_attendances.py index 2f57bb7..32f0bae 100644 --- a/tests/test_mock_api_attendances.py +++ b/tests/test_mock_api_attendances.py @@ -1,14 +1,16 @@ -import responses import re - from datetime import timedelta, date -from personio_py import Attendance, Employee +import responses -from tests.mock_data import json_dict_attendance_create_no_break, \ +from personio_py import Attendance, Employee +from tests.mock_data import ( + json_dict_attendance_create_no_break, json_dict_attendance_rms, json_dict_attendance_patch, json_dict_attendance_delete +) from tests.test_mock_api import compare_labeled_attributes, mock_personio + @responses.activate def test_create_attendance(): mock_create_attendance() @@ -68,25 +70,25 @@ def test_delete_attendances(): personio = mock_personio() attendances = personio.get_attendances(2116366) attendance_to_delete = attendances[0] - personio.delete_attendance(attendance_to_delete) + personio.delete_attendance(attendance_to_delete) def mock_attendances(): # mock the get absences endpoint (with different array offsets) responses.add( responses.GET, re.compile('https://api.personio.de/v1/company/attendances?.*'), status=200, json=json_dict_attendance_rms, adding_headers={'Authorization': 'Bearer foo'}) - + def mock_create_attendance(): responses.add( responses.POST, 'https://api.personio.de/v1/company/attendances', status=200, json=json_dict_attendance_create_no_break, adding_headers={'Authorization': 'Bearer bar'}) - + def mock_patch_attendance(): responses.add( responses.PATCH, 'https://api.personio.de/v1/company/attendances/33479712', status=200, json=json_dict_attendance_patch, adding_headers={'Authorization': 'Bearer bar'}) - + def mock_delete_attendance(): responses.add( responses.DELETE, 'https://api.personio.de/v1/company/attendances/33479712', - status=200, json=json_dict_attendance_delete, adding_headers={'Authorization': 'Bearer bar'}) \ No newline at end of file + status=200, json=json_dict_attendance_delete, adding_headers={'Authorization': 'Bearer bar'})