migrate the remaining methods

This commit is contained in:
Damien Elmes 2020-05-24 08:36:50 +10:00
parent 4bf8175bcb
commit 89dde3aeb0
13 changed files with 390 additions and 444 deletions

View file

@ -158,6 +158,27 @@ service BackendService {
rpc SyncMedia (SyncMediaIn) returns (Empty);
rpc AbortMediaSync (Empty) returns (Empty);
rpc BeforeUpload (Empty) returns (Empty);
// translation/messages
rpc TranslateString (TranslateStringIn) returns (String);
rpc FormatTimespan (FormatTimespanIn) returns (String);
// tags
rpc RegisterTags (RegisterTagsIn) returns (Bool);
rpc AllTags (Empty) returns (AllTagsOut);
rpc GetChangedTags (Int32) returns (GetChangedTagsOut);
// config/preferences
rpc GetConfigJson (String) returns (Json);
rpc SetConfigJson (SetConfigJsonIn) returns (Empty);
rpc RemoveConfig (String) returns (Empty);
rpc SetAllConfig (Json) returns (Empty);
rpc GetAllConfig (Empty) returns (Json);
rpc GetPreferences (Empty) returns (Preferences);
rpc SetPreferences (Preferences) returns (Empty);
}
// Protobuf stored in .anki2 files
@ -398,49 +419,9 @@ message I18nBackendInit {
string locale_folder_path = 5;
}
// Legacy enum - needs migrating to RPC above
// Errors
///////////////////////////////////////////////////////////
message BackendInput {
oneof value {
TranslateStringIn translate_string = 30;
FormatTimeSpanIn format_time_span = 31;
RegisterTagsIn register_tags = 48;
Empty all_tags = 50;
int32 get_changed_tags = 51;
string get_config_json = 52;
SetConfigJson set_config_json = 53;
bytes set_all_config = 54;
Empty get_all_config = 55;
Empty get_preferences = 84;
Preferences set_preferences = 85;
}
}
message BackendOutput {
oneof value {
string translate_string = 30;
string format_time_span = 31;
bool register_tags = 48;
AllTagsOut all_tags = 50;
GetChangedTagsOut get_changed_tags = 51;
bytes get_config_json = 52;
Empty set_config_json = 53;
Empty set_all_config = 54;
bytes get_all_config = 55;
Preferences get_preferences = 84;
Empty set_preferences = 85;
BackendError error = 2047;
}
}
message BackendError {
// localized error description suitable for displaying to the user
string localized = 1;
@ -504,6 +485,10 @@ message MediaSyncUploadProgress {
uint32 deletions = 2;
}
// Messages
///////////////////////////////////////////////////////////
message SchedTimingTodayOut {
uint32 days_elapsed = 1;
int64 next_day_at = 2;
@ -632,7 +617,7 @@ message TranslateArgValue {
}
}
message FormatTimeSpanIn {
message FormatTimespanIn {
enum Context {
PRECISE = 0;
ANSWER_BUTTONS = 1;
@ -736,12 +721,9 @@ message GetChangedTagsOut {
repeated string tags = 1;
}
message SetConfigJson {
message SetConfigJsonIn {
string key = 1;
oneof op {
bytes val = 2;
Empty remove = 3;
}
}
enum StockNoteType {

View file

@ -26,7 +26,13 @@ from anki.lang import _
from anki.media import MediaManager, media_paths_from_col_path
from anki.models import ModelManager
from anki.notes import Note
from anki.rsbackend import TR, DBError, RustBackend, pb
from anki.rsbackend import (
TR,
DBError,
FormatTimeSpanContext,
RustBackend,
pb,
)
from anki.sched import Scheduler as V1Scheduler
from anki.schedv2 import Scheduler as V2Scheduler
from anki.tags import TagManager
@ -65,13 +71,23 @@ class Collection:
n = os.path.splitext(os.path.basename(self.path))[0]
return n
def tr(self, key: TR, **kwargs: Union[str, int, float]) -> str:
return self.backend.translate(key, **kwargs)
def weakref(self) -> Collection:
"Shortcut to create a weak reference that doesn't break code completion."
return weakref.proxy(self)
# I18n/messages
##########################################################################
def tr(self, key: TR, **kwargs: Union[str, int, float]) -> str:
return self.backend.translate(key, **kwargs)
def format_timespan(
self,
seconds: float,
context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS,
) -> str:
return self.backend.format_timespan(seconds, context)
# Scheduler
##########################################################################

View file

@ -23,6 +23,7 @@ import weakref
from typing import Any
import anki
from anki.rsbackend import NotFoundError, from_json_bytes, to_json_bytes
class ConfigManager:
@ -30,10 +31,13 @@ class ConfigManager:
self.col = col.weakref()
def get_immutable(self, key: str) -> Any:
return self.col.backend.get_config_json(key)
try:
return from_json_bytes(self.col.backend.get_config_json(key))
except NotFoundError:
raise KeyError
def set(self, key: str, val: Any) -> None:
self.col.backend.set_config_json(key, val)
self.col.backend.set_config_json(key, to_json_bytes(val))
def remove(self, key: str) -> None:
self.col.backend.remove_config(key)

View file

@ -153,7 +153,7 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception:
MediaSyncProgress = pb.MediaSyncProgress
FormatTimeSpanContext = pb.FormatTimeSpanIn.Context
FormatTimeSpanContext = pb.FormatTimespanIn.Context
class ProgressKind(enum.Enum):
@ -203,35 +203,6 @@ class RustBackend:
self._backend = ankirspy.open_backend(init_msg.SerializeToString())
self._backend.set_progress_callback(_on_progress)
def _run_command(
self, input: pb.BackendInput, release_gil: bool = False
) -> pb.BackendOutput:
input_bytes = input.SerializeToString()
output_bytes = self._backend.command(input_bytes, release_gil)
output = pb.BackendOutput()
output.ParseFromString(output_bytes)
kind = output.WhichOneof("value")
if kind == "error":
raise proto_exception_to_native(output.error)
else:
return output
def translate(self, key: TR, **kwargs: Union[str, int, float]) -> str:
return self._run_command(
pb.BackendInput(translate_string=translate_string_in(key, **kwargs))
).translate_string
def format_time_span(
self,
seconds: float,
context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS,
) -> str:
return self._run_command(
pb.BackendInput(
format_time_span=pb.FormatTimeSpanIn(seconds=seconds, context=context)
)
).format_time_span
def db_query(
self, sql: str, args: Sequence[ValueForDB], first_row_only: bool
) -> List[DBRow]:
@ -254,76 +225,23 @@ class RustBackend:
def _db_command(self, input: Dict[str, Any]) -> Any:
return orjson.loads(self._backend.db_command(orjson.dumps(input)))
def all_tags(self) -> Iterable[TagUsnTuple]:
return self._run_command(pb.BackendInput(all_tags=pb.Empty())).all_tags.tags
def translate(self, key: TR, **kwargs: Union[str, int, float]) -> str:
return self.translate_string(translate_string_in(key, **kwargs))
def register_tags(self, tags: str, usn: Optional[int], clear_first: bool) -> bool:
if usn is None:
preserve_usn = False
usn_ = 0
else:
usn_ = usn
preserve_usn = True
return self._run_command(
pb.BackendInput(
register_tags=pb.RegisterTagsIn(
tags=tags,
usn=usn_,
preserve_usn=preserve_usn,
clear_first=clear_first,
def format_time_span(
self,
seconds: float,
context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS,
) -> str:
print(
"please use col.format_timespan() instead of col.backend.format_time_span()"
)
)
).register_tags
return self.format_timespan(seconds, context)
def get_changed_tags(self, usn: int) -> List[str]:
return list(
self._run_command(
pb.BackendInput(get_changed_tags=usn)
).get_changed_tags.tags
)
def get_config_json(self, key: str) -> Any:
b = self._run_command(pb.BackendInput(get_config_json=key)).get_config_json
if b == b"":
raise KeyError
return orjson.loads(b)
def set_config_json(self, key: str, val: Any):
self._run_command(
pb.BackendInput(
set_config_json=pb.SetConfigJson(key=key, val=orjson.dumps(val))
)
)
def remove_config(self, key: str):
self._run_command(
pb.BackendInput(
set_config_json=pb.SetConfigJson(key=key, remove=pb.Empty())
)
)
def get_all_config(self) -> Dict[str, Any]:
jstr = self._run_command(
pb.BackendInput(get_all_config=pb.Empty())
).get_all_config
return orjson.loads(jstr)
def set_all_config(self, conf: Dict[str, Any]):
self._run_command(pb.BackendInput(set_all_config=orjson.dumps(conf)))
def get_preferences(self) -> pb.Preferences:
return self._run_command(
pb.BackendInput(get_preferences=pb.Empty())
).get_preferences
def set_preferences(self, prefs: pb.Preferences) -> None:
self._run_command(pb.BackendInput(set_preferences=prefs))
def _run_command2(self, method: int, input: Any) -> bytes:
def _run_command(self, method: int, input: Any) -> bytes:
input_bytes = input.SerializeToString()
try:
return self._backend.command2(method, input_bytes)
return self._backend.command(method, input_bytes)
except Exception as e:
err_bytes = bytes(e.args[0])
err = pb.BackendError()
@ -338,7 +256,7 @@ class RustBackend:
def extract_av_tags(self, text: str, question_side: bool) -> pb.ExtractAVTagsOut:
input = pb.ExtractAVTagsIn(text=text, question_side=question_side)
output = pb.ExtractAVTagsOut()
output.ParseFromString(self._run_command2(1, input))
output.ParseFromString(self._run_command(1, input))
return output
def extract_latex(
@ -346,19 +264,19 @@ class RustBackend:
) -> pb.ExtractLatexOut:
input = pb.ExtractLatexIn(text=text, svg=svg, expand_clozes=expand_clozes)
output = pb.ExtractLatexOut()
output.ParseFromString(self._run_command2(2, input))
output.ParseFromString(self._run_command(2, input))
return output
def get_empty_cards(self) -> pb.EmptyCardsReport:
input = pb.Empty()
output = pb.EmptyCardsReport()
output.ParseFromString(self._run_command2(3, input))
output.ParseFromString(self._run_command(3, input))
return output
def render_existing_card(self, card_id: int, browser: bool) -> pb.RenderCardOut:
input = pb.RenderExistingCardIn(card_id=card_id, browser=browser)
output = pb.RenderCardOut()
output.ParseFromString(self._run_command2(4, input))
output.ParseFromString(self._run_command(4, input))
return output
def render_uncommitted_card(
@ -368,25 +286,25 @@ class RustBackend:
note=note, card_ord=card_ord, template=template, fill_empty=fill_empty
)
output = pb.RenderCardOut()
output.ParseFromString(self._run_command2(5, input))
output.ParseFromString(self._run_command(5, input))
return output
def strip_av_tags(self, val: str) -> str:
input = pb.String(val=val)
output = pb.String()
output.ParseFromString(self._run_command2(6, input))
output.ParseFromString(self._run_command(6, input))
return output.val
def search_cards(self, search: str, order: pb.SortOrder) -> Sequence[int]:
input = pb.SearchCardsIn(search=search, order=order)
output = pb.SearchCardsOut()
output.ParseFromString(self._run_command2(7, input))
output.ParseFromString(self._run_command(7, input))
return output.card_ids
def search_notes(self, search: str) -> Sequence[int]:
input = pb.SearchNotesIn(search=search)
output = pb.SearchNotesOut()
output.ParseFromString(self._run_command2(8, input))
output.ParseFromString(self._run_command(8, input))
return output.note_ids
def find_and_replace(
@ -407,67 +325,67 @@ class RustBackend:
field_name=field_name,
)
output = pb.UInt32()
output.ParseFromString(self._run_command2(9, input))
output.ParseFromString(self._run_command(9, input))
return output.val
def local_minutes_west(self, val: int) -> int:
input = pb.Int64(val=val)
output = pb.Int32()
output.ParseFromString(self._run_command2(10, input))
output.ParseFromString(self._run_command(10, input))
return output.val
def set_local_minutes_west(self, val: int) -> pb.Empty:
input = pb.Int32(val=val)
output = pb.Empty()
output.ParseFromString(self._run_command2(11, input))
output.ParseFromString(self._run_command(11, input))
return output
def sched_timing_today(self) -> pb.SchedTimingTodayOut:
input = pb.Empty()
output = pb.SchedTimingTodayOut()
output.ParseFromString(self._run_command2(12, input))
output.ParseFromString(self._run_command(12, input))
return output
def studied_today(self, cards: int, seconds: float) -> str:
input = pb.StudiedTodayIn(cards=cards, seconds=seconds)
output = pb.String()
output.ParseFromString(self._run_command2(13, input))
output.ParseFromString(self._run_command(13, input))
return output.val
def congrats_learn_message(self, next_due: float, remaining: int) -> str:
input = pb.CongratsLearnMessageIn(next_due=next_due, remaining=remaining)
output = pb.String()
output.ParseFromString(self._run_command2(14, input))
output.ParseFromString(self._run_command(14, input))
return output.val
def check_media(self) -> pb.CheckMediaOut:
input = pb.Empty()
output = pb.CheckMediaOut()
output.ParseFromString(self._run_command2(15, input))
output.ParseFromString(self._run_command(15, input))
return output
def trash_media_files(self, fnames: Sequence[str]) -> pb.Empty:
input = pb.TrashMediaFilesIn(fnames=fnames)
output = pb.Empty()
output.ParseFromString(self._run_command2(16, input))
output.ParseFromString(self._run_command(16, input))
return output
def add_media_file(self, desired_name: str, data: bytes) -> str:
input = pb.AddMediaFileIn(desired_name=desired_name, data=data)
output = pb.String()
output.ParseFromString(self._run_command2(17, input))
output.ParseFromString(self._run_command(17, input))
return output.val
def empty_trash(self) -> pb.Empty:
input = pb.Empty()
output = pb.Empty()
output.ParseFromString(self._run_command2(18, input))
output.ParseFromString(self._run_command(18, input))
return output
def restore_trash(self) -> pb.Empty:
input = pb.Empty()
output = pb.Empty()
output.ParseFromString(self._run_command2(19, input))
output.ParseFromString(self._run_command(19, input))
return output
def add_or_update_deck_legacy(
@ -477,37 +395,37 @@ class RustBackend:
deck=deck, preserve_usn_and_mtime=preserve_usn_and_mtime
)
output = pb.DeckID()
output.ParseFromString(self._run_command2(20, input))
output.ParseFromString(self._run_command(20, input))
return output.did
def deck_tree(self, include_counts: bool, top_deck_id: int) -> pb.DeckTreeNode:
input = pb.DeckTreeIn(include_counts=include_counts, top_deck_id=top_deck_id)
output = pb.DeckTreeNode()
output.ParseFromString(self._run_command2(21, input))
output.ParseFromString(self._run_command(21, input))
return output
def deck_tree_legacy(self) -> bytes:
input = pb.Empty()
output = pb.Json()
output.ParseFromString(self._run_command2(22, input))
output.ParseFromString(self._run_command(22, input))
return output.json
def get_all_decks_legacy(self) -> bytes:
input = pb.Empty()
output = pb.Json()
output.ParseFromString(self._run_command2(23, input))
output.ParseFromString(self._run_command(23, input))
return output.json
def get_deck_id_by_name(self, val: str) -> int:
input = pb.String(val=val)
output = pb.DeckID()
output.ParseFromString(self._run_command2(24, input))
output.ParseFromString(self._run_command(24, input))
return output.did
def get_deck_legacy(self, did: int) -> bytes:
input = pb.DeckID(did=did)
output = pb.Json()
output.ParseFromString(self._run_command2(25, input))
output.ParseFromString(self._run_command(25, input))
return output.json
def get_deck_names(
@ -517,19 +435,19 @@ class RustBackend:
skip_empty_default=skip_empty_default, include_filtered=include_filtered
)
output = pb.DeckNames()
output.ParseFromString(self._run_command2(26, input))
output.ParseFromString(self._run_command(26, input))
return output.entries
def new_deck_legacy(self, val: bool) -> bytes:
input = pb.Bool(val=val)
output = pb.Json()
output.ParseFromString(self._run_command2(27, input))
output.ParseFromString(self._run_command(27, input))
return output.json
def remove_deck(self, did: int) -> pb.Empty:
input = pb.DeckID(did=did)
output = pb.Empty()
output.ParseFromString(self._run_command2(28, input))
output.ParseFromString(self._run_command(28, input))
return output
def add_or_update_deck_config_legacy(
@ -539,76 +457,76 @@ class RustBackend:
config=config, preserve_usn_and_mtime=preserve_usn_and_mtime
)
output = pb.DeckConfigID()
output.ParseFromString(self._run_command2(29, input))
output.ParseFromString(self._run_command(29, input))
return output.dcid
def all_deck_config_legacy(self) -> bytes:
input = pb.Empty()
output = pb.Json()
output.ParseFromString(self._run_command2(30, input))
output.ParseFromString(self._run_command(30, input))
return output.json
def get_deck_config_legacy(self, dcid: int) -> bytes:
input = pb.DeckConfigID(dcid=dcid)
output = pb.Json()
output.ParseFromString(self._run_command2(31, input))
output.ParseFromString(self._run_command(31, input))
return output.json
def new_deck_config_legacy(self) -> bytes:
input = pb.Empty()
output = pb.Json()
output.ParseFromString(self._run_command2(32, input))
output.ParseFromString(self._run_command(32, input))
return output.json
def remove_deck_config(self, dcid: int) -> pb.Empty:
input = pb.DeckConfigID(dcid=dcid)
output = pb.Empty()
output.ParseFromString(self._run_command2(33, input))
output.ParseFromString(self._run_command(33, input))
return output
def get_card(self, cid: int) -> pb.Card:
input = pb.CardID(cid=cid)
output = pb.Card()
output.ParseFromString(self._run_command2(34, input))
output.ParseFromString(self._run_command(34, input))
return output
def update_card(self, input: pb.Card) -> pb.Empty:
output = pb.Empty()
output.ParseFromString(self._run_command2(35, input))
output.ParseFromString(self._run_command(35, input))
return output
def add_card(self, input: pb.Card) -> int:
output = pb.CardID()
output.ParseFromString(self._run_command2(36, input))
output.ParseFromString(self._run_command(36, input))
return output.cid
def new_note(self, ntid: int) -> pb.Note:
input = pb.NoteTypeID(ntid=ntid)
output = pb.Note()
output.ParseFromString(self._run_command2(37, input))
output.ParseFromString(self._run_command(37, input))
return output
def add_note(self, note: pb.Note, deck_id: int) -> int:
input = pb.AddNoteIn(note=note, deck_id=deck_id)
output = pb.NoteID()
output.ParseFromString(self._run_command2(38, input))
output.ParseFromString(self._run_command(38, input))
return output.nid
def update_note(self, input: pb.Note) -> pb.Empty:
output = pb.Empty()
output.ParseFromString(self._run_command2(39, input))
output.ParseFromString(self._run_command(39, input))
return output
def get_note(self, nid: int) -> pb.Note:
input = pb.NoteID(nid=nid)
output = pb.Note()
output.ParseFromString(self._run_command2(40, input))
output.ParseFromString(self._run_command(40, input))
return output
def add_note_tags(self, nids: Sequence[int], tags: str) -> int:
input = pb.AddNoteTagsIn(nids=nids, tags=tags)
output = pb.UInt32()
output.ParseFromString(self._run_command2(41, input))
output.ParseFromString(self._run_command(41, input))
return output.val
def update_note_tags(
@ -618,12 +536,12 @@ class RustBackend:
nids=nids, tags=tags, replacement=replacement, regex=regex
)
output = pb.UInt32()
output.ParseFromString(self._run_command2(42, input))
output.ParseFromString(self._run_command(42, input))
return output.val
def cloze_numbers_in_note(self, input: pb.Note) -> Sequence[int]:
output = pb.ClozeNumbersInNoteOut()
output.ParseFromString(self._run_command2(43, input))
output.ParseFromString(self._run_command(43, input))
return output.numbers
def after_note_updates(
@ -635,13 +553,13 @@ class RustBackend:
generate_cards=generate_cards,
)
output = pb.Empty()
output.ParseFromString(self._run_command2(44, input))
output.ParseFromString(self._run_command(44, input))
return output
def field_names_for_notes(self, nids: Sequence[int]) -> Sequence[str]:
input = pb.FieldNamesForNotesIn(nids=nids)
output = pb.FieldNamesForNotesOut()
output.ParseFromString(self._run_command2(45, input))
output.ParseFromString(self._run_command(45, input))
return output.fields
def add_or_update_notetype(self, json: bytes, preserve_usn_and_mtime: bool) -> int:
@ -649,43 +567,43 @@ class RustBackend:
json=json, preserve_usn_and_mtime=preserve_usn_and_mtime
)
output = pb.NoteTypeID()
output.ParseFromString(self._run_command2(46, input))
output.ParseFromString(self._run_command(46, input))
return output.ntid
def get_stock_notetype_legacy(self, kind: pb.StockNoteType) -> bytes:
input = pb.GetStockNotetypeIn(kind=kind)
output = pb.Json()
output.ParseFromString(self._run_command2(47, input))
output.ParseFromString(self._run_command(47, input))
return output.json
def get_notetype_legacy(self, ntid: int) -> bytes:
input = pb.NoteTypeID(ntid=ntid)
output = pb.Json()
output.ParseFromString(self._run_command2(48, input))
output.ParseFromString(self._run_command(48, input))
return output.json
def get_notetype_names(self) -> Sequence[pb.NoteTypeNameID]:
input = pb.Empty()
output = pb.NoteTypeNames()
output.ParseFromString(self._run_command2(49, input))
output.ParseFromString(self._run_command(49, input))
return output.entries
def get_notetype_names_and_counts(self) -> Sequence[pb.NoteTypeNameIDUseCount]:
input = pb.Empty()
output = pb.NoteTypeUseCounts()
output.ParseFromString(self._run_command2(50, input))
output.ParseFromString(self._run_command(50, input))
return output.entries
def get_notetype_id_by_name(self, val: str) -> int:
input = pb.String(val=val)
output = pb.NoteTypeID()
output.ParseFromString(self._run_command2(51, input))
output.ParseFromString(self._run_command(51, input))
return output.ntid
def remove_notetype(self, ntid: int) -> pb.Empty:
input = pb.NoteTypeID(ntid=ntid)
output = pb.Empty()
output.ParseFromString(self._run_command2(52, input))
output.ParseFromString(self._run_command(52, input))
return output
def open_collection(
@ -702,37 +620,114 @@ class RustBackend:
log_path=log_path,
)
output = pb.Empty()
output.ParseFromString(self._run_command2(53, input))
output.ParseFromString(self._run_command(53, input))
return output
def close_collection(self, downgrade_to_schema11: bool) -> pb.Empty:
input = pb.CloseCollectionIn(downgrade_to_schema11=downgrade_to_schema11)
output = pb.Empty()
output.ParseFromString(self._run_command2(54, input))
output.ParseFromString(self._run_command(54, input))
return output
def check_database(self) -> Sequence[str]:
input = pb.Empty()
output = pb.CheckDatabaseOut()
output.ParseFromString(self._run_command2(55, input))
output.ParseFromString(self._run_command(55, input))
return output.problems
def sync_media(self, hkey: str, endpoint: str) -> pb.Empty:
input = pb.SyncMediaIn(hkey=hkey, endpoint=endpoint)
output = pb.Empty()
output.ParseFromString(self._run_command2(56, input))
output.ParseFromString(self._run_command(56, input))
return output
def abort_media_sync(self) -> pb.Empty:
input = pb.Empty()
output = pb.Empty()
output.ParseFromString(self._run_command2(57, input))
output.ParseFromString(self._run_command(57, input))
return output
def before_upload(self) -> pb.Empty:
input = pb.Empty()
output = pb.Empty()
output.ParseFromString(self._run_command2(58, input))
output.ParseFromString(self._run_command(58, input))
return output
def translate_string(self, input: pb.TranslateStringIn) -> str:
output = pb.String()
output.ParseFromString(self._run_command(59, input))
return output.val
def format_timespan(
self, seconds: float, context: pb.FormatTimespanIn.Context
) -> str:
input = pb.FormatTimespanIn(seconds=seconds, context=context)
output = pb.String()
output.ParseFromString(self._run_command(60, input))
return output.val
def register_tags(
self, tags: str, preserve_usn: bool, usn: int, clear_first: bool
) -> bool:
input = pb.RegisterTagsIn(
tags=tags, preserve_usn=preserve_usn, usn=usn, clear_first=clear_first
)
output = pb.Bool()
output.ParseFromString(self._run_command(61, input))
return output.val
def all_tags(self) -> Sequence[pb.TagUsnTuple]:
input = pb.Empty()
output = pb.AllTagsOut()
output.ParseFromString(self._run_command(62, input))
return output.tags
def get_changed_tags(self, val: int) -> Sequence[str]:
input = pb.Int32(val=val)
output = pb.GetChangedTagsOut()
output.ParseFromString(self._run_command(63, input))
return output.tags
def get_config_json(self, val: str) -> bytes:
input = pb.String(val=val)
output = pb.Json()
output.ParseFromString(self._run_command(64, input))
return output.json
def set_config_json(self, key: str, val: bytes) -> pb.Empty:
input = pb.SetConfigJsonIn(key=key, val=val)
output = pb.Empty()
output.ParseFromString(self._run_command(65, input))
return output
def remove_config(self, val: str) -> pb.Empty:
input = pb.String(val=val)
output = pb.Empty()
output.ParseFromString(self._run_command(66, input))
return output
def set_all_config(self, json: bytes) -> pb.Empty:
input = pb.Json(json=json)
output = pb.Empty()
output.ParseFromString(self._run_command(67, input))
return output
def get_all_config(self) -> bytes:
input = pb.Empty()
output = pb.Json()
output.ParseFromString(self._run_command(68, input))
return output.json
def get_preferences(self) -> pb.CollectionSchedulingSettings:
input = pb.Empty()
output = pb.Preferences()
output.ParseFromString(self._run_command(69, input))
return output.sched
def set_preferences(self, sched: pb.CollectionSchedulingSettings) -> pb.Empty:
input = pb.Preferences(sched=sched)
output = pb.Empty()
output.ParseFromString(self._run_command(70, input))
return output
# @@AUTOGEN@@

View file

@ -1369,9 +1369,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
ivl_secs = self.nextIvl(card, ease)
if not ivl_secs:
return _("(end)")
s = self.col.backend.format_time_span(
ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS
)
s = self.col.format_timespan(ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS)
if ivl_secs < self.col.conf["collapseTime"]:
s = "<" + s
return s

View file

@ -52,9 +52,7 @@ class CardStats:
if next:
self.addLine(self.col.tr(TR.STATISTICS_DUE_DATE), next)
if c.queue == QUEUE_TYPE_REV:
self.addLine(
_("Interval"), self.col.backend.format_time_span(c.ivl * 86400)
)
self.addLine(_("Interval"), self.col.format_timespan(c.ivl * 86400))
self.addLine(_("Ease"), "%d%%" % (c.factor / 10.0))
self.addLine(_("Reviews"), "%d" % c.reps)
self.addLine(_("Lapses"), "%d" % c.lapses)
@ -86,9 +84,7 @@ class CardStats:
return time.strftime("%Y-%m-%d", time.localtime(tm))
def time(self, tm: float) -> str:
return self.col.backend.format_time_span(
tm, context=FormatTimeSpanContext.PRECISE
)
return self.col.format_timespan(tm, context=FormatTimeSpanContext.PRECISE)
# Collection stats
@ -645,12 +641,8 @@ group by day order by day)"""
),
)
i: List[str] = []
self._line(
i, _("Average interval"), self.col.backend.format_time_span(avg * 86400)
)
self._line(
i, _("Longest interval"), self.col.backend.format_time_span(max_ * 86400)
)
self._line(i, _("Average interval"), self.col.format_timespan(avg * 86400))
self._line(i, _("Longest interval"), self.col.format_timespan(max_ * 86400))
return txt + self._lineTbl(i)
def _ivls(self) -> Tuple[List[Any], int]:

View file

@ -19,6 +19,8 @@ from . import hooks
from .httpclient import HttpClient
# add-on compat
from .rsbackend import from_json_bytes, to_json_bytes
AnkiRequestsClient = HttpClient
@ -402,7 +404,7 @@ from notes where %s"""
##########################################################################
def getTags(self) -> List:
return self.col.backend.get_changed_tags(self.maxUsn)
return list(self.col.backend.get_changed_tags(self.maxUsn))
def mergeTags(self, tags) -> None:
self.col.tags.register(tags, usn=self.maxUsn)
@ -448,10 +450,10 @@ from notes where %s"""
##########################################################################
def getConf(self) -> Dict[str, Any]:
return self.col.backend.get_all_config()
return from_json_bytes(self.col.backend.get_all_config())
def mergeConf(self, conf: Dict[str, Any]) -> None:
self.col.backend.set_all_config(conf)
self.col.backend.set_all_config(to_json_bytes(conf))
# HTTP syncing tools

View file

@ -36,7 +36,16 @@ class TagManager:
def register(
self, tags: Collection[str], usn: Optional[int] = None, clear=False
) -> None:
self.col.backend.register_tags(" ".join(tags), usn, clear)
if usn is None:
preserve_usn = False
usn_ = 0
else:
usn_ = usn
preserve_usn = True
self.col.backend.register_tags(
tags=" ".join(tags), preserve_usn=preserve_usn, usn=usn_, clear_first=clear
)
def registerNotes(self, nids: Optional[List[int]] = None) -> None:
"Add any missing tags from notes to the tags list."

View file

@ -29,6 +29,9 @@ LABEL_OPTIONAL = 1
LABEL_REQUIRED = 2
LABEL_REPEATED = 3
# messages we don't want to unroll in codegen
SKIP_UNROLL_INPUT = {"TranslateString"}
def python_type(field):
type = python_type_inner(field)
@ -50,13 +53,20 @@ def python_type_inner(field):
elif type == TYPE_BYTES:
return "bytes"
elif type == TYPE_MESSAGE:
return "pb." + field.message_type.name
return fullname(field.message_type.full_name)
elif type == TYPE_ENUM:
return "pb." + field.enum_type.name
return fullname(field.enum_type.full_name)
else:
raise Exception(f"unknown type: {type}")
def fullname(fullname):
if "FluentString" in fullname:
return fullname.replace("backend_proto", "anki.fluent_pb2")
else:
return fullname.replace("backend_proto", "pb")
# get_deck_i_d -> get_deck_id etc
def fix_snakecase(name):
for fix in "a_v", "i_d":
@ -80,14 +90,18 @@ def get_input_assign(msg):
def render_method(method, idx):
input_name = method.input_type.name
if input_name.endswith("In") or len(method.input_type.fields) < 2:
if (
(input_name.endswith("In") or len(method.input_type.fields) < 2)
and not method.input_type.oneofs
and not method.name in SKIP_UNROLL_INPUT
):
input_args = get_input_args(method.input_type)
input_assign = get_input_assign(method.input_type)
input_assign_outer = (
f"input = pb.{method.input_type.name}({input_assign})\n "
f"input = {fullname(method.input_type.full_name)}({input_assign})\n "
)
else:
input_args = f"self, input: pb.{input_name}"
input_args = f"self, input: {fullname(method.input_type.full_name)}"
input_assign_outer = ""
name = fix_snakecase(stringcase.snakecase(method.name))
if len(method.output_type.fields) == 1:
@ -101,7 +115,7 @@ def render_method(method, idx):
return f"""\
def {name}({input_args}) -> {return_type}:
{input_assign_outer}output = pb.{method.output_type.name}()
output.ParseFromString(self._run_command2({idx+1}, input))
output.ParseFromString(self._run_command({idx+1}, input))
return output{single_field}
"""

View file

@ -323,7 +323,7 @@ class DataModel(QAbstractTableModel):
return _("(new)")
elif c.type == CARD_TYPE_LRN:
return _("(learning)")
return self.col.backend.format_time_span(c.ivl * 86400)
return self.col.format_timespan(c.ivl * 86400)
elif type == "cardEase":
if c.type == CARD_TYPE_NEW:
return _("(new)")
@ -1491,7 +1491,7 @@ border: 1px solid #000; padding: 3px; '>%s</div>"""
s += ("<td align=right>%s</td>" * 2) % (
"%d%%" % (factor / 10) if factor else "",
self.col.backend.format_time_span(taken),
self.col.format_timespan(taken),
) + "</tr>"
s += "</table>"
if cnt < self.card.reps:

View file

@ -40,7 +40,7 @@ def stripSounds(text) -> str:
def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99):
print("fmtTimeSpan() has become col.backend.format_time_span()")
return aqt.mw.col.backend.format_time_span(time)
return aqt.mw.col.format_timespan(time)
def install_pylib_legacy() -> None:

View file

@ -41,7 +41,7 @@ use crate::{
use fluent::FluentValue;
use futures::future::{AbortHandle, Abortable};
use log::error;
use pb::{backend_input::Value, BackendService};
use pb::BackendService;
use prost::Message;
use serde_json::Value as JsonValue;
use std::collections::{HashMap, HashSet};
@ -99,13 +99,6 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
}
}
// Convert an Anki error to a protobuf output.
impl std::convert::From<pb::BackendError> for pb::backend_output::Value {
fn from(err: pb::BackendError) -> Self {
pb::backend_output::Value::Error(err)
}
}
impl std::convert::From<NetworkErrorKind> for i32 {
fn from(e: NetworkErrorKind) -> Self {
use pb::network_error::NetworkErrorKind as V;
@ -947,6 +940,133 @@ impl BackendService for Backend {
fn before_upload(&mut self, _input: Empty) -> BackendResult<Empty> {
self.with_col(|col| col.before_upload().map(Into::into))
}
// i18n/messages
//-------------------------------------------------------------------
fn translate_string(&mut self, input: pb::TranslateStringIn) -> BackendResult<pb::String> {
let key = match pb::FluentString::from_i32(input.key) {
Some(key) => key,
None => return Ok("invalid key".to_string().into()),
};
let map = input
.args
.iter()
.map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v)))
.collect();
Ok(self.i18n.trn(key, map).into())
}
fn format_timespan(&mut self, input: pb::FormatTimespanIn) -> BackendResult<pb::String> {
let context = match pb::format_timespan_in::Context::from_i32(input.context) {
Some(context) => context,
None => return Ok("".to_string().into()),
};
Ok(match context {
pb::format_timespan_in::Context::Precise => time_span(input.seconds, &self.i18n, true),
pb::format_timespan_in::Context::Intervals => {
time_span(input.seconds, &self.i18n, false)
}
pb::format_timespan_in::Context::AnswerButtons => {
answer_button_time(input.seconds, &self.i18n)
}
}
.into())
}
// tags
//-------------------------------------------------------------------
fn all_tags(&mut self, _input: Empty) -> BackendResult<pb::AllTagsOut> {
let tags = self.with_col(|col| col.storage.all_tags())?;
let tags: Vec<_> = tags
.into_iter()
.map(|(tag, usn)| pb::TagUsnTuple { tag, usn: usn.0 })
.collect();
Ok(pb::AllTagsOut { tags })
}
fn register_tags(&mut self, input: pb::RegisterTagsIn) -> BackendResult<pb::Bool> {
self.with_col(|col| {
col.transact(None, |col| {
let usn = if input.preserve_usn {
Usn(input.usn)
} else {
col.usn()?
};
col.register_tags(&input.tags, usn, input.clear_first)
.map(|val| pb::Bool { val })
})
})
}
fn get_changed_tags(&mut self, input: pb::Int32) -> BackendResult<pb::GetChangedTagsOut> {
self.with_col(|col| {
col.transact(None, |col| {
Ok(pb::GetChangedTagsOut {
tags: col.storage.get_changed_tags(Usn(input.val))?,
})
})
})
}
// config/preferences
//-------------------------------------------------------------------
fn get_config_json(&mut self, input: pb::String) -> BackendResult<pb::Json> {
self.with_col(|col| {
let val: Option<JsonValue> = col.get_config_optional(input.val.as_str());
val.ok_or(AnkiError::NotFound)
.and_then(|v| serde_json::to_vec(&v).map_err(Into::into))
.map(Into::into)
})
}
fn set_config_json(&mut self, input: pb::SetConfigJsonIn) -> BackendResult<Empty> {
self.with_col(|col| {
col.transact(None, |col| {
// ensure it's a well-formed object
let val: JsonValue = serde_json::from_slice(&input.val)?;
col.set_config(input.key.as_str(), &val)
})
})
.map(Into::into)
}
fn remove_config(&mut self, input: pb::String) -> BackendResult<Empty> {
self.with_col(|col| col.transact(None, |col| col.remove_config(input.val.as_str())))
.map(Into::into)
}
fn set_all_config(&mut self, input: pb::Json) -> BackendResult<Empty> {
let val: HashMap<String, JsonValue> = serde_json::from_slice(&input.json)?;
self.with_col(|col| {
col.transact(None, |col| {
col.storage
.set_all_config(val, col.usn()?, TimestampSecs::now())
})
})
.map(Into::into)
}
fn get_all_config(&mut self, _input: Empty) -> BackendResult<pb::Json> {
self.with_col(|col| {
let conf = col.storage.get_all_config()?;
serde_json::to_vec(&conf).map_err(Into::into)
})
.map(Into::into)
}
fn get_preferences(&mut self, _input: Empty) -> BackendResult<pb::Preferences> {
self.with_col(|col| col.get_preferences())
}
fn set_preferences(&mut self, input: pb::Preferences) -> BackendResult<Empty> {
self.with_col(|col| col.transact(None, |col| col.set_preferences(input)))
.map(Into::into)
}
}
impl Backend {
@ -964,30 +1084,7 @@ impl Backend {
&self.i18n
}
/// Decode a request, process it, and return the encoded result.
pub fn run_command_bytes(&mut self, req: &[u8]) -> Vec<u8> {
let mut buf = vec![];
let req = match pb::BackendInput::decode(req) {
Ok(req) => req,
Err(_e) => {
// unable to decode
let err = AnkiError::invalid_input("couldn't decode backend request");
let oerr = anki_error_to_proto_error(err, &self.i18n);
let output = pb::BackendOutput {
value: Some(oerr.into()),
};
output.encode(&mut buf).expect("encode failed");
return buf;
}
};
let resp = self.run_command(req);
resp.encode(&mut buf).expect("encode failed");
buf
}
pub fn run_command_bytes2(
pub fn run_command_bytes(
&mut self,
method: u32,
input: &[u8],
@ -1016,53 +1113,6 @@ impl Backend {
)
}
fn run_command(&mut self, input: pb::BackendInput) -> pb::BackendOutput {
let oval = if let Some(ival) = input.value {
match self.run_command_inner(ival) {
Ok(output) => output,
Err(err) => anki_error_to_proto_error(err, &self.i18n).into(),
}
} else {
anki_error_to_proto_error(
AnkiError::invalid_input("unrecognized backend input value"),
&self.i18n,
)
.into()
};
pb::BackendOutput { value: Some(oval) }
}
fn run_command_inner(
&mut self,
ival: pb::backend_input::Value,
) -> Result<pb::backend_output::Value> {
use pb::backend_output::Value as OValue;
Ok(match ival {
Value::TranslateString(input) => OValue::TranslateString(self.translate_string(input)),
Value::FormatTimeSpan(input) => OValue::FormatTimeSpan(self.format_time_span(input)),
Value::AllTags(_) => OValue::AllTags(self.all_tags()?),
Value::RegisterTags(input) => OValue::RegisterTags(self.register_tags(input)?),
Value::GetChangedTags(usn) => OValue::GetChangedTags(self.get_changed_tags(usn)?),
Value::GetConfigJson(key) => OValue::GetConfigJson(self.get_config_json(&key)?),
Value::SetConfigJson(input) => OValue::SetConfigJson({
self.set_config_json(input)?;
pb::Empty {}
}),
Value::SetAllConfig(input) => OValue::SetConfigJson({
self.set_all_config(&input)?;
pb::Empty {}
}),
Value::GetAllConfig(_) => OValue::GetAllConfig(self.get_all_config()?),
Value::GetPreferences(_) => OValue::GetPreferences(self.get_preferences()?),
Value::SetPreferences(prefs) => OValue::SetPreferences({
self.set_preferences(prefs)?;
pb::Empty {}
}),
})
}
fn fire_progress_callback(&self, progress: Progress) -> bool {
if let Some(cb) = &self.progress_callback {
let bytes = progress_to_proto_bytes(progress, &self.i18n);
@ -1105,126 +1155,9 @@ impl Backend {
ret
}
fn translate_string(&self, input: pb::TranslateStringIn) -> String {
let key = match pb::FluentString::from_i32(input.key) {
Some(key) => key,
None => return "invalid key".to_string(),
};
let map = input
.args
.iter()
.map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v)))
.collect();
self.i18n.trn(key, map)
}
fn format_time_span(&self, input: pb::FormatTimeSpanIn) -> String {
let context = match pb::format_time_span_in::Context::from_i32(input.context) {
Some(context) => context,
None => return "".to_string(),
};
match context {
pb::format_time_span_in::Context::Precise => time_span(input.seconds, &self.i18n, true),
pb::format_time_span_in::Context::Intervals => {
time_span(input.seconds, &self.i18n, false)
}
pb::format_time_span_in::Context::AnswerButtons => {
answer_button_time(input.seconds, &self.i18n)
}
}
}
pub fn db_command(&self, input: &[u8]) -> Result<String> {
self.with_col(|col| db_command_bytes(&col.storage, input))
}
fn all_tags(&self) -> Result<pb::AllTagsOut> {
let tags = self.with_col(|col| col.storage.all_tags())?;
let tags: Vec<_> = tags
.into_iter()
.map(|(tag, usn)| pb::TagUsnTuple { tag, usn: usn.0 })
.collect();
Ok(pb::AllTagsOut { tags })
}
fn register_tags(&self, input: pb::RegisterTagsIn) -> Result<bool> {
self.with_col(|col| {
col.transact(None, |col| {
let usn = if input.preserve_usn {
Usn(input.usn)
} else {
col.usn()?
};
col.register_tags(&input.tags, usn, input.clear_first)
})
})
}
fn get_changed_tags(&self, usn: i32) -> Result<pb::GetChangedTagsOut> {
self.with_col(|col| {
col.transact(None, |col| {
Ok(pb::GetChangedTagsOut {
tags: col.storage.get_changed_tags(Usn(usn))?,
})
})
})
}
fn get_config_json(&self, key: &str) -> Result<Vec<u8>> {
self.with_col(|col| {
let val: Option<JsonValue> = col.get_config_optional(key);
match val {
None => Ok(vec![]),
Some(val) => Ok(serde_json::to_vec(&val)?),
}
})
}
fn set_config_json(&self, input: pb::SetConfigJson) -> Result<()> {
self.with_col(|col| {
col.transact(None, |col| {
if let Some(op) = input.op {
match op {
pb::set_config_json::Op::Val(val) => {
// ensure it's a well-formed object
let val: JsonValue = serde_json::from_slice(&val)?;
col.set_config(input.key.as_str(), &val)
}
pb::set_config_json::Op::Remove(_) => col.remove_config(input.key.as_str()),
}
} else {
Err(AnkiError::invalid_input("no op received"))
}
})
})
}
fn set_all_config(&self, conf: &[u8]) -> Result<()> {
let val: HashMap<String, JsonValue> = serde_json::from_slice(conf)?;
self.with_col(|col| {
col.transact(None, |col| {
col.storage
.set_all_config(val, col.usn()?, TimestampSecs::now())
})
})
}
fn get_all_config(&self) -> Result<Vec<u8>> {
self.with_col(|col| {
let conf = col.storage.get_all_config()?;
serde_json::to_vec(&conf).map_err(Into::into)
})
}
fn get_preferences(&self) -> Result<pb::Preferences> {
self.with_col(|col| col.get_preferences())
}
fn set_preferences(&self, prefs: pb::Preferences) -> Result<()> {
self.with_col(|col| col.transact(None, |col| col.set_preferences(prefs)))
}
}
fn to_nids(ids: Vec<i64>) -> Vec<NoteID> {

View file

@ -93,6 +93,18 @@ fn want_release_gil(method: u32) -> bool {
BackendMethod::CloseCollection => true,
BackendMethod::AbortMediaSync => true,
BackendMethod::BeforeUpload => true,
BackendMethod::TranslateString => false,
BackendMethod::FormatTimespan => false,
BackendMethod::RegisterTags => true,
BackendMethod::AllTags => true,
BackendMethod::GetChangedTags => true,
BackendMethod::GetConfigJson => true,
BackendMethod::SetConfigJson => true,
BackendMethod::RemoveConfig => true,
BackendMethod::SetAllConfig => true,
BackendMethod::GetAllConfig => true,
BackendMethod::GetPreferences => true,
BackendMethod::SetPreferences => true,
}
} else {
false
@ -101,23 +113,12 @@ fn want_release_gil(method: u32) -> bool {
#[pymethods]
impl Backend {
fn command(&mut self, py: Python, input: &PyBytes, release_gil: bool) -> PyObject {
let in_bytes = input.as_bytes();
let out_bytes = if release_gil {
py.allow_threads(move || self.backend.run_command_bytes(in_bytes))
} else {
self.backend.run_command_bytes(in_bytes)
};
let out_obj = PyBytes::new(py, &out_bytes);
out_obj.into()
}
fn command2(&mut self, py: Python, method: u32, input: &PyBytes) -> PyResult<PyObject> {
fn command(&mut self, py: Python, method: u32, input: &PyBytes) -> PyResult<PyObject> {
let in_bytes = input.as_bytes();
if want_release_gil(method) {
py.allow_threads(move || self.backend.run_command_bytes2(method, in_bytes))
py.allow_threads(move || self.backend.run_command_bytes(method, in_bytes))
} else {
self.backend.run_command_bytes2(method, in_bytes)
self.backend.run_command_bytes(method, in_bytes)
}
.map(|out_bytes| {
let out_obj = PyBytes::new(py, &out_bytes);