Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Source HubSpot: fix property scopes #18624

Merged
merged 5 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@
- name: HubSpot
sourceDefinitionId: 36c891d9-4bd9-43ac-bad2-10e12756272c
dockerRepository: airbyte/source-hubspot
dockerImageTag: 0.2.2
dockerImageTag: 0.2.3
documentationUrl: https://docs.airbyte.com/integrations/sources/hubspot
icon: hubspot.svg
sourceType: api
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5143,7 +5143,7 @@
supportsNormalization: false
supportsDBT: false
supported_destination_sync_modes: []
- dockerImage: "airbyte/source-hubspot:0.2.2"
- dockerImage: "airbyte/source-hubspot:0.2.3"
spec:
documentationUrl: "https://docs.airbyte.com/integrations/sources/hubspot"
connectionSpecification:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ COPY source_hubspot ./source_hubspot
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]

LABEL io.airbyte.version=0.2.2
LABEL io.airbyte.version=0.2.3
LABEL io.airbyte.name=airbyte/source-hubspot
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#

import logging
from itertools import chain
from typing import Any, Iterator, List, Mapping, MutableMapping, Optional, Tuple, Union

import requests
Expand Down Expand Up @@ -130,6 +131,12 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]:
available_streams = [stream for stream in streams if stream.scope_is_granted(granted_scopes)]
unavailable_streams = [stream for stream in streams if not stream.scope_is_granted(granted_scopes)]
self.logger.info(f"The following streams are unavailable: {[s.name for s in unavailable_streams]}")
partially_available_streams = [stream for stream in streams if not stream.properties_scope_is_granted()]
required_scoped = set(chain(*[x.properties_scopes for x in partially_available_streams]))
self.logger.info(
f"The following streams are partially available: {[s.name for s in partially_available_streams]}, "
f"add the following scopes to download all available data: {required_scoped}"
)
else:
self.logger.info("No scopes to grant when authenticating with API key.")
available_streams = streams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,16 @@ class Stream(HttpStream, ABC):
filter_old_records: bool = True
denormalize_records: bool = False # one record from API response can result in multiple records emitted
raise_on_http_errors: bool = True
granted_scopes: Set = None
properties_scopes: Set = None

@property
@abstractmethod
def scopes(self) -> Set[str]:
"""Set of required scopes. Users need to grant at least one of the scopes for the stream to be avaialble to them"""

def scope_is_granted(self, granted_scopes: Set[str]) -> bool:
self.granted_scopes = set(granted_scopes)
if not self.scopes:
return True
else:
Expand Down Expand Up @@ -631,16 +634,24 @@ def _get_field_props(field_type: str) -> Mapping[str, List[str]]:
@lru_cache()
def properties(self) -> Mapping[str, Any]:
"""Some entities has dynamic set of properties, so we trying to resolve those at runtime"""
if not self.entity:
return {}

props = {}
if not self.entity:
return props
if not self.properties_scope_is_granted():
logger.warning(
f"Check your API key has the following permissions granted: {self.properties_scopes}, "
f"to be able to fetch all properties available."
)
return props
data, response = self._api.get(f"/properties/v2/{self.entity}/properties")
for row in data:
props[row["name"]] = self._get_field_props(row["type"])

return props

def properties_scope_is_granted(self):
return not self.properties_scopes - self.granted_scopes if self.properties_scopes and self.granted_scopes else True

def _flat_associations(self, records: Iterable[MutableMapping]) -> Iterable[MutableMapping]:
"""When result has associations we prefer to have it flat, so we transform this:

Expand Down Expand Up @@ -1127,6 +1138,7 @@ class ContactsListMemberships(Stream):
page_field = "vid-offset"
primary_key = "canonical-vid"
scopes = {"crm.objects.contacts.read"}
properties_scopes = {"crm.schemas.contacts.read"}

def _transform(self, records: Iterable) -> Iterable:
"""Extracting list membership records from contacts
Expand Down Expand Up @@ -1421,6 +1433,7 @@ class PropertyHistory(Stream):
limit_field = "count"
limit = 100
scopes = {"crm.objects.contacts.read"}
properties_scopes = {"crm.schemas.contacts.read"}

def request_params(
self,
Expand Down