Skip to content

Data Models

The following Pydantic models represent the core data structures returned by the pyzot.queries modules.

pyzot.models.Item

Bases: BaseModel

Source code in src/pyzot/models.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
class Item(BaseModel):
    item_id: int
    key: str
    item_type: str  # journalArticle, book, conferencePaper, …
    library_id: int
    date_added: datetime
    date_modified: datetime
    fields: dict[str, str] = {}
    creators: list[Creator] = []
    tags: list[str] = []
    collections: list[int] = []
    attachments: list[Attachment] = []
    notes: list[Note] = []

    model_config = {"arbitrary_types_allowed": True}

    # ------------------------------------------------------------------
    # Convenience properties
    # ------------------------------------------------------------------

    @property
    def title(self) -> str:
        return self.fields.get("title", "(no title)")

    @property
    def year(self) -> str | None:
        date = self.fields.get("date", "")
        if date:
            m = re.search(r"\b(\d{4})\b", date)
            if m:
                return m.group(1)
        return None

    @property
    def doi(self) -> str | None:
        return self.fields.get("DOI") or self.fields.get("doi")

    @property
    def authors(self) -> list[str]:
        return [
            c.display_name
            for c in sorted(self.creators, key=lambda c: c.order_index)
            if c.creator_type == "author"
        ]

    @property
    def authors_short(self) -> str:
        """First author last name + et al. for display."""
        a = self.authors
        if not a:
            return ""
        if len(a) == 1:
            return a[0].split(",")[0]
        return a[0].split(",")[0] + " et al."

    @property
    def citation_key(self) -> str | None:
        # Check standard fields first if any plugin adds them
        if "citationKey" in self.fields:
            return self.fields["citationKey"]
        if "citekey" in self.fields:
            return self.fields["citekey"]

        # Check 'extra' field where Better BibTeX stores it
        extra = self.fields.get("extra", "")
        if extra:
            for line in extra.splitlines():
                if line.lower().startswith("citation key:"):
                    return line.split(":", 1)[1].strip()
        return None

    def __str__(self) -> str:
        return f"[{self.item_type}] {self.title} ({self.year or '?'})"

authors_short property

First author last name + et al. for display.

pyzot.models.Collection

Bases: BaseModel

Source code in src/pyzot/models.py
18
19
20
21
22
23
24
25
26
27
class Collection(BaseModel):
    collection_id: int
    key: str
    name: str
    parent_collection_id: int | None = None
    library_id: int
    children: list[Collection] = []
    item_count: int = 0

    model_config = {"arbitrary_types_allowed": True}

pyzot.models.Attachment

Bases: BaseModel

Source code in src/pyzot/models.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Attachment(BaseModel):
    item_id: int
    key: str
    parent_item_id: int
    link_mode: int  # 0=imported_file, 1=imported_url, 2=linked_file, 3=linked_url
    content_type: str
    path: str | None = None
    absolute_path: Path | None = None
    file_exists: bool = False

    model_config = {"arbitrary_types_allowed": True}

    @property
    def link_mode_name(self) -> str:
        return {0: "imported_file", 1: "imported_url", 2: "linked_file", 3: "linked_url"}.get(
            self.link_mode, f"unknown({self.link_mode})"
        )

    @property
    def filename(self) -> str | None:
        if self.path:
            return Path(self.path).name
        return None

pyzot.models.Creator

Bases: BaseModel

Source code in src/pyzot/models.py
30
31
32
33
34
35
36
37
38
39
40
41
class Creator(BaseModel):
    creator_id: int
    first_name: str
    last_name: str
    creator_type: str  # author, editor, translator, …
    order_index: int

    @property
    def display_name(self) -> str:
        if self.last_name and self.first_name:
            return f"{self.last_name}, {self.first_name}"
        return self.last_name or self.first_name

pyzot.models.Note

Bases: BaseModel

Source code in src/pyzot/models.py
69
70
71
72
73
74
75
76
77
78
79
80
81
class Note(BaseModel):
    item_id: int
    parent_item_id: int
    title: str
    note: str  # HTML content
    plain_text: str  # stripped version

    @field_validator("plain_text", mode="before")
    @classmethod
    def strip_html(cls, v: str) -> str:
        if v:
            return re.sub(r"<[^>]+>", "", v).strip()
        return v or ""