chore: format

This commit is contained in:
Timothy Jaeryang Baek
2026-04-19 22:45:54 +09:00
parent 8c9f267ad2
commit 37eba1c5a6
79 changed files with 1272 additions and 207 deletions
+1 -3
View File
@@ -1524,9 +1524,7 @@ USER_PERMISSIONS_FEATURES_AUTOMATIONS = (
os.environ.get('USER_PERMISSIONS_FEATURES_AUTOMATIONS', 'False').lower() == 'true'
)
USER_PERMISSIONS_FEATURES_CALENDAR = (
os.environ.get('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true'
)
USER_PERMISSIONS_FEATURES_CALENDAR = os.environ.get('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true'
USER_PERMISSIONS_SETTINGS_INTERFACE = os.environ.get('USER_PERMISSIONS_SETTINGS_INTERFACE', 'True').lower() == 'true'
@@ -5,6 +5,7 @@ Revises: c1d2e3f4a5b6
Create Date: 2026-04-19 16:20:58.162045
"""
from typing import Sequence, Union
from alembic import op
@@ -19,52 +20,55 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table('calendar',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('is_default', sa.Boolean(), nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id')
op.create_table(
'calendar',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('is_default', sa.Boolean(), nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False)
op.create_table('calendar_event',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('calendar_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('title', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('start_at', sa.BigInteger(), nullable=False),
sa.Column('end_at', sa.BigInteger(), nullable=True),
sa.Column('all_day', sa.Boolean(), nullable=False),
sa.Column('rrule', sa.Text(), nullable=True),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('is_cancelled', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id')
op.create_table(
'calendar_event',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('calendar_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('title', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('start_at', sa.BigInteger(), nullable=False),
sa.Column('end_at', sa.BigInteger(), nullable=True),
sa.Column('all_day', sa.Boolean(), nullable=False),
sa.Column('rrule', sa.Text(), nullable=True),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('is_cancelled', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False)
op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False)
op.create_table('calendar_event_attendee',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('event_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('status', sa.Text(), nullable=False),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee')
op.create_table(
'calendar_event_attendee',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('event_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('status', sa.Text(), nullable=False),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'),
)
op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False)
+4 -9
View File
@@ -153,15 +153,11 @@ class AutomationTable:
row = await db.get(Automation, id)
return AutomationModel.model_validate(row) if row else None
async def get_active_by_user(
self, user_id: str, db: Optional[AsyncSession] = None
) -> list[AutomationModel]:
async def get_active_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[AutomationModel]:
"""Get active automations for a user (for calendar RRULE expansion)."""
async with get_async_db_context(db) as db:
result = await db.execute(
select(Automation)
.filter_by(user_id=user_id, is_active=True)
.order_by(Automation.created_at.desc())
select(Automation).filter_by(user_id=user_id, is_active=True).order_by(Automation.created_at.desc())
)
return [AutomationModel.model_validate(r) for r in result.scalars().all()]
@@ -291,9 +287,8 @@ class AutomationTable:
timezone_by_user_id: dict[str, Optional[str]] = {}
if user_ids:
from open_webui.models.users import User
tz_result = await db.execute(
select(User.id, User.timezone).where(User.id.in_(user_ids))
)
tz_result = await db.execute(select(User.id, User.timezone).where(User.id.in_(user_ids)))
timezone_by_user_id = {uid: tz for uid, tz in tz_result.all()}
for row in rows:
+31 -67
View File
@@ -232,9 +232,7 @@ class CalendarEventListResponse(BaseModel):
class CalendarTable:
async def _get_access_grants(
self, calendar_id: str, db: Optional[AsyncSession] = None
) -> list[AccessGrantModel]:
async def _get_access_grants(self, calendar_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]:
return await AccessGrants.get_grants_by_resource('calendar', calendar_id, db=db)
async def _to_calendar_model(
@@ -245,15 +243,11 @@ class CalendarTable:
) -> CalendarModel:
cal_data = CalendarModel.model_validate(cal).model_dump(exclude={'access_grants'})
cal_data['access_grants'] = (
access_grants
if access_grants is not None
else await self._get_access_grants(cal_data['id'], db=db)
access_grants if access_grants is not None else await self._get_access_grants(cal_data['id'], db=db)
)
return CalendarModel.model_validate(cal_data)
async def get_or_create_defaults(
self, user_id: str, db: Optional[AsyncSession] = None
) -> list[CalendarModel]:
async def get_or_create_defaults(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]:
"""Return user's calendars, creating 'Personal' and 'Scheduled Tasks' if none exist."""
async with get_async_db_context(db) as db:
result = await db.execute(
@@ -289,9 +283,7 @@ class CalendarTable:
await db.commit()
return [CalendarModel.model_validate(c) for c in defaults]
async def get_calendars_by_user(
self, user_id: str, db: Optional[AsyncSession] = None
) -> list[CalendarModel]:
async def get_calendars_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]:
"""Owned + shared calendars."""
async with get_async_db_context(db) as db:
user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
@@ -317,14 +309,9 @@ class CalendarTable:
cal_ids = [c.id for c in calendars]
grants_map = await AccessGrants.get_grants_by_resources('calendar', cal_ids, db=db)
return [
await self._to_calendar_model(c, access_grants=grants_map.get(c.id, []), db=db)
for c in calendars
]
return [await self._to_calendar_model(c, access_grants=grants_map.get(c.id, []), db=db) for c in calendars]
async def get_calendar_by_id(
self, id: str, db: Optional[AsyncSession] = None
) -> Optional[CalendarModel]:
async def get_calendar_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarModel]:
async with get_async_db_context(db) as db:
result = await db.execute(select(Calendar).filter(Calendar.id == id))
cal = result.scalars().first()
@@ -414,9 +401,7 @@ class CalendarTable:
.values(is_default=False)
)
# Set the new default
result = await db.execute(
select(Calendar).filter(Calendar.id == calendar_id, Calendar.user_id == user_id)
)
result = await db.execute(select(Calendar).filter(Calendar.id == calendar_id, Calendar.user_id == user_id))
cal = result.scalars().first()
if not cal:
return None
@@ -435,15 +420,11 @@ class CalendarTable:
return False
# Delete attendees for all events in this calendar
event_ids_result = await db.execute(
select(CalendarEvent.id).filter(CalendarEvent.calendar_id == id)
)
event_ids_result = await db.execute(select(CalendarEvent.id).filter(CalendarEvent.calendar_id == id))
event_ids = [r[0] for r in event_ids_result.all()]
if event_ids:
await db.execute(
delete(CalendarEventAttendee).filter(
CalendarEventAttendee.event_id.in_(event_ids)
)
delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids))
)
# Delete events
@@ -465,9 +446,7 @@ class CalendarEventTable:
self, event_id: str, db: Optional[AsyncSession] = None
) -> list[CalendarEventAttendeeModel]:
async with get_async_db_context(db) as db:
result = await db.execute(
select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)
)
result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id))
rows = result.scalars().all()
return [CalendarEventAttendeeModel.model_validate(r) for r in rows]
@@ -515,9 +494,7 @@ class CalendarEventTable:
return await self._to_event_model(event, db=db)
async def get_event_by_id(
self, id: str, db: Optional[AsyncSession] = None
) -> Optional[CalendarEventModel]:
async def get_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarEventModel]:
async with get_async_db_context(db) as db:
result = await db.execute(select(CalendarEvent).filter(CalendarEvent.id == id))
event = result.scalars().first()
@@ -559,9 +536,7 @@ class CalendarEventTable:
# Also get event IDs where user is an attendee
attendee_event_ids_result = await db.execute(
select(CalendarEventAttendee.event_id).filter(
CalendarEventAttendee.user_id == user_id
)
select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id)
)
attendee_event_ids = [r[0] for r in attendee_event_ids_result.all()]
@@ -608,16 +583,12 @@ class CalendarEventTable:
# Batch-load attendees for all events in one query (avoid N+1)
event_ids = [event.id for event, _user in items]
att_result = await db.execute(
select(CalendarEventAttendee).filter(
CalendarEventAttendee.event_id.in_(event_ids)
)
select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids))
)
att_rows = att_result.scalars().all()
att_map: dict[str, list[CalendarEventAttendeeModel]] = {}
for a in att_rows:
att_map.setdefault(a.event_id, []).append(
CalendarEventAttendeeModel.model_validate(a)
)
att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a))
events = []
for event, user in items:
@@ -697,16 +668,12 @@ class CalendarEventTable:
# Batch-load attendees
event_ids = [event.id for event, _user in items]
att_result = await db.execute(
select(CalendarEventAttendee).filter(
CalendarEventAttendee.event_id.in_(event_ids)
)
select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids))
)
att_rows = att_result.scalars().all()
att_map: dict[str, list[CalendarEventAttendeeModel]] = {}
for a in att_rows:
att_map.setdefault(a.event_id, []).append(
CalendarEventAttendeeModel.model_validate(a)
)
att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a))
events = []
for event, user in items:
@@ -732,8 +699,16 @@ class CalendarEventTable:
update_data = form_data.model_dump(exclude_unset=True)
for field in [
'calendar_id', 'title', 'description', 'start_at', 'end_at',
'all_day', 'rrule', 'color', 'location', 'is_cancelled',
'calendar_id',
'title',
'description',
'start_at',
'end_at',
'all_day',
'rrule',
'color',
'location',
'is_cancelled',
]:
if field in update_data:
setattr(event, field, update_data[field])
@@ -750,13 +725,10 @@ class CalendarEventTable:
await db.commit()
return await self._to_event_model(event, db=db)
async def delete_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool:
try:
async with get_async_db_context(db) as db:
await db.execute(
delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == id)
)
await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == id))
await db.execute(delete(CalendarEvent).filter(CalendarEvent.id == id))
await db.commit()
return True
@@ -774,9 +746,7 @@ class CalendarEventAttendeeTable:
"""
async with get_async_db_context(db) as db:
# Remove existing
await db.execute(
delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)
)
await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id))
now = int(time.time_ns())
models = []
@@ -819,20 +789,14 @@ class CalendarEventAttendeeTable:
self, event_id: str, db: Optional[AsyncSession] = None
) -> list[CalendarEventAttendeeModel]:
async with get_async_db_context(db) as db:
result = await db.execute(
select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)
)
result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id))
return [CalendarEventAttendeeModel.model_validate(r) for r in result.scalars().all()]
async def get_events_by_attendee(
self, user_id: str, db: Optional[AsyncSession] = None
) -> list[str]:
async def get_events_by_attendee(self, user_id: str, db: Optional[AsyncSession] = None) -> list[str]:
"""Return event IDs where user is an attendee."""
async with get_async_db_context(db) as db:
result = await db.execute(
select(CalendarEventAttendee.event_id).filter(
CalendarEventAttendee.user_id == user_id
)
select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id)
)
return [r[0] for r in result.all()]
+30 -37
View File
@@ -47,9 +47,7 @@ async def check_calendar_permission(request: Request, user):
)
async def _check_calendar_access(
calendar_id: str, user: UserModel, permission: str = 'write'
) -> CalendarModel:
async def _check_calendar_access(calendar_id: str, user: UserModel, permission: str = 'write') -> CalendarModel:
"""Verify user has access to a calendar. Returns the calendar or raises 403/404."""
cal = await Calendars.get_calendar_by_id(calendar_id)
if not cal:
@@ -139,9 +137,7 @@ async def get_events(
for event in events:
event_dict = event.model_dump()
if event_dict.get('rrule'):
instances = expand_recurring_event(
event_dict, start_ns, end_ns, tz=user.timezone
)
instances = expand_recurring_event(event_dict, start_ns, end_ns, tz=user.timezone)
for inst in instances:
expanded.append(CalendarEventUserResponse(**{**inst, 'user': event.user}))
else:
@@ -189,34 +185,34 @@ async def get_events(
expanded.append(CalendarEventUserResponse(**inst))
# Past runs: single range query joined with automation
runs_with_auto = await AutomationRuns.get_runs_by_user_range(
user.id, start_ns, end_ns
)
runs_with_auto = await AutomationRuns.get_runs_by_user_range(user.id, start_ns, end_ns)
for run, auto in runs_with_auto:
expanded.append(CalendarEventUserResponse(
id=f'run_{run.id}',
calendar_id=scheduled_cal.id,
user_id=user.id,
title=auto.name,
description=run.error if run.status == 'error' else '',
start_at=run.created_at,
end_at=None,
all_day=False,
color=None,
location=None,
data=None,
meta={
'automation_id': auto.id,
'run_id': run.id,
'chat_id': run.chat_id,
'status': run.status,
},
is_cancelled=False,
attendees=[],
created_at=run.created_at,
updated_at=run.created_at,
user=None,
))
expanded.append(
CalendarEventUserResponse(
id=f'run_{run.id}',
calendar_id=scheduled_cal.id,
user_id=user.id,
title=auto.name,
description=run.error if run.status == 'error' else '',
start_at=run.created_at,
end_at=None,
all_day=False,
color=None,
location=None,
data=None,
meta={
'automation_id': auto.id,
'run_id': run.id,
'chat_id': run.chat_id,
'status': run.status,
},
is_cancelled=False,
attendees=[],
created_at=run.created_at,
updated_at=run.created_at,
user=None,
)
)
except Exception as e:
log.warning(f'Failed to compute automation events: {e}', exc_info=True)
@@ -239,9 +235,7 @@ async def search_events(
user: UserModel = Depends(get_verified_user),
):
await check_calendar_permission(request, user)
return await CalendarEvents.search_events(
user_id=user.id, query=query, skip=skip, limit=limit
)
return await CalendarEvents.search_events(user_id=user.id, query=query, skip=skip, limit=limit)
@router.get('/events/{event_id}', response_model=CalendarEventModel)
@@ -341,7 +335,6 @@ async def delete_calendar(request: Request, calendar_id: str, user: UserModel =
if cal.user_id != user.id and user.role != 'admin':
raise HTTPException(status_code=403, detail='Only owner can delete calendar')
result = await Calendars.delete_calendar_by_id(calendar_id)
if not result:
raise HTTPException(status_code=500, detail='Failed to delete')
+1 -3
View File
@@ -539,9 +539,7 @@ async def update_knowledge_access_by_id(
'sharing.public_knowledge',
)
knowledge.access_grants = await AccessGrants.set_access_grants(
'knowledge', id, form_data.access_grants, db=db
)
knowledge.access_grants = await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+7 -6
View File
@@ -2868,9 +2868,6 @@ def _event_to_dict(event, tz) -> dict:
}
async def search_calendar_events(
query: Optional[str] = None,
start: Optional[str] = None,
@@ -2915,7 +2912,11 @@ async def search_calendar_events(
return json.dumps({'error': f'Invalid start datetime: {e}'})
try:
end_ns = _dt_to_ns(end, tz) if end else int(time.time() * 1_000) * 1_000_000 + 365 * 86400 * 1_000_000_000_000
end_ns = (
_dt_to_ns(end, tz)
if end
else int(time.time() * 1_000) * 1_000_000 + 365 * 86400 * 1_000_000_000_000
)
except (ValueError, TypeError) as e:
return json.dumps({'error': f'Invalid end datetime: {e}'})
@@ -2929,7 +2930,8 @@ async def search_calendar_events(
if query:
q = query.lower()
items = [
e for e in items
e
for e in items
if q in (e.title or '').lower()
or q in (e.description or '').lower()
or q in (e.location or '').lower()
@@ -3229,4 +3231,3 @@ async def delete_calendar_event(
except Exception as e:
log.exception(f'delete_calendar_event error: {e}')
return json.dumps({'error': str(e)})
+23 -7
View File
@@ -427,7 +427,11 @@ def _render_openai_tool_call_handler(item: dict, done: bool) -> str:
if atype == 'search':
queries = action.get('queries') or []
query = action.get('query', '')
summary = f'Search: {", ".join(str(q) for q in queries)}' if queries else (f'Search: {query}' if query else '')
summary = (
f'Search: {", ".join(str(q) for q in queries)}'
if queries
else (f'Search: {query}' if query else '')
)
elif atype == 'open_page':
summary = f'Open page: {action.get("url", "")}' if action.get('url') else ''
elif atype == 'find_in_page':
@@ -490,9 +494,13 @@ def serialize_output(output: list) -> str:
files = result_item.get('files')
embeds = result_item.get('embeds', '')
parts.append(f'<details type="tool_calls" done="true" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}" files="{html.escape(json.dumps(files)) if files else ""}" embeds="{html.escape(json.dumps(embeds))}">\n<summary>Tool Executed</summary>\n{html.escape(json.dumps(result_text, ensure_ascii=False))}\n</details>')
parts.append(
f'<details type="tool_calls" done="true" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}" files="{html.escape(json.dumps(files)) if files else ""}" embeds="{html.escape(json.dumps(embeds))}">\n<summary>Tool Executed</summary>\n{html.escape(json.dumps(result_text, ensure_ascii=False))}\n</details>'
)
else:
parts.append(f'<details type="tool_calls" done="false" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}">\n<summary>Executing...</summary>\n</details>')
parts.append(
f'<details type="tool_calls" done="false" id="{call_id}" name="{name}" arguments="{html.escape(json.dumps(arguments))}">\n<summary>Executing...</summary>\n</details>'
)
elif item_type == 'function_call_output':
# Already handled inline with function_call above
@@ -529,9 +537,13 @@ def serialize_output(output: list) -> str:
)
if status == 'completed' or duration is not None or not is_last_item:
parts.append(f'<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>')
parts.append(
f'<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>'
)
else:
parts.append(f'<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>')
parts.append(
f'<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>'
)
elif item_type == 'open_webui:code_interpreter':
# Code interpreter needs to inspect/mutate prior accumulated content
@@ -570,9 +582,13 @@ def serialize_output(output: list) -> str:
output_attr = f' output="{html.escape(output_json)}"'
if status == 'completed' or duration is not None or not is_last_item:
parts.append(f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>')
parts.append(
f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>'
)
else:
parts.append(f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>')
parts.append(
f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>'
)
return '\n'.join(parts).strip()