Drop Pauker and SuperMemo importers from legacy importer

The legacy importer has only been kept around to support some add-ons,
and these are so infrequently used that they're better off shifted
to add-ons (even they even still work)
This commit is contained in:
Damien Elmes 2025-06-24 13:05:06 +07:00
parent 9b287dc51a
commit 73edf23954
9 changed files with 2 additions and 611 deletions

View file

@ -6,8 +6,6 @@ The following included source code items use a license other than AGPL3:
In the pylib folder: In the pylib folder:
* The SuperMemo importer: GPL3 and 0BSD.
* The Pauker importer: BSD-3.
* statsbg.py: CC BY 4.0. * statsbg.py: CC BY 4.0.
In the qt folder: In the qt folder:

View file

@ -65,7 +65,6 @@ importing-with-deck-configs-help =
If enabled, any deck options that the deck sharer included will also be imported. If enabled, any deck options that the deck sharer included will also be imported.
Otherwise, all decks will be assigned the default preset. Otherwise, all decks will be assigned the default preset.
importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip) importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
# the '|' character # the '|' character
importing-pipe = Pipe importing-pipe = Pipe
# Warning displayed when the csv import preview table is clipped (some columns were hidden) # Warning displayed when the csv import preview table is clipped (some columns were hidden)
@ -78,7 +77,6 @@ importing-rows-had-num1d-fields-expected-num2d = '{ $row }' had { $found } field
importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual. importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual.
importing-semicolon = Semicolon importing-semicolon = Semicolon
importing-skipped = Skipped importing-skipped = Skipped
importing-supermemo-xml-export-xml = Supermemo XML export (*.xml)
importing-tab = Tab importing-tab = Tab
importing-tag-modified-notes = Tag modified notes: importing-tag-modified-notes = Tag modified notes:
importing-text-separated-by-tabs-or-semicolons = Text separated by tabs or semicolons (*) importing-text-separated-by-tabs-or-semicolons = Text separated by tabs or semicolons (*)
@ -252,3 +250,5 @@ importing-importing-collection = Importing collection...
importing-unable-to-import-filename = Unable to import { $filename }: file type not supported importing-unable-to-import-filename = Unable to import { $filename }: file type not supported
importing-notes-that-could-not-be-imported = Notes that could not be imported as note type has changed: { $val } importing-notes-that-could-not-be-imported = Notes that could not be imported as note type has changed: { $val }
importing-added = Added importing-added = Added
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
importing-supermemo-xml-export-xml = Supermemo XML export (*.xml)

View file

@ -11,8 +11,6 @@ from anki.importing.apkg import AnkiPackageImporter
from anki.importing.base import Importer from anki.importing.base import Importer
from anki.importing.csvfile import TextImporter 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.supermemo_xml import SupermemoXmlImporter # type: ignore
from anki.lang import TR from anki.lang import TR
@ -24,8 +22,6 @@ def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]:
AnkiPackageImporter, AnkiPackageImporter,
), ),
(col.tr.importing_mnemosyne_20_deck_db(), MnemosyneImporter), (col.tr.importing_mnemosyne_20_deck_db(), MnemosyneImporter),
(col.tr.importing_supermemo_xml_export_xml(), SupermemoXmlImporter),
(col.tr.importing_pauker_18_lesson_paugz(), PaukerImporter),
] ]
anki.hooks.importing_importers(importers) anki.hooks.importing_importers(importers)
return importers return importers

View file

@ -1,94 +0,0 @@
# Copyright: Andreas Klauer <Andreas.Klauer@metamorpher.de>
# License: BSD-3
# pylint: disable=invalid-name
import gzip
import html
import math
import random
import time
import xml.etree.ElementTree as ET
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
from anki.stdmodels import _legacy_add_forward_reverse
ONE_DAY = 60 * 60 * 24
class PaukerImporter(NoteImporter):
"""Import Pauker 1.8 Lesson (*.pau.gz)"""
needMapper = False
allowHTML = True
def run(self):
model = _legacy_add_forward_reverse(self.col)
model["name"] = "Pauker"
self.col.models.save(model, updateReqs=False)
self.col.models.set_current(model)
self.model = model
self.initMapping()
NoteImporter.run(self)
def fields(self):
"""Pauker is Front/Back"""
return 2
def foreignNotes(self):
"""Build and return a list of notes."""
notes = []
try:
f = gzip.open(self.file)
tree = ET.parse(f) # type: ignore
lesson = tree.getroot()
assert lesson.tag == "Lesson"
finally:
f.close()
index = -4
for batch in lesson.findall("./Batch"):
index += 1
for card in batch.findall("./Card"):
# Create a note for this card.
front = card.findtext("./FrontSide/Text")
back = card.findtext("./ReverseSide/Text")
note = ForeignNote()
assert front and back
note.fields = [
html.escape(x.strip())
.replace("\n", "<br>")
.replace(" ", " &nbsp;")
for x in [front, back]
]
notes.append(note)
# Determine due date for cards.
frontdue = card.find("./FrontSide[@LearnedTimestamp]")
backdue = card.find("./ReverseSide[@Batch][@LearnedTimestamp]")
if frontdue is not None:
note.cards[0] = self._learnedCard(
index, int(frontdue.attrib["LearnedTimestamp"])
)
if backdue is not None:
note.cards[1] = self._learnedCard(
int(backdue.attrib["Batch"]),
int(backdue.attrib["LearnedTimestamp"]),
)
return notes
def _learnedCard(self, batch, timestamp):
ivl = math.exp(batch)
now = time.time()
due = ivl - (now - timestamp / 1000.0) / ONE_DAY
fc = ForeignCard()
fc.due = self.col.sched.today + int(due + 0.5)
fc.ivl = random.randint(int(ivl * 0.90), int(ivl + 0.5))
fc.factor = random.randint(1500, 2500)
return fc

View file

@ -1,484 +0,0 @@
# Copyright: petr.michalec@gmail.com
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# pytype: disable=attribute-error
# type: ignore
# pylint: disable=C
from __future__ import annotations
import re
import sys
import time
import unicodedata
from string import capwords
from xml.dom import minidom
from xml.dom.minidom import Element, Text
from anki.collection import Collection
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
from anki.stdmodels import _legacy_add_basic_model
class SmartDict(dict):
"""
See http://www.peterbe.com/plog/SmartDict
Copyright 2005, Peter Bengtsson, peter@fry-it.com
0BSD
A smart dict can be instantiated either from a pythonic dict
or an instance object (eg. SQL recordsets) but it ensures that you can
do all the convenient lookups such as x.first_name, x['first_name'] or
x.get('first_name').
"""
def __init__(self, *a, **kw) -> None:
if a:
if isinstance(type(a[0]), dict):
kw.update(a[0])
elif isinstance(type(a[0]), object):
kw.update(a[0].__dict__)
elif hasattr(a[0], "__class__") and a[0].__class__.__name__ == "SmartDict":
kw.update(a[0].__dict__)
dict.__init__(self, **kw)
self.__dict__ = self
class SuperMemoElement(SmartDict):
"SmartDict wrapper to store SM Element data"
def __init__(self, *a, **kw) -> None:
SmartDict.__init__(self, *a, **kw)
# default content
self.__dict__["lTitle"] = None
self.__dict__["Title"] = None
self.__dict__["Question"] = None
self.__dict__["Answer"] = None
self.__dict__["Count"] = None
self.__dict__["Type"] = None
self.__dict__["ID"] = None
self.__dict__["Interval"] = None
self.__dict__["Lapses"] = None
self.__dict__["Repetitions"] = None
self.__dict__["LastRepetiton"] = None
self.__dict__["AFactor"] = None
self.__dict__["UFactor"] = None
# This is an AnkiImporter
class SupermemoXmlImporter(NoteImporter):
needMapper = False
allowHTML = True
"""
Supermemo XML export's to Anki parser.
Goes through a SM collection and fetch all elements.
My SM collection was a big mess where topics and items were mixed.
I was unable to parse my content in a regular way like for loop on
minidom.getElementsByTagName() etc. My collection had also an
limitation, topics were splited into branches with max 100 items
on each. Learning themes were in deep structure. I wanted to have
full title on each element to be stored in tags.
Code should be upgrade to support importing of SM2006 exports.
"""
def __init__(self, col: Collection, file: str) -> None:
"""Initialize internal variables.
Pameters to be exposed to GUI are stored in self.META"""
NoteImporter.__init__(self, col, file)
m = _legacy_add_basic_model(self.col)
m["name"] = "Supermemo"
self.col.models.save(m)
self.initMapping()
self.lines = None
self.numFields = int(2)
# SmXmlParse VARIABLES
self.xmldoc = None
self.pieces = []
self.cntBuf = [] # to store last parsed data
self.cntElm = [] # to store SM Elements data
self.cntCol = [] # to store SM Colections data
# store some meta info related to parse algorithm
# SmartDict works like dict / class wrapper
self.cntMeta = SmartDict()
self.cntMeta.popTitles = False
self.cntMeta.title = []
# META stores controls of import script, should be
# exposed to import dialog. These are default values.
self.META = SmartDict()
self.META.resetLearningData = False # implemented
self.META.onlyMemorizedItems = False # implemented
self.META.loggerLevel = 2 # implemented 0no,1info,2error,3debug
self.META.tagAllTopics = True
self.META.pathsToBeTagged = [
"English for beginners",
"Advanced English 97",
"Phrasal Verbs",
] # path patterns to be tagged - in gui entered like 'Advanced English 97|My Vocablary'
self.META.tagMemorizedItems = True # implemented
self.META.logToStdOutput = False # implemented
self.notes = []
## TOOLS
def _fudgeText(self, text: str) -> str:
"Replace sm syntax to Anki syntax"
text = text.replace("\n\r", "<br>")
text = text.replace("\n", "<br>")
return text
def _unicode2ascii(self, str: str) -> str:
"Remove diacritic punctuation from strings (titles)"
return "".join(
[
c
for c in unicodedata.normalize("NFKD", str)
if not unicodedata.combining(c)
]
)
def _decode_htmlescapes(self, html: str) -> str:
"""Unescape HTML code."""
# In case of bad formatted html you can import MinimalSoup etc.. see BeautifulSoup source code
from bs4 import BeautifulSoup
# my sm2004 also ecaped & char in escaped sequences.
html = re.sub("&amp;", "&", html)
# https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx
if html.find(">") < 0:
return html
# unescaped solitary chars < or > that were ok for minidom confuse btfl soup
# html = re.sub(u'>',u'&gt;',html)
# html = re.sub(u'<',u'&lt;',html)
return str(BeautifulSoup(html, "html.parser"))
def _afactor2efactor(self, af: float) -> float:
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
# Ranges for A-factors and E-factors
af_min = 1.2
af_max = 6.9
ef_min = 1.3
ef_max = 3.3
# Sanity checks for the A-factor
if af < af_min:
af = af_min
elif af > af_max:
af = af_max
# Scale af to the range 0..1
af_scaled = (af - af_min) / (af_max - af_min)
# Rescale to the interval ef_min..ef_max
ef = ef_min + af_scaled * (ef_max - ef_min)
return ef
## DEFAULT IMPORTER METHODS
def foreignNotes(self) -> list[ForeignNote]:
# Load file and parse it by minidom
self.loadSource(self.file)
# Migrating content / time consuming part
# addItemToCards is called for each sm element
self.logger("Parsing started.")
self.parse()
self.logger("Parsing done.")
# Return imported cards
self.total = len(self.notes)
self.log.append("%d cards imported." % self.total)
return self.notes
def fields(self) -> int:
return 2
## PARSER METHODS
def addItemToCards(self, item: SuperMemoElement) -> None:
"This method actually do conversion"
# new anki card
note = ForeignNote()
# clean Q and A
note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Question)))
note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Answer)))
note.tags = []
# pre-process scheduling data
# convert learning data
if (
not self.META.resetLearningData
and int(item.Interval) >= 1
and getattr(item, "LastRepetition", None)
):
# migration of LearningData algorithm
tLastrep = time.mktime(time.strptime(item.LastRepetition, "%d.%m.%Y"))
tToday = time.time()
card = ForeignCard()
card.ivl = int(item.Interval)
card.lapses = int(item.Lapses)
card.reps = int(item.Repetitions) + int(item.Lapses)
nextDue = tLastrep + (float(item.Interval) * 86400.0)
remDays = int((nextDue - time.time()) / 86400)
card.due = self.col.sched.today + remDays
card.factor = int(
self._afactor2efactor(float(item.AFactor.replace(",", "."))) * 1000
)
note.cards[0] = card
# categories & tags
# it's worth to have every theme (tree structure of sm collection) stored in tags, but sometimes not
# you can deceide if you are going to tag all toppics or just that containing some pattern
tTaggTitle = False
for pattern in self.META.pathsToBeTagged:
if (
item.lTitle is not None
and pattern.lower() in " ".join(item.lTitle).lower()
):
tTaggTitle = True
break
if tTaggTitle or self.META.tagAllTopics:
# normalize - remove diacritic punctuation from unicode chars to ascii
item.lTitle = [self._unicode2ascii(topic) for topic in item.lTitle]
# Transform xyz / aaa / bbb / ccc on Title path to Tag xyzAaaBbbCcc
# clean things like [999] or [111-2222] from title path, example: xyz / [1000-1200] zyx / xyz
# clean whitespaces
# set Capital letters for first char of the word
tmp = list(
{re.sub(r"(\[[0-9]+\])", " ", i).replace("_", " ") for i in item.lTitle}
)
tmp = list({re.sub(r"(\W)", " ", i) for i in tmp})
tmp = list({re.sub("^[0-9 ]+$", "", i) for i in tmp})
tmp = list({capwords(i).replace(" ", "") for i in tmp})
tags = [j[0].lower() + j[1:] for j in tmp if j.strip() != ""]
note.tags += tags
if self.META.tagMemorizedItems and int(item.Interval) > 0:
note.tags.append("Memorized")
self.logger("Element tags\t- " + repr(note.tags), level=3)
self.notes.append(note)
def logger(self, text: str, level: int = 1) -> None:
"Wrapper for Anki logger"
dLevels = {0: "", 1: "Info", 2: "Verbose", 3: "Debug"}
if level <= self.META.loggerLevel:
# self.deck.updateProgress(_(text))
if self.META.logToStdOutput:
print(
self.__class__.__name__
+ " - "
+ dLevels[level].ljust(9)
+ " -\t"
+ text
)
# OPEN AND LOAD
def openAnything(self, source):
"""Open any source / actually only opening of files is used
@return an open handle which must be closed after use, i.e., handle.close()"""
if source == "-":
return sys.stdin
# try to open with urllib (if source is http, ftp, or file URL)
import urllib.error
import urllib.parse
import urllib.request
try:
return urllib.request.urlopen(source)
except OSError:
pass
# try to open with native open function (if source is pathname)
try:
return open(source, encoding="utf8")
except OSError:
pass
# treat source as string
import io
return io.StringIO(str(source))
def loadSource(self, source: str) -> None:
"""Load source file and parse with xml.dom.minidom"""
self.source = source
self.logger("Load started...")
sock = open(self.source, encoding="utf8")
self.xmldoc = minidom.parse(sock).documentElement
sock.close()
self.logger("Load done.")
# PARSE
def parse(self, node: Text | Element | None = None) -> None:
"Parse method - parses document elements"
if node is None and self.xmldoc is not None:
node = self.xmldoc
_method = "parse_%s" % node.__class__.__name__
if hasattr(self, _method):
parseMethod = getattr(self, _method)
parseMethod(node)
else:
self.logger("No handler for method %s" % _method, level=3)
def parse_Document(self, node):
"Parse XML document"
self.parse(node.documentElement)
def parse_Element(self, node: Element) -> None:
"Parse XML element"
_method = "do_%s" % node.tagName
if hasattr(self, _method):
handlerMethod = getattr(self, _method)
handlerMethod(node)
else:
self.logger("No handler for method %s" % _method, level=3)
# print traceback.print_exc()
def parse_Text(self, node: Text) -> None:
"Parse text inside elements. Text is stored into local buffer."
text = node.data
self.cntBuf.append(text)
# def parse_Comment(self, node):
# """
# Source can contain XML comments, but we ignore them
# """
# pass
# DO
def do_SuperMemoCollection(self, node: Element) -> None:
"Process SM Collection"
for child in node.childNodes:
self.parse(child)
def do_SuperMemoElement(self, node: Element) -> None:
"Process SM Element (Type - Title,Topics)"
self.logger("=" * 45, level=3)
self.cntElm.append(SuperMemoElement())
self.cntElm[-1]["lTitle"] = self.cntMeta["title"]
# parse all child elements
for child in node.childNodes:
self.parse(child)
# strip all saved strings, just for sure
for key in list(self.cntElm[-1].keys()):
if hasattr(self.cntElm[-1][key], "strip"):
self.cntElm[-1][key] = self.cntElm[-1][key].strip()
# pop current element
smel = self.cntElm.pop()
# Process cntElm if is valid Item (and not an Topic etc..)
# if smel.Lapses != None and smel.Interval != None and smel.Question != None and smel.Answer != None:
if smel.Title is None and smel.Question is not None and smel.Answer is not None:
if smel.Answer.strip() != "" and smel.Question.strip() != "":
# migrate only memorized otherway skip/continue
if self.META.onlyMemorizedItems and not (int(smel.Interval) > 0):
self.logger("Element skipped \t- not memorized ...", level=3)
else:
# import sm element data to Anki
self.addItemToCards(smel)
self.logger("Import element \t- " + smel["Question"], level=3)
# print element
self.logger("-" * 45, level=3)
for key in list(smel.keys()):
self.logger(
"\t{} {}".format((key + ":").ljust(15), smel[key]), level=3
)
else:
self.logger("Element skipped \t- no valid Q and A ...", level=3)
else:
# now we know that item was topic
# parsing of whole node is now finished
# test if it's really topic
if smel.Title is not None:
# remove topic from title list
t = self.cntMeta["title"].pop()
self.logger("End of topic \t- %s" % (t), level=2)
def do_Content(self, node: Element) -> None:
"Process SM element Content"
for child in node.childNodes:
if hasattr(child, "tagName") and child.firstChild is not None:
self.cntElm[-1][child.tagName] = child.firstChild.data
def do_LearningData(self, node: Element) -> None:
"Process SM element LearningData"
for child in node.childNodes:
if hasattr(child, "tagName") and child.firstChild is not None:
self.cntElm[-1][child.tagName] = child.firstChild.data
# It's being processed in do_Content now
# def do_Question(self, node):
# for child in node.childNodes: self.parse(child)
# self.cntElm[-1][node.tagName]=self.cntBuf.pop()
# It's being processed in do_Content now
# def do_Answer(self, node):
# for child in node.childNodes: self.parse(child)
# self.cntElm[-1][node.tagName]=self.cntBuf.pop()
def do_Title(self, node: Element) -> None:
"Process SM element Title"
t = self._decode_htmlescapes(node.firstChild.data)
self.cntElm[-1][node.tagName] = t
self.cntMeta["title"].append(t)
self.cntElm[-1]["lTitle"] = self.cntMeta["title"]
self.logger("Start of topic \t- " + " / ".join(self.cntMeta["title"]), level=2)
def do_Type(self, node: Element) -> None:
"Process SM element Type"
if len(self.cntBuf) >= 1:
self.cntElm[-1][node.tagName] = self.cntBuf.pop()
# if __name__ == '__main__':
# for testing you can start it standalone
# file = u'/home/epcim/hg2g/dev/python/sm2anki/ADVENG2EXP.xxe.esc.zaloha_FINAL.xml'
# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_noOEM.xml'
# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_oem_1250.xml'
# file = str(sys.argv[1])
# impo = SupermemoXmlImporter(Deck(),file)
# impo.foreignCards()
# sys.exit(1)
# vim: ts=4 sts=2 ft=python

View file

@ -4,7 +4,6 @@ dynamic = ["version"]
requires-python = ">=3.9" requires-python = ">=3.9"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
dependencies = [ dependencies = [
"beautifulsoup4",
"decorator", "decorator",
"markdown", "markdown",
"orjson", "orjson",

View file

@ -13,7 +13,6 @@ from anki.importing import (
Anki2Importer, Anki2Importer,
AnkiPackageImporter, AnkiPackageImporter,
MnemosyneImporter, MnemosyneImporter,
SupermemoXmlImporter,
TextImporter, TextImporter,
) )
from tests.shared import getEmptyCol, getUpgradeDeckPath from tests.shared import getEmptyCol, getUpgradeDeckPath
@ -306,22 +305,6 @@ def test_csv_tag_only_if_modified():
col.close() col.close()
@pytest.mark.filterwarnings("ignore:Using or importing the ABCs")
def test_supermemo_xml_01_unicode():
col = getEmptyCol()
file = str(os.path.join(testDir, "support", "supermemo1.xml"))
i = SupermemoXmlImporter(col, file)
# i.META.logToStdOutput = True
i.run()
assert i.total == 1
cid = col.db.scalar("select id from cards")
c = col.get_card(cid)
# Applies A Factor-to-E Factor conversion
assert c.factor == 2879
assert c.reps == 7
col.close()
def test_mnemo(): def test_mnemo():
col = getEmptyCol() col = getEmptyCol()
file = str(os.path.join(testDir, "support", "mnemo.db")) file = str(os.path.join(testDir, "support", "mnemo.db"))

View file

@ -21,12 +21,7 @@ use walkdir::WalkDir;
const NONSTANDARD_HEADER: &[&str] = &[ const NONSTANDARD_HEADER: &[&str] = &[
"./pylib/anki/_vendor/stringcase.py", "./pylib/anki/_vendor/stringcase.py",
"./pylib/anki/importing/pauker.py",
"./pylib/anki/importing/supermemo_xml.py",
"./pylib/anki/statsbg.py", "./pylib/anki/statsbg.py",
"./pylib/tools/protoc-gen-mypy.py",
"./python/pyqt/install.py",
"./python/write_wheel.py",
"./qt/aqt/mpv.py", "./qt/aqt/mpv.py",
"./qt/aqt/winpaths.py", "./qt/aqt/winpaths.py",
]; ];

View file

@ -51,7 +51,6 @@ wheels = [
name = "anki" name = "anki"
source = { editable = "pylib" } source = { editable = "pylib" }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" },
{ name = "decorator" }, { name = "decorator" },
{ name = "distro", marker = "(sys_platform != 'darwin' and sys_platform != 'win32') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" }, { name = "distro", marker = "(sys_platform != 'darwin' and sys_platform != 'win32') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" },
{ name = "markdown" }, { name = "markdown" },
@ -64,7 +63,6 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "beautifulsoup4" },
{ name = "decorator" }, { name = "decorator" },
{ name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, { name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" },
{ name = "markdown" }, { name = "markdown" },