diff --git a/airbyte-integrations/connectors/source-google-drive/metadata.yaml b/airbyte-integrations/connectors/source-google-drive/metadata.yaml index 111341af029b..8dc9d674089b 100644 --- a/airbyte-integrations/connectors/source-google-drive/metadata.yaml +++ b/airbyte-integrations/connectors/source-google-drive/metadata.yaml @@ -7,7 +7,7 @@ data: connectorSubtype: file connectorType: source definitionId: 9f8dda77-1048-4368-815b-269bf54ee9b8 - dockerImageTag: 0.2.4 + dockerImageTag: 0.3.0 dockerRepository: airbyte/source-google-drive githubIssueLabel: source-google-drive icon: google-drive.svg diff --git a/airbyte-integrations/connectors/source-google-drive/poetry.lock b/airbyte-integrations/connectors/source-google-drive/poetry.lock index 4fece01f2383..f48ef76cca1b 100644 --- a/airbyte-integrations/connectors/source-google-drive/poetry.lock +++ b/airbyte-integrations/connectors/source-google-drive/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "airbyte-cdk" -version = "6.38.3" +version = "6.38.5" description = "A framework for writing Airbyte Connectors." optional = false python-versions = "<3.13,>=3.10" files = [ - {file = "airbyte_cdk-6.38.3-py3-none-any.whl", hash = "sha256:3bb29acf69da7188bd10f8c23cd2e683b08079bde4f5d16d87718b39cfa92cb2"}, - {file = "airbyte_cdk-6.38.3.tar.gz", hash = "sha256:dfece60e4fbf51eae2f565f2389afaa9b4c7827f342ee77fcfdfdd7d7742a255"}, + {file = "airbyte_cdk-6.38.5-py3-none-any.whl", hash = "sha256:0e1e7471a2e7919b377d914089aa3b45a955f6e928c1f96651b2566332a8cba1"}, + {file = "airbyte_cdk-6.38.5.tar.gz", hash = "sha256:c4e7182238acadd01301c173e04044090cdbb7ed8b88b8ec40097ade2ab04741"}, ] [package.dependencies] @@ -129,20 +129,20 @@ files = [ [[package]] name = "attrs" -version = "25.1.0" +version = "25.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" files = [ - {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, - {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, + {file = "attrs-25.2.0-py3-none-any.whl", hash = "sha256:611344ff0a5fed735d86d7784610c84f8126b95e549bcad9ff61b4242f2d386b"}, + {file = "attrs-25.2.0.tar.gz", hash = "sha256:18a06db706db43ac232cce80443fcd9f2500702059ecf53489e3c5a3f417acaf"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -766,21 +766,21 @@ files = [ [[package]] name = "google-api-core" -version = "2.24.1" +version = "2.24.2" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, - {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, + {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, + {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, ] [package.dependencies] -google-auth = ">=2.14.1,<3.0.dev0" -googleapis-common-protos = ">=1.56.2,<2.0.dev0" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" -requests = ">=2.18.0,<3.0.0.dev0" +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" +proto-plus = ">=1.22.3,<2.0.0" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] @@ -1825,17 +1825,17 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "proto-plus" -version = "1.26.0" +version = "1.26.1" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" files = [ - {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, - {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] [package.dependencies] -protobuf = ">=3.19.0,<6.0.0dev" +protobuf = ">=3.19.0,<7.0.0" [package.extras] testing = ["google-api-core (>=1.31.5)"] @@ -3312,4 +3312,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10,<3.12" -content-hash = "4809ab3188755f5d93d0a0af97895bdd34d8960df72e28f92c55e94894cc87a1" +content-hash = "9fbc5121c9d058b32df444857f06ab120b52f3efa0d60c7b9b11ed791e6495f6" diff --git a/airbyte-integrations/connectors/source-google-drive/pyproject.toml b/airbyte-integrations/connectors/source-google-drive/pyproject.toml index 057b1401a8a1..c83662899ad1 100644 --- a/airbyte-integrations/connectors/source-google-drive/pyproject.toml +++ b/airbyte-integrations/connectors/source-google-drive/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",] build-backend = "poetry.core.masonry.api" [tool.poetry] -version = "0.2.4" +version = "0.3.0" name = "source-google-drive" description = "Source implementation for Google Drive." authors = [ "Airbyte ",] @@ -21,7 +21,7 @@ google-api-python-client = "==2.104.0" google-auth-httplib2 = "==0.1.1" google-auth-oauthlib = "==1.1.0" google-api-python-client-stubs = "==1.18.0" -airbyte-cdk = {extras = ["file-based"], version = "^6.33.6"} +airbyte-cdk = {extras = ["file-based"], version = "^6.38.5"} [tool.poetry.scripts] diff --git a/airbyte-integrations/connectors/source-google-drive/source_google_drive/source.py b/airbyte-integrations/connectors/source-google-drive/source_google_drive/source.py index dcd2f39838b5..9e0e07ae526f 100644 --- a/airbyte-integrations/connectors/source-google-drive/source_google_drive/source.py +++ b/airbyte-integrations/connectors/source-google-drive/source_google_drive/source.py @@ -10,6 +10,7 @@ from airbyte_cdk.sources.file_based.file_based_source import FileBasedSource from airbyte_cdk.sources.file_based.stream.cursor.default_file_based_cursor import DefaultFileBasedCursor from source_google_drive.spec import SourceGoogleDriveSpec +from source_google_drive.stream_permissions_reader import SourceGoogleDriveStreamPermissionsReader from source_google_drive.stream_reader import SourceGoogleDriveStreamReader @@ -22,6 +23,7 @@ def __init__(self, catalog: Optional[ConfiguredAirbyteCatalog], config: Optional config=config, state=state, cursor_cls=DefaultFileBasedCursor, + stream_permissions_reader=SourceGoogleDriveStreamPermissionsReader(), ) def spec(self, *args: Any, **kwargs: Any) -> ConnectorSpecification: diff --git a/airbyte-integrations/connectors/source-google-drive/source_google_drive/stream_permissions_reader.py b/airbyte-integrations/connectors/source-google-drive/source_google_drive/stream_permissions_reader.py new file mode 100644 index 000000000000..6b4c4f3bafe8 --- /dev/null +++ b/airbyte-integrations/connectors/source-google-drive/source_google_drive/stream_permissions_reader.py @@ -0,0 +1,221 @@ +# Copyright (c) 2025 Airbyte, Inc., all rights reserved. + +import json +import logging +import uuid +from datetime import datetime +from typing import Any, Dict, Iterator, List, Tuple + +import pytz +from google.oauth2 import credentials, service_account +from googleapiclient.discovery import build + +from airbyte_cdk import AirbyteTracedException, FailureType +from airbyte_cdk.sources.file_based.file_based_stream_permissions_reader import AbstractFileBasedStreamPermissionsReader +from airbyte_cdk.sources.streams.core import package_name_from_class +from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader +from source_google_drive.exceptions import ErrorFetchingMetadata +from source_google_drive.spec import RemoteIdentity, RemoteIdentityType, RemotePermissions, SourceGoogleDriveSpec + + +DRIVE_SERVICE_SCOPES = [ + "https://www.googleapis.com/auth/admin.directory.group.readonly", + "https://www.googleapis.com/auth/admin.directory.group.member.readonly", + "https://www.googleapis.com/auth/admin.directory.user.readonly", +] + +PUBLIC_PERMISSION_IDS = [ + "anyoneWithLink", + "anyoneCanFind", + "domainCanFind", + "domainWithLink", +] + + +def datetime_now() -> datetime: + return datetime.now(pytz.UTC) + + +class SourceGoogleDriveStreamPermissionsReader(AbstractFileBasedStreamPermissionsReader): + def __init__(self): + super().__init__() + self._drive_service = None + self._directory_service = None + + @property + def config(self) -> SourceGoogleDriveSpec: + return self._config + + @config.setter + def config(self, value: SourceGoogleDriveSpec): + assert isinstance(value, SourceGoogleDriveSpec) + self._config = value + + def _build_google_service(self, service_name: str, version: str, scopes: List[str] = None): + if self.config is None: + # We shouldn't hit this; config should always get set before attempting to + # list or read files. + raise ValueError(f"Source config is missing; cannot create the Google {service_name} client.") + try: + if self.config.credentials.auth_type == "Client": + creds = credentials.Credentials.from_authorized_user_info(self.config.credentials.dict()) + else: + creds = service_account.Credentials.from_service_account_info( + json.loads(self.config.credentials.service_account_info), scopes=scopes + ) + google_service = build(service_name, version, credentials=creds) + except Exception as e: + raise AirbyteTracedException( + internal_message=str(e), + message=f"Could not authenticate with Google {service_name}. Please check your credentials.", + failure_type=FailureType.config_error, + exception=e, + ) + + return google_service + + @property + def google_drive_service(self): + if self._drive_service is None: + self._drive_service = self._build_google_service("drive", "v3") + return self._drive_service + + @property + def google_directory_service(self): + if self._directory_service is None: + self._directory_service = self._build_google_service("admin", "directory_v1", DRIVE_SERVICE_SCOPES) + return self._directory_service + + def _get_looping_google_api_list_response( + self, service: Any, key: str, args: dict[str, Any], logger: logging.Logger + ) -> Iterator[dict[str, Any]]: + try: + looping = True + next_page_token: str | None = None + while looping: + rsp = service.list(pageToken=next_page_token, **args).execute() + next_page_token = rsp.get("nextPageToken") + items: list[dict[str, Any]] = rsp.get(key) + + if items is None or len(items) == 0: + looping = False + break + + if rsp.get("nextPageToken") is None: + looping = False + else: + next_page_token = rsp.get("nextPageToken") + + for item in items: + yield item + except Exception as e: + logger.error(f"There was an error listing {key} with {args}: {str(e)}") + raise e + + def _to_remote_file_identity(self, identity: dict[str, Any]) -> RemoteIdentity | None: + if identity.get("id") in PUBLIC_PERMISSION_IDS: + return None + if identity.get("deleted") is True: + return None + + return RemoteIdentity( + modified_at=datetime.now(), + id=uuid.uuid4(), + remote_id=identity.get("emailAddress"), + name=identity.get("name"), + email_address=identity.get("emailAddress"), + type=identity.get("type"), + description=None, + ) + + def get_file_permissions(self, file_id: str, file_name: str, logger: logging.Logger) -> Tuple[List[RemoteIdentity], bool]: + """ + Retrieves the permissions of a file in Google Drive and checks for public access. + + Args: + file_id (str): The file to get permissions for. + file_name (str): The name of the file to get permissions for. + logger (logging.Logger): Logger for debugging and information. + + Returns: + Tuple(List[RemoteFileIdentity], boolean): A list of RemoteFileIdentity objects containing permission details. + """ + try: + request = self.google_drive_service.permissions().list( + fileId=file_id, + fields="permissions, permissions/role, permissions/type, permissions/id, permissions/emailAddress", + supportsAllDrives=True, + ) + response = request.execute() + permissions = response.get("permissions", []) + is_public = False + + remote_identities = [] + + for p in permissions: + identity = self._to_remote_file_identity(p) + if p.get("id") in PUBLIC_PERMISSION_IDS: + is_public = True + if identity is not None: + remote_identities.append(identity) + + return remote_identities, is_public + except Exception as e: + raise ErrorFetchingMetadata(f"An error occurred while retrieving file permissions: {str(e)}") + + def get_file_acl_permissions(self, file: Any, logger: logging.Logger) -> Dict[str, Any]: + remote_identities, is_public = self.get_file_permissions(file.id, file_name=file.uri, logger=logger) + return RemotePermissions( + id=file.id, + file_path=file.uri, + allowed_identity_remote_ids=[p.remote_id for p in remote_identities], + publicly_accessible=is_public, + ).dict(exclude_none=True) + + def load_identity_groups(self, logger: logging.Logger) -> Iterator[Dict[str, Any]]: + domain = self.config.delivery_method.domain + if not domain: + logger.info("No domain provided. Trying to fetch identities from the user workspace.") + api_args = {"customer": "my_customer"} + else: + api_args = {"domain": domain} + + users_api = self.google_directory_service.users() + groups_api = self.google_directory_service.groups() + members_api = self.google_directory_service.members() + + for user in self._get_looping_google_api_list_response(users_api, "users", args=api_args, logger=logger): + rfp = RemoteIdentity( + id=uuid.uuid4(), + remote_id=user["primaryEmail"], + name=user["name"]["fullName"] if user["name"] is not None else None, + email_address=user["primaryEmail"], + member_email_addresses=[x["address"] for x in user["emails"]], + type=RemoteIdentityType.USER, + modified_at=datetime_now(), + ) + yield rfp.dict() + + for group in self._get_looping_google_api_list_response(groups_api, "groups", args=api_args, logger=logger): + rfp = RemoteIdentity( + id=uuid.uuid4(), + remote_id=group["email"], + name=group["name"], + email_address=group["email"], + type=RemoteIdentityType.GROUP, + modified_at=datetime_now(), + ) + + for member in self._get_looping_google_api_list_response(members_api, "members", {"groupKey": group["id"]}, logger): + rfp.member_email_addresses = rfp.member_email_addresses or [] + rfp.member_email_addresses.append(member["email"]) + + yield rfp.dict() + + @property + def file_permissions_schema(self) -> Dict[str, Any]: + return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("file_permissions") + + @property + def identities_schema(self) -> Dict[str, Any]: + return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("identities") diff --git a/airbyte-integrations/connectors/source-google-drive/source_google_drive/stream_reader.py b/airbyte-integrations/connectors/source-google-drive/source_google_drive/stream_reader.py index 39d7a1bfafdb..c0dbb1952bbf 100644 --- a/airbyte-integrations/connectors/source-google-drive/source_google_drive/stream_reader.py +++ b/airbyte-integrations/connectors/source-google-drive/source_google_drive/stream_reader.py @@ -58,24 +58,6 @@ GOOGLE_DRAWING_MIME_TYPE: {EXPORT_MEDIA_MIME_TYPE_KEY: EXPORT_MEDIA_MIME_TYPE_PDF, DOCUMENT_FILE_EXTENSION_KEY: ".pdf"}, } -PUBLIC_PERMISSION_IDS = [ - "anyoneWithLink", - "anyoneCanFind", - "domainCanFind", - "domainWithLink", -] - - -DRIVE_SERVICE_SCOPES = [ - "https://www.googleapis.com/auth/admin.directory.group.readonly", - "https://www.googleapis.com/auth/admin.directory.group.member.readonly", - "https://www.googleapis.com/auth/admin.directory.user.readonly", -] - - -def datetime_now() -> datetime: - return datetime.now(pytz.UTC) - class GoogleDriveRemoteFile(RemoteFile): id: str @@ -90,7 +72,6 @@ class SourceGoogleDriveStreamReader(AbstractFileBasedStreamReader): def __init__(self): super().__init__() self._drive_service = None - self._directory_service = None @property def config(self) -> SourceGoogleDriveSpec: @@ -139,12 +120,6 @@ def google_drive_service(self): self._drive_service = self._build_google_service("drive", "v3") return self._drive_service - @property - def google_directory_service(self): - if self._directory_service is None: - self._directory_service = self._build_google_service("admin", "directory_v1", DRIVE_SERVICE_SCOPES) - return self._directory_service - def get_matching_files(self, globs: List[str], prefix: Optional[str], logger: logging.Logger) -> Iterable[RemoteFile]: """ Get all files matching the specified glob patterns. @@ -317,137 +292,3 @@ def get_file(self, file: GoogleDriveRemoteFile, local_directory: str, logger: lo except Exception as e: raise ErrorDownloadingFile(f"There was an error while trying to download the file {file.uri}: {str(e)}") - - def get_file_permissions(self, file_id: str, file_name: str, logger: logging.Logger) -> Tuple[List[RemoteIdentity], bool]: - """ - Retrieves the permissions of a file in Google Drive and checks for public access. - - Args: - file_id (str): The file to get permissions for. - file_name (str): The name of the file to get permissions for. - logger (logging.Logger): Logger for debugging and information. - - Returns: - Tuple(List[RemoteFileIdentity], boolean): A list of RemoteFileIdentity objects containing permission details. - """ - try: - request = self.google_drive_service.permissions().list( - fileId=file_id, - fields="permissions, permissions/role, permissions/type, permissions/id, permissions/emailAddress", - supportsAllDrives=True, - ) - response = request.execute() - permissions = response.get("permissions", []) - is_public = False - - remote_identities = [] - - for p in permissions: - identity = self._to_remote_file_identity(p) - if p.get("id") in PUBLIC_PERMISSION_IDS: - is_public = True - if identity is not None: - remote_identities.append(identity) - - return remote_identities, is_public - except Exception as e: - raise ErrorFetchingMetadata(f"An error occurred while retrieving file permissions: {str(e)}") - - def _to_remote_file_identity(self, identity: dict[str, Any]) -> RemoteIdentity | None: - if identity.get("id") in PUBLIC_PERMISSION_IDS: - return None - if identity.get("deleted") is True: - return None - - return RemoteIdentity( - modified_at=datetime.now(), - id=uuid.uuid4(), - remote_id=identity.get("emailAddress"), - name=identity.get("name"), - email_address=identity.get("emailAddress"), - type=identity.get("type"), - description=None, - ) - - def get_file_acl_permissions(self, file: GoogleDriveRemoteFile, logger: logging.Logger) -> Dict[str, Any]: - remote_identities, is_public = self.get_file_permissions(file.id, file_name=file.uri, logger=logger) - return RemotePermissions( - id=file.id, - file_path=file.uri, - allowed_identity_remote_ids=[p.remote_id for p in remote_identities], - publicly_accessible=is_public, - ).dict(exclude_none=True) - - def _get_looping_google_api_list_response( - self, service: Any, key: str, args: dict[str, Any], logger: logging.Logger - ) -> Iterator[dict[str, Any]]: - try: - looping = True - next_page_token: str | None = None - while looping: - rsp = service.list(pageToken=next_page_token, **args).execute() - next_page_token = rsp.get("nextPageToken") - items: list[dict[str, Any]] = rsp.get(key) - - if items is None or len(items) == 0: - looping = False - break - - if rsp.get("nextPageToken") is None: - looping = False - else: - next_page_token = rsp.get("nextPageToken") - - for item in items: - yield item - except Exception as e: - logger.error(f"There was an error listing {key} with {args}: {str(e)}") - raise e - - def load_identity_groups(self, logger: logging.Logger) -> Dict[str, Any]: - domain = self.config.delivery_method.domain - if not domain: - logger.info("No domain provided. Trying to fetch identities from the user workspace.") - api_args = {"customer": "my_customer"} - else: - api_args = {"domain": domain} - - users_api = self.google_directory_service.users() - groups_api = self.google_directory_service.groups() - members_api = self.google_directory_service.members() - - for user in self._get_looping_google_api_list_response(users_api, "users", args=api_args, logger=logger): - rfp = RemoteIdentity( - id=uuid.uuid4(), - remote_id=user["primaryEmail"], - name=user["name"]["fullName"] if user["name"] is not None else None, - email_address=user["primaryEmail"], - member_email_addresses=[x["address"] for x in user["emails"]], - type=RemoteIdentityType.USER, - modified_at=datetime_now(), - ) - yield rfp.dict() - - for group in self._get_looping_google_api_list_response(groups_api, "groups", args=api_args, logger=logger): - rfp = RemoteIdentity( - id=uuid.uuid4(), - remote_id=group["email"], - name=group["name"], - email_address=group["email"], - type=RemoteIdentityType.GROUP, - modified_at=datetime_now(), - ) - - for member in self._get_looping_google_api_list_response(members_api, "members", {"groupKey": group["id"]}, logger): - rfp.member_email_addresses = rfp.member_email_addresses or [] - rfp.member_email_addresses.append(member["email"]) - - yield rfp.dict() - - @property - def file_permissions_schema(self) -> Dict[str, Any]: - return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("file_permissions") - - @property - def identities_schema(self) -> Dict[str, Any]: - return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("identities") diff --git a/airbyte-integrations/connectors/source-google-drive/unit_tests/test_utils.py b/airbyte-integrations/connectors/source-google-drive/unit_tests/test_utils.py index 2740a3d88ee5..a30f8c573ca2 100644 --- a/airbyte-integrations/connectors/source-google-drive/unit_tests/test_utils.py +++ b/airbyte-integrations/connectors/source-google-drive/unit_tests/test_utils.py @@ -15,7 +15,7 @@ ("https://drive.google.com/drive/u/0/folders/1q2w3e4r5t6y7u8i9o0p?usp=link_sharing", "1q2w3e4r5t6y7u8i9o0p", False), ("https://drive.google.com/drive/u/0/folders/1q2w3e4r5t6y7u8i9o0p#abc", "1q2w3e4r5t6y7u8i9o0p", False), ("https://docs.google.com/document/d/fsgfjdsh", None, True), - ("https://drive.google.com/drive/my-drive", None, True), + ("https://drive.google.com/drive/my-drive", "root", False), ("http://drive.google.com/drive/u/0/folders/1q2w3e4r5t6y7u8i9o0p/", None, True), ("https://drive.google.com/", None, True), ], diff --git a/docs/integrations/sources/google-drive.md b/docs/integrations/sources/google-drive.md index 89fb08fd63a8..f8aab86b8be5 100644 --- a/docs/integrations/sources/google-drive.md +++ b/docs/integrations/sources/google-drive.md @@ -320,6 +320,7 @@ By default, this stream is enabled and retrieves information about **users and g | Version | Date | Pull Request | Subject | |---------|------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------| +| 0.3.0 | 2025-03-11 | [55689](https://github.com/airbytehq/airbyte/pull/55689) | Refactor to use new Stream Permissions Reader | | 0.2.4 | 2025-03-08 | [55349](https://github.com/airbytehq/airbyte/pull/55349) | Update dependencies | | 0.2.3 | 2025-03-01 | [54955](https://github.com/airbytehq/airbyte/pull/54955) | Update dependencies | | 0.2.2 | 2025-02-22 | [54416](https://github.com/airbytehq/airbyte/pull/54416) | Update dependencies |