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.
Files changed (38) hide show
  1. brynq_sdk_acerta-1.0.0/PKG-INFO +10 -0
  2. brynq_sdk_acerta-1.0.0/README.md +137 -0
  3. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/__init__.py +13 -0
  4. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/acerta.py +116 -0
  5. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/addresses.py +99 -0
  6. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/agreements.py +498 -0
  7. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/bank_accounts.py +84 -0
  8. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/code_lists.py +264 -0
  9. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/contact_information.py +79 -0
  10. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/cost_centers.py +94 -0
  11. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/employees.py +121 -0
  12. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/employees_additional_information.py +87 -0
  13. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/employer.py +179 -0
  14. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/family_members.py +98 -0
  15. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/family_situation.py +99 -0
  16. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/inservice.py +99 -0
  17. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/salaries.py +74 -0
  18. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/__init__.py +131 -0
  19. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/address.py +77 -0
  20. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/agreement.py +627 -0
  21. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/bank_account.py +84 -0
  22. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/contact_information.py +80 -0
  23. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/cost_center.py +79 -0
  24. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/employee.py +400 -0
  25. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/employer.py +71 -0
  26. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/family.py +214 -0
  27. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/in_service.py +243 -0
  28. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/in_service_config.py +28 -0
  29. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/planning.py +37 -0
  30. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta/schemas/salaries.py +84 -0
  31. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/PKG-INFO +10 -0
  32. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/SOURCES.txt +36 -0
  33. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/dependency_links.txt +1 -0
  34. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/not-zip-safe +1 -0
  35. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/requires.txt +6 -0
  36. brynq_sdk_acerta-1.0.0/brynq_sdk_acerta.egg-info/top_level.txt +1 -0
  37. brynq_sdk_acerta-1.0.0/setup.cfg +4 -0
  38. brynq_sdk_acerta-1.0.0/setup.py +21 -0
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 1.0
2
+ Name: brynq_sdk_acerta
3
+ Version: 1.0.0
4
+ Summary: Acerta wrapper from BrynQ
5
+ Home-page: UNKNOWN
6
+ Author: BrynQ
7
+ Author-email: support@brynq.com
8
+ License: BrynQ License
9
+ Description: Acerta wrapper from BrynQ
10
+ Platform: UNKNOWN
@@ -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