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
20 changes: 10 additions & 10 deletions taskuary/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ def _rail_tids() -> set:
@app.post('/api/tasks')
def create_task(body: TaskBody):
if not body.Title: raise HTTPException(422, 'Title is required')
tid = store.create_task({k: v for k, v in body.dict().items() if v is not None}, ACTOR)
tid = store.create_task({k: v for k, v in body.model_dump().items() if v is not None}, ACTOR)
# A task created by the owner is their durable TODO. Agent runs may come and go without
# silently completing it; only routed/triaged work is eligible for automatic completion.
from . import selfclose
Expand Down Expand Up @@ -1294,7 +1294,7 @@ def remind_task(task_id: int, body: RemindBody):
def update_task(task_id: int, body: TaskBody, background: BackgroundTasks = None):
t = store.get_task(task_id)
if not t: raise HTTPException(404, 'task not found')
fields = {k: v for k, v in body.dict().items() if v is not None}
fields = {k: v for k, v in body.model_dump().items() if v is not None}
# One task has one worker mode. Switching the kind from the assistant chat to coding (or
# back to a human TODO) must close that live assistant session before the coding terminal
# opens; otherwise both stayed registered on the task and the UI could attach to the wrong
Expand Down Expand Up @@ -1812,8 +1812,8 @@ def split_task_api(task_id: int, body: TaskSplitBody):
"""Triage filed two jobs as one. This task keeps its ref, session and report; the second
job becomes a new task, with the messages you ticked."""
try:
new = reshape.split_task(store, task_id, body.second.dict(),
body.first.dict() if body.first else None, body.move_message_ids, ACTOR)
new = reshape.split_task(store, task_id, body.second.model_dump(),
body.first.model_dump() if body.first else None, body.move_message_ids, ACTOR)
except ValueError as e:
raise HTTPException(404 if 'no task' in str(e) else 422, str(e))
return {'taskId': new, 'ref': task_ref(new)}
Expand Down Expand Up @@ -3226,7 +3226,7 @@ def calendar_prep(body: MeetingPrepBody):
"""
from . import calendar as cal, ownwork
subject = (body.subject or 'the meeting').strip()[:120]
brief = cal.prep_brief(body.dict())
brief = cal.prep_brief(body.model_dump())
ask = (body.instruction or '').strip() or 'Get me ready for this meeting.'
tid = store.create_task({'Title': f'Prep: {subject}'[:200], 'Summary': f'{ask}\n\n{brief}',
'Kind': 'general', 'Tags': ASK_TAG, 'Source': 'calendar',
Expand Down Expand Up @@ -3943,7 +3943,7 @@ def _llm(target_store=None):

@app.post('/api/ingest/push')
def push(body: MsgBody):
m = body.dict()
m = body.model_dump()
m['external_id'] = m.get('external_id') or f'api:{datetime.now().isoformat()}'
m['sent_at'] = m.get('sent_at') or datetime.now().isoformat(sep=' ', timespec='seconds')
out = ingest_message(store, m, llm=_llm())
Expand Down Expand Up @@ -3998,7 +3998,7 @@ def sources():

@app.post('/api/sources')
def save_source(body: SourceBody):
fields = {k: (int(v) if k == 'Active' else v) for k, v in body.dict().items() if v is not None}
fields = {k: (int(v) if k == 'Active' else v) for k, v in body.model_dump().items() if v is not None}
# Owner is PROVENANCE - who or what put this row here - and only a CREATE sets it. It was set
# on every save, and `Owner` is in SOURCE_COLS, so an ordinary Reports-tab save (and the on/off
# toggle, which posts {SourceId, Active}) silently took the row over: a Telegram chat lost the
Expand Down Expand Up @@ -4243,7 +4243,7 @@ def brains():

@app.post('/api/connectors')
def save_connector(body: ConnectorBody):
fields = {k: (int(v) if k == 'Active' else v) for k, v in body.dict().items() if v is not None}
fields = {k: (int(v) if k == 'Active' else v) for k, v in body.model_dump().items() if v is not None}
if fields.get('Name') is not None:
fields['Name'] = fields['Name'].strip()
if not fields['Name']: raise HTTPException(422, 'connector name cannot be blank')
Expand Down Expand Up @@ -5717,7 +5717,7 @@ def policies(): return {'data': store.list_policies(active_only=False)}

@app.post('/api/policies')
def save_policy(body: PolicyBody):
fields = {k: (int(v) if k == 'Active' else v) for k, v in body.dict().items() if v is not None}
fields = {k: (int(v) if k == 'Active' else v) for k, v in body.model_dump().items() if v is not None}
if not fields.get('PolicyId') and not all(fields.get(k) for k in ('Name', 'Kind', 'Action', 'Reason')):
raise HTTPException(422, 'new policies need Name, Kind, Action, Reason')
pid = store.save_policy(fields, ACTOR)
Expand Down Expand Up @@ -6983,7 +6983,7 @@ def metric_delete(mid: int):
@app.post('/api/semantic/metrics/{mid}/fixtures')
def metric_add_fixture(mid: int, body: FixtureBody):
if not store.get_metric(mid): raise HTTPException(404, 'no such metric')
fid = store.add_fixture(mid, body.dict(), ACTOR)
fid = store.add_fixture(mid, body.model_dump(), ACTOR)
store.audit('metric', mid, 'fixture_add', ACTOR, detail={'scope': body.Scope, 'period': body.Period, 'expected': body.Expected})
return _metric_row(store.get_metric(mid)) | {'fixtureId': fid}

Expand Down
24 changes: 24 additions & 0 deletions tests/test_no_pydantic_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""The API speaks pydantic v2.

We require `pydantic>=2`, but v1's accessors still answer through a deprecation shim: every
`body.dict()` in the API logged `PydanticDeprecatedSince20` on each request, and the method is
gone in v3. `server.py` was the last place that called it - ten times, all on request bodies.
This test is the lock, so the eleventh does not arrive quietly.
"""
import pathlib
import re
import unittest

SERVER = pathlib.Path(__file__).resolve().parent.parent / 'taskuary' / 'server.py'
V1_DICT = re.compile(r'\.dict\(\s*\)')


class NoPydanticV1Tests(unittest.TestCase):
def test_the_api_does_not_call_the_v1_dict(self):
hits = [f' server.py:{n}: {line.strip()}'
for n, line in enumerate(SERVER.read_text(encoding='utf-8').splitlines(), 1)
if V1_DICT.search(line)]
self.assertEqual(hits, [], 'pydantic v1 `.dict()` is back - use `.model_dump()`:\n' + '\n'.join(hits))


if __name__ == '__main__': unittest.main()