File attachments — attach a file to a task and read its bytes back.¶
A task attachment is a taskFileAttachment whose content travels as a
base64-encoded contentBytes string. This script attaches a small file
(generated in memory, or --file if given), lists the attachments, decodes
the content into the temp directory, and removes it again.
Microsoft Graph only accepts simple (inline) attachments up to 3 MB for To Do; upload sessions for larger files are not currently supported by the service.
python file.py
python file.py --file report.pdf --task "Review Q3"
Permissions
Requires delegated permission Tasks.ReadWrite for the signed-in user.
Reference¶
from __future__ import annotations
import argparse
import base64
import mimetypes
import pathlib
import tempfile
from office365.graph_client import GraphClient
from office365.runtime.client_request_exception import ClientRequestException
from office365.runtime.transfer import INLINE_ATTACHMENT_MAX
from tests.settings import client_id, password, tenant, username
LIST_NAME = "SDK Tasks Demo"
PERMISSION_CODES = {"accessDenied", "Authorization_RequestDenied", "ErrorAccessDenied", "Forbidden"}
_PREVIEW_BYTES = 120
def _client() -> GraphClient:
return GraphClient(tenant=tenant).with_username_and_password(client_id, username, password)
def _source(args: argparse.Namespace) -> tuple[str, bytes, str]:
if args.file:
path = pathlib.Path(args.file)
content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
return path.name, path.read_bytes(), content_type
name = "todo-note.txt"
return name, b"Generated by the office365-rest-python-client To Do example.\n", "text/plain"
def main() -> None:
parser = argparse.ArgumentParser(description="Attach a small file to a Microsoft To Do task")
parser.add_argument("--file", help="path to the file to attach (default: an in-memory text note)")
parser.add_argument("--task", default="Review the draft", help="task title")
parser.add_argument("--keep", action="store_true", help="keep the list and task instead of deleting them")
args = parser.parse_args()
client = _client()
try:
name, content, content_type = _source(args)
if len(content) > INLINE_ATTACHMENT_MAX:
print(f"{name} is {len(content)} bytes; To Do only supports inline attachments up to 3 MB.")
return
task_list = client.me.todo.lists.ensure(LIST_NAME).execute_query()
task = task_list.tasks.ensure(args.task).execute_query()
attachment = task.upload_attachment(content, name, content_type)
print(f"Attached {attachment.name!r} ({len(content)} bytes, {attachment.content_type})")
print("Attachments on the task:")
for item in task.attachments.get().execute_query():
print(f" {item.name:30s} {item.size:>8d} bytes {item.content_type} {item.last_modified_datetime}")
stored = task.attachments[attachment.id].get().execute_query()
raw = base64.b64decode(stored.content_bytes or "")
out_dir = pathlib.Path(tempfile.gettempdir()) / "todo-attachments"
out_dir.mkdir(exist_ok=True)
out_path = out_dir / stored.name
out_path.write_bytes(raw)
print(f"\nRead back {len(raw)} bytes -> {out_path}")
print(f"Preview: {raw[:_PREVIEW_BYTES]!r}")
attachment.delete_object().execute_query()
print("Removed the attachment.")
if args.keep:
print("Keeping the list and task (--keep).")
return
task.delete_object().execute_query()
client.me.todo.lists[task_list.id].delete_object().execute_query()
print("Cleaned up the demo list and task.")
except ClientRequestException as exc:
if exc.code not in PERMISSION_CODES:
raise
print(f"To Do request failed: {exc.code}: {exc.message}")
print("Grant and consent the delegated permission Tasks.ReadWrite for the signed-in user, then re-run.")
raise SystemExit(0) from None
if __name__ == "__main__":
main()