fix: convert BDAY datetime.date to string before Pydantic validation
pythonvCard4 parses vCard BDAY fields into datetime.date objects, but the Contact model expects Optional[str]. This caused a validation error that crashed the entire contact list. Convert at the client layer (consistent with the calendar client pattern) with a defensive check at the server mapping layer. Closes #672 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
97ae750916
commit
da380a38c6
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from pythonvCard4.vcard import Contact
|
from pythonvCard4.vcard import Contact
|
||||||
|
|
||||||
@@ -272,7 +273,9 @@ class ContactsClient(BaseNextcloudClient):
|
|||||||
"contact": {
|
"contact": {
|
||||||
"fullname": contact.fn,
|
"fullname": contact.fn,
|
||||||
"nickname": contact.nickname,
|
"nickname": contact.nickname,
|
||||||
"birthday": contact.bday,
|
"birthday": contact.bday.isoformat()
|
||||||
|
if isinstance(contact.bday, date)
|
||||||
|
else contact.bday,
|
||||||
"email": contact.email,
|
"email": contact.email,
|
||||||
"tel": contact.tel,
|
"tel": contact.tel,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from datetime import date
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
@@ -83,7 +84,9 @@ def _raw_contact_to_model(raw: dict) -> Contact:
|
|||||||
uid=raw["vcard_id"],
|
uid=raw["vcard_id"],
|
||||||
fn=contact_info.get("fullname", ""),
|
fn=contact_info.get("fullname", ""),
|
||||||
etag=raw.get("getetag"),
|
etag=raw.get("getetag"),
|
||||||
birthday=contact_info.get("birthday"),
|
birthday=contact_info["birthday"].isoformat()
|
||||||
|
if isinstance(contact_info.get("birthday"), date)
|
||||||
|
else contact_info.get("birthday"),
|
||||||
emails=emails,
|
emails=emails,
|
||||||
phones=phones,
|
phones=phones,
|
||||||
custom_fields=custom_fields,
|
custom_fields=custom_fields,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Unit tests for Pydantic response models."""
|
"""Unit tests for Pydantic response models."""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nextcloud_mcp_server.models.contacts import (
|
from nextcloud_mcp_server.models.contacts import (
|
||||||
@@ -310,6 +312,46 @@ def test_contact_mapping_preserves_email_birthday_nickname():
|
|||||||
assert contact.custom_fields["nickname"] == "JD"
|
assert contact.custom_fields["nickname"] == "JD"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_contact_mapping_birthday_datetime_date_object():
|
||||||
|
"""Test that a datetime.date birthday is converted to ISO string.
|
||||||
|
|
||||||
|
Regression test for GH #672: pythonvCard4 returns datetime.date objects
|
||||||
|
for BDAY fields, which caused Pydantic validation errors.
|
||||||
|
"""
|
||||||
|
raw_contact = {
|
||||||
|
"vcard_id": "bday-date-1",
|
||||||
|
"contact": {
|
||||||
|
"fullname": "Date Object",
|
||||||
|
"birthday": date(1990, 5, 15),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
contact = _map_contact(raw_contact)
|
||||||
|
|
||||||
|
assert contact.birthday == "1990-05-15"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_contact_mapping_birthday_apple_unknown_year():
|
||||||
|
"""Test Apple/iOS unknown-year birthday convention (year 1604).
|
||||||
|
|
||||||
|
Apple contacts use BDAY;VALUE=DATE:16040808 when the birth year is unknown.
|
||||||
|
pythonvCard4 parses this as datetime.date(1604, 8, 8).
|
||||||
|
"""
|
||||||
|
raw_contact = {
|
||||||
|
"vcard_id": "bday-apple-1",
|
||||||
|
"contact": {
|
||||||
|
"fullname": "Apple Contact",
|
||||||
|
"birthday": date(1604, 8, 8),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
contact = _map_contact(raw_contact)
|
||||||
|
|
||||||
|
assert contact.birthday == "1604-08-08"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_contact_mapping_multiple_emails():
|
def test_contact_mapping_multiple_emails():
|
||||||
"""Test that multiple emails are mapped correctly."""
|
"""Test that multiple emails are mapped correctly."""
|
||||||
|
|||||||
Reference in New Issue
Block a user