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 SyncMedia (SyncMediaIn) returns (Empty);
rpc AbortMediaSync (Empty) returns (Empty); rpc AbortMediaSync (Empty) returns (Empty);
rpc BeforeUpload (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 // Protobuf stored in .anki2 files
@ -398,49 +419,9 @@ message I18nBackendInit {
string locale_folder_path = 5; 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 { message BackendError {
// localized error description suitable for displaying to the user // localized error description suitable for displaying to the user
string localized = 1; string localized = 1;
@ -504,6 +485,10 @@ message MediaSyncUploadProgress {
uint32 deletions = 2; uint32 deletions = 2;
} }
// Messages
///////////////////////////////////////////////////////////
message SchedTimingTodayOut { message SchedTimingTodayOut {
uint32 days_elapsed = 1; uint32 days_elapsed = 1;
int64 next_day_at = 2; int64 next_day_at = 2;
@ -632,7 +617,7 @@ message TranslateArgValue {
} }
} }
message FormatTimeSpanIn { message FormatTimespanIn {
enum Context { enum Context {
PRECISE = 0; PRECISE = 0;
ANSWER_BUTTONS = 1; ANSWER_BUTTONS = 1;
@ -736,12 +721,9 @@ message GetChangedTagsOut {
repeated string tags = 1; repeated string tags = 1;
} }
message SetConfigJson { message SetConfigJsonIn {
string key = 1; string key = 1;
oneof op { bytes val = 2;
bytes val = 2;
Empty remove = 3;
}
} }
enum StockNoteType { enum StockNoteType {

View file

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

View file

@ -23,6 +23,7 @@ import weakref
from typing import Any from typing import Any
import anki import anki
from anki.rsbackend import NotFoundError, from_json_bytes, to_json_bytes
class ConfigManager: class ConfigManager:
@ -30,10 +31,13 @@ class ConfigManager:
self.col = col.weakref() self.col = col.weakref()
def get_immutable(self, key: str) -> Any: 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: 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: def remove(self, key: str) -> None:
self.col.backend.remove_config(key) self.col.backend.remove_config(key)

View file

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

View file

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

View file

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

View file

@ -36,7 +36,16 @@ class TagManager:
def register( def register(
self, tags: Collection[str], usn: Optional[int] = None, clear=False self, tags: Collection[str], usn: Optional[int] = None, clear=False
) -> None: ) -> 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: def registerNotes(self, nids: Optional[List[int]] = None) -> None:
"Add any missing tags from notes to the tags list." "Add any missing tags from notes to the tags list."

View file

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

View file

@ -323,7 +323,7 @@ class DataModel(QAbstractTableModel):
return _("(new)") return _("(new)")
elif c.type == CARD_TYPE_LRN: elif c.type == CARD_TYPE_LRN:
return _("(learning)") return _("(learning)")
return self.col.backend.format_time_span(c.ivl * 86400) return self.col.format_timespan(c.ivl * 86400)
elif type == "cardEase": elif type == "cardEase":
if c.type == CARD_TYPE_NEW: if c.type == CARD_TYPE_NEW:
return _("(new)") return _("(new)")
@ -1491,7 +1491,7 @@ border: 1px solid #000; padding: 3px; '>%s</div>"""
s += ("<td align=right>%s</td>" * 2) % ( s += ("<td align=right>%s</td>" * 2) % (
"%d%%" % (factor / 10) if factor else "", "%d%%" % (factor / 10) if factor else "",
self.col.backend.format_time_span(taken), self.col.format_timespan(taken),
) + "</tr>" ) + "</tr>"
s += "</table>" s += "</table>"
if cnt < self.card.reps: 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): def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99):
print("fmtTimeSpan() has become col.backend.format_time_span()") 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: def install_pylib_legacy() -> None:

View file

@ -41,7 +41,7 @@ use crate::{
use fluent::FluentValue; use fluent::FluentValue;
use futures::future::{AbortHandle, Abortable}; use futures::future::{AbortHandle, Abortable};
use log::error; use log::error;
use pb::{backend_input::Value, BackendService}; use pb::BackendService;
use prost::Message; use prost::Message;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use std::collections::{HashMap, HashSet}; 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 { impl std::convert::From<NetworkErrorKind> for i32 {
fn from(e: NetworkErrorKind) -> Self { fn from(e: NetworkErrorKind) -> Self {
use pb::network_error::NetworkErrorKind as V; use pb::network_error::NetworkErrorKind as V;
@ -947,6 +940,133 @@ impl BackendService for Backend {
fn before_upload(&mut self, _input: Empty) -> BackendResult<Empty> { fn before_upload(&mut self, _input: Empty) -> BackendResult<Empty> {
self.with_col(|col| col.before_upload().map(Into::into)) 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 { impl Backend {
@ -964,30 +1084,7 @@ impl Backend {
&self.i18n &self.i18n
} }
/// Decode a request, process it, and return the encoded result. pub fn run_command_bytes(
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(
&mut self, &mut self,
method: u32, method: u32,
input: &[u8], 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 { fn fire_progress_callback(&self, progress: Progress) -> bool {
if let Some(cb) = &self.progress_callback { if let Some(cb) = &self.progress_callback {
let bytes = progress_to_proto_bytes(progress, &self.i18n); let bytes = progress_to_proto_bytes(progress, &self.i18n);
@ -1105,126 +1155,9 @@ impl Backend {
ret 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> { pub fn db_command(&self, input: &[u8]) -> Result<String> {
self.with_col(|col| db_command_bytes(&col.storage, input)) 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> { 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::CloseCollection => true,
BackendMethod::AbortMediaSync => true, BackendMethod::AbortMediaSync => true,
BackendMethod::BeforeUpload => 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 { } else {
false false
@ -101,23 +113,12 @@ fn want_release_gil(method: u32) -> bool {
#[pymethods] #[pymethods]
impl Backend { impl Backend {
fn command(&mut self, py: Python, input: &PyBytes, release_gil: bool) -> PyObject { fn command(&mut self, py: Python, method: u32, input: &PyBytes) -> PyResult<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> {
let in_bytes = input.as_bytes(); let in_bytes = input.as_bytes();
if want_release_gil(method) { 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 { } else {
self.backend.run_command_bytes2(method, in_bytes) self.backend.run_command_bytes(method, in_bytes)
} }
.map(|out_bytes| { .map(|out_bytes| {
let out_obj = PyBytes::new(py, &out_bytes); let out_obj = PyBytes::new(py, &out_bytes);