diff --git a/airbyte-integrations/connectors/source-bamboo-hr/Dockerfile b/airbyte-integrations/connectors/source-bamboo-hr/Dockerfile index 238106b2127f..5cb1ecec5872 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/Dockerfile +++ b/airbyte-integrations/connectors/source-bamboo-hr/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.10-slim # Bash is installed for more convenient debugging. RUN apt-get update && apt-get install -y bash && rm -rf /var/lib/apt/lists/* diff --git a/airbyte-integrations/connectors/source-bamboo-hr/integration_tests/configured_catalog.json b/airbyte-integrations/connectors/source-bamboo-hr/integration_tests/configured_catalog.json index 3138ca460460..b5f8e95d604c 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/integration_tests/configured_catalog.json +++ b/airbyte-integrations/connectors/source-bamboo-hr/integration_tests/configured_catalog.json @@ -2,24 +2,646 @@ "streams": [ { "stream": { - "name": "custom_reports_stream", + "name": "meta_tables_stream", "json_schema": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "zipcode": { - "type": ["null", "string"] + "alias": { + "type": [ + "string" + ] + }, + "fields": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "alias", + "type" + ] + } + ] + }, + "required": [ + "alias", + "fields" + ] + } + }, + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "append_dedup" + }, + { + "stream": { + "name": "custom_reports_stream", + "json_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": [ + "object" + ], + "additionalProperties": true, + "required": [], + "properties": { + "acaStatusCategory": { + "type": [ + "null", + "string" + ] + }, + "address1": { + "type": [ + "null", + "string" + ] + }, + "address2": { + "type": [ + "null", + "string" + ] + }, + "age": { + "type": [ + "null", + "string" + ] + }, + "bestEmail": { + "type": [ + "null", + "string" + ] + }, + "birthday": { + "type": [ + "null", + "string" + ] + }, + "bonusAmount": { + "type": [ + "null", + "string" + ] + }, + "bonusComment": { + "type": [ + "null", + "string" + ] + }, + "bonusDate": { + "type": [ + "null", + "string" + ] + }, + "bonusReason": { + "type": [ + "null", + "string" + ] + }, + "city": { + "type": [ + "null", + "string" + ] + }, + "commissionAmount": { + "type": [ + "null", + "string" + ] + }, + "commissionComment": { + "type": [ + "null", + "string" + ] + }, + "commissionDate": { + "type": [ + "null", + "string" + ] + }, + "commisionDate": { + "type": [ + "null", + "string" + ] + }, + "country": { + "type": [ + "null", + "string" + ] + }, + "createdByUserId": { + "type": [ + "null", + "string" + ] + }, + "dateOfBirth": { + "type": [ + "null", + "string" + ] + }, + "department": { + "type": [ + "null", + "string" + ] + }, + "division": { + "type": [ + "null", + "string" + ] + }, + "eeo": { + "type": [ + "null", + "string" + ] + }, + "employeeNumber": { + "type": [ + "null", + "string" + ] + }, + "employmentHistoryStatus": { + "type": [ + "null", + "string" + ] + }, + "ethnicity": { + "type": [ + "null", + "string" + ] + }, + "exempt": { + "type": [ + "null", + "string" + ] + }, + "firstName": { + "type": [ + "null", + "string" + ] + }, + "flsaCode": { + "type": [ + "null", + "string" + ] + }, + "fullName1": { + "type": [ + "null", + "string" + ] + }, + "fullName2": { + "type": [ + "null", + "string" + ] + }, + "fullName3": { + "type": [ + "null", + "string" + ] + }, + "fullName4": { + "type": [ + "null", + "string" + ] + }, + "fullName5": { + "type": [ + "null", + "string" + ] + }, + "displayName": { + "type": [ + "null", + "string" + ] + }, + "gender": { + "type": [ + "null", + "string" + ] + }, + "hireDate": { + "type": [ + "null", + "string" + ] + }, + "originalHireDate": { + "type": [ + "null", + "string" + ] + }, + "homeEmail": { + "type": [ + "null", + "string" + ] + }, + "homePhone": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "string" + ] + }, + "isPhotoUploaded": { + "type": [ + "null", + "string" + ] + }, + "jobTitle": { + "type": [ + "null", + "string" + ] + }, + "lastChanged": { + "type": [ + "null", + "string" + ] + }, + "lastName": { + "type": [ + "null", + "string" + ] + }, + "location": { + "type": [ + "null", + "string" + ] + }, + "maritalStatus": { + "type": [ + "null", + "string" + ] + }, + "middleName": { + "type": [ + "null", + "string" + ] + }, + "mobilePhone": { + "type": [ + "null", + "string" + ] + }, + "nationalId": { + "type": [ + "null", + "string" + ] + }, + "nationality": { + "type": [ + "null", + "string" + ] + }, + "nin": { + "type": [ + "null", + "string" + ] + }, + "payChangeReason": { + "type": [ + "null", + "string" + ] + }, + "payGroup": { + "type": [ + "null", + "string" + ] + }, + "payGroupId": { + "type": [ + "null", + "string" + ] + }, + "payRate": { + "type": [ + "null", + "string" + ] + }, + "payRateEffectiveDate": { + "type": [ + "null", + "string" + ] + }, + "payType": { + "type": [ + "null", + "string" + ] + }, + "paidPer": { + "type": [ + "null", + "string" + ] + }, + "paySchedule": { + "type": [ + "null", + "string" + ] + }, + "payScheduleId": { + "type": [ + "null", + "string" + ] + }, + "payFrequency": { + "type": [ + "null", + "string" + ] + }, + "includeInPayroll": { + "type": [ + "null", + "string" + ] + }, + "timeTrackingEnabled": { + "type": [ + "null", + "string" + ] + }, + "preferredName": { + "type": [ + "null", + "string" + ] + }, + "ssn": { + "type": [ + "null", + "string" + ] + }, + "sin": { + "type": [ + "null", + "string" + ] + }, + "standardHoursPerWeek": { + "type": [ + "null", + "string" + ] + }, + "state": { + "type": [ + "null", + "string" + ] + }, + "stateCode": { + "type": [ + "null", + "string" + ] + }, + "status": { + "type": [ + "null", + "string" + ] + }, + "supervisor": { + "type": [ + "null", + "string" + ] + }, + "supervisorId": { + "type": [ + "null", + "string" + ] + }, + "supervisorEId": { + "type": [ + "null", + "string" + ] + }, + "supervisorEmail": { + "type": [ + "null", + "string" + ] }, "terminationDate": { - "type": ["null", "string"] + "type": [ + "null", + "string" + ] + }, + "workEmail": { + "type": [ + "null", + "string" + ] + }, + "workPhone": { + "type": [ + "null", + "string" + ] + }, + "workPhonePlusExtension": { + "type": [ + "null", + "string" + ] + }, + "workPhoneExtension": { + "type": [ + "null", + "string" + ] + }, + "zipcode": { + "type": [ + "null", + "string" + ] + } + } + }, + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "append_dedup" + }, + { + "stream": { + "name": "meta_fields_stream", + "json_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": [ + "object" + ], + "required": [ + "id", + "name", + "type" + ], + "properties": { + "id": { + "type": [ + "string" + ] + }, + "name": { + "type": [ + "string" + ] + }, + "type": { + "type": [ + "string" + ] + }, + "alias": { + "type": [ + "string" + ] + }, + "deprecated": { + "type": [ + "boolean" + ] + } + } + }, + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "append_dedup" + }, + { + "stream": { + "name": "tables_stream", + "json_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": [ + "object" + ], + "additionalProperties": true, + "required": [], + "properties": { + "id": { + "type": [ + "integer", + "string" + ] + }, + "employeeId": { + "type": [ + "integer", + "string" + ] } } }, - "supported_sync_modes": ["full_refresh"], - "supported_destination_sync_modes": ["overwrite", "append_dedup"] + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] }, "sync_mode": "full_refresh", "destination_sync_mode": "append_dedup" } ] -} +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-bamboo-hr/sample_files/configured_catalog.json b/airbyte-integrations/connectors/source-bamboo-hr/sample_files/configured_catalog.json index 2486e6964fb1..72daf969d2bc 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/sample_files/configured_catalog.json +++ b/airbyte-integrations/connectors/source-bamboo-hr/sample_files/configured_catalog.json @@ -2,96 +2,644 @@ "streams": [ { "stream": { - "name": "employees_directory_stream", + "name": "meta_tables_stream", "json_schema": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "id": { - "type": ["null", "string"] + "alias": { + "type": [ + "string" + ] }, - "displayName": { - "type": ["null", "string"] + "fields": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "alias", + "type" + ] + } + ] + }, + "required": [ + "alias", + "fields" + ] + } + }, + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "append_dedup" + }, + { + "stream": { + "name": "custom_reports_stream", + "json_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": [ + "object" + ], + "required": [], + "properties": { + "acaStatusCategory": { + "type": [ + "null", + "string" + ] + }, + "address1": { + "type": [ + "null", + "string" + ] + }, + "address2": { + "type": [ + "null", + "string" + ] + }, + "age": { + "type": [ + "null", + "string" + ] + }, + "bestEmail": { + "type": [ + "null", + "string" + ] + }, + "birthday": { + "type": [ + "null", + "string" + ] + }, + "bonusAmount": { + "type": [ + "null", + "string" + ] + }, + "bonusComment": { + "type": [ + "null", + "string" + ] + }, + "bonusDate": { + "type": [ + "null", + "string" + ] + }, + "bonusReason": { + "type": [ + "null", + "string" + ] + }, + "city": { + "type": [ + "null", + "string" + ] + }, + "commissionAmount": { + "type": [ + "null", + "string" + ] + }, + "commissionComment": { + "type": [ + "null", + "string" + ] + }, + "commissionDate": { + "type": [ + "null", + "string" + ] + }, + "commisionDate": { + "type": [ + "null", + "string" + ] + }, + "country": { + "type": [ + "null", + "string" + ] + }, + "createdByUserId": { + "type": [ + "null", + "string" + ] + }, + "dateOfBirth": { + "type": [ + "null", + "string" + ] + }, + "department": { + "type": [ + "null", + "string" + ] + }, + "division": { + "type": [ + "null", + "string" + ] + }, + "eeo": { + "type": [ + "null", + "string" + ] + }, + "employeeNumber": { + "type": [ + "null", + "string" + ] + }, + "employmentHistoryStatus": { + "type": [ + "null", + "string" + ] + }, + "ethnicity": { + "type": [ + "null", + "string" + ] + }, + "exempt": { + "type": [ + "null", + "string" + ] }, "firstName": { - "type": ["null", "string"] + "type": [ + "null", + "string" + ] }, - "lastName": { - "type": ["null", "string"] + "flsaCode": { + "type": [ + "null", + "string" + ] }, - "preferredName": { - "type": ["null", "string"] + "fullName1": { + "type": [ + "null", + "string" + ] + }, + "fullName2": { + "type": [ + "null", + "string" + ] + }, + "fullName3": { + "type": [ + "null", + "string" + ] + }, + "fullName4": { + "type": [ + "null", + "string" + ] + }, + "fullName5": { + "type": [ + "null", + "string" + ] + }, + "displayName": { + "type": [ + "null", + "string" + ] + }, + "gender": { + "type": [ + "null", + "string" + ] + }, + "hireDate": { + "type": [ + "null", + "string" + ] + }, + "originalHireDate": { + "type": [ + "null", + "string" + ] + }, + "homeEmail": { + "type": [ + "null", + "string" + ] + }, + "homePhone": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "string" + ] + }, + "isPhotoUploaded": { + "type": [ + "null", + "string" + ] }, "jobTitle": { - "type": ["null", "string"] + "type": [ + "null", + "string" + ] }, - "workPhone": { - "type": ["null", "string"] + "lastChanged": { + "type": [ + "null", + "string" + ] + }, + "lastName": { + "type": [ + "null", + "string" + ] + }, + "location": { + "type": [ + "null", + "string" + ] + }, + "maritalStatus": { + "type": [ + "null", + "string" + ] + }, + "middleName": { + "type": [ + "null", + "string" + ] }, "mobilePhone": { - "type": ["null", "string"] + "type": [ + "null", + "string" + ] }, - "workEmail": { - "type": ["null", "string"] + "nationalId": { + "type": [ + "null", + "string" + ] }, - "department": { - "type": ["null", "string"] + "nationality": { + "type": [ + "null", + "string" + ] }, - "location": { - "type": ["null", "string"] + "nin": { + "type": [ + "null", + "string" + ] }, - "division": { - "type": ["null", "string"] + "payChangeReason": { + "type": [ + "null", + "string" + ] }, - "linkedIn": { - "type": ["null", "string"] + "payGroup": { + "type": [ + "null", + "string" + ] }, - "pronouns": { - "type": ["null", "string"] + "payGroupId": { + "type": [ + "null", + "string" + ] }, - "workPhoneExtension": { - "type": ["null", "string"] + "payRate": { + "type": [ + "null", + "string" + ] + }, + "payRateEffectiveDate": { + "type": [ + "null", + "string" + ] + }, + "payType": { + "type": [ + "null", + "string" + ] + }, + "paidPer": { + "type": [ + "null", + "string" + ] + }, + "paySchedule": { + "type": [ + "null", + "string" + ] + }, + "payScheduleId": { + "type": [ + "null", + "string" + ] + }, + "payFrequency": { + "type": [ + "null", + "string" + ] + }, + "includeInPayroll": { + "type": [ + "null", + "string" + ] + }, + "timeTrackingEnabled": { + "type": [ + "null", + "string" + ] + }, + "preferredName": { + "type": [ + "null", + "string" + ] + }, + "ssn": { + "type": [ + "null", + "string" + ] + }, + "sin": { + "type": [ + "null", + "string" + ] + }, + "standardHoursPerWeek": { + "type": [ + "null", + "string" + ] + }, + "state": { + "type": [ + "null", + "string" + ] + }, + "stateCode": { + "type": [ + "null", + "string" + ] + }, + "status": { + "type": [ + "null", + "string" + ] }, "supervisor": { - "type": ["null", "string"] + "type": [ + "null", + "string" + ] + }, + "supervisorId": { + "type": [ + "null", + "string" + ] }, - "photoUploaded": { - "type": ["null", "boolean"] + "supervisorEId": { + "type": [ + "null", + "string" + ] }, - "photoUrl": { - "type": ["null", "string"] + "supervisorEmail": { + "type": [ + "null", + "string" + ] }, - "canUploadPhoto": { - "type": ["null", "boolean"] + "terminationDate": { + "type": [ + "null", + "string" + ] + }, + "workEmail": { + "type": [ + "null", + "string" + ] + }, + "workPhone": { + "type": [ + "null", + "string" + ] + }, + "workPhonePlusExtension": { + "type": [ + "null", + "string" + ] + }, + "workPhoneExtension": { + "type": [ + "null", + "string" + ] + }, + "zipcode": { + "type": [ + "null", + "string" + ] } } }, - "supported_sync_modes": ["full_refresh"], - "supported_destination_sync_modes": ["overwrite", "append_dedup"] + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] }, "sync_mode": "full_refresh", "destination_sync_mode": "append_dedup" }, { "stream": { - "name": "custom_reports_stream", + "name": "meta_fields_stream", "json_schema": { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", + "type": [ + "object" + ], + "required": [ + "id", + "name", + "type" + ], "properties": { - "zipcode": { - "type": ["null", "string"] + "id": { + "type": [ + "string" + ] }, - "terminationDate": { - "type": ["null", "string"] + "name": { + "type": [ + "string" + ] + }, + "type": { + "type": [ + "string" + ] + }, + "alias": { + "type": [ + "string" + ] + }, + "deprecated": { + "type": [ + "boolean" + ] + } + } + }, + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] + }, + "sync_mode": "full_refresh", + "destination_sync_mode": "append_dedup" + }, + { + "stream": { + "name": "tables_stream", + "json_schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": [ + "object" + ], + "required": [], + "properties": { + "id": { + "type": [ + "int", + "string" + ] + }, + "employeeId": { + "type": [ + "int", + "string" + ] } } }, - "supported_sync_modes": ["full_refresh"], - "supported_destination_sync_modes": ["overwrite", "append_dedup"] + "supported_sync_modes": [ + "full_refresh" + ], + "supported_destination_sync_modes": [ + "overwrite", + "append_dedup" + ] }, "sync_mode": "full_refresh", "destination_sync_mode": "append_dedup" } ] -} +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/exception.py b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/exception.py index ec1c757bc428..616e0f180e97 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/exception.py +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/exception.py @@ -9,16 +9,6 @@ class BambooHrError(Exception): def __init__(self): super().__init__(self.message) - -class NullFieldsError(BambooHrError): - message = "Field `custom_reports_fields` cannot be empty if `custom_reports_include_default_fields` is false." - - class AvailableFieldsAccessDeniedError(BambooHrError): message = "You hasn't access to any report fields. Please check your access level." - -class CustomFieldsAccessDeniedError(Exception): - def __init__(self, denied_fields): - self.message = f"Access to fields: {', '.join(denied_fields)} - denied. Please check your access level." - super().__init__(self.message) diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/custom_reports_stream.json b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/custom_reports_stream.json index 8e9482fea238..ec6dc1e33135 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/custom_reports_stream.json +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/custom_reports_stream.json @@ -1,246 +1,14 @@ { - "type": ["null", "object"], + "$schema": "http://json-schema.org/draft-07/schema#", + "type": [ "object"], + "additionalProperties": true, "required": [], "properties": { - "acaStatus": { - "type": ["null", "string"] - }, - "acaStatusCategory": { - "type": ["null", "string"] - }, - "address1": { - "type": ["null", "string"] - }, - "address2": { - "type": ["null", "string"] - }, - "age": { - "type": ["null", "string"] - }, - "bestEmail": { - "type": ["null", "string"] - }, - "birthday": { - "type": ["null", "string"] - }, - "bonusAmount": { - "type": ["null", "string"] - }, - "bonusComment": { - "type": ["null", "string"] - }, - "bonusDate": { - "type": ["null", "string"] - }, - "bonusReason": { - "type": ["null", "string"] - }, - "city": { - "type": ["null", "string"] - }, - "commissionAmount": { - "type": ["null", "string"] - }, - "commissionComment": { - "type": ["null", "string"] - }, - "commissionDate": { - "type": ["null", "string"] - }, - "commisionDate": { - "type": ["null", "string"] - }, - "country": { - "type": ["null", "string"] - }, - "createdByUserId": { - "type": ["null", "string"] - }, - "dateOfBirth": { - "type": ["null", "string"] - }, - "department": { - "type": ["null", "string"] - }, - "division": { - "type": ["null", "string"] - }, - "eeo": { - "type": ["null", "string"] - }, - "employeeNumber": { - "type": ["null", "string"] - }, - "employmentHistoryStatus": { - "type": ["null", "string"] - }, - "ethnicity": { - "type": ["null", "string"] - }, - "exempt": { - "type": ["null", "string"] - }, - "firstName": { - "type": ["null", "string"] - }, - "flsaCode": { - "type": ["null", "string"] - }, - "fullName1": { - "type": ["null", "string"] - }, - "fullName2": { - "type": ["null", "string"] - }, - "fullName3": { - "type": ["null", "string"] - }, - "fullName4": { - "type": ["null", "string"] - }, - "fullName5": { - "type": ["null", "string"] - }, - "displayName": { - "type": ["null", "string"] - }, - "gender": { - "type": ["null", "string"] - }, - "hireDate": { - "type": ["null", "string"] - }, - "originalHireDate": { - "type": ["null", "string"] - }, - "homeEmail": { - "type": ["null", "string"] - }, - "homePhone": { - "type": ["null", "string"] - }, "id": { - "type": ["null", "string"] - }, - "isPhotoUploaded": { - "type": ["null", "string"] - }, - "jobTitle": { - "type": ["null", "string"] - }, - "lastChanged": { - "type": ["null", "string"] - }, - "lastName": { - "type": ["null", "string"] - }, - "location": { - "type": ["null", "string"] - }, - "maritalStatus": { - "type": ["null", "string"] - }, - "middleName": { - "type": ["null", "string"] - }, - "mobilePhone": { - "type": ["null", "string"] - }, - "nationalId": { - "type": ["null", "string"] - }, - "nationality": { - "type": ["null", "string"] - }, - "nin": { - "type": ["null", "string"] - }, - "payChangeReason": { - "type": ["null", "string"] - }, - "payGroup": { - "type": ["null", "string"] - }, - "payGroupId": { - "type": ["null", "string"] - }, - "payRate": { - "type": ["null", "string"] - }, - "payRateEffectiveDate": { - "type": ["null", "string"] - }, - "payType": { - "type": ["null", "string"] - }, - "paidPer": { - "type": ["null", "string"] - }, - "paySchedule": { - "type": ["null", "string"] - }, - "payScheduleId": { - "type": ["null", "string"] - }, - "payFrequency": { - "type": ["null", "string"] - }, - "includeInPayroll": { - "type": ["null", "string"] - }, - "timeTrackingEnabled": { - "type": ["null", "string"] - }, - "preferredName": { - "type": ["null", "string"] - }, - "ssn": { - "type": ["null", "string"] - }, - "sin": { - "type": ["null", "string"] - }, - "standardHoursPerWeek": { - "type": ["null", "string"] - }, - "state": { - "type": ["null", "string"] - }, - "stateCode": { - "type": ["null", "string"] - }, - "status": { - "type": ["null", "string"] - }, - "supervisor": { - "type": ["null", "string"] - }, - "supervisorId": { - "type": ["null", "string"] - }, - "supervisorEId": { - "type": ["null", "string"] - }, - "supervisorEmail": { - "type": ["null", "string"] - }, - "terminationDate": { - "type": ["null", "string"] - }, - "workEmail": { - "type": ["null", "string"] - }, - "workPhone": { - "type": ["null", "string"] - }, - "workPhonePlusExtension": { - "type": ["null", "string"] - }, - "workPhoneExtension": { - "type": ["null", "string"] + "type": [ "string" ] }, - "zipcode": { - "type": ["null", "string"] + "data": { + "type": [ "object" ] } } } diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/employees_directory_stream.json b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/employees_directory_stream.json index 0cd1291f31f3..0e856bba62ca 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/employees_directory_stream.json +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/employees_directory_stream.json @@ -1,5 +1,7 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "type": ["null", "object"], + "additionalProperties": true, "required": [], "properties": { "id": { diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/meta_fields_stream.json b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/meta_fields_stream.json new file mode 100644 index 000000000000..e714473e4560 --- /dev/null +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/meta_fields_stream.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": ["object"], + "required": ["id", "name", "type"], + "properties": { + "id": { + "type": ["string","integer"] + }, + "name": { + "type": ["string"] + }, + "type": { + "type": ["string"] + }, + "alias": { + "type": ["string"] + }, + "deprecated": { + "type": ["boolean"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/meta_tables_stream.json b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/meta_tables_stream.json new file mode 100644 index 000000000000..6aafafd68427 --- /dev/null +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/meta_tables_stream.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "alias", + "fields" + ], + "properties": { + "alias": { + "type": [ + "string" + ] + }, + "fields": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "alias", + "type" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/tables_stream.json b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/tables_stream.json new file mode 100644 index 000000000000..e6717d2febe4 --- /dev/null +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/schemas/tables_stream.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": [ + "object" + ], + "required": [ + "id", + "employeeId", + "table_name", + "data" + ], + "properties": { + "id": { + "type": [ "string", "integer" ] + }, + "employee_id": { + "type": [ + "string", "integer" + ] + }, + "table_name": { + "type": [ "string"] + }, + "data": { + "type": [ "object" ], + "additionalProperties": true + } + } +} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/source.py b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/source.py index 66e893b53743..94335dd39299 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/source.py +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/source.py @@ -6,17 +6,33 @@ import base64 import logging from abc import ABC -from typing import Any, Iterable, List, Mapping, Optional, Tuple +from typing import ( + Any, + Iterable, + List, + Mapping, + Optional, + Tuple, + NamedTuple, + Union, + Dict, +) +from typing import cast import requests +from requests.exceptions import HTTPError from airbyte_cdk.models.airbyte_protocol import SyncMode from airbyte_cdk.sources import AbstractSource from airbyte_cdk.sources.streams import Stream from airbyte_cdk.sources.streams.http import HttpStream from airbyte_cdk.sources.streams.http.requests_native_auth import TokenAuthenticator -from .exception import AvailableFieldsAccessDeniedError, CustomFieldsAccessDeniedError, NullFieldsError -from .utils import convert_custom_reports_fields_to_list, validate_custom_fields +from .exception import ( + AvailableFieldsAccessDeniedError, +) +from .utils import ( + chunk_iterable, +) class BambooHrStream(HttpStream, ABC): @@ -26,146 +42,440 @@ def __init__(self, config: Mapping[str, Any]) -> None: @property def url_base(self) -> str: - return f"https://api.bamboohr.com/api/gateway.php/{self.config['subdomain']}/v1/" - - def request_headers( - self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + return ( + f"https://api.bamboohr.com/api/gateway.php/{self.config['subdomain']}/v1/" + ) + + def request_headers( # type: ignore + self, + stream_state: Mapping[str, Any], + stream_slice: Optional[Mapping[str, Any]] = None, + next_page_token: Optional[Mapping[str, Any]] = None, ) -> Mapping[str, Any]: return {"Accept": "application/json"} - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: + def next_page_token( + self, response: requests.Response + ) -> Optional[Mapping[str, Any]]: """ BambooHR does not support pagination. """ pass +class MetaTablesStream(BambooHrStream): + primary_key = None # type: ignore + + def path(self, **kwargs) -> str: # type: ignore + return "meta/tables" + + def parse_response( + self, + response: requests.Response, + **kwarg, # type: ignore + ) -> Iterable[Mapping[str, Any]]: + yield from response.json() + + +class BambooMetaField(NamedTuple): + """Immutable typed representation of what is returned from the meta/fields + endpoint.""" + + id: Union[int, str] + name: str + type: str + alias: Optional[str] = None + deprecated: Optional[bool] = None + + class MetaFieldsStream(BambooHrStream): - primary_key = None + primary_key = None # type: ignore - def path(self, **kwargs) -> str: + def path(self, **kwargs) -> str: # type: ignore return "meta/fields" - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + def parse_response( + self, + response: requests.Response, + **kwargs, # type: ignore + ) -> Iterable[Mapping[str, Any]]: yield from response.json() -class EmployeesDirectoryStream(BambooHrStream): - primary_key = "id" +class BambooMetaTableField(NamedTuple): + """Immutable typed representation of the field data returned from the meta/tables + endpoint.""" - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - yield from response.json()["employees"] + id: int + name: str + alias: str + type: str - def path(self, **kwargs) -> str: - return "employees/directory" +class BambooMetaTable(NamedTuple): + """Immutable typed representation of what is returned from the meta/tables + endpoint.""" -class CustomReportsStream(BambooHrStream): - primary_key = None + alias: str + fields: List[BambooMetaTableField] - def __init__(self, *args, **kwargs): - self._schema = None - super().__init__(*args, **kwargs) - @property - def schema(self): - if not self._schema: - self._schema = self.get_json_schema() - return self._schema - - def _get_json_schema_from_config(self): - if self.config.get("custom_reports_fields"): - properties = { - field.strip(): {"type": ["null", "string"]} - for field in convert_custom_reports_fields_to_list(self.config.get("custom_reports_fields", "")) - } +class TablesStream(BambooHrStream): + primary_key = None # type: ignore + raise_on_http_errors = False # type: ignore + skip_http_status_codes = [ + requests.codes.NOT_FOUND, + ] + + @staticmethod + def convert_raw_meta_table_to_typed( + raw_meta_table: Mapping[str, Any] + ) -> BambooMetaTable: + """ + Converts a raw meta table to a typed BambooMetaTable. + """ + return BambooMetaTable( + alias=raw_meta_table.get("alias", ""), + fields=[ + BambooMetaTableField(**field) + for field in raw_meta_table.get("fields", []) + ], + ) + + def read_records( + self, + sync_mode: SyncMode, + cursor_field: List[str] | None = None, + stream_slice: Mapping[str, Any] | None = None, + stream_state: Mapping[str, Any] | None = None, + ) -> Iterable[Mapping[str, Any]]: + if stream_slice is not None: + table_name = stream_slice["table"] + for record in super().read_records( + sync_mode, cursor_field, stream_slice, stream_state + ): + # If the record is empty, skip it. + # This may occur if parse_response yields an empty record, + # which can happen if the response is not 2xx. + if record == {} or not isinstance(record,Mapping): + self.logger.warn( + f"Empty record or non-map record encountered in TablesStream. Record: {record}") + continue + else: + # Augment the record for easier lookup/better + # performance in the destination. + new_record : Mapping[str,Any] = { + "id": record["id"], + "employee_id": record["employeeId"], + "table_name": table_name, + "data": record, + } + yield new_record else: - properties = {} - return { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": properties, - } + self.logger.error("Stream slice is None in TablesStream.") + return iter([]) + + def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]: # type: ignore + # Each table has an 'alias' field that we use to grab + # all values. See `path` method for how it's used in the URL. + available_tables: List[BambooMetaTable] = self.config.get( + "available_tables", [] + ) + for meta_table in available_tables: # Add default value of empty list + table = meta_table.alias + yield {"table": table} + + def path(self, stream_slice: Mapping[str, Any], **kwargs) -> str: # type: ignore + target_table = stream_slice["table"] + return f"employees/all/tables/{target_table}" + + def parse_response( + self, + response: requests.Response, + stream_state: Mapping[str, Any] | None = None, + stream_slice: Mapping[str, Any] | None = None, + **kwargs, # type: ignore + ) -> Iterable[Mapping[str, Any]]: + try: + # This will raise an exception if the response is not 2xx + response.raise_for_status() + yield from response.json() + except HTTPError as e: + # Check to see if this error code is one we expect. + # If so, raise an error. + if not ( + self.skip_http_status_codes + and e.response.status_code in self.skip_http_status_codes + ): + raise e + + # Otherwise, just log a warning. + self.logger.warning( + f"Stream `{self.name}`. An error occurred, details: {e}. Skipping for now." + ) + yield {} - def _get_json_schema_from_file(self): - return super().get_json_schema() - @staticmethod - def _union_schemas(schema1, schema2): - schema1["properties"] = {**schema1["properties"], **schema2["properties"]} - return schema1 +class CustomReportsStream(BambooHrStream): + primary_key = None # type: ignore - def get_json_schema(self) -> Mapping[str, Any]: - """ - Returns the JSON schema. + def __init__(self, *args, **kwargs): # type: ignore + super().__init__(*args, **kwargs) # type: ignore - The final schema is constructed by first generating a schema for the fields - in the config and, if default fields should be included, adding these to the - schema. - """ - schema = self._get_json_schema_from_config() - if self.config.get("custom_reports_include_default_fields"): - default_schema = self._get_json_schema_from_file() - schema = self._union_schemas(default_schema, schema) - return schema + def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]: # type: ignore + available_fields: List[str] = self.config.get("available_fields", []) + for fields in chunk_iterable(available_fields, 100): + yield {"fields": fields} - def path(self, **kwargs) -> str: + def path(self, **kwargs) -> str: # type: ignore return "reports/custom" + def read_records( + self, + sync_mode: SyncMode, + cursor_field: List[str] | None = None, + stream_slice: Mapping[str, Any] | None = None, + stream_state: Mapping[str, Any] | None = None, + ) -> Iterable[Mapping[str, Any]]: + for record in super().read_records( + sync_mode, cursor_field, stream_slice, stream_state + ): + # Augment the record with the table name. + if record == {} or not isinstance(record,Mapping): + self.logger.warn("Empty record or non-map record encountered in CustomReportsStream.") + continue + else: + new_record = { + "id": record["id"], + "data": record, + } + yield new_record + @property def http_method(self) -> str: return "POST" - def request_body_json(self, **kwargs) -> Optional[Mapping]: - return {"title": "Airbyte", "fields": list(self.schema["properties"].keys())} + @staticmethod + def get_default_bamboo_fields() -> List[str]: + # As per https://documentation.bamboohr.com/docs/list-of-field-names + return [ + "acaStatusCategory" + "address1" + "address2" + "age" + "bestEmail" + "birthday" + "bonusAmount" + "bonusComment" + "bonusDate" + "bonusReason" + "city" + "commissionAmount" + "commissionComment" + "commissionDate" + "commisionDate" + "country" + "createdByUserId" + "dateOfBirth" + "department" + "division" + "eeo" + "employeeNumber" + "employmentHistoryStatus" + "ethnicity" + "exempt" + "firstName" + "flsaCode" + "fullName1" + "fullName2" + "fullName3" + "fullName4" + "fullName5" + "displayName" + "gender" + "hireDate" + "originalHireDate" + "homeEmail" + "homePhone" + "id" + "isPhotoUploaded" + "jobTitle" + "lastChanged" + "lastName" + "location" + "maritalStatus" + "middleName" + "mobilePhone" + "nationalId" + "nationality" + "nin" + "payChangeReason" + "payGroup" + "payGroupId" + "payRate" + "payRateEffectiveDate" + "payType" + "paidPer" + "paySchedule" + "payScheduleId" + "payFrequency" + "includeInPayroll" + "timeTrackingEnabled" + "preferredName" + # This is supported, but we don't want it. + # "ssn" + "sin" + "standardHoursPerWeek" + "state" + "stateCode" + "status" + "supervisor" + "supervisorId" + "supervisorEId" + "supervisorEmail" + "terminationDate" + "workEmail" + "workPhone" + "workPhonePlusExtension" + "workPhoneExtension" + "zipcode" + ] - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + @staticmethod + def convert_field_to_id(field: BambooMetaField) -> str: + """Converts a BambooMetaField to an id for the custom report endpoint.""" + + # The reports/custom endpoint takes a list of fields, each of + # which can be referred to by its alias (if one exists) or + # by its stringified id. + if field.alias is None: + return str(id) + else: + return field.alias + + def request_body_json( # type: ignore + self, + stream_slice: Mapping[str, Any] | None = None, + **kwargs, # type: ignore + ) -> Optional[Mapping[str, Any]]: + fields: List[str] = stream_slice["fields"] if stream_slice is not None else [] + return {"title": "Airbyte", "fields": fields} + + def parse_response( + self, + response: requests.Response, + **kwargs, # type: ignore + ) -> Iterable[Mapping[str, Any]]: yield from response.json()["employees"] +class EmployeesDirectoryStream(BambooHrStream): + """ + This is not currently in use as per + https://documentation.bamboohr.com/reference/get-employees-directory-1 + """ + + primary_key = "id" # type: ignore + + def parse_response( + self, + response: requests.Response, + **kwargs, # type: ignore + ) -> Iterable[Mapping[str, Any]]: + yield from response.json()["employees"] + + def path(self, **kwargs) -> str: # type: ignore + return "employees/directory" + + class SourceBambooHr(AbstractSource): @staticmethod - def _get_authenticator(api_key): + def _get_authenticator(api_key: str): """ Returns a TokenAuthenticator. The API token is concatenated with `:x` and the resulting string is base-64 encoded. See https://documentation.bamboohr.com/docs#authentication """ - return TokenAuthenticator(token=base64.b64encode(f"{api_key}:x".encode("utf-8")).decode("utf-8"), auth_method="Basic") + return TokenAuthenticator( + token=base64.b64encode(f"{api_key}:x".encode("utf-8")).decode("utf-8"), + auth_method="Basic", + ) @staticmethod - def add_authenticator_to_config(config: Mapping[str, Any]) -> Mapping[str, Any]: + def add_authenticator_to_config(config: Dict[str, Any]) -> Dict[str, Any]: """ Adds an authenticator entry to the config and returns the config. """ config["authenticator"] = SourceBambooHr._get_authenticator(config["api_key"]) return config - def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Optional[Any]]: + def check_connection( + self, logger: logging.Logger, config: Mapping[str, Any] + ) -> Tuple[bool, Optional[Any]]: """ Verifies the config and attempts to fetch the fields from the meta/fields endpoint. """ - config = SourceBambooHr.add_authenticator_to_config(config) - - if not config.get("custom_reports_fields") and not config.get("custom_reports_include_default_fields"): - return False, NullFieldsError() - - available_fields = MetaFieldsStream(config).read_records(sync_mode=SyncMode.full_refresh) - custom_fields = convert_custom_reports_fields_to_list(config.get("custom_reports_fields", "")) - denied_fields = validate_custom_fields(custom_fields, available_fields) + config = SourceBambooHr.add_authenticator_to_config( + cast(Dict[str, Any], config) + ) - if denied_fields: - return False, CustomFieldsAccessDeniedError(denied_fields) + available_fields = MetaFieldsStream(config).read_records( + sync_mode=SyncMode.full_refresh + ) try: - next(available_fields) + # Check to see that we get some fields back. + next(available_fields) # type: ignore return True, None except StopIteration: return False, AvailableFieldsAccessDeniedError() def streams(self, config: Mapping[str, Any]) -> List[Stream]: - config = SourceBambooHr.add_authenticator_to_config(config) + config = SourceBambooHr.add_authenticator_to_config( + cast(Dict[str, Any], config) + ) + + # Grabbing these early on and sending them through the config seemed + # simpler than passing them along as parent streams. + available_fields: List[str] = ( + list( + map( + lambda field: CustomReportsStream.convert_field_to_id( + BambooMetaField(**field) # type: ignore + ), + MetaFieldsStream(config).read_records( + sync_mode=SyncMode.full_refresh + ), + ) + ) + + CustomReportsStream.get_default_bamboo_fields() + ) + + available_tables = list( + map( + lambda meta_table: TablesStream.convert_raw_meta_table_to_typed( + meta_table # type: ignore + ), + MetaTablesStream(config).read_records(sync_mode=SyncMode.full_refresh), + ) + ) + + """ + 1. Convert fields in to a list of strings. + 2. Create a function that returns a list of strings. + 3. Just pass that all along. + """ + + config["available_fields"] = available_fields + config["available_tables"] = available_tables + return [ + MetaTablesStream(config), + TablesStream(config), + MetaFieldsStream(config), CustomReportsStream(config), + # Keeping this around in case we ever need it, but + # we should be able to get the same data from custom_reports. + # EmployeesDirectoryStream ] diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/spec.json b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/spec.json index 1c362f7fac33..d99ae51b7bc7 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/spec.json +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/spec.json @@ -15,16 +15,6 @@ "type": "string", "description": "Api key of bamboo hr", "airbyte_secret": true - }, - "custom_reports_fields": { - "type": "string", - "default": "", - "description": "Comma-separated list of fields to include in custom reports." - }, - "custom_reports_include_default_fields": { - "type": "boolean", - "default": true, - "description": "If true, the custom reports endpoint will include the default fields defined here: https://documentation.bamboohr.com/docs/list-of-field-names." } } } diff --git a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/utils.py b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/utils.py index ea662129fe19..3b8ee20d7fb3 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/utils.py +++ b/airbyte-integrations/connectors/source-bamboo-hr/source_bamboo_hr/utils.py @@ -1,17 +1,49 @@ # # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # +from typing import Iterable, Iterator, TypeVar, Tuple, Mapping, Any +import itertools +T = TypeVar('T') -def convert_custom_reports_fields_to_list(custom_reports_fields: str) -> list: - return custom_reports_fields.split(",") if custom_reports_fields else [] +def get_json_schema_for_field_type(field_type: str) -> Mapping[str, Any]: + """ + Returns the JSON schema for the given BambooHR field type. + """ + # As per https://documentation.bamboohr.com/docs/field-types + default_field_schema = {"type": ["string", "null"]} + if field_type == "currency": + currency_field_schema = { + "type": ["object", "null"], + "properties": { + "value": {"type": ["string"]}, + "currency": {"type": ["string"]}, + }, + } + return currency_field_schema + else: + return default_field_schema +def chunk_iterable(iterable: Iterable[T], chunk_size: int) -> Iterator[Tuple[T, ...]]: + """ + Generates chunks of the given iterable with the specified size. -def validate_custom_fields(custom_fields, available_fields): - denied_fields = [] - for custom_field in custom_fields: - has_access_to_custom_field = any(available_field.get("name") == custom_field for available_field in available_fields) - if not has_access_to_custom_field: - denied_fields.append(custom_field) + Args: + iterable: An iterable to be chunked. + chunk_size: The size of each chunk. - return denied_fields + Yields: + Chunks of the iterable, each up to the specified size. + """ + # An iterator for the input iterable + iterator = iter(iterable) + + while True: + # Take the next chunk_size elements from the iterator + chunk = tuple(itertools.islice(iterator, chunk_size)) + + if not chunk: + # If the chunk is empty, stop the loop + break + + yield chunk diff --git a/airbyte-integrations/connectors/source-bamboo-hr/unit_tests/unit_test.py b/airbyte-integrations/connectors/source-bamboo-hr/unit_tests/unit_test.py index b842ff138f36..deb52cbf980f 100644 --- a/airbyte-integrations/connectors/source-bamboo-hr/unit_tests/unit_test.py +++ b/airbyte-integrations/connectors/source-bamboo-hr/unit_tests/unit_test.py @@ -5,80 +5,18 @@ import pytest from airbyte_cdk.logger import AirbyteLogger from airbyte_cdk.models import Status -from source_bamboo_hr.source import CustomReportsStream, EmployeesDirectoryStream, SourceBambooHr +from source_bamboo_hr.source import EmployeesDirectoryStream, SourceBambooHr @pytest.fixture def config(): - return {"api_key": "foo", "subdomain": "bar", "authenticator": "baz", "custom_reports_include_default_fields": True} - + return {"api_key": "foo", "subdomain": "bar", "authenticator": "baz" } def test_source_bamboo_hr_client_wrong_credentials(): source = SourceBambooHr() result = source.check(logger=AirbyteLogger, config={"subdomain": "test", "api_key": "blah-blah"}) assert result.status == Status.FAILED - -@pytest.mark.parametrize( - "custom_reports_fields,custom_reports_include_default_fields,available_fields,expected_message", - [ - ( - "", - False, - {}, - "NullFieldsError('Field `custom_reports_fields` cannot be empty if `custom_reports_include_default_fields` is false.')", - ), - ("", True, {}, 'AvailableFieldsAccessDeniedError("You hasn\'t access to any report fields. Please check your access level.")'), - ( - "Test", - True, - [{"name": "NewTest"}], - "CustomFieldsAccessDeniedError('Access to fields: Test - denied. Please check your access level.')", - ), - ], -) -def test_check_failed( - config, requests_mock, custom_reports_fields, custom_reports_include_default_fields, available_fields, expected_message -): - config["custom_reports_fields"] = custom_reports_fields - config["custom_reports_include_default_fields"] = custom_reports_include_default_fields - requests_mock.get("https://api.bamboohr.com/api/gateway.php/bar/v1/meta/fields", json=available_fields) - - source = SourceBambooHr() - result = source.check(logger=AirbyteLogger, config=config) - - assert result.status == Status.FAILED - assert result.message == expected_message - - def test_employees_directory_stream_url_base(config): stream = EmployeesDirectoryStream(config) assert stream.url_base == "https://api.bamboohr.com/api/gateway.php/bar/v1/" - - -def test_custom_reports_stream_get_json_schema_from_config(config): - config["custom_reports_fields"] = "one,two , three" - assert CustomReportsStream(config)._get_json_schema_from_config() == { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "one": {"type": ["null", "string"]}, - "two": {"type": ["null", "string"]}, - "three": {"type": ["null", "string"]}, - }, - } - - -def test_custom_reports_stream_union_schemas(): - schema1 = {"properties": {"one": 1, "two": 2}} - schema2 = {"properties": {"two": 2, "three": 3}} - assert CustomReportsStream._union_schemas(schema1, schema2) == {"properties": {"one": 1, "two": 2, "three": 3}} - - -def test_custom_reports_stream_request_body_json(config): - stream = CustomReportsStream(config) - stream._schema = {"properties": {"one": 1, "two": 2}} - assert stream.request_body_json() == { - "title": "Airbyte", - "fields": ["one", "two"], - }