Skip to content

Commit 6b502d8

Browse files
midavadimetsybaev
andauthored
🎉 square: added oauth support (#6842)
* fixed test which check incorrect cred config * Added oauth2 authentication * Added oauth creds * fixed formatting * added oauth2 spec section, added missing type hints * Added java part of Square OAuth * fixed checkstyle * removed commented code * added support for old format of spec.json files, updated change logs docs * renamed spec property 'authentication' to default 'credentials'. fixed changes in java part * recovered empty files * updated OAuthImplementationFactory.java * fixed issue with autheticator for sub streams, added config catalog with all streams, updated docs * use advanced_auth * added advanced_auth * moved scopes to private property * updated source version * Revert "updated source version" This reverts commit ce3d061. * updated source version * added new version for airbyte index Co-authored-by: ievgeniit <[email protected]>
1 parent 032b06d commit 6b502d8

File tree

14 files changed

+588
-41
lines changed

14 files changed

+588
-41
lines changed

airbyte-config/init/src/main/resources/config/STANDARD_SOURCE_DEFINITION/77225a51-cd15-4a13-af02-65816bd0ecf4.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"sourceDefinitionId": "77225a51-cd15-4a13-af02-65816bd0ecf4",
33
"name": "Square",
44
"dockerRepository": "airbyte/source-square",
5-
"dockerImageTag": "0.1.3",
5+
"dockerImageTag": "0.1.4",
66
"documentationUrl": "https://docs.airbyte.io/integrations/sources/square",
77
"icon": "square.svg"
88
}

airbyte-config/init/src/main/resources/seed/source_definitions.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@
676676
- name: Square
677677
sourceDefinitionId: 77225a51-cd15-4a13-af02-65816bd0ecf4
678678
dockerRepository: airbyte/source-square
679-
dockerImageTag: 0.1.3
679+
dockerImageTag: 0.1.4
680680
documentationUrl: https://docs.airbyte.io/integrations/sources/square
681681
icon: square.svg
682682
sourceType: api

airbyte-config/init/src/main/resources/seed/source_specs.yaml

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7049,52 +7049,148 @@
70497049
supportsNormalization: false
70507050
supportsDBT: false
70517051
supported_destination_sync_modes: []
7052-
- dockerImage: "airbyte/source-square:0.1.3"
7052+
- dockerImage: "airbyte/source-square:0.1.4"
70537053
spec:
70547054
documentationUrl: "https://docs.airbyte.io/integrations/sources/square"
70557055
connectionSpecification:
70567056
$schema: "http://json-schema.org/draft-07/schema#"
70577057
title: "Square Source CDK Specifications"
70587058
type: "object"
70597059
required:
7060-
- "api_key"
70617060
- "is_sandbox"
7062-
additionalProperties: false
7061+
additionalProperties: true
70637062
properties:
7064-
api_key:
7065-
type: "string"
7066-
description: "The API key for a Square application."
7067-
title: "API Key"
7068-
airbyte_secret: true
70697063
is_sandbox:
70707064
type: "boolean"
70717065
description: "Determines whether to use the sandbox or production environment."
70727066
title: "Sandbox"
70737067
examples:
70747068
- true
70757069
- false
7076-
default: true
7070+
default: false
70777071
start_date:
70787072
type: "string"
70797073
description: "UTC date in the format YYYY-MM-DD. Any data before this date\
70807074
\ will not be replicated. If not set, all data will be replicated."
70817075
title: "Start Date"
70827076
examples:
70837077
- "2021-01-01"
7084-
default: "1970-01-01"
7078+
default: "2021-01-01"
70857079
pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
70867080
include_deleted_objects:
70877081
type: "boolean"
70887082
description: "In some streams there is an option to include deleted objects\
70897083
\ (Items, Categories, Discounts, Taxes)"
7090-
title: "Include Deleded Objects"
7084+
title: "Include Deleted Objects"
70917085
examples:
70927086
- true
70937087
- false
70947088
default: false
7089+
credentials:
7090+
type: "object"
7091+
title: "Credential Type"
7092+
oneOf:
7093+
- title: "Oauth authentication"
7094+
type: "object"
7095+
required:
7096+
- "auth_type"
7097+
- "client_id"
7098+
- "client_secret"
7099+
- "refresh_token"
7100+
properties:
7101+
auth_type:
7102+
type: "string"
7103+
const: "Oauth"
7104+
enum:
7105+
- "Oauth"
7106+
default: "Oauth"
7107+
order: 0
7108+
client_id:
7109+
title: "Client ID"
7110+
type: "string"
7111+
description: "The Square-issued ID of your application"
7112+
airbyte_secret: true
7113+
client_secret:
7114+
title: "Client Secret"
7115+
type: "string"
7116+
description: "The Square-issued application secret for your application"
7117+
airbyte_secret: true
7118+
refresh_token:
7119+
title: "Refresh Token"
7120+
type: "string"
7121+
description: "A refresh token generated using the above client ID\
7122+
\ and secret"
7123+
airbyte_secret: true
7124+
- type: "object"
7125+
title: "API Key"
7126+
required:
7127+
- "auth_type"
7128+
- "api_key"
7129+
properties:
7130+
auth_type:
7131+
type: "string"
7132+
const: "Apikey"
7133+
enum:
7134+
- "Apikey"
7135+
default: "Apikey"
7136+
order: 1
7137+
api_key:
7138+
title: "API key token"
7139+
type: "string"
7140+
description: "The API key for a Square application"
7141+
airbyte_secret: true
70957142
supportsNormalization: false
70967143
supportsDBT: false
70977144
supported_destination_sync_modes: []
7145+
authSpecification:
7146+
auth_type: "oauth2.0"
7147+
oauth2Specification:
7148+
rootObject:
7149+
- "credentials"
7150+
- "0"
7151+
oauthFlowInitParameters:
7152+
- - "client_id"
7153+
- - "client_secret"
7154+
oauthFlowOutputParameters:
7155+
- - "refresh_token"
7156+
advanced_auth:
7157+
auth_flow_type: "oauth2.0"
7158+
predicate_key:
7159+
- "credentials"
7160+
- "auth_type"
7161+
predicate_value: "Oauth"
7162+
oauth_config_specification:
7163+
complete_oauth_output_specification:
7164+
type: "object"
7165+
additionalProperties: false
7166+
properties:
7167+
refresh_token:
7168+
type: "string"
7169+
path_in_connector_config:
7170+
- "credentials"
7171+
- "refresh_token"
7172+
complete_oauth_server_input_specification:
7173+
type: "object"
7174+
additionalProperties: false
7175+
properties:
7176+
client_id:
7177+
type: "string"
7178+
client_secret:
7179+
type: "string"
7180+
complete_oauth_server_output_specification:
7181+
type: "object"
7182+
additionalProperties: false
7183+
properties:
7184+
client_id:
7185+
type: "string"
7186+
path_in_connector_config:
7187+
- "credentials"
7188+
- "client_id"
7189+
client_secret:
7190+
type: "string"
7191+
path_in_connector_config:
7192+
- "credentials"
7193+
- "client_secret"
70987194
- dockerImage: "airbyte/source-strava:0.1.2"
70997195
spec:
71007196
documentationUrl: "https://docs.airbyte.io/integrations/sources/strava"

airbyte-integrations/connectors/source-square/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ RUN pip install .
1212
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
1313
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]
1414

15-
LABEL io.airbyte.version=0.1.3
15+
LABEL io.airbyte.version=0.1.4
1616
LABEL io.airbyte.name=airbyte/source-square

airbyte-integrations/connectors/source-square/acceptance-test-config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ tests:
55
connection:
66
- config_path: "secrets/config.json"
77
status: "succeed"
8+
- config_path: "secrets/config_oauth.json"
9+
status: "succeed"
810
- config_path: "integration_tests/invalid_config.json"
911
status: "failed"
1012
discovery:
1113
- config_path: "secrets/config.json"
14+
- config_path: "secrets/config_oauth.json"
1215
basic_read:
1316
- config_path: "secrets/config.json"
1417
configured_catalog_path: "integration_tests/configured_catalog.json"
18+
- config_path: "secrets/config_oauth.json"
19+
configured_catalog_path: "integration_tests/configured_catalog_oauth.json"
1520
incremental:
1621
- config_path: "secrets/config.json"
1722
configured_catalog_path: "integration_tests/configured_catalog.json"
1823
future_state_path: "integration_tests/abnormal_state.json"
1924
full_refresh:
2025
- config_path: "secrets/config.json"
2126
configured_catalog_path: "integration_tests/configured_catalog.json"
27+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"streams": [
3+
{
4+
"stream": {
5+
"name": "locations",
6+
"json_schema": {},
7+
"supported_sync_modes": ["full_refresh"],
8+
"source_defined_cursor": true,
9+
"default_cursor_field": ["id"]
10+
},
11+
"sync_mode": "full_refresh",
12+
"cursor_field": ["id"],
13+
"destination_sync_mode": "overwrite"
14+
},
15+
{
16+
"stream": {
17+
"name": "team_members",
18+
"json_schema": {},
19+
"supported_sync_modes": ["full_refresh"],
20+
"source_defined_cursor": true,
21+
"default_cursor_field": ["id"]
22+
},
23+
"sync_mode": "full_refresh",
24+
"cursor_field": ["id"],
25+
"destination_sync_mode": "overwrite"
26+
},
27+
{
28+
"stream": {
29+
"name": "team_member_wages",
30+
"json_schema": {},
31+
"supported_sync_modes": ["full_refresh"],
32+
"source_defined_cursor": true,
33+
"default_cursor_field": ["id"]
34+
},
35+
"sync_mode": "full_refresh",
36+
"cursor_field": ["id"],
37+
"destination_sync_mode": "overwrite"
38+
}
39+
]
40+
}

airbyte-integrations/connectors/source-square/source_square/source.py

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44

55
import json
66
from abc import ABC, abstractmethod
7-
from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple
7+
from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union
88

99
import pendulum
1010
import requests
11+
from airbyte_cdk.logger import AirbyteLogger
1112
from airbyte_cdk.models import SyncMode
1213
from airbyte_cdk.sources import AbstractSource
1314
from airbyte_cdk.sources.streams import Stream
1415
from airbyte_cdk.sources.streams.http import HttpStream
15-
from airbyte_cdk.sources.streams.http.auth import TokenAuthenticator
16+
from airbyte_cdk.sources.streams.http.auth.core import HttpAuthenticator
17+
from airbyte_cdk.sources.streams.http.requests_native_auth import Oauth2Authenticator, TokenAuthenticator
18+
from requests.auth import AuthBase
1619
from source_square.utils import separate_items_by_count
1720

1821

@@ -35,8 +38,16 @@ def parse_square_error_response(error: requests.exceptions.HTTPError) -> SquareE
3538

3639

3740
class SquareStream(HttpStream, ABC):
38-
def __init__(self, is_sandbox: bool, api_version: str, start_date: str, include_deleted_objects: bool, **kwargs):
39-
super().__init__(**kwargs)
41+
def __init__(
42+
self,
43+
is_sandbox: bool,
44+
api_version: str,
45+
start_date: str,
46+
include_deleted_objects: bool,
47+
authenticator: Union[AuthBase, HttpAuthenticator],
48+
):
49+
super().__init__(authenticator)
50+
self._authenticator = authenticator
4051
self.is_sandbox = is_sandbox
4152
self.api_version = api_version
4253
# Converting users ISO 8601 format (YYYY-MM-DD) to RFC 3339 (2021-06-14T13:47:56.799Z)
@@ -358,16 +369,75 @@ def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]:
358369
yield {"location_ids": location}
359370

360371

372+
class Oauth2AuthenticatorSquare(Oauth2Authenticator):
373+
def refresh_access_token(self) -> Tuple[str, int]:
374+
"""Handle differences in expiration attr:
375+
from API: "expires_at": "2021-11-05T14:26:57Z"
376+
expected: "expires_in": number of seconds
377+
"""
378+
token, expires_at = super().refresh_access_token()
379+
expires_in = pendulum.parse(expires_at) - pendulum.now()
380+
return token, expires_in.seconds
381+
382+
361383
class SourceSquare(AbstractSource):
362-
api_version = "2021-06-16" # Latest Stable Release
384+
api_version = "2021-09-15" # Latest Stable Release
385+
386+
@staticmethod
387+
def get_auth(config: Mapping[str, Any]) -> AuthBase:
388+
389+
credential = config.get("credentials", {})
390+
auth_type = credential.get("auth_type")
391+
if auth_type == "Oauth":
392+
# scopes needed for all currently supported streams:
393+
scopes = [
394+
"CUSTOMERS_READ",
395+
"EMPLOYEES_READ",
396+
"ITEMS_READ",
397+
"MERCHANT_PROFILE_READ",
398+
"ORDERS_READ",
399+
"PAYMENTS_READ",
400+
"TIMECARDS_READ",
401+
# OAuth Permissions:
402+
# https://developer.squareup.com/docs/oauth-api/square-permissions
403+
# https://developer.squareup.com/reference/square/enums/OAuthPermission
404+
# "DISPUTES_READ",
405+
# "GIFTCARDS_READ",
406+
# "INVENTORY_READ",
407+
# "INVOICES_READ",
408+
# "TIMECARDS_SETTINGS_READ",
409+
# "LOYALTY_READ",
410+
# "ONLINE_STORE_SITE_READ",
411+
# "ONLINE_STORE_SNIPPETS_READ",
412+
# "SUBSCRIPTIONS_READ",
413+
]
414+
415+
auth = Oauth2AuthenticatorSquare(
416+
token_refresh_endpoint="https://connect.squareup.com/oauth2/token",
417+
client_secret=credential.get("client_secret"),
418+
client_id=credential.get("client_id"),
419+
refresh_token=credential.get("refresh_token"),
420+
scopes=scopes,
421+
expires_in_name="expires_at",
422+
)
423+
elif auth_type == "Apikey":
424+
auth = TokenAuthenticator(token=credential.get("api_key"))
425+
elif not auth_type and config.get("api_key"):
426+
auth = TokenAuthenticator(token=config.get("api_key"))
427+
else:
428+
raise Exception(f"Invalid auth type: {auth_type}")
429+
430+
return auth
363431

364-
def check_connection(self, logger, config) -> Tuple[bool, any]:
432+
def check_connection(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> Tuple[bool, any]:
365433

366434
headers = {
367435
"Square-Version": self.api_version,
368-
"Authorization": "Bearer {}".format(config["api_key"]),
369436
"Content-Type": "application/json",
370437
}
438+
auth = self.get_auth(config)
439+
headers.update(auth.get_auth_header())
440+
371441
url = "https://connect.squareup{}.com/v2/catalog/info".format("sandbox" if config["is_sandbox"] else "")
372442

373443
try:
@@ -383,9 +453,8 @@ def check_connection(self, logger, config) -> Tuple[bool, any]:
383453

384454
def streams(self, config: Mapping[str, Any]) -> List[Stream]:
385455

386-
auth = TokenAuthenticator(token=config["api_key"])
387456
args = {
388-
"authenticator": auth,
457+
"authenticator": self.get_auth(config),
389458
"is_sandbox": config["is_sandbox"],
390459
"api_version": self.api_version,
391460
"start_date": config["start_date"],

0 commit comments

Comments
 (0)