Skip to content

Commit

Permalink
Merge pull request #36 from at-gmbh/feature/projects-fateme
Browse files Browse the repository at this point in the history
Feature/projects
  • Loading branch information
klamann authored May 5, 2023
2 parents d2478be + 96a884f commit b5ead78
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 25 deletions.
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ 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
## [Unreleased](https://github.com/at-gmbh/personio-py/compare/v0.2.3...HEAD)

* debug attendance feature
* add support for attendance paginated API requests
...

## [Unreleased](https://github.com/at-gmbh/personio-py/compare/v0.2.2...HEAD)
## [0.2.3](https://github.com/at-gmbh/personio-py/tree/v0.2.3) - 2023-05-05

* add support for Projects ([#36](https://github.com/at-gmbh/personio-py/pull/36))
* add support for attendances, with paginated API requests ([#35](https://github.com/at-gmbh/personio-py/pull/35))

## [0.2.2](https://github.com/at-gmbh/personio-py/tree/v0.2.2) - 2022-07-04

Expand Down
73 changes: 53 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,26 +84,59 @@ This project is released on [PyPI](https://pypi.org/project/personio-py/). Most

## API Functions

Available

* [`POST /auth`](https://developer.personio.de/reference#auth): fully transparent authentication handling
* [`GET /company/employees`](https://developer.personio.de/reference#get_company-employees): list all employees
* [`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
* [`GET /company/time-offs/{id}`](https://developer.personio.de/reference#get_company-time-offs-id): get the absence entry with the specified ID
* [`DELETE /company/time-offs/{id}`](https://developer.personio.de/reference#delete_company-time-offs-id): delete the absence entry with the specified ID

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
Since the [Personio API](https://developer.personio.de/reference/introduction) gets extended over time, personio-py usually only implements a subset of all available API features. This section gives an overview, which API functions are accessible through personio-py.

### Available

Authentication

* [`POST /auth`](https://developer.personio.de/reference/post_auth-1): fully transparent authentication handling

Employees

* [`GET /company/employees`](https://developer.personio.de/reference/get_company-employees): list all employees
* [`POST /company/employees`](https://developer.personio.de/reference/post_company-employees): create a new employee
* [`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

Attendances

* [`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

Projects

* [`GET /company/attendances/projects`](https://developer.personio.de/reference/get_company-attendances-projects): provides a list of all company projects
* [`POST /company/attendances/projects`](https://developer.personio.de/reference/post_company-attendances-projects): creates a project into the company account
* [`DELETE /company/attendances/projects/{id}`](https://developer.personio.de/reference/delete_company-attendances-projects-id): deletes a project from the company account
* [`PATCH /company/attendances/projects/{id}`](https://developer.personio.de/reference/patch_company-attendances-projects-id): updates a project with the given data

Absences

* [`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
* [`GET /company/time-offs/{id}`](https://developer.personio.de/reference/get_company-time-offs-id): get the absence entry with the specified ID
* [`DELETE /company/time-offs/{id}`](https://developer.personio.de/reference/delete_company-time-offs-id): delete the absence entry with the specified ID

### Not yet implemented

* [`PATCH /company/employees/{id}`](https://developer.personio.de/reference/patch_company-employees-employee-id): update an existing employee entry
* [`GET /company/employees/{employee_id}/absences/balance`](https://developer.personio.de/reference/get_company-employees-employee-id-absences-balance): retrieve the absence balance for a specific employee
* [`GET /company/employees/custom-attributes`](https://developer.personio.de/reference/get_company-employees-custom-attributes): this endpoint is an alias for /company/employees/attributes
* [`GET /company/employees/attributes`](https://developer.personio.de/reference/get_company-employees-attributes): lists all the allowed attributes per API credentials including custom (dynamic) attributes.
* [`GET /company/absence-periods`](https://developer.personio.de/reference/get_company-absence-periods)
* [`POST /company/absence-periods`](https://developer.personio.de/reference/post_company-absence-periods)
* [`DELETE /company/absence-periods/{id}`](https://developer.personio.de/reference/delete_company-absence-periods-id)

* [`GET /company/document-categories`](https://developer.personio.de/reference/get_company-document-categories): this endpoint is responsible for fetching all document categories of the company
* [`POST /company/documents`](https://developer.personio.de/reference/post_company-documents): this endpoint is responsible for uploading documents for the company employees
* [`GET /company/custom-reports/reports`](https://developer.personio.de/reference/listreports): this endpoint provides you with metadata about existing custom reports in your Personio account, such as report name, report type, report date / timeframe
* [`GET /company/custom-reports/reports/{report_id}`](https://developer.personio.de/reference/listreportitems): this endpoint provides you with the data of an existing Custom Report
* [`GET /company/custom-reports/columns`](https://developer.personio.de/reference/listcolumns): this endpoint provides human-readable labels for report table columns
* all of the [recruiting API](https://developer.personio.de/reference/introduction-1)

## Contact

Expand Down
1 change: 1 addition & 0 deletions src/personio_py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
ShortEmployee,
Team,
WorkSchedule,
Project
)
from personio_py.client import Personio
59 changes: 58 additions & 1 deletion src/personio_py/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import requests
from requests import Response

from personio_py import Absence, AbsenceType, Attendance, DynamicMapping, Employee
from personio_py import Absence, AbsenceType, Attendance, DynamicMapping, Employee, Project
from personio_py.errors import MissingCredentialsError, PersonioApiError, PersonioError
from personio_py.models import PersonioResource
from personio_py.search import SearchIndex
Expand All @@ -37,6 +37,7 @@ class Personio:
"""base URL of the Personio HTTP API"""
ATTENDANCE_URL = 'company/attendances'
ABSENCE_URL = 'company/time-offs'
PROJECT_URL = 'company/attendances/projects'

def __init__(self, base_url: str = None, client_id: str = None, client_secret: str = None,
dynamic_fields: List[DynamicMapping] = None):
Expand Down Expand Up @@ -514,6 +515,62 @@ def invalidate_index(self):
"""
self.search_index.invalidate()

def get_projects(self) -> List[Project]:
"""
Get a list of all company projects.
:return: list of ``Project`` records
"""
response = self.request_json(self.PROJECT_URL)
projects = [Project.from_dict(d, self) for d in response['data']]
return projects

def create_project(self, project: Project) -> Project:
"""
Creates a project record on the Personio servers.
:param project: The project object to be created
:raises PersonioError: If the project could not be created on the Personio servers
"""
data = project.to_body_params()
response = self.request_json(self.PROJECT_URL, method='POST', data=data)
if response['success']:
project.id_ = response['data']['id']
return project
raise PersonioError("Could not create project")

def update_project(self, project: Project) -> Project:
"""
Updates a project record on the Personio servers.
:param project: The project object to be updated
:raises PersonioErrror: If the project could not be created on the Personio servers
"""
data = project.to_body_params()
response = self.request_json(f'{self.PROJECT_URL}/{project.id_}', method='PATCH', data=data)
if response['success']:
return project
raise PersonioError("Could not update project")

def delete_project(self, project: Union[Project, int]) -> None:
"""
Deletes a project record on the Personio servers.
:param project: The project object to be updated
:raises ValueError: If a query is required but not allowed
or the query does not provide exactly one result.
"""
if isinstance(project, int):
response = self.request(f'{self.PROJECT_URL}/{project}', method='DELETE')
return response
elif isinstance(project, Project):
if project.id_ is not None:
return self.delete_project(project.id_)
else:
raise ValueError("Only a project with a project id can be deleted.")
else:
raise ValueError("project must be a Project object or an integer")

def _get_employee_metadata(
self, path: str, resource_cls: Type[PersonioResourceType],
employees: Union[int, List[int], Employee, List[Employee]], start_date: datetime = None,
Expand Down
46 changes: 46 additions & 0 deletions src/personio_py/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,52 @@ def to_body_params(self):
return data


class Project(WritablePersonioResource):

_api_type_name = "Project"
_field_mapping_list = [
# note: the id is actually not in the attributes dict, but one level higher
NumericFieldMapping('id', 'id_', int),
FieldMapping('name', 'name', str),
BooleanFieldMapping('active', 'active'),
DateTimeFieldMapping('created_at', 'created_at'),
DateTimeFieldMapping('updated_at', 'updated_at')
]

def __init__(self, client: 'Personio' = None, dynamic: Dict[str, Any] = None,
dynamic_raw: List['DynamicAttr'] = None, id_: int = None, name: str = None,
active: bool = None, created_at: datetime = None, updated_at: datetime = None,
**kwargs):
super().__init__(client=client, dynamic=dynamic, dynamic_raw=dynamic_raw, **kwargs)
self.id_ = id_
self.name = name
self.active = active
self.created_at = created_at
self.updated_at = updated_at

def _create(self, client: 'Personio' = None):
return get_client(self, client).create_project(self)

def _delete(self, client: 'Personio' = None):
return get_client(self, client).delete_project(self)

def _update(self, client: 'Personio' = None):
return get_client(self, client).update_project(self)

def to_dict(self, nested=False) -> Dict[str, Any]:
# yes, this is weird an unnecessary, but that's how the api works
d = super().to_dict()
d['id'] = self.id_
del d['attributes']['id']
return d

def to_body_params(self):
data = {
'name': self.name,
'active': self.active}
return data


class Attendance(WritablePersonioResource):

_api_type_name = "AttendancePeriod"
Expand Down
73 changes: 73 additions & 0 deletions tests/mock_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,3 +1179,76 @@
}
"""
json_dict_attendance_delete = json.loads(json_string_attendance_delete)

json_string_project_rms = """
{
"success": true,
"data": [
{
"id": 238751,
"type": "Project",
"attributes": {
"name": "conwik project",
"active": true,
"created_at": "2019-03-01T16:00:00",
"updated_at": "2020-03-02T16:11:35"
}
}, {
"id": 238750,
"type": "Project",
"attributes": {
"name": "mock project",
"active": true,
"created_at": "2023-03-28T16:11:35",
"updated_at": "2023-03-28T16:11:35"
}
}, {
"id": 238752,
"type": "Project",
"attributes": {
"name": "alianz project",
"active": false,
"created_at": "2020-10-10T10:11:35",
"updated_at": "2023-02-22T17:11:35"
}
}
]
}
"""
json_dict_project_rms = json.loads(json_string_project_rms)

json_string_project_create = """
{
"success":true,
"data":{
"id": 83648600,
"message": "success"
}
}
"""
json_dict_project_create = json.loads(json_string_project_create)

json_string_project_delete = """
{
"text":"",
"status_code": 204
}
"""
json_dict_project_delete = json.loads(json_string_project_delete)

json_string_project_update = """
{
"success": true,
"data": {
"id": 238760,
"type": "Project",
"attributes": {
"name": "updated mock project",
"active": true,
"created_at": "2023-03-28T16:11:35",
"updated_at": "2023-03-28T16:11:52"
}
}
}
"""
json_dict_project_update = json.loads(json_string_project_update)
52 changes: 52 additions & 0 deletions tests/test_api_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from .apitest_shared import *
from personio_py import Project


@skip_if_no_auth
def test_get_projects():
create_test_project(name = "test project")
projects = personio.get_projects()

assert len(projects)>0
assert projects[-1].name == "test project"
assert projects[-1].active == False
personio.delete_project(projects[-1])


@skip_if_no_auth
def test_update_project():
"""
Test the update of project records on the server.
"""
project = create_test_project(name="initial project", active=True)
project.name = "updated project"
updated_project = personio.update_project(project)

assert updated_project.name == "updated project"

project.active = False
updated_project = personio.update_project(project)

assert updated_project.active == False

personio.delete_project(project)

@skip_if_no_auth
def test_delete_project_by_id():
project = create_test_project(name="delete project by id", active=True)
response = personio.delete_project(project.id_)

assert response.status_code == 204

@skip_if_no_auth
def test_delete_project_by_project_object():
project = create_test_project(name="delete project by object", active=True)
response = personio.delete_project(project)

assert response.status_code == 204

def create_test_project(name: str = 'project name', active: bool = False):
p = Project(name=name, active=active)
project = personio.create_project(p)

return project
Loading

0 comments on commit b5ead78

Please sign in to comment.