Skip to content

Commit 5ce3b12

Browse files
SAC-29934: Refactored and fixed integration tests to support new streams (#43)
* Refactored and fixed integration tests to support new streams * Excluded organization_actions stream as it doesn't have enough data * Added organization_actions stream to untestable_streams * Fixed bookmarks assertion * Fixed duplicate records assertion error * Fix automatic fields for child streams by calling modify_object in FullTableStream * Fix package installation by using find_packages() to include streams submodule * Fixed Field mismatch errors * Addressed review comments * Fix API rate limits * Fix assertion error for card_custom_field_items stream
1 parent b2e9e1e commit 5ce3b12

18 files changed

+889
-791
lines changed

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
from setuptools import setup
2+
from setuptools import find_packages, setup
33

44
setup(
55
name="tap-trello",
@@ -27,9 +27,9 @@
2727
[console_scripts]
2828
tap-trello=tap_trello:main
2929
""",
30-
packages=["tap_trello"],
30+
packages=find_packages(),
3131
package_data = {
32-
"tap_trello": ["tap_trello/schemas/*.json"]
32+
"tap_trello": ["schemas/*.json"]
3333
},
3434
include_package_data=True,
3535
)

tap_trello/streams/abstracts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ def sync(
572572
self.update_data_payload(parent_obj=parent_obj)
573573
with metrics.record_counter(self.tap_stream_id) as counter:
574574
for record in self.get_records():
575+
record = self.modify_object(record, parent_obj)
575576
transformed_record = transformer.transform(
576577
record, self.schema, self.metadata
577578
)

tap_trello/streams/board_custom_fields.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class BoardCustomFields(FullTableStream):
66
replication_method = "FULL_TABLE"
77
path = "/boards/{id}/customFields"
88
parent = "boards"
9+
10+
def modify_object(self, record, parent_record=None):
11+
"""Add boardId to board custom field records."""
12+
if parent_record and 'id' in parent_record:
13+
record["boardId"] = parent_record['id']
14+
return record

tap_trello/streams/board_labels.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class BoardLabels(FullTableStream):
66
replication_method = "FULL_TABLE"
77
path = "/boards/{id}/labels"
88
parent = "boards"
9+
10+
def modify_object(self, record, parent_record=None):
11+
"""Add boardId to board label records."""
12+
if parent_record and 'id' in parent_record:
13+
record["boardId"] = parent_record['id']
14+
return record

tap_trello/streams/card_attachments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class CardAttachments(FullTableStream):
66
replication_method = "FULL_TABLE"
77
path = "/cards/{id}/attachments"
88
parent = "cards"
9+
10+
def modify_object(self, record, parent_record=None):
11+
"""Add card_id to card attachment records."""
12+
if parent_record and 'id' in parent_record:
13+
record["card_id"] = parent_record['id']
14+
return record

tap_trello/streams/card_custom_field_items.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class CardCustomFieldItems(FullTableStream):
66
replication_method = "FULL_TABLE"
77
path = "/cards/{id}/customFieldItems"
88
parent = "cards"
9+
10+
def modify_object(self, record, parent_record=None):
11+
"""Add card_id to card custom field item records."""
12+
if parent_record and 'id' in parent_record:
13+
record["card_id"] = parent_record['id']
14+
return record

tap_trello/streams/organization_actions.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import json
2+
from typing import Dict
3+
24
import singer
5+
from singer import Transformer
36

47
from tap_trello.streams.abstracts import ChildBaseStream
58

@@ -13,14 +16,38 @@ class OrganizationActions(ChildBaseStream):
1316
replication_keys = ["date"]
1417
path = "/organizations/{id}/actions"
1518
parent = "organizations"
16-
bookmark_value = None
19+
params = {'limit': 1000}
20+
21+
def sync(
22+
self,
23+
state: Dict,
24+
transformer: Transformer,
25+
parent_obj: Dict = None,
26+
) -> Dict:
27+
"""Override sync to store state and add date filtering."""
28+
self._sync_state = state
29+
self._sync_parent_obj = parent_obj
1730

31+
return super().sync(state, transformer, parent_obj)
1832

1933
def get_records(self):
20-
url = self.get_url_endpoint(getattr(self, 'parent_obj', None)) if hasattr(self, 'parent_obj') else self.get_url_endpoint()
34+
"""Get records with date filtering for incremental replication."""
35+
url = self.get_url_endpoint(getattr(self, '_sync_parent_obj', None))
2136
params = dict(self.params) if hasattr(self, 'params') else {}
2237
params.pop('page', None)
2338

39+
# Add date filtering for incremental replication
40+
# Access state from the temporarily stored sync state
41+
state = getattr(self, '_sync_state', {})
42+
bookmark_date = self.get_bookmark(state, self.tap_stream_id)
43+
if bookmark_date:
44+
params['since'] = bookmark_date
45+
else:
46+
# Use start_date from config if no bookmark exists
47+
start_date = self.client.config.get('start_date')
48+
if start_date:
49+
params['since'] = start_date
50+
2451
response = self.client.make_request(
2552
self.http_method,
2653
url,

tap_trello/streams/organization_members.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class OrganizationMembers(FullTableStream):
66
replication_method = "FULL_TABLE"
77
path = "/organizations/{id}/members"
88
parent = "organizations"
9+
10+
def modify_object(self, record, parent_record=None):
11+
"""Add organization_id to organization member records."""
12+
if parent_record and 'id' in parent_record:
13+
record["organization_id"] = parent_record['id']
14+
return record

tap_trello/streams/organization_memberships.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ class OrganizationMemberships(FullTableStream):
66
replication_method = "FULL_TABLE"
77
path = "/organizations/{id}/memberships"
88
parent = "organizations"
9+
10+
def modify_object(self, record, parent_record=None):
11+
"""Add organization_id to organization membership records."""
12+
if parent_record and 'id' in parent_record:
13+
record["organization_id"] = parent_record['id']
14+
return record

tests/base.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import os
2+
import unittest
3+
4+
from datetime import datetime as dt
5+
6+
import tap_tester.connections as connections
7+
import tap_tester.menagerie as menagerie
8+
9+
10+
class TrelloBaseTest(unittest.TestCase):
11+
"""Base test class with common setup and utility methods for Trello tap tests"""
12+
13+
START_DATE = ""
14+
START_DATE_FORMAT = "%Y-%m-%dT00:00:00Z"
15+
TEST_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
16+
17+
def setUp(self):
18+
missing_envs = [x for x in [
19+
"TAP_TRELLO_CONSUMER_KEY",
20+
"TAP_TRELLO_CONSUMER_SECRET",
21+
"TAP_TRELLO_ACCESS_TOKEN",
22+
"TAP_TRELLO_ACCESS_TOKEN_SECRET",
23+
] if os.getenv(x) == None]
24+
if len(missing_envs) != 0:
25+
raise Exception("Missing environment variables: {}".format(missing_envs))
26+
27+
def name(self):
28+
return "tap_tester_trello_base_test"
29+
30+
def get_type(self):
31+
return "platform.trello"
32+
33+
def get_credentials(self):
34+
return {
35+
'consumer_key': os.getenv('TAP_TRELLO_CONSUMER_KEY'),
36+
'consumer_secret': os.getenv('TAP_TRELLO_CONSUMER_SECRET'),
37+
'access_token': os.getenv('TAP_TRELLO_ACCESS_TOKEN'),
38+
'access_token_secret': os.getenv('TAP_TRELLO_ACCESS_TOKEN_SECRET'),
39+
}
40+
41+
def tap_name(self):
42+
return "tap-trello"
43+
44+
def get_properties(self):
45+
return {
46+
'start_date': dt.strftime(dt.utcnow(), self.START_DATE_FORMAT),
47+
}
48+
49+
def testable_streams(self):
50+
return self.expected_check_streams().difference(self.untestable_streams())
51+
52+
def untestable_streams(self):
53+
return set()
54+
55+
def expected_check_streams(self):
56+
return {
57+
'actions',
58+
'board_custom_fields',
59+
'board_labels',
60+
'board_memberships',
61+
'boards',
62+
'card_attachments',
63+
'card_custom_field_items',
64+
'cards',
65+
'checklists',
66+
'lists',
67+
'members',
68+
'organization_actions',
69+
'organization_members',
70+
'organization_memberships',
71+
'organizations',
72+
'users'
73+
}
74+
75+
def expected_sync_streams(self):
76+
return self.expected_check_streams()
77+
78+
def expected_full_table_streams(self):
79+
return {
80+
'boards',
81+
'board_custom_fields',
82+
'board_labels',
83+
'board_memberships',
84+
'cards',
85+
'card_attachments',
86+
'card_custom_field_items',
87+
'checklists',
88+
'lists',
89+
'members',
90+
'organizations',
91+
'organization_members',
92+
'organization_memberships',
93+
'users',
94+
}
95+
96+
def expected_full_table_sync_streams(self):
97+
return self.expected_full_table_streams()
98+
99+
def expected_incremental_streams(self):
100+
return {
101+
'actions',
102+
'organization_actions'
103+
}
104+
105+
def expected_pks(self):
106+
return {
107+
'actions': {"id"},
108+
'boards': {"id"},
109+
'board_custom_fields': {"id", "boardId"},
110+
'board_labels': {"id", "boardId"},
111+
'board_memberships': {"id", "boardId"},
112+
'cards': {'id'},
113+
'card_attachments': {"id", "card_id"},
114+
'card_custom_field_items': {"id", "card_id"},
115+
'checklists': {'id'},
116+
'lists': {"id"},
117+
'members': {"id"},
118+
'organizations': {"id"},
119+
'organization_actions': {"id", "organization_id"},
120+
'organization_members': {"id", "organization_id"},
121+
'organization_memberships': {"id", "organization_id"},
122+
'users': {"id", "boardId"}
123+
}
124+
125+
def expected_automatic_fields(self):
126+
return {
127+
'actions': {"id", "date"},
128+
'boards': {"id"},
129+
'board_custom_fields': {"id", "boardId"},
130+
'board_labels': {"id", "boardId"},
131+
'board_memberships': {"id", "boardId"},
132+
'cards': {'id'},
133+
'card_attachments': {"id", "card_id"},
134+
'card_custom_field_items': {"id", "card_id"},
135+
'checklists': {'id'},
136+
'lists': {"id"},
137+
'members': {"id"},
138+
'organizations': {"id"},
139+
'organization_actions': {"id", "organization_id", "date"},
140+
'organization_members': {"id", "organization_id"},
141+
'organization_memberships': {"id", "organization_id"},
142+
'users': {"id", "boardId"}
143+
}
144+
145+
def select_all_streams_and_fields(self, conn_id, catalogs, select_all_fields: bool = True):
146+
"""
147+
Select all streams and optionally all fields within streams.
148+
149+
Args:
150+
conn_id: Connection ID
151+
catalogs: List of catalogs to select
152+
select_all_fields: If False, only select automatic fields
153+
"""
154+
for catalog in catalogs:
155+
schema = menagerie.get_annotated_schema(conn_id, catalog['stream_id'])
156+
157+
non_selected_properties = []
158+
if not select_all_fields:
159+
# get a list of all properties so that none are selected
160+
non_selected_properties = schema.get('annotated-schema', {}).get(
161+
'properties', {})
162+
# remove properties that are automatic
163+
for prop in self.expected_automatic_fields().get(catalog['stream_name'], []):
164+
if prop in non_selected_properties:
165+
del non_selected_properties[prop]
166+
additional_md = []
167+
168+
connections.select_catalog_and_fields_via_metadata(
169+
conn_id, catalog, schema, additional_md=additional_md,
170+
non_selected_fields=non_selected_properties.keys()
171+
)

0 commit comments

Comments
 (0)