brynq-sdk-bob 2.6.2.dev10__py3-none-any.whl → 2.9.4__py3-none-any.whl

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_bob/__init__.py CHANGED
@@ -1,11 +1,9 @@
1
1
  import base64
2
- import re
3
- from typing import Union, List, Optional, Literal
4
- import pandas as pd
2
+ from typing import List, Optional, Literal
5
3
  import requests
6
4
  import os
5
+ from .reports import Reports
7
6
  from brynq_sdk_brynq import BrynQ
8
- from brynq_sdk_functions import Functions
9
7
  from .bank import Bank
10
8
  from .company import Company
11
9
  from .documents import CustomDocuments
@@ -17,13 +15,15 @@ from .salaries import Salaries
17
15
  from .timeoff import TimeOff
18
16
  from .work import Work
19
17
  from .custom_tables import CustomTables
18
+ from .payroll_history import History
20
19
 
21
20
  class Bob(BrynQ):
22
21
  def __init__(self, system_type: Optional[Literal['source', 'target']] = None, test_environment: bool = True, debug: bool = False, target_system: str = None):
23
22
  super().__init__()
24
23
  self.timeout = 3600
24
+ self.test_environment = test_environment
25
25
  self.headers = self._get_request_headers(system_type)
26
- if test_environment:
26
+ if self.test_environment:
27
27
  self.base_url = "https://api.sandbox.hibob.com/v1/"
28
28
  else:
29
29
  self.base_url = "https://api.hibob.com/v1/"
@@ -40,15 +40,22 @@ class Bob(BrynQ):
40
40
  self.companies = Company(self)
41
41
  self.named_lists = NamedLists(self)
42
42
  self.custom_tables = CustomTables(self)
43
+ self.payroll_history = History(self)
44
+ self.reports = Reports(self)
43
45
  self.data_interface_id = os.getenv("DATA_INTERFACE_ID")
44
46
  self.debug = debug
45
47
 
46
48
  def _get_request_headers(self, system_type):
47
49
  credentials = self.interfaces.credentials.get(system='bob', system_type=system_type)
48
- if type(credentials) is list:
49
- credentials = credentials[0]
50
-
51
- print(credentials)
50
+ # multiple creds possible, not fetched by environment test status, get first occurence
51
+ if isinstance(credentials, list):
52
+ credentials = next(
53
+ (
54
+ element for element in credentials
55
+ if element.get('data', {}).get('Test Environment') == self.test_environment
56
+ ),
57
+ credentials[0]
58
+ )
52
59
  auth_token = base64.b64encode(f"{credentials.get('data').get('User ID')}:{credentials.get('data').get('API Token')}".encode()).decode('utf-8')
53
60
  headers = {
54
61
  "accept": "application/json",
brynq_sdk_bob/bank.py CHANGED
@@ -2,35 +2,23 @@ import pandas as pd
2
2
  from brynq_sdk_functions import Functions
3
3
  from .schemas.bank import BankSchema
4
4
 
5
- import time
6
- from tqdm import tqdm
7
- from tenacity import retry, stop_after_delay, wait_exponential
8
5
 
9
6
  class Bank:
10
7
  def __init__(self, bob):
11
8
  self.bob = bob
12
9
  self.schema = BankSchema
13
10
 
14
- @retry(stop=stop_after_delay(120), wait=wait_exponential(multiplier=1, min=1, max=10))
15
- def _get_bank_accounts(self, person_id: str):
16
- """Fetch bank accounts for a person with retry logic (max 2 minutes)."""
17
- resp = self.bob.session.get(url=f"{self.bob.base_url}people/{person_id}/bank-accounts", timeout=self.bob.timeout)
18
- resp.raise_for_status()
19
- return resp
20
-
21
11
  def get(self, person_ids: pd.Series, field_selection: list[str] = []) -> (pd.DataFrame, pd.DataFrame):
22
12
  data = []
23
- for person_id in tqdm(person_ids, desc="Fetching bank accounts"):
24
- resp = self._get_bank_accounts(person_id)
13
+ for person_id in person_ids:
14
+ resp = self.bob.session.get(url=f"{self.bob.base_url}people/{person_id}/bank-accounts", timeout=self.bob.timeout)
15
+ resp.raise_for_status()
25
16
  temp_data = resp.json()['values']
26
17
  # when an employee has one or more bank accounts, the response is a list of dictionaries.
27
18
  for account in temp_data:
28
19
  account['employee_id'] = person_id
29
20
  data += temp_data
30
21
 
31
- # rate limit is 50 per minute
32
- time.sleep(1.3)
33
-
34
22
  df = pd.DataFrame(data)
35
23
 
36
24
  valid_banks, invalid_banks = Functions.validate_data(df=df, schema=BankSchema, debug=True)
@@ -1,9 +1,7 @@
1
1
  import pandas as pd
2
2
  import requests
3
-
4
- from brynq_sdk_functions import Functions
5
-
6
3
  from .schemas.employment import EmploymentSchema
4
+ from brynq_sdk_functions import Functions
7
5
 
8
6
 
9
7
  class Employment:
brynq_sdk_bob/payments.py CHANGED
@@ -2,7 +2,6 @@ import time
2
2
  from typing import List, Optional
3
3
 
4
4
  import pandas as pd
5
- from tqdm import tqdm
6
5
 
7
6
  from brynq_sdk_functions import Functions
8
7
 
@@ -14,61 +13,31 @@ class Payments:
14
13
  self.bob = bob
15
14
  self.schema = VariablePaymentSchema
16
15
 
17
- def _apply_named_list_mappings(self, df: pd.DataFrame) -> pd.DataFrame:
18
- """Apply named list ID-to-value mappings to dataframe columns."""
19
- if df.empty:
20
- return df
21
-
22
- # Fetch named lists from Bob API
23
- resp_named_lists = self.bob.session.get(
24
- url=f"{self.bob.base_url}company/named-lists",
25
- timeout=self.bob.timeout,
26
- headers=self.bob.headers
27
- )
28
- named_lists = resp_named_lists.json()
29
-
30
- # Transform named_lists to create id-to-value mappings for each field
31
- named_lists = {
32
- key.split('.')[-1]: {item['id']: item['value'] for item in value['values']}
33
- for key, value in named_lists.items()
34
- }
35
-
36
- # rename payrollVariableType to variableType in named lists
37
- named_lists['variableType'] = named_lists['payrollVariableType']
38
-
39
- for field in df.columns:
40
- # Fields in the response and in the named-list have different building blocks
41
- # but they both end with the same last block
42
- field_df = field.split('.')[-1].split('work_')[-1]
43
- if field_df in named_lists.keys() and field_df not in ['site']:
44
- mapping = named_lists[field_df]
45
- df[field] = df[field].apply(
46
- lambda v: [mapping.get(x, x) for x in v] if isinstance(v, list) else mapping.get(v, v)
47
- )
16
+ def get(self, person_ids: List[str]) -> (pd.DataFrame, pd.DataFrame):
17
+ records = []
48
18
 
49
- return df
19
+ for person_id in person_ids:
20
+ # Throttle requests to respect API rate limits
21
+ time.sleep(0.2)
50
22
 
51
- def get(self, person_ids: List[str]) -> (pd.DataFrame, pd.DataFrame):
52
- df = pd.DataFrame()
53
- for person_id in tqdm(person_ids, desc="Fetching variable payments"):
54
- resp = self.bob.session.get(url=f"{self.bob.base_url}people/{person_id}/variable", timeout=self.bob.timeout)
23
+ resp = self.bob.session.get(
24
+ url=f"{self.bob.base_url}people/{person_id}/variable",
25
+ timeout=self.bob.timeout
26
+ )
55
27
  resp.raise_for_status()
56
- data = resp.json()
57
- df = pd.concat([df, pd.json_normalize(
58
- data,
59
- record_path='values'
60
- )])
61
- df['employee_id'] = person_id
62
28
 
63
- # Rate limit is 50 per minute
64
- time.sleep(1.3)
29
+ data = resp.json()
65
30
 
66
- df = df.reset_index(drop=True)
31
+ # Normalize nested json and assign ID before appending to list
32
+ df_temp = pd.json_normalize(data, record_path='values')
33
+ df_temp['employee_id'] = person_id
34
+ records.append(df_temp)
67
35
 
68
- # Apply named list mappings
69
- df = self._apply_named_list_mappings(df)
36
+ # Concatenate once to improve performance
37
+ df = pd.concat(records, ignore_index=True) if records else pd.DataFrame()
70
38
 
71
39
  valid_payments, invalid_payments = Functions.validate_data(df=df, schema=self.schema, debug=True)
40
+
72
41
  return valid_payments, invalid_payments
73
42
 
74
43
  def get_actual_payments(
@@ -154,9 +123,6 @@ class Payments:
154
123
 
155
124
  df = pd.json_normalize(all_results)
156
125
 
157
- # Apply named list mappings
158
- df = self._apply_named_list_mappings(df)
159
-
160
126
  valid_payments, invalid_payments = Functions.validate_data(
161
127
  df=df,
162
128
  schema=ActualPaymentsSchema,
brynq_sdk_bob/people.py CHANGED
@@ -1,15 +1,14 @@
1
- from typing import List, Optional
2
-
3
1
  import pandas as pd
4
-
5
- from brynq_sdk_functions import BrynQPanderaDataFrameModel, Functions
6
-
2
+ import re
3
+ from typing import Optional
4
+ from brynq_sdk_functions import Functions
5
+ from brynq_sdk_functions import BrynQPanderaDataFrameModel
7
6
  from .bank import Bank
8
- from .custom_tables import CustomTables
9
7
  from .employment import Employment
10
8
  from .salaries import Salaries
11
9
  from .schemas.people import PeopleSchema
12
10
  from .work import Work
11
+ from .custom_tables import CustomTables
13
12
 
14
13
 
15
14
  class People:
@@ -23,6 +22,7 @@ class People:
23
22
  self.schema = PeopleSchema
24
23
 
25
24
 
25
+
26
26
  # Build API fields using column metadata if present (api_field), otherwise use the column (alias) name
27
27
  def __build_api_fields(self, schema_model: BrynQPanderaDataFrameModel) -> list[str]:
28
28
  schema = schema_model.to_schema()
@@ -31,78 +31,96 @@ class People:
31
31
  for col_name, col in schema.columns.items()
32
32
  ]
33
33
 
34
- def get(self, schema_custom_fields: Optional[BrynQPanderaDataFrameModel] = None, employee_ids: Optional[List[str]] = None, show_inactive: bool = False) -> pd.DataFrame:
34
+ def get(self, schema_custom_fields: Optional[BrynQPanderaDataFrameModel] = None) -> pd.DataFrame:
35
+
35
36
  core_fields = self.__build_api_fields(PeopleSchema)
36
37
  custom_fields = self.__build_api_fields(schema_custom_fields) if schema_custom_fields is not None else []
37
38
  fields = core_fields + custom_fields
38
39
 
39
- # Build filters based on employee_ids if provided
40
- filters = []
41
- if employee_ids is not None:
42
- filters = [
43
- {
44
- "fieldPath": "root.id",
45
- "operator": "equals",
46
- "values": employee_ids
47
- }
48
- ]
49
-
50
40
  resp = self.bob.session.post(url=f"{self.bob.base_url}people/search",
51
41
  json={
52
42
  "fields": fields,
53
- "filters": filters,
54
- "showInactive": show_inactive,
43
+ "filters": []
55
44
  #"humanReadable": "REPLACE"
56
45
  },
57
46
  timeout=self.bob.timeout)
58
47
  resp.raise_for_status()
59
48
  df = pd.json_normalize(resp.json()['employees'])
60
- if df.empty and employee_ids is not None and resp.status_code == 200:
61
- raise Exception(f"No employees found in HiBob for employee_ids: {employee_ids}")
62
49
 
63
- df = df.loc[:, ~df.columns.str.contains('value')]
64
50
 
65
51
  # Normalize separators in incoming data: convert '/' to '.' to match schema aliases
66
52
  df.columns = df.columns.str.replace('/', '.', regex=False)
67
53
 
54
+
55
+ # Clean up custom .value columns; keeps only one field per custom key. the .value column actually contains the value., other column is nested key.
56
+ custom_cols = [col for col in df.columns if "custom" in str(col) and str(col).endswith('.value')]
57
+ for col in custom_cols:
58
+ new_col = col.removesuffix('.value')
59
+ # prefer non-empty clean column, otherwise upgrade .value data
60
+ if new_col not in df.columns:
61
+ df = df.rename(columns={col: new_col})
62
+ elif df[new_col].isna().all() and not df[col].isna().all():
63
+ df[new_col] = df[col]
64
+ df = df.drop(columns=[col])
65
+ else:
66
+ # Drop identical/redundant .value col
67
+ if df[new_col].equals(df[col]):
68
+ df = df.drop(columns=[col])
69
+ pass
70
+
71
+
68
72
  # 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.
69
73
  resp_named_lists = self.bob.session.get(url=f"{self.bob.base_url}company/named-lists", timeout=self.bob.timeout, headers=self.bob.headers)
70
74
  named_lists = resp_named_lists.json()
71
- # save json to file
72
- # import json
73
- # with open('named_lists.json', 'w') as f:
74
- # json.dump(named_lists, f, indent=4)
75
75
 
76
76
  # Transform named_lists to create id-to-value mappings for each field
77
77
  named_lists = {key.split('.')[-1]: {item['id']: item['value'] for item in value['values']} for key, value in named_lists.items()}
78
78
 
79
- deviating_named_list_cols_mapping = {
80
- 'payroll.employment.actualWorkingPattern.workingPatternId': 'workingPattern_entity_list',
81
- 'payroll.employment.type': 'payrollEmploymentType',
82
- 'home.familyStatus': 'familystatus',
83
- 'personal.nationality': 'nationalities',
84
- 'internal.terminationReason': 'terminationreason',
85
- }
86
-
87
79
  for field in df.columns:
88
80
  # 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
89
81
  field_df = field.split('.')[-1].split('work_')[-1]
90
-
91
- # Check if this field has a deviating mapping
92
- named_list_key = deviating_named_list_cols_mapping.get(field, field_df)
93
-
94
- if named_list_key in named_lists.keys() and named_list_key not in ['site']:
95
- mapping = named_lists[named_list_key]
82
+ if field_df in named_lists.keys() and field_df not in ['site']:
83
+ mapping = named_lists[field_df]
96
84
  df[field] = df[field].apply(
97
85
  lambda v: [mapping.get(x, x) for x in v] if isinstance(v, list) else mapping.get(v, v)
98
86
  )
99
87
 
88
+
100
89
  if schema_custom_fields is not None:
90
+
101
91
  valid_people, invalid_people_custom = Functions.validate_data(df=df, schema=schema_custom_fields, debug=True)
92
+
93
+
102
94
  else:
103
95
  valid_people = df
104
96
  invalid_people_custom = pd.DataFrame()
105
97
 
98
+
106
99
  valid_people, invalid_people = Functions.validate_data(df=valid_people, schema=PeopleSchema, debug=True)
107
100
 
101
+ # For columns ending with .value.value, use fillna to fill the corresponding base column since that's not done automatically
102
+ def _normalize_key(key: str) -> str:
103
+ # Build mapping from custom schema alias (API path) to the real column name used after validation
104
+ key = key.lstrip('.')
105
+ key = key.replace('/', '.')
106
+ key = key.replace('.', '_')
107
+ key = re.sub(r'(?<!^)([A-Z])', r'_\1', key).lower()
108
+ return key
109
+
110
+ alias_to_real: dict = {}
111
+ if schema_custom_fields is not None:
112
+ alias_map = getattr(schema_custom_fields, "_alias_map", {}) # real_name -> alias
113
+ alias_to_real = {_normalize_key(alias): real for real, alias in alias_map.items()}
114
+
115
+ for col in valid_people.columns:
116
+ if col.endswith('value.value'):
117
+ # Compute the base alias (remove suffix), normalise, and resolve to real column name via custom schema
118
+ base_alias = col[:-12]
119
+ target_key = _normalize_key(base_alias)
120
+ target_col = alias_to_real.get(target_key, target_key)
121
+ if target_col in valid_people.columns:
122
+ valid_people[target_col] = valid_people[target_col].fillna(valid_people[col])
123
+
124
+ # Remove columns that contain '.value' or '_get'
125
+ valid_people = valid_people.loc[:, ~valid_people.columns.str.contains(r'\.value|_get')]
108
126
  return valid_people, pd.concat([invalid_people, invalid_people_custom])
@@ -0,0 +1,38 @@
1
+ from datetime import datetime
2
+ from io import BytesIO
3
+ from typing import Optional, TYPE_CHECKING
4
+
5
+ import pandas as pd
6
+ from brynq_sdk_functions import Functions
7
+ if TYPE_CHECKING:
8
+ from brynq_sdk_bob import Bob
9
+
10
+
11
+ class Reports:
12
+ def __init__(self, bob):
13
+ self.bob: Bob = bob
14
+
15
+ def get(self) -> pd.DataFrame:
16
+ resp = self.bob.session.get(url=f"{self.bob.base_url}company/reports", timeout=self.bob.timeout)
17
+ resp.raise_for_status()
18
+ data = resp.json()
19
+ df = pd.json_normalize(
20
+ data,
21
+ record_path='views'
22
+ )
23
+ # df = self.bob.rename_camel_columns_to_snake_case(df)
24
+ # valid_documents, invalid_documents = Functions.validate_data(df=df, schema=DocumentsSchema, debug=True)
25
+
26
+ return df
27
+
28
+ def download(self, report_id: int | str = None, report_format: str = "csv") -> bytes:
29
+ if report_id:
30
+ url = f"{self.bob.base_url}company/reports/{report_id}/download"
31
+ else:
32
+ raise ValueError("Either report_id or report_name must be provided")
33
+
34
+ resp = self.bob.session.get(url=url, timeout=self.bob.timeout, params={"format": report_format})
35
+ resp.raise_for_status()
36
+ data = resp.content
37
+
38
+ return data
brynq_sdk_bob/salaries.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import pandas as pd
2
2
  import requests
3
- from typing import Optional, List
4
3
  from brynq_sdk_functions import Functions
5
4
  from .schemas.salary import SalarySchema, SalaryCreateSchema
6
5
 
@@ -10,16 +9,10 @@ class Salaries:
10
9
  self.bob = bob
11
10
  self.schema = SalarySchema
12
11
 
13
- def get(self, employee_ids: Optional[List[str]] = None) -> tuple[pd.DataFrame, pd.DataFrame]:
14
- params = {"limit": 100}
15
-
16
- # Add employeeIds filter if provided
17
- if employee_ids is not None:
18
- params["employeeIds"] = ",".join(employee_ids)
19
-
12
+ def get(self) -> tuple[pd.DataFrame, pd.DataFrame]:
20
13
  request = requests.Request(method='GET',
21
14
  url=f"{self.bob.base_url}bulk/people/salaries",
22
- params=params)
15
+ params={"limit": 100})
23
16
  data = self.bob.get_paginated_result(request)
24
17
  df = pd.json_normalize(
25
18
  data,
@@ -1,5 +1,4 @@
1
1
  import pandera as pa
2
- from typing import Optional
3
2
  from pandera.typing import Series, String
4
3
  import pandas as pd
5
4
  from brynq_sdk_functions import BrynQPanderaDataFrameModel
@@ -7,19 +6,19 @@ from brynq_sdk_functions import BrynQPanderaDataFrameModel
7
6
 
8
7
  class BankSchema(BrynQPanderaDataFrameModel):
9
8
  id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Bank ID", alias="id")
10
- employee_id: Series[String] = pa.Field(coerce=True, description="Employee ID", alias="employee_id")
11
- amount: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, description="Amount", alias="amount")
12
- allocation: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Allocation", alias="allocation")
13
- branch_address: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Branch Address", alias="branchAddress")
9
+ employee_id: Series[String] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
10
+ amount: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Amount", alias="amount")
11
+ allocation: Series[String] = pa.Field(coerce=True, nullable=True, description="Allocation", alias="allocation")
12
+ branch_address: Series[String] = pa.Field(coerce=True, nullable=True, description="Branch Address", alias="branchAddress")
14
13
  bank_name: Series[String] = pa.Field(coerce=True, nullable=True, description="Bank Name", alias="bankName")
15
14
  account_number: Series[String] = pa.Field(coerce=True, nullable=True, description="Account Number", alias="accountNumber")
16
- routing_number: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Routing Number", alias="routingNumber")
15
+ routing_number: Series[String] = pa.Field(coerce=True, nullable=True, description="Routing Number", alias="routingNumber")
17
16
  bank_account_type: Series[String] = pa.Field(coerce=True, nullable=True, description="Bank Account Type", alias="bankAccountType")
18
- bic_or_swift: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="BIC or SWIFT", alias="bicOrSwift")
17
+ bic_or_swift: Series[String] = pa.Field(coerce=True, nullable=True, description="BIC or SWIFT", alias="bicOrSwift")
19
18
  changed_by: Series[String] = pa.Field(coerce=True, nullable=True, description="Changed By", alias="changedBy")
20
19
  iban: Series[String] = pa.Field(coerce=True, description="IBAN", alias="iban")
21
20
  account_nickname: Series[String] = pa.Field(coerce=True, nullable=True, description="Account Nickname", alias="accountNickname")
22
- use_for_bonus: Optional[Series[pd.BooleanDtype]] = pa.Field(coerce=True, nullable=True, description="Use for Bonus", alias="useForBonus")
21
+ use_for_bonus: Series[pd.BooleanDtype] = pa.Field(coerce=True, nullable=True, description="Use for Bonus", alias="useForBonus")
23
22
 
24
23
  class Config:
25
24
  coerce = True
@@ -1,31 +1,32 @@
1
1
  import pandas as pd
2
2
  import pandera as pa
3
+ from typing import Optional
3
4
  from pandera import Bool
4
5
  from pandera.typing import Series, String, Float, DateTime
5
6
  from brynq_sdk_functions import BrynQPanderaDataFrameModel
6
7
 
7
8
  class EmploymentSchema(BrynQPanderaDataFrameModel):
8
9
  id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Employment ID", alias="id")
9
- employee_id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
10
+ employee_id: Series[String] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
10
11
  active_effective_date: Series[DateTime] = pa.Field(coerce=True, description="Active Effective Date", alias="activeEffectiveDate")
11
- contract: Series[String] = pa.Field(coerce=True, nullable=True, description="Contract", alias="contract") # has a list of possible values
12
- creation_date: Series[DateTime] = pa.Field(coerce=True, nullable=True, description="Creation Date", alias="creationDate")
12
+ contract: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Contract", alias="contract") # has a list of possible values
13
+ creation_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="Creation Date", alias="creationDate")
13
14
  effective_date: Series[DateTime] = pa.Field(coerce=True, description="Effective Date", alias="effectiveDate")
14
- end_effective_date: Series[DateTime] = pa.Field(coerce=True, nullable=True, description="End Effective Date", alias="endEffectiveDate")
15
+ end_effective_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="End Effective Date", alias="endEffectiveDate")
15
16
  fte: Series[Float] = pa.Field(coerce=True, description="FTE", alias="fte")
16
17
  is_current: Series[Bool] = pa.Field(coerce=True, description="Is Current", alias="isCurrent")
17
- modification_date: Series[DateTime] = pa.Field(coerce=True, nullable=True, description="Modification Date", alias="modificationDate")
18
- salary_pay_type: Series[String] = pa.Field(coerce=True, nullable=True, description="Salary Pay Type", alias="salaryPayType")
19
- weekly_hours: Series[Float] = pa.Field(coerce=True, nullable=True, description="Weekly Hours", alias="weeklyHours")
18
+ modification_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="Modification Date", alias="modificationDate")
19
+ salary_pay_type: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Salary Pay Type", alias="salaryPayType")
20
+ weekly_hours: Optional[Series[Float]] = pa.Field(coerce=True, nullable=True, description="Weekly Hours", alias="weeklyHours")
20
21
  # weekly_hours_sort_factor: Series[pd.Int64Dtype] = pa.Field(coerce=True, nullable=False)
21
- actual_working_pattern_working_pattern_type: Series[pa.String] = pa.Field(nullable=True, description="Actual Working Pattern Working Pattern Type", alias="actualWorkingPattern.workingPatternType")
22
- actual_working_pattern_days_sunday: Series[Float] = pa.Field(nullable=True, description="Actual Working Pattern Days Sunday", alias="actualWorkingPattern.days.sunday")
23
- actual_working_pattern_days_tuesday: Series[Float] = pa.Field(nullable=True, description="Actual Working Pattern Days Tuesday", alias="actualWorkingPattern.days.tuesday")
24
- actual_working_pattern_days_wednesday: Series[Float] = pa.Field(nullable=True, description="Actual Working Pattern Days Wednesday", alias="actualWorkingPattern.days.wednesday")
25
- actual_working_pattern_days_monday: Series[Float] = pa.Field(nullable=True, description="Actual Working Pattern Days Monday", alias="actualWorkingPattern.days.monday")
26
- actual_working_pattern_days_friday: Series[Float] = pa.Field(nullable=True, description="Actual Working Pattern Days Friday", alias="actualWorkingPattern.days.friday")
27
- actual_working_pattern_days_thursday: Series[Float] = pa.Field(nullable=True, description="Actual Working Pattern Days Thursday", alias="actualWorkingPattern.days.thursday")
28
- actual_working_pattern_days_saturday: Series[Float] = pa.Field(nullable=True, description="Actual Working Pattern Days Saturday", alias="actualWorkingPattern.days.saturday")
22
+ actual_working_pattern_working_pattern_type: Optional[Series[pa.String]] = pa.Field(nullable=True, description="Actual Working Pattern Working Pattern Type", alias="actualWorkingPattern.workingPatternType")
23
+ actual_working_pattern_days_sunday: Optional[Series[Float]] = pa.Field(nullable=True, description="Actual Working Pattern Days Sunday", alias="actualWorkingPattern.days.sunday")
24
+ actual_working_pattern_days_tuesday: Optional[Series[Float]] = pa.Field(nullable=True, description="Actual Working Pattern Days Tuesday", alias="actualWorkingPattern.days.tuesday")
25
+ actual_working_pattern_days_wednesday: Optional[Series[Float]] = pa.Field(nullable=True, description="Actual Working Pattern Days Wednesday", alias="actualWorkingPattern.days.wednesday")
26
+ actual_working_pattern_days_monday: Optional[Series[Float]] = pa.Field(nullable=True, description="Actual Working Pattern Days Monday", alias="actualWorkingPattern.days.monday")
27
+ actual_working_pattern_days_friday: Optional[Series[Float]] = pa.Field(nullable=True, description="Actual Working Pattern Days Friday", alias="actualWorkingPattern.days.friday")
28
+ actual_working_pattern_days_thursday: Optional[Series[Float]] = pa.Field(nullable=True, description="Actual Working Pattern Days Thursday", alias="actualWorkingPattern.days.thursday")
29
+ actual_working_pattern_days_saturday: Optional[Series[Float]] = pa.Field(nullable=True, description="Actual Working Pattern Days Saturday", alias="actualWorkingPattern.days.saturday")
29
30
 
30
31
  class Config:
31
32
  coerce = True
@@ -24,7 +24,6 @@ class VariablePaymentSchema(BrynQPanderaDataFrameModel):
24
24
  amount_value: Optional[Series[Float]] = pa.Field(coerce=True, description="Amount Value", alias="amount.value")
25
25
  amount_alternative_value: Optional[Series[Float]] = pa.Field(coerce=True, description="Amount Value", alias="amount")
26
26
  amount_currency: Optional[Series[String]] = pa.Field(coerce=True, description="Amount Currency", alias="amount.currency")
27
- change_reason: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Reason", alias="change.reason")
28
27
  change_changed_by: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Changed By", alias="change.changedBy")
29
28
  change_changed_by_id: Series[pd.Int64Dtype] = pa.Field(nullable=True, coerce=True, description="Change Changed By ID", alias="change.changedById")
30
29
  employee_id: Series[String] = pa.Field(coerce=True, description="Employee ID", alias="employee_id") #set manually
@@ -38,7 +37,6 @@ class ActualPaymentsSchema(BrynQPanderaDataFrameModel):
38
37
  pay_type: Series[String] = pa.Field(coerce=True, description="Pay Type", alias="payType")
39
38
  amount_value: Series[Float] = pa.Field(coerce=True, description="Amount Value", alias="amount.value")
40
39
  amount_currency: Series[String] = pa.Field(coerce=True, description="Amount Currency", alias="amount.currency")
41
- # change_reason: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Reason", alias="change.reason")
42
40
  change_changed_by: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Changed By", alias="change.changedBy")
43
41
  change_changed_by_id: Series[String] = pa.Field(nullable=True, coerce=True, description="Change Changed By ID", alias="change.changedById")
44
42
 
@@ -1,14 +1,13 @@
1
1
  from datetime import datetime
2
- from typing import Dict, List, Optional
2
+ from typing import Optional, List, Dict
3
3
 
4
4
  import pandas as pd
5
5
  import pandera as pa
6
- import pandera.extensions as extensions
7
6
  from pandera import Bool
8
- from pandera.engines.pandas_engine import DateTime
9
- from pandera.typing import Float, Series, String
10
-
7
+ from pandera.typing import Series, String, Float
8
+ import pandera.extensions as extensions
11
9
  from brynq_sdk_functions import BrynQPanderaDataFrameModel
10
+ from pandera.engines.pandas_engine import DateTime
12
11
 
13
12
 
14
13
  @extensions.register_check_method()
@@ -27,7 +26,7 @@ class PeopleSchema(BrynQPanderaDataFrameModel):
27
26
  first_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="First Name", alias="firstName", metadata={"api_field": "root.firstName"})
28
27
  full_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Full Name", alias="fullName", metadata={"api_field": "root.fullName"})
29
28
  # the date is in DD/MM/YYYY format,
30
- personal_birth_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="Personal Birth Date", alias="personal.birthDate") # , dtype_kwargs={"to_datetime_kwargs": {"format": "%d/%m/%Y"}}
29
+ personal_birth_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="Personal Birth Date", alias="personal.birthDate")
31
30
  personal_pronouns: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Personal Pronouns", alias="personal.pronouns")
32
31
  personal_honorific: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Personal Honorific", alias="personal.honorific")
33
32
  personal_nationality: Optional[Series[object]] = pa.Field(coerce=True, check_name=check_list, description="Personal Nationality", alias="personal.nationality")
@@ -41,10 +40,10 @@ class PeopleSchema(BrynQPanderaDataFrameModel):
41
40
  work_manager: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Work Manager", alias="work.manager")
42
41
  work_work_phone: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Work Work Phone", alias="work.workPhone")
43
42
  work_tenure_duration_period_i_s_o: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Work Tenure Duration Period ISO", alias="work.tenureDuration.periodISO")
44
- work_tenure_duration_sort_factor: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, nullable=True, description="Work Tenure Duration Sort Factor", alias="work.tenureDuration.sortFactor")
43
+ work_tenure_duration_sort_factor: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, nullable=False, description="Work Tenure Duration Sort Factor", alias="work.tenureDuration.sortFactor")
45
44
  work_tenure_duration_humanize: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Work Tenure Duration Humanize", alias="work.tenureDuration.humanize")
46
45
  work_duration_of_employment_period_i_s_o: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Work Duration of Employment Period ISO", alias="work.durationOfEmployment.periodISO")
47
- work_duration_of_employment_sort_factor: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Work Duration of Employment Sort Factor", alias="work.durationOfEmployment.sortFactor")
46
+ work_duration_of_employment_sort_factor: Optional[Series[String]] = pa.Field(coerce=True, nullable=False, description="Work Duration of Employment Sort Factor", alias="work.durationOfEmployment.sortFactor")
48
47
  work_duration_of_employment_humanize: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Work Duration of Employment Humanize", alias="work.durationOfEmployment.humanize")
49
48
  work_reports_to_id_in_company: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, nullable=True, description="Work Reports to ID in Company", alias="work.reportsToIdInCompany")
50
49
  work_employee_id_in_company: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, nullable=True, description="Work Employee ID in Company", alias="work.employeeIdInCompany")
@@ -203,7 +202,7 @@ class PeopleSchema(BrynQPanderaDataFrameModel):
203
202
  payroll_employment_actual_working_pattern_days_thursday: Optional[Series[float]] = pa.Field(coerce=True, nullable=True, description="Actual Working Pattern - Thursday", alias="payroll.employment.actualWorkingPattern.days.thursday")
204
203
  payroll_employment_actual_working_pattern_days_saturday: Optional[Series[float]] = pa.Field(coerce=True, nullable=True, description="Actual Working Pattern - Saturday", alias="payroll.employment.actualWorkingPattern.days.saturday")
205
204
  payroll_employment_actual_working_pattern_hours_per_day: Optional[Series[float]] = pa.Field(coerce=True, nullable=True, description="Actual Working Pattern - Hours Per Day", alias="payroll.employment.actualWorkingPattern.hoursPerDay")
206
- payroll_employment_actual_working_pattern_working_pattern_id: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Actual Working Pattern - Working Pattern ID", alias="payroll.employment.actualWorkingPattern.workingPatternId")
205
+ payroll_employment_actual_working_pattern_working_pattern_id: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, nullable=True, description="Actual Working Pattern - Working Pattern ID", alias="payroll.employment.actualWorkingPattern.workingPatternId")
207
206
  payroll_employment_site_working_pattern_working_pattern_type: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Site Working Pattern - Working Pattern Type", alias="payroll.employment.siteWorkingPattern.workingPatternType")
208
207
  payroll_employment_site_working_pattern_days_sunday: Optional[Series[float]] = pa.Field(coerce=True, nullable=True, description="Site Working Pattern - Sunday", alias="payroll.employment.siteWorkingPattern.days.sunday")
209
208
  payroll_employment_site_working_pattern_days_tuesday: Optional[Series[float]] = pa.Field(coerce=True, nullable=True, description="Site Working Pattern - Tuesday", alias="payroll.employment.siteWorkingPattern.days.tuesday")
@@ -215,13 +214,24 @@ class PeopleSchema(BrynQPanderaDataFrameModel):
215
214
  payroll_employment_site_working_pattern_hours_per_day: Optional[Series[float]] = pa.Field(coerce=True, nullable=True, description="Site Working Pattern - Hours Per Day", alias="payroll.employment.siteWorkingPattern.hoursPerDay")
216
215
  payroll_employment_site_working_pattern_working_pattern_id: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, nullable=True, description="Site Working Pattern - Working Pattern ID", alias="payroll.employment.siteWorkingPattern.workingPatternId")
217
216
  payroll_employment_calendar_id: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Calendar ID", alias="payroll.employment.calendarId")
218
-
219
-
220
217
  payroll_employment_salary_pay_type: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Salary Pay Type", alias="payroll.employment.salaryPayType")
221
218
  payroll_employment_flsa_code: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="FLSA Code", alias="payroll.employment.flsaCode")
222
219
  payroll_employment_fte: Optional[Series[Float]] = pa.Field(coerce=True, nullable=True, description="FTE %", alias="payroll.employment.fte")
223
220
  payroll_employment_weekly_hours: Optional[Series[Float]] = pa.Field(coerce=True, nullable=True, description="Weekly Hours", alias="payroll.employment.weeklyHours")
224
221
 
222
+ # Salary information
223
+ # SOLUTION NOW FOR MFT: TODO: FIX UNNESTING (including .value.value)
224
+ payroll_salary_monthly_get: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Base salary (monthly), used for getting nested json", alias="payroll.salary.monthlyPayment")
225
+ payroll_salary_monthly_payment: Optional[Series[Float]] = pa.Field(coerce=True, nullable=True, description="Base salary (monthly)", alias="payroll.salary.monthlyPayment.value")
226
+ payroll_salary_monthly_payment_currency: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Base salary (monthly) currency", alias="payroll.salary.monthlyPayment.currency")
227
+
228
+ payroll_salary_yearly_get: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Base salary (yearly), used for getting nested json", alias="payroll.salary.yearlyPayment")
229
+ payroll_salary_yearly_payment: Optional[Series[Float]] = pa.Field(coerce=True, nullable=True, description="Base salary (yearly)", alias="payroll.salary.yearlyPayment.value")
230
+ payroll_salary_yearly_payment_currency: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Base salary (yearly) currency", alias="payroll.salary.yearlyPayment.currency")
231
+
232
+ payroll_salary_active_effective_date: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="effective date payment", alias="payroll.salary.activeEffectiveDate")
233
+ payroll_salary_payment: Optional[Series[Float]] = pa.Field(coerce=True, nullable=True, description="Base Salary", alias="payroll.salary.payment")
234
+
225
235
  # Emergency contact
226
236
  emergency_first_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Emergency First Name", alias="emergency.firstName")
227
237
  emergency_second_name: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Emergency Middle Name", alias="emergency.secondName")
@@ -277,7 +287,6 @@ class PeopleSchema(BrynQPanderaDataFrameModel):
277
287
  # Positions / Job profile
278
288
  employee_position_opening_id: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Position Opening ID", alias="employee.positionOpeningId")
279
289
  employee_job_profile_id: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Job Profile ID", alias="employee.jobProfileId")
280
- employee_job_profile_code: Optional[Series[String]] = pa.Field(coerce=True, nullable=True, description="Job Profile Code", alias="employee.jobProfileCode")
281
290
  employee_recent_leave_start_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="Recent Leave Start Date", alias="employee.recentLeaveStartDate")
282
291
  employee_recent_leave_end_date: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="Recent Leave End Date", alias="employee.recentLeaveEndDate")
283
292
  employee_first_day_of_work: Optional[Series[DateTime]] = pa.Field(coerce=True, nullable=True, description="First Day Of Work", alias="employee.firstDayOfWork")
@@ -18,7 +18,6 @@ class SalarySchema(BrynQPanderaDataFrameModel):
18
18
  modification_date: Series[DateTime] = pa.Field(coerce=True, nullable=True, description="Modification Date", alias="modificationDate")
19
19
  effective_date: Series[DateTime] = pa.Field(coerce=True, description="Effective Date", alias="effectiveDate")
20
20
  end_effective_date: Series[DateTime] = pa.Field(coerce=True, nullable=True, description="End Effective Date", alias="endEffectiveDate")
21
- change_reason: Series[str] = pa.Field(coerce=True, nullable=True, description="Change Reason", alias="change.reason")
22
21
  pay_period: Series[String] = pa.Field(coerce=True, nullable=True, description="Pay Period", alias="payPeriod")
23
22
  base_value: Series[Float] = pa.Field(coerce=True, nullable=True, description="Base Value", alias="base.value") #needs to become base.value?
24
23
  base_currency: Series[String] = pa.Field(coerce=True, nullable=True, description="Base Currency", alias="base.currency")
@@ -4,14 +4,20 @@ from typing import Optional
4
4
  import pandas as pd
5
5
  from brynq_sdk_functions import BrynQPanderaDataFrameModel
6
6
 
7
+
8
+ # =============================================================================
9
+ # TimeOffSchema - For /timeoff/requests/changes endpoint (change events)
10
+ # =============================================================================
11
+
7
12
  class TimeOffSchema(BrynQPanderaDataFrameModel):
13
+ """Schema for time off change events from /timeoff/requests/changes endpoint."""
8
14
  change_type: Series[String] = pa.Field(coerce=True, description="Change Type", alias="changeType")
9
- employee_id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
15
+ employee_id: Series[String] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
10
16
  employee_display_name: Series[String] = pa.Field(coerce=True, description="Employee Display Name", alias="employeeDisplayName")
11
17
  employee_email: Series[String] = pa.Field(coerce=True, description="Employee Email", alias="employeeEmail")
12
18
  request_id: Series[pd.Int64Dtype] = pa.Field(coerce=True, description="Request ID", alias="requestId")
13
19
  policy_type_display_name: Series[String] = pa.Field(coerce=True, description="Policy Type Display Name", alias="policyTypeDisplayName")
14
- type: Series[String] = pa.Field(coerce=True, description="Type", alias="type")
20
+ type: Series[String] = pa.Field(coerce=True, description="Request type", alias="type")
15
21
  start_date: Series[String] = pa.Field(coerce=True, nullable=True, description="Start Date", alias="startDate")
16
22
  start_portion: Series[String] = pa.Field(coerce=True, nullable=True, description="Start Portion", alias="startPortion")
17
23
  end_date: Series[String] = pa.Field(coerce=True, nullable=True, description="End Date", alias="endDate")
@@ -30,7 +36,191 @@ class TimeOffSchema(BrynQPanderaDataFrameModel):
30
36
  coerce = True
31
37
 
32
38
 
39
+ # =============================================================================
40
+ # TimeOffRequest - For /timeoff/employees/{id}/requests/{requestId} endpoint
41
+ # =============================================================================
42
+
43
+ class TimeOffRequest(BrynQPanderaDataFrameModel):
44
+ """
45
+ Schema for time off request details from Bob API.
46
+
47
+ Based on: https://apidocs.hibob.com/reference/get_timeoff-employees-id-requests-requestid
48
+
49
+ Supports all request types (discriminated by 'type' field):
50
+ - days: Request for X days
51
+ - hours: Request for X hours during the day (policy types measured in hours)
52
+ - portionOnRange: Every morning or afternoon during days requested
53
+ - hoursOnRange: X hours every day during days requested
54
+ - differentDayDurations: Different hours on each day requested
55
+ - specificHoursDayDurations: Specific hours per day
56
+ - differentSpecificHoursDayDurations: Different specific hours on each day
57
+ - percentageOnRange: X percent of every day during days requested
58
+ - openEnded: Request without an end date yet
59
+
60
+ All type-specific fields are optional since they vary by request type.
61
+
62
+ Note: Complex nested fields (attachmentLinks, durations arrays) are not included
63
+ """
64
+
65
+ # -------------------------------------------------------------------------
66
+ # IDENTIFIERS
67
+ # -------------------------------------------------------------------------
68
+ employee_id: Series[String] = pa.Field(
69
+ coerce=True, description="Employee ID", alias="employeeId"
70
+ )
71
+ request_id: Series[pd.Int64Dtype] = pa.Field(
72
+ coerce=True, description="Time Off Request ID", alias="requestId"
73
+ )
74
+
75
+ # -------------------------------------------------------------------------
76
+ # REQUEST METADATA
77
+ # -------------------------------------------------------------------------
78
+ policy_type_display_name: Series[String] = pa.Field(
79
+ coerce=True, description="Display name of the policy type", alias="policyTypeDisplayName"
80
+ )
81
+ created_on: Series[String] = pa.Field(
82
+ coerce=True, description="Date and time the request was created", alias="createdOn"
83
+ )
84
+ description: Optional[Series[String]] = pa.Field(
85
+ nullable=True, coerce=True, description="Request description", alias="description"
86
+ )
87
+
88
+ # -------------------------------------------------------------------------
89
+ # TYPE DISCRIMINATOR
90
+ # Valid values: days, hours, portionOnRange, hoursOnRange, differentDayDurations,
91
+ # specificHoursDayDurations, differentSpecificHoursDayDurations,
92
+ # percentageOnRange, openEnded
93
+ # -------------------------------------------------------------------------
94
+ type: Series[String] = pa.Field(
95
+ coerce=True, description="Request type discriminator", alias="type"
96
+ )
97
+
98
+ # GENERAL INFO
99
+ duration_unit: Series[String] = pa.Field(
100
+ coerce=True, description="Unit for totalDuration/totalCost: 'days' or 'hours'", alias="durationUnit"
101
+ )
102
+ total_duration: Series[Float] = pa.Field(
103
+ coerce=True, description="Total time including regular days off", alias="totalDuration"
104
+ )
105
+ total_cost: Series[Float] = pa.Field(
106
+ coerce=True, description="Amount deducted from balance", alias="totalCost"
107
+ )
108
+ status: Series[String] = pa.Field(
109
+ coerce=True, description="Request status: approved, pending, canceled, etc.", alias="status"
110
+ )
111
+ approved: Series[pd.BooleanDtype] = pa.Field(
112
+ coerce=True, description="Whether request is approved", alias="approved"
113
+ )
114
+
115
+ has_attachment: Series[pd.BooleanDtype] = pa.Field(
116
+ coerce=True, description="Whether request has attachments", alias="hasAttachment"
117
+ )
118
+ # Note: attachmentLinks array is not included (complex nested structure)
119
+
120
+ reason_code: Optional[Series[String]] = pa.Field(
121
+ nullable=True, coerce=True, description="Reason code from policy type's list", alias="reasonCode"
122
+ )
123
+
124
+ previous_request_id: Optional[Series[pd.Int64Dtype]] = pa.Field(
125
+ nullable=True, coerce=True,
126
+ description="ID of replaced request when date/time updated", alias="previousRequestId"
127
+ )
128
+ original_request_id: Optional[Series[pd.Int64Dtype]] = pa.Field(
129
+ nullable=True, coerce=True,
130
+ description="ID of the very first request in history chain", alias="originalRequestId"
131
+ )
132
+
133
+ approved_by: Optional[Series[String]] = pa.Field(
134
+ nullable=True, coerce=True, description="Who approved the request", alias="approvedBy"
135
+ )
136
+ approved_at: Optional[Series[String]] = pa.Field(
137
+ nullable=True, coerce=True, description="When request was approved", alias="approvedAt"
138
+ )
139
+
140
+ declined_by: Optional[Series[String]] = pa.Field(
141
+ nullable=True, coerce=True, description="Who declined the request", alias="declinedBy"
142
+ )
143
+ declined_at: Optional[Series[String]] = pa.Field(
144
+ nullable=True, coerce=True, description="When request was declined", alias="declinedAt"
145
+ )
146
+ decline_reason: Optional[Series[String]] = pa.Field(
147
+ nullable=True, coerce=True, description="Why request was declined", alias="declineReason"
148
+ )
149
+
150
+ visibility: Series[String] = pa.Field(
151
+ coerce=True, description="Visibility: 'Public', 'Private' or 'Custom name'", alias="visibility"
152
+ )
153
+ time_zone_offset: Optional[Series[String]] = pa.Field(
154
+ nullable=True, coerce=True,
155
+ description="GMT offset (e.g., 'GMT -5:00') for requests with specific times", alias="timeZoneOffset"
156
+ )
157
+
158
+ # -------------------------------------------------------------------------
159
+ # TYPE-SPECIFIC FIELDS (optional, presence depends on 'type' value)
160
+ # -------------------------------------------------------------------------
161
+
162
+ # For types: days, portionOnRange, hoursOnRange, differentDayDurations,
163
+ # specificHoursDayDurations, differentSpecificHoursDayDurations,
164
+ # percentageOnRange, openEnded
165
+ start_date: Optional[Series[String]] = pa.Field(
166
+ nullable=True, coerce=True, description="First day of time off", alias="startDate"
167
+ )
168
+ end_date: Optional[Series[String]] = pa.Field(
169
+ nullable=True, coerce=True, description="Last day of time off (null for openEnded)", alias="endDate"
170
+ )
171
+
172
+ # For types: days, openEnded
173
+ start_portion: Optional[Series[String]] = pa.Field(
174
+ nullable=True, coerce=True,
175
+ description="First day portion: all_day, morning, afternoon", alias="startPortion"
176
+ )
177
+ end_portion: Optional[Series[String]] = pa.Field(
178
+ nullable=True, coerce=True,
179
+ description="Last day portion: all_day, morning, afternoon (null for openEnded)", alias="endPortion"
180
+ )
181
+
182
+ # For type: hours
183
+ date: Optional[Series[String]] = pa.Field(
184
+ nullable=True, coerce=True, description="Date for single-day hours request", alias="date"
185
+ )
186
+ hours_on_date: Optional[Series[Float]] = pa.Field(
187
+ nullable=True, coerce=True, description="Hours for single-day request", alias="hoursOnDate"
188
+ )
189
+
190
+ # For type: portionOnRange
191
+ day_portion: Optional[Series[String]] = pa.Field(
192
+ nullable=True, coerce=True,
193
+ description="Portion for range: morning or afternoon", alias="dayPortion"
194
+ )
195
+
196
+ # For type: hoursOnRange
197
+ daily_hours: Optional[Series[Float]] = pa.Field(
198
+ nullable=True, coerce=True, description="Hours per day for range", alias="dailyHours"
199
+ )
200
+
201
+ # For type: percentageOnRange
202
+ percentage_of_day: Optional[Series[pd.Int64Dtype]] = pa.Field(
203
+ nullable=True, coerce=True, description="Percent of each day requested", alias="percentageOfDay"
204
+ )
205
+
206
+ # For types: specificHoursDayDurations, differentSpecificHoursDayDurations, openEnded
207
+ time_zone: Optional[Series[String]] = pa.Field(
208
+ nullable=True, coerce=True,
209
+ description="Time zone name (e.g., 'Europe/London')", alias="timeZone"
210
+ )
211
+
212
+ # Note: 'durations' array is not included (complex nested structure with per-day details)
213
+
214
+ class Config:
215
+ coerce = True
216
+
217
+
218
+ # =============================================================================
219
+ # TimeOffBalanceSchema - For /timeoff/employees/{id}/balance endpoint
220
+ # =============================================================================
221
+
33
222
  class TimeOffBalanceSchema(BrynQPanderaDataFrameModel):
223
+ """Schema for time off balance from /timeoff/employees/{id}/balance endpoint."""
34
224
  employee_id: Series[String] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
35
225
  policy_type_name: Series[String] = pa.Field(coerce=True, description="Policy Type Name", alias="policyTypeName")
36
226
  policy_type_display_name: Series[String] = pa.Field(coerce=True, description="Policy Type Display Name", alias="policyTypeDisplayName")
@@ -1,9 +1,13 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ import pandas as pd
1
5
  import pandera as pa
2
6
  from pandera.typing import Series
3
- import pandas as pd
4
- from datetime import datetime
7
+
5
8
  from brynq_sdk_functions import BrynQPanderaDataFrameModel
6
9
 
10
+
7
11
  class WorkSchema(BrynQPanderaDataFrameModel):
8
12
  can_be_deleted: Series[pa.Bool] = pa.Field(coerce=True, description="Can Be Deleted", alias="canBeDeleted")
9
13
  work_change_type: Series[str] = pa.Field(coerce=True, description="Work Change Type", alias="workChangeType")
@@ -18,14 +22,14 @@ class WorkSchema(BrynQPanderaDataFrameModel):
18
22
  active_effective_date: Series[datetime] = pa.Field(coerce=True, nullable=True, description="Active Effective Date", alias="activeEffectiveDate")
19
23
  department: Series[str] = pa.Field(coerce=True, nullable=True, description="Department", alias="department")
20
24
  effective_date: Series[datetime] = pa.Field(coerce=True, nullable=True, description="Effective Date", alias="effectiveDate")
21
- change_reason: Series[str] = pa.Field(coerce=True, nullable=True, description="Change Reason", alias="change.reason")
22
25
  change_changed_by: Series[str] = pa.Field(coerce=True, nullable=True, description="Change Changed By", alias="change.changedBy")
23
26
  change_changed_by_id: Series[str] = pa.Field(coerce=True, nullable=True, description="Change Changed By ID", alias="change.changedById")
24
- reports_to_id: Series[str] = pa.Field(coerce=True, nullable=True, description="Reports To ID", alias="reportsTo.id")
25
- reports_to_first_name: Series[str] = pa.Field(coerce=True, nullable=True, description="Reports To First Name", alias="reportsTo.firstName")
26
- reports_to_surname: Series[str] = pa.Field(coerce=True, nullable=True, description="Reports To Surname", alias="reportsTo.surname")
27
- reports_to_email: Series[str] = pa.Field(coerce=True, nullable=True, description="Reports To Email", alias="reportsTo.email")
28
- reports_to_display_name: Series[str] = pa.Field(coerce=True, nullable=True, description="Reports To Display Name", alias="reportsTo.displayName")
27
+ reports_to_id: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Reports To ID", alias="reportsTo.id")
28
+ reports_to_first_name: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Reports To First Name", alias="reportsTo.firstName")
29
+ reports_to_surname: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Reports To Surname", alias="reportsTo.surname")
30
+ reports_to_email: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Reports To Email", alias="reportsTo.email")
31
+ reports_to_display_name: Optional[Series[str]] = pa.Field(coerce=True, nullable=True, description="Reports To Display Name", alias="reportsTo.displayName")
32
+ reports_to: Optional[Series[pd.Int64Dtype]] = pa.Field(coerce=True, nullable=True, description="Reports To", alias="reportsTo")
29
33
  employee_id: Series[str] = pa.Field(coerce=True, description="Employee ID", alias="employeeId")
30
34
 
31
35
  class Config:
brynq_sdk_bob/timeoff.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from datetime import datetime, timezone, timedelta
2
+ from typing import Union
2
3
  import pandas as pd
3
4
  from brynq_sdk_functions import Functions
4
- from .schemas.timeoff import TimeOffSchema, TimeOffBalanceSchema
5
+ from .schemas.timeoff import TimeOffSchema, TimeOffBalanceSchema, TimeOffRequest
5
6
  import warnings
6
7
 
7
8
 
@@ -40,13 +41,43 @@ class TimeOff:
40
41
  params={'since': since, 'includePending': 'true' if include_pending else 'false'},
41
42
  timeout=self.bob.timeout)
42
43
  resp.raise_for_status()
43
- data = resp.json()['changes']
44
- # data = self.bob.get_paginated_result(request)
44
+ data = resp.json().get('changes', [])
45
45
  df = pd.DataFrame(data)
46
46
  valid_timeoff, invalid_timeoff = Functions.validate_data(df=df, schema=self.schema, debug=True)
47
47
 
48
48
  return valid_timeoff, invalid_timeoff
49
49
 
50
+ def get_by_request_id(
51
+ self,
52
+ employee_id: Union[str, int],
53
+ request_id: Union[str, int],
54
+ ) -> tuple[pd.DataFrame, pd.DataFrame]:
55
+ """
56
+ Get time off request details by request ID.
57
+
58
+ Args:
59
+ employee_id: The Employee ID (from database or Bob URL).
60
+ Example: "3332883884017713238" from URL "https://app.hibob.com/employee-profile/3332883884017713238"
61
+ request_id: The time off request ID.
62
+
63
+ Returns:
64
+ tuple[pd.DataFrame, pd.DataFrame]: (valid_request, invalid_request) as single-row DataFrames.
65
+ """
66
+ resp = self.bob.session.get(
67
+ url=f"{self.bob.base_url}timeoff/employees/{employee_id}/requests/{request_id}",
68
+ timeout=self.bob.timeout
69
+ )
70
+ resp.raise_for_status()
71
+ data = resp.json()
72
+
73
+ # Single request returns a dict, wrap in list for DataFrame
74
+ df = pd.DataFrame([data])
75
+
76
+ valid_request, invalid_request = Functions.validate_data(df=df, schema=TimeOffRequest, debug=True)
77
+
78
+ return valid_request, invalid_request
79
+
80
+
50
81
  def get_balance(self, employee_id: str, policy_type: str = None, as_of_date: str = None) -> tuple[pd.DataFrame, pd.DataFrame]:
51
82
  """
52
83
  Get time off balance for a specific employee
brynq_sdk_bob/work.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import pandas as pd
2
2
  import requests
3
- from typing import Optional, List
4
3
  from brynq_sdk_functions import Functions
5
4
  from .schemas.work import WorkSchema
6
5
 
@@ -10,16 +9,9 @@ class Work:
10
9
  self.bob = bob
11
10
  self.schema = WorkSchema
12
11
 
13
- def get(self, employee_ids: Optional[List[str]] = None) ->(pd.DataFrame, pd.DataFrame):
14
- params = {"limit": 100}
15
-
16
- # Add employeeIds filter if provided
17
- if employee_ids is not None:
18
- params["employeeIds"] = ",".join(employee_ids)
19
-
12
+ def get(self) ->(pd.DataFrame, pd.DataFrame):
20
13
  request = requests.Request(method='GET',
21
- url=f"{self.bob.base_url}bulk/people/work",
22
- params=params)
14
+ url=f"{self.bob.base_url}bulk/people/work")
23
15
  data = self.bob.get_paginated_result(request)
24
16
  df = pd.json_normalize(
25
17
  data,
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brynq_sdk_bob
3
- Version: 2.6.2.dev10
3
+ Version: 2.9.4
4
4
  Summary: Bob wrapper from BrynQ
5
5
  Author: BrynQ
6
6
  Author-email: support@brynq.com
7
7
  License: BrynQ License
8
8
  Requires-Dist: brynq-sdk-brynq<5,>=4
9
9
  Requires-Dist: pandas<3.0.0,>=2.2.0
10
- Requires-Dist: tenacity==8.2.3
11
10
  Dynamic: author
12
11
  Dynamic: author-email
13
12
  Dynamic: description
@@ -0,0 +1,29 @@
1
+ brynq_sdk_bob/__init__.py,sha256=AFIEA5akwZqu-USQbMt26Rp8krqumvOsG1J83jhMQSg,3470
2
+ brynq_sdk_bob/bank.py,sha256=zTdfe_qCZt2FB7SZbQ7njIDspwTinLFdbeH_xUby2FY,966
3
+ brynq_sdk_bob/company.py,sha256=rjOpkm0CZ1EeJ-jddBl36GrGKUQviC1ca1aUL2tl1_M,848
4
+ brynq_sdk_bob/custom_tables.py,sha256=MvnR2mIcyK0rpwd0P7xV3BPIvCYQVEClBvo901GttPs,2642
5
+ brynq_sdk_bob/documents.py,sha256=ww101owiBGARCxOANdDtmWrNedSBe9V-BEed6QspQPg,1756
6
+ brynq_sdk_bob/employment.py,sha256=uNllQrIBbo8yPG_2-ln1PWeWUFU672T289PpWWvL-V8,763
7
+ brynq_sdk_bob/named_lists.py,sha256=ksLXV2ysBFegq4gZiiaC56gjkgdnPzL7WajZTGvjYIM,1069
8
+ brynq_sdk_bob/payments.py,sha256=43Ctdt5T8gtpCud67dm75xa90W8m6FyMrU2hAWKrzMc,4221
9
+ brynq_sdk_bob/payroll_history.py,sha256=wHo6da7kLDe1ViL4egyMdyJBMZnWVhwjNjmh4cTCTeY,3972
10
+ brynq_sdk_bob/people.py,sha256=t1A1dABX6UZ0pyLTGOL-Sp5pHY630KWIyIO3JQ_Pjdk,5970
11
+ brynq_sdk_bob/reports.py,sha256=Tawmqm_ZmQ487loyk-29-A_fTCrgImbWCEf6zfwuaq4,1245
12
+ brynq_sdk_bob/salaries.py,sha256=BGQm-PT9QuKKJ9DP5nX6wmC8SZRAlm9M9I2EJhoZaII,1523
13
+ brynq_sdk_bob/timeoff.py,sha256=JtTu14PWFqQIEn9r-Z8ipeNE-5p7hqPz5N6wjjBeLTs,4438
14
+ brynq_sdk_bob/work.py,sha256=0bVZkQ0I6z-z2_ql-EsOpFExx8VgsJvpcCQdOfiJYQM,712
15
+ brynq_sdk_bob/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ brynq_sdk_bob/schemas/bank.py,sha256=lDmXP4P8091N20fL2CmhPU2wFuaK60Z-p-dvYSNCMaQ,1846
17
+ brynq_sdk_bob/schemas/custom_tables.py,sha256=638NH2n50vHzw1LFlbKVCBKBhfSsfGqtEGuor0Z-QS4,1567
18
+ brynq_sdk_bob/schemas/employment.py,sha256=1LsPjp-TexEc4B9nXjciMKFw94bl6xhEp3TNsgnLcds,3387
19
+ brynq_sdk_bob/schemas/named_lists.py,sha256=HJBRKrAI2vhrkq-5MVXqQcmpGNzFtoOnaZI2Ii_6_vs,725
20
+ brynq_sdk_bob/schemas/payments.py,sha256=izFY-xZLk54JhdgrzLocjT1vfYUEVkehw_ZwjR1-Qbw,4040
21
+ brynq_sdk_bob/schemas/payroll_history.py,sha256=JdAq0XaArHHEw8EsXo3GD0EhSAyBhPtYQMmdvjCiY8g,806
22
+ brynq_sdk_bob/schemas/people.py,sha256=42BJVgJmT-h5kzuQl6iI7wZDSGNA0KTQQVIAqeeyHNk,40149
23
+ brynq_sdk_bob/schemas/salary.py,sha256=7pq66_JfxmPbSWowX-25c-aKQvz3IGmoG5toRIq3H7g,4418
24
+ brynq_sdk_bob/schemas/timeoff.py,sha256=gTYu_bNcfHrkTz4eIHCZ4WzgMTj2U4nI3X6JTzDovhk,12817
25
+ brynq_sdk_bob/schemas/work.py,sha256=1odd3ia97SZff8VjLzL1a0FEQaF2ojGeWsfovcWvkhM,3017
26
+ brynq_sdk_bob-2.9.4.dist-info/METADATA,sha256=QZFoC1aZR74xLxLMFdqyVBlKN1a9VThkausO5vr3XMs,371
27
+ brynq_sdk_bob-2.9.4.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
28
+ brynq_sdk_bob-2.9.4.dist-info/top_level.txt,sha256=oGiWqOuAAiVoLIzGe6F-Lo4IJBYz5ftOwBft7HtPuoY,14
29
+ brynq_sdk_bob-2.9.4.dist-info/RECORD,,
@@ -1,28 +0,0 @@
1
- brynq_sdk_bob/__init__.py,sha256=bw2Www_wgVxqZczJpz9K_94k7OdgqwWKF0aQaKG5TQE,3052
2
- brynq_sdk_bob/bank.py,sha256=5ncWsmeAvNoiLnVvIvPgFDRQ5DB5_VY2ipMC439GVHU,1460
3
- brynq_sdk_bob/company.py,sha256=rjOpkm0CZ1EeJ-jddBl36GrGKUQviC1ca1aUL2tl1_M,848
4
- brynq_sdk_bob/custom_tables.py,sha256=MvnR2mIcyK0rpwd0P7xV3BPIvCYQVEClBvo901GttPs,2642
5
- brynq_sdk_bob/documents.py,sha256=ww101owiBGARCxOANdDtmWrNedSBe9V-BEed6QspQPg,1756
6
- brynq_sdk_bob/employment.py,sha256=kBEKfUmKEw-A_FjC9fOuJqcsT7NxUKwXJs_V_-x9LbI,765
7
- brynq_sdk_bob/named_lists.py,sha256=ksLXV2ysBFegq4gZiiaC56gjkgdnPzL7WajZTGvjYIM,1069
8
- brynq_sdk_bob/payments.py,sha256=e1TnJcXlbotOfULukVUiaYZ1N-bDGynAtmPgpux7It0,5670
9
- brynq_sdk_bob/payroll_history.py,sha256=wHo6da7kLDe1ViL4egyMdyJBMZnWVhwjNjmh4cTCTeY,3972
10
- brynq_sdk_bob/people.py,sha256=6b7uCucl_xqtAq_4YJZU457fUY0qssMwfCCStzbNG0M,5036
11
- brynq_sdk_bob/salaries.py,sha256=8xq9XDTK473Az2MpuAPofz9CvZstjufSoWtF0bi1wC4,1766
12
- brynq_sdk_bob/timeoff.py,sha256=NbBZ39qy9D7jbS_z9bpmB-BKNuUGmNrkYTbEw034tZ0,3339
13
- brynq_sdk_bob/work.py,sha256=IADipEuI_ofhhLZZV9oI8PV10VN07yluPPkLZT19Ze8,1013
14
- brynq_sdk_bob/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- brynq_sdk_bob/schemas/bank.py,sha256=Yfq-ouzcAyaqcZj5_lWLcaQgtLikI9zlcsHhsuPUQzY,1935
16
- brynq_sdk_bob/schemas/custom_tables.py,sha256=638NH2n50vHzw1LFlbKVCBKBhfSsfGqtEGuor0Z-QS4,1567
17
- brynq_sdk_bob/schemas/employment.py,sha256=uErbSyl8xYaYo5Hu0FZ7_tl1WCOaytcU9pjkIWJbmOo,3226
18
- brynq_sdk_bob/schemas/named_lists.py,sha256=HJBRKrAI2vhrkq-5MVXqQcmpGNzFtoOnaZI2Ii_6_vs,725
19
- brynq_sdk_bob/schemas/payments.py,sha256=N4Ylommed-hsw0aX7rjuZuVZQc4XU0iDBdSIxz8eAxc,4292
20
- brynq_sdk_bob/schemas/payroll_history.py,sha256=JdAq0XaArHHEw8EsXo3GD0EhSAyBhPtYQMmdvjCiY8g,806
21
- brynq_sdk_bob/schemas/people.py,sha256=cy0mLKRdUhjOv52WB53AWd1XHn6mwrACGZAvK2ufsz4,38780
22
- brynq_sdk_bob/schemas/salary.py,sha256=TSaM1g92y3oiDcUrfJW7ushgKZenI9xB6XW3kKuU0dE,4540
23
- brynq_sdk_bob/schemas/timeoff.py,sha256=BHImTTT4n8j7bF7T5Ue_B0WHmmj1_QTPV9TKAlHeBZM,4124
24
- brynq_sdk_bob/schemas/work.py,sha256=klzJtQf-avwhOkRHuPTM-jMhTIsroDPE56da1H5xaYs,2926
25
- brynq_sdk_bob-2.6.2.dev10.dist-info/METADATA,sha256=hlLk5XIWFqHhiKBwQb8E-EPiyr34wWndRQvBxcSP5_M,408
26
- brynq_sdk_bob-2.6.2.dev10.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
27
- brynq_sdk_bob-2.6.2.dev10.dist-info/top_level.txt,sha256=oGiWqOuAAiVoLIzGe6F-Lo4IJBYz5ftOwBft7HtPuoY,14
28
- brynq_sdk_bob-2.6.2.dev10.dist-info/RECORD,,