Skip to content

Commit

Permalink
🎉 Source smartsheets: oauth support (#9792)
Browse files Browse the repository at this point in the history
* Added SAT, fixed tests, fixed schema validation error

* Added Oauth support for smartsheets source

* minor changes after review

* added advanced_auth property instead of authSpecification

* Updated connector version

* removed empty unittests

* updated version in source_specs.yaml
  • Loading branch information
midavadim committed Feb 4, 2022
1 parent b5b0976 commit 9e6da46
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"sourceDefinitionId": "374ebc65-6636-4ea0-925c-7d35999a8ffc",
"name": "Smartsheets",
"dockerRepository": "airbyte/source-smartsheets",
"dockerImageTag": "0.1.7",
"dockerImageTag": "0.1.8",
"documentationUrl": "https://docs.airbyte.io/integrations/sources/smartsheets",
"icon": "smartsheet.svg"
}
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@
- name: Smartsheets
sourceDefinitionId: 374ebc65-6636-4ea0-925c-7d35999a8ffc
dockerRepository: airbyte/source-smartsheets
dockerImageTag: 0.1.7
dockerImageTag: 0.1.8
documentationUrl: https://docs.airbyte.io/integrations/sources/smartsheets
icon: smartsheet.svg
sourceType: api
Expand Down
29 changes: 27 additions & 2 deletions airbyte-config/init/src/main/resources/seed/source_specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7303,7 +7303,7 @@
oauthFlowOutputParameters:
- - "access_token"
- - "refresh_token"
- dockerImage: "airbyte/source-smartsheets:0.1.7"
- dockerImage: "airbyte/source-smartsheets:0.1.8"
spec:
documentationUrl: "https://docs.airbyte.io/integrations/sources/smartsheets"
connectionSpecification:
Expand All @@ -7313,7 +7313,7 @@
required:
- "access_token"
- "spreadsheet_id"
additionalProperties: false
additionalProperties: true
properties:
access_token:
title: "Access Token"
Expand All @@ -7328,6 +7328,31 @@
supportsNormalization: false
supportsDBT: false
supported_destination_sync_modes: []
advanced_auth:
auth_flow_type: "oauth2.0"
predicate_key: []
predicate_value: ""
oauth_config_specification:
complete_oauth_output_specification:
type: "object"
additionalProperties: false
properties:
access_token:
type: "string"
path_in_connector_config:
- "access_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: {}
- dockerImage: "airbyte/source-snapchat-marketing:0.1.4"
spec:
documentationUrl: "https://docs.airbyte.io/integrations/sources/snapchat-marketing"
Expand Down
5 changes: 2 additions & 3 deletions airbyte-integrations/connectors/source-smartsheets/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ ENV CODE_PATH="source_smartsheets"

WORKDIR /airbyte/integration_code
COPY $CODE_PATH ./$CODE_PATH
COPY setup.py ./
COPY main.py ./

COPY setup.py ./
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.7
LABEL io.airbyte.version=0.1.8
LABEL io.airbyte.name=airbyte/source-smartsheets
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env sh

# Build latest connector image
docker build . -t $(cat acceptance-test-config.yml | grep "connector_image" | head -n 1 | cut -d: -f2-)

# Pull latest acctest image
docker pull airbyte/source-acceptance-test:latest

# Run
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /tmp:/tmp \
-v $(pwd):/test_input \
airbyte/source-acceptance-test \
--acceptance-test-config /test_input

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"streams": [
{
"stream": {
"name": "aws_s3_sample",
"supported_sync_modes": ["full_refresh"],
"source_defined_cursor": false,
"source_defined_primary_key": [["id"]],
"json_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"email": {
"type": "string"
},
"gender": {
"type": "string"
},
"ip_address": {
"type": "string"
},
"dob": {
"type": "string",
"format": "date"
}
}
}
},
"sync_mode": "full_refresh",
"cursor_field": null,
"destination_sync_mode": "overwrite"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ def get_prop(col_type: str) -> Dict[str, any]:
"DATE": {"type": "string", "format": "date"},
"DATETIME": {"type": "string", "format": "date-time"},
}
if col_type in props.keys():
return props[col_type]
else: # assume string
return props["TEXT_NUMBER"]
return props.get(col_type, {"type": "string"})


def get_json_schema(sheet: Dict) -> Dict:
Expand Down Expand Up @@ -82,6 +79,7 @@ def discover(self, logger: AirbyteLogger, config: json) -> AirbyteCatalog:
logger.info(f"Running discovery on sheet: {sheet['name']} with {spreadsheet_id}")

stream = AirbyteStream(name=sheet["name"], json_schema=sheet_json_schema)
stream.supported_sync_modes = ["full_refresh"]
streams.append(stream)

except Exception as e:
Expand Down Expand Up @@ -115,7 +113,8 @@ def read(
logger.info(f"Row count: {sheet['totalRowCount']}")

for row in sheet["rows"]:
values = tuple(i["value"] if "value" in i else "" for i in row["cells"])
# convert all data to string as it is only expected format in schema
values = tuple(str(i["value"]) if "value" in i else "" for i in row["cells"])
try:
data = dict(zip(columns, values))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"title": "Smartsheets Source Spec",
"type": "object",
"required": ["access_token", "spreadsheet_id"],
"additionalProperties": false,
"additionalProperties": true,
"properties": {
"access_token": {
"title": "Access Token",
Expand All @@ -19,5 +19,40 @@
"type": "string"
}
}
},
"advanced_auth": {
"auth_flow_type": "oauth2.0",
"predicate_key": [],
"predicate_value": "",
"oauth_config_specification": {
"complete_oauth_output_specification": {
"type": "object",
"additionalProperties": false,
"properties": {
"access_token": {
"type": "string",
"path_in_connector_config": ["access_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": {
}
}
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public OAuthImplementationFactory(final ConfigRepository configRepository, final
.put("airbyte/source-linkedin-ads", new LinkedinAdsOAuthFlow(configRepository, httpClient))
.put("airbyte/source-salesforce", new SalesforceOAuthFlow(configRepository, httpClient))
.put("airbyte/source-slack", new SlackOAuthFlow(configRepository, httpClient))
.put("airbyte/source-smartsheets", new SmartsheetsOAuthFlow(configRepository, httpClient))
.put("airbyte/source-snapchat-marketing", new SnapchatMarketingOAuthFlow(configRepository, httpClient))
.put("airbyte/source-square", new SquareOAuthFlow(configRepository, httpClient))
.put("airbyte/source-strava", new StravaOAuthFlow(configRepository, httpClient))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2021 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.oauth.flows;

import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import io.airbyte.config.persistence.ConfigRepository;
import io.airbyte.oauth.BaseOAuth2Flow;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;
import org.apache.http.client.utils.URIBuilder;

public class SmartsheetsOAuthFlow extends BaseOAuth2Flow {

private static final String AUTHORIZE_URL = "https://app.smartsheet.com/b/authorize";
private static final String ACCESS_TOKEN_URL = "https://api.smartsheet.com/2.0/token";

public SmartsheetsOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient) {
super(configRepository, httpClient);
}

@VisibleForTesting
public SmartsheetsOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient, final Supplier<String> stateSupplier) {
super(configRepository, httpClient, stateSupplier);
}

@Override
protected String formatConsentUrl(final UUID definitionId,
final String clientId,
final String redirectUrl,
final JsonNode inputOAuthConfiguration)
throws IOException {
try {
return new URIBuilder(AUTHORIZE_URL)
.addParameter("client_id", clientId)
.addParameter("response_type", "code")
.addParameter("state", getState())
.addParameter("scope", "READ_SHEETS")
.build().toString();
} catch (final URISyntaxException e) {
throw new IOException("Failed to format Consent URL for OAuth flow", e);
}
}

@Override
protected String getAccessTokenUrl(final JsonNode inputOAuthConfiguration) {
return ACCESS_TOKEN_URL;
}

@Override
protected Map<String, Object> extractOAuthOutput(final JsonNode data, final String accessTokenUrl) {
Preconditions.checkArgument(data.has("access_token"), "Missing 'access_token' in query params from %s", ACCESS_TOKEN_URL);
return Map.of("access_token", data.get("access_token").asText());
}

@Override
public List<String> getDefaultOAuthOutputPath() {
return List.of();
}

@Override
protected Map<String, String> getAccessTokenQueryParameters(final String clientId,
final String clientSecret,
final String authCode,
final String redirectUrl) {
return ImmutableMap.<String, String>builder()
// required
.put("grant_type", "authorization_code")
.put("client_id", clientId)
.put("client_secret", clientSecret)
.put("code", authCode)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2021 Airbyte, Inc., all rights reserved.
*/

package io.airbyte.oauth.flows;

import com.fasterxml.jackson.databind.JsonNode;
import io.airbyte.oauth.BaseOAuthFlow;
import io.airbyte.oauth.MoreOAuthParameters;
import java.util.List;
import java.util.Map;

public class SmartsheetsOAuthFlowTest extends BaseOAuthFlowTest {

@Override
protected BaseOAuthFlow getOAuthFlow() {
return new SmartsheetsOAuthFlow(getConfigRepository(), getHttpClient(), this::getConstantState);
}

@Override
protected List<String> getExpectedOutputPath() {
return List.of();
}

@Override
protected String getExpectedConsentUrl() {
return "https://app.smartsheet.com/b/authorize?client_id=test_client_id&response_type=code&state=state&scope=READ_SHEETS";
}

@Override
protected Map<String, String> getExpectedOutput() {
return Map.of(
"access_token", "access_token_response",
"client_id", MoreOAuthParameters.SECRET_MASK,
"client_secret", MoreOAuthParameters.SECRET_MASK);
}

@Override
protected JsonNode getCompleteOAuthOutputSpecification() {
return getJsonSchema(Map.of("access_token", Map.of("type", "string")));
}

@Override
protected Map<String, String> getExpectedFilteredOutput() {
return Map.of(
"access_token", "access_token_response",
"client_id", MoreOAuthParameters.SECRET_MASK);
}

}
7 changes: 7 additions & 0 deletions docs/integrations/sources/smartsheets.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@ To setup your new Smartsheets source, Airbyte will need:
1. Your API access token
2. The spreadsheet ID

## Changelog

| Version | Date | Pull Request | Subject |
|:--------|:-----------| :--- |:--------------------|
| 0.1.8 | 2022-02-04 | [9792](https://github.com/airbytehq/airbyte/pull/9792) | Added oauth support |


0 comments on commit 9e6da46

Please sign in to comment.