mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
start reworking protobuf handling
Will allow us to cut down on boilerplate by automatically generating code from RPC service definitions
This commit is contained in:
parent
58a243aa6c
commit
9c20d9a02b
9 changed files with 363 additions and 105 deletions
1
proto/.gitignore
vendored
1
proto/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
fluent.proto
|
fluent.proto
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@ message OptionalUInt32 {
|
||||||
uint32 val = 1;
|
uint32 val = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service BackendService {
|
||||||
|
rpc RenderExistingCard (RenderExistingCardIn) returns (RenderCardOut);
|
||||||
|
rpc RenderUncommittedCard (RenderUncommittedCardIn) returns (RenderCardOut);
|
||||||
|
}
|
||||||
|
|
||||||
// Protobuf stored in .anki2 files
|
// Protobuf stored in .anki2 files
|
||||||
// These should be moved to a separate file in the future
|
// These should be moved to a separate file in the future
|
||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -39,12 +39,14 @@ all: check
|
||||||
python -m pip install -r requirements.dev
|
python -m pip install -r requirements.dev
|
||||||
@touch $@
|
@touch $@
|
||||||
|
|
||||||
PROTODEPS := $(wildcard ../proto/*.proto)
|
PROTODEPS := ../proto/backend.proto ../proto/fluent.proto
|
||||||
|
|
||||||
.build/py-proto: .build/dev-deps $(PROTODEPS)
|
.build/py-proto: .build/dev-deps $(PROTODEPS)
|
||||||
protoc --proto_path=../proto --python_out=anki --mypy_out=anki $(PROTODEPS)
|
protoc --proto_path=../proto --python_out=anki --mypy_out=anki $(PROTODEPS)
|
||||||
perl -i'' -pe 's/from fluent_pb2/from anki.fluent_pb2/' anki/backend_pb2.pyi
|
perl -i'' -pe 's/from fluent_pb2/from anki.fluent_pb2/' anki/backend_pb2.pyi
|
||||||
perl -i'' -pe 's/import fluent_pb2/import anki.fluent_pb2/' anki/backend_pb2.py
|
perl -i'' -pe 's/import fluent_pb2/import anki.fluent_pb2/' anki/backend_pb2.py
|
||||||
|
python tools/genbackend.py
|
||||||
|
python -m black anki/rsbackend.py
|
||||||
@touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/hooks: tools/genhooks.py tools/hookslib.py
|
.build/hooks: tools/genhooks.py tools/hookslib.py
|
||||||
|
@ -52,7 +54,7 @@ PROTODEPS := $(wildcard ../proto/*.proto)
|
||||||
python -m black anki/hooks.py
|
python -m black anki/hooks.py
|
||||||
@touch $@
|
@touch $@
|
||||||
|
|
||||||
BUILD_STEPS := .build/vernum .build/run-deps .build/dev-deps .build/py-proto anki/buildinfo.py .build/hooks
|
BUILD_STEPS := .build/vernum .build/run-deps .build/dev-deps anki/buildinfo.py .build/py-proto .build/hooks
|
||||||
|
|
||||||
# Checking
|
# Checking
|
||||||
######################
|
######################
|
||||||
|
|
|
@ -65,6 +65,10 @@ except:
|
||||||
loads = json.loads
|
loads = json.loads
|
||||||
|
|
||||||
|
|
||||||
|
to_json_bytes = orjson.dumps
|
||||||
|
from_json_bytes = orjson.loads
|
||||||
|
|
||||||
|
|
||||||
class Interrupted(Exception):
|
class Interrupted(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -161,22 +165,6 @@ def av_tag_to_native(tag: pb.AVTag) -> AVTag:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TemplateReplacement:
|
|
||||||
field_name: str
|
|
||||||
current_text: str
|
|
||||||
filters: List[str]
|
|
||||||
|
|
||||||
|
|
||||||
TemplateReplacementList = List[Union[str, TemplateReplacement]]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PartiallyRenderedCard:
|
|
||||||
qnodes: TemplateReplacementList
|
|
||||||
anodes: TemplateReplacementList
|
|
||||||
|
|
||||||
|
|
||||||
MediaSyncProgress = pb.MediaSyncProgress
|
MediaSyncProgress = pb.MediaSyncProgress
|
||||||
|
|
||||||
MediaCheckOutput = pb.MediaCheckOut
|
MediaCheckOutput = pb.MediaCheckOut
|
||||||
|
@ -207,24 +195,6 @@ class Progress:
|
||||||
val: Union[MediaSyncProgress, str]
|
val: Union[MediaSyncProgress, str]
|
||||||
|
|
||||||
|
|
||||||
def proto_replacement_list_to_native(
|
|
||||||
nodes: List[pb.RenderedTemplateNode],
|
|
||||||
) -> TemplateReplacementList:
|
|
||||||
results: TemplateReplacementList = []
|
|
||||||
for node in nodes:
|
|
||||||
if node.WhichOneof("value") == "text":
|
|
||||||
results.append(node.text)
|
|
||||||
else:
|
|
||||||
results.append(
|
|
||||||
TemplateReplacement(
|
|
||||||
field_name=node.replacement.field_name,
|
|
||||||
current_text=node.replacement.current_text,
|
|
||||||
filters=list(node.replacement.filters),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def proto_progress_to_native(progress: pb.Progress) -> Progress:
|
def proto_progress_to_native(progress: pb.Progress) -> Progress:
|
||||||
kind = progress.WhichOneof("value")
|
kind = progress.WhichOneof("value")
|
||||||
if kind == "media_sync":
|
if kind == "media_sync":
|
||||||
|
@ -302,40 +272,6 @@ class RustBackend:
|
||||||
pb.BackendInput(sched_timing_today=pb.Empty())
|
pb.BackendInput(sched_timing_today=pb.Empty())
|
||||||
).sched_timing_today
|
).sched_timing_today
|
||||||
|
|
||||||
def render_existing_card(self, cid: int, browser: bool) -> PartiallyRenderedCard:
|
|
||||||
out = self._run_command(
|
|
||||||
pb.BackendInput(
|
|
||||||
render_existing_card=pb.RenderExistingCardIn(
|
|
||||||
card_id=cid, browser=browser,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).render_existing_card
|
|
||||||
|
|
||||||
qnodes = proto_replacement_list_to_native(out.question_nodes) # type: ignore
|
|
||||||
anodes = proto_replacement_list_to_native(out.answer_nodes) # type: ignore
|
|
||||||
|
|
||||||
return PartiallyRenderedCard(qnodes, anodes)
|
|
||||||
|
|
||||||
def render_uncommitted_card(
|
|
||||||
self, note: BackendNote, card_ord: int, template: Dict, fill_empty: bool
|
|
||||||
) -> PartiallyRenderedCard:
|
|
||||||
template_json = orjson.dumps(template)
|
|
||||||
out = self._run_command(
|
|
||||||
pb.BackendInput(
|
|
||||||
render_uncommitted_card=pb.RenderUncommittedCardIn(
|
|
||||||
note=note,
|
|
||||||
template=template_json,
|
|
||||||
card_ord=card_ord,
|
|
||||||
fill_empty=fill_empty,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).render_uncommitted_card
|
|
||||||
|
|
||||||
qnodes = proto_replacement_list_to_native(out.question_nodes) # type: ignore
|
|
||||||
anodes = proto_replacement_list_to_native(out.answer_nodes) # type: ignore
|
|
||||||
|
|
||||||
return PartiallyRenderedCard(qnodes, anodes)
|
|
||||||
|
|
||||||
def local_minutes_west(self, stamp: int) -> int:
|
def local_minutes_west(self, stamp: int) -> int:
|
||||||
return self._run_command(
|
return self._run_command(
|
||||||
pb.BackendInput(local_minutes_west=stamp)
|
pb.BackendInput(local_minutes_west=stamp)
|
||||||
|
@ -830,6 +766,39 @@ class RustBackend:
|
||||||
).cloze_numbers_in_note.numbers
|
).cloze_numbers_in_note.numbers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _run_command2(self, method: int, input: Any) -> bytes:
|
||||||
|
input_bytes = input.SerializeToString()
|
||||||
|
try:
|
||||||
|
return self._backend.command2(method, input_bytes)
|
||||||
|
except Exception as e:
|
||||||
|
err_bytes = bytes(e.args[0])
|
||||||
|
err = pb.BackendError()
|
||||||
|
err.ParseFromString(err_bytes)
|
||||||
|
raise proto_exception_to_native(err)
|
||||||
|
|
||||||
|
# The code in this section is automatically generated - any edits you make
|
||||||
|
# will be lost.
|
||||||
|
|
||||||
|
# @@AUTOGEN@@
|
||||||
|
|
||||||
|
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(1, input))
|
||||||
|
return output
|
||||||
|
|
||||||
|
def render_uncommitted_card(
|
||||||
|
self, note: pb.Note, card_ord: int, template: bytes, fill_empty: bool
|
||||||
|
) -> pb.RenderCardOut:
|
||||||
|
input = pb.RenderUncommittedCardIn(
|
||||||
|
note=note, card_ord=card_ord, template=template, fill_empty=fill_empty
|
||||||
|
)
|
||||||
|
output = pb.RenderCardOut()
|
||||||
|
output.ParseFromString(self._run_command2(2, input))
|
||||||
|
return output
|
||||||
|
|
||||||
|
# @@AUTOGEN@@
|
||||||
|
|
||||||
|
|
||||||
def translate_string_in(
|
def translate_string_in(
|
||||||
key: TR, **kwargs: Union[str, int, float]
|
key: TR, **kwargs: Union[str, int, float]
|
||||||
|
|
|
@ -29,7 +29,7 @@ template_legacy.py file, using the legacy addHook() system.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
|
@ -37,7 +37,7 @@ from anki.cards import Card
|
||||||
from anki.decks import DeckManager
|
from anki.decks import DeckManager
|
||||||
from anki.models import NoteType
|
from anki.models import NoteType
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.rsbackend import PartiallyRenderedCard, TemplateReplacementList
|
from anki.rsbackend import pb, to_json_bytes
|
||||||
from anki.sound import AVTag
|
from anki.sound import AVTag
|
||||||
|
|
||||||
CARD_BLANK_HELP = (
|
CARD_BLANK_HELP = (
|
||||||
|
@ -45,6 +45,47 @@ CARD_BLANK_HELP = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TemplateReplacement:
|
||||||
|
field_name: str
|
||||||
|
current_text: str
|
||||||
|
filters: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
TemplateReplacementList = List[Union[str, TemplateReplacement]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PartiallyRenderedCard:
|
||||||
|
qnodes: TemplateReplacementList
|
||||||
|
anodes: TemplateReplacementList
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_proto(cls, out: pb.RenderCardOut) -> PartiallyRenderedCard:
|
||||||
|
qnodes = cls.nodes_from_proto(out.question_nodes)
|
||||||
|
anodes = cls.nodes_from_proto(out.answer_nodes)
|
||||||
|
|
||||||
|
return PartiallyRenderedCard(qnodes, anodes)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def nodes_from_proto(
|
||||||
|
nodes: Sequence[pb.RenderedTemplateNode],
|
||||||
|
) -> TemplateReplacementList:
|
||||||
|
results: TemplateReplacementList = []
|
||||||
|
for node in nodes:
|
||||||
|
if node.WhichOneof("value") == "text":
|
||||||
|
results.append(node.text)
|
||||||
|
else:
|
||||||
|
results.append(
|
||||||
|
TemplateReplacement(
|
||||||
|
field_name=node.replacement.field_name,
|
||||||
|
current_text=node.replacement.current_text,
|
||||||
|
filters=list(node.replacement.filters),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
class TemplateRenderContext:
|
class TemplateRenderContext:
|
||||||
"""Holds information for the duration of one card render.
|
"""Holds information for the duration of one card render.
|
||||||
|
|
||||||
|
@ -177,15 +218,16 @@ class TemplateRenderContext:
|
||||||
def _partially_render(self) -> PartiallyRenderedCard:
|
def _partially_render(self) -> PartiallyRenderedCard:
|
||||||
if self._template:
|
if self._template:
|
||||||
# card layout screen
|
# card layout screen
|
||||||
return self._col.backend.render_uncommitted_card(
|
out = self._col.backend.render_uncommitted_card(
|
||||||
self._note.to_backend_note(),
|
self._note.to_backend_note(),
|
||||||
self._card.ord,
|
self._card.ord,
|
||||||
self._template,
|
to_json_bytes(self._template),
|
||||||
self._fill_empty,
|
self._fill_empty,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# existing card (eg study mode)
|
# existing card (eg study mode)
|
||||||
return self._col.backend.render_existing_card(self._card.id, self._browser)
|
out = self._col.backend.render_existing_card(self._card.id, self._browser)
|
||||||
|
return PartiallyRenderedCard.from_proto(out)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
107
pylib/tools/genbackend.py
Executable file
107
pylib/tools/genbackend.py
Executable file
|
@ -0,0 +1,107 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import re
|
||||||
|
|
||||||
|
from anki import backend_pb2 as pb
|
||||||
|
import stringcase
|
||||||
|
|
||||||
|
TYPE_DOUBLE = 1
|
||||||
|
TYPE_FLOAT = 2
|
||||||
|
TYPE_INT64 = 3
|
||||||
|
TYPE_UINT64 = 4
|
||||||
|
TYPE_INT32 = 5
|
||||||
|
TYPE_FIXED64 = 6
|
||||||
|
TYPE_FIXED32 = 7
|
||||||
|
TYPE_BOOL = 8
|
||||||
|
TYPE_STRING = 9
|
||||||
|
TYPE_GROUP = 10
|
||||||
|
TYPE_MESSAGE = 11
|
||||||
|
TYPE_BYTES = 12
|
||||||
|
TYPE_UINT32 = 13
|
||||||
|
TYPE_ENUM = 14
|
||||||
|
TYPE_SFIXED32 = 15
|
||||||
|
TYPE_SFIXED64 = 16
|
||||||
|
TYPE_SINT32 = 17
|
||||||
|
TYPE_SINT64 = 18
|
||||||
|
|
||||||
|
LABEL_OPTIONAL = 1
|
||||||
|
LABEL_REQUIRED = 2
|
||||||
|
LABEL_REPEATED = 3
|
||||||
|
|
||||||
|
|
||||||
|
def python_type(field):
|
||||||
|
type = python_type_inner(field)
|
||||||
|
if field.label == LABEL_REPEATED:
|
||||||
|
type = f"List[{type}]"
|
||||||
|
return type
|
||||||
|
|
||||||
|
|
||||||
|
def python_type_inner(field):
|
||||||
|
type = field.type
|
||||||
|
if type == TYPE_BOOL:
|
||||||
|
return "bool"
|
||||||
|
elif type in (1, 2):
|
||||||
|
return "float"
|
||||||
|
elif type in (3, 4, 5, 6, 7, 13, 15, 16, 17, 18):
|
||||||
|
return "int"
|
||||||
|
elif type == TYPE_STRING:
|
||||||
|
return "str"
|
||||||
|
elif type == TYPE_BYTES:
|
||||||
|
return "bytes"
|
||||||
|
elif type == 11:
|
||||||
|
return "pb." + field.message_type.name
|
||||||
|
else:
|
||||||
|
raise Exception(f"unknown type: {type}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_input_args(msg):
|
||||||
|
fields = sorted(msg.fields, key=lambda x: x.number)
|
||||||
|
return ", ".join(["self"] + [f"{f.name}: {python_type(f)}" for f in fields])
|
||||||
|
|
||||||
|
|
||||||
|
def get_input_assign(msg):
|
||||||
|
fields = sorted(msg.fields, key=lambda x: x.number)
|
||||||
|
return ", ".join(f"{f.name}={f.name}" for f in fields)
|
||||||
|
|
||||||
|
|
||||||
|
def render_method(method, idx):
|
||||||
|
input_args = get_input_args(method.input_type)
|
||||||
|
input_assign = get_input_assign(method.input_type)
|
||||||
|
name = stringcase.snakecase(method.name)
|
||||||
|
if len(method.output_type.fields) == 1:
|
||||||
|
# unwrap single return arg
|
||||||
|
f = method.output_type.fields[0]
|
||||||
|
single_field = f".{f.name}"
|
||||||
|
return_type = python_type(f)
|
||||||
|
else:
|
||||||
|
single_field = ""
|
||||||
|
return_type = f"pb.{method.output_type.name}"
|
||||||
|
return f"""\
|
||||||
|
def {name}({input_args}) -> {return_type}:
|
||||||
|
input = pb.{method.input_type.name}({input_assign})
|
||||||
|
output = pb.{method.output_type.name}()
|
||||||
|
output.ParseFromString(self._run_command2({idx+1}, input))
|
||||||
|
return output{single_field}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for idx, method in enumerate(pb._BACKENDSERVICE.methods):
|
||||||
|
out.append(render_method(method, idx))
|
||||||
|
|
||||||
|
out = "\n".join(out)
|
||||||
|
|
||||||
|
path = "anki/rsbackend.py"
|
||||||
|
|
||||||
|
with open(path) as file:
|
||||||
|
orig = file.read()
|
||||||
|
|
||||||
|
new = re.sub(
|
||||||
|
"(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n",
|
||||||
|
f"# @@AUTOGEN@@\n\n{out}\n # @@AUTOGEN@@\n",
|
||||||
|
orig,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(path, "wb") as file:
|
||||||
|
file.write(new.encode("utf8"))
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::fmt::Write;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
@ -91,6 +92,82 @@ const FLUENT_KEYS: &[&str] = &[
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CustomGenerator {}
|
||||||
|
|
||||||
|
fn write_method_enum(buf: &mut String, service: &prost_build::Service) {
|
||||||
|
buf.push_str(
|
||||||
|
r#"
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
#[derive(PartialEq,TryFromPrimitive)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum BackendMethod {
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
for (idx, method) in service.methods.iter().enumerate() {
|
||||||
|
write!(buf, " {} = {},\n", method.proto_name, idx + 1).unwrap();
|
||||||
|
}
|
||||||
|
buf.push_str("}\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_method_trait(buf: &mut String, service: &prost_build::Service) {
|
||||||
|
buf.push_str(
|
||||||
|
r#"
|
||||||
|
use prost::Message;
|
||||||
|
pub type BackendResult<T> = std::result::Result<T, crate::err::AnkiError>;
|
||||||
|
pub trait BackendService {
|
||||||
|
fn run_command_bytes2_inner(&mut self, method: u32, input: &[u8]) -> std::result::Result<Vec<u8>, crate::err::AnkiError> {
|
||||||
|
match method {
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (idx, method) in service.methods.iter().enumerate() {
|
||||||
|
write!(
|
||||||
|
buf,
|
||||||
|
concat!(" ",
|
||||||
|
"{idx} => {{ let input = {input_type}::decode(input)?;\n",
|
||||||
|
"let output = self.{rust_method}(input)?;\n",
|
||||||
|
"let mut out_bytes = Vec::new(); output.encode(&mut out_bytes)?; Ok(out_bytes) }}, "),
|
||||||
|
idx = idx + 1,
|
||||||
|
input_type = method.input_type,
|
||||||
|
rust_method = method.name
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
buf.push_str(
|
||||||
|
r#"
|
||||||
|
_ => Err(crate::err::AnkiError::invalid_input("invalid command")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
for method in &service.methods {
|
||||||
|
write!(
|
||||||
|
buf,
|
||||||
|
concat!(
|
||||||
|
" fn {method_name}(&mut self, input: {input_type}) -> ",
|
||||||
|
"BackendResult<{output_type}>;\n"
|
||||||
|
),
|
||||||
|
method_name = method.name,
|
||||||
|
input_type = method.input_type,
|
||||||
|
output_type = method.output_type
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
buf.push_str("}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl prost_build::ServiceGenerator for CustomGenerator {
|
||||||
|
fn generate(&mut self, service: prost_build::Service, buf: &mut String) {
|
||||||
|
write_method_enum(buf, &service);
|
||||||
|
write_method_trait(buf, &service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn service_generator() -> Box<dyn prost_build::ServiceGenerator> {
|
||||||
|
Box::new(CustomGenerator {})
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
// write template.ftl
|
// write template.ftl
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
|
@ -126,7 +203,12 @@ fn main() -> std::io::Result<()> {
|
||||||
// we avoid default OUT_DIR for now, as it breaks code completion
|
// we avoid default OUT_DIR for now, as it breaks code completion
|
||||||
std::env::set_var("OUT_DIR", "src");
|
std::env::set_var("OUT_DIR", "src");
|
||||||
println!("cargo:rerun-if-changed=../proto/backend.proto");
|
println!("cargo:rerun-if-changed=../proto/backend.proto");
|
||||||
prost_build::compile_protos(&["../proto/backend.proto"], &["../proto"]).unwrap();
|
|
||||||
|
let mut config = prost_build::Config::new();
|
||||||
|
config.service_generator(service_generator());
|
||||||
|
config
|
||||||
|
.compile_protos(&["../proto/backend.proto"], &["../proto"])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// write the other language ftl files
|
// write the other language ftl files
|
||||||
let mut ftl_lang_dirs = vec!["./ftl/repo/core".to_string()];
|
let mut ftl_lang_dirs = vec!["./ftl/repo/core".to_string()];
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
pub use crate::backend_proto::BackendMethod;
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::dbproxy::db_command_bytes,
|
backend::dbproxy::db_command_bytes,
|
||||||
backend_proto as pb,
|
backend_proto as pb,
|
||||||
backend_proto::builtin_search_order::BuiltinSortKind,
|
backend_proto::builtin_search_order::BuiltinSortKind,
|
||||||
backend_proto::{AddOrUpdateDeckConfigIn, Empty, RenderedTemplateReplacement, SyncMediaIn},
|
backend_proto::{
|
||||||
|
AddOrUpdateDeckConfigIn, BackendResult, Empty, RenderedTemplateReplacement, SyncMediaIn,
|
||||||
|
},
|
||||||
card::{Card, CardID},
|
card::{Card, CardID},
|
||||||
card::{CardQueue, CardType},
|
card::{CardQueue, CardType},
|
||||||
cloze::add_cloze_numbers_in_string,
|
cloze::add_cloze_numbers_in_string,
|
||||||
|
@ -37,13 +40,16 @@ 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;
|
use pb::{backend_input::Value, 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};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::{
|
||||||
|
result,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
mod dbproxy;
|
mod dbproxy;
|
||||||
|
@ -141,6 +147,36 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result<Backend, String> {
|
||||||
Ok(Backend::new(i18n, input.server))
|
Ok(Backend::new(i18n, input.server))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BackendService for Backend {
|
||||||
|
fn render_existing_card(
|
||||||
|
&mut self,
|
||||||
|
input: pb::RenderExistingCardIn,
|
||||||
|
) -> BackendResult<pb::RenderCardOut> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.render_existing_card(CardID(input.card_id), input.browser)
|
||||||
|
.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_uncommitted_card(
|
||||||
|
&mut self,
|
||||||
|
input: pb::RenderUncommittedCardIn,
|
||||||
|
) -> BackendResult<pb::RenderCardOut> {
|
||||||
|
let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?;
|
||||||
|
let template = schema11.into();
|
||||||
|
let mut note = input
|
||||||
|
.note
|
||||||
|
.ok_or_else(|| AnkiError::invalid_input("missing note"))?
|
||||||
|
.into();
|
||||||
|
let ord = input.card_ord as u16;
|
||||||
|
let fill_empty = input.fill_empty;
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.render_uncommitted_card(&mut note, &template, ord, fill_empty)
|
||||||
|
.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
pub fn new(i18n: I18n, server: bool) -> Backend {
|
pub fn new(i18n: I18n, server: bool) -> Backend {
|
||||||
Backend {
|
Backend {
|
||||||
|
@ -179,6 +215,19 @@ impl Backend {
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_command_bytes2(
|
||||||
|
&mut self,
|
||||||
|
method: u32,
|
||||||
|
input: &[u8],
|
||||||
|
) -> result::Result<Vec<u8>, Vec<u8>> {
|
||||||
|
self.run_command_bytes2_inner(method, input).map_err(|err| {
|
||||||
|
let backend_err = anki_error_to_proto_error(err, &self.i18n);
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
backend_err.encode(&mut bytes).unwrap();
|
||||||
|
bytes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// If collection is open, run the provided closure while holding
|
/// If collection is open, run the provided closure while holding
|
||||||
/// the mutex.
|
/// the mutex.
|
||||||
/// If collection is not open, return an error.
|
/// If collection is not open, return an error.
|
||||||
|
@ -461,31 +510,6 @@ impl Backend {
|
||||||
self.with_col(|col| col.deck_tree(input.include_counts, lim))
|
self.with_col(|col| col.deck_tree(input.include_counts, lim))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_existing_card(&self, input: pb::RenderExistingCardIn) -> Result<pb::RenderCardOut> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.render_existing_card(CardID(input.card_id), input.browser)
|
|
||||||
.map(Into::into)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_uncommitted_card(
|
|
||||||
&self,
|
|
||||||
input: pb::RenderUncommittedCardIn,
|
|
||||||
) -> Result<pb::RenderCardOut> {
|
|
||||||
let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?;
|
|
||||||
let template = schema11.into();
|
|
||||||
let mut note = input
|
|
||||||
.note
|
|
||||||
.ok_or_else(|| AnkiError::invalid_input("missing note"))?
|
|
||||||
.into();
|
|
||||||
let ord = input.card_ord as u16;
|
|
||||||
let fill_empty = input.fill_empty;
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.render_uncommitted_card(&mut note, &template, ord, fill_empty)
|
|
||||||
.map(Into::into)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> pb::ExtractAvTagsOut {
|
fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> pb::ExtractAvTagsOut {
|
||||||
let (text, tags) = extract_av_tags(&input.text, input.question_side);
|
let (text, tags) = extract_av_tags(&input.text, input.question_side);
|
||||||
let pt_tags = tags
|
let pt_tags = tags
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use anki::backend::{init_backend, Backend as RustBackend};
|
use anki::backend::{init_backend, Backend as RustBackend, BackendMethod};
|
||||||
use pyo3::exceptions::Exception;
|
use pyo3::exceptions::Exception;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use pyo3::types::PyBytes;
|
use pyo3::types::PyBytes;
|
||||||
use pyo3::{create_exception, exceptions, wrap_pyfunction};
|
use pyo3::{create_exception, exceptions, wrap_pyfunction};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
// Regular backend
|
// Regular backend
|
||||||
//////////////////////////////////
|
//////////////////////////////////
|
||||||
|
@ -16,6 +17,7 @@ struct Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
create_exception!(ankirspy, DBError, Exception);
|
create_exception!(ankirspy, DBError, Exception);
|
||||||
|
create_exception!(ankirspy, BackendError, Exception);
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn buildhash() -> &'static str {
|
fn buildhash() -> &'static str {
|
||||||
|
@ -30,6 +32,16 @@ fn open_backend(init_msg: &PyBytes) -> PyResult<Backend> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn want_release_gil(method: u32) -> bool {
|
||||||
|
if let Ok(method) = BackendMethod::try_from(method) {
|
||||||
|
match method {
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[pymethods]
|
#[pymethods]
|
||||||
impl Backend {
|
impl Backend {
|
||||||
fn command(&mut self, py: Python, input: &PyBytes, release_gil: bool) -> PyObject {
|
fn command(&mut self, py: Python, input: &PyBytes, release_gil: bool) -> PyObject {
|
||||||
|
@ -43,6 +55,20 @@ impl Backend {
|
||||||
out_obj.into()
|
out_obj.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command2(&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))
|
||||||
|
} else {
|
||||||
|
self.backend.run_command_bytes2(method, in_bytes)
|
||||||
|
}
|
||||||
|
.map(|out_bytes| {
|
||||||
|
let out_obj = PyBytes::new(py, &out_bytes);
|
||||||
|
out_obj.into()
|
||||||
|
})
|
||||||
|
.map_err(|err_bytes| BackendError::py_err(err_bytes))
|
||||||
|
}
|
||||||
|
|
||||||
fn set_progress_callback(&mut self, callback: PyObject) {
|
fn set_progress_callback(&mut self, callback: PyObject) {
|
||||||
if callback.is_none() {
|
if callback.is_none() {
|
||||||
self.backend.set_progress_callback(None);
|
self.backend.set_progress_callback(None);
|
||||||
|
|
Loading…
Reference in a new issue