brynq-sdk-bob 2.8.4__py3-none-any.whl → 2.9.0__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
@@ -21,8 +21,9 @@ class Bob(BrynQ):
21
21
  def __init__(self, system_type: Optional[Literal['source', 'target']] = None, test_environment: bool = True, debug: bool = False, target_system: str = None):
22
22
  super().__init__()
23
23
  self.timeout = 3600
24
+ self.test_environment = test_environment
24
25
  self.headers = self._get_request_headers(system_type)
25
- if test_environment:
26
+ if self.test_environment:
26
27
  self.base_url = "https://api.sandbox.hibob.com/v1/"
27
28
  else:
28
29
  self.base_url = "https://api.hibob.com/v1/"
@@ -46,6 +47,15 @@ class Bob(BrynQ):
46
47
 
47
48
  def _get_request_headers(self, system_type):
48
49
  credentials = self.interfaces.credentials.get(system='bob', system_type=system_type)
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
+ )
49
59
  auth_token = base64.b64encode(f"{credentials.get('data').get('User ID')}:{credentials.get('data').get('API Token')}".encode()).decode('utf-8')
50
60
  headers = {
51
61
  "accept": "application/json",
@@ -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
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")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brynq_sdk_bob
3
- Version: 2.8.4
3
+ Version: 2.9.0
4
4
  Summary: Bob wrapper from BrynQ
5
5
  Author: BrynQ
6
6
  Author-email: support@brynq.com
@@ -1,4 +1,4 @@
1
- brynq_sdk_bob/__init__.py,sha256=M2y-juQ-GgzglQa0HvbWmeA9fBzv5L3MShHbTfb9rsk,3014
1
+ brynq_sdk_bob/__init__.py,sha256=AFIEA5akwZqu-USQbMt26Rp8krqumvOsG1J83jhMQSg,3470
2
2
  brynq_sdk_bob/bank.py,sha256=zTdfe_qCZt2FB7SZbQ7njIDspwTinLFdbeH_xUby2FY,966
3
3
  brynq_sdk_bob/company.py,sha256=rjOpkm0CZ1EeJ-jddBl36GrGKUQviC1ca1aUL2tl1_M,848
4
4
  brynq_sdk_bob/custom_tables.py,sha256=MvnR2mIcyK0rpwd0P7xV3BPIvCYQVEClBvo901GttPs,2642
@@ -10,7 +10,7 @@ brynq_sdk_bob/payroll_history.py,sha256=wHo6da7kLDe1ViL4egyMdyJBMZnWVhwjNjmh4cTC
10
10
  brynq_sdk_bob/people.py,sha256=t1A1dABX6UZ0pyLTGOL-Sp5pHY630KWIyIO3JQ_Pjdk,5970
11
11
  brynq_sdk_bob/reports.py,sha256=Tawmqm_ZmQ487loyk-29-A_fTCrgImbWCEf6zfwuaq4,1245
12
12
  brynq_sdk_bob/salaries.py,sha256=BGQm-PT9QuKKJ9DP5nX6wmC8SZRAlm9M9I2EJhoZaII,1523
13
- brynq_sdk_bob/timeoff.py,sha256=NbBZ39qy9D7jbS_z9bpmB-BKNuUGmNrkYTbEw034tZ0,3339
13
+ brynq_sdk_bob/timeoff.py,sha256=JtTu14PWFqQIEn9r-Z8ipeNE-5p7hqPz5N6wjjBeLTs,4438
14
14
  brynq_sdk_bob/work.py,sha256=0bVZkQ0I6z-z2_ql-EsOpFExx8VgsJvpcCQdOfiJYQM,712
15
15
  brynq_sdk_bob/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  brynq_sdk_bob/schemas/bank.py,sha256=lDmXP4P8091N20fL2CmhPU2wFuaK60Z-p-dvYSNCMaQ,1846
@@ -21,9 +21,9 @@ brynq_sdk_bob/schemas/payments.py,sha256=LrSr8WApYxqbMDmhYmh0EISEbWJinWovsULZV6s
21
21
  brynq_sdk_bob/schemas/payroll_history.py,sha256=JdAq0XaArHHEw8EsXo3GD0EhSAyBhPtYQMmdvjCiY8g,806
22
22
  brynq_sdk_bob/schemas/people.py,sha256=42BJVgJmT-h5kzuQl6iI7wZDSGNA0KTQQVIAqeeyHNk,40149
23
23
  brynq_sdk_bob/schemas/salary.py,sha256=TSaM1g92y3oiDcUrfJW7ushgKZenI9xB6XW3kKuU0dE,4540
24
- brynq_sdk_bob/schemas/timeoff.py,sha256=uswH42djiiWE2H0wXKHoneDvOJQ5BlaIeNdNl-kYd-s,4117
24
+ brynq_sdk_bob/schemas/timeoff.py,sha256=gTYu_bNcfHrkTz4eIHCZ4WzgMTj2U4nI3X6JTzDovhk,12817
25
25
  brynq_sdk_bob/schemas/work.py,sha256=YgtBJ0WXJOq55bFlT_kY_IbHh0SlQEtaa0W8vms-xA4,3048
26
- brynq_sdk_bob-2.8.4.dist-info/METADATA,sha256=i8xKzW4Se01HsfeIOHSe_uHIxGVpornQ0Elqhdz0kHE,371
27
- brynq_sdk_bob-2.8.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- brynq_sdk_bob-2.8.4.dist-info/top_level.txt,sha256=oGiWqOuAAiVoLIzGe6F-Lo4IJBYz5ftOwBft7HtPuoY,14
29
- brynq_sdk_bob-2.8.4.dist-info/RECORD,,
26
+ brynq_sdk_bob-2.9.0.dist-info/METADATA,sha256=aYpW-QeJZdjrvK9wfDMu3y228359XcZVyZLim7TNgc8,371
27
+ brynq_sdk_bob-2.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ brynq_sdk_bob-2.9.0.dist-info/top_level.txt,sha256=oGiWqOuAAiVoLIzGe6F-Lo4IJBYz5ftOwBft7HtPuoY,14
29
+ brynq_sdk_bob-2.9.0.dist-info/RECORD,,