ffd392de21
* Accept iterables as inputs to backend methods * Shift add-on check to backend; use new endpoint The new endpoint will return info on a suitable branch if found, instead of returning all branches. This simplifies the frontend code, and means that you can now drop support for certain versions without it also remotely disabling the add-on for people who are running one of the excluded versions, like in https://forums.ankiweb.net/t/prevent-add-ons-from-being-disabled-remote-stealthily-surreptitiously/33427 * Bump version to 23.09 This changes Anki's version numbering system to year.month.patch, as previously mentioned on https://forums.ankiweb.net/t/use-a-different-versioning-system-semver-perhaps/20046/5 This is shaping up to be a big release, with the introduction of FSRS and image occlusion, and it seems like a good time to be finally updating the version scheme as well. AnkiWeb has been updated to understand the new format, and add-on authors will now specify version compatibility using the full version number, as can be seen here: https://ankiweb.net/shared/info/3918629684 * Shift update check to backend, and tidy up update.py * Use the shared client for sync connections too
259 lines
7.9 KiB
Rust
259 lines
7.9 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use std::io::BufWriter;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
|
|
use anki_io::create_dir_all;
|
|
use anki_io::create_file;
|
|
use anki_proto_gen::BackendService;
|
|
use anki_proto_gen::Method;
|
|
use anyhow::Result;
|
|
use inflections::Inflect;
|
|
use prost_reflect::FieldDescriptor;
|
|
use prost_reflect::Kind;
|
|
use prost_reflect::MessageDescriptor;
|
|
|
|
pub(crate) fn write_python_interface(services: &[BackendService]) -> Result<()> {
|
|
let output_path = Path::new("../../out/pylib/anki/_backend_generated.py");
|
|
create_dir_all(output_path.parent().unwrap())?;
|
|
let mut out = BufWriter::new(create_file(output_path)?);
|
|
write_header(&mut out)?;
|
|
|
|
for service in services {
|
|
if service.name == "BackendAnkidroidService" {
|
|
continue;
|
|
}
|
|
for method in service.all_methods() {
|
|
render_method(service, method, &mut out);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Generates text like the following:
|
|
///
|
|
/// def get_field_names_raw(self, message: bytes) -> bytes:
|
|
/// return self._run_command(7, 16, message)
|
|
///
|
|
/// def get_field_names(self, ntid: int) -> Sequence[str]:
|
|
/// message = anki.notetypes_pb2.NotetypeId(ntid=ntid)
|
|
/// raw_bytes = self._run_command(7, 16, message.SerializeToString())
|
|
/// output = anki.generic_pb2.StringList()
|
|
/// output.ParseFromString(raw_bytes)
|
|
/// return output.vals
|
|
fn render_method(service: &BackendService, method: &Method, out: &mut impl Write) {
|
|
let method_name = method.name.to_snake_case();
|
|
let input = method.proto.input();
|
|
let output = method.proto.output();
|
|
let service_idx = service.index;
|
|
let method_idx = method.index;
|
|
let comments = format_comments(&method.comments);
|
|
|
|
// raw bytes
|
|
write!(
|
|
out,
|
|
r#" def {method_name}_raw(self, message: bytes) -> bytes:
|
|
{comments}return self._run_command({service_idx}, {method_idx}, message)
|
|
|
|
"#
|
|
)
|
|
.unwrap();
|
|
|
|
// (possibly destructured) message
|
|
let (input_params, input_assign) = maybe_destructured_input(&input);
|
|
let output_constructor = full_name_to_python(output.full_name());
|
|
let (output_msg_or_single_field, output_type) = maybe_destructured_output(&output);
|
|
write!(
|
|
out,
|
|
r#" def {method_name}({input_params}) -> {output_type}:
|
|
{comments}{input_assign}
|
|
raw_bytes = self._run_command({service_idx}, {method_idx}, message.SerializeToString())
|
|
output = {output_constructor}()
|
|
output.ParseFromString(raw_bytes)
|
|
return {output_msg_or_single_field}
|
|
|
|
"#
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn format_comments(comments: &Option<String>) -> String {
|
|
comments
|
|
.as_ref()
|
|
.map(|c| {
|
|
format!(
|
|
r#""""{c}"""
|
|
"#
|
|
)
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// If any of the following apply to the input type:
|
|
/// - it has a single field
|
|
/// - its name ends in Request
|
|
/// - it has any optional fields
|
|
/// ...then destructuring will be skipped, and the method will take the input
|
|
/// message directly. Returns (params_line, assignment_lines)
|
|
fn maybe_destructured_input(input: &MessageDescriptor) -> (String, String) {
|
|
if (input.name().ends_with("Request") || input.fields().len() < 2)
|
|
&& input.oneofs().next().is_none()
|
|
{
|
|
// destructure
|
|
let method_args = build_method_arguments(input);
|
|
let input_type = full_name_to_python(input.full_name());
|
|
let input_message_args = build_input_message_arguments(input);
|
|
let assignment = format!("message = {input_type}({input_message_args})",);
|
|
(method_args, assignment)
|
|
} else {
|
|
// no destructure
|
|
let params = format!("self, message: {}", full_name_to_python(input.full_name()));
|
|
let assignment = String::new();
|
|
(params, assignment)
|
|
}
|
|
}
|
|
|
|
/// e.g. "self, *, note_ids: Sequence[int], new_fields: Sequence[int]"
|
|
fn build_method_arguments(input: &MessageDescriptor) -> String {
|
|
let fields = input.fields();
|
|
let mut args = vec!["self".to_string()];
|
|
if fields.len() >= 2 {
|
|
args.push("*".to_string());
|
|
}
|
|
for field in fields {
|
|
let arg = format!("{}: {}", field.name(), python_type(&field, false));
|
|
args.push(arg);
|
|
}
|
|
args.join(", ")
|
|
}
|
|
|
|
/// e.g. "note_ids=note_ids, new_fields=new_fields"
|
|
fn build_input_message_arguments(input: &MessageDescriptor) -> String {
|
|
input
|
|
.fields()
|
|
.map(|field| {
|
|
let name = field.name();
|
|
format!("{name}={name}")
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
}
|
|
|
|
// If output type has a single field and is not an enum, we return its single
|
|
// field value directly. Returns (expr, type), where expr is 'output' or
|
|
// 'output.<only_field>'.
|
|
fn maybe_destructured_output(output: &MessageDescriptor) -> (String, String) {
|
|
let first_field = output.fields().next();
|
|
if output.fields().len() == 1 && !matches!(first_field.as_ref().unwrap().kind(), Kind::Enum(_))
|
|
{
|
|
let field = first_field.unwrap();
|
|
(
|
|
format!("output.{}", field.name()),
|
|
python_type(&field, true),
|
|
)
|
|
} else {
|
|
("output".into(), full_name_to_python(output.full_name()))
|
|
}
|
|
}
|
|
|
|
/// e.g. uint32 -> int; repeated bool -> Sequence[bool]
|
|
fn python_type(field: &FieldDescriptor, output: bool) -> String {
|
|
let kind = match field.kind() {
|
|
Kind::Int32
|
|
| Kind::Int64
|
|
| Kind::Uint32
|
|
| Kind::Uint64
|
|
| Kind::Sint32
|
|
| Kind::Sint64
|
|
| Kind::Fixed32
|
|
| Kind::Fixed64
|
|
| Kind::Sfixed32
|
|
| Kind::Sfixed64 => "int".into(),
|
|
Kind::Float | Kind::Double => "float".into(),
|
|
Kind::Bool => "bool".into(),
|
|
Kind::String => "str".into(),
|
|
Kind::Bytes => "bytes".into(),
|
|
Kind::Message(msg) => full_name_to_python(msg.full_name()),
|
|
Kind::Enum(en) => format!("{}.V", full_name_to_python(en.full_name())),
|
|
};
|
|
if field.is_list() {
|
|
if output {
|
|
format!("Sequence[{}]", kind)
|
|
} else {
|
|
format!("Iterable[{}]", kind)
|
|
}
|
|
} else if field.is_map() {
|
|
let map_kind = field.kind();
|
|
let map_kind = map_kind.as_message().unwrap();
|
|
let map_kv: Vec<_> = map_kind.fields().map(|f| python_type(&f, output)).collect();
|
|
format!("Mapping[{}, {}]", map_kv[0], map_kv[1])
|
|
} else {
|
|
kind
|
|
}
|
|
}
|
|
|
|
// e.g. anki.import_export.ImportResponse ->
|
|
// anki.import_export_pb2.ImportResponse
|
|
fn full_name_to_python(name: &str) -> String {
|
|
let mut name = name.splitn(3, '.');
|
|
format!(
|
|
"{}.{}_pb2.{}",
|
|
name.next().unwrap(),
|
|
name.next().unwrap(),
|
|
name.next().unwrap()
|
|
)
|
|
}
|
|
|
|
fn write_header(out: &mut impl Write) -> Result<()> {
|
|
out.write_all(
|
|
br#"# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; https://www.gnu.org/licenses/agpl.html
|
|
# pylint: skip-file
|
|
|
|
from __future__ import annotations
|
|
|
|
"""
|
|
THIS FILE IS AUTOMATICALLY GENERATED - DO NOT EDIT.
|
|
|
|
Please do not access methods on the backend directly - they may be changed
|
|
or removed at any time. Instead, please use the methods on the collection
|
|
instead. Eg, don't use col.backend.all_deck_config(), instead use
|
|
col.decks.all_config()
|
|
"""
|
|
|
|
from typing import *
|
|
|
|
import anki
|
|
import anki.ankiweb_pb2
|
|
import anki.backend_pb2
|
|
import anki.card_rendering_pb2
|
|
import anki.cards_pb2
|
|
import anki.collection_pb2
|
|
import anki.config_pb2
|
|
import anki.deck_config_pb2
|
|
import anki.decks_pb2
|
|
import anki.i18n_pb2
|
|
import anki.image_occlusion_pb2
|
|
import anki.import_export_pb2
|
|
import anki.links_pb2
|
|
import anki.media_pb2
|
|
import anki.notes_pb2
|
|
import anki.notetypes_pb2
|
|
import anki.scheduler_pb2
|
|
import anki.search_pb2
|
|
import anki.stats_pb2
|
|
import anki.sync_pb2
|
|
import anki.tags_pb2
|
|
|
|
class RustBackendGenerated:
|
|
def _run_command(self, service: int, method: int, input: Any) -> bytes:
|
|
raise Exception("not implemented")
|
|
|
|
"#,
|
|
)?;
|
|
Ok(())
|
|
}
|