brynq-sdk-bob 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brynq_sdk_bob-0.0.1/PKG-INFO +10 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/__init__.py +73 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/bank.py +27 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/company.py +23 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/documents.py +40 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/employment.py +23 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/payments.py +22 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/people.py +54 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/salaries.py +24 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/timeoff.py +25 -0
- brynq_sdk_bob-0.0.1/brynq_sdk/bob/work.py +25 -0
- brynq_sdk_bob-0.0.1/brynq_sdk_bob.egg-info/PKG-INFO +10 -0
- brynq_sdk_bob-0.0.1/brynq_sdk_bob.egg-info/SOURCES.txt +17 -0
- brynq_sdk_bob-0.0.1/brynq_sdk_bob.egg-info/dependency_links.txt +1 -0
- brynq_sdk_bob-0.0.1/brynq_sdk_bob.egg-info/not-zip-safe +1 -0
- brynq_sdk_bob-0.0.1/brynq_sdk_bob.egg-info/requires.txt +2 -0
- brynq_sdk_bob-0.0.1/brynq_sdk_bob.egg-info/top_level.txt +1 -0
- brynq_sdk_bob-0.0.1/setup.cfg +4 -0
- brynq_sdk_bob-0.0.1/setup.py +18 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import re
|
|
3
|
+
from typing import Union, List
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import requests
|
|
6
|
+
from brynq_sdk.brynq import BrynQ
|
|
7
|
+
|
|
8
|
+
from .bank import Bank
|
|
9
|
+
from .employment import Employment
|
|
10
|
+
from .payments import Payments
|
|
11
|
+
from .people import People
|
|
12
|
+
from .salaries import Salaries
|
|
13
|
+
from .timeoff import TimeOff
|
|
14
|
+
from .work import Work
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Bob(BrynQ):
|
|
18
|
+
def __init__(self, label: Union[str, List], test_environment: bool = True, debug: bool = False):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.headers = self._get_request_headers(label=label)
|
|
21
|
+
if test_environment:
|
|
22
|
+
self.base_url = "https://api.sandbox.hibob.com/v1/"
|
|
23
|
+
else:
|
|
24
|
+
self.base_url = "https://api.hibob.com/v1/"
|
|
25
|
+
self.session = requests.Session()
|
|
26
|
+
self.session.headers.update(self.headers)
|
|
27
|
+
self.people = People(self)
|
|
28
|
+
self.salaries = Salaries(self)
|
|
29
|
+
self.work = Work(self)
|
|
30
|
+
self.bank = Bank(self)
|
|
31
|
+
self.employment = Employment(self)
|
|
32
|
+
self.payments = Payments(self)
|
|
33
|
+
self.time_off = TimeOff(self)
|
|
34
|
+
|
|
35
|
+
def _get_request_headers(self, label):
|
|
36
|
+
credentials = self.get_system_credential(system='bob', label=label)
|
|
37
|
+
auth_token = base64.b64encode(f"{credentials['User ID']}:{credentials['API Token']}".encode()).decode('utf-8')
|
|
38
|
+
headers = {
|
|
39
|
+
"accept": "application/json",
|
|
40
|
+
"Authorization": f"Basic {auth_token}",
|
|
41
|
+
"Partner-Token": "001Vg00000A6FY6IAN"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return headers
|
|
45
|
+
|
|
46
|
+
def get_paginated_result(self, request: requests.Request) -> List:
|
|
47
|
+
has_next_page = True
|
|
48
|
+
result_data = []
|
|
49
|
+
while has_next_page:
|
|
50
|
+
prepped = request.prepare()
|
|
51
|
+
prepped.headers.update(self.session.headers)
|
|
52
|
+
resp = self.session.send(prepped)
|
|
53
|
+
resp.raise_for_status()
|
|
54
|
+
response_data = resp.json()
|
|
55
|
+
result_data += response_data['results']
|
|
56
|
+
next_cursor = response_data.get('response_metadata').get('next_cursor')
|
|
57
|
+
# If there is no next page, set has_next_page to False, we could use the falsy value of None but this is more readable
|
|
58
|
+
has_next_page = next_cursor is not None
|
|
59
|
+
if has_next_page:
|
|
60
|
+
request.params.update({"cursor": next_cursor})
|
|
61
|
+
|
|
62
|
+
return result_data
|
|
63
|
+
|
|
64
|
+
def rename_camel_columns_to_snake_case(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
65
|
+
def camel_to_snake_case(column):
|
|
66
|
+
# Replace periods with underscores
|
|
67
|
+
column = column.replace('.', '_')
|
|
68
|
+
# Insert underscores before capital letters and convert to lowercase
|
|
69
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '_', column).lower()
|
|
70
|
+
|
|
71
|
+
df.columns = map(camel_to_snake_case, df.columns)
|
|
72
|
+
|
|
73
|
+
return df
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from brynq_sdk.functions import Functions
|
|
3
|
+
|
|
4
|
+
from src.bob.schemas.bank import BankSchema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Bank:
|
|
8
|
+
def __init__(self, bob):
|
|
9
|
+
self.bob = bob
|
|
10
|
+
|
|
11
|
+
def get(self, person_ids: pd.Series) -> pd.DataFrame:
|
|
12
|
+
data = []
|
|
13
|
+
for person_id in person_ids:
|
|
14
|
+
resp = self.bob.session.get(url=f"{self.bob.base_url}people/{person_id}/bank-accounts")
|
|
15
|
+
resp.raise_for_status()
|
|
16
|
+
temp_data = resp.json()['values']
|
|
17
|
+
# when an employee has one or more bank accounts, the response is a list of dictionaries.
|
|
18
|
+
for account in temp_data:
|
|
19
|
+
account['employee_id'] = person_id
|
|
20
|
+
data += temp_data
|
|
21
|
+
|
|
22
|
+
df = pd.DataFrame(data)
|
|
23
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
24
|
+
|
|
25
|
+
valid_banks, invalid_banks = Functions.validate_data(df=df, schema=BankSchema, debug=True)
|
|
26
|
+
|
|
27
|
+
return valid_banks
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Company:
|
|
5
|
+
def __init__(self, bob):
|
|
6
|
+
self.bob = bob
|
|
7
|
+
|
|
8
|
+
def get_variable_values(self, list_name: str = None) -> dict:
|
|
9
|
+
values = {}
|
|
10
|
+
|
|
11
|
+
if list_name is not None:
|
|
12
|
+
resp = self.bob.session.get(url=f"{self.bob.base_url}company/named-lists/{list_name}")
|
|
13
|
+
resp.raise_for_status()
|
|
14
|
+
data = resp.json()
|
|
15
|
+
values.update({data["name"]: [value['id'] for value in data['values']]})
|
|
16
|
+
else:
|
|
17
|
+
resp = self.bob.session.get(url=f"{self.bob.base_url}company/named-lists")
|
|
18
|
+
resp.raise_for_status()
|
|
19
|
+
data = resp.json()
|
|
20
|
+
for list_key, list_data in data.items():
|
|
21
|
+
values.update({list_key: [value['id'] for value in list_data['values']]})
|
|
22
|
+
|
|
23
|
+
return values
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from brynq_sdk.functions import Functions
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Documents:
|
|
7
|
+
def __init__(self, bob):
|
|
8
|
+
self.bob = bob
|
|
9
|
+
# self.headers_upload = self.bob.headers.copy()
|
|
10
|
+
# self.headers_upload['Content-Type'] = 'multipart/form-data'
|
|
11
|
+
# self.headers_upload['Accept'] = 'application/json'
|
|
12
|
+
|
|
13
|
+
def get(self, person_id: datetime) -> pd.DataFrame:
|
|
14
|
+
resp = self.bob.session.get(url=f"{self.bob.base_url}docs/people/{person_id}")
|
|
15
|
+
resp.raise_for_status()
|
|
16
|
+
data = resp.json()['documents']
|
|
17
|
+
df = pd.DataFrame(data)
|
|
18
|
+
# data = self.bob.get_paginated_result(request)
|
|
19
|
+
# df = pd.json_normalize(
|
|
20
|
+
# data,
|
|
21
|
+
# record_path='changes',
|
|
22
|
+
# meta=['employeeId']
|
|
23
|
+
# )
|
|
24
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
25
|
+
valid_documents, invalid_documents = Functions.validate_data(df=df, schema=DocumentsSchema, debug=True)
|
|
26
|
+
|
|
27
|
+
return valid_documents
|
|
28
|
+
|
|
29
|
+
def create_confidential(self,
|
|
30
|
+
person_id: datetime,
|
|
31
|
+
file_name: str,
|
|
32
|
+
file_path: str):
|
|
33
|
+
files = {"file": (file_name, open(file_path, "rb"), "application/pdf")}
|
|
34
|
+
resp = self.bob.session.post(url=f"{self.bob.base_url}people/{person_id}/confidential/upload",
|
|
35
|
+
files=files)
|
|
36
|
+
resp.raise_for_status()
|
|
37
|
+
|
|
38
|
+
# def create_shared(self, person_id: datetime):
|
|
39
|
+
# resp = self.bob.session.post(url=f"{self.bob.base_url}people/{person_id}/shared/upload")
|
|
40
|
+
# resp.raise_for_status()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import requests
|
|
3
|
+
from src.bob.schemas.employment import EmploymentSchema
|
|
4
|
+
from src.utils.validation_tracker import enhanced_validate_data, ValidationTracker
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
class Employment:
|
|
8
|
+
def __init__(self, bob):
|
|
9
|
+
self.bob = bob
|
|
10
|
+
|
|
11
|
+
def get(self) -> Tuple[pd.DataFrame, ValidationTracker]:
|
|
12
|
+
request = requests.Request(method='GET',
|
|
13
|
+
url=f"{self.bob.base_url}bulk/people/employment")
|
|
14
|
+
data = self.bob.get_paginated_result(request)
|
|
15
|
+
df = pd.json_normalize(
|
|
16
|
+
data,
|
|
17
|
+
record_path='values',
|
|
18
|
+
meta=['employeeId']
|
|
19
|
+
)
|
|
20
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
21
|
+
valid_contracts, invalid_contracts, tracker = enhanced_validate_data(df=df, schema=EmploymentSchema, debug=True)
|
|
22
|
+
|
|
23
|
+
return valid_contracts, tracker
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from brynq_sdk.functions import Functions
|
|
4
|
+
from src.bob.schemas.payments import VariablePaymentSchema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Payments:
|
|
8
|
+
def __init__(self, bob):
|
|
9
|
+
self.bob = bob
|
|
10
|
+
|
|
11
|
+
def get(self, person_id: datetime) -> (pd.DataFrame, pd.DataFrame):
|
|
12
|
+
resp = self.bob.session.get(url=f"{self.bob.base_url}people/{person_id}/variable")
|
|
13
|
+
resp.raise_for_status()
|
|
14
|
+
data = resp.json()['values']
|
|
15
|
+
df = pd.json_normalize(
|
|
16
|
+
data,
|
|
17
|
+
record_path='values'
|
|
18
|
+
)
|
|
19
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
20
|
+
valid_payments, invalid_payments = Functions.validate_data(df=df, schema=VariablePaymentSchema, debug=True)
|
|
21
|
+
|
|
22
|
+
return valid_payments, invalid_payments
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from brynq_sdk.functions import Functions
|
|
3
|
+
|
|
4
|
+
from src.bob.bank import Bank
|
|
5
|
+
from src.bob.employment import Employment
|
|
6
|
+
from src.bob.salaries import Salaries
|
|
7
|
+
from src.bob.schemas.people import PeopleSchema
|
|
8
|
+
from src.bob.work import Work
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class People:
|
|
12
|
+
def __init__(self, bob):
|
|
13
|
+
self.bob = bob
|
|
14
|
+
self.salaries = Salaries(bob)
|
|
15
|
+
self.employment = Employment(bob)
|
|
16
|
+
self.bank = Bank(bob)
|
|
17
|
+
self.work = Work(bob)
|
|
18
|
+
|
|
19
|
+
def get(self) -> pd.DataFrame:
|
|
20
|
+
resp = self.bob.session.get(url=f"{self.bob.base_url}profiles")
|
|
21
|
+
# Bob sucks with default fields so you need to do a search call to retrieve additional fields.
|
|
22
|
+
additional_fields = [
|
|
23
|
+
"personal.birthDate",
|
|
24
|
+
"address.city",
|
|
25
|
+
"address.postCode",
|
|
26
|
+
"address.line1",
|
|
27
|
+
"address.line2",
|
|
28
|
+
"address.activeEffectiveDate",
|
|
29
|
+
# "address.fullAddress",
|
|
30
|
+
"address.country",
|
|
31
|
+
"home.legalGender",
|
|
32
|
+
"home.spouse.firstName",
|
|
33
|
+
"home.spouse.surname",
|
|
34
|
+
"home.spouse.birthDate",
|
|
35
|
+
"home.spouse.gender",
|
|
36
|
+
"internal.terminationReason",
|
|
37
|
+
"internal.terminationDate",
|
|
38
|
+
"internal.terminationType",
|
|
39
|
+
"employee.lastDayOfWork"
|
|
40
|
+
]
|
|
41
|
+
resp_additional_fields = self.bob.session.post(url=f"{self.bob.base_url}people/search",
|
|
42
|
+
json={
|
|
43
|
+
"fields": ["root.id"] + additional_fields,
|
|
44
|
+
"filters": []
|
|
45
|
+
})
|
|
46
|
+
df_extra_fields = pd.json_normalize(resp_additional_fields.json()['employees'])
|
|
47
|
+
resp.raise_for_status()
|
|
48
|
+
data = resp.json()
|
|
49
|
+
df = pd.json_normalize(data['employees'])
|
|
50
|
+
df = pd.merge(df, df_extra_fields[["id"] + additional_fields], left_on='id', right_on='id')
|
|
51
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
52
|
+
valid_people, invalid_people = Functions.validate_data(df=df, schema=PeopleSchema, debug=True)
|
|
53
|
+
|
|
54
|
+
return valid_people
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import requests
|
|
3
|
+
from brynq_sdk.functions import Functions
|
|
4
|
+
from src.bob.schemas.salary import SalarySchema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Salaries:
|
|
8
|
+
def __init__(self, bob):
|
|
9
|
+
self.bob = bob
|
|
10
|
+
|
|
11
|
+
def get(self) -> pd.DataFrame:
|
|
12
|
+
request = requests.Request(method='GET',
|
|
13
|
+
url=f"{self.bob.base_url}bulk/people/salaries",
|
|
14
|
+
params={"limit": 100})
|
|
15
|
+
data = self.bob.get_paginated_result(request)
|
|
16
|
+
df = pd.json_normalize(
|
|
17
|
+
data,
|
|
18
|
+
record_path='values',
|
|
19
|
+
meta=['employeeId']
|
|
20
|
+
)
|
|
21
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
22
|
+
valid_salaries, invalid_salaries = Functions.validate_data(df=df, schema=SalarySchema, debug=True)
|
|
23
|
+
|
|
24
|
+
return valid_salaries
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from brynq_sdk.functions import Functions
|
|
4
|
+
from src.bob.schemas.timeoff import TimeOffSchema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TimeOff:
|
|
8
|
+
def __init__(self, bob):
|
|
9
|
+
self.bob = bob
|
|
10
|
+
|
|
11
|
+
def get(self, since: datetime) -> (pd.DataFrame, pd.DataFrame):
|
|
12
|
+
resp = self.bob.session.get(url=f"{self.bob.base_url}timeoff/requests/changes",
|
|
13
|
+
params={'since': since.replace(tzinfo=timezone.utc).isoformat(timespec='milliseconds')})
|
|
14
|
+
resp.raise_for_status()
|
|
15
|
+
data = resp.json()['changes']
|
|
16
|
+
# data = self.bob.get_paginated_result(request)
|
|
17
|
+
df = pd.json_normalize(
|
|
18
|
+
data,
|
|
19
|
+
record_path='changes',
|
|
20
|
+
meta=['employeeId']
|
|
21
|
+
)
|
|
22
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
23
|
+
valid_timeoff, invalid_timeoff = Functions.validate_data(df=df, schema=TimeOffSchema, debug=True)
|
|
24
|
+
|
|
25
|
+
return valid_timeoff, invalid_timeoff
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import requests
|
|
3
|
+
from brynq_sdk.functions import Functions
|
|
4
|
+
|
|
5
|
+
from src.bob.schemas.work import WorkSchema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Work:
|
|
9
|
+
def __init__(self, bob):
|
|
10
|
+
self.bob = bob
|
|
11
|
+
|
|
12
|
+
def get(self) -> pd.DataFrame:
|
|
13
|
+
request = requests.Request(method='GET',
|
|
14
|
+
url=f"{self.bob.base_url}bulk/people/work")
|
|
15
|
+
data = self.bob.get_paginated_result(request)
|
|
16
|
+
df = pd.json_normalize(
|
|
17
|
+
data,
|
|
18
|
+
record_path='values',
|
|
19
|
+
meta=['employeeId']
|
|
20
|
+
)
|
|
21
|
+
df = self.bob.rename_camel_columns_to_snake_case(df)
|
|
22
|
+
|
|
23
|
+
valid_work, invalid_work = Functions.validate_data(df=df, schema=WorkSchema, debug=True)
|
|
24
|
+
|
|
25
|
+
return valid_work
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
brynq_sdk/bob/__init__.py
|
|
3
|
+
brynq_sdk/bob/bank.py
|
|
4
|
+
brynq_sdk/bob/company.py
|
|
5
|
+
brynq_sdk/bob/documents.py
|
|
6
|
+
brynq_sdk/bob/employment.py
|
|
7
|
+
brynq_sdk/bob/payments.py
|
|
8
|
+
brynq_sdk/bob/people.py
|
|
9
|
+
brynq_sdk/bob/salaries.py
|
|
10
|
+
brynq_sdk/bob/timeoff.py
|
|
11
|
+
brynq_sdk/bob/work.py
|
|
12
|
+
brynq_sdk_bob.egg-info/PKG-INFO
|
|
13
|
+
brynq_sdk_bob.egg-info/SOURCES.txt
|
|
14
|
+
brynq_sdk_bob.egg-info/dependency_links.txt
|
|
15
|
+
brynq_sdk_bob.egg-info/not-zip-safe
|
|
16
|
+
brynq_sdk_bob.egg-info/requires.txt
|
|
17
|
+
brynq_sdk_bob.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brynq_sdk
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name='brynq_sdk_bob',
|
|
6
|
+
version='0.0.1',
|
|
7
|
+
description='Bob wrapper from BrynQ',
|
|
8
|
+
long_description='Bob wrapper from BrynQ',
|
|
9
|
+
author='BrynQ',
|
|
10
|
+
author_email='support@brynq.com',
|
|
11
|
+
packages=["brynq_sdk.bob"],
|
|
12
|
+
license='BrynQ License',
|
|
13
|
+
install_requires=[
|
|
14
|
+
'brynq-sdk-brynq>=1',
|
|
15
|
+
'pandas>=2.2.0,<3.0.0',
|
|
16
|
+
],
|
|
17
|
+
zip_safe=False,
|
|
18
|
+
)
|