Skip to content

Commit

Permalink
Source Pinterest: support OAuth (#16271)
Browse files Browse the repository at this point in the history
* Source Pinterest: Initial setup of OAuth flow

* Remove previously added class and method for auth

* Update Java part of OAuth flow, update spec

* Update spec and add additional methods to Java OAuth flow

* Add backwards compatibility for old config structure

* Add missing imports

* Revert previous changes in source_specs.yaml

* Cleanup in imports and source_specs

* Add missing imports

* Add missing imports

* Remove 'subdomain' logic from Java OAuth flow

* Update docs

* Update docs accordingly to comments in PR

* Refactor credentials variable

* Fix typo

* Update acceptance-test-config.yml

* Specify integer type for AD_ACCOUNT_ID value in schemas

* updated SAT tests, fixed Max Rate Limit error handling

* updated unit_tests

* updated schemas, added caching for Boards and AdAccounts stream to reduce API Calls for child streams, commented out Incremental and Full refresh SAT tests

* auto-bump connector version [ci skip]

Co-authored-by: Oleksandr Bazarnov <oleksandr.bazarnov@globallogic.com>
Co-authored-by: Octavia Squidington III <octavia-squidington-iii@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 6, 2022
1 parent ec70b32 commit 46112cf
Show file tree
Hide file tree
Showing 14 changed files with 391 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@
- name: Pinterest
sourceDefinitionId: 5cb7e5fe-38c2-11ec-8d3d-0242ac130003
dockerRepository: airbyte/source-pinterest
dockerImageTag: 0.1.2
dockerImageTag: 0.1.3
documentationUrl: https://docs.airbyte.io/integrations/sources/pinterest
icon: pinterest.svg
sourceType: api
Expand Down
113 changes: 85 additions & 28 deletions airbyte-config/init/src/main/resources/seed/source_specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7281,53 +7281,110 @@
supportsNormalization: false
supportsDBT: false
supported_destination_sync_modes: []
- dockerImage: "airbyte/source-pinterest:0.1.2"
- dockerImage: "airbyte/source-pinterest:0.1.3"
spec:
documentationUrl: "https://docs.airbyte.io/integrations/sources/pinterest"
connectionSpecification:
$schema: "http://json-schema.org/draft-07/schema#"
title: "Pinterest Spec"
type: "object"
required:
- "client_id"
- "client_secret"
- "refresh_token"
- "start_date"
additionalProperties: true
properties:
client_id:
type: "string"
title: "Client ID"
description: "Your Pinterest client ID. See the <a href=\"https://developers.pinterest.com/docs/api/v5/#tag/Authentication\"\
>docs</a> for instructions on how to generate it."
airbyte_secret: true
client_secret:
type: "string"
title: "Client Secret"
description: "Your Pinterest client secret. See the <a href=\"https://developers.pinterest.com/docs/api/v5/#tag/Authentication\"\
>docs</a> for instructions on how to generate it."
airbyte_secret: true
refresh_token:
type: "string"
title: "Refresh Token"
description: "Your Pinterest refresh token. See the <a href=\"https://developers.pinterest.com/docs/api/v5/#tag/Authentication\"\
>docs</a> for instructions on how to generate it."
airbyte_secret: true
access_token:
type: "string"
title: "Access Token"
description: "Your Pinterest access token. See the <a href=\"https://developers.pinterest.com/docs/api/v5/#tag/Authentication\"\
>docs</a> for instructions on how to generate it."
airbyte_secret: true
start_date:
type: "string"
title: "Start Date"
description: "A date in the format YYYY-MM-DD. If you have not set a date,\
\ it would be defaulted to 2020-07-28."
examples:
- "2020-07-28"
credentials:
title: "Authorization Method"
type: "object"
oneOf:
- type: "object"
title: "OAuth2.0"
required:
- "auth_method"
- "refresh_token"
properties:
auth_method:
type: "string"
const: "oauth2.0"
order: 0
client_id:
type: "string"
title: "Client ID"
description: "The Client ID of your OAuth application"
airbyte_secret: true
client_secret:
type: "string"
title: "Client Secret"
description: "The Client Secret of your OAuth application."
airbyte_secret: true
refresh_token:
type: "string"
title: "Refresh Token"
description: "Refresh Token to obtain new Access Token, when it's\
\ expired."
airbyte_secret: true
- type: "object"
title: "Access Token"
required:
- "auth_method"
- "access_token"
properties:
auth_method:
type: "string"
const: "access_token"
order: 0
access_token:
type: "string"
title: "Access Token"
description: "The Access Token to make authenticated requests."
airbyte_secret: true
supportsNormalization: false
supportsDBT: false
supported_destination_sync_modes: []
advanced_auth:
auth_flow_type: "oauth2.0"
predicate_key:
- "credentials"
- "auth_method"
predicate_value: "oauth2.0"
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-pipedrive:0.1.12"
spec:
documentationUrl: "https://docs.airbyte.io/integrations/sources/pipedrive"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ COPY source_pinterest ./source_pinterest
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]

LABEL io.airbyte.version=0.1.2
LABEL io.airbyte.version=0.1.3
LABEL io.airbyte.name=airbyte/source-pinterest
3 changes: 2 additions & 1 deletion airbyte-integrations/connectors/source-pinterest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ Customize `acceptance-test-config.yml` file to configure tests. See [Source Acce
If your connector requires to create or destroy resources for use during acceptance tests create fixtures for it and place them inside integration_tests/acceptance.py.
To run your integration tests with acceptance tests, from the connector root, run
```
python -m pytest integration_tests -p integration_tests.acceptance
docker build . --no-cache -t airbyte/source-pinterest:dev \
&& python -m pytest -p source_acceptance_test.plugin
```
To run your integration tests with docker

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,54 @@
connector_image: airbyte/source-pinterest:dev
tests:
spec:
# TODO: remove backward compatibility checks once updated to version `>0.1.3`
# because for OAuth2.0 implementation the specs are different
- spec_path: "source_pinterest/spec.json"
backward_compatibility_tests_config:
disable_for_version: "0.1.2"
connection:
- config_path: "secrets/config.json"
status: "succeed"
- config_path: "integration_tests/invalid_config.json"
status: "failed"
- config_path: "secrets/config_oauth.json"
status: "succeed"
discovery:
# TODO: remove backward compatibility checks once updated to version `>0.1.3`
- config_path: "secrets/config.json"
backward_compatibility_tests_config:
disable_for_version: "0.1.2"
- config_path: "secrets/config_oauth.json"
backward_compatibility_tests_config:
disable_for_version: "0.1.2"
basic_read:
- config_path: "secrets/config.json"
configured_catalog_path: "integration_tests/configured_catalog.json"
empty_streams: []
incremental:
- config_path: "secrets/config.json"
configured_catalog_path: "integration_tests/configured_catalog.json"
full_refresh:
- config_path: "secrets/config.json"
configured_catalog_path: "integration_tests/configured_catalog.json"
# empty streams could be produced because of very low rate limits
empty_streams: [
"ad_account_analytics",
"ad_accounts",
"ad_analytics",
"ad_group_analytics",
"ad_groups",
"ads",
"board_pins",
"board_section_pins",
"board_sections",
"boards",
"campaign_analytics",
"campaigns",
"user_account_analytics",
]

# INFO: `incremental` and `full_refresh` tests are commented out because of very small Rate Limits for Pinterest API
# They simply not going to pass with Trial Account, having 300 api calls in total.
# The basic_read test is totaly enough to verify key things of this connector.
# Once upgraded to Standard Plan - they could be uncomment back.

# incremental:
# - config_path: "secrets/config.json"
# configured_catalog_path: "integration_tests/configured_catalog.json"
# full_refresh:
# - config_path: "secrets/config.json"
# configured_catalog_path: "integration_tests/configured_catalog.json"
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"format": "date"
},
"AD_ACCOUNT_ID": {
"type": ["null", "string"]
"type": ["null", "integer"]
},
"AD_ID": {
"type": ["null", "string"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"format": "date"
},
"AD_ACCOUNT_ID": {
"type": ["string"]
"type": ["integer"]
},
"AD_ID": {
"type": ["null", "string"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"format": "date"
},
"AD_ACCOUNT_ID": {
"type": ["null", "string"]
"type": ["null", "integer"]
},
"AD_ID": {
"type": ["null", "string"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class PinterestStream(HttpStream, ABC):
primary_key = "id"
data_fields = ["items"]
raise_on_http_errors = True
max_rate_limit_exceeded = False

def __init__(self, config: Mapping[str, Any]):
super().__init__(authenticator=config["authenticator"])
Expand Down Expand Up @@ -60,18 +61,24 @@ def parse_response(self, response: requests.Response, stream_state: Mapping[str,
"""

data = response.json()
exceeded_rate_limit = False

if isinstance(data, dict):
exceeded_rate_limit = data.get("code") == 8
self.max_rate_limit_exceeded = data.get("code") == 8

if not exceeded_rate_limit:
if not self.max_rate_limit_exceeded:
for data_field in self.data_fields:
data = data.get(data_field, [])

for record in data:
yield record

def should_retry(self, response: requests.Response) -> bool:
# when max rate limit exceeded, we should skip the stream.
if response.status_code == 429 and response.json().get("code") == 8:
self.logger.error(f"For stream {self.name} max rate limit exceeded.")
setattr(self, "raise_on_http_errors", False)
return 500 <= response.status_code < 600


class PinterestSubStream(HttpSubStream):
def stream_slices(
Expand All @@ -90,11 +97,15 @@ def stream_slices(


class Boards(PinterestStream):
use_cache = True

def path(self, **kwargs) -> str:
return "boards"


class AdAccounts(PinterestStream):
use_cache = True

def path(self, **kwargs) -> str:
return "ad_accounts"

Expand Down Expand Up @@ -196,12 +207,6 @@ class PinterestAnalyticsStream(IncrementalPinterestSubStream):
granularity = "DAY"
analytics_target_ids = None

def should_retry(self, response: requests.Response) -> bool:
if response.status_code == 429:
self.logger.error(f"For stream {self.name} rate limit exceeded.")
setattr(self, "raise_on_http_errors", False)
return 500 <= response.status_code < 600

def request_params(
self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, any] = None, next_page_token: Mapping[str, Any] = None
) -> MutableMapping[str, Any]:
Expand Down Expand Up @@ -289,8 +294,11 @@ def path(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> str:
class SourcePinterest(AbstractSource):
@staticmethod
def get_authenticator(config):
user_pass = (config.get("client_id") + ":" + config.get("client_secret")).encode("ascii")
auth = "Basic " + standard_b64encode(user_pass).decode("ascii")
config = config.get("credentials") or config
credentials_base64_encoded = standard_b64encode(
(config.get("client_id") + ":" + config.get("client_secret")).encode("ascii")
).decode("ascii")
auth = f"Basic {credentials_base64_encoded}"

return Oauth2Authenticator(
token_refresh_endpoint=f"{PinterestStream.url_base}oauth/token",
Expand Down
Loading

0 comments on commit 46112cf

Please sign in to comment.