Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"sourceDefinitionId": "fe2b4084-3386-4d3b-9ad6-308f61a6f1e6",
"name": "Harvest",
"dockerRepository": "airbyte/source-harvest",
"dockerImageTag": "0.1.5",
"dockerImageTag": "0.1.6",
"documentationUrl": "https://docs.airbyte.io/integrations/sources/harvest"
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
- name: Harvest
sourceDefinitionId: fe2b4084-3386-4d3b-9ad6-308f61a6f1e6
dockerRepository: airbyte/source-harvest
dockerImageTag: 0.1.5
dockerImageTag: 0.1.6
documentationUrl: https://docs.airbyte.io/integrations/sources/harvest
sourceType: api
- name: HubSpot
Expand Down
116 changes: 108 additions & 8 deletions airbyte-config/init/src/main/resources/seed/source_specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2321,30 +2321,25 @@
supportsNormalization: false
supportsDBT: false
supported_destination_sync_modes: []
- dockerImage: "airbyte/source-harvest:0.1.5"
- dockerImage: "airbyte/source-harvest:0.1.6"
spec:
documentationUrl: "https://docs.airbyte.io/integrations/sources/harvest"
connectionSpecification:
$schema: "http://json-schema.org/draft-07/schema#"
title: "Harvest Spec"
type: "object"
required:
- "api_token"
- "account_id"
- "replication_start_date"
additionalProperties: false
additionalProperties: true
properties:
api_token:
title: "API Token"
description: "Harvest API Token."
airbyte_secret: true
type: "string"
account_id:
title: "Account ID"
description: "Harvest account ID. Required for all Harvest requests in pair\
\ with API Key"
airbyte_secret: true
type: "string"
order: 0
replication_start_date:
title: "Replication Start Date"
description: "UTC date and time in the format 2017-01-25T00:00:00Z. Any\
Expand All @@ -2353,11 +2348,116 @@
examples:
- "2017-01-25T00:00:00Z"
type: "string"
order: 1
credentials:
title: "Authentication mechanism"
description: "Choose how to authenticate to Harvest"
type: "object"
order: 2
oneOf:
- type: "object"
title: "Authenticate via Harvest (Oauth)"
required:
- "client_id"
- "client_secret"
- "refresh_token"
additionalProperties: false
properties:
auth_type:
type: "string"
const: "Client"
enum:
- "Client"
default: "Client"
order: 0
client_id:
title: "Client ID"
type: "string"
description: "The Client ID of your application"
client_secret:
title: "Client Secret"
type: "string"
description: "The client secret of your application"
airbyte_secret: true
refresh_token:
title: "Refresh Token"
type: "string"
description: "A refresh token generated using the above client ID\
\ and secret"
airbyte_secret: true
- type: "object"
title: "Authenticate with Personal Access Token"
required:
- "api_token"
additionalProperties: false
properties:
auth_type:
type: "string"
const: "Token"
enum:
- "Token"
default: "Token"
order: 0
api_token:
title: "Personal Access Token"
description: "Log into Harvest and then create new <a href=\"https://id.getharvest.com/developers\"\
> personal access token</a>."
type: "string"
airbyte_secret: true
supportsIncremental: true
supportsNormalization: false
supportsDBT: false
supported_destination_sync_modes:
- "append"
authSpecification:
auth_type: "oauth2.0"
oauth2Specification:
rootObject:
- "credentials"
- "0"
oauthFlowInitParameters:
- - "client_id"
- - "client_secret"
oauthFlowOutputParameters:
- - "refresh_token"
advancedAuth:
auth_flow_type: "oauth2.0"
predicate_key:
- "credentials"
- "auth_type"
predicate_value: "Client"
oauth_config_specification:
complete_oauth_output_specification:
type: "object"
additionalProperties: false
properties:
refresh_token:
type: "string"
path_in_connector_config:
- "credentials"
- "refresh_token"
complete_oauth_server_input_specification:
type: "object"
additionalProperties: false
properties:
client_id:
type: "string"
client_secret:
type: "string"
complete_oauth_server_output_specification:
type: "object"
additionalProperties: false
properties:
client_id:
type: "string"
path_in_connector_config:
- "credentials"
- "client_id"
client_secret:
type: "string"
path_in_connector_config:
- "credentials"
- "client_secret"
- dockerImage: "airbyte/source-hubspot:0.1.24"
spec:
documentationUrl: "https://docs.airbyte.io/integrations/sources/hubspot"
Expand Down
2 changes: 1 addition & 1 deletion airbyte-integrations/connectors/source-harvest/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ RUN pip install .
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]

LABEL io.airbyte.version=0.1.5
LABEL io.airbyte.version=0.1.6
LABEL io.airbyte.name=airbyte/source-harvest
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ tests:
connection:
- config_path: "secrets/config.json"
status: "succeed"
- config_path: "secrets/old_config.json"
status: "succeed"
- config_path: "secrets/config_oauth.json"
status: "succeed"
- config_path: "integration_tests/invalid_config.json"
status: "failed"
discovery:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,32 @@

from typing import Any, Mapping

from airbyte_cdk.sources.streams.http.auth import TokenAuthenticator
from airbyte_cdk.sources.streams.http.auth import Oauth2Authenticator, TokenAuthenticator


class HarvestTokenAuthenticator(TokenAuthenticator):
def __init__(self, token: str, account_id: str, account_id_header: str = "Harvest-Account-ID", **kwargs):
super().__init__(token, **kwargs)
class HarvestMixin:
"""
Mixin class for providing additional HTTP header for specifying account ID
https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/
"""
def __init__(self, *, account_id: str, account_id_header: str = "Harvest-Account-ID", **kwargs):
super().__init__(**kwargs)
self.account_id = account_id
self.account_id_header = account_id_header

def get_auth_header(self) -> Mapping[str, Any]:
return {**super().get_auth_header(), self.account_id_header: self.account_id}


class HarvestTokenAuthenticator(HarvestMixin, TokenAuthenticator):
"""
Auth class for Personal Access Token
https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/#personal-access-tokens
"""


class HarvestOauth2Authenticator(HarvestMixin, Oauth2Authenticator):
"""
Auth class for OAuth2
https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/#for-server-side-applications
"""
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,30 @@
Users,
)

from .auth import HarvestTokenAuthenticator
from .auth import HarvestOauth2Authenticator, HarvestTokenAuthenticator


class SourceHarvest(AbstractSource):
@staticmethod
def get_authenticator(config):
credentials = config.get("credentials", {})
if credentials and "client_id" in credentials:
return HarvestOauth2Authenticator(
token_refresh_endpoint="https://id.getharvest.com/api/v2/oauth2/token",
client_id=credentials.get("client_id"),
client_secret=credentials.get("client_secret"),
refresh_token=credentials.get("refresh_token"),
account_id=config["account_id"],
)

api_token = credentials.get("api_token", config.get("api_token"))
if not api_token:
raise Exception("Config validation error: 'api_token' is a required property")
return HarvestTokenAuthenticator(token=api_token, account_id=config["account_id"])

def check_connection(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> Tuple[bool, Any]:
try:
auth = HarvestTokenAuthenticator(token=config["api_token"], account_id=config["account_id"])
auth = self.get_authenticator(config)
replication_start_date = pendulum.parse(config["replication_start_date"])
users_gen = Users(authenticator=auth, replication_start_date=replication_start_date).read_records(
sync_mode=SyncMode.full_refresh
Expand All @@ -65,7 +82,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]:
"""
:param config: A Mapping of the user input configuration as defined in the connector spec.
"""
auth = HarvestTokenAuthenticator(token=config["api_token"], account_id=config["account_id"])
auth = self.get_authenticator(config)
replication_start_date = pendulum.parse(config["replication_start_date"])
from_date = replication_start_date.date()

Expand Down
Loading