google-drive-haystack 1.1.0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/CHANGELOG.md +7 -0
  2. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/PKG-INFO +7 -4
  3. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/README.md +3 -0
  4. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/pyproject.toml +3 -2
  5. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/google_drive/utils.py +22 -0
  6. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/fetchers/google_drive/fetcher.py +3 -1
  7. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/retrievers/google_drive/retriever.py +7 -7
  8. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/tests/test_fetcher.py +45 -0
  9. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/.gitignore +0 -0
  10. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/LICENSE.txt +0 -0
  11. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/pydoc/config_docusaurus.yml +0 -0
  12. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/google_drive/__init__.py +0 -0
  13. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/google_drive/errors.py +0 -0
  14. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/py.typed +0 -0
  15. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/fetchers/google_drive/__init__.py +0 -0
  16. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/fetchers/py.typed +0 -0
  17. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/retrievers/google_drive/__init__.py +0 -0
  18. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
  19. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/tests/__init__.py +0 -0
  20. {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/tests/test_retriever.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/google_drive-v1.1.0] - 2026-06-25
4
+
5
+ ### 🚀 Features
6
+
7
+ - *(google_drive)* Add Google Drive fetcher (#3488)
8
+
9
+
3
10
  ## [integrations/google_drive-v1.0.0] - 2026-06-23
4
11
 
5
12
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: google-drive-haystack
3
- Version: 1.1.0
3
+ Version: 2.0.0
4
4
  Summary: Haystack integration for google_drive
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/google_drive#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -12,14 +12,14 @@ Keywords: drive,fetcher,google,haystack,retriever
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: License :: OSI Approved :: Apache Software License
14
14
  Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
20
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
21
  Classifier: Programming Language :: Python :: Implementation :: PyPy
22
- Requires-Python: >=3.10
22
+ Requires-Python: >=3.11
23
23
  Requires-Dist: haystack-ai>=2.17.0
24
24
  Requires-Dist: httpx>=0.28.0
25
25
  Requires-Dist: tenacity>=8.2.3
@@ -30,6 +30,7 @@ Description-Content-Type: text/markdown
30
30
  [![PyPI - Version](https://img.shields.io/pypi/v/google-drive-haystack.svg)](https://pypi.org/project/google-drive-haystack)
31
31
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/google-drive-haystack.svg)](https://pypi.org/project/google-drive-haystack)
32
32
 
33
+ - [Integration page](https://haystack.deepset.ai/integrations/google-drive)
33
34
  - [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/google_drive/CHANGELOG.md)
34
35
 
35
36
  ---
@@ -37,3 +38,5 @@ Description-Content-Type: text/markdown
37
38
  ## Contributing
38
39
 
39
40
  Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
41
+
42
+ To run integration tests locally, you need to export the `GOOGLE_DRIVE_ACCESS_TOKEN` environment variable.
@@ -3,6 +3,7 @@
3
3
  [![PyPI - Version](https://img.shields.io/pypi/v/google-drive-haystack.svg)](https://pypi.org/project/google-drive-haystack)
4
4
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/google-drive-haystack.svg)](https://pypi.org/project/google-drive-haystack)
5
5
 
6
+ - [Integration page](https://haystack.deepset.ai/integrations/google-drive)
6
7
  - [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/google_drive/CHANGELOG.md)
7
8
 
8
9
  ---
@@ -10,3 +11,5 @@
10
11
  ## Contributing
11
12
 
12
13
  Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
14
+
15
+ To run integration tests locally, you need to export the `GOOGLE_DRIVE_ACCESS_TOKEN` environment variable.
@@ -7,7 +7,7 @@ name = "google-drive-haystack"
7
7
  dynamic = ["version"]
8
8
  description = "Haystack integration for google_drive"
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  license = "Apache-2.0"
12
12
  keywords = [
13
13
  "haystack",
@@ -21,11 +21,11 @@ classifiers = [
21
21
  "License :: OSI Approved :: Apache Software License",
22
22
  "Development Status :: 4 - Beta",
23
23
  "Programming Language :: Python",
24
- "Programming Language :: Python :: 3.10",
25
24
  "Programming Language :: Python :: 3.11",
26
25
  "Programming Language :: Python :: 3.12",
27
26
  "Programming Language :: Python :: 3.13",
28
27
  "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: 3.15",
29
29
  "Programming Language :: Python :: Implementation :: CPython",
30
30
  "Programming Language :: Python :: Implementation :: PyPy",
31
31
  ]
@@ -133,6 +133,7 @@ ignore = [
133
133
  "PLR0912",
134
134
  "PLR0913",
135
135
  "PLR0915",
136
+ "PLR0917",
136
137
  # Allow `Any` type - used legitimately for dynamic types and SDK boundaries
137
138
  "ANN401",
138
139
  ]
@@ -2,6 +2,9 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ import asyncio
6
+ from typing import TypeVar
7
+
5
8
  import httpx
6
9
  from haystack import logging
7
10
  from haystack.utils import Secret
@@ -18,12 +21,31 @@ from .errors import GoogleDriveConfigError, GoogleDriveRequestError
18
21
 
19
22
  logger = logging.getLogger(__name__)
20
23
 
24
+ T = TypeVar("T")
25
+
21
26
  DEFAULT_API_BASE_URL = "https://www.googleapis.com/drive/v3"
22
27
  _RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504})
23
28
  # Fallback backoff when a throttled response carries no `Retry-After` header: 1s, 2s, 4s, ...
24
29
  _EXPONENTIAL_BACKOFF = wait_exponential()
25
30
 
26
31
 
32
+ async def _gather_tasks_with_cancel(tasks: list[asyncio.Task[T]]) -> list[T]:
33
+ """
34
+ Wait for all tasks, cancelling and draining unfinished siblings if one fails.
35
+
36
+ :param tasks: Tasks to wait for.
37
+ :returns:
38
+ The task results in input order.
39
+ """
40
+ try:
41
+ return await asyncio.gather(*tasks)
42
+ except BaseException:
43
+ for task in tasks:
44
+ task.cancel()
45
+ await asyncio.gather(*tasks, return_exceptions=True)
46
+ raise
47
+
48
+
27
49
  def resolve_access_token(access_token: str | Secret) -> str:
28
50
  """Return the bearer token string, resolving it if a `Secret` was passed."""
29
51
  if not isinstance(access_token, Secret):
@@ -14,6 +14,7 @@ from haystack.utils import Secret
14
14
  from haystack_integrations.common.google_drive.errors import GoogleDriveConfigError, GoogleDriveRequestError
15
15
  from haystack_integrations.common.google_drive.utils import (
16
16
  DEFAULT_API_BASE_URL,
17
+ _gather_tasks_with_cancel,
17
18
  build_async_retrying,
18
19
  build_retrying,
19
20
  resolve_access_token,
@@ -197,7 +198,8 @@ class GoogleDriveFetcher:
197
198
  return await self._process_async(client, token, file_id, hints)
198
199
 
199
200
  async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) as client:
200
- results = await asyncio.gather(*(fetch_one(client, file_id, hints) for file_id, hints in resolved))
201
+ tasks = [asyncio.create_task(fetch_one(client, file_id, hints)) for file_id, hints in resolved]
202
+ results = await _gather_tasks_with_cancel(tasks)
201
203
 
202
204
  return {"streams": [stream for stream in results if stream is not None]}
203
205
 
@@ -59,7 +59,7 @@ class GoogleDriveRetriever:
59
59
  file content is needed.
60
60
 
61
61
  The retriever takes a per-user `access_token` as a run input, typically wired from an upstream
62
- `OAuthResolver`. The token must carry a delegated Google OAuth scope that allows search
62
+ `OAuthTokenResolver`. The token must carry a delegated Google OAuth scope that allows search
63
63
  (for example `https://www.googleapis.com/auth/drive.readonly`). The metadata-only
64
64
  `drive.metadata.readonly` scope cannot search file content or export documents.
65
65
 
@@ -67,15 +67,15 @@ class GoogleDriveRetriever:
67
67
  ```python
68
68
  from haystack import Pipeline
69
69
  from haystack.utils import Secret
70
- from haystack_integrations.components.connectors.oauth import OAuthResolver
71
- from haystack_integrations.utils.oauth import RefreshTokenSource
70
+ from haystack_integrations.components.connectors.oauth import OAuthTokenResolver
71
+ from haystack_integrations.utils.oauth import OAuthRefreshTokenSource
72
72
  from haystack_integrations.components.retrievers.google_drive import GoogleDriveRetriever
73
73
 
74
74
  pipeline = Pipeline()
75
75
  pipeline.add_component(
76
76
  "resolver",
77
- OAuthResolver(
78
- token_source=RefreshTokenSource(
77
+ OAuthTokenResolver(
78
+ token_source=OAuthRefreshTokenSource(
79
79
  token_url="https://oauth2.googleapis.com/token",
80
80
  client_id="aaa-bbb-ccc",
81
81
  refresh_token=Secret.from_env_var("GOOGLE_REFRESH_TOKEN"),
@@ -150,7 +150,7 @@ class GoogleDriveRetriever:
150
150
  :param query: The search query string, matched against the full text of files via
151
151
  `fullText contains`.
152
152
  :param access_token: A delegated Google OAuth bearer token for the user whose Drive is searched,
153
- typically wired from an upstream `OAuthResolver` (which emits a plain `str`). A `Secret` is also
153
+ typically wired from an upstream `OAuthTokenResolver` (which emits a plain `str`). A `Secret` is also
154
154
  accepted and resolved internally.
155
155
  :param top_k: Overrides the `top_k` configured at initialization for this run.
156
156
  :returns: A dictionary with a `documents` key holding the list of retrieved `Document` objects.
@@ -187,7 +187,7 @@ class GoogleDriveRetriever:
187
187
  :param query: The search query string, matched against the full text of files via
188
188
  `fullText contains`.
189
189
  :param access_token: A delegated Google OAuth bearer token for the user whose Drive is searched,
190
- typically wired from an upstream `OAuthResolver` (which emits a plain `str`). A `Secret` is also
190
+ typically wired from an upstream `OAuthTokenResolver` (which emits a plain `str`). A `Secret` is also
191
191
  accepted and resolved internally.
192
192
  :param top_k: Overrides the `top_k` configured at initialization for this run.
193
193
  :returns: A dictionary with a `documents` key holding the list of retrieved `Document` objects.
@@ -1,6 +1,7 @@
1
1
  # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
+ import asyncio
4
5
  import os
5
6
  from unittest.mock import AsyncMock, patch
6
7
 
@@ -358,6 +359,50 @@ class TestPipeline:
358
359
 
359
360
  @pytest.mark.asyncio
360
361
  class TestRunAsync:
362
+ async def test_failure_cancels_and_drains_siblings(self):
363
+ fetcher = GoogleDriveFetcher(max_retries=0)
364
+ slow_started = asyncio.Event()
365
+ slow_cancelled = False
366
+ current_task = asyncio.current_task()
367
+ tasks_before = asyncio.all_tasks() - {current_task}
368
+
369
+ async def get(url, *_args, **_kwargs):
370
+ nonlocal slow_cancelled
371
+ if "SLOW" in url:
372
+ slow_started.set()
373
+ try:
374
+ await asyncio.Event().wait()
375
+ except asyncio.CancelledError:
376
+ slow_cancelled = True
377
+ raise
378
+ await slow_started.wait()
379
+ msg = "failed"
380
+ raise httpx.ReadError(msg)
381
+
382
+ targets = [_drive_doc(file_id="SLOW"), _drive_doc(file_id="FAILING")]
383
+ with patch.object(httpx.AsyncClient, "get", AsyncMock(side_effect=get)):
384
+ with pytest.raises(httpx.ReadError):
385
+ await fetcher.run_async(access_token="tok", targets=targets)
386
+
387
+ assert slow_cancelled is True
388
+ assert asyncio.all_tasks() - {current_task} == tasks_before
389
+
390
+ async def test_raise_on_failure_false_skips_failed_files(self):
391
+ fetcher = GoogleDriveFetcher(max_retries=0, raise_on_failure=False)
392
+
393
+ async def get(url, *_args, **_kwargs):
394
+ if "MISSING" in url:
395
+ msg = "failed"
396
+ raise httpx.ReadError(msg)
397
+ return _binary(content=b"ok")
398
+
399
+ targets = [_drive_doc(file_id="MISSING"), _drive_doc(file_id="OK")]
400
+ with patch.object(httpx.AsyncClient, "get", AsyncMock(side_effect=get)):
401
+ streams = (await fetcher.run_async(access_token="tok", targets=targets))["streams"]
402
+
403
+ assert len(streams) == 1
404
+ assert streams[0].data == b"ok"
405
+
361
406
  async def test_downloads_binary(self):
362
407
  fetcher = GoogleDriveFetcher()
363
408
  get = AsyncMock(return_value=_binary(content=b"async-bytes"))