Skip to content
Merged
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
40 changes: 20 additions & 20 deletions src/zammad_mcp_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from enum import Enum
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_validator


class TicketState(str, Enum):
Expand Down Expand Up @@ -50,7 +50,7 @@ class Visibility(str, Enum):
class StateBrief(BaseModel):
"""Brief state information."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
name: str
Expand All @@ -60,7 +60,7 @@ class StateBrief(BaseModel):
class PriorityBrief(BaseModel):
"""Brief priority information."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
name: str
Expand All @@ -69,7 +69,7 @@ class PriorityBrief(BaseModel):
class GroupBrief(BaseModel):
"""Brief group information."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
name: str
Expand All @@ -78,7 +78,7 @@ class GroupBrief(BaseModel):
class UserBrief(BaseModel):
"""Brief user information."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
firstname: str | None = None
Expand All @@ -90,7 +90,7 @@ class UserBrief(BaseModel):
class OrganizationBrief(BaseModel):
"""Brief organization information."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
name: str
Expand All @@ -99,10 +99,10 @@ class OrganizationBrief(BaseModel):
class Article(BaseModel):
"""Zammad article (ticket message/comment)."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
ticket_id: int = Field(alias="ticketId")
ticket_id: int = Field(validation_alias=AliasChoices("ticket_id", "ticketId"))
type: str | None = None
sender: str | None = None
subject: str | None = None
Expand All @@ -128,7 +128,7 @@ def parse_user_field(cls, v: Any) -> Any:
class Ticket(BaseModel):
"""Zammad ticket model."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
number: str | None = None
Expand Down Expand Up @@ -191,7 +191,7 @@ def get_group_name(self) -> str | None:
class User(BaseModel):
"""Zammad user model."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
login: str | None = None
Expand Down Expand Up @@ -225,7 +225,7 @@ def get_full_name(self) -> str:
class Organization(BaseModel):
"""Zammad organization model."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
name: str
Expand All @@ -242,7 +242,7 @@ class Organization(BaseModel):
class Group(BaseModel):
"""Zammad group model."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

id: int
name: str
Expand All @@ -256,7 +256,7 @@ class Group(BaseModel):
class GroupCreateRequest(BaseModel):
"""Request model for creating a group."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

name: str = Field(..., min_length=1, max_length=500)
active: bool = True
Expand All @@ -266,7 +266,7 @@ class GroupCreateRequest(BaseModel):
class TicketStats(BaseModel):
"""Ticket statistics."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

total: int
open: int
Expand All @@ -281,7 +281,7 @@ class TicketStats(BaseModel):
class SearchResult(BaseModel):
"""Generic search result wrapper."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

items: list[Any]
total_count: int
Expand All @@ -293,7 +293,7 @@ class SearchResult(BaseModel):
class TicketCreateRequest(BaseModel):
"""Request model for creating a ticket."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

title: str = Field(..., min_length=1, max_length=500)
group: str = Field(..., min_length=1)
Expand All @@ -309,7 +309,7 @@ class TicketCreateRequest(BaseModel):
class TicketUpdateRequest(BaseModel):
"""Request model for updating a ticket."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

title: str | None = Field(None, max_length=500)
group: str | None = None
Expand All @@ -322,7 +322,7 @@ class TicketUpdateRequest(BaseModel):
class ArticleCreateRequest(BaseModel):
"""Request model for creating an article."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

ticket_id: int = Field(..., gt=0)
subject: str | None = Field(None, max_length=500)
Expand All @@ -336,7 +336,7 @@ class ArticleCreateRequest(BaseModel):
class UserCreateRequest(BaseModel):
"""Request model for creating a user."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

login: str | None = None
firstname: str | None = None
Expand All @@ -352,7 +352,7 @@ class UserCreateRequest(BaseModel):
class OrganizationCreateRequest(BaseModel):
"""Request model for creating an organization."""

model_config = ConfigDict(extra="allow")
model_config = ConfigDict(extra="allow", populate_by_name=True)

name: str = Field(..., min_length=1, max_length=500)
shared: bool = True
Expand Down
22 changes: 22 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ArticleCreateRequest,
ArticleType,
Group,
GroupBrief,
Organization,
OrganizationCreateRequest,
PriorityBrief,
Expand Down Expand Up @@ -156,6 +157,27 @@ def test_article_internal_default(self) -> None:
assert article.internal is False


def test_article_accepts_zammad_snake_case_ticket_id(self) -> None:
"""Test article parsing from Zammad API response format."""
article = Article.model_validate({
"id": 1,
"ticket_id": 100,
"body": "This is the article body",
})

assert article.ticket_id == 100

def test_article_accepts_camel_case_ticket_id(self) -> None:
"""Test article parsing remains compatible with camelCase ticketId."""
article = Article.model_validate({
"id": 1,
"ticketId": 100,
"body": "This is the article body",
})

assert article.ticket_id == 100


class TestOrganizationModel:
"""Test suite for Organization model."""

Expand Down