Skip to content

Commit ce2bf4e

Browse files
committed
Handle production Procore environments + saving refresh_tokens
1 parent a017a2d commit ce2bf4e

File tree

3 files changed

+89
-10
lines changed

3 files changed

+89
-10
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "tap-procore"
3-
version = "0.0.7"
3+
version = "0.0.8"
44
description = "`tap-procore` is Singer tap for procore, built with the Singer SDK."
55
authors = ["hotglue"]
66
license = "Apache 2.0"

tap_procore/streams.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
from pathlib import Path
77
from typing import Any, Dict, Optional, Union, List, Iterable
88

9+
from singer.schema import Schema
910

1011
from singer_sdk.streams import RESTStream
12+
from singer_sdk.helpers._util import utc_now
13+
from singer_sdk.plugin_base import PluginBase as TapBaseClass
1114

1215

1316
from singer_sdk.authenticators import (
@@ -40,30 +43,64 @@ def oauth_request_body(self) -> dict:
4043
'grant_type': 'refresh_token',
4144
'client_id': self.config["client_id"],
4245
'client_secret': self.config["client_secret"],
43-
'refresh_token': self.config["refresh_token"],
46+
'refresh_token': self.config["refresh_token"] if not self.refresh_token else self.refresh_token,
4447
'redirect_uri': self.config["redirect_uri"]
4548
}
4649

4750
return req
4851

52+
def update_access_token(self):
53+
"""Update `access_token` along with: `last_refreshed` and `expires_in`."""
54+
request_time = utc_now()
55+
auth_request_payload = self.oauth_request_payload
56+
token_response = requests.post(self.auth_endpoint, data=auth_request_payload)
57+
try:
58+
token_response.raise_for_status()
59+
self.logger.info("OAuth authorization attempt was successful.")
60+
except Exception as ex:
61+
raise RuntimeError(
62+
f"Failed OAuth login, response was '{token_response.json()}'. {ex}"
63+
)
64+
token_json = token_response.json()
65+
self.access_token = token_json["access_token"]
66+
self.expires_in = token_json["expires_in"]
67+
self.last_refreshed = request_time
68+
69+
if token_json.get("refresh_token") is not None:
70+
self.refresh_token = token_json["refresh_token"]
71+
4972

5073
class ProcoreStream(RESTStream):
5174
"""Procore stream class."""
5275

76+
def __init__(
77+
self,
78+
tap: TapBaseClass,
79+
name: Optional[str] = None,
80+
schema: Optional[Union[Dict[str, Any], Schema]] = None,
81+
path: Optional[str] = None,
82+
):
83+
"""Initialize the Procore stream."""
84+
super().__init__(name=name, schema=schema, tap=tap, path=path)
85+
self._config = tap._config
86+
5387
@property
5488
def url_base(self) -> str:
5589
"""Return the API URL root, configurable via tap settings."""
5690
return "https://sandbox.procore.com/rest/v1.0" if self.config["is_sandbox"] else "https://api.procore.com/rest/v1.0"
5791

5892
@property
5993
def authenticator(self) -> APIAuthenticatorBase:
60-
auth_endpoint = "https://login-sandbox.procore.com/oauth/token" if self.config[
61-
"is_sandbox"] else "https://login.procore.com/oauth/token"
94+
if not self._config.get("authenticator"):
95+
auth_endpoint = "https://login-sandbox.procore.com/oauth/token" if self.config[
96+
"is_sandbox"] else "https://login.procore.com/oauth/token"
97+
98+
self._config["authenticator"] = ProcoreAuthenticator(
99+
stream=self,
100+
auth_endpoint=auth_endpoint
101+
)
62102

63-
return ProcoreAuthenticator(
64-
stream=self,
65-
auth_endpoint=auth_endpoint
66-
)
103+
return self._config["authenticator"]
67104

68105

69106
class CompaniesStream(ProcoreStream):

tap_procore/tap.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""procore tap class."""
22

3-
from pathlib import Path
4-
from typing import List
3+
from pathlib import Path, PurePath
4+
from typing import List, Optional, Union
5+
import json
56

67
from singer_sdk import Tap, Stream
78
from singer_sdk.typing import (
@@ -54,10 +55,51 @@ class TapProcore(Tap):
5455
).to_dict()
5556

5657

58+
def __init__(
59+
self,
60+
config: Optional[Union[dict, PurePath, str, List[Union[PurePath, str]]]] = None,
61+
catalog: Union[PurePath, str, dict, None] = None,
62+
state: Union[PurePath, str, dict, None] = None,
63+
parse_env_config: bool = False,
64+
) -> None:
65+
"""Initialize the tap."""
66+
self.config_path = config
67+
super().__init__(config=config, catalog=catalog, state=state, parse_env_config=parse_env_config)
68+
69+
5770
def discover_streams(self) -> List[Stream]:
5871
"""Return a list of discovered streams."""
5972
return [stream_class(tap=self) for stream_class in STREAM_TYPES]
6073

74+
def sync_all(self):
75+
"""Sync all streams."""
76+
# Do the sync
77+
try:
78+
super().sync_all()
79+
except Exception as e:
80+
self.logger.error(e)
81+
finally:
82+
# Update config if needed
83+
self.update_config()
84+
85+
def update_config(self):
86+
"""Update config.json with new access + refresh token."""
87+
self.logger.info("Updating config.")
88+
path = self.config_path
89+
auth = self._config.pop("authenticator", None)
90+
91+
if auth is not None:
92+
if auth.refresh_token is not None:
93+
self._config["refresh_token"] = auth.refresh_token
94+
95+
if auth.access_token is not None:
96+
self._config["access_token"] = auth.access_token
97+
98+
if isinstance(path, list):
99+
path = path[0]
100+
101+
with open(path, 'w') as f:
102+
json.dump(self._config, f, indent=4)
61103

62104
# CLI Execution:
63105

0 commit comments

Comments
 (0)