brynq-sdk-acerta 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brynq_sdk_acerta-1.0.0/PKG-INFO +10 -0
- brynq_sdk_acerta-1.0.0/README.md +137 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/__init__.py +13 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/acerta.py +116 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/addresses.py +99 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/agreements.py +498 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/bank_accounts.py +84 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/code_lists.py +264 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/contact_information.py +79 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/cost_centers.py +94 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/employees.py +121 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/employees_additional_information.py +87 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/employer.py +179 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/family_members.py +98 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/family_situation.py +99 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/inservice.py +99 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/salaries.py +74 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/__init__.py +131 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/address.py +77 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/agreement.py +627 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/bank_account.py +84 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/contact_information.py +80 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/cost_center.py +79 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/employee.py +400 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/employer.py +71 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/family.py +214 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/in_service.py +243 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/in_service_config.py +28 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/planning.py +37 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/salaries.py +84 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/PKG-INFO +10 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/SOURCES.txt +36 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/dependency_links.txt +1 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/not-zip-safe +1 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/requires.txt +6 -0
- brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/top_level.txt +1 -0
- brynq_sdk_acerta-1.0.0/setup.cfg +4 -0
- brynq_sdk_acerta-1.0.0/setup.py +21 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# BrynQ SDK - Acerta
|
|
2
|
+
|
|
3
|
+
Python SDK for interacting with the Acerta HR & Payroll API.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Acerta SDK provides a clean and intuitive interface to interact with Acerta's HR and payroll management system. Built on top of the BrynQ platform, it offers seamless integration with validated data handling using Pandera and Pydantic.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔐 **Secure Authentication**: Bearer token-based authentication with automatic credential management
|
|
12
|
+
- 🌍 **Multi-Environment Support**: Easily switch between test and production environments
|
|
13
|
+
- ✅ **Data Validation**: Built-in validation using Pandera (GET) and Pydantic (POST/PUT/PATCH)
|
|
14
|
+
- 📊 **Pandas Integration**: GET operations return validated DataFrames for easy data manipulation
|
|
15
|
+
- 🔄 **Complete CRUD Operations**: Full support for Create, Read, Update, and Delete operations
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install brynq-sdk-acerta
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from brynq_sdk_acerta import Acerta
|
|
27
|
+
|
|
28
|
+
# Initialize client (debug=True for test environment)
|
|
29
|
+
client = Acerta(system_type="source", debug=False)
|
|
30
|
+
|
|
31
|
+
# Get employees
|
|
32
|
+
employees_df, invalid_df = client.employees.get()
|
|
33
|
+
|
|
34
|
+
# Get planning data
|
|
35
|
+
planning_df, invalid_df = client.planning.get()
|
|
36
|
+
|
|
37
|
+
# Create new employee
|
|
38
|
+
employee_data = {
|
|
39
|
+
"firstName": "John",
|
|
40
|
+
"lastName": "Doe",
|
|
41
|
+
"email": "john.doe@example.com"
|
|
42
|
+
}
|
|
43
|
+
response = client.employees.create(employee_data)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Available Resources
|
|
47
|
+
|
|
48
|
+
### Core Resources
|
|
49
|
+
- **`employees`** - Employee management (includes addresses, contact information, family, bank accounts)
|
|
50
|
+
- **`employment`** - Employment contracts and relationships
|
|
51
|
+
- **`employer`** - Employer information and cost centers
|
|
52
|
+
- **`agreements`** - Employment agreements and contracts
|
|
53
|
+
- **`planning`** - Workforce planning and scheduling
|
|
54
|
+
|
|
55
|
+
### Nested Resources
|
|
56
|
+
Access nested resources through their parent:
|
|
57
|
+
```python
|
|
58
|
+
# Employee sub-resources
|
|
59
|
+
client.employees.addresses
|
|
60
|
+
client.employees.contact_information
|
|
61
|
+
client.employees.family
|
|
62
|
+
client.employees.bank_accounts
|
|
63
|
+
|
|
64
|
+
# Employer sub-resources
|
|
65
|
+
client.employer.cost_centers
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Environment Configuration
|
|
69
|
+
|
|
70
|
+
The SDK supports two environments:
|
|
71
|
+
|
|
72
|
+
- **Production**: `https://api.acerta.be`
|
|
73
|
+
- **Test**: `https://a-api.acerta.be` (enabled with `debug=True`)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Production environment
|
|
77
|
+
client = Acerta(system_type="source", debug=False)
|
|
78
|
+
|
|
79
|
+
# Test environment
|
|
80
|
+
client = Acerta(system_type="source", debug=True)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Data Validation
|
|
84
|
+
|
|
85
|
+
### GET Operations
|
|
86
|
+
Returns a tuple of two DataFrames:
|
|
87
|
+
- **Valid data**: Successfully validated records
|
|
88
|
+
- **Invalid data**: Records that failed validation with error details
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
valid_df, invalid_df = client.employees.get()
|
|
92
|
+
print(f"Valid records: {len(valid_df)}")
|
|
93
|
+
print(f"Invalid records: {len(invalid_df)}")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### POST/PUT/PATCH Operations
|
|
97
|
+
Data is automatically validated against Pydantic schemas before sending:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# Data is validated before the request
|
|
101
|
+
response = client.employees.create({
|
|
102
|
+
"firstName": "Jane",
|
|
103
|
+
"lastName": "Smith",
|
|
104
|
+
"dateOfBirth": "1990-01-15"
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Error Handling
|
|
109
|
+
|
|
110
|
+
The SDK includes comprehensive error handling with descriptive messages:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
try:
|
|
114
|
+
response = client.employees.create(employee_data)
|
|
115
|
+
if response.status_code == 201:
|
|
116
|
+
print("Employee created successfully")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"Error: {str(e)}")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Requirements
|
|
122
|
+
|
|
123
|
+
- Python 3.8+
|
|
124
|
+
- pandas >= 2.2.0
|
|
125
|
+
- pydantic >= 2.5.0
|
|
126
|
+
- pandera >= 0.16.0
|
|
127
|
+
- requests >= 2.25.1
|
|
128
|
+
- brynq-sdk-functions >= 2.0.5
|
|
129
|
+
- brynq-sdk-brynq >= 3
|
|
130
|
+
|
|
131
|
+
## Support
|
|
132
|
+
|
|
133
|
+
For issues, questions, or contributions, please contact BrynQ support at support@brynq.com
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
BrynQ License - © BrynQ
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brynq SDK for Acerta API integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .acerta import Acerta
|
|
6
|
+
from .agreements import Agreements
|
|
7
|
+
from .employees import Employees
|
|
8
|
+
from .code_lists import CodeLists
|
|
9
|
+
from .employer import Employer
|
|
10
|
+
from .employees_additional_information import EmployeesAdditionalInformation
|
|
11
|
+
from .inservice import InService
|
|
12
|
+
|
|
13
|
+
__all__ = ['Acerta', 'CodeLists', 'Agreements', 'Employees', 'Employer', 'InService', 'EmployeesAdditionalInformation']
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing_extensions import List
|
|
2
|
+
from typing import Literal, Optional, Union
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from .salaries import Salaries
|
|
6
|
+
from .cost_centers import CostCenters
|
|
7
|
+
from brynq_sdk_brynq import BrynQ
|
|
8
|
+
from .code_lists import CodeLists
|
|
9
|
+
from .agreements import Agreements
|
|
10
|
+
from .inservice import InService
|
|
11
|
+
from .employees import Employees
|
|
12
|
+
from .employer import Employer
|
|
13
|
+
from requests_oauthlib import OAuth2Session
|
|
14
|
+
from oauthlib.oauth2 import BackendApplicationClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Acerta(BrynQ):
|
|
18
|
+
"""
|
|
19
|
+
Base class for interacting with the Acerta API.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Default timeout in seconds for all requests
|
|
23
|
+
TIMEOUT = 30
|
|
24
|
+
|
|
25
|
+
def __init__(self, employers: Union[str, List], system_type: Optional[Literal['source', 'target']] = None, test_environment: bool = True, debug: bool = False):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the Acerta API client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
system_type (str): System type ('source' or 'target')
|
|
31
|
+
debug (bool): Debug flag - if True uses test environment, if False uses production
|
|
32
|
+
"""
|
|
33
|
+
super().__init__()
|
|
34
|
+
|
|
35
|
+
# Compute environment-specific prefix once and reuse
|
|
36
|
+
env_prefix = "a-" if test_environment else ""
|
|
37
|
+
self.base_url = f"https://{env_prefix}api.acerta.be"
|
|
38
|
+
|
|
39
|
+
# Extract credentials and configure OAuth2 session with automatic token renewal
|
|
40
|
+
credentials = self.interfaces.credentials.get(
|
|
41
|
+
system="acerta-acceptance",
|
|
42
|
+
system_type=system_type,
|
|
43
|
+
)
|
|
44
|
+
data = credentials.get("data", {})
|
|
45
|
+
client_id = data.get("client_id")
|
|
46
|
+
client_secret = data.get("client_secret")
|
|
47
|
+
|
|
48
|
+
# Token endpoint (match test/prod like base_url)
|
|
49
|
+
token_url = f"https://{env_prefix}signin.acerta.be/am/oauth2/access_token"
|
|
50
|
+
|
|
51
|
+
# Store client credentials for reuse
|
|
52
|
+
self._client_id = client_id
|
|
53
|
+
self._client_secret = client_secret
|
|
54
|
+
self._token_url = token_url
|
|
55
|
+
|
|
56
|
+
# Create OAuth2 session using client credentials (Backend Application flow)
|
|
57
|
+
client = BackendApplicationClient(client_id=self._client_id)
|
|
58
|
+
oauth_session = OAuth2Session(client=client)
|
|
59
|
+
|
|
60
|
+
# Fetch initial token
|
|
61
|
+
token = oauth_session.fetch_token(
|
|
62
|
+
token_url=self._token_url,
|
|
63
|
+
client_id=self._client_id,
|
|
64
|
+
client_secret=self._client_secret,
|
|
65
|
+
include_client_id=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Keep access_token attribute for backward compatibility
|
|
69
|
+
self.access_token = token.get("access_token")
|
|
70
|
+
|
|
71
|
+
# Attach default headers; Authorization is managed by OAuth2Session
|
|
72
|
+
oauth_session.headers.update({
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
# Ensure token is valid before each request (client_credentials has no refresh_token)
|
|
78
|
+
self._orig_request = oauth_session.request
|
|
79
|
+
oauth_session.request = self._request_with_pre_expiry # type: ignore[assignment]
|
|
80
|
+
|
|
81
|
+
# Use the OAuth2 session for all requests
|
|
82
|
+
self.session = oauth_session
|
|
83
|
+
self.session.timeout = self.TIMEOUT # type: ignore[attr-defined]
|
|
84
|
+
self._employer_ids = employers if isinstance(employers, List) else [employers]
|
|
85
|
+
self._employee_ids = set()
|
|
86
|
+
self._agreement_ids = set()
|
|
87
|
+
|
|
88
|
+
# Set debug mode
|
|
89
|
+
self.debug = debug
|
|
90
|
+
|
|
91
|
+
# Initialize resource classes
|
|
92
|
+
self.agreements = Agreements(self)
|
|
93
|
+
self.inservice = InService(self)
|
|
94
|
+
self.cost_centers = CostCenters(self)
|
|
95
|
+
self.code_lists = CodeLists(self)
|
|
96
|
+
self.employees = Employees(self)
|
|
97
|
+
self.employers = Employer(self)
|
|
98
|
+
self.salaries = Salaries(self)
|
|
99
|
+
|
|
100
|
+
def _ensure_valid_token(self):
|
|
101
|
+
"""Ensure the OAuth token exists and is not about to expire."""
|
|
102
|
+
tok = getattr(self.session, "token", {}) or {}
|
|
103
|
+
expires_at = tok.get("expires_at")
|
|
104
|
+
# Refresh if missing or expiring within 30 seconds
|
|
105
|
+
if not expires_at or (expires_at - time.time()) < 30:
|
|
106
|
+
new_token = self.session.fetch_token(
|
|
107
|
+
token_url=self._token_url,
|
|
108
|
+
client_id=self._client_id,
|
|
109
|
+
client_secret=self._client_secret,
|
|
110
|
+
include_client_id=True,
|
|
111
|
+
)
|
|
112
|
+
self.access_token = new_token.get("access_token")
|
|
113
|
+
|
|
114
|
+
def _request_with_pre_expiry(self, method, url, **kwargs):
|
|
115
|
+
self._ensure_valid_token()
|
|
116
|
+
return self._orig_request(method, url, **kwargs)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Tuple, Dict, Any
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import requests
|
|
4
|
+
from brynq_sdk_functions import Functions
|
|
5
|
+
from .schemas.address import AddressGet, AddressUpdate
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .acerta import Acerta
|
|
9
|
+
|
|
10
|
+
class Addresses:
|
|
11
|
+
"""Resource class for Employee Address endpoints"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, acerta):
|
|
14
|
+
self.acerta: Acerta = acerta
|
|
15
|
+
self.base_uri = "v3/employee-data-management/v3/employees"
|
|
16
|
+
|
|
17
|
+
def get(self, from_date: str = "1900-01-01", until_date: str = "9999-12-31") -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
18
|
+
"""
|
|
19
|
+
GET /employee-data-management/v3/employees/{employeeId}/addresses - Employee Addresses
|
|
20
|
+
|
|
21
|
+
Retrieve employee address history within a specified time window for all cached employees.
|
|
22
|
+
Returns both official and correspondence addresses with their validity periods.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
from_date: Start date of the time window (default: "1900-01-01")
|
|
26
|
+
until_date: End date of the time window (default: "9999-12-31")
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple[pd.DataFrame, pd.DataFrame]: (valid_df, invalid_df) after validation
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
RuntimeError: If the retrieval fails
|
|
33
|
+
"""
|
|
34
|
+
params = {
|
|
35
|
+
"fromDate": from_date,
|
|
36
|
+
"untilDate": until_date
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if not self.acerta._employee_ids:
|
|
40
|
+
# Auto-warm cache by fetching agreements (populates employee IDs)
|
|
41
|
+
self.acerta.agreements.get()
|
|
42
|
+
|
|
43
|
+
all_frames = []
|
|
44
|
+
for employee_id in self.acerta._employee_ids:
|
|
45
|
+
response = self.acerta.session.get(
|
|
46
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/addresses",
|
|
47
|
+
params=params,
|
|
48
|
+
timeout=self.acerta.TIMEOUT,
|
|
49
|
+
)
|
|
50
|
+
response.raise_for_status()
|
|
51
|
+
content = response.json()
|
|
52
|
+
df = pd.json_normalize(
|
|
53
|
+
content,
|
|
54
|
+
record_path=['addressSegments'],
|
|
55
|
+
meta=['employeeId'],
|
|
56
|
+
sep='.'
|
|
57
|
+
)
|
|
58
|
+
if 'externalReferences' in df.columns:
|
|
59
|
+
df = df.drop(columns=['externalReferences'])
|
|
60
|
+
all_frames.append(df)
|
|
61
|
+
|
|
62
|
+
combined = pd.concat(all_frames, ignore_index=True) if all_frames else pd.DataFrame()
|
|
63
|
+
|
|
64
|
+
valid_data, invalid_data = Functions.validate_data(combined, AddressGet)
|
|
65
|
+
|
|
66
|
+
return valid_data, invalid_data
|
|
67
|
+
|
|
68
|
+
def update(self, employee_id: str, data: Dict[str, Any]) -> requests.Response:
|
|
69
|
+
"""
|
|
70
|
+
PATCH /employee-data-management/v3/employees/{employeeId}/addresses - Employee Address
|
|
71
|
+
|
|
72
|
+
Update employee address information including official address and optional
|
|
73
|
+
correspondence address. Addresses are historical data with validity dates.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
employee_id: Unique identifier of an employee
|
|
77
|
+
data: Flat dictionary with address data
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
requests.Response: Raw response object
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
RuntimeError: If the address update fails
|
|
84
|
+
"""
|
|
85
|
+
# Convert flat data to nested using Functions.flat_to_nested_with_prefix
|
|
86
|
+
nested_data = Functions.flat_to_nested_with_prefix(data, AddressUpdate)
|
|
87
|
+
|
|
88
|
+
# Validate the nested data
|
|
89
|
+
validated_data = AddressUpdate(**nested_data)
|
|
90
|
+
|
|
91
|
+
# Make API request
|
|
92
|
+
response = self.acerta.session.patch(
|
|
93
|
+
url=f"{self.acerta.base_url}/{self.base_uri}/{employee_id}/addresses",
|
|
94
|
+
json=validated_data.model_dump(by_alias=True, exclude_none=True),
|
|
95
|
+
timeout=self.acerta.TIMEOUT,
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
|
|
99
|
+
return response
|