-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add list_google_docs_revisions action
- Loading branch information
1 parent
90a0e8a
commit b85a941
Showing
19 changed files
with
487 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
admyral/actions/integrations/compliance/google_drive.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
from typing import Annotated | ||
from googleapiclient.discovery import build | ||
from google.oauth2.service_account import Credentials as ServiceAccountCredentials | ||
from difflib import unified_diff | ||
import requests | ||
from dateutil import parser | ||
|
||
from admyral.action import action, ArgumentMetadata | ||
from admyral.context import ctx | ||
from admyral.typings import JsonValue | ||
|
||
|
||
@action( | ||
display_name="List Google Docs Revisions", | ||
display_namespace="Google Drive", | ||
description="Fetch revisions of a Google Docs document.", | ||
secrets_placeholders=["GOOGLE_DRIVE_SECRET"], | ||
) | ||
def list_google_docs_revisions( | ||
file_id: Annotated[ | ||
str, | ||
ArgumentMetadata( | ||
display_name="File ID", description="The ID of the Google Doc." | ||
), | ||
], | ||
start_time: Annotated[ | ||
str | None, | ||
ArgumentMetadata( | ||
display_name="Start Time", | ||
description="The start time for the revisions to list. Must be in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ).", | ||
), | ||
] = None, | ||
end_time: Annotated[ | ||
str | None, | ||
ArgumentMetadata( | ||
display_name="End Time", | ||
description="The end time for the revisions to list. Must be in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ).", | ||
), | ||
] = None, | ||
) -> list[dict[str, JsonValue]]: | ||
# https://googleapis.github.io/google-api-python-client/docs/dyn/drive_v3.revisions.html#list | ||
|
||
secret = ctx.get().secrets.get("GOOGLE_DRIVE_SECRET") | ||
creds = ServiceAccountCredentials.from_service_account_info( | ||
info=secret, scopes=["https://www.googleapis.com/auth/drive.readonly"] | ||
) | ||
|
||
drive_service = build("drive", "v3", credentials=creds) | ||
|
||
request = drive_service.revisions().list( | ||
fileId=file_id, | ||
fields="nextPageToken, revisions(id, modifiedTime, lastModifyingUser, exportLinks)", | ||
) | ||
|
||
response = request.execute() | ||
revisions = response.get("revisions", []) | ||
|
||
if start_time: | ||
start_time = parser.parse(start_time) | ||
if end_time: | ||
end_time = parser.parse(end_time) | ||
|
||
result = [] | ||
|
||
while revisions: | ||
for revision in revisions: | ||
revision_id = revision["id"] | ||
modified_time = parser.parse(revision["modifiedTime"]) | ||
|
||
if (start_time and modified_time < start_time) or ( | ||
end_time and end_time < modified_time | ||
): | ||
# Skip revisions outside the time range | ||
continue | ||
|
||
mime_type = "text/plain" | ||
export_link = revision["exportLinks"][mime_type] | ||
export_response = requests.get( | ||
export_link, | ||
headers={"Authorization": f"Bearer {creds.token}", "Accept": mime_type}, | ||
) | ||
export_response.raise_for_status() | ||
|
||
prev_text = "" if len(result) == 0 else result[-1]["content"] | ||
diff = "\n".join( | ||
unified_diff( | ||
prev_text, | ||
export_response.text.splitlines(), | ||
fromfile="Before", | ||
tofile="After", | ||
lineterm="", | ||
) | ||
) | ||
|
||
result.append( | ||
{ | ||
"id": revision_id, | ||
"lastModifyingUser": revision["lastModifyingUser"]["emailAddress"], | ||
"modifiedTime": revision["modifiedTime"], | ||
"content": export_response.text, | ||
"diff": diff, | ||
} | ||
) | ||
|
||
if request := drive_service.revisions().list_next(request, response): | ||
response = request.execute() | ||
revisions = response.get("revisions", []) | ||
else: | ||
break | ||
|
||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,20 @@ | ||
from typing import Any | ||
|
||
|
||
def _is_json_value(obj: Any) -> bool: | ||
def throw_if_not_allowed_return_type(obj: Any) -> None: | ||
if isinstance(obj, (str, int, float, bool)) or obj is None: | ||
return True | ||
return | ||
elif isinstance(obj, list): | ||
return all(_is_json_value(item) for item in obj) | ||
for item in obj: | ||
throw_if_not_allowed_return_type(item) | ||
elif isinstance(obj, dict): | ||
return all( | ||
isinstance(key, str) and _is_json_value(value) for key, value in obj.items() | ||
) | ||
for key, value in obj.items(): | ||
if not isinstance(key, str): | ||
raise ValueError( | ||
f"Key of type {type(key)} is not allowed in the JSON object. The key must be a string." | ||
) | ||
throw_if_not_allowed_return_type(value) | ||
else: | ||
return False | ||
|
||
|
||
def is_allowed_return_type(obj: Any) -> bool: | ||
return _is_json_value(obj) | ||
|
||
|
||
def throw_if_not_allowed_return_type(obj: Any) -> None: | ||
if not is_allowed_return_type(obj): | ||
raise ValueError( | ||
f"Object of type {type(obj)} is not allowed in the return statement. The type must be JSON." | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"google_drive": "Connect Google Drive", | ||
"apis": "APIs" | ||
} |
87 changes: 87 additions & 0 deletions
87
docs/pages/integrations/google_drive/apis/list_google_docs_revisions.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { Callout } from "nextra/components"; | ||
|
||
# List Google Docs Revisions | ||
|
||
List Google Docs revisions of a document. | ||
|
||
<Callout type="info"> | ||
For more information on the API for listing revisions, see [Drive API v3 | ||
revisions.list](https://developers.google.com/drive/api/reference/rest/v3/revisions/list?hl=de). | ||
</Callout> | ||
|
||
The following scopes are required: | ||
|
||
- `https://www.googleapis.com/auth/drive.readonly` | ||
|
||
<Callout type="warning"> | ||
**IMPORTANT:** In order to make the API work, you need to invite to invite | ||
your service account as a collaborator to your Google document. You need to | ||
use the email of the service account. | ||
</Callout> | ||
|
||
**SDK Import:** | ||
|
||
```python | ||
from admyral.actions import list_google_docs_revisions | ||
``` | ||
|
||
## Arguments: | ||
|
||
| Argument Name | Description | Required | | ||
| --------------------------- | -------------------------------------------------------------------------------------------- | :------: | | ||
| **File ID** `file_id` | The file ID of the Google Doc. | Yes | | ||
| **Start Time** `start_time` | The start time for the revisions to list. Must be in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ). | - | | ||
| **End Time** `end_time` | The end time for the revisions to list. Must be in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ). | - | | ||
|
||
## Returns | ||
|
||
A list of revision objects. Each revision object has the following fields: | ||
|
||
- `id`: The ID of the revision. | ||
- `lastModifyingUser`: The email of the user who last modified the document. | ||
- `modifiedTime`: The timestamp when the modification happened. | ||
- `content`: The content of the revision. | ||
- `diff`: The diff between the content of the current and the previous revision. | ||
|
||
## Required Secrets | ||
|
||
| Secret Placeholder | Description | | ||
| --------------------- | -------------------------------------------------------------------------------------- | | ||
| `GOOGLE_DRIVE_SECRET` | Google Drive secret. See [Google Drive setup](/integrations/google_drive/google_drive) | | ||
|
||
## SDK Example | ||
|
||
```python | ||
result = list_google_docs_revisions( | ||
file_id="1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M", | ||
secrets={ | ||
"GOOGLE_DRIVE_SECRET": "my_stored_google_drive_secret" | ||
} | ||
) | ||
``` | ||
|
||
## Example Output | ||
|
||
```json | ||
[ | ||
{ | ||
"id": "7", | ||
"lastModifyingUser": "[email protected]", | ||
"exportLink": { | ||
"application/rtf": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=rtf", | ||
"application/vnd.oasis.opendocument.text": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=odt", | ||
"text/html": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=html", | ||
"application/pdf": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=pdf", | ||
"text/x-markdown": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=markdown", | ||
"text/markdown": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=markdown", | ||
"application/epub+zip": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=epub", | ||
"application/zip": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=zip", | ||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=docx", | ||
"text/plain": "https://docs.google.com/feeds/download/documents/export/Export?id=1ItOTzHcEaa2ZMOqaQq3jnNtvlfXOk5M-HWYo89Id2vQ&revision=7&exportFormat=txt" | ||
}, | ||
"modifiedTime": "2024-10-11T11:14:08.607Z", | ||
"content": "test test test", | ||
"diff": "--- Before\n+++ After\n@@ -1 +1 @@\n-\n+test test test" | ||
} | ||
] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Connect Google Drive | ||
|
||
1. First, you need to create a service account. For this, go to your Google Cloud console. | ||
|
||
2. Go to **APIs & Services** > **Library**. | ||
|
||
3. Search for **Google Drive API** and enable the API. | ||
|
||
4. Go to **APIs & Services** > **Credentials**. | ||
|
||
5. Click **CREATE CREDENTIALS** > **Service account** and create a service account. | ||
|
||
 | ||
|
||
6. Click on your newly created service account. This should forward you to **IAM & Admin** > **Service Accounts** > your service account. | ||
|
||
7. Click on **KEYS** and then on **ADD KEY** > **Create new key**. Select `JSON` as Key type. Click on the create button. This should start the download of a JSON file. | ||
|
||
 | ||
|
||
8. Go to **IAM & Admin** > **Service Accounts** > **your service account**. In the **Details** page, you should see **Advanced settings**. Open the settings. | ||
|
||
9. Below **Domain-wide Delegation**, there is a **Client ID**. Copy it. | ||
|
||
 | ||
|
||
10. Next, click on **VIEW GOOGLE WORKSPACE ADMIN CONSOLE**. | ||
|
||
11. Log in to your **Google Admin Workspace**. | ||
|
||
12. Inside **Google Admin Workspace**, go to **Security** > **Access and data control** > **API controls**. | ||
|
||
 | ||
|
||
13. Click on **MANAGE DOMAIN WIDE DELEGATION**. | ||
|
||
14. Click on **Add new** to add your new service account. Use the previously copied **Client ID** (Step 9) and paste it here. Additionally, define your required scopes depending on which APIs you want to use (e.g., `List Google Docs Revisions` requires `https://www.googleapis.com/auth/drive.readonly`). | ||
|
||
15. Open the previously downloaded JSON file. | ||
|
||
16. Go to the **Settings** page in **Admyral**. Click on **Create New Secret** in the **Secrets** section. | ||
|
||
17. Give your credential a name, e.g. `Google Drive`. | ||
|
||
18. The following secrets structure is expected: | ||
|
||
| Key | Value | | ||
| ----------------------------- | --------------------------------------------------------------- | | ||
| `type` | Paste the `type` from the JSON file here | | ||
| `project_id` | Paste the `project_id` from the JSON file here | | ||
| `private_key_id` | Paste the `private_key_id` from the JSON file here | | ||
| `private_key` | Paste the `private_key` from the JSON file here | | ||
| `client_email` | Paste the `client_email` from the JSON file here | | ||
| `auth_uri` | Paste the `auth_uri` from the JSON file here | | ||
| `token_uri` | Paste the `token_uri` from the JSON file here | | ||
| `auth_provider_x509_cert_url` | Paste the `auth_provider_x509_cert_url` from the JSON file here | | ||
| `client_x509_cert_url` | Paste the `client_x509_cert_url` from the JSON file here | | ||
| `universe_domain` | Paste the `universe_domain` from the JSON file here | | ||
|
||
19. Click on the **Save** button. | ||
|
||
Alternatively, you can use the following CLI command structure: | ||
|
||
```bash | ||
admyral secret set google_secret --value type=your_type --value project_id=your_project_id --value private_key_id=your_private_key_id --value private_key=your_private_key --value client_email=your_client_email --value auth_uri=your_auth_uri --value token_uri=your_token_uri | ||
--value auth_provider_x509_cert_url=your_auth_provider_x509_cert_url --value client_x509_cert_url=your_client_x509_cert_url --value universe_domain=your_universe_domain | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions
8
examples/access_review/workflows/google_docs_policy_revision_monitoring.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
""" | ||
1. Fetch revisions of a Google Docs document | ||
2. For each revision, prompt the LLM to review the changes/diff | ||
2.1 Is material change? | ||
2.2 Can we find a new entry in the revision history? | ||
""" |
Oops, something went wrong.