Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 58 additions & 58 deletions box/box.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from autohive_integrations_sdk import Integration, ExecutionContext, ActionHandler
from autohive_integrations_sdk import Integration, ExecutionContext, ActionHandler, ActionResult, ActionError
from typing import Dict, Any
import json
import base64
Expand Down Expand Up @@ -27,7 +27,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
if page_token:
params["offset"] = page_token

data = await context.fetch(url, method="GET", params=params)
response = await context.fetch(url, method="GET", params=params)
data = response.data

# Filter for folders only
folders = []
Expand All @@ -44,18 +45,18 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
}
)

response_data = {"folders": folders, "result": True}
response_data = {"folders": folders}

# Add pagination token if there are more items
total_count = data.get("total_count", 0)
current_offset = int(params.get("offset", 0))
if current_offset + page_size < total_count:
response_data["nextPageToken"] = str(current_offset + page_size)

return response_data
return ActionResult(data=response_data, cost_usd=0.0)

except Exception as e:
return {"folders": [], "result": False, "error": str(e)}
return ActionError(message=str(e))


@box.action("list_files")
Expand Down Expand Up @@ -90,7 +91,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
url = f"{BOX_API_BASE}/folders/0/items"
params = {"limit": page_size, "fields": "id,name,type,size,modified_at,created_at"}

data = await context.fetch(url, method="GET", params=params)
response = await context.fetch(url, method="GET", params=params)
data = response.data

# Format the files response
files = []
Expand All @@ -108,16 +110,16 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
}
)

response_data = {"files": files, "result": True}
response_data = {"files": files}

# Add pagination if available
if "next_marker" in data:
response_data["nextPageToken"] = data["next_marker"]

return response_data
return ActionResult(data=response_data, cost_usd=0.0)

except Exception as e:
return {"files": [], "result": False, "error": str(e)}
return ActionError(message=str(e))


@box.action("list_folder_contents")
Expand All @@ -131,7 +133,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
url = f"{BOX_API_BASE}/folders/{folder_id}/items"
params = {"limit": page_size, "fields": "id,name,type,size,created_at,modified_at"}

data = await context.fetch(url, method="GET", params=params)
response = await context.fetch(url, method="GET", params=params)
data = response.data

# Format the items response
items = []
Expand All @@ -154,7 +157,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
if recursive and item.get("type") == "folder":
try:
subfolder_url = f"{BOX_API_BASE}/folders/{item['id']}/items"
sub_data = await context.fetch(subfolder_url, method="GET", params=params)
sub_response = await context.fetch(subfolder_url, method="GET", params=params)
sub_data = sub_response.data

for sub_item in sub_data.get("entries", []):
sub_formatted_item = {
Expand All @@ -170,16 +174,16 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
except Exception: # nosec B110
pass # Skip subfolders that can't be read

response_data = {"items": items, "result": True}
response_data = {"items": items}

# Add pagination if available
if "next_marker" in data:
response_data["nextPageToken"] = data["next_marker"]

return response_data
return ActionResult(data=response_data, cost_usd=0.0)

except Exception as e:
return {"items": [], "result": False, "error": str(e)}
return ActionError(message=str(e))


@box.action("get_file")
Expand All @@ -190,7 +194,8 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):

# First get file metadata
metadata_url = f"{BOX_API_BASE}/files/{file_id}"
metadata = await context.fetch(metadata_url, method="GET")
metadata_response = await context.fetch(metadata_url, method="GET")
metadata = metadata_response.data

# For file content download, we need to handle binary data manually
# since context.fetch() calls response.text() which fails for binary content
Expand All @@ -209,18 +214,13 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
if "access_token" in credentials:
headers["Authorization"] = f"Bearer {credentials['access_token']}"

async with session.get(content_url, headers=headers) as response:
if response.status != 200:
error_text = await response.text()
return {
"file": {"name": "", "content": "", "contentType": ""},
"metadata": {"id": file_id},
"result": False,
"error": f"Box API error getting content: {response.status} - {error_text}",
}
async with session.get(content_url, headers=headers) as resp:
if resp.status != 200:
error_text = await resp.text()
return ActionError(message=f"Box API error getting content: {resp.status} - {error_text}")

# Read binary content and encode as base64
file_content = await response.read()
file_content = await resp.read()
content_base64 = base64.b64encode(file_content).decode("utf-8")

# Extract information from metadata
Expand All @@ -238,19 +238,16 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
"parents": [metadata.get("parent", {}).get("id", "")] if metadata.get("parent") else [],
}

return {
"file": {"name": file_name, "content": content_base64, "contentType": content_type},
"metadata": structured_metadata,
"result": True,
}
return ActionResult(
data={
"file": {"name": file_name, "content": content_base64, "contentType": content_type},
"metadata": structured_metadata,
},
cost_usd=0.0,
)

except Exception as e:
return {
"file": {"name": "", "content": "", "contentType": ""},
"metadata": {"id": file_id},
"result": False,
"error": str(e),
}
return ActionError(message=str(e))


@box.action("upload_file")
Expand All @@ -276,13 +273,13 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
# We'll need to use a different approach for uploads
async with context: # Use context as async context manager
# Prepare multipart form data for upload
data = aiohttp.FormData()
data.add_field(
form_data = aiohttp.FormData()
form_data.add_field(
"attributes",
json.dumps({"name": file_name, "parent": {"id": folder_id}}),
content_type="application/json",
)
data.add_field("file", file_content, filename=file_name, content_type=content_type)
form_data.add_field("file", file_content, filename=file_name, content_type=content_type)

# Use context's session directly with authentication handled
session = context._session
Expand All @@ -297,34 +294,37 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
if "access_token" in credentials:
headers["Authorization"] = f"Bearer {credentials['access_token']}"

async with session.post(url, headers=headers, data=data) as response:
if response.status not in [200, 201]:
error_text = await response.text()
return {"result": False, "error": f"Box upload error: {response.status} - {error_text}"}
async with session.post(url, headers=headers, data=form_data) as resp:
if resp.status not in [200, 201]:
error_text = await resp.text()
return ActionError(message=f"Box upload error: {resp.status} - {error_text}")

upload_result = await response.json()
upload_result = await resp.json()

# Extract file information from response
entries = upload_result.get("entries", [])
if entries:
uploaded_file = entries[0]
return {
"file_id": uploaded_file.get("id"),
"file_name": uploaded_file.get("name"),
"file_size": uploaded_file.get("size"),
"content_type": content_type,
"result": True,
}
return ActionResult(
data={
"file_id": uploaded_file.get("id"),
"file_name": uploaded_file.get("name"),
"file_size": uploaded_file.get("size"),
"content_type": content_type,
},
cost_usd=0.0,
)
else:
return {
"file_name": file_name,
"content_type": content_type,
"result": True,
"error": "Upload succeeded but no file details returned",
}
return ActionResult(
data={
"file_name": file_name,
"content_type": content_type,
},
cost_usd=0.0,
)

except Exception as e:
return {"result": False, "error": str(e)}
return ActionError(message=str(e))


# ---- Polling Trigger Handlers ----
61 changes: 7 additions & 54 deletions box/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Box",
"version": "0.0.1",
"version": "2.0.0",
"description": "Box integration for managing files and folders",
"entry_point": "box.py",
"auth": {
Expand Down Expand Up @@ -39,19 +39,10 @@
"nextPageToken": {
"type": "string",
"description": "Page token for the next page of folders (only present if there are more folders)"
},
"result": {
"type": "boolean",
"description": "The result of the operation"
},
"error": {
"type": "string",
"description": "Error message if the action failed"
}
},
"required": [
"folders",
"result"
"folders"
]
}
},
Expand Down Expand Up @@ -125,19 +116,10 @@
"nextPageToken": {
"type": "string",
"description": "Page token for the next page of files (only present if there are more files)"
},
"result": {
"type": "boolean",
"description": "The result of the operation"
},
"error": {
"type": "string",
"description": "Error message if the action failed"
}
},
"required": [
"files",
"result"
"files"
]
}
},
Expand Down Expand Up @@ -207,19 +189,10 @@
"nextPageToken": {
"type": "string",
"description": "Page token for the next page of items (only present if there are more items)"
},
"result": {
"type": "boolean",
"description": "The result of the operation"
},
"error": {
"type": "string",
"description": "Error message if the action failed"
}
},
"required": [
"items",
"result"
"items"
]
}
},
Expand All @@ -242,8 +215,7 @@
"type": "object",
"required": [
"file",
"metadata",
"result"
"metadata"
],
"properties": {
"file": {
Expand Down Expand Up @@ -305,14 +277,6 @@
}
},
"description": "The complete file metadata from Box API"
},
"result": {
"type": "boolean",
"description": "The result of the operation"
},
"error": {
"type": "string",
"description": "Error message if the action failed"
}
}
}
Expand Down Expand Up @@ -377,21 +341,10 @@
"upload_url": {
"type": "string",
"description": "URL to access the uploaded file"
},
"result": {
"type": "boolean",
"description": "The result of the operation"
},
"error": {
"type": "string",
"description": "Error message if the action failed"
}
},
"required": [
"result"
]
}
}
}
},
"display_name": "Box"
}
}
2 changes: 1 addition & 1 deletion box/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
autohive-integrations-sdk~=1.0.2
autohive-integrations-sdk~=2.0.0
aiohttp # Required directly: SDK context.fetch() lacks binary download and multipart form upload support
5 changes: 5 additions & 0 deletions box/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys
import os

# Allow 'from context import ...' to work when pytest runs from repo root
sys.path.insert(0, os.path.dirname(__file__))
Loading
Loading