Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/pythontest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,4 @@ jobs:
Add-Content -Path $env:GITHUB_PATH -Value "$env:GITHUB_WORKSPACE\tools\poppler-21.11.0\Library\bin" -Encoding utf8
- name: Run tests
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
run: uv run --group test --extra google_drive pytest
run: uv run --group test --extra google_drive --extra box pytest
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies = [

[project.optional-dependencies]
google_drive = ["google-api-python-client", "google-auth"]
box = ["boxsdk>=10"]
sentry = ["sentry-sdk>=2.32.0"]

[project.scripts]
Expand Down
4 changes: 4 additions & 0 deletions ricecooker/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ def setup_logging(level=logging.INFO, main_log=None, error_log=None, add_loggers
if GOOGLE_SERVICE_ACCOUNT_CREDENTIALS_PATH:
TASK_THREADS = 1 # If using service account, only one thread is allowed - random errors happen otherwise.

BOX_CLIENT_ID = os.getenv("BOX_CLIENT_ID", None)
BOX_CLIENT_SECRET = os.getenv("BOX_CLIENT_SECRET", None)
BOX_ENTERPRISE_ID = os.getenv("BOX_ENTERPRISE_ID", None)

# CSV headers
CSV_HEADERS = [
"Source ID",
Expand Down
73 changes: 73 additions & 0 deletions ricecooker/utils/pipeline/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,78 @@ def handle_file(self, path: str):
)


box_instructions = "Please install ricecooker using `pip install ricecooker[box]` to include required dependencies"


class BoxHandler(WebResourceHandler):
"""Handles downloading from Box shared link URLs"""

PATTERNS = ["box.com"]

def __init__(self):
super().__init__()
self._box_client = None

@property
def HANDLED_EXCEPTIONS(self):
try:
from box_sdk_gen.box.errors import BoxAPIError

return [BoxAPIError]
except ImportError:
return []

@property
def box_client(self):
try:
from box_sdk_gen import BoxCCGAuth
from box_sdk_gen import BoxClient
from box_sdk_gen import CCGConfig
except ImportError:
raise RuntimeError(
"Box downloads require the boxsdk library\n" + box_instructions
)

if self._box_client is None:
if not config.BOX_CLIENT_ID or not config.BOX_CLIENT_SECRET:
raise RuntimeError(
"Box downloads require client credentials.\n"
"Please set BOX_CLIENT_ID, BOX_CLIENT_SECRET, and BOX_ENTERPRISE_ID environment variables."
)
ccg_config = CCGConfig(
client_id=config.BOX_CLIENT_ID,
client_secret=config.BOX_CLIENT_SECRET,
enterprise_id=config.BOX_ENTERPRISE_ID,
)
auth = BoxCCGAuth(config=ccg_config)
self._box_client = BoxClient(auth=auth)
return self._box_client

def handle_file(self, path: str):
boxapi_header = f"shared_link={path}"

# Resolve the shared link to a file object
config.LOGGER.info(f"\tResolving Box shared link: {path}")
file_info = self.box_client.shared_links_files.find_file_for_shared_link(
boxapi=boxapi_header
)

filename = file_info.name
_, ext = os.path.splitext(filename)
config.LOGGER.info(
f"\tBox file: {filename} (id={file_info.id}, type={file_info.type})"
)

with self.write_file(ext.lstrip(".")) as fh:
self.box_client.downloads.download_file_to_output_stream(
file_id=file_info.id,
output_stream=fh,
boxapi=boxapi_header,
)

return FileMetadata(original_filename=filename)


class Base64FileHandler(FileHandler):
def should_handle(self, path: str) -> bool:
return bool(get_base64_encoding(path))
Expand All @@ -418,6 +490,7 @@ class DownloadStageHandler(StageHandler):
DEFAULT_CHILDREN = [
YoutubeDownloadHandler,
GoogleDriveHandler,
BoxHandler,
CatchAllWebResourceDownloadHandler,
DiskResourceHandler,
Base64FileHandler,
Expand Down
6,246 changes: 6,246 additions & 0 deletions tests/cassettes/test_box_audio.yaml

Large diffs are not rendered by default.

18,117 changes: 18,117 additions & 0 deletions tests/cassettes/test_box_pdf.yaml

Large diffs are not rendered by default.

11,745 changes: 11,745 additions & 0 deletions tests/cassettes/test_box_video.yaml

Large diffs are not rendered by default.

249 changes: 249 additions & 0 deletions tests/cassettes/test_box_vtt.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
interactions:
- request:
body: grant_type=client_credentials&box_subject_type=enterprise
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '173'
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- box-python-generated-sdk-10.5.0
X-Box-UA:
- agent=box-python-generated-sdk/10.5.0; env=python/3.10.19
method: POST
uri: https://api.box.com/oauth2/token
response:
body:
string: '{"access_token": "FILTERED", "expires_in": 4119, "restricted_to": [],
"token_type": "bearer"}'
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Transfer-Encoding:
- chunked
box-request-id:
- 0afbc97cd862e0e5f11381d4d5e92bf9f
cache-control:
- no-store
content-type:
- application/json;charset=UTF-8
date:
- Fri, 05 Jun 2026 20:21:09 GMT
strict-transport-security:
- max-age=31536000
via:
- 1.1 google
x-envoy-upstream-service-time:
- '170'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Type:
- application/json
User-Agent:
- box-python-generated-sdk-10.5.0
X-Box-UA:
- agent=box-python-generated-sdk/10.5.0; env=python/3.10.19
boxapi:
- shared_link=https://app.box.com/s/2z5rwa70suvtmocwdmv0pedaax539poa
method: GET
uri: https://api.box.com/2.0/shared_items
response:
body:
string: '{"type": "file", "id": "2267417515024", "file_version": {"type": "file_version",
"id": "2509841911024", "sha1": "5080f8972e5ada52364857db8736887b5e8ffaf2"},
"sequence_id": "2", "etag": "2", "sha1": "5080f8972e5ada52364857db8736887b5e8ffaf2",
"name": "encapsulated.vtt", "description": "", "size": 470, "path_collection":
{"total_count": 0, "entries": []}, "created_at": "2026-06-05T12:20:57-07:00",
"modified_at": "2026-06-05T12:20:57-07:00", "trashed_at": null, "purged_at":
null, "content_created_at": "2024-12-27T10:24:32-08:00", "content_modified_at":
"2024-12-27T10:24:32-08:00", "created_by": {"type": "user", "id": "FILTERED",
"name": "FILTERED", "login": "FILTERED"}, "modified_by": {"type": "user",
"id": "FILTERED", "name": "FILTERED", "login": "FILTERED"}, "owned_by": {"type":
"user", "id": "FILTERED", "name": "FILTERED", "login": "FILTERED"}, "shared_link":
null, "parent": null, "item_status": "active"}'
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Transfer-Encoding:
- chunked
box-request-id:
- 1cfdaef7c46640a07df9e51a3b8274f61
cache-control:
- no-cache, no-store
content-type:
- application/json
date:
- Fri, 05 Jun 2026 20:21:10 GMT
etag:
- '"2"'
strict-transport-security:
- max-age=31536000
via:
- 1.1 google
x-envoy-upstream-service-time:
- '184'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Type:
- application/json
User-Agent:
- box-python-generated-sdk-10.5.0
X-Box-UA:
- agent=box-python-generated-sdk/10.5.0; env=python/3.10.19
boxapi:
- shared_link=https://app.box.com/s/2z5rwa70suvtmocwdmv0pedaax539poa
method: GET
uri: https://api.box.com/2.0/files/2267417515024/content
response:
body:
string: ''
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Length:
- '0'
box-request-id:
- 01f6de2de54e83f77c63640ff646a01d3
cache-control:
- no-cache, no-store
date:
- Fri, 05 Jun 2026 20:21:10 GMT
location:
- https://public.boxcloud.com/d/1/b1!cn8be2quzk7dHDSQi_Kp-IoaDCWr8j0XeITNiNVEqPonXw2p-qFFlh9nqg7PHdnAopy2PqN6angEaAsDocesK-tMpPRSDM2jFpWLqNEWMc-g52rT2aUB2R177YNn1YCFQPCdqBkcUtcQJb3jjuRBK3gD_oW1QeePPqPZDgggZRC1A4FzT1r7tetN34WCbPLjVQt3g2oQSSeSLOq1PNu0IMlb-ii92fM3XZmEzFhEdv_ozRkrW_JrcgksZdEVT-7d4DCvsiJZID2zYCcRPX7BAB9qZaoVBFyZ-eW5cjPrA3iTocsE08TrHofQHVQqAV1m3sGofpvAT08Spzk01ssuJAMSi1AdBIEiz3o1SOHDvQJnMXFaVXZOUQ1AVGb_hAAXh3cFeuOE_G73cmlZ56wm6RhgLECqvBAXKIpY0qxXqcgBlKf4N6vO5r8ex6aGY2NZihUi0qKN3-MdfCTqQcRt6-OB72ef8iiJYhytj6lsuSUVo8wqLupa6CKT09M-cAjdjdDPu5EgRiI9VQ1MTxvN54wsmuOD_59X9cauqQVwKlc2zP7F6Fz0PkCaiqjXS9XFzkbxKPLKlob7I6Xj9jBEe-RHTNpgEpbSEMjujjcfisrkAg4giafv5DUSioe_MpK61fmWOPvCJRIdHO33D6Ydvp0S3VOvHAhqIH1oYd6lpR2pp_O4R00ba-SFQieyybwbtGvxN9suadiIy8wuRIvoXA54SbU3FmyDlO5A5ZG43BEHimPX_hqiV8vUS78x6nw0Kya9mzaPzGb50RJfVsC769qsS0hnDivUHSEnzRCm9vhCJ2QS42RyTzIgMl8U_fUS6NclI0xfKEsRNs-fQSj6j94DGzoxDZ1meeYig0YmDq_SndjQbOyYjB-ZXWjhmY8jz3eHijEmXHsaSgoAB0CHHR789U2fjoObL4KN46GPTNmz0-USEYuCwruBO6hpUMh1ihYe8YCxCS5-gk7EEbH5agl4mJglyxvNfslZkRu0n3uShNAUoqbVkWufqRYrXBvIhVvYV8IOUwVGDD8B26TJ-B1MtN_5vFByrNkrNr-6wcIGGo2qmDXzQS6UsKpkSr8BNnAa82kYwhN43ghgWXuuUIa3ZU51Bq9jOa_GJtM5rikLZd-ftoJ1-Gas3K4wL_SNnfjKoAHGid6OJXC6aC_7TAxNuwY6YJy-p0quj5t2MR47_wErK4g8770ZUghnzfZWQHrKt_kgsm25AwWU5pp_lh8iyzF5EyFOs8i-YP-plwKMnPVEITso3aboFABjpQNEdOGoLMKOz8fGWiyCLiopqbm813DlNcSJX6Sl1k2JZ7zhtIjH2JJTbNbjz0SzODbIkrBD_KG3G0DHY94efbu23QHKws5q9Jj5yploohaKbL_fwaY50ppppcP_8vWHAw2S6fjZdjFghLdTOHQo6UMHt3Ju/download
strict-transport-security:
- max-age=31536000
via:
- 1.1 google
x-envoy-upstream-service-time:
- '309'
status:
code: 302
message: Found
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- box-python-generated-sdk-10.5.0
X-Box-UA:
- agent=box-python-generated-sdk/10.5.0; env=python/3.10.19
boxapi:
- shared_link=https://app.box.com/s/2z5rwa70suvtmocwdmv0pedaax539poa
method: GET
uri: https://public.boxcloud.com/d/1/b1!cn8be2quzk7dHDSQi_Kp-IoaDCWr8j0XeITNiNVEqPonXw2p-qFFlh9nqg7PHdnAopy2PqN6angEaAsDocesK-tMpPRSDM2jFpWLqNEWMc-g52rT2aUB2R177YNn1YCFQPCdqBkcUtcQJb3jjuRBK3gD_oW1QeePPqPZDgggZRC1A4FzT1r7tetN34WCbPLjVQt3g2oQSSeSLOq1PNu0IMlb-ii92fM3XZmEzFhEdv_ozRkrW_JrcgksZdEVT-7d4DCvsiJZID2zYCcRPX7BAB9qZaoVBFyZ-eW5cjPrA3iTocsE08TrHofQHVQqAV1m3sGofpvAT08Spzk01ssuJAMSi1AdBIEiz3o1SOHDvQJnMXFaVXZOUQ1AVGb_hAAXh3cFeuOE_G73cmlZ56wm6RhgLECqvBAXKIpY0qxXqcgBlKf4N6vO5r8ex6aGY2NZihUi0qKN3-MdfCTqQcRt6-OB72ef8iiJYhytj6lsuSUVo8wqLupa6CKT09M-cAjdjdDPu5EgRiI9VQ1MTxvN54wsmuOD_59X9cauqQVwKlc2zP7F6Fz0PkCaiqjXS9XFzkbxKPLKlob7I6Xj9jBEe-RHTNpgEpbSEMjujjcfisrkAg4giafv5DUSioe_MpK61fmWOPvCJRIdHO33D6Ydvp0S3VOvHAhqIH1oYd6lpR2pp_O4R00ba-SFQieyybwbtGvxN9suadiIy8wuRIvoXA54SbU3FmyDlO5A5ZG43BEHimPX_hqiV8vUS78x6nw0Kya9mzaPzGb50RJfVsC769qsS0hnDivUHSEnzRCm9vhCJ2QS42RyTzIgMl8U_fUS6NclI0xfKEsRNs-fQSj6j94DGzoxDZ1meeYig0YmDq_SndjQbOyYjB-ZXWjhmY8jz3eHijEmXHsaSgoAB0CHHR789U2fjoObL4KN46GPTNmz0-USEYuCwruBO6hpUMh1ihYe8YCxCS5-gk7EEbH5agl4mJglyxvNfslZkRu0n3uShNAUoqbVkWufqRYrXBvIhVvYV8IOUwVGDD8B26TJ-B1MtN_5vFByrNkrNr-6wcIGGo2qmDXzQS6UsKpkSr8BNnAa82kYwhN43ghgWXuuUIa3ZU51Bq9jOa_GJtM5rikLZd-ftoJ1-Gas3K4wL_SNnfjKoAHGid6OJXC6aC_7TAxNuwY6YJy-p0quj5t2MR47_wErK4g8770ZUghnzfZWQHrKt_kgsm25AwWU5pp_lh8iyzF5EyFOs8i-YP-plwKMnPVEITso3aboFABjpQNEdOGoLMKOz8fGWiyCLiopqbm813DlNcSJX6Sl1k2JZ7zhtIjH2JJTbNbjz0SzODbIkrBD_KG3G0DHY94efbu23QHKws5q9Jj5yploohaKbL_fwaY50ppppcP_8vWHAw2S6fjZdjFghLdTOHQo6UMHt3Ju/download
response:
body:
string: 'WEBVTT


00:09.209 --> 00:12.312

( clock ticking )


00:14.848 --> 00:17.000

MAN:

When we think

\u266a ...say bow, wow, \u266a


00:17.000 --> 00:18.752 align:right

we have this vision of Einstein


00:18.752 --> 00:20.887

 

as an old, wrinkly man

with white hair.


00:20.887 --> 00:26.760

MAN 2:

E equals m c-squared is

not about an old Einstein.


00:26.760 --> 00:32.200

MAN 2:

It''s all about an eternal Einstein.


00:32.200 --> 00:36.200

<LAUGHING & WHOOPS!>

'
headers:
Alt-Svc:
- h3=":443"; ma=2592000
Content-Length:
- '470'
accept-ranges:
- bytes
cache-control:
- private
content-disposition:
- attachment;filename="encapsulated.vtt";filename*=UTF-8''encapsulated.vtt
content-type:
- application/octet-stream
date:
- Fri, 05 Jun 2026 20:21:10 GMT
server:
- nginx
strict-transport-security:
- max-age=31536000
via:
- 1.1 google
x-content-type-options:
- nosniff
x-envoy-upstream-service-time:
- '196'
x-robots-tag:
- noindex, nofollow
status:
code: 200
message: OK
version: 1
Loading
Loading