Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# Format: VARIABLE_NAME=value (no quotes needed)
# ============================================================================

# -- Box --
# BOX_ACCESS_TOKEN=

# -- Bitly --
# BITLY_ACCESS_TOKEN=

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ Supports basic HTTP authentication and Bearer token authentication via the SDK.

[coda](coda): Coda integration for managing documents, pages, tables, and rows. Supports full CRUD operations for docs (list, get, create, update, delete) and pages (list, get, create with HTML/Markdown content, update metadata, delete). Includes table and column discovery (list tables/columns, get table/column details) and complete row management (list with filtering/sorting, get, upsert with keyColumns, update, delete single/multiple). Features Bearer token authentication, pagination support, async processing (HTTP 202 responses), multiple value formats (simple/rich), and comprehensive error handling. Ideal for document automation, content management, and data synchronization workflows.

### Count

[count](count): Count is a modern accounting platform for startups and SMBs. The integration provides 43 actions covering chart of accounts, customers, vendors, products, transactions, invoices, bills, journal entries, tags, and financial reports (trial balance, balance sheet, profit & loss). Supports OAuth 2.0 authentication via the Count developer portal. Ideal for automating accounting workflows, syncing financial data, managing invoices and bills, and generating reports for accounting firms.

### ElevenLabs

[elevenlabs](elevenlabs): Text-to-speech and speech-to-text integration with ElevenLabs API for voice generation, audio transcription, and audio management. Supports converting text to realistic speech with customizable voice settings, transcribing audio/video files using ElevenLabs Scribe with word-level timestamps and speaker diarization, browsing voices, managing transcripts, and monitoring subscription usage. Features 10 actions (1 paid, 9 free) covering text-to-speech, speech-to-text, voice management, history, and account. Uses API key authentication. Ideal for content creation, audiobook narration, voiceovers, meeting transcription, and automated audio/transcription workflows.
Expand Down
124 changes: 66 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 All @@ -66,6 +67,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
file_extensions = inputs.get("file_extensions", [])
folder_id = inputs.get("folder_id")
page_size = inputs.get("pageSize", 100)
page_token = inputs.get("pageToken")

if query or file_extensions or folder_id:
# Use search API
Expand All @@ -85,12 +87,17 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
"type": "file",
"fields": "id,name,type,size,modified_at,created_at",
}
if page_token:
params["marker"] = page_token
else:
# List recent files from root
url = f"{BOX_API_BASE}/folders/0/items"
params = {"limit": page_size, "fields": "id,name,type,size,modified_at,created_at"}
if page_token:
params["marker"] = page_token

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 +115,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 @@ -127,11 +134,15 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
folder_id = inputs["folder_id"]
recursive = inputs.get("recursive", False)
page_size = inputs.get("pageSize", 100)
page_token = inputs.get("pageToken")

url = f"{BOX_API_BASE}/folders/{folder_id}/items"
params = {"limit": page_size, "fields": "id,name,type,size,created_at,modified_at"}
if page_token:
params["marker"] = page_token

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 +165,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 +182,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 +202,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 +222,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 +246,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 +281,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 +302,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 ----
Loading
Loading