brynq-sdk-bob 2.4.0__tar.gz → 2.4.3__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 (35) hide show
  1. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/PKG-INFO +1 -1
  2. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/__init__.py +2 -0
  3. brynq_sdk_bob-2.4.3/brynq_sdk_bob/custom_tables.py +75 -0
  4. brynq_sdk_bob-2.4.0/brynq_sdk_bob/people.py → brynq_sdk_bob-2.4.3/brynq_sdk_bob/payroll_history.py +1 -12
  5. brynq_sdk_bob-2.4.3/brynq_sdk_bob/people.py +340 -0
  6. brynq_sdk_bob-2.4.3/brynq_sdk_bob/schemas/custom_tables.py +27 -0
  7. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/payments.py +1 -1
  8. brynq_sdk_bob-2.4.3/brynq_sdk_bob/schemas/payroll_history.py +24 -0
  9. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/people.py +4 -4
  10. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/timeoff.py +10 -9
  11. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/timeoff.py +3 -7
  12. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob.egg-info/PKG-INFO +1 -1
  13. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob.egg-info/SOURCES.txt +2 -0
  14. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/setup.py +1 -1
  15. brynq_sdk_bob-2.4.0/brynq_sdk_bob/custom_tables.py +0 -36
  16. brynq_sdk_bob-2.4.0/brynq_sdk_bob/schemas/custom_tables.py +0 -11
  17. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/bank.py +0 -0
  18. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/company.py +0 -0
  19. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/documents.py +0 -0
  20. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/employment.py +0 -0
  21. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/named_lists.py +0 -0
  22. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/payments.py +0 -0
  23. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/salaries.py +0 -0
  24. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/__init__.py +0 -0
  25. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/bank.py +0 -0
  26. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/employment.py +0 -0
  27. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/named_lists.py +0 -0
  28. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/salary.py +0 -0
  29. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/schemas/work.py +0 -0
  30. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob/work.py +0 -0
  31. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob.egg-info/dependency_links.txt +0 -0
  32. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob.egg-info/not-zip-safe +0 -0
  33. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob.egg-info/requires.txt +0 -0
  34. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/brynq_sdk_bob.egg-info/top_level.txt +0 -0
  35. {brynq_sdk_bob-2.4.0 → brynq_sdk_bob-2.4.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq_sdk_bob
3
- Version: 2.4.0
3
+ Version: 2.4.3
4
4
  Summary: Bob wrapper from BrynQ
5
5
  Home-page: UNKNOWN
6
6
  Author: BrynQ
@@ -16,6 +16,7 @@ from .people import People
16
16
  from .salaries import Salaries
17
17
  from .timeoff import TimeOff
18
18
  from .work import Work
19
+ from .custom_tables import CustomTables
19
20
 
20
21
  class Bob(BrynQ):
21
22
  def __init__(self, system_type: Optional[Literal['source', 'target']] = None, test_environment: bool = True, debug: bool = False, target_system: str = None):
@@ -38,6 +39,7 @@ class Bob(BrynQ):
38
39
  self.documents = CustomDocuments(self)
39
40
  self.companies = Company(self)
40
41
  self.named_lists = NamedLists(self)
42
+ self.custom_tables = CustomTables(self)
41
43
  self.data_interface_id = os.getenv("DATA_INTERFACE_ID")
42
44
  self.debug = debug
43
45
 
@@ -0,0 +1,75 @@
1
+ import pandas as pd
2
+ from brynq_sdk_functions import Functions
3
+ from .schemas.custom_tables import CustomTableSchema, CustomTableMetadataSchema
4
+
5
+
6
+ class CustomTables:
7
+ def __init__(self, bob):
8
+ self.bob = bob
9
+ self.schema = CustomTableSchema
10
+
11
+ def get(self, employee_id: str, custom_table_id: str) -> tuple[pd.DataFrame, pd.DataFrame]:
12
+ """
13
+ Get custom table data for an employee
14
+
15
+ Args:
16
+ employee_id: The employee ID
17
+ custom_table_id: The custom table ID
18
+
19
+ Returns:
20
+ A tuple of (valid_data, invalid_data) as pandas DataFrames
21
+ """
22
+ resp = self.bob.session.get(url=f"{self.bob.base_url}people/custom-tables/{employee_id}/{custom_table_id}")
23
+ resp.raise_for_status()
24
+ data = resp.json()
25
+
26
+ # Normalize the nested JSON response
27
+ df = pd.json_normalize(
28
+ data,
29
+ record_path=['values']
30
+ )
31
+
32
+ df['employee_id'] = employee_id
33
+ valid_data, invalid_data = Functions.validate_data(df=df, schema=self.schema, debug=True)
34
+
35
+ return valid_data, invalid_data
36
+
37
+ def get_metadata(self) -> tuple[pd.DataFrame, pd.DataFrame]:
38
+ """
39
+ Get metadata for all custom tables
40
+
41
+ Returns:
42
+ A tuple of (valid_data, invalid_data) as pandas DataFrames containing table and column metadata
43
+ """
44
+ url = f"{self.bob.base_url}people/custom-tables/metadata"
45
+ resp = self.bob.session.get(url=url)
46
+ resp.raise_for_status()
47
+ data = resp.json()
48
+
49
+ # Flatten the nested structure - create one row per column with table info repeated
50
+ rows = []
51
+ for table in data.get('tables', []):
52
+ table_info = {
53
+ 'table_id': table.get('id'),
54
+ 'table_name': table.get('name'),
55
+ 'table_category': table.get('category'),
56
+ 'table_description': table.get('description')
57
+ }
58
+
59
+ for column in table.get('columns', []):
60
+ row = {
61
+ **table_info,
62
+ 'column_id': column.get('id'),
63
+ 'column_name': column.get('name'),
64
+ 'column_description': column.get('description'),
65
+ 'column_mandatory': column.get('mandatory'),
66
+ 'column_type': column.get('type')
67
+ }
68
+ rows.append(row)
69
+
70
+ df = pd.DataFrame(rows)
71
+
72
+ # Validate against the metadata schema
73
+ valid_data, invalid_data = Functions.validate_data(df=df, schema=CustomTableMetadataSchema, debug=True)
74
+
75
+ return valid_data, invalid_data
@@ -1,21 +1,10 @@
1
1
  import pandas as pd
2
2
  from brynq_sdk_functions import Functions
3
- from .bank import Bank
4
- from .employment import Employment
5
- from .salaries import Salaries
6
3
  from .schemas.people import PeopleSchema
7
- from .work import Work
8
- from .custom_tables import CustomTables
9
4
 
10
-
11
- class People:
5
+ class History:
12
6
  def __init__(self, bob):
13
7
  self.bob = bob
14
- self.salaries = Salaries(bob)
15
- self.employment = Employment(bob)
16
- self.bank = Bank(bob)
17
- self.work = Work(bob)
18
- self.custom_tables = CustomTables(bob)
19
8
  self.schema = PeopleSchema
20
9
  self.field_name_in_body, self.field_name_in_response, self.endpoint_to_response = self._init_fields()
21
10
 
@@ -0,0 +1,340 @@
1
+ import pandas as pd
2
+ import re
3
+ from brynq_sdk_functions import Functions
4
+ from .bank import Bank
5
+ from .employment import Employment
6
+ from .salaries import Salaries
7
+ from .schemas.people import PeopleSchema
8
+ from .work import Work
9
+ from .custom_tables import CustomTables
10
+
11
+
12
+ class People:
13
+ def __init__(self, bob):
14
+ self.bob = bob
15
+ self.salaries = Salaries(bob)
16
+ self.employment = Employment(bob)
17
+ self.bank = Bank(bob)
18
+ self.work = Work(bob)
19
+ self.custom_tables = CustomTables(bob)
20
+ self.schema = PeopleSchema
21
+ self.field_name_in_body, self.field_name_in_response, self.endpoint_to_response = self._init_fields()
22
+
23
+ # Define payroll information types and their configurations
24
+ self.payroll_types = {
25
+ 'entitlement': {
26
+ 'pattern': 'payroll.entitlement.',
27
+ 'named_list_key': 'entitlementType'
28
+ },
29
+ 'variable': {
30
+ 'pattern': 'payroll.variable.',
31
+ 'named_list_key': 'payrollVariableType'
32
+ }
33
+ }
34
+ self.payroll_mappings = {}
35
+
36
+ def _add_payroll_fields_to_request(self, body_fields: list, response_fields: list, payroll_type: str) -> tuple[list, list]:
37
+ """
38
+ Add payroll fields to request fields for a specific payroll type.
39
+
40
+ Args:
41
+ body_fields: Current body fields
42
+ response_fields: Current response fields
43
+ payroll_type: Type of payroll information (e.g., 'entitlement', 'variable')
44
+
45
+ Returns:
46
+ - body_fields: Updated body fields including payroll fields
47
+ - response_fields: Updated response fields including payroll fields
48
+ """
49
+ if payroll_type not in self.payroll_types:
50
+ return body_fields, response_fields
51
+
52
+ pattern = self.payroll_types[payroll_type]['pattern']
53
+ payroll_fields_to_add = [field for field in self.field_name_in_body if pattern in field.lower()]
54
+ payroll_response_fields = [self.endpoint_to_response.get(field) for field in payroll_fields_to_add if field in self.endpoint_to_response]
55
+
56
+ for field in payroll_fields_to_add:
57
+ if field not in body_fields:
58
+ body_fields.append(field)
59
+
60
+ for field in payroll_response_fields:
61
+ if field and field not in response_fields:
62
+ response_fields.append(field)
63
+
64
+ return body_fields, response_fields
65
+
66
+ def _get_payroll_mapping(self, payroll_type: str) -> dict:
67
+ """
68
+ Get mapping of payroll IDs to their names from the named-lists endpoint for a specific payroll type.
69
+
70
+ Args:
71
+ payroll_type: Type of payroll information (e.g., 'entitlement', 'variable')
72
+
73
+ Returns:
74
+ Dictionary mapping IDs to names
75
+ """
76
+ if payroll_type not in self.payroll_types:
77
+ return {}
78
+
79
+ # Check if we already have the mapping cached
80
+ if payroll_type in self.payroll_mappings:
81
+ return self.payroll_mappings[payroll_type]
82
+
83
+ resp = self.bob.session.get(
84
+ url=f"{self.bob.base_url}company/named-lists",
85
+ timeout=self.bob.timeout,
86
+ headers=self.bob.headers
87
+ )
88
+ named_lists = resp.json()
89
+
90
+ # Extract the mapping for this payroll type
91
+ mapping = {}
92
+ named_list_key = self.payroll_types[payroll_type]['named_list_key']
93
+ if named_list_key in named_lists:
94
+ for value in named_lists[named_list_key]['values']:
95
+ mapping[value['id']] = value['name']
96
+
97
+ # Cache the mapping
98
+ self.payroll_mappings[payroll_type] = mapping
99
+ return mapping
100
+
101
+ def _flatten_nested_payroll_data(self, df: pd.DataFrame, pattern: str) -> pd.DataFrame:
102
+ """
103
+ Flatten nested JSON structures in payroll columns.
104
+
105
+ Args:
106
+ df: DataFrame with potentially nested payroll data
107
+ pattern: Pattern to identify payroll columns (e.g., 'payroll.variable.')
108
+
109
+ Returns:
110
+ DataFrame with flattened payroll columns
111
+ """
112
+ # Identify payroll columns
113
+ payroll_columns = [col for col in df.columns if pattern in col.lower()]
114
+
115
+ if not payroll_columns:
116
+ return df
117
+
118
+ # Create a copy to avoid modifying the original
119
+ df_result = df.copy()
120
+
121
+ # Process each payroll column
122
+ for col in payroll_columns:
123
+ # Check if the column contains nested data
124
+ if df_result[col].notna().any():
125
+ # Get the first non-null value to check structure
126
+ sample_value = df_result[col].dropna().iloc[0] if not df_result[col].dropna().empty else None
127
+
128
+ if isinstance(sample_value, dict):
129
+ # Flatten nested structure
130
+ nested_df = pd.json_normalize(df_result[col].tolist())
131
+
132
+ # Rename columns to include the original column name as prefix
133
+ nested_df.columns = [f"{col}.{subcol}" for subcol in nested_df.columns]
134
+
135
+ # Drop the original column and add flattened columns
136
+ df_result = df_result.drop(columns=[col])
137
+ df_result = pd.concat([df_result, nested_df], axis=1)
138
+
139
+ return df_result
140
+
141
+ def _extract_payroll_columns(self, df: pd.DataFrame, payroll_type: str) -> pd.DataFrame:
142
+ """
143
+ Extract payroll columns from DataFrame and rename them based on mapping.
144
+
145
+ Args:
146
+ df: DataFrame containing all data including payroll columns
147
+ payroll_type: Type of payroll information (e.g., 'entitlement', 'variable')
148
+
149
+ Returns:
150
+ DataFrame with only payroll columns (renamed if mapping available)
151
+ """
152
+ if payroll_type not in self.payroll_types:
153
+ return pd.DataFrame(index=df.index)
154
+
155
+ # Get the pattern for this payroll type
156
+ pattern = self.payroll_types[payroll_type]['pattern']
157
+
158
+ # Identify all payroll columns for this type
159
+ payroll_columns = [col for col in df.columns if pattern in col.lower()]
160
+
161
+ if not payroll_columns:
162
+ # No payroll columns found, return empty DataFrame
163
+ return pd.DataFrame(index=df.index)
164
+
165
+ # Extract payroll columns
166
+ df_payroll = df[payroll_columns].copy()
167
+
168
+ # Get mapping for this payroll type
169
+ payroll_mapping = self._get_payroll_mapping(payroll_type)
170
+
171
+ # Rename payroll columns if mapping is available
172
+ rename_dict = {}
173
+ if payroll_mapping:
174
+ for col in df_payroll.columns:
175
+ # Extract the ID from the column name (any digits after the pattern)
176
+ # Use case-insensitive match but extract from original column name
177
+ pattern_regex = pattern.replace('.', r'\.') # Escape dots for regex
178
+ match = re.search(rf'({pattern_regex})(\d+)', col, re.IGNORECASE)
179
+ if match:
180
+ payroll_id = match.group(2)
181
+ if payroll_id in payroll_mapping:
182
+ # Replace only the first occurrence of the ID with the name
183
+ # This preserves any suffixes like .value, .currency, etc.
184
+ new_col_name = col[:match.start(2)] + payroll_mapping[payroll_id] + col[match.end(2):]
185
+ rename_dict[col] = new_col_name
186
+
187
+ # Apply the renaming
188
+ if rename_dict:
189
+ df_payroll = df_payroll.rename(columns=rename_dict)
190
+
191
+ return df_payroll
192
+
193
+ def get(self, additional_fields: list[str] = None, field_selection: list[str] = None, add_payroll_information: list[str] = None) -> tuple[pd.DataFrame, pd.DataFrame]:
194
+ """
195
+ Get people from Bob
196
+
197
+ Args:
198
+ additional_fields (list[str]): Additional fields to get (not defined in the schema)
199
+ field_selection (list[str]): Fields to get (defined in the schema), if not provided, all fields are returned
200
+ add_payroll_information (list[str]): List of payroll information types to include (valid options: 'entitlement', 'variable')
201
+
202
+ Returns:
203
+ valid_people (pd.DataFrame): Validated people data (with payroll information appended if requested)
204
+ invalid_people (pd.DataFrame): Invalid records
205
+ """
206
+ #resp = self.bob.session.get(url=f"{self.bob.base_url}profiles", timeout=self.bob.timeout)
207
+ if additional_fields is None:
208
+ additional_fields = []
209
+ body_fields = list(set(self.field_name_in_body + additional_fields))
210
+ response_fields = list(set(self.field_name_in_response + additional_fields))
211
+
212
+ if field_selection:
213
+ body_fields = [field for field in body_fields if field in field_selection]
214
+ response_fields = [self.endpoint_to_response.get(field) for field in field_selection if field in self.endpoint_to_response]
215
+
216
+ # Add payroll fields to request if needed (before making the API call)
217
+ if add_payroll_information:
218
+ # Validate and filter payroll types
219
+ valid_payroll_types = [pt for pt in add_payroll_information if pt in self.payroll_types]
220
+
221
+ for payroll_type in valid_payroll_types:
222
+ body_fields, response_fields = self._add_payroll_fields_to_request(body_fields, response_fields, payroll_type)
223
+
224
+ # Bob sucks with default fields so you need to do a search call to retrieve additional fields.
225
+ resp_additional_fields = self.bob.session.post(url=f"{self.bob.base_url}people/search",
226
+ json={
227
+ "fields": body_fields,
228
+ "filters": []
229
+ },
230
+ timeout=self.bob.timeout)
231
+ df = pd.json_normalize(resp_additional_fields.json()['employees'])
232
+
233
+ # Validate payroll types if requested
234
+ valid_payroll_types = []
235
+ if add_payroll_information:
236
+ valid_payroll_types = [pt for pt in add_payroll_information if pt in self.payroll_types]
237
+
238
+ # Flatten nested data for each payroll type
239
+ for payroll_type in valid_payroll_types:
240
+ pattern = self.payroll_types[payroll_type]['pattern']
241
+ df = self._flatten_nested_payroll_data(df, pattern)
242
+
243
+ # Now filter columns - include original response_fields plus any flattened payroll columns
244
+ columns_to_keep = []
245
+ for col in df.columns:
246
+ # Keep if it's in response_fields
247
+ if col in response_fields:
248
+ columns_to_keep.append(col)
249
+ # Or if it's a payroll column (original or flattened)
250
+ elif valid_payroll_types:
251
+ for payroll_type in valid_payroll_types:
252
+ pattern = self.payroll_types[payroll_type]['pattern']
253
+ if pattern in col.lower():
254
+ columns_to_keep.append(col)
255
+ break
256
+
257
+ df = df[columns_to_keep]
258
+
259
+ # Map DataFrame columns (response fields) back to alias (endpoint) fields for validation
260
+ response_to_endpoint = {v: k for k, v in self.endpoint_to_response.items() if v}
261
+ df = df.rename(columns={col: response_to_endpoint[col] for col in df.columns if col in response_to_endpoint})
262
+
263
+ # Extract payroll information if requested
264
+ payroll_dataframes = {}
265
+ if valid_payroll_types:
266
+ for payroll_type in valid_payroll_types:
267
+ # Extract payroll columns into separate DataFrame
268
+ df_payroll = self._extract_payroll_columns(df, payroll_type)
269
+ if not df_payroll.empty:
270
+ payroll_dataframes[payroll_type] = df_payroll
271
+
272
+ # Remove payroll columns from main DataFrame for validation
273
+ pattern = self.payroll_types[payroll_type]['pattern']
274
+ payroll_columns = [col for col in df.columns if pattern in col.lower()]
275
+ if payroll_columns:
276
+ df = df.drop(columns=payroll_columns)
277
+
278
+ # Validate the data (without payroll information)
279
+ valid_people, invalid_people = Functions.validate_data(df=df, schema=PeopleSchema, debug=True)
280
+
281
+ # Append all payroll information to valid_people if they exist
282
+ if payroll_dataframes and not valid_people.empty:
283
+ for payroll_type, df_payroll in payroll_dataframes.items():
284
+ # Only include payroll data for valid rows
285
+ df_payroll_valid = df_payroll.loc[valid_people.index]
286
+ # Append payroll columns to valid_people
287
+ valid_people = pd.concat([valid_people, df_payroll_valid], axis=1)
288
+
289
+
290
+ # A lot of fields from Bob are returned with only ID's. Those fields should be mapped to names. Therefore, we need to get the mapping from the named-lists endpoint.
291
+ resp_named_lists = self.bob.session.get(url=f"{self.bob.base_url}company/named-lists", timeout=self.bob.timeout, headers=self.bob.headers)
292
+ named_lists = resp_named_lists.json()
293
+
294
+ # Transform named_lists to create id-to-value mappings for each field
295
+ named_lists = {key.split('.')[-1]: {item['id']: item['value'] for item in value['values']} for key, value in named_lists.items()}
296
+
297
+ for field in valid_people.columns:
298
+ # Fields in the response and in the named-list does have different building blocks (e.g. people.payroll.entitlement. or people.entitlement.). But they both end with the same last block
299
+ field_df = field.split('.')[-1].split('work_')[-1]
300
+ if field_df in named_lists.keys() and field_df not in ['site']:
301
+ valid_people[field] = valid_people[field].map(named_lists[field_df])
302
+
303
+ return valid_people, invalid_people
304
+
305
+
306
+ def _init_fields(self) -> tuple[list[str], list[str], dict[str, str]]:
307
+ resp_fields = self.bob.session.get(
308
+ url=f"{self.bob.base_url}company/people/fields",
309
+ timeout=self.bob.timeout,
310
+ headers=self.bob.headers
311
+ )
312
+ fields = resp_fields.json()
313
+ field_name_in_body = [field.get('id') for field in fields]
314
+ # For all field names in field_name_in_body containing 'root.', add an alternative without 'root.' in addition to those fields
315
+ field_name_in_body = field_name_in_body
316
+ field_name_in_response = [field['jsonPath'] for field in fields]
317
+ endpoint_to_response = {field['id']: field['jsonPath'] for field in fields}
318
+
319
+ return field_name_in_body, field_name_in_response, endpoint_to_response
320
+
321
+
322
+
323
+ def _get_employee_id_to_person_id_mapping(self) -> tuple[pd.DataFrame, pd.DataFrame]:
324
+ employee_id_in_company = "work.employeeIdInCompany"
325
+ person_id = "root.id"
326
+
327
+ body_fields = [employee_id_in_company, person_id]
328
+ response_fields = [self.endpoint_to_response.get(field) for field in body_fields if field in self.endpoint_to_response]
329
+
330
+ resp_additional_fields = self.bob.session.post(url=f"{self.bob.base_url}people/search",
331
+ json={
332
+ "fields": body_fields,
333
+ "filters": []
334
+ },
335
+ timeout=self.bob.timeout)
336
+ df = pd.json_normalize(resp_additional_fields.json()['employees'])
337
+ df = df[[col for col in response_fields if col in df.columns]]
338
+ # Get the valid column names from PeopleSchema
339
+ valid_people, invalid_people = Functions.validate_data(df=df, schema=PeopleSchema, debug=True)
340
+ return valid_people, invalid_people
@@ -0,0 +1,27 @@
1
+ import pandera as pa
2
+ from pandera.typing import Series
3
+ import pandas as pd
4
+ from brynq_sdk_functions import BrynQPanderaDataFrameModel
5
+
6
+ class CustomTableSchema(BrynQPanderaDataFrameModel):
7
+ id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Custom Table ID", alias="id")
8
+ employee_id: Series[str] = pa.Field(coerce=True, description="Employee ID", alias="employee_id")
9
+
10
+ class Config:
11
+ coerce = True
12
+ class CustomTableMetadataSchema(BrynQPanderaDataFrameModel):
13
+ # Table information
14
+ table_id: Series[str] = pa.Field(coerce=True, description="Table ID", alias="table_id")
15
+ table_name: Series[str] = pa.Field(coerce=True, description="Table Name", alias="table_name")
16
+ table_category: Series[str] = pa.Field(coerce=True, description="Table Category", alias="table_category")
17
+ table_description: Series[str] = pa.Field(coerce=True, nullable=True, description="Table Description", alias="table_description")
18
+
19
+ # Column information
20
+ column_id: Series[str] = pa.Field(coerce=True, description="Column ID", alias="column_id")
21
+ column_name: Series[str] = pa.Field(coerce=True, description="Column Name", alias="column_name")
22
+ column_description: Series[str] = pa.Field(coerce=True, nullable=True, description="Column Description", alias="column_description")
23
+ column_mandatory: Series[bool] = pa.Field(coerce=True, description="Is Column Mandatory", alias="column_mandatory")
24
+ column_type: Series[str] = pa.Field(coerce=True, description="Column Type", alias="column_type")
25
+
26
+ class Config:
27
+ coerce = True
@@ -26,7 +26,7 @@ class VariablePaymentSchema(BrynQPanderaDataFrameModel):
26
26
  change_reason: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Reason", alias="change.reason")
27
27
  change_changed_by: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Changed By", alias="change.changedBy")
28
28
  change_changed_by_id: Series[pd.Int64Dtype] = pa.Field(nullable=True, coerce=True, description="Change Changed By ID", alias="change.changedById")
29
- employee_id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Employee ID", alias="employee_id") #set manually
29
+ employee_id: Series[String] = pa.Field(coerce=True, description="Employee ID", alias="employee_id") #set manually
30
30
  class Config:
31
31
  coerce = True
32
32
 
@@ -0,0 +1,24 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ import pandas as pd
5
+ import pandera as pa
6
+ from pandera import Bool
7
+ from pandera.typing import Series, String, Float, DateTime
8
+ import pandera.extensions as extensions
9
+ from brynq_sdk_functions import BrynQPanderaDataFrameModel
10
+
11
+
12
+
13
+ @extensions.register_check_method()
14
+ def check_list(x):
15
+ return isinstance(x, list)
16
+
17
+
18
+ class PayrollHistorySchema(BrynQPanderaDataFrameModel):
19
+ id: Optional[Series[String]] = pa.Field(coerce=True, description="Person ID", alias="id")
20
+ display_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Display Name", alias="displayName")
21
+ company_id: Optional[Series[String]] = pa.Field(coerce=True, description="Company ID", alias="companyId")
22
+
23
+ class Config:
24
+ coerce = True
@@ -16,13 +16,13 @@ def check_list(x):
16
16
 
17
17
 
18
18
  class PeopleSchema(BrynQPanderaDataFrameModel):
19
- id: Optional[Series[String]] = pa.Field(coerce=True, description="Person ID", alias="id")
19
+ id: Optional[Series[String]] = pa.Field(coerce=True, description="Person ID", alias="root.id")
20
20
  display_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Display Name", alias="displayName")
21
21
  company_id: Optional[Series[String]] = pa.Field(coerce=True, description="Company ID", alias="companyId")
22
- email: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Email", alias="email")
22
+ email: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Email", alias="root.email")
23
23
  home_mobile_phone: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Personal Mobile Phone", alias="home.mobilePhone")
24
- surname: Optional[Series[String]] = pa.Field(coerce=True, description="Surname", alias="surname")
25
- first_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="First Name", alias="firstName")
24
+ surname: Optional[Series[String]] = pa.Field(coerce=True, description="Surname", alias="root.surname")
25
+ first_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="First Name", alias="root.firstName")
26
26
  full_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Full Name", alias="fullName")
27
27
  personal_birth_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="Personal Birth Date", alias="personal.birthDate")
28
28
  personal_pronouns: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Personal Pronouns", alias="personal.pronouns")
@@ -1,5 +1,6 @@
1
1
  import pandera as pa
2
2
  from pandera.typing import Series, String, Float
3
+ from typing import Optional
3
4
  import pandas as pd
4
5
  from brynq_sdk_functions import BrynQPanderaDataFrameModel
5
6
 
@@ -15,15 +16,15 @@ class TimeOffSchema(BrynQPanderaDataFrameModel):
15
16
  start_portion: Series[String] = pa.Field(coerce=True, description="Start Portion", alias="startPortion")
16
17
  end_date: Series[String] = pa.Field(coerce=True, description="End Date", alias="endDate")
17
18
  end_portion: Series[String] = pa.Field(coerce=True, description="End Portion", alias="endPortion")
18
- day_portion: Series[String] = pa.Field(coerce=True, description="Day Portion", alias="dayPortion")
19
- date: Series[String] = pa.Field(coerce=True, description="Date", alias="date")
20
- hours_on_date: Series[Float] = pa.Field(coerce=True, description="Hours on Date", alias="hoursOnDate")
21
- daily_hours: Series[Float] = pa.Field(coerce=True, description="Daily Hours", alias="dailyHours")
22
- duration_unit: Series[String] = pa.Field(coerce=True, description="Duration Unit", alias="durationUnit")
23
- total_duration: Series[Float] = pa.Field(coerce=True, description="Total Duration", alias="totalDuration")
24
- total_cost: Series[Float] = pa.Field(coerce=True, description="Total Cost", alias="totalCost")
25
- change_reason: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Reason", alias="changeReason")
26
- visibility: Series[String] = pa.Field(coerce=True, description="Visibility", alias="visibility")
19
+ day_portion: Optional[Series[String]] = pa.Field(coerce=True, description="Day Portion", alias="dayPortion")
20
+ date: Optional[Series[String]] = pa.Field(coerce=True, description="Date", alias="date")
21
+ hours_on_date: Optional[Series[Float]] = pa.Field(coerce=True, description="Hours on Date", alias="hoursOnDate")
22
+ daily_hours: Optional[Series[Float]] = pa.Field(coerce=True, description="Daily Hours", alias="dailyHours")
23
+ duration_unit: Optional[Series[String]] = pa.Field(coerce=True, description="Duration Unit", alias="durationUnit")
24
+ total_duration: Optional[Series[Float]] = pa.Field(coerce=True, description="Total Duration", alias="totalDuration")
25
+ total_cost: Optional[Series[Float]] = pa.Field(coerce=True, description="Total Cost", alias="totalCost")
26
+ change_reason: Optional[Series[String]] = pa.Field(nullable=True, coerce=True, description="Change Reason", alias="changeReason")
27
+ visibility: Optional[Series[String]] = pa.Field(coerce=True, description="Visibility", alias="visibility")
27
28
 
28
29
  class Config:
29
30
  coerce = True
@@ -11,7 +11,7 @@ class TimeOff:
11
11
  self.schema = TimeOffSchema
12
12
  self.balance_schema = TimeOffBalanceSchema
13
13
 
14
- def get(self, since: datetime = None) -> tuple[pd.DataFrame, pd.DataFrame]:
14
+ def get(self, since: datetime = None, include_pending: bool = False) -> tuple[pd.DataFrame, pd.DataFrame]:
15
15
  """
16
16
  Get time off requests
17
17
 
@@ -37,16 +37,12 @@ class TimeOff:
37
37
 
38
38
  since = since.replace(tzinfo=timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z')
39
39
  resp = self.bob.session.get(url=f"{self.bob.base_url}timeoff/requests/changes",
40
- params={'since': since},
40
+ params={'since': since, 'includePending': 'true' if include_pending else 'false'},
41
41
  timeout=self.bob.timeout)
42
42
  resp.raise_for_status()
43
43
  data = resp.json()['changes']
44
44
  # data = self.bob.get_paginated_result(request)
45
- df = pd.json_normalize(
46
- data,
47
- record_path='changes',
48
- meta=['employeeId']
49
- )
45
+ df = pd.DataFrame(data)
50
46
  valid_timeoff, invalid_timeoff = Functions.validate_data(df=df, schema=self.schema, debug=True)
51
47
 
52
48
  return valid_timeoff, invalid_timeoff
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq-sdk-bob
3
- Version: 2.4.0
3
+ Version: 2.4.3
4
4
  Summary: Bob wrapper from BrynQ
5
5
  Home-page: UNKNOWN
6
6
  Author: BrynQ
@@ -7,6 +7,7 @@ brynq_sdk_bob/documents.py
7
7
  brynq_sdk_bob/employment.py
8
8
  brynq_sdk_bob/named_lists.py
9
9
  brynq_sdk_bob/payments.py
10
+ brynq_sdk_bob/payroll_history.py
10
11
  brynq_sdk_bob/people.py
11
12
  brynq_sdk_bob/salaries.py
12
13
  brynq_sdk_bob/timeoff.py
@@ -23,6 +24,7 @@ brynq_sdk_bob/schemas/custom_tables.py
23
24
  brynq_sdk_bob/schemas/employment.py
24
25
  brynq_sdk_bob/schemas/named_lists.py
25
26
  brynq_sdk_bob/schemas/payments.py
27
+ brynq_sdk_bob/schemas/payroll_history.py
26
28
  brynq_sdk_bob/schemas/people.py
27
29
  brynq_sdk_bob/schemas/salary.py
28
30
  brynq_sdk_bob/schemas/timeoff.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_namespace_packages
2
2
 
3
3
  setup(
4
4
  name='brynq_sdk_bob',
5
- version='2.4.0',
5
+ version='2.4.3',
6
6
  description='Bob wrapper from BrynQ',
7
7
  long_description='Bob wrapper from BrynQ',
8
8
  author='BrynQ',
@@ -1,36 +0,0 @@
1
- from datetime import datetime
2
- import pandas as pd
3
- from brynq_sdk_functions import Functions
4
- from .schemas.custom_tables import CustomTableSchema
5
-
6
-
7
- class CustomTables:
8
- def __init__(self, bob):
9
- self.bob = bob
10
- self.schema = CustomTableSchema
11
-
12
- def get(self, employee_id: str, custom_table_id: str) -> (pd.DataFrame, pd.DataFrame):
13
- """
14
- Get custom table data for an employee
15
-
16
- Args:
17
- employee_id: The employee ID
18
- custom_table_id: The custom table ID
19
-
20
- Returns:
21
- A tuple of (valid_data, invalid_data) as pandas DataFrames
22
- """
23
- resp = self.bob.session.get(url=f"{self.bob.base_url}people/custom-tables/{employee_id}/{custom_table_id}")
24
- resp.raise_for_status()
25
- data = resp.json()
26
-
27
- # Normalize the nested JSON response
28
- df = pd.json_normalize(
29
- data,
30
- record_path=['values']
31
- )
32
-
33
- df['employee_id'] = employee_id
34
- valid_data, invalid_data = Functions.validate_data(df=df, schema=self.schema, debug=True)
35
-
36
- return valid_data, invalid_data
@@ -1,11 +0,0 @@
1
- import pandera as pa
2
- from pandera.typing import Series
3
- import pandas as pd
4
- from brynq_sdk_functions import BrynQPanderaDataFrameModel
5
-
6
- class CustomTableSchema(BrynQPanderaDataFrameModel):
7
- id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Custom Table ID", alias="id")
8
- employee_id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
9
-
10
- class Config:
11
- coerce = True
File without changes