start work on more clearly defining backend/protobuf boundaries

- anki._backend stores the protobuf files and rsbackend.py code
- pylib modules import protobuf messages directly from the
_pb2 files, and explicitly export any will be returned or consumed
by public pylib functions, so that calling code can import from pylib
- the "rsbackend" no longer imports and re-exports protobuf messages
- pylib can just consume them directly.
- move errors to errors.py

Still todo:

- rsbridge
- finishing the work on rsbackend, and check what we need to add
back to the original file location to avoid breaking add-ons
This commit is contained in:
Damien Elmes 2021-01-31 15:55:08 +10:00
parent cd9767be80
commit 9d853bbb03
60 changed files with 420 additions and 367 deletions

View file

@ -1,5 +1,5 @@
[settings] [settings]
skip=aqt/forms,backend_pb2.py,backend_pb2.pyi,fluent_pb2.py,fluent_pb2.pyi,rsbackend_gen.py,hooks_gen.py,genbackend.py skip=aqt/forms,backend_pb2.py,backend_pb2.pyi,fluent_pb2.py,fluent_pb2.pyi,rsbackend_gen.py,generated.py,hooks_gen.py,genbackend.py
profile=black profile=black
multi_line_output=3 multi_line_output=3
include_trailing_comma=True include_trailing_comma=True

View file

@ -1,7 +1,6 @@
load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
load("@rules_python//python:defs.bzl", "py_library") load("@rules_python//python:defs.bzl", "py_library")
load("@py_deps//:requirements.bzl", "requirement") load("@py_deps//:requirements.bzl", "requirement")
load("//pylib:protobuf.bzl", "py_proto_library_typed")
load("@rules_python//experimental/python:wheel.bzl", "py_package", "py_wheel") load("@rules_python//experimental/python:wheel.bzl", "py_package", "py_wheel")
load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//lib:selects.bzl", "selects")
load("//:defs.bzl", "anki_version") load("//:defs.bzl", "anki_version")
@ -13,13 +12,6 @@ copy_file(
out = "buildinfo.txt", out = "buildinfo.txt",
) )
genrule(
name = "rsbackend_gen",
outs = ["rsbackend_gen.py"],
cmd = "$(location //pylib/tools:genbackend) > $@",
tools = ["//pylib/tools:genbackend"],
)
genrule( genrule(
name = "hooks_gen", name = "hooks_gen",
outs = ["hooks_gen.py"], outs = ["hooks_gen.py"],
@ -27,22 +19,6 @@ genrule(
tools = ["//pylib/tools:genhooks"], tools = ["//pylib/tools:genhooks"],
) )
py_proto_library_typed(
name = "backend_pb2",
src = "//rslib:backend.proto",
visibility = [
"//visibility:public",
],
)
py_proto_library_typed(
name = "fluent_pb2",
src = "//rslib:fluent.proto",
visibility = [
"//visibility:public",
],
)
copy_file( copy_file(
name = "rsbridge_unix", name = "rsbridge_unix",
src = "//pylib/rsbridge", src = "//pylib/rsbridge",
@ -69,7 +45,6 @@ alias(
_py_srcs = glob( _py_srcs = glob(
["**/*.py"], ["**/*.py"],
exclude = [ exclude = [
"rsbackend_gen.py",
"hooks_gen.py", "hooks_gen.py",
], ],
) )
@ -79,12 +54,10 @@ py_library(
srcs = _py_srcs, srcs = _py_srcs,
data = [ data = [
"py.typed", "py.typed",
":backend_pb2",
":buildinfo", ":buildinfo",
":fluent_pb2",
":hooks_gen", ":hooks_gen",
":rsbackend_gen",
":rsbridge", ":rsbridge",
"//pylib/anki/_backend",
], ],
imports = [ imports = [
"..", "..",

View file

@ -0,0 +1,50 @@
load("@rules_python//python:defs.bzl", "py_binary")
load("@py_deps//:requirements.bzl", "requirement")
load("//pylib:protobuf.bzl", "py_proto_library_typed")
py_proto_library_typed(
name = "backend_pb2",
src = "//rslib:backend.proto",
visibility = [
"//visibility:public",
],
)
py_proto_library_typed(
name = "fluent_pb2",
src = "//rslib:fluent.proto",
visibility = [
"//visibility:public",
],
)
py_binary(
name = "genbackend",
srcs = [
"backend_pb2",
"genbackend.py",
],
deps = [
requirement("black"),
requirement("stringcase"),
requirement("protobuf"),
],
)
genrule(
name = "rsbackend_gen",
outs = ["generated.py"],
cmd = "$(location genbackend) > $@",
tools = ["genbackend"],
)
filegroup(
name = "_backend",
srcs = [
"__init__.py",
":backend_pb2",
":fluent_pb2",
":rsbackend_gen",
],
visibility = ["//pylib:__subpackages__"],
)

View file

@ -22,19 +22,19 @@ import os
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union
import anki._backend.backend_pb2 as pb
import anki._rsbridge import anki._rsbridge
import anki.backend_pb2 as pb
import anki.buildinfo import anki.buildinfo
from anki import hooks from anki import hooks
from anki._backend.generated import RustBackendGenerated
from anki.dbproxy import Row as DBRow from anki.dbproxy import Row as DBRow
from anki.dbproxy import ValueForDB from anki.dbproxy import ValueForDB
from anki.fluent_pb2 import FluentString as TR from anki.errors import backend_exception_to_pylib
from anki.rsbackend_gen import RustBackendGenerated from anki.lang import FormatTimeSpanContext
from anki.utils import from_json_bytes, to_json_bytes
if TYPE_CHECKING: if TYPE_CHECKING:
from anki.fluent_pb2 import FluentStringValue as TRValue from anki.lang import FormatTimeSpanContextValue, TRValue
FormatTimeSpanContextValue = pb.FormatTimespanIn.ContextValue
assert anki._rsbridge.buildhash() == anki.buildinfo.buildhash assert anki._rsbridge.buildhash() == anki.buildinfo.buildhash
@ -48,151 +48,10 @@ BackendNote = pb.Note
Tag = pb.Tag Tag = pb.Tag
TagTreeNode = pb.TagTreeNode TagTreeNode = pb.TagTreeNode
NoteType = pb.NoteType NoteType = pb.NoteType
DeckTreeNode = pb.DeckTreeNode
StockNoteType = pb.StockNoteType StockNoteType = pb.StockNoteType
ConcatSeparator = pb.ConcatenateSearchesIn.Separator ConcatSeparator = pb.ConcatenateSearchesIn.Separator
SyncAuth = pb.SyncAuth
SyncOutput = pb.SyncCollectionOut
SyncStatus = pb.SyncStatusOut
CountsForDeckToday = pb.CountsForDeckTodayOut CountsForDeckToday = pb.CountsForDeckTodayOut
try:
import orjson
to_json_bytes = orjson.dumps
from_json_bytes = orjson.loads
except:
print("orjson is missing; DB operations will be slower")
to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore
from_json_bytes = json.loads
class Interrupted(Exception):
pass
class StringError(Exception):
def __str__(self) -> str:
return self.args[0] # pylint: disable=unsubscriptable-object
NetworkErrorKind = pb.NetworkError.NetworkErrorKind
SyncErrorKind = pb.SyncError.SyncErrorKind
class NetworkError(StringError):
def kind(self) -> pb.NetworkError.NetworkErrorKindValue:
return self.args[1]
class SyncError(StringError):
def kind(self) -> pb.SyncError.SyncErrorKindValue:
return self.args[1]
class IOError(StringError):
pass
class DBError(StringError):
pass
class TemplateError(StringError):
pass
class NotFoundError(Exception):
pass
class ExistsError(Exception):
pass
class DeckIsFilteredError(Exception):
pass
class InvalidInput(StringError):
pass
def proto_exception_to_native(err: pb.BackendError) -> Exception:
val = err.WhichOneof("value")
if val == "interrupted":
return Interrupted()
elif val == "network_error":
return NetworkError(err.localized, err.network_error.kind)
elif val == "sync_error":
return SyncError(err.localized, err.sync_error.kind)
elif val == "io_error":
return IOError(err.localized)
elif val == "db_error":
return DBError(err.localized)
elif val == "template_parse":
return TemplateError(err.localized)
elif val == "invalid_input":
return InvalidInput(err.localized)
elif val == "json_error":
return StringError(err.localized)
elif val == "not_found_error":
return NotFoundError()
elif val == "exists":
return ExistsError()
elif val == "deck_is_filtered":
return DeckIsFilteredError()
elif val == "proto_error":
return StringError(err.localized)
else:
print("unhandled error type:", val)
return StringError(err.localized)
MediaSyncProgress = pb.MediaSyncProgress
FullSyncProgress = pb.FullSyncProgress
NormalSyncProgress = pb.NormalSyncProgress
DatabaseCheckProgress = pb.DatabaseCheckProgress
FormatTimeSpanContext = pb.FormatTimespanIn.Context
class ProgressKind(enum.Enum):
NoProgress = 0
MediaSync = 1
MediaCheck = 2
FullSync = 3
NormalSync = 4
DatabaseCheck = 5
@dataclass
class Progress:
kind: ProgressKind
val: Union[
MediaSyncProgress,
pb.FullSyncProgress,
NormalSyncProgress,
DatabaseCheckProgress,
str,
]
@staticmethod
def from_proto(proto: pb.Progress) -> Progress:
kind = proto.WhichOneof("value")
if kind == "media_sync":
return Progress(kind=ProgressKind.MediaSync, val=proto.media_sync)
elif kind == "media_check":
return Progress(kind=ProgressKind.MediaCheck, val=proto.media_check)
elif kind == "full_sync":
return Progress(kind=ProgressKind.FullSync, val=proto.full_sync)
elif kind == "normal_sync":
return Progress(kind=ProgressKind.NormalSync, val=proto.normal_sync)
elif kind == "database_check":
return Progress(kind=ProgressKind.DatabaseCheck, val=proto.database_check)
else:
return Progress(kind=ProgressKind.NoProgress, val="")
class RustBackend(RustBackendGenerated): class RustBackend(RustBackendGenerated):
def __init__( def __init__(
@ -240,7 +99,7 @@ class RustBackend(RustBackendGenerated):
err_bytes = bytes(e.args[0]) err_bytes = bytes(e.args[0])
err = pb.BackendError() err = pb.BackendError()
err.ParseFromString(err_bytes) err.ParseFromString(err_bytes)
raise proto_exception_to_native(err) raise backend_exception_to_pylib(err)
def translate(self, key: TRValue, **kwargs: Union[str, int, float]) -> str: def translate(self, key: TRValue, **kwargs: Union[str, int, float]) -> str:
return self.translate_string(translate_string_in(key, **kwargs)) return self.translate_string(translate_string_in(key, **kwargs))
@ -263,7 +122,7 @@ class RustBackend(RustBackendGenerated):
err_bytes = bytes(e.args[0]) err_bytes = bytes(e.args[0])
err = pb.BackendError() err = pb.BackendError()
err.ParseFromString(err_bytes) err.ParseFromString(err_bytes)
raise proto_exception_to_native(err) raise backend_exception_to_pylib(err)
def translate_string_in( def translate_string_in(

View file

@ -0,0 +1 @@
../../../bazel-bin/pylib/anki/_backend/backend_pb2.pyi

View file

@ -0,0 +1 @@
../../../bazel-bin/pylib/anki/_backend/fluent_pb2.pyi

View file

@ -5,7 +5,7 @@ import os
import re import re
import sys import sys
import pylib.anki.backend_pb2 as pb import pylib.anki._backend.backend_pb2 as pb
import stringcase import stringcase
@ -168,7 +168,7 @@ col.decks.all_config()
from typing import * from typing import *
import anki.backend_pb2 as pb import anki._backend.backend_pb2 as pb
class RustBackendGenerated: class RustBackendGenerated:
def _run_command(self, method: int, input: Any) -> bytes: def _run_command(self, method: int, input: Any) -> bytes:

View file

@ -0,0 +1 @@
../../../bazel-bin/pylib/anki/_backend/generated.py

View file

@ -1 +0,0 @@
../../bazel-bin/pylib/anki/backend_pb2.pyi

View file

@ -8,11 +8,11 @@ import time
from typing import List, Optional from typing import List, Optional
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb
from anki import hooks from anki import hooks
from anki.consts import * from anki.consts import *
from anki.models import NoteType, Template from anki.models import NoteType, Template
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import BackendCard
from anki.sound import AVTag from anki.sound import AVTag
# Cards # Cards
@ -45,14 +45,14 @@ class Card:
self.load() self.load()
else: else:
# new card with defaults # new card with defaults
self._load_from_backend_card(BackendCard()) self._load_from_backend_card(_pb.Card())
def load(self) -> None: def load(self) -> None:
c = self.col.backend.get_card(self.id) c = self.col.backend.get_card(self.id)
assert c assert c
self._load_from_backend_card(c) self._load_from_backend_card(c)
def _load_from_backend_card(self, c: BackendCard) -> None: def _load_from_backend_card(self, c: _pb.Card) -> None:
self._render_output = None self._render_output = None
self._note = None self._note = None
self.id = c.id self.id = c.id
@ -86,7 +86,7 @@ class Card:
self._bugcheck() self._bugcheck()
hooks.card_will_flush(self) hooks.card_will_flush(self)
# mtime & usn are set by backend # mtime & usn are set by backend
card = BackendCard( card = _pb.Card(
id=self.id, id=self.id,
note_id=self.nid, note_id=self.nid,
deck_id=self.did, deck_id=self.did,

View file

@ -4,51 +4,66 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import enum
import os import os
import pprint import pprint
import re import re
import time import time
import traceback import traceback
import weakref import weakref
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Union
import anki.backend_pb2 as pb import anki._backend.backend_pb2 as _pb
import anki.find import anki.find
import anki.latex # sets up hook import anki.latex # sets up hook
import anki.template import anki.template
from anki import hooks from anki import hooks
from anki.backend_pb2 import SearchTerm from anki._backend import ( # pylint: disable=unused-import
ConcatSeparator,
FormatTimeSpanContext,
RustBackend,
)
# from anki._backend import _SyncStatus as SyncStatus
from anki.cards import Card from anki.cards import Card
from anki.config import ConfigManager from anki.config import ConfigManager
from anki.consts import * from anki.consts import *
from anki.dbproxy import DBProxy from anki.dbproxy import DBProxy
from anki.decks import DeckManager from anki.decks import DeckManager
from anki.errors import AnkiError from anki.errors import AnkiError, DBError
from anki.lang import TR
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 ( # pylint: disable=unused-import
TR,
ConcatSeparator,
DBError,
FormatTimeSpanContext,
InvalidInput,
Progress,
RustBackend,
from_json_bytes,
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
from anki.utils import devMode, ids2str, intTime, splitFields, stripHTMLMedia from anki.utils import (
devMode,
from_json_bytes,
ids2str,
intTime,
splitFields,
stripHTMLMedia,
)
ConfigBoolKey = pb.ConfigBool.Key # pylint: disable=no-member # public exports
SearchTerm = _pb.SearchTerm
MediaSyncProgress = _pb.MediaSyncProgress
FullSyncProgress = _pb.FullSyncProgress
NormalSyncProgress = _pb.NormalSyncProgress
DatabaseCheckProgress = _pb.DatabaseCheckProgress
ConfigBoolKey = _pb.ConfigBool.Key # pylint: disable=no-member
EmptyCardsReport = _pb.EmptyCardsReport
NoteWithEmptyCards = _pb.NoteWithEmptyCards
GraphPreferences = _pb.GraphPreferences
# pylint: disable=no-member
if TYPE_CHECKING: if TYPE_CHECKING:
from anki.rsbackend import FormatTimeSpanContextValue, TRValue from anki.lang import FormatTimeSpanContextValue, TRValue
ConfigBoolKeyValue = pb.ConfigBool.KeyValue # pylint: disable=no-member ConfigBoolKeyValue = _pb.ConfigBool.KeyValue
class Collection: class Collection:
@ -394,6 +409,9 @@ class Collection:
def set_deck(self, card_ids: List[int], deck_id: int) -> None: def set_deck(self, card_ids: List[int], deck_id: int) -> None:
self.backend.set_deck(card_ids=card_ids, deck_id=deck_id) self.backend.set_deck(card_ids=card_ids, deck_id=deck_id)
def get_empty_cards(self) -> EmptyCardsReport:
return self.backend.get_empty_cards()
# legacy # legacy
def remCards(self, ids: List[int], notes: bool = True) -> None: def remCards(self, ids: List[int], notes: bool = True) -> None:
@ -445,20 +463,20 @@ class Collection:
order: Union[ order: Union[
bool, bool,
str, str,
pb.BuiltinSearchOrder.BuiltinSortKindValue, # pylint: disable=no-member _pb.BuiltinSearchOrder.BuiltinSortKindValue, # pylint: disable=no-member
] = False, ] = False,
reverse: bool = False, reverse: bool = False,
) -> Sequence[int]: ) -> Sequence[int]:
if isinstance(order, str): if isinstance(order, str):
mode = pb.SortOrder(custom=order) mode = _pb.SortOrder(custom=order)
elif isinstance(order, bool): elif isinstance(order, bool):
if order is True: if order is True:
mode = pb.SortOrder(from_config=pb.Empty()) mode = _pb.SortOrder(from_config=_pb.Empty())
else: else:
mode = pb.SortOrder(none=pb.Empty()) mode = _pb.SortOrder(none=_pb.Empty())
else: else:
mode = pb.SortOrder( mode = _pb.SortOrder(
builtin=pb.BuiltinSearchOrder(kind=order, reverse=reverse) builtin=_pb.BuiltinSearchOrder(kind=order, reverse=reverse)
) )
return self.backend.search_cards(search=query, order=mode) return self.backend.search_cards(search=query, order=mode)
@ -603,6 +621,19 @@ table.review-log {{ {revlog_style} }}
def studied_today(self) -> str: def studied_today(self) -> str:
return self.backend.studied_today() return self.backend.studied_today()
def graph_data(self, search: str, days: int) -> bytes:
return self.backend.graphs(search=search, days=days)
def get_graph_preferences(self) -> bytes:
return self.backend.get_graph_preferences()
def set_graph_preferences(self, prefs: GraphPreferences) -> None:
self.backend.set_graph_preferences(input=prefs)
def congrats_info(self) -> bytes:
"Don't use this, it will likely go away in the future."
return self.backend.congrats_info().SerializeToString()
# legacy # legacy
def cardStats(self, card: Card) -> str: def cardStats(self, card: Card) -> str:
@ -797,5 +828,42 @@ table.review-log {{ {revlog_style} }}
) )
class ProgressKind(enum.Enum):
NoProgress = 0
MediaSync = 1
MediaCheck = 2
FullSync = 3
NormalSync = 4
DatabaseCheck = 5
@dataclass
class Progress:
kind: ProgressKind
val: Union[
MediaSyncProgress,
FullSyncProgress,
NormalSyncProgress,
DatabaseCheckProgress,
str,
]
@staticmethod
def from_proto(proto: _pb.Progress) -> Progress:
kind = proto.WhichOneof("value")
if kind == "media_sync":
return Progress(kind=ProgressKind.MediaSync, val=proto.media_sync)
elif kind == "media_check":
return Progress(kind=ProgressKind.MediaCheck, val=proto.media_check)
elif kind == "full_sync":
return Progress(kind=ProgressKind.FullSync, val=proto.full_sync)
elif kind == "normal_sync":
return Progress(kind=ProgressKind.NormalSync, val=proto.normal_sync)
elif kind == "database_check":
return Progress(kind=ProgressKind.DatabaseCheck, val=proto.database_check)
else:
return Progress(kind=ProgressKind.NoProgress, val="")
# legacy name # legacy name
_Collection = Collection _Collection = Collection

View file

@ -23,7 +23,8 @@ 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 from anki.errors import NotFoundError
from anki.utils import from_json_bytes, to_json_bytes
class ConfigManager: class ConfigManager:

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import anki import anki
from anki.rsbackend import TR from anki.lang import TR
# whether new cards should be mixed with reviews, or shown first or last # whether new cards should be mixed with reviews, or shown first or last
NEW_CARDS_DISTRIBUTE = 0 NEW_CARDS_DISTRIBUTE = 0

View file

@ -21,7 +21,7 @@ class DBProxy:
# Lifecycle # Lifecycle
############### ###############
def __init__(self, backend: anki.rsbackend.RustBackend) -> None: def __init__(self, backend: anki._backend.RustBackend) -> None:
self._backend = backend self._backend = backend
self.mod = False self.mod = False
self.last_begin_at = 0 self.last_begin_at = 0

View file

@ -8,17 +8,14 @@ import pprint
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.backend_pb2 as pb import anki._backend.backend_pb2 as _pb
from anki.consts import * from anki.consts import *
from anki.errors import DeckRenameError from anki.errors import DeckIsFilteredError, DeckRenameError, NotFoundError
from anki.rsbackend import ( from anki.utils import from_json_bytes, ids2str, intTime, to_json_bytes
TR,
DeckTreeNode, # public exports
NotFoundError, DeckTreeNode = _pb.DeckTreeNode
from_json_bytes, DeckNameID = _pb.DeckNameID
to_json_bytes,
)
from anki.utils import ids2str, intTime
# legacy code may pass this in as the type argument to .id() # legacy code may pass this in as the type argument to .id()
defaultDeck = 0 defaultDeck = 0
@ -139,7 +136,7 @@ class DeckManager:
def all_names_and_ids( def all_names_and_ids(
self, skip_empty_default=False, include_filtered=True self, skip_empty_default=False, include_filtered=True
) -> Sequence[pb.DeckNameID]: ) -> Sequence[DeckNameID]:
"A sorted sequence of deck names and IDs." "A sorted sequence of deck names and IDs."
return self.col.backend.get_deck_names( return self.col.backend.get_deck_names(
skip_empty_default=skip_empty_default, include_filtered=include_filtered skip_empty_default=skip_empty_default, include_filtered=include_filtered
@ -166,7 +163,7 @@ class DeckManager:
def new_deck_legacy(self, filtered: bool) -> Deck: def new_deck_legacy(self, filtered: bool) -> Deck:
return from_json_bytes(self.col.backend.new_deck_legacy(filtered)) return from_json_bytes(self.col.backend.new_deck_legacy(filtered))
def deck_tree(self) -> pb.DeckTreeNode: def deck_tree(self) -> DeckTreeNode:
return self.col.backend.deck_tree(top_deck_id=0, now=0) return self.col.backend.deck_tree(top_deck_id=0, now=0)
@classmethod @classmethod
@ -250,7 +247,7 @@ class DeckManager:
g["id"] = self.col.backend.add_or_update_deck_legacy( g["id"] = self.col.backend.add_or_update_deck_legacy(
deck=to_json_bytes(g), preserve_usn_and_mtime=preserve_usn deck=to_json_bytes(g), preserve_usn_and_mtime=preserve_usn
) )
except anki.rsbackend.DeckIsFilteredError as exc: except DeckIsFilteredError as exc:
raise DeckRenameError("deck was filtered") from exc raise DeckRenameError("deck was filtered") from exc
def rename(self, g: Deck, newName: str) -> None: def rename(self, g: Deck, newName: str) -> None:

View file

@ -1,8 +1,92 @@
# 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
from __future__ import annotations
from typing import Any from typing import Any
import anki._backend.backend_pb2 as _pb
# fixme: notfounderror etc need to be in rsbackend.py
class StringError(Exception):
def __str__(self) -> str:
return self.args[0] # pylint: disable=unsubscriptable-object
class Interrupted(Exception):
pass
class NetworkError(StringError):
pass
class SyncError(StringError):
# pylint: disable=no-member
def is_auth_error(self) -> bool:
return self.args[1] == _pb.SyncError.SyncErrorKind.AUTH_FAILED
class IOError(StringError):
pass
class DBError(StringError):
pass
class TemplateError(StringError):
pass
class NotFoundError(Exception):
pass
class ExistsError(Exception):
pass
class DeckIsFilteredError(Exception):
pass
class InvalidInput(StringError):
pass
def backend_exception_to_pylib(err: _pb.BackendError) -> Exception:
val = err.WhichOneof("value")
if val == "interrupted":
return Interrupted()
elif val == "network_error":
return NetworkError(err.localized, err.network_error.kind)
elif val == "sync_error":
return SyncError(err.localized, err.sync_error.kind)
elif val == "io_error":
return IOError(err.localized)
elif val == "db_error":
return DBError(err.localized)
elif val == "template_parse":
return TemplateError(err.localized)
elif val == "invalid_input":
return InvalidInput(err.localized)
elif val == "json_error":
return StringError(err.localized)
elif val == "not_found_error":
return NotFoundError()
elif val == "exists":
return ExistsError()
elif val == "deck_is_filtered":
return DeckIsFilteredError()
elif val == "proto_error":
return StringError(err.localized)
else:
print("unhandled error type:", val)
return StringError(err.localized)
class AnkiError(Exception): class AnkiError(Exception):
def __init__(self, type, **data) -> None: def __init__(self, type, **data) -> None:

View file

@ -13,7 +13,7 @@ from zipfile import ZipFile
from anki import hooks from anki import hooks
from anki.collection import Collection from anki.collection import Collection
from anki.rsbackend import TR from anki.lang import TR
from anki.utils import ids2str, namedtmp, splitFields, stripHTML from anki.utils import ids2str, namedtmp, splitFields, stripHTML

View file

@ -1 +0,0 @@
../../bazel-bin/pylib/anki/fluent_pb2.pyi

View file

@ -7,8 +7,7 @@ from anki.importing.csvfile import TextImporter
from anki.importing.mnemo import MnemosyneImporter from anki.importing.mnemo import MnemosyneImporter
from anki.importing.pauker import PaukerImporter from anki.importing.pauker import PaukerImporter
from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore
from anki.lang import tr_legacyglobal from anki.lang import TR, tr_legacyglobal
from anki.rsbackend import TR
Importers = ( Importers = (
(tr_legacyglobal(TR.IMPORTING_TEXT_SEPARATED_BY_TABS_OR_SEMICOLONS), TextImporter), (tr_legacyglobal(TR.IMPORTING_TEXT_SEPARATED_BY_TABS_OR_SEMICOLONS), TextImporter),

View file

@ -9,7 +9,7 @@ from anki.collection import Collection
from anki.consts import * from anki.consts import *
from anki.decks import DeckManager from anki.decks import DeckManager
from anki.importing.base import Importer from anki.importing.base import Importer
from anki.rsbackend import TR from anki.lang import TR
from anki.utils import intTime, joinFields, splitFields from anki.utils import intTime, joinFields, splitFields
GUID = 1 GUID = 1

View file

@ -7,7 +7,7 @@ from typing import Any, List, Optional, TextIO, Union
from anki.collection import Collection from anki.collection import Collection
from anki.importing.noteimp import ForeignNote, NoteImporter from anki.importing.noteimp import ForeignNote, NoteImporter
from anki.rsbackend import TR from anki.lang import TR
class TextImporter(NoteImporter): class TextImporter(NoteImporter):

View file

@ -7,7 +7,7 @@ from typing import cast
from anki.db import DB from anki.db import DB
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
from anki.rsbackend import TR from anki.lang import TR
from anki.stdmodels import addBasicModel, addClozeModel from anki.stdmodels import addBasicModel, addClozeModel

View file

@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple, Union
from anki.collection import Collection from anki.collection import Collection
from anki.consts import NEW_CARDS_RANDOM, STARTING_FACTOR from anki.consts import NEW_CARDS_RANDOM, STARTING_FACTOR
from anki.importing.base import Importer from anki.importing.base import Importer
from anki.rsbackend import TR from anki.lang import TR
from anki.utils import ( from anki.utils import (
fieldChecksum, fieldChecksum,
guid64, guid64,

View file

@ -5,9 +5,20 @@ from __future__ import annotations
import locale import locale
import re import re
from typing import Optional, Tuple from typing import TYPE_CHECKING, Optional, Tuple
import anki import anki
import anki._backend.backend_pb2 as _pb
import anki._backend.fluent_pb2 as _fluent_pb
# public exports
TR = _fluent_pb.FluentString
FormatTimeSpanContext = _pb.FormatTimespanIn.Context # pylint: disable=no-member
# pylint: disable=no-member
if TYPE_CHECKING:
TRValue = _fluent_pb.FluentStringValue
FormatTimeSpanContextValue = _pb.FormatTimespanIn.ContextValue
langs = sorted( langs = sorted(
[ [
@ -142,7 +153,7 @@ def lang_to_disk_lang(lang: str) -> str:
currentLang = "en" currentLang = "en"
# the current Fluent translation instance # the current Fluent translation instance
current_i18n: Optional[anki.rsbackend.RustBackend] = None current_i18n: Optional[anki._backend.RustBackend] = None
# path to locale folder # path to locale folder
locale_folder = "" locale_folder = ""
@ -169,7 +180,7 @@ def tr_legacyglobal(*args, **kwargs) -> str:
def set_lang(lang: str, locale_dir: str) -> None: def set_lang(lang: str, locale_dir: str) -> None:
global currentLang, current_i18n, locale_folder global currentLang, current_i18n, locale_folder
currentLang = lang currentLang = lang
current_i18n = anki.rsbackend.RustBackend(ftl_folder=locale_folder, langs=[lang]) current_i18n = anki._backend.RustBackend(ftl_folder=locale_folder, langs=[lang])
locale_folder = locale_dir locale_folder = locale_dir

View file

@ -10,9 +10,10 @@ from dataclasses import dataclass
from typing import Any, List, Optional, Tuple from typing import Any, List, Optional, Tuple
import anki import anki
import anki._backend.backend_pb2 as _pb
from anki import hooks from anki import hooks
from anki.lang import TR
from anki.models import NoteType from anki.models import NoteType
from anki.rsbackend import TR, pb
from anki.template import TemplateRenderContext, TemplateRenderOutput from anki.template import TemplateRenderContext, TemplateRenderOutput
from anki.utils import call, isMac, namedtmp, tmpdir from anki.utils import call, isMac, namedtmp, tmpdir
@ -45,7 +46,7 @@ class ExtractedLatexOutput:
latex: List[ExtractedLatex] latex: List[ExtractedLatex]
@staticmethod @staticmethod
def from_proto(proto: pb.ExtractLatexOut) -> ExtractedLatexOutput: def from_proto(proto: _pb.ExtractLatexOut) -> ExtractedLatexOutput:
return ExtractedLatexOutput( return ExtractedLatexOutput(
html=proto.text, html=proto.text,
latex=[ latex=[

View file

@ -14,9 +14,9 @@ import urllib.request
from typing import Any, Callable, List, Optional, Tuple from typing import Any, Callable, List, Optional, Tuple
import anki import anki
import anki._backend.backend_pb2 as _pb
from anki.consts import * from anki.consts import *
from anki.latex import render_latex, render_latex_returning_errors from anki.latex import render_latex, render_latex_returning_errors
from anki.rsbackend import pb
from anki.utils import intTime from anki.utils import intTime
@ -26,6 +26,9 @@ def media_paths_from_col_path(col_path: str) -> Tuple[str, str]:
return (media_folder, media_db) return (media_folder, media_db)
CheckMediaOut = _pb.CheckMediaOut
# fixme: look into whether we can drop chdir() below # fixme: look into whether we can drop chdir() below
# - need to check aa89d06304fecd3597da4565330a3e55bdbb91fe # - need to check aa89d06304fecd3597da4565330a3e55bdbb91fe
# - and audio handling code # - and audio handling code
@ -188,7 +191,7 @@ class MediaManager:
# Checking media # Checking media
########################################################################## ##########################################################################
def check(self) -> pb.CheckMediaOut: def check(self) -> CheckMediaOut:
output = self.col.backend.check_media() output = self.col.backend.check_media()
# files may have been renamed on disk, so an undo at this point could # files may have been renamed on disk, so an undo at this point could
# break file references # break file references

View file

@ -9,17 +9,24 @@ import time
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.backend_pb2 as pb import anki._backend.backend_pb2 as _pb
from anki.consts import * from anki.consts import *
from anki.lang import without_unicode_isolation from anki.errors import NotFoundError
from anki.rsbackend import ( from anki.lang import TR, without_unicode_isolation
TR, from anki.utils import (
NotFoundError, checksum,
StockNoteType,
from_json_bytes, from_json_bytes,
ids2str,
intTime,
joinFields,
splitFields,
to_json_bytes, to_json_bytes,
) )
from anki.utils import checksum, ids2str, intTime, joinFields, splitFields
# public exports
NoteTypeNameID = _pb.NoteTypeNameID
NoteTypeNameIDUseCount = _pb.NoteTypeNameIDUseCount
# types # types
NoteType = Dict[str, Any] NoteType = Dict[str, Any]
@ -121,10 +128,10 @@ class ModelManager:
# Listing note types # Listing note types
############################################################# #############################################################
def all_names_and_ids(self) -> Sequence[pb.NoteTypeNameID]: def all_names_and_ids(self) -> Sequence[NoteTypeNameID]:
return self.col.backend.get_notetype_names() return self.col.backend.get_notetype_names()
def all_use_counts(self) -> Sequence[pb.NoteTypeNameIDUseCount]: def all_use_counts(self) -> Sequence[NoteTypeNameIDUseCount]:
return self.col.backend.get_notetype_names_and_counts() return self.col.backend.get_notetype_names_and_counts()
# legacy # legacy
@ -200,7 +207,7 @@ class ModelManager:
# caller should call save() after modifying # caller should call save() after modifying
nt = from_json_bytes( nt = from_json_bytes(
self.col.backend.get_stock_notetype_legacy( self.col.backend.get_stock_notetype_legacy(
StockNoteType.STOCK_NOTE_TYPE_BASIC _pb.StockNoteType.STOCK_NOTE_TYPE_BASIC
) )
) )
nt["flds"] = [] nt["flds"] = []
@ -293,7 +300,7 @@ class ModelManager:
assert isinstance(name, str) assert isinstance(name, str)
nt = from_json_bytes( nt = from_json_bytes(
self.col.backend.get_stock_notetype_legacy( self.col.backend.get_stock_notetype_legacy(
StockNoteType.STOCK_NOTE_TYPE_BASIC _pb.StockNoteType.STOCK_NOTE_TYPE_BASIC
) )
) )
field = nt["flds"][0] field = nt["flds"][0]
@ -354,7 +361,7 @@ class ModelManager:
def new_template(self, name: str) -> Template: def new_template(self, name: str) -> Template:
nt = from_json_bytes( nt = from_json_bytes(
self.col.backend.get_stock_notetype_legacy( self.col.backend.get_stock_notetype_legacy(
StockNoteType.STOCK_NOTE_TYPE_BASIC _pb.StockNoteType.STOCK_NOTE_TYPE_BASIC
) )
) )
template = nt["tmpls"][0] template = nt["tmpls"][0]
@ -508,5 +515,5 @@ and notes.mid = ? and cards.ord = ?""",
self, m: NoteType, flds: str, allowEmpty: bool = True self, m: NoteType, flds: str, allowEmpty: bool = True
) -> List[int]: ) -> List[int]:
print("_availClozeOrds() is deprecated; use note.cloze_numbers_in_fields()") print("_availClozeOrds() is deprecated; use note.cloze_numbers_in_fields()")
note = anki.rsbackend.BackendNote(fields=[flds]) note = anki._backend.BackendNote(fields=[flds])
return list(self.col.backend.cloze_numbers_in_note(note)) return list(self.col.backend.cloze_numbers_in_note(note))

View file

@ -7,9 +7,9 @@ import pprint
from typing import Any, List, Optional, Sequence, Tuple from typing import Any, List, Optional, Sequence, Tuple
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb
from anki import hooks from anki import hooks
from anki.models import NoteType from anki.models import NoteType
from anki.rsbackend import BackendNote
from anki.utils import joinFields from anki.utils import joinFields
@ -41,7 +41,7 @@ class Note:
assert n assert n
self._load_from_backend_note(n) self._load_from_backend_note(n)
def _load_from_backend_note(self, n: BackendNote) -> None: def _load_from_backend_note(self, n: _pb.Note) -> None:
self.id = n.id self.id = n.id
self.guid = n.guid self.guid = n.guid
self.mid = n.notetype_id self.mid = n.notetype_id
@ -51,9 +51,9 @@ class Note:
self.fields = list(n.fields) self.fields = list(n.fields)
self._fmap = self.col.models.fieldMap(self.model()) self._fmap = self.col.models.fieldMap(self.model())
def to_backend_note(self) -> BackendNote: def to_backend_note(self) -> _pb.Note:
hooks.note_will_flush(self) hooks.note_will_flush(self)
return BackendNote( return _pb.Note(
id=self.id, id=self.id,
guid=self.guid, guid=self.guid,
notetype_id=self.mid, notetype_id=self.mid,

View file

@ -1 +0,0 @@
../../bazel-bin/pylib/anki/rsbackend_gen.py

View file

@ -20,30 +20,25 @@ from typing import (
) )
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki.backend_pb2 as pb import anki._backend.backend_pb2 as _pb
from anki import hooks from anki import hooks
from anki._backend import CountsForDeckToday, FormatTimeSpanContext, SchedTimingToday
from anki.cards import Card from anki.cards import Card
from anki.consts import * from anki.consts import *
from anki.decks import Deck, DeckConfig, DeckManager, QueueConfig from anki.decks import Deck, DeckConfig, DeckManager, DeckTreeNode, QueueConfig
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import ( from anki.utils import from_json_bytes, ids2str, intTime
TR,
CountsForDeckToday,
DeckTreeNode,
FormatTimeSpanContext,
SchedTimingToday,
from_json_bytes,
)
from anki.utils import ids2str, intTime
UnburyCurrentDeckMode = pb.UnburyCardsInCurrentDeckIn.Mode # pylint:disable=no-member CongratsInfoOut = anki._backend.backend_pb2.CongratsInfoOut
BuryOrSuspendMode = pb.BuryOrSuspendCardsIn.Mode # pylint:disable=no-member
UnburyCurrentDeckMode = _pb.UnburyCardsInCurrentDeckIn.Mode # pylint:disable=no-member
BuryOrSuspendMode = _pb.BuryOrSuspendCardsIn.Mode # pylint:disable=no-member
if TYPE_CHECKING: if TYPE_CHECKING:
UnburyCurrentDeckModeValue = ( UnburyCurrentDeckModeValue = (
pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint:disable=no-member _pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint:disable=no-member
) )
BuryOrSuspendModeValue = ( BuryOrSuspendModeValue = (
pb.BuryOrSuspendCardsIn.ModeValue # pylint:disable=no-member _pb.BuryOrSuspendCardsIn.ModeValue # pylint:disable=no-member
) )
# card types: 0=new, 1=lrn, 2=rev, 3=relrn # card types: 0=new, 1=lrn, 2=rev, 3=relrn
@ -1241,7 +1236,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
# Deck finished state # Deck finished state
########################################################################## ##########################################################################
def congratulations_info(self) -> pb.CongratsInfoOut: def congratulations_info(self) -> CongratsInfoOut:
return self.col.backend.congrats_info() return self.col.backend.congrats_info()
def finishedMsg(self) -> str: def finishedMsg(self) -> str:

View file

@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
import anki import anki
from anki.consts import * from anki.consts import *
from anki.rsbackend import TR, FormatTimeSpanContext from anki.lang import TR, FormatTimeSpanContext
from anki.utils import ids2str from anki.utils import ids2str
# Card stats # Card stats

View file

@ -5,12 +5,15 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Callable, List, Tuple from typing import TYPE_CHECKING, Callable, List, Tuple
from anki._backend import StockNoteType
from anki.collection import Collection from anki.collection import Collection
from anki.models import NoteType from anki.models import NoteType
from anki.rsbackend import StockNoteType, from_json_bytes from anki.utils import from_json_bytes
if TYPE_CHECKING: if TYPE_CHECKING:
from anki.backend_pb2 import StockNoteTypeValue # pylint: disable=no-name-in-module from anki._backend.backend_pb2 import ( # pylint: disable=no-name-in-module
StockNoteTypeValue,
)
# add-on authors can add ("note type name", function_like_addBasicModel) # add-on authors can add ("note type name", function_like_addBasicModel)

View file

@ -1,8 +1,15 @@
# 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
#
import anki._backend.backend_pb2 as _pb
# public exports
SyncAuth = _pb.SyncAuth
SyncOutput = _pb.SyncCollectionOut
SyncStatus = _pb.SyncStatusOut
# Legacy attributes some add-ons may be using # Legacy attributes some add-ons may be using
#
from .httpclient import HttpClient from .httpclient import HttpClient

View file

@ -26,7 +26,7 @@ except ImportError as e:
from flask import Response from flask import Response
from anki import Collection from anki import Collection
from anki.backend_pb2 import SyncServerMethodIn from anki._backend.backend_pb2 import SyncServerMethodIn
Method = SyncServerMethodIn.Method # pylint: disable=no-member Method = SyncServerMethodIn.Method # pylint: disable=no-member

View file

@ -16,9 +16,13 @@ import re
from typing import Collection, List, Optional, Sequence, Tuple from typing import Collection, List, Optional, Sequence, Tuple
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
from anki.collection import SearchTerm import anki._backend.backend_pb2 as _pb
import anki.collection
from anki.utils import ids2str from anki.utils import ids2str
# public exports
TagTreeNode = _pb.TagTreeNode
class TagManager: class TagManager:
def __init__(self, col: anki.collection.Collection) -> None: def __init__(self, col: anki.collection.Collection) -> None:
@ -37,6 +41,9 @@ class TagManager:
def allItems(self) -> List[Tuple[str, int]]: def allItems(self) -> List[Tuple[str, int]]:
return [(t.name, t.usn) for t in self.col.backend.all_tags()] return [(t.name, t.usn) for t in self.col.backend.all_tags()]
def tree(self) -> TagTreeNode:
return self.col.backend.tag_tree()
# Registering and fetching tags # Registering and fetching tags
############################################################# #############################################################
@ -87,7 +94,7 @@ class TagManager:
def rename_tag(self, old: str, new: str) -> int: def rename_tag(self, old: str, new: str) -> int:
"Rename provided tag, returning number of changed notes." "Rename provided tag, returning number of changed notes."
nids = self.col.find_notes(SearchTerm(tag=old)) nids = self.col.find_notes(anki.collection.SearchTerm(tag=old))
if not nids: if not nids:
return 0 return 0
escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old) escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old)

View file

@ -32,13 +32,15 @@ from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
import anki import anki
import anki._backend.backend_pb2 as _pb
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.decks import DeckManager from anki.decks import DeckManager
from anki.errors import TemplateError
from anki.models import NoteType from anki.models import NoteType
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import pb, to_json_bytes
from anki.sound import AVTag, SoundOrVideoTag, TTSTag from anki.sound import AVTag, SoundOrVideoTag, TTSTag
from anki.utils import to_json_bytes
CARD_BLANK_HELP = ( CARD_BLANK_HELP = (
"https://anki.tenderapp.com/kb/card-appearance/the-front-of-this-card-is-blank" "https://anki.tenderapp.com/kb/card-appearance/the-front-of-this-card-is-blank"
@ -61,7 +63,7 @@ class PartiallyRenderedCard:
anodes: TemplateReplacementList anodes: TemplateReplacementList
@classmethod @classmethod
def from_proto(cls, out: pb.RenderCardOut) -> PartiallyRenderedCard: def from_proto(cls, out: _pb.RenderCardOut) -> PartiallyRenderedCard:
qnodes = cls.nodes_from_proto(out.question_nodes) qnodes = cls.nodes_from_proto(out.question_nodes)
anodes = cls.nodes_from_proto(out.answer_nodes) anodes = cls.nodes_from_proto(out.answer_nodes)
@ -69,7 +71,7 @@ class PartiallyRenderedCard:
@staticmethod @staticmethod
def nodes_from_proto( def nodes_from_proto(
nodes: Sequence[pb.RenderedTemplateNode], nodes: Sequence[_pb.RenderedTemplateNode],
) -> TemplateReplacementList: ) -> TemplateReplacementList:
results: TemplateReplacementList = [] results: TemplateReplacementList = []
for node in nodes: for node in nodes:
@ -86,7 +88,7 @@ class PartiallyRenderedCard:
return results return results
def av_tag_to_native(tag: pb.AVTag) -> AVTag: def av_tag_to_native(tag: _pb.AVTag) -> AVTag:
val = tag.WhichOneof("value") val = tag.WhichOneof("value")
if val == "sound_or_video": if val == "sound_or_video":
return SoundOrVideoTag(filename=tag.sound_or_video) return SoundOrVideoTag(filename=tag.sound_or_video)
@ -100,7 +102,7 @@ def av_tag_to_native(tag: pb.AVTag) -> AVTag:
) )
def av_tags_to_native(tags: Sequence[pb.AVTag]) -> List[AVTag]: def av_tags_to_native(tags: Sequence[_pb.AVTag]) -> List[AVTag]:
return list(map(av_tag_to_native, tags)) return list(map(av_tag_to_native, tags))
@ -206,7 +208,7 @@ class TemplateRenderContext:
def render(self) -> TemplateRenderOutput: def render(self) -> TemplateRenderOutput:
try: try:
partial = self._partially_render() partial = self._partially_render()
except anki.rsbackend.TemplateError as e: except TemplateError as e:
return TemplateRenderOutput( return TemplateRenderOutput(
question_text=str(e), question_text=str(e),
answer_text=str(e), answer_text=str(e),

View file

@ -26,6 +26,17 @@ from anki.dbproxy import DBProxy
_tmpdir: Optional[str] _tmpdir: Optional[str]
try:
# pylint: disable=c-extension-no-member
import orjson
to_json_bytes = orjson.dumps
from_json_bytes = orjson.loads
except:
print("orjson is missing; DB operations will be slower")
to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore
from_json_bytes = json.loads
# Time handling # Time handling
############################################################################## ##############################################################################

View file

@ -5,8 +5,7 @@ import tempfile
from anki import Collection as aopen from anki import Collection as aopen
from anki.dbproxy import emulate_named_args from anki.dbproxy import emulate_named_args
from anki.lang import without_unicode_isolation from anki.lang import TR, without_unicode_isolation
from anki.rsbackend import TR
from anki.stdmodels import addBasicModel, get_stock_notetypes from anki.stdmodels import addBasicModel, get_stock_notetypes
from anki.utils import isWin from anki.utils import isWin
from tests.shared import assertException, getEmptyCol from tests.shared import assertException, getEmptyCol

View file

@ -1,9 +1,9 @@
# coding: utf-8 # coding: utf-8
import pytest import pytest
from anki._backend import BuiltinSortKind
from anki.collection import ConfigBoolKey from anki.collection import ConfigBoolKey
from anki.consts import * from anki.consts import *
from anki.rsbackend import BuiltinSortKind
from tests.shared import getEmptyCol, isNearCutoff from tests.shared import getEmptyCol, isNearCutoff

View file

@ -2,7 +2,7 @@
import time import time
from anki.consts import MODEL_CLOZE from anki.consts import MODEL_CLOZE
from anki.rsbackend import NotFoundError from anki.errors import NotFoundError
from anki.utils import isWin, stripHTML from anki.utils import isWin, stripHTML
from tests.shared import getEmptyCol from tests.shared import getEmptyCol

View file

@ -22,20 +22,6 @@ py_binary(
], ],
) )
py_binary(
name = "genbackend",
srcs = [
"genbackend.py",
"//pylib/anki:backend_pb2",
],
visibility = ["//pylib:__subpackages__"],
deps = [
requirement("black"),
requirement("stringcase"),
requirement("protobuf"),
],
)
py_library( py_library(
name = "hookslib", name = "hookslib",
srcs = ["hookslib.py"], srcs = ["hookslib.py"],

View file

@ -14,8 +14,8 @@ from typing import Any, Callable, Dict, Optional, Union
import anki.lang import anki.lang
from anki import version as _version from anki import version as _version
from anki._backend import RustBackend
from anki.consts import HELP_SITE from anki.consts import HELP_SITE
from anki.rsbackend import RustBackend
from anki.utils import checksum, isLin, isMac from anki.utils import checksum, isLin, isMac
from aqt.qt import * from aqt.qt import *
from aqt.utils import TR, locale_dir, tr from aqt.utils import TR, locale_dir, tr

View file

@ -13,8 +13,9 @@ from typing import List, Optional, Sequence, Tuple, cast
import aqt import aqt
import aqt.forms import aqt.forms
from anki.cards import Card from anki.cards import Card
from anki.collection import Collection, ConfigBoolKey, InvalidInput, SearchTerm from anki.collection import Collection, ConfigBoolKey, SearchTerm
from anki.consts import * from anki.consts import *
from anki.errors import InvalidInput
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.models import NoteType from anki.models import NoteType
from anki.notes import Note from anki.notes import Note

View file

@ -9,9 +9,9 @@ from typing import Any, Dict, List, Optional
import aqt import aqt
from anki.cards import Card from anki.cards import Card
from anki.consts import * from anki.consts import *
from anki.errors import TemplateError
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import TemplateError
from anki.template import TemplateRenderContext from anki.template import TemplateRenderContext
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *

View file

@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
import aqt import aqt
from anki.rsbackend import DatabaseCheckProgress, ProgressKind from anki.collection import DatabaseCheckProgress, ProgressKind
from aqt.qt import * from aqt.qt import *
from aqt.utils import showText, tooltip from aqt.utils import showText, tooltip

View file

@ -9,8 +9,8 @@ from dataclasses import dataclass
from typing import Any from typing import Any
import aqt import aqt
from anki.decks import DeckTreeNode
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from anki.rsbackend import DeckTreeNode
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player

View file

@ -4,7 +4,8 @@
from typing import List, Optional from typing import List, Optional
import aqt import aqt
from anki.collection import InvalidInput, SearchTerm from anki.collection import SearchTerm
from anki.errors import InvalidInput
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import re import re
import aqt import aqt
from anki.backend_pb2 import EmptyCardsReport, NoteWithEmptyCards from anki.collection import EmptyCardsReport, NoteWithEmptyCards
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import QDialog, QDialogButtonBox, qconnect from aqt.qt import QDialog, QDialogButtonBox, qconnect
from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, tr from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, tr
@ -24,7 +24,7 @@ def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
diag = EmptyCardsDialog(mw, report) diag = EmptyCardsDialog(mw, report)
diag.show() diag.show()
mw.taskman.run_in_background(mw.col.backend.get_empty_cards, on_done) mw.taskman.run_in_background(mw.col.get_empty_cards, on_done)
class EmptyCardsDialog(QDialog): class EmptyCardsDialog(QDialog):

View file

@ -3,9 +3,9 @@
import aqt import aqt
from anki.consts import * from anki.consts import *
from anki.errors import TemplateError
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.models import NoteType from anki.models import NoteType
from anki.rsbackend import TemplateError
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.schema_change_tracker import ChangeTracker from aqt.schema_change_tracker import ChangeTracker

View file

@ -26,11 +26,11 @@ import aqt.stats
import aqt.toolbar import aqt.toolbar
import aqt.webview import aqt.webview
from anki import hooks from anki import hooks
from anki._backend import RustBackend as _RustBackend
from anki.collection import Collection, SearchTerm from anki.collection import Collection, SearchTerm
from anki.decks import Deck from anki.decks import Deck
from anki.hooks import runHook from anki.hooks import runHook
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.rsbackend import RustBackend
from anki.sound import AVTag, SoundOrVideoTag from anki.sound import AVTag, SoundOrVideoTag
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
from aqt import gui_hooks from aqt import gui_hooks
@ -100,7 +100,7 @@ class AnkiQt(QMainWindow):
self, self,
app: QApplication, app: QApplication,
profileManager: ProfileManagerType, profileManager: ProfileManagerType,
backend: RustBackend, backend: _RustBackend,
opts: Namespace, opts: Namespace,
args: List[Any], args: List[Any],
) -> None: ) -> None:

View file

@ -9,8 +9,10 @@ from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar from typing import Iterable, List, Optional, Sequence, TypeVar
import aqt import aqt
from anki.collection import SearchTerm from anki.collection import ProgressKind, SearchTerm
from anki.rsbackend import TR, Interrupted, ProgressKind, pb from anki.errors import Interrupted
from anki.lang import TR
from anki.media import CheckMediaOut
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
askUser, askUser,
@ -74,7 +76,7 @@ class MediaChecker:
self.mw.taskman.run_on_main(lambda: self.mw.progress.update(progress.val)) self.mw.taskman.run_on_main(lambda: self.mw.progress.update(progress.val))
def _check(self) -> pb.CheckMediaOut: def _check(self) -> CheckMediaOut:
"Run the check on a background thread." "Run the check on a background thread."
return self.mw.col.media.check() return self.mw.col.media.check()
@ -87,7 +89,7 @@ class MediaChecker:
if isinstance(exc, Interrupted): if isinstance(exc, Interrupted):
return return
output: pb.CheckMediaOut = future.result() output: CheckMediaOut = future.result()
report = output.report report = output.report
# show report and offer to delete # show report and offer to delete

View file

@ -18,11 +18,10 @@ import flask_cors # type: ignore
from flask import Response, request from flask import Response, request
from waitress.server import create_server from waitress.server import create_server
import anki.backend_pb2 as pb
import aqt import aqt
from anki import hooks from anki import hooks
from anki.rsbackend import from_json_bytes from anki.collection import GraphPreferences
from anki.utils import devMode from anki.utils import devMode, from_json_bytes
from aqt.qt import * from aqt.qt import *
from aqt.utils import aqt_data_folder from aqt.utils import aqt_data_folder
@ -253,22 +252,21 @@ def _redirectWebExports(path):
def graph_data() -> bytes: def graph_data() -> bytes:
args = from_json_bytes(request.data) args = from_json_bytes(request.data)
return aqt.mw.col.backend.graphs(search=args["search"], days=args["days"]) return aqt.mw.col.graph_data(search=args["search"], days=args["days"])
def graph_preferences() -> bytes: def graph_preferences() -> bytes:
return aqt.mw.col.backend.get_graph_preferences() return aqt.mw.col.get_graph_preferences()
def set_graph_preferences() -> None: def set_graph_preferences() -> None:
input = pb.GraphPreferences() prefs = GraphPreferences()
input.ParseFromString(request.data) prefs.ParseFromString(request.data)
aqt.mw.col.backend.set_graph_preferences(input=input) aqt.mw.col.set_graph_preferences(prefs)
def congrats_info() -> bytes: def congrats_info() -> bytes:
info = aqt.mw.col.backend.congrats_info() return aqt.mw.col.congrats_info()
return info.SerializeToString()
post_handlers = { post_handlers = {

View file

@ -9,13 +9,9 @@ from dataclasses import dataclass
from typing import Callable, List, Optional, Union from typing import Callable, List, Optional, Union
import aqt import aqt
from anki.rsbackend import ( from anki.collection import MediaSyncProgress, ProgressKind
TR, from anki.errors import Interrupted, NetworkError
Interrupted, from anki.lang import TR
MediaSyncProgress,
NetworkError,
ProgressKind,
)
from anki.types import assert_exhaustive from anki.types import assert_exhaustive
from anki.utils import intTime from anki.utils import intTime
from aqt import gui_hooks from aqt import gui_hooks

View file

@ -6,11 +6,9 @@ from typing import Any, List, Optional, Sequence
import aqt.clayout import aqt.clayout
from anki import stdmodels from anki import stdmodels
from anki.backend_pb2 import NoteTypeNameIDUseCount
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.models import NoteType from anki.models import NoteType, NoteTypeNameIDUseCount
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import pb
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
@ -51,7 +49,7 @@ class Models(QDialog):
self.form.buttonBox.helpRequested, self.form.buttonBox.helpRequested,
lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE), lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE),
) )
self.models: List[pb.NoteTypeNameIDUseCount] = [] self.models: List[NoteTypeNameIDUseCount] = []
self.setupModels() self.setupModels()
restoreGeom(self, "models") restoreGeom(self, "models")
self.exec_() self.exec_()
@ -111,7 +109,7 @@ class Models(QDialog):
self.saveAndRefresh(nt) self.saveAndRefresh(nt)
def saveAndRefresh(self, nt: NoteType) -> None: def saveAndRefresh(self, nt: NoteType) -> None:
def save() -> Sequence[pb.NoteTypeNameIDUseCount]: def save() -> Sequence[NoteTypeNameIDUseCount]:
self.mm.save(nt) self.mm.save(nt)
return self.col.models.all_use_counts() return self.col.models.all_use_counts()
@ -161,7 +159,7 @@ class Models(QDialog):
nt = self.current_notetype() nt = self.current_notetype()
def save() -> Sequence[pb.NoteTypeNameIDUseCount]: def save() -> Sequence[NoteTypeNameIDUseCount]:
self.mm.rem(nt) self.mm.rem(nt)
return self.col.models.all_use_counts() return self.col.models.all_use_counts()

View file

@ -20,7 +20,7 @@ import aqt.sound
from anki import Collection from anki import Collection
from anki.db import DB from anki.db import DB
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.rsbackend import SyncAuth from anki.sync import SyncAuth
from anki.utils import intTime, isMac, isWin from anki.utils import intTime, isMac, isWin
from aqt import appHelpSite from aqt import appHelpSite
from aqt.qt import * from aqt.qt import *

View file

@ -9,9 +9,10 @@ from enum import Enum
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast
import aqt import aqt
from anki.collection import ConfigBoolKey, InvalidInput, SearchTerm from anki.collection import ConfigBoolKey, SearchTerm
from anki.errors import DeckRenameError from anki.decks import DeckTreeNode
from anki.rsbackend import DeckTreeNode, TagTreeNode from anki.errors import DeckRenameError, InvalidInput
from anki.tags import TagTreeNode
from aqt import gui_hooks from aqt import gui_hooks
from aqt.main import ResetReason from aqt.main import ResetReason
from aqt.models import Models from aqt.models import Models
@ -524,7 +525,7 @@ class SidebarTreeView(QTreeView):
newhead = head + node.name + "::" newhead = head + node.name + "::"
render(item, node.children, newhead) render(item, node.children, newhead)
tree = self.col.backend.tag_tree() tree = self.col.tags.tree()
root = self._section_root( root = self._section_root(
root=root, root=root,
name=TR.BROWSING_SIDEBAR_TAGS, name=TR.BROWSING_SIDEBAR_TAGS,

View file

@ -8,18 +8,10 @@ import os
from typing import Callable, Tuple from typing import Callable, Tuple
import aqt import aqt
from anki.lang import without_unicode_isolation from anki.collection import FullSyncProgress, NormalSyncProgress, ProgressKind
from anki.rsbackend import ( from anki.errors import Interrupted, SyncError
TR, from anki.lang import TR, without_unicode_isolation
FullSyncProgress, from anki.sync import SyncOutput, SyncStatus
Interrupted,
NormalSyncProgress,
ProgressKind,
SyncError,
SyncErrorKind,
SyncOutput,
SyncStatus,
)
from anki.utils import platDesc from anki.utils import platDesc
from aqt.qt import ( from aqt.qt import (
QDialog, QDialog,
@ -69,7 +61,7 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None])
def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception): def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception):
if isinstance(err, SyncError): if isinstance(err, SyncError):
if err.kind() == SyncErrorKind.AUTH_FAILED: if err.is_auth_error():
mw.pm.clear_sync_auth() mw.pm.clear_sync_auth()
elif isinstance(err, Interrupted): elif isinstance(err, Interrupted):
# no message to show # no message to show
@ -249,7 +241,7 @@ def sync_login(
try: try:
auth = fut.result() auth = fut.result()
except SyncError as e: except SyncError as e:
if e.kind() == SyncErrorKind.AUTH_FAILED: if e.is_auth_error():
showWarning(str(e)) showWarning(str(e))
sync_login(mw, on_success, username, password) sync_login(mw, on_success, username, password)
else: else:

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import aqt import aqt
from anki.rsbackend import SyncStatus from anki.sync import SyncStatus
from aqt import gui_hooks from aqt import gui_hooks
from aqt.qt import * from aqt.qt import *
from aqt.sync import get_sync_status from aqt.sync import get_sync_status

View file

@ -14,13 +14,14 @@ from markdown import markdown
import anki import anki
import aqt import aqt
from anki.rsbackend import TR, InvalidInput # pylint: disable=unused-import from anki.errors import InvalidInput
from anki.lang import TR # pylint: disable=unused-import
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
from aqt.qt import * from aqt.qt import *
from aqt.theme import theme_manager from aqt.theme import theme_manager
if TYPE_CHECKING: if TYPE_CHECKING:
from anki.rsbackend import TRValue from anki.lang import TRValue
TextFormat = Union[Literal["plain", "rich"]] TextFormat = Union[Literal["plain", "rich"]]

View file

@ -1,5 +1,5 @@
import anki.lang import anki.lang
from anki.rsbackend import TR from anki.lang import TR
def test_no_collection_i18n(): def test_no_collection_i18n():