mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00

In order to split backend.proto into a more manageable size, the protobuf handling needed to be updated. This took more time than I would have liked, as each language handles protobuf differently: - The Python Protobuf code ignores "package" directives, and relies solely on how the files are laid out on disk. While it would have been nice to keep the generated files in a private subpackage, Protobuf gets confused if the files are located in a location that does not match their original .proto layout, so the old approach of storing them in _backend/ will not work. They now clutter up pylib/anki instead. I'm rather annoyed by that, but alternatives seem to be having to add an extra level to the Protobuf path, making the other languages suffer, or trying to hack around the issue by munging sys.modules. - Protobufjs fails to expose packages if they don't start with a capital letter, despite the fact that lowercase packages are the norm in most languages :-( This required a patch to fix. - Rust was the easiest, as Prost is relatively straightforward compared to Google's tools. The Protobuf files are now stored in /proto/anki, with a separate package for each file. I've split backend.proto into a few files as a test, but the majority of that work is still to come. The Python Protobuf building is a bit of a hack at the moment, hard-coding "proto" as the top level folder, but it seems to get the job done for now. Also changed the workspace name, as there seems to be a number of Bazel repos moving away from the more awkward reverse DNS naming style.
198 lines
5.8 KiB
Python
198 lines
5.8 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
# pylint: enable=invalid-name
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from typing import Any, List, NewType, Optional, Sequence, Tuple, Union
|
|
|
|
import anki # pylint: disable=unused-import
|
|
import anki.backend_pb2 as _pb
|
|
from anki import hooks
|
|
from anki._legacy import DeprecatedNamesMixin
|
|
from anki.consts import MODEL_STD
|
|
from anki.models import NotetypeDict, NotetypeId, TemplateDict
|
|
from anki.utils import joinFields
|
|
|
|
DuplicateOrEmptyResult = _pb.NoteFieldsCheckResponse.State
|
|
NoteFieldsCheckResult = _pb.NoteFieldsCheckResponse.State
|
|
|
|
# types
|
|
NoteId = NewType("NoteId", int)
|
|
|
|
|
|
class Note(DeprecatedNamesMixin):
|
|
# not currently exposed
|
|
flags = 0
|
|
data = ""
|
|
id: NoteId
|
|
mid: NotetypeId
|
|
|
|
def __init__(
|
|
self,
|
|
col: anki.collection.Collection,
|
|
model: Optional[Union[NotetypeDict, NotetypeId]] = None,
|
|
id: Optional[NoteId] = None,
|
|
) -> None:
|
|
assert not (model and id)
|
|
notetype_id = model["id"] if isinstance(model, dict) else model
|
|
self.col = col.weakref()
|
|
|
|
if id:
|
|
# existing note
|
|
self.id = id
|
|
self.load()
|
|
else:
|
|
# new note for provided notetype
|
|
self._load_from_backend_note(self.col._backend.new_note(notetype_id))
|
|
|
|
def load(self) -> None:
|
|
note = self.col._backend.get_note(self.id)
|
|
assert note
|
|
self._load_from_backend_note(note)
|
|
|
|
def _load_from_backend_note(self, note: _pb.Note) -> None:
|
|
self.id = NoteId(note.id)
|
|
self.guid = note.guid
|
|
self.mid = NotetypeId(note.notetype_id)
|
|
self.mod = note.mtime_secs
|
|
self.usn = note.usn
|
|
self.tags = list(note.tags)
|
|
self.fields = list(note.fields)
|
|
self._fmap = self.col.models.field_map(self.note_type())
|
|
|
|
def _to_backend_note(self) -> _pb.Note:
|
|
hooks.note_will_flush(self)
|
|
return _pb.Note(
|
|
id=self.id,
|
|
guid=self.guid,
|
|
notetype_id=self.mid,
|
|
mtime_secs=self.mod,
|
|
usn=self.usn,
|
|
tags=self.tags,
|
|
fields=self.fields,
|
|
)
|
|
|
|
def flush(self) -> None:
|
|
"""This preserves any current checkpoint.
|
|
For an undo entry, use col.update_note() instead."""
|
|
assert self.id != 0
|
|
self.col._backend.update_note(
|
|
note=self._to_backend_note(), skip_undo_entry=True
|
|
)
|
|
|
|
def joined_fields(self) -> str:
|
|
return joinFields(self.fields)
|
|
|
|
def ephemeral_card(
|
|
self,
|
|
ord: int = 0,
|
|
*,
|
|
custom_note_type: NotetypeDict = None,
|
|
custom_template: TemplateDict = None,
|
|
fill_empty: bool = False,
|
|
) -> anki.cards.Card:
|
|
card = anki.cards.Card(self.col)
|
|
card.ord = ord
|
|
card.did = anki.decks.DEFAULT_DECK_ID
|
|
|
|
model = custom_note_type or self.note_type()
|
|
template = copy.copy(
|
|
custom_template
|
|
or (
|
|
model["tmpls"][ord] if model["type"] == MODEL_STD else model["tmpls"][0]
|
|
)
|
|
)
|
|
# may differ in cloze case
|
|
template["ord"] = card.ord
|
|
|
|
output = anki.template.TemplateRenderContext.from_card_layout(
|
|
self,
|
|
card,
|
|
notetype=model,
|
|
template=template,
|
|
fill_empty=fill_empty,
|
|
).render()
|
|
card.set_render_output(output)
|
|
card._note = self
|
|
return card
|
|
|
|
def cards(self) -> List[anki.cards.Card]:
|
|
return [self.col.getCard(id) for id in self.card_ids()]
|
|
|
|
def card_ids(self) -> Sequence[anki.cards.CardId]:
|
|
return self.col.card_ids_of_note(self.id)
|
|
|
|
def note_type(self) -> Optional[NotetypeDict]:
|
|
return self.col.models.get(self.mid)
|
|
|
|
_note_type = property(note_type)
|
|
|
|
def cloze_numbers_in_fields(self) -> Sequence[int]:
|
|
return self.col._backend.cloze_numbers_in_note(self._to_backend_note())
|
|
|
|
# Dict interface
|
|
##################################################
|
|
|
|
def keys(self) -> List[str]:
|
|
return list(self._fmap.keys())
|
|
|
|
def values(self) -> List[str]:
|
|
return self.fields
|
|
|
|
def items(self) -> List[Tuple[str, str]]:
|
|
return [(f["name"], self.fields[ord]) for ord, f in sorted(self._fmap.values())]
|
|
|
|
def _field_index(self, key: str) -> int:
|
|
try:
|
|
return self._fmap[key][0]
|
|
except Exception as exc:
|
|
raise KeyError(key) from exc
|
|
|
|
def __getitem__(self, key: str) -> str:
|
|
return self.fields[self._field_index(key)]
|
|
|
|
def __setitem__(self, key: str, value: str) -> None:
|
|
self.fields[self._field_index(key)] = value
|
|
|
|
def __contains__(self, key: str) -> bool:
|
|
return key in self._fmap
|
|
|
|
# Tags
|
|
##################################################
|
|
|
|
def has_tag(self, tag: str) -> bool:
|
|
return self.col.tags.inList(tag, self.tags)
|
|
|
|
def remove_tag(self, tag: str) -> None:
|
|
rem = []
|
|
for tag_ in self.tags:
|
|
if tag_.lower() == tag.lower():
|
|
rem.append(tag_)
|
|
for tag_ in rem:
|
|
self.tags.remove(tag_)
|
|
|
|
def add_tag(self, tag: str) -> None:
|
|
"Add tag. Duplicates will be stripped on save."
|
|
self.tags.append(tag)
|
|
|
|
def string_tags(self) -> Any:
|
|
return self.col.tags.join(self.col.tags.canonify(self.tags))
|
|
|
|
def set_tags_from_str(self, tags: str) -> None:
|
|
self.tags = self.col.tags.split(tags)
|
|
|
|
# Unique/duplicate/cloze check
|
|
##################################################
|
|
|
|
def fields_check(self) -> NoteFieldsCheckResult.V:
|
|
return self.col._backend.note_fields_check(self._to_backend_note()).state
|
|
|
|
dupeOrEmpty = duplicate_or_empty = fields_check
|
|
|
|
|
|
Note.register_deprecated_aliases(
|
|
delTag=Note.remove_tag, _fieldOrd=Note._field_index, model=Note.note_type
|
|
)
|