136 lines
3.8 KiB
Python
136 lines
3.8 KiB
Python
"""
|
|
Ticket models with self-parsing from Jira objects.
|
|
"""
|
|
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
from datetime import datetime
|
|
|
|
|
|
class Attachment(BaseModel):
|
|
id: str
|
|
filename: str
|
|
mimetype: str
|
|
size: int # bytes
|
|
url: str
|
|
content_base64: Optional[str] = None # populated when include_attachments=true
|
|
|
|
@classmethod
|
|
def from_jira(cls, att) -> "Attachment":
|
|
return cls(
|
|
id=att.id,
|
|
filename=att.filename,
|
|
mimetype=att.mimeType,
|
|
size=att.size,
|
|
url=att.content,
|
|
)
|
|
|
|
|
|
class Ticket(BaseModel):
|
|
key: str
|
|
summary: str
|
|
description: Optional[str] = None
|
|
status: str
|
|
status_category: Optional[str] = None
|
|
issue_type: str
|
|
priority: Optional[str] = None
|
|
project: str
|
|
assignee: Optional[str] = None
|
|
reporter: Optional[str] = None
|
|
labels: List[str] = []
|
|
created: Optional[datetime] = None
|
|
updated: Optional[datetime] = None
|
|
url: str
|
|
parent_key: Optional[str] = None # For subtasks
|
|
|
|
@classmethod
|
|
def from_jira(cls, issue, base_url: str) -> "Ticket":
|
|
f = issue.fields
|
|
status_cat = None
|
|
if hasattr(f.status, "statusCategory"):
|
|
status_cat = f.status.statusCategory.name
|
|
|
|
# Get parent key for subtasks
|
|
parent = None
|
|
if hasattr(f, "parent") and f.parent:
|
|
parent = f.parent.key
|
|
|
|
return cls(
|
|
key=issue.key,
|
|
summary=f.summary or "",
|
|
description=f.description,
|
|
status=f.status.name,
|
|
status_category=status_cat,
|
|
issue_type=f.issuetype.name,
|
|
priority=f.priority.name if f.priority else None,
|
|
project=f.project.key,
|
|
assignee=f.assignee.displayName if f.assignee else None,
|
|
reporter=f.reporter.displayName if f.reporter else None,
|
|
labels=f.labels or [],
|
|
created=cls._parse_dt(f.created),
|
|
updated=cls._parse_dt(f.updated),
|
|
url=f"{base_url}/browse/{issue.key}",
|
|
parent_key=parent,
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse_dt(val: Optional[str]) -> Optional[datetime]:
|
|
if not val:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(val.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
class TicketDetail(Ticket):
|
|
comments: List[dict] = []
|
|
linked_issues: List[str] = []
|
|
subtasks: List[str] = []
|
|
attachments: List[Attachment] = []
|
|
|
|
@classmethod
|
|
def from_jira(cls, issue, base_url: str) -> "TicketDetail":
|
|
base = Ticket.from_jira(issue, base_url)
|
|
f = issue.fields
|
|
|
|
comments = []
|
|
if hasattr(f, "comment") and f.comment:
|
|
for c in f.comment.comments:
|
|
comments.append({
|
|
"author": c.author.displayName if hasattr(c, "author") else None,
|
|
"body": c.body,
|
|
"created": c.created,
|
|
})
|
|
|
|
linked = []
|
|
if hasattr(f, "issuelinks") and f.issuelinks:
|
|
for link in f.issuelinks:
|
|
if hasattr(link, "outwardIssue"):
|
|
linked.append(link.outwardIssue.key)
|
|
if hasattr(link, "inwardIssue"):
|
|
linked.append(link.inwardIssue.key)
|
|
|
|
subtasks = []
|
|
if hasattr(f, "subtasks") and f.subtasks:
|
|
subtasks = [st.key for st in f.subtasks]
|
|
|
|
attachments = []
|
|
if hasattr(f, "attachment") and f.attachment:
|
|
attachments = [Attachment.from_jira(a) for a in f.attachment]
|
|
|
|
return cls(
|
|
**base.model_dump(),
|
|
comments=comments,
|
|
linked_issues=linked,
|
|
subtasks=subtasks,
|
|
attachments=attachments,
|
|
)
|
|
|
|
|
|
class TicketList(BaseModel):
|
|
tickets: List[Ticket]
|
|
total: int
|
|
page: int
|
|
page_size: int
|