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.
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/CHANGELOG.md +7 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/PKG-INFO +7 -4
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/README.md +3 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/pyproject.toml +3 -2
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/google_drive/utils.py +22 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/fetchers/google_drive/fetcher.py +3 -1
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/retrievers/google_drive/retriever.py +7 -7
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/tests/test_fetcher.py +45 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/.gitignore +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/LICENSE.txt +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/pydoc/config_docusaurus.yml +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/google_drive/__init__.py +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/google_drive/errors.py +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/common/py.typed +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/fetchers/google_drive/__init__.py +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/fetchers/py.typed +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/retrievers/google_drive/__init__.py +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/src/haystack_integrations/components/retrievers/py.typed +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/tests/__init__.py +0 -0
- {google_drive_haystack-1.1.0 → google_drive_haystack-2.0.0}/tests/test_retriever.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: google-drive-haystack
|
|
3
|
-
Version:
|
|
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.
|
|
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
|
[](https://pypi.org/project/google-drive-haystack)
|
|
31
31
|
[](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
|
[](https://pypi.org/project/google-drive-haystack)
|
|
4
4
|
[](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
|
+
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
|
-
|
|
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
|
-
`
|
|
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
|
|
71
|
-
from haystack_integrations.utils.oauth import
|
|
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
|
-
|
|
78
|
-
token_source=
|
|
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 `
|
|
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 `
|
|
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"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|