Skip to content

Commit 2baabcc

Browse files
committed
SAC-30896: Handle un-auth stream exclusion from catalog
1 parent b35c2f3 commit 2baabcc

4 files changed

Lines changed: 495 additions & 5 deletions

File tree

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from setuptools import setup, find_packages
33

44
setup(name="tap-xero",
5-
version="2.6.0",
5+
version="2.7.0",
66
description="Singer.io tap for extracting data from the Xero API",
77
author="Stitch",
88
url="http://singer.io",

tap_xero/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from singer import metadata, metrics, utils
66
from singer.catalog import Catalog, CatalogEntry, Schema
77
from . import streams as streams_
8-
from .client import XeroClient
8+
from .client import XeroClient, XeroForbiddenError
99
from .context import Context
1010

1111
REQUIRED_CONFIG_KEYS = [
@@ -67,10 +67,15 @@ def load_metadata(stream, schema):
6767
def ensure_credentials_are_valid(config):
6868
XeroClient(config).filter("currencies")
6969

70+
7071
def discover(ctx):
72+
LOGGER.info("Starting discover")
7173
ctx.check_platform_access()
7274
catalog = Catalog([])
7375
for stream in streams_.all_streams:
76+
if not stream.check_access(ctx):
77+
continue
78+
7479
schema_dict = load_schema(stream.tap_stream_id)
7580
mdata = load_metadata(stream, schema_dict)
7681

@@ -82,6 +87,13 @@ def discover(ctx):
8287
schema=schema,
8388
metadata=mdata
8489
))
90+
91+
if not catalog.streams:
92+
error_msg = "The provided credentials doesn't have access to any streams. Please recheck configuration"
93+
LOGGER.error(error_msg)
94+
raise XeroForbiddenError(error_msg)
95+
96+
LOGGER.info("Finished discover")
8597
return catalog
8698

8799

@@ -126,6 +138,8 @@ def main_impl():
126138

127139
sync(Context(args.config, args.state, catalog, args.config_path))
128140

141+
142+
@singer.utils.handle_top_exception(LOGGER)
129143
def main():
130144
try:
131145
main_impl()

tap_xero/streams.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
from requests.exceptions import HTTPError
1+
import backoff
22
import singer
3-
from singer import metadata, metrics, Transformer
3+
from requests.exceptions import HTTPError
4+
from singer import Transformer, metadata, metrics
45
from singer.utils import strptime_with_tz
5-
import backoff
6+
7+
from tap_xero.context import Context
8+
69
from . import transform
10+
from .client import XeroForbiddenError, XeroUnauthorizedError
711

812
LOGGER = singer.get_logger()
913
FULL_PAGE_SIZE = 100
@@ -54,6 +58,28 @@ def __init__(self, tap_stream_id, pk_fields, bookmark_key="UpdatedDateUTC", form
5458
self.bookmark_key = bookmark_key
5559
self.replication_method = "INCREMENTAL"
5660
self.filter_options = {}
61+
self.probe_filter_options = {}
62+
63+
def check_access(self, ctx: Context):
64+
"""
65+
Verify that the API credentials have read access to this stream.
66+
Returns True if accessible, False if a 403 Forbidden or 401 Unauthorized error is raised.
67+
"""
68+
ctx.refresh_credentials()
69+
70+
try:
71+
LOGGER.info("Checking access for stream %s...", self.tap_stream_id)
72+
ctx.client.filter(self.tap_stream_id, self.probe_filter_options)
73+
LOGGER.info("Stream %s is accessible with the provided credentials.", self.tap_stream_id)
74+
75+
return True
76+
except (XeroUnauthorizedError, XeroForbiddenError):
77+
LOGGER.warning(
78+
"Stream '%s' does not have read permission, "
79+
"excluding from catalog.",
80+
self.tap_stream_id,
81+
)
82+
return False
5783

5884
def metrics(self, records):
5985
with metrics.record_counter(self.tap_stream_id) as counter:
@@ -135,6 +161,8 @@ class Journals(Stream):
135161
"""The Journals endpoint is a special case. It has its own way of ordering
136162
and paging the data. See
137163
https://developer.xero.com/documentation/api/journals"""
164+
probe_filter_options = {"offset": 0}
165+
138166
def sync(self, ctx):
139167
bookmark = [self.tap_stream_id, self.bookmark_key]
140168
journal_number = ctx.get_bookmark(bookmark) or 0
@@ -158,6 +186,8 @@ class LinkedTransactions(Stream):
158186
the UpdatedDateUTC timestamp in them. Therefore we must always iterate over
159187
all of the data, but we can manually omit records based on the
160188
UpdatedDateUTC property."""
189+
probe_filter_options = {"page": 1}
190+
161191
def sync(self, ctx):
162192
bookmark = [self.tap_stream_id, self.bookmark_key]
163193
offset = [self.tap_stream_id, "page"]

0 commit comments

Comments
 (0)