paper.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. """论文领域模型 —— Paper 数据类及其序列化/反序列化."""
  2. from dataclasses import dataclass, field
  3. from typing import Any
  4. from datetime import datetime
  5. from .author import Author
  6. @dataclass
  7. class Paper:
  8. title: str
  9. authors: list[Author] = field(default_factory=list)
  10. abstract: str | None = None
  11. doi: str | None = None
  12. pmid: str | None = None
  13. arxiv_id: str | None = None
  14. pmc_id: str | None = None
  15. journal: str | None = None
  16. venue_type: str | None = None
  17. year: int | None = None
  18. volume: str | None = None
  19. issue: str | None = None
  20. pages: str | None = None
  21. publisher: str | None = None
  22. pdf_url: str | None = None
  23. source_url: str | None = None
  24. local_pdf_path: str | None = None
  25. keywords: list[str] = field(default_factory=list)
  26. mesh_terms: list[str] = field(default_factory=list)
  27. references: list[str] = field(default_factory=list)
  28. citations: int = 0
  29. source: str = "unknown"
  30. relevance_score: int = 0
  31. notes: str | None = None
  32. tags: list[str] = field(default_factory=list)
  33. category: str | None = None
  34. rating: int | None = None
  35. read_status: str = "unread"
  36. importance: str = "normal"
  37. id: int | None = None
  38. created_at: datetime = field(default_factory=datetime.now)
  39. updated_at: datetime = field(default_factory=datetime.now)
  40. def to_dict(self) -> dict[str, Any]:
  41. return {
  42. "id": self.id,
  43. "title": self.title,
  44. "authors": [a.to_dict() if hasattr(a, "to_dict") else {"name": str(a)} for a in self.authors],
  45. "abstract": self.abstract,
  46. "doi": self.doi,
  47. "pmid": self.pmid,
  48. "arxiv_id": self.arxiv_id,
  49. "pmc_id": self.pmc_id,
  50. "journal": self.journal,
  51. "venue_type": self.venue_type,
  52. "year": self.year,
  53. "volume": self.volume,
  54. "issue": self.issue,
  55. "pages": self.pages,
  56. "publisher": self.publisher,
  57. "pdf_url": self.pdf_url,
  58. "source_url": self.source_url,
  59. "local_pdf_path": self.local_pdf_path,
  60. "keywords": self.keywords,
  61. "mesh_terms": self.mesh_terms,
  62. "references": self.references,
  63. "citations": self.citations,
  64. "source": self.source,
  65. "relevance_score": self.relevance_score,
  66. "notes": self.notes,
  67. "tags": self.tags,
  68. "category": self.category,
  69. "rating": self.rating,
  70. "read_status": self.read_status,
  71. "importance": self.importance,
  72. "created_at": self.created_at.isoformat() if self.created_at else None,
  73. "updated_at": self.updated_at.isoformat() if self.updated_at else None,
  74. }
  75. @classmethod
  76. def from_dict(cls, data: dict[str, Any]) -> "Paper":
  77. def _parse_dt(val: Any) -> datetime | None:
  78. if val is None:
  79. return None
  80. if isinstance(val, datetime):
  81. return val
  82. if isinstance(val, str):
  83. s = val.strip()
  84. if not s:
  85. return None
  86. try:
  87. if s.endswith("Z"):
  88. s = s[:-1] + "+00:00"
  89. return datetime.fromisoformat(s)
  90. except ValueError:
  91. return None
  92. return None
  93. def _safe_int(val: Any, default: int = 0) -> int:
  94. if val is None:
  95. return default
  96. if isinstance(val, bool):
  97. return int(val)
  98. if isinstance(val, int):
  99. return val
  100. try:
  101. return int(float(val))
  102. except (TypeError, ValueError):
  103. return default
  104. def _safe_year(val: Any) -> int | None:
  105. if val is None or val == "":
  106. return None
  107. y = _safe_int(val, -1)
  108. return y if 1000 <= y <= 3000 else None
  109. def _str_list(val: Any) -> list[str]:
  110. if not isinstance(val, list):
  111. return []
  112. out: list[str] = []
  113. for x in val:
  114. if x is None:
  115. continue
  116. if isinstance(x, str):
  117. t = x.strip()
  118. if t:
  119. out.append(t)
  120. else:
  121. out.append(str(x).strip())
  122. return out
  123. def _safe_id(val: Any) -> int | None:
  124. if val is None or val == "":
  125. return None
  126. i = _safe_int(val, -1)
  127. return i if i >= 0 else None
  128. created_at = _parse_dt(data.get("created_at"))
  129. updated_at = _parse_dt(data.get("updated_at"))
  130. return cls(
  131. title=data.get("title", ""),
  132. authors=[Author.from_dict(a) if isinstance(a, dict) else Author(name=str(a)) for a in data.get("authors", [])],
  133. abstract=data.get("abstract"),
  134. doi=data.get("doi"),
  135. pmid=data.get("pmid"),
  136. arxiv_id=data.get("arxiv_id"),
  137. pmc_id=data.get("pmc_id"),
  138. journal=data.get("journal"),
  139. venue_type=data.get("venue_type"),
  140. year=_safe_year(data.get("year")),
  141. volume=data.get("volume"),
  142. issue=data.get("issue"),
  143. pages=data.get("pages"),
  144. publisher=data.get("publisher"),
  145. pdf_url=data.get("pdf_url"),
  146. source_url=data.get("source_url"),
  147. local_pdf_path=data.get("local_pdf_path"),
  148. keywords=_str_list(data.get("keywords")),
  149. mesh_terms=_str_list(data.get("mesh_terms")),
  150. references=_str_list(data.get("references")),
  151. citations=_safe_int(data.get("citations"), 0),
  152. source=data.get("source", "unknown"),
  153. relevance_score=_safe_int(data.get("relevance_score"), 0),
  154. notes=data.get("notes"),
  155. tags=_str_list(data.get("tags")),
  156. category=data.get("category"),
  157. rating=_safe_int(data.get("rating"), 0) if data.get("rating") is not None else None,
  158. read_status=data.get("read_status", "unread"),
  159. importance=data.get("importance", "normal"),
  160. id=_safe_id(data.get("id")),
  161. created_at=created_at or datetime.now(),
  162. updated_at=updated_at or datetime.now(),
  163. )