Skip to content

Commit

Permalink
feat: add list_google_docs_revisions action
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgrittner committed Oct 11, 2024
1 parent 90a0e8a commit b85a941
Show file tree
Hide file tree
Showing 19 changed files with 487 additions and 17 deletions.
2 changes: 2 additions & 0 deletions admyral/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
list_review_history_for_pull_request,
compare_two_github_commits,
list_merged_pull_requests_without_approval,
list_google_docs_revisions,
)
from admyral.actions.integrations.cloud import (
steampipe_query_aws,
Expand Down Expand Up @@ -141,4 +142,5 @@
"get_time_interval_of_last_n_hours",
"format_json_to_list_view_string",
"run_sql_query",
"list_google_docs_revisions",
]
4 changes: 4 additions & 0 deletions admyral/actions/integrations/compliance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
compare_two_github_commits,
list_merged_pull_requests_without_approval,
)
from admyral.actions.integrations.compliance.google_drive import (
list_google_docs_revisions,
)

__all__ = [
"list_retool_inactive_users",
Expand All @@ -24,4 +27,5 @@
"list_review_history_for_pull_request",
"compare_two_github_commits",
"list_merged_pull_requests_without_approval",
"list_google_docs_revisions",
]
111 changes: 111 additions & 0 deletions admyral/actions/integrations/compliance/google_drive.py
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
3 changes: 3 additions & 0 deletions admyral/cli/secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ def set(ctx: click.Context, secret_id: str, value: list[tuple[str, str]]) -> Non
"""Create a secret in Admyral"""
capture(event_name="secret:set")
client: AdmyralClient = ctx.obj

value = [(key, val.encode("utf-8").decode("unicode_escape")) for key, val in value]

try:
client.set_secret(Secret(secret_id=secret_id, secret=dict(value)))
except Exception as e:
Expand Down
25 changes: 10 additions & 15 deletions admyral/utils/json.py
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."
)
1 change: 1 addition & 0 deletions docs/pages/integrations/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"azure_openai": "Azure OpenAI",
"database": "Database",
"github": "GitHub",
"google_drive": "Google Drive",
"greynoise": "GreyNoise",
"jira": "Jira",
"mistral": "Mistral AI",
Expand Down
4 changes: 4 additions & 0 deletions docs/pages/integrations/google_drive/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"google_drive": "Connect Google Drive",
"apis": "APIs"
}
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"
}
]
```
67 changes: 67 additions & 0 deletions docs/pages/integrations/google_drive/google_drive.mdx
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.

![Google Create Service Account](/google_create_service_account.png)

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.

![Google Service Account Create Key](/google_service_account_create_key.png)

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.

![Google Service Account Client ID](/google_service_accounts_client_id.png)

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**.

![Google Admin Workspace](/google_admin_workspace.png)

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
```
2 changes: 1 addition & 1 deletion docs/pages/integrations/greynoise/apis/ip_lookup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A JSON object.
result = grey_noise_ip_lookup(
ip_address="8.8.8.8",
secrets={
"GREY_NOISE_SECRET"
"GREY_NOISE_SECRET": "my_stored_greynoise_secret"
}
)
```
Expand Down
Binary file added docs/public/google_admin_workspace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/google_create_service_account.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/google_service_account_create_key.png
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.
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?
"""
Loading

0 comments on commit b85a941

Please sign in to comment.