feat(microsoft-graph-files): O365 file management integration (Graph API)
Microsoft Graph file management for OneDrive / SharePoint / Teams with app-only (client credentials) authentication, stateless over urllib (no dependency). 19 commands: browse sites/drives/content, create folders, delete/upload/replace/ download files (content passed via base64 or source URL; download returns the pre-authenticated Graph URL), site permission management (list/create/update/ delete), SharePoint list reading (lists/items/get-item), and Excel worksheet editing (append row, read range, update cell). Each script obtains a bearer token via the client-credentials grant and calls Graph directly. Re-implemented cleanly from a customized source: dropped the hosted-proxy/auth-code/managed-identity/certificate flows, the duplicated and broken Excel helpers, and the platform-specific file-entry handling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
id: microsoft_graph_files
|
||||
name: Microsoft Graph Files
|
||||
version: 1.0.0
|
||||
description: "Microsoft Graph file management for O365 (OneDrive / SharePoint / Teams) — browse sites, drives and content, create folders, upload/replace/download files, manage site permissions, read SharePoint lists, and edit Excel worksheets. App-only authentication (client credentials)."
|
||||
changelog: "1.0.0 — Initial release: SharePoint sites/drives/content browsing, folder creation, file delete/upload/replace/download (via URL or base64), site permission management (list/create/update/delete), SharePoint list reading (lists/items/get-item), and Excel worksheet editing (add-row/get-worksheet/update-cell). App-only client-credentials authentication."
|
||||
category: productivity
|
||||
|
||||
# Per-instance configuration. Uses app-only (client credentials) authentication
|
||||
# against Microsoft Graph. Register an Azure AD application with the required
|
||||
# application permissions (e.g. Sites.ReadWrite.All, Files.ReadWrite.All) and admin
|
||||
# consent, then provide the tenant id, client id and client secret.
|
||||
config_schema:
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
description: "Microsoft Graph base URL"
|
||||
default: https://graph.microsoft.com
|
||||
login_url:
|
||||
type: string
|
||||
description: "Azure AD login endpoint (change for national clouds, e.g. https://login.microsoftonline.us)"
|
||||
default: https://login.microsoftonline.com
|
||||
tenant_id:
|
||||
type: string
|
||||
description: "Azure AD tenant ID"
|
||||
client_id:
|
||||
type: string
|
||||
description: "Application (client) ID"
|
||||
client_secret:
|
||||
type: string
|
||||
description: "Client secret"
|
||||
x-soar-sensitive: true
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required:
|
||||
- tenant_id
|
||||
- client_id
|
||||
- client_secret
|
||||
|
||||
# Documented for reference; the bundled scripts obtain a bearer token themselves
|
||||
# via the client-credentials grant and set Authorization: Bearer <token>.
|
||||
auth:
|
||||
- id: apikey
|
||||
type: api_key
|
||||
in: header
|
||||
name: Authorization
|
||||
value_template: "Bearer {{secret}}"
|
||||
secret_field: client_secret
|
||||
|
||||
commands:
|
||||
- id: test_connection
|
||||
name: msgraph-files-auth-test
|
||||
description: "Verify connectivity and credentials against Microsoft Graph (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Sites / drives / content ──────────────────────────────────────────────
|
||||
- id: list_sharepoint_sites
|
||||
name: msgraph-list-sharepoint-sites
|
||||
description: "Search the tenant's SharePoint sites (requires Sites.Read.All)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
keyword: { type: string, description: "Search keyword; defaults to all sites (*)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_drives_in_site
|
||||
name: msgraph-list-drives-in-site
|
||||
description: "List the document libraries (drives) of a site."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (use msgraph-list-sharepoint-sites to find it)" }
|
||||
limit: { type: number, description: "Maximum results" }
|
||||
next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_drive_content
|
||||
name: msgraph-list-drive-content
|
||||
description: "List files and folders in a drive (object_type: drives/groups/sites/users)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
item_id: { type: string, description: "Folder item ID (default root)" }
|
||||
limit: { type: number, description: "Maximum results" }
|
||||
next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" }
|
||||
required: [object_type, object_type_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: create_new_folder
|
||||
name: msgraph-create-new-folder
|
||||
description: "Create a folder under a parent item in a drive."
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
parent_id: { type: string, description: "Parent item ID (root or a folder ID)" }
|
||||
folder_name: { type: string, description: "Name of the new folder" }
|
||||
required: [object_type, object_type_id, parent_id, folder_name]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: delete_file
|
||||
name: msgraph-delete-file
|
||||
description: "Delete a drive item (file or folder) by its ID."
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
item_id: { type: string, description: "Item ID to delete" }
|
||||
required: [object_type, object_type_id, item_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── File transfer (URL / base64) ──────────────────────────────────────────
|
||||
- id: download_file
|
||||
name: msgraph-download-file
|
||||
description: "Return a drive item's metadata and a pre-authenticated download URL. Set as_base64=true to also return the content base64-encoded (small files only)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
item_id: { type: string, description: "Item ID to download" }
|
||||
as_base64: { type: string, description: "Set true to also include base64 content (small files)" }
|
||||
required: [object_type, object_type_id, item_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: upload_new_file
|
||||
name: msgraph-upload-new-file
|
||||
description: "Upload a new file into a folder. Provide the content via content_base64 or source_url."
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
parent_id: { type: string, description: "Parent folder ID" }
|
||||
file_name: { type: string, description: "Name for the uploaded file" }
|
||||
content_base64: { type: string, description: "File content, base64-encoded" }
|
||||
source_url: { type: string, description: "URL to fetch the file content from (alternative to content_base64)" }
|
||||
required: [object_type, object_type_id, parent_id, file_name]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: replace_existing_file
|
||||
name: msgraph-replace-existing-file
|
||||
description: "Replace the content of an existing file. Provide the new content via content_base64 or source_url."
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
item_id: { type: string, description: "Item ID of the file to replace" }
|
||||
content_base64: { type: string, description: "New file content, base64-encoded" }
|
||||
source_url: { type: string, description: "URL to fetch the new content from (alternative to content_base64)" }
|
||||
required: [object_type, object_type_id, item_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Site permissions ──────────────────────────────────────────────────────
|
||||
- id: list_site_permissions
|
||||
name: msgraph-list-site-permissions
|
||||
description: "List application permissions on a site, or a single permission when permission_id is given. Provide site_id or site_name."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (or provide site_name)" }
|
||||
site_name: { type: string, description: "Site name (resolved to an ID; or provide site_id)" }
|
||||
permission_id: { type: string, description: "A specific permission ID to retrieve" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: create_site_permissions
|
||||
name: msgraph-create-site-permissions
|
||||
description: "Grant an application permission (read/write/owner) on a site."
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (or provide site_name)" }
|
||||
site_name: { type: string, description: "Site name (or provide site_id)" }
|
||||
role: { type: string, description: "read, write or owner" }
|
||||
app_id: { type: string, description: "Application (client) ID to grant" }
|
||||
display_name: { type: string, description: "Display name of the application" }
|
||||
required: [role, app_id, display_name]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: update_site_permissions
|
||||
name: msgraph-update-site-permissions
|
||||
description: "Update the role of an existing site permission."
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (or provide site_name)" }
|
||||
site_name: { type: string, description: "Site name (or provide site_id)" }
|
||||
permission_id: { type: string, description: "Permission ID to update" }
|
||||
role: { type: string, description: "read, write or owner" }
|
||||
required: [permission_id, role]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: delete_site_permissions
|
||||
name: msgraph-delete-site-permissions
|
||||
description: "Delete an application permission from a site."
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (or provide site_name)" }
|
||||
site_name: { type: string, description: "Site name (or provide site_id)" }
|
||||
permission_id: { type: string, description: "Permission ID to delete" }
|
||||
required: [permission_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── SharePoint lists ──────────────────────────────────────────────────────
|
||||
- id: list_sharepoint_lists
|
||||
name: msgraph-list-sharepoint-lists
|
||||
description: "List the SharePoint lists of a site. Provide site_id or site_name."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (or provide site_name)" }
|
||||
site_name: { type: string, description: "Site name (or provide site_id)" }
|
||||
limit: { type: number, description: "Maximum results" }
|
||||
next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_sharepoint_list_items
|
||||
name: msgraph-list-sharepoint-list-items
|
||||
description: "List items of a SharePoint list (with their fields). Provide list_id or list_name."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (or provide site_name)" }
|
||||
site_name: { type: string, description: "Site name (or provide site_id)" }
|
||||
list_id: { type: string, description: "List ID (or provide list_name)" }
|
||||
list_name: { type: string, description: "List name (or provide list_id)" }
|
||||
limit: { type: number, description: "Maximum results" }
|
||||
filter: { type: string, description: "OData $filter expression" }
|
||||
orderby: { type: string, description: "OData $orderby expression" }
|
||||
fields_select: { type: string, description: "Comma-separated list of field columns to expand" }
|
||||
next_page_url: { type: string, description: "The @odata.nextLink URL for the next page" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_sharepoint_list_item
|
||||
name: msgraph-get-sharepoint-list-item
|
||||
description: "Get a single SharePoint list item with its fields. Provide list_id or list_name."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
site_id: { type: string, description: "Site ID (or provide site_name)" }
|
||||
site_name: { type: string, description: "Site name (or provide site_id)" }
|
||||
list_id: { type: string, description: "List ID (or provide list_name)" }
|
||||
list_name: { type: string, description: "List name (or provide list_id)" }
|
||||
item_id: { type: string, description: "List item ID" }
|
||||
required: [item_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Excel ─────────────────────────────────────────────────────────────────
|
||||
- id: excel_add_row
|
||||
name: msgraph-excel-add-row
|
||||
description: "Append a row to an Excel worksheet. 'data' is a JSON object mapping column number (1-based) to value, e.g. {\"1\":\"foo\",\"2\":42}."
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
item_id: { type: string, description: "Workbook (xlsx) item ID" }
|
||||
worksheet_name: { type: string, description: "Worksheet name" }
|
||||
data: { type: object, description: "Object mapping 1-based column number to value" }
|
||||
start_row: { type: number, description: "Row to start appending from (default 1)" }
|
||||
required: [object_type_id, item_id, worksheet_name, data]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: excel_get_worksheet
|
||||
name: msgraph-excel-get-worksheet
|
||||
description: "Read a range from an Excel worksheet (formatted text values)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
item_id: { type: string, description: "Workbook (xlsx) item ID" }
|
||||
worksheet_name: { type: string, description: "Worksheet name" }
|
||||
range: { type: string, description: "Range address, e.g. A1:Z500 (default A1:Z500)" }
|
||||
required: [object_type_id, item_id, worksheet_name]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: excel_update_cell
|
||||
name: msgraph-excel-update-cell
|
||||
description: "Overwrite a single Excel cell. 'column' may be a number or 'Col_2'."
|
||||
inputs_schema:
|
||||
properties:
|
||||
object_type: { type: string, description: "drives, groups, sites or users" }
|
||||
object_type_id: { type: string, description: "The resource ID" }
|
||||
item_id: { type: string, description: "Workbook (xlsx) item ID" }
|
||||
worksheet_name: { type: string, description: "Worksheet name" }
|
||||
row_index: { type: number, description: "1-based row index" }
|
||||
column: { type: string, description: "Column number or 'Col_2'" }
|
||||
value: { type: string, description: "New cell value" }
|
||||
required: [object_type_id, item_id, worksheet_name, row_index, column, value]
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,164 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
parent = drive_item_base(I["object_type"], I["object_type_id"], I["parent_id"])
|
||||
body = {"name": I["folder_name"], "folder": {}, "@microsoft.graph.conflictBehavior": "rename"}
|
||||
out(graph("POST", parent + "/children", json_body=body))
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,167 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
site_id = resolve_site_id()
|
||||
body = {
|
||||
"roles": _list(I["role"]),
|
||||
"grantedToIdentities": [{"application": {"id": I["app_id"], "displayName": I["display_name"]}}],
|
||||
}
|
||||
out(graph("POST", "/sites/" + site_id + "/permissions", json_body=body))
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,164 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
uri = drive_item_base(I["object_type"], I["object_type_id"], I["item_id"])
|
||||
graph("DELETE", uri, resp="response")
|
||||
out({"deleted": I["item_id"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,165 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
site_id = resolve_site_id()
|
||||
uri = "/sites/" + site_id + "/permissions/" + I["permission_id"]
|
||||
graph("DELETE", uri, resp="response")
|
||||
out({"deleted": I["permission_id"], "site_id": site_id})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,168 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
base = drive_item_base(I["object_type"], I["object_type_id"], I["item_id"])
|
||||
meta = graph("GET", base)
|
||||
url = meta.get("@microsoft.graph.downloadUrl")
|
||||
result = {"id": meta.get("id"), "name": meta.get("name"), "size": meta.get("size"), "download_url": url}
|
||||
if _bool(I.get("as_base64")) and url:
|
||||
result["content_base64"] = base64.b64encode(fetch_url(url)).decode("ascii")
|
||||
out(result)
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,179 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
data = I["data"]
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
if not data:
|
||||
fail("data is empty")
|
||||
return
|
||||
base = worksheet_base(I["object_type"] or "sites", I["object_type_id"], I["item_id"], I["worksheet_name"])
|
||||
used = graph("GET", base + "/usedRange", params={"$select": "rowCount"})
|
||||
row_count = int(used.get("rowCount", 0))
|
||||
start_row = int(I.get("start_row") or 1)
|
||||
target_row = max(start_row, row_count + 1)
|
||||
max_col = max(int(k) for k in data)
|
||||
row_values = [""] * max_col
|
||||
for k, v in data.items():
|
||||
row_values[int(k) - 1] = v
|
||||
addr = "A" + str(target_row) + ":" + col_letter(max_col) + str(target_row)
|
||||
res = graph("PATCH", base + "/range(address='" + addr + "')", json_body={"values": [row_values]})
|
||||
out({"address": res.get("address", addr), "row": target_row, "values": row_values})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,165 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
base = worksheet_base(I.get("object_type") or "sites", I["object_type_id"], I["item_id"], I["worksheet_name"])
|
||||
range_addr = I.get("range") or "A1:Z500"
|
||||
res = graph("GET", base + "/range(address='" + range_addr + "')")
|
||||
out({"address": res.get("address"), "values": res.get("text") or res.get("values")})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,170 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
match = re.search(r"\d+", str(I["column"]))
|
||||
if not match:
|
||||
fail("Invalid column: " + str(I["column"]))
|
||||
return
|
||||
col_num = int(match.group())
|
||||
addr = col_letter(col_num) + str(int(I["row_index"]))
|
||||
base = worksheet_base(I.get("object_type") or "sites", I["object_type_id"], I["item_id"], I["worksheet_name"])
|
||||
graph("PATCH", base + "/range(address='" + addr + "')", json_body={"values": [[I["value"]]]})
|
||||
out({"cell": addr, "value": I["value"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,165 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
site_id = resolve_site_id()
|
||||
list_id = resolve_list_id(site_id)
|
||||
uri = "/sites/" + site_id + "/lists/" + list_id + "/items/" + I["item_id"]
|
||||
out(graph("GET", uri, params={"$expand": "fields"}))
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,168 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
if I.get("next_page_url"):
|
||||
res = graph("GET", I["next_page_url"], params={"$top": I.get("limit")})
|
||||
else:
|
||||
item = I.get("item_id") or "root"
|
||||
uri = drive_item_base(I["object_type"], I["object_type_id"], item) + "/children"
|
||||
res = graph("GET", uri, params={"$top": I.get("limit")})
|
||||
out({"children": res.get("value", []), "next": res.get("@odata.nextLink")})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,170 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
params = {"$top": I.get("limit")}
|
||||
if I.get("next_page_url"):
|
||||
res = graph("GET", I["next_page_url"], params=params)
|
||||
elif I.get("site_id"):
|
||||
res = graph("GET", "/sites/" + I["site_id"] + "/drives", params=params)
|
||||
else:
|
||||
fail("Provide site_id or next_page_url")
|
||||
return
|
||||
out({"drives": res.get("value", []), "next": res.get("@odata.nextLink")})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,170 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
site_id = resolve_site_id()
|
||||
list_id = resolve_list_id(site_id)
|
||||
expand = "fields($select=" + I["fields_select"] + ")" if I.get("fields_select") else "fields"
|
||||
params = {"$top": I.get("limit"), "$filter": I.get("filter"), "$orderby": I.get("orderby"), "$expand": expand}
|
||||
if I.get("next_page_url"):
|
||||
res = graph("GET", I["next_page_url"], params={"$top": I.get("limit")})
|
||||
else:
|
||||
res = graph("GET", "/sites/" + site_id + "/lists/" + list_id + "/items", params=params)
|
||||
out({"items": res.get("value", []), "next": res.get("@odata.nextLink")})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,167 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
site_id = resolve_site_id()
|
||||
if I.get("next_page_url"):
|
||||
res = graph("GET", I["next_page_url"], params={"$top": I.get("limit")})
|
||||
else:
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": I.get("limit")})
|
||||
out({"lists": res.get("value", []), "next": res.get("@odata.nextLink")})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,163 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
res = graph("GET", "/sites", params={"search": I.get("keyword") or "*"})
|
||||
out({"sites": res.get("value", []), "next": res.get("@odata.nextLink")})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,166 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
site_id = resolve_site_id()
|
||||
pid = I.get("permission_id")
|
||||
uri = "/sites/" + site_id + "/permissions" + ("/" + pid if pid else "")
|
||||
res = graph("GET", uri)
|
||||
out(res if pid else {"permissions": res.get("value", [])})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,164 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
content = input_bytes()
|
||||
uri = drive_item_base(I["object_type"], I["object_type_id"], I["item_id"]) + "/content"
|
||||
out(graph("PUT", uri, data=content))
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,162 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
def run():
|
||||
graph("GET", "/sites", params={"search": "*", "$top": 1})
|
||||
out({"ok": True})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,164 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
site_id = resolve_site_id()
|
||||
uri = "/sites/" + site_id + "/permissions/" + I["permission_id"]
|
||||
out(graph("PATCH", uri, json_body={"roles": _list(I["role"])}))
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
@@ -0,0 +1,166 @@
|
||||
import json, os, re, ssl, sys, base64, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
GRAPH = (S.get("host") or "https://graph.microsoft.com").rstrip("/")
|
||||
LOGIN = (S.get("login_url") or "https://login.microsoftonline.com").rstrip("/")
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _bool(v):
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _ctx():
|
||||
return ssl._create_unverified_context() if _bool(S.get("insecure")) else ssl.create_default_context()
|
||||
|
||||
|
||||
_TOKEN = None
|
||||
|
||||
|
||||
def token():
|
||||
global _TOKEN
|
||||
if _TOKEN is not None:
|
||||
return _TOKEN
|
||||
data = urllib.parse.urlencode({
|
||||
"client_id": S.get("client_id", ""),
|
||||
"client_secret": S.get("client_secret", ""),
|
||||
"grant_type": "client_credentials",
|
||||
"scope": GRAPH + "/.default",
|
||||
}).encode("utf-8")
|
||||
url = LOGIN + "/" + urllib.parse.quote(S.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx()) as r:
|
||||
_TOKEN = json.loads(r.read())["access_token"]
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Authentication failed: HTTP " + str(e.code) + " " + e.read().decode("utf-8", "replace"))
|
||||
return _TOKEN
|
||||
|
||||
|
||||
def graph(method, path, params=None, json_body=None, data=None, headers=None, resp="json"):
|
||||
url = path if path.startswith("http") else (GRAPH + "/v1.0" + path)
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean)
|
||||
h = {"Authorization": "Bearer " + token(), "Accept": "application/json"}
|
||||
body = None
|
||||
if json_body is not None:
|
||||
body = json.dumps(json_body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
elif data is not None:
|
||||
body = data
|
||||
h["Content-Type"] = "application/octet-stream"
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, data=body, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
raw = r.read()
|
||||
if resp == "response":
|
||||
return {"status": r.status}
|
||||
if resp == "content":
|
||||
return raw
|
||||
return json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raise Exception("Graph HTTP " + str(e.code) + ": " + e.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
def fetch_url(url):
|
||||
"""Fetch raw bytes from an arbitrary URL (no Graph auth header)."""
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx()) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def input_bytes():
|
||||
"""Resolve upload content from content_base64 or source_url."""
|
||||
if I.get("content_base64"):
|
||||
return base64.b64decode(I["content_base64"])
|
||||
if I.get("source_url"):
|
||||
return fetch_url(I["source_url"])
|
||||
fail("Provide either content_base64 or source_url")
|
||||
|
||||
|
||||
def col_letter(n):
|
||||
s = ""
|
||||
n = int(n)
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
s = chr(65 + rem) + s
|
||||
return s
|
||||
|
||||
|
||||
def resolve_site_id():
|
||||
if I.get("site_id"):
|
||||
return I["site_id"]
|
||||
name = I.get("site_name")
|
||||
if not name:
|
||||
fail("Provide site_id or site_name")
|
||||
res = graph("GET", "/sites", params={"search": name})
|
||||
value = res.get("value", [])
|
||||
if not value:
|
||||
fail("Site not found: " + name)
|
||||
return value[0]["id"]
|
||||
|
||||
|
||||
def resolve_list_id(site_id):
|
||||
if I.get("list_id"):
|
||||
return I["list_id"]
|
||||
name = I.get("list_name")
|
||||
if not name:
|
||||
fail("Provide list_id or list_name")
|
||||
res = graph("GET", "/sites/" + site_id + "/lists", params={"$top": 999})
|
||||
for lst in res.get("value", []):
|
||||
if (lst.get("displayName") or lst.get("name") or "").lower() == name.lower():
|
||||
return lst["id"]
|
||||
fail("List not found: " + name)
|
||||
|
||||
|
||||
def drive_item_base(object_type, object_type_id, item_id):
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id
|
||||
|
||||
|
||||
def worksheet_base(object_type, object_type_id, item_id, worksheet):
|
||||
ws = urllib.parse.quote(worksheet)
|
||||
if object_type == "drives":
|
||||
return "/drives/" + object_type_id + "/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
return "/" + object_type + "/" + object_type_id + "/drive/items/" + item_id + "/workbook/worksheets/" + ws
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
content = input_bytes()
|
||||
name = urllib.parse.quote(I["file_name"])
|
||||
base = drive_item_base(I["object_type"], I["object_type_id"], I["parent_id"])
|
||||
uri = base + ":/" + name + ":/content"
|
||||
out(graph("PUT", uri, data=content))
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
Reference in New Issue
Block a user