Merge pull request #119 from ankicommunity/release/v2.3.0

This commit is contained in:
Vikash Kothary 2022-01-16 23:03:12 +00:00 committed by GitHub
commit a8c91b449c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1639 additions and 848 deletions

43
Makefile Normal file → Executable file
View File

@ -1,26 +1,43 @@
#/bin/make #!/usr/bin/env make
ANKISYNCD_NAME ?= Anki Sync Server
ANKISYNCD_VERSION ?= v2.3.0
ANKISYNCD_DESCRIPTION ?= Self-hosted Anki Sync Server.
ANKI_SERVER_NAME ?= "Anki Sync Server"
ANKI_SERVER_VERSION ?= "v0.1.0"
ANKI_SERVER_DESCRIPTION ?= "Self-hosted Anki Sync Server."
ENV ?= local ENV ?= local
-include config/.env.${ENV} -include config/.env.${ENV}
-include config/secrets/.env.*.${ENV}
export export
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.PHONY: help #: Display list of command and exit. .PHONY: help #: Display list of command and exit.
help: help:
@awk 'BEGIN {FS = " ?#?: "; print ""${ANKI_SERVER_NAME}" "${ANKI_SERVER_VERSION}"\n"${ANKI_SERVER_DESCRIPTION}"\n\nUsage: make \033[36m<command>\033[0m\n\nCommands:"} /^.PHONY: ?[a-zA-Z_-]/ { printf " \033[36m%-10s\033[0m %s\n", $$2, $$3 }' $(MAKEFILE_LIST) @${AWK} 'BEGIN {FS = " ?#?: "; print "${ANKISYNCD_NAME} ${ANKISYNCD_VERSION}\n${ANKISYNCD_DESCRIPTION}\n\nUsage: make \033[36m<command>\033[0m\n\nCommands:"} /^.PHONY: ?[a-zA-Z_-]/ { printf " \033[36m%-10s\033[0m %s\n", $$2, $$3 }' $(MAKEFILE_LIST)
.PHONY: docs #: Build and serve documentation. .PHONY: docs #: Build and serve documentation.
docs: print-env docs:
@${MKDOCS} ${MKDOCS_OPTION} -f docs/mkdocs.yml @${MKDOCS} ${MKDOCS_CMD} -f docs/mkdocs.yml ${MKDOCS_OPTS}
.PHONY: notebooks #: Run jupyter notebooks. .PHONY: tests #: Run unit tests.
notebooks: tests:
@${JUPYTER} ${JUPYTER_OPTION} @${UNITTEST} discover -s tests
.PHONY: run
run:
@${PYTHON} src/ankisyncd/__main__.py
# Run scripts using make
%: %:
@test -f scripts/${*}.sh @if [[ -f "scripts/${*}.sh" ]]; then \
@${SHELL} scripts/${*}.sh ${BASH} "scripts/${*}.sh"; fi
.PHONY: init #: Download Python dependencies.
init:
@${POETRY} install
.PHONY: release #: Create new Git release and tags.
release: release-branch release-tags
.PHONY: open
open:
@${OPEN} ${ANKISYNCD_URL}

View File

@ -29,6 +29,8 @@ It supports Python 3 and Anki 2.1.
- [Anki 2.1](#anki-21) - [Anki 2.1](#anki-21)
- [Anki 2.0](#anki-20) - [Anki 2.0](#anki-20)
- [AnkiDroid](#ankidroid) - [AnkiDroid](#ankidroid)
- [Development](#development)
- [Testing](#testing)
- [ENVVAR configuration overrides](#envvar-configuration-overrides) - [ENVVAR configuration overrides](#envvar-configuration-overrides)
- [Support for other database backends](#support-for-other-database-backends) - [Support for other database backends](#support-for-other-database-backends)
</details> </details>
@ -59,34 +61,46 @@ Installing
requests to ankisyncd. requests to ankisyncd.
An example configuration with ankisyncd running on the same machine as Nginx An example configuration with ankisyncd running on the same machine as Nginx
and listening on port `27702` may look like: and listening on port `27702` may look like ([entire config template click me](https://github.com/ankicommunity/anki-sync-server/blob/develop/docs/nginx.conf)):
``` ```nginx
server { server {
listen 27701; listen 27701;
server_name default; server_name default;
location / { location / {
proxy_http_version 1.0; proxy_http_version 1.0;
proxy_pass http://localhost:27702/; proxy_pass http://127.0.0.1:27702/;
} }
} }
``` ```
5. Run ankisyncd: 5. Run ankisyncd:
```
$ python -m ankisyncd $ python -m ankisyncd
```
--- ---
Installing (Docker) Installing (Docker)
------------------- -------------------
Follow [these instructions](https://github.com/kuklinistvan/docker-anki-sync-server#usage). Follow [these instructions](https://github.com/ankicommunity/anki-devops-services#about-this-docker-image).
Setting up Anki Setting up Anki
--------------- ---------------
### Install addon from ankiweb (support 2.1)
1.on add-on window,click `Get Add-ons` and fill in the textbox with the code `358444159`
2.there,you get add-on `custom sync server redirector`,choose it.Then click `config` below right
3.apply your server ip address
if this step is taken,the following instructions regarding addon setting 2.1( including 2.1.28 and above) can be skipped.
### Anki 2.1.28 and above ### Anki 2.1.28 and above
Create a new directory in [the add-ons folder][addons21] (name it something Create a new directory in [the add-ons folder][addons21] (name it something
@ -142,6 +156,35 @@ Even though the AnkiDroid interface will request an email address, this is not
required; it will simply be the username you configured with `ankisyncctl.py required; it will simply be the username you configured with `ankisyncctl.py
adduser`. adduser`.
Development
-----------
### Testing
0. Prerequites
This project uses [GNU Make](https://www.gnu.org/software/make/) to simplify the development commands. It also uses [Poetry](https://python-poetry.org/) to manage the Python dependencies. Ensure they are installed.
1. Create a config for your local environment.
```bash
$ cp config/.env.example config/.env.local
```
See [ENVVAR configuration overrides](#envvar-configuration-overrides) for more information.
2. Download Python dependencies.
```bash
$ make init
```
3. Run unit tests.
```bash
$ make tests
```
ENVVAR configuration overrides ENVVAR configuration overrides
------------------------------ ------------------------------

View File

@ -1,8 +1,5 @@
# .env.example (anki-sync-server) # file: .env.example (ankisyncd)
# cp config/.env.example config/.env.local
## Make
MKDOCS=mkdocs
JUPYTER=jupyter
## Ankisyncd ## Ankisyncd
ANKISYNCD_HOST=0.0.0.0 ANKISYNCD_HOST=0.0.0.0
@ -12,17 +9,31 @@ ANKISYNCD_BASE_URL=/sync/
ANKISYNCD_BASE_MEDIA_URL=/msync/ ANKISYNCD_BASE_MEDIA_URL=/msync/
ANKISYNCD_AUTH_DB_PATH=./auth.db ANKISYNCD_AUTH_DB_PATH=./auth.db
ANKISYNCD_SESSION_DB_PATH=./session.db ANKISYNCD_SESSION_DB_PATH=./session.db
### ANKISYNCD_FULL_SYNC_MANAGER
ANKISYNCD_FULL_SYNC_MANAGER ### ANKISYNCD_SESSION_MANAGER
ANKISYNCD_SESSION_MANAGER ### ANKISYNCD_USER_MANAGER
ANKISYNCD_USER_MANAGER ### ANKISYNCD_COLLECTION_WRAPPER
ANKISYNCD_COLLECTION_WRAPPER ANKISYNCD_URL=http://${ANKISYNCD_HOST}:${ANKISYNCD_PORT}
## Mkdocs ## Mkdocs
MKDOCS_OPTION=serve MKDOCS_HOST=localhost
MKDOCS_PORT=5000
MKDOCS_OPTS=-a ${MKDOCS_HOST}:${MKDOCS_PORT}
MKDOCS_CMD=serve
## Jupyter ## Jupyter
JUPYTER_OPTION=lab ### JUPYTER_CMD
### JUPYTER_NOTEBOOK_DIR
## Path ## Make
PATH:=.venv/bin/path:${PATH} AWK=awk
BASH=bash
POETRY=poetry
PYTHON=python
MKDOCS=mkdocs
JUPYTER=jupyter
UNITTEST=python -m unittest
## Shell
SHELL=${BASH}
PATH:=.venv/bin:${PATH}

View File

@ -7,3 +7,5 @@ site_url: https://ankicommunity.github.io/anki-sync-server
repo_url: https://github.com/ankicommunity/anki-sync-server repo_url: https://github.com/ankicommunity/anki-sync-server
docs_dir: src docs_dir: src
site_dir: build site_dir: build
plugins:
- mkdocs-jupyter

View File

@ -0,0 +1,29 @@
# file: nginx.example.conf
# description: Example nginx.conf to set up a reverse proxy.
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
client_max_body_size 2048m;
server {
listen 27701;
# server_name should be modified (LAN eg: 192.168.1.43 )
server_name default;
location / {
proxy_http_version 1.0;
proxy_pass http://127.0.0.1:27702/;
}
}
}

View File

@ -149,13 +149,6 @@
"source": [ "source": [
"col.close()" "col.close()"
] ]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
} }
], ],
"metadata": { "metadata": {

1876
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,17 @@ name = "anki-sync-server"
version = "2.3.0" version = "2.3.0"
description = "Self-hosted Anki Sync Server." description = "Self-hosted Anki Sync Server."
authors = ["Vikash Kothary <kothary.vikash@gmail.com>"] authors = ["Vikash Kothary <kothary.vikash@gmail.com>"]
packages = [
{ include = "ankisyncd", from = "src" }
]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
anki = "^2.1.36" anki = "2.1.43"
beautifulsoup4 = "^4.9.1" beautifulsoup4 = "^4.9.1"
requests = "^2.24.0" requests = "^2.24.0"
markdown = "^3.2.2" markdown = "^3.2.2"
send2trash = "^1.5.0" send2trash = "^1.5.0"
pyaudio = "^0.2.11"
decorator = "^4.4.2" decorator = "^4.4.2"
psutil = "^5.7.2" psutil = "^5.7.2"
distro = "^1.5.0" distro = "^1.5.0"
@ -21,6 +23,8 @@ webob = "^1.8.6"
mkdocs = "^1.1.2" mkdocs = "^1.1.2"
jupyter = "^1.0.0" jupyter = "^1.0.0"
jupyterlab = "^2.2.2" jupyterlab = "^2.2.2"
webtest = "^2.0.35"
mkdocs-jupyter = "^0.19.0"
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry>=0.12"]

7
scripts/jupyter.sh Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# file: jupyter.sh
# description: Run Jupyter Notebooks.
[[ ! -z "${JUPYTER_CMD}" ]] || JUPYTER_CMD=lab
[[ ! -z "${JUPYTER_NOTEBOOK_DIR}" ]] || JUPYTER_NOTEBOOK_DIR=docs/src/notebooks
${JUPYTER} ${JUPYTER_CMD} --notebook-dir=${JUPYTER_NOTEBOOK_DIR}

View File

@ -1,13 +0,0 @@
#!/bin/bash
# file: lock.sh
# description: Lock dependencies and export requirements.
echo "THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!\n\n" > src/requirements.txt
echo "THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!\n\n" > src/requirements-dev.txt
poetry lock
poetry export --without-hashes -f requirements.txt >> src/requirements.txt
poetry export --dev --without-hashes -f requirements.txt >> src/requirements-dev.txt
echo "-e src/." >> src/requirements-dev.txt

15
scripts/poetry-export.sh Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
# file: poetry-export.sh
# description: Lock dependencies and export them as backward-compatible requirements.txt files.
echo '[INFO] Updating poetry.lock file.'
poetry lock
echo '[INFO] Generating requirements.txt.'
echo -e "# THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!\n\n" > src/requirements.txt
poetry export --without-hashes -f requirements.txt >> src/requirements.txt
echo '[INFO] Generating requirements-dev.txt.'
echo -e "# THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!\n\n" > src/requirements-dev.txt
poetry export --dev --without-hashes -f requirements.txt >> src/requirements-dev.txt
echo "-e src/." >> src/requirements-dev.txt

View File

@ -1,5 +0,0 @@
#!/bin/bash
# file: print-env.sh
# description: Print env variable.
echo "${ENV}"

5
scripts/printenv.sh Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
# file: printenv.sh
# description: Print all environment variables. Used for debugging.
printenv | sort

39
scripts/release-branch.sh Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env bash
# file: release-branch.sh
# description: Prepare a release branch from develop to master.
## Build release context as environment variables.
GIT_BRANCH=$(git symbolic-ref --short HEAD)
## TODO: get package version from pyproject.toml
CURRENT_VERSION=v2.2.0
## TODO: get new package version e.g. minor, major, bugfix
NEW_VERSION=v2.3.0
## TODO: ensure you're on the develop branch else fail
if [[ "${GIT_BRANCH}" != "develop" ]]; then
echo 'Please switch to to the develop branch to create a release branch.'
exit 0
fi
## Create release branch
git checkout -b "release/${NEW_VERSION}" develop
## TODO: bump package version in pyproject.toml
## TODO: commit changes to pyproject.toml
## TODO: generate new CHANGELOG entry from commits
## TODO: commit changes to CHANGELOG
## Return to develop
git checkout develop
if [[ -z "${CI}" ]]; then
echo "Please confirm the release branch is correct."
read -p "Press enter to continue"
echo
fi
## Push branch and tags
git push origin "release/${NEW_VERSION}"
## TODO: create PR for review

32
scripts/release-tags.sh Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# file: release-tags.sh
# description: Automatic consistent release tags following the SemVer convension.
# set -x
# trap read debug
## Build release context as environment variables.
GIT_BRANCH=$(git symbolic-ref --short HEAD)
## TODO: ensure this is the main branch
if [[ "${GIT_BRANCH}" != "main" ]]; then
echo 'Please switch to the main branch to create release tags.'
exit 0
fi
## TODO: get package version from pyproject.toml
CURRENT_VERSION=2.3.0
## Create GitHub Release
git tag -a ${CURRENT_VERSION} -m "v${CURRENT_VERSION}"
if [[ -z "${CI}" ]]; then
echo "Please confirm the release tag is correct."
read -p "Press enter to continue"
echo
fi
git push --tags
## TODO: publish to PyPI.

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import os import os
import sys import sys
import getpass import getpass

View File

@ -10,6 +10,7 @@ import random
import requests import requests
import json import json
import os import os
from typing import List,Tuple
from anki.db import DB, DBError from anki.db import DB, DBError
from anki.utils import ids2str, intTime, platDesc, checksum, devMode from anki.utils import ids2str, intTime, platDesc, checksum, devMode
@ -72,9 +73,40 @@ class Syncer(object):
if 'crt' in rchg: if 'crt' in rchg:
self.col.crt = rchg['crt'] self.col.crt = rchg['crt']
self.prepareToChunk() self.prepareToChunk()
# this fn was cloned from anki module(version 2.1.36)
def basicCheck(self) -> bool:
"Basic integrity check for syncing. True if ok."
# cards without notes
if self.col.db.scalar(
"""
select 1 from cards where nid not in (select id from notes) limit 1"""
):
return False
# notes without cards or models
if self.col.db.scalar(
"""
select 1 from notes where id not in (select distinct nid from cards)
or mid not in %s limit 1"""
% ids2str(self.col.models.ids())
):
return False
# invalid ords
for m in self.col.models.all():
# ignore clozes
if m["type"] != MODEL_STD:
continue
if self.col.db.scalar(
"""
select 1 from cards where ord not in %s and nid in (
select id from notes where mid = ?) limit 1"""
% ids2str([t["ord"] for t in m["tmpls"]]),
m["id"],
):
return False
return True
def sanityCheck(self, full): def sanityCheck(self, full):
if not self.col.basicCheck(): if not self.basicCheck():
return "failed basic check" return "failed basic check"
for t in "cards", "notes", "revlog", "graves": for t in "cards", "notes", "revlog", "graves":
if self.col.db.scalar( if self.col.db.scalar(
@ -83,7 +115,7 @@ class Syncer(object):
for g in self.col.decks.all(): for g in self.col.decks.all():
if g['usn'] == -1: if g['usn'] == -1:
return "deck had usn = -1" return "deck had usn = -1"
for t, usn in self.col.tags.allItems(): for t, usn in self.allItems():
if usn == -1: if usn == -1:
return "tag had usn = -1" return "tag had usn = -1"
found = False found = False
@ -276,10 +308,12 @@ from notes where %s""" % lim, self.maxUsn)
# Tags # Tags
########################################################################## ##########################################################################
def allItems(self) -> List[Tuple[str, int]]:
tags=self.col.db.execute("select tag, usn from tags")
return [(tag, int(usn)) for tag,usn in tags]
def getTags(self): def getTags(self):
tags = [] tags = []
for t, usn in self.col.tags.allItems(): for t, usn in self.allItems():
if usn == -1: if usn == -1:
self.col.tags.tags[t] = self.maxUsn self.col.tags.tags[t] = self.maxUsn
tags.append(t) tags.append(t)
@ -331,7 +365,9 @@ from notes where %s""" % lim, self.maxUsn)
return self.col.conf return self.col.conf
def mergeConf(self, conf): def mergeConf(self, conf):
self.col.backend.set_all_config(json.dumps(conf).encode()) for key, value in conf.items():
self.col.set_config(key, value)
# self.col.backend.set_all_config(json.dumps(conf).encode())
# Wrapper for requests that tracks upload/download progress # Wrapper for requests that tracks upload/download progress
########################################################################## ##########################################################################
@ -568,7 +604,7 @@ class FullSyncer(HttpSyncer):
# make sure it's ok before we try to upload # make sure it's ok before we try to upload
if self.col.db.scalar("pragma integrity_check") != "ok": if self.col.db.scalar("pragma integrity_check") != "ok":
return False return False
if not self.col.basicCheck(): if not self.basicCheck():
return False return False
# apply some adjustments, then upload # apply some adjustments, then upload
self.col.beforeUpload() self.col.beforeUpload()

View File

@ -121,6 +121,10 @@ class SyncCollectionHandler(Syncer):
self.minUsn = minUsn self.minUsn = minUsn
self.lnewer = not lnewer self.lnewer = not lnewer
lgraves = self.removed() lgraves = self.removed()
# convert grave:None to {'cards': [], 'notes': [], 'decks': []}
# because req.POST['data'] returned value of grave is None
if graves==None:
graves={'cards': [], 'notes': [], 'decks': []}
self.remove(graves) self.remove(graves)
return lgraves return lgraves
@ -178,7 +182,7 @@ class SyncCollectionHandler(Syncer):
] ]
def getTags(self): def getTags(self):
return [t for t, usn in self.col.tags.allItems() return [t for t, usn in self.allItems()
if usn >= self.minUsn] if usn >= self.minUsn]
class SyncMediaHandler: class SyncMediaHandler:

View File

@ -1,91 +1,113 @@
THE FILE WAS GENERATED BY POETRY, DO NOT EDIT! # THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!
anki==2.1.37; python_version >= "3.8" anki==2.1.43; python_version >= "3.8"
appnope==0.1.2; platform_system == "Darwin" and python_version >= "3.7" and sys_platform == "darwin" appnope==0.1.2; platform_system == "Darwin" and python_version >= "3.8" and sys_platform == "darwin"
argon2-cffi==20.1.0; python_version >= "3.5" argon2-cffi-bindings==21.2.0; python_version >= "3.6"
async-generator==1.10; python_version >= "3.6" argon2-cffi==21.3.0; python_version >= "3.6"
attrs==20.3.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" asttokens==2.0.5; python_version >= "3.8"
backcall==0.2.0; python_version >= "3.7" attrs==21.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
beautifulsoup4==4.9.3 backcall==0.2.0; python_version >= "3.8"
bleach==3.2.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" beautifulsoup4==4.10.0; python_full_version > "3.0.0"
certifi==2020.12.5; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" black==21.12b0; python_full_version >= "3.6.2" and python_version >= "3.8"
cffi==1.14.4; implementation_name === "pypy" and python_version >= "3.5" bleach==4.1.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
chardet==4.0.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" certifi==2021.10.8; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8"
click==7.1.2; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" cffi==1.15.0; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3.8"
click==8.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8"
colorama==0.4.4; python_version >= "3.8" and python_full_version < "3.0.0" and platform_system == "Windows" and sys_platform == "win32" or platform_system == "Windows" and python_version >= "3.8" and python_full_version >= "3.5.0" and sys_platform == "win32"
debugpy==1.5.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
decorator==4.4.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0") decorator==4.4.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0")
defusedxml==0.6.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" defusedxml==0.7.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
distro==1.5.0 distro==1.6.0
entrypoints==0.3; python_version >= "3.6" entrypoints==0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
future==0.18.2; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5" executing==0.8.2; python_version >= "3.8"
idna==2.10; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" ghp-import==2.0.2; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
ipykernel==5.4.2; python_version >= "3.6" idna==3.3; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8"
ipython-genutils==0.2.0; python_version >= "3.7" importlib-metadata==4.10.0; python_version < "3.10" and python_version >= "3.7" and python_full_version >= "3.7.1"
ipython==7.19.0; python_version >= "3.7" importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.7" and python_full_version >= "3.7.1"
ipywidgets==7.5.1 ipykernel==6.7.0; python_version >= "3.7"
jedi==0.17.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" ipython-genutils==0.2.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
jinja2==2.11.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" ipython==8.0.0; python_version >= "3.8"
joblib==1.0.0; python_version >= "3.6" ipywidgets==7.6.5
json5==0.9.5; python_version >= "3.5" jedi==0.18.1; python_version >= "3.8"
jsonschema==3.2.0; python_version >= "3.5" jinja2==3.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
jupyter-client==6.1.7; python_version >= "3.6" json5==0.9.6; python_version >= "3.5"
jupyter-console==6.2.0; python_version >= "3.6" jsonschema==4.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
jupyter-core==4.7.0; python_version >= "3.6" jupyter-client==7.1.1; python_full_version >= "3.7.1" and python_version >= "3.7" and python_version < "4"
jupyter-console==6.4.0; python_version >= "3.6"
jupyter-core==4.9.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
jupyter==1.0.0 jupyter==1.0.0
jupyterlab-pygments==0.1.2; python_version >= "3.6" jupyterlab-pygments==0.1.2; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
jupyterlab-server==1.2.0; python_version >= "3.5" jupyterlab-server==1.2.0; python_version >= "3.5"
jupyterlab==2.2.9; python_version >= "3.5" jupyterlab-widgets==1.0.2; python_version >= "3.6"
livereload==2.6.3; python_version >= "3.5" jupyterlab==2.3.2; python_version >= "3.5"
lunr==0.5.8; python_version >= "3.5" jupytext==1.13.6; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
markdown==3.3.3; python_version >= "3.6" markdown-it-py==1.1.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
markupsafe==1.1.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" markdown==3.3.6; python_version >= "3.6"
mistune==0.8.4; python_version >= "3.6" markupsafe==2.0.1; python_version >= "3.6"
mkdocs==1.1.2; python_version >= "3.5" matplotlib-inline==0.1.3; python_version >= "3.8"
nbclient==0.5.1; python_version >= "3.6" mdit-py-plugins==0.3.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
nbconvert==6.0.7; python_version >= "3.6" mergedeep==1.3.4; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
nbformat==5.0.8; python_version >= "3.6" mistune==0.8.4; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
nest-asyncio==1.4.3; python_version >= "3.6" mkdocs-jupyter==0.19.0; python_full_version >= "3.7.1" and python_version < "4"
nltk==3.5; python_version >= "3.5" mkdocs-material-extensions==1.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
notebook==6.1.5; python_version >= "3.5" mkdocs-material==8.1.7; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
orjson==3.4.6; python_version >= "3.8" mkdocs==1.2.3; python_version >= "3.6"
packaging==20.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" mypy-extensions==0.4.3; python_full_version >= "3.6.2" and python_version >= "3.8"
pandocfilters==1.4.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" nbclient==0.5.10; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
parso==0.7.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" nbconvert==6.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.7" nbformat==5.1.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
pickleshare==0.7.5; python_version >= "3.7" nest-asyncio==1.5.4; python_full_version >= "3.7.1" and python_version >= "3.7" and python_version < "4"
prometheus-client==0.9.0; python_version >= "3.5" notebook==6.4.7; python_version >= "3.6"
prompt-toolkit==3.0.8; python_full_version >= "3.6.1" and python_version >= "3.7" orjson==3.6.5; platform_machine == "x86_64" and python_version >= "3.8"
protobuf==3.14.0; python_version >= "3.8" packaging==21.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
psutil==5.8.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") pandocfilters==1.5.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
ptyprocess==0.6.0; os_name != "nt" and python_version >= "3.7" and sys_platform != "win32" parso==0.8.3; python_version >= "3.8"
py==1.10.0; python_version >= "3.5" and python_full_version < "3.0.0" and implementation_name === "pypy" or implementation_name === "pypy" and python_version >= "3.5" and python_full_version >= "3.4.0" pathspec==0.9.0; python_full_version >= "3.6.2" and python_version >= "3.8"
pyaudio==0.2.11 pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.8"
pycparser==2.20; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" pickleshare==0.7.5; python_version >= "3.8"
pygments==2.7.3; python_version >= "3.7" platformdirs==2.4.1; python_full_version >= "3.6.2" and python_version >= "3.8"
pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" prometheus-client==0.12.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
pyrsistent==0.17.3; python_version >= "3.5" prompt-toolkit==3.0.24; python_full_version >= "3.6.2" and python_version >= "3.8"
pysocks==1.7.1; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" protobuf==3.19.3; python_version >= "3.8"
python-dateutil==2.8.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5" psutil==5.9.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
pywin32==300; sys_platform == "win32" and python_version >= "3.6" ptyprocess==0.7.0; os_name != "nt" and python_version >= "3.8" and sys_platform != "win32"
pywinpty==0.5.7; os_name == "nt" and python_version >= "3.6" pure-eval==0.2.1; python_version >= "3.8"
pyyaml==5.3.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" py==1.11.0; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
pyzmq==20.0.0; python_version >= "3.6" pycparser==2.21; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
qtconsole==5.0.1; python_version >= "3.6" pygments==2.11.2; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8"
qtpy==1.9.0; python_version >= "3.6" pymdown-extensions==9.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
regex==2020.11.13; python_version >= "3.5" pyparsing==3.0.6; python_version >= "3.6"
requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") pyrsistent==0.18.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
send2trash==1.5.0 pysocks==1.7.1; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8"
six==1.15.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" python-dateutil==2.8.2; python_full_version >= "3.6.1" and python_version >= "3.7"
soupsieve==2.1; python_version >= "3.8" pywin32==303; sys_platform == "win32" and platform_python_implementation != "PyPy" and python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
terminado==0.9.1; python_version >= "3.6" pywinpty==1.1.6; os_name == "nt" and python_version >= "3.6"
testpath==0.4.4; python_version >= "3.6" pyyaml-env-tag==0.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
tornado==6.1; python_version >= "3.6" pyyaml==6.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
tqdm==4.54.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_version >= "3.5" and python_full_version >= "3.4.0" pyzmq==22.3.0; python_full_version >= "3.6.1" and python_version >= "3.7"
traitlets==5.0.5; python_version >= "3.7" qtconsole==5.2.2; python_version >= "3.6"
urllib3==1.26.2; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.8" qtpy==2.0.0; python_version >= "3.6"
wcwidth==0.2.5; python_full_version >= "3.6.1" and python_version >= "3.6" requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
webencodings==0.5.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" send2trash==1.8.0
webob==1.8.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") six==1.16.0; python_full_version >= "3.7.1" and python_version >= "3.7" and python_version < "4" and (python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.8")
widgetsnbextension==3.5.1 soupsieve==2.3.1; python_full_version > "3.0.0" and python_version >= "3.8"
stack-data==0.1.4; python_version >= "3.8"
terminado==0.12.1; python_version >= "3.6"
testpath==0.5.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
toml==0.10.2; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
tomli==1.2.3; python_full_version >= "3.6.2" and python_version >= "3.8"
tornado==6.1; python_full_version >= "3.6.1" and python_version >= "3.7"
traitlets==5.1.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8"
typing-extensions==4.0.1
urllib3==1.26.8; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.8"
waitress==2.0.0; python_full_version >= "3.6.0"
watchdog==2.1.6; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
wcwidth==0.2.5; python_full_version >= "3.6.2" and python_version >= "3.8"
webencodings==0.5.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
webob==1.8.7; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0")
webtest==2.0.35
widgetsnbextension==3.5.2
zipp==3.7.0; python_version < "3.9" and python_version >= "3.7" and python_full_version >= "3.7.1"
-e src/. -e src/.

View File

@ -1,22 +1,22 @@
THE FILE WAS GENERATED BY POETRY, DO NOT EDIT! # THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!
anki==2.1.37; python_version >= "3.8" anki==2.1.43; python_version >= "3.8"
beautifulsoup4==4.9.3 beautifulsoup4==4.10.0; python_full_version > "3.0.0"
certifi==2020.12.5; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" certifi==2021.10.8; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8"
chardet==4.0.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3.8"
decorator==4.4.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0") decorator==4.4.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0")
distro==1.5.0 distro==1.6.0
idna==2.10; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" idna==3.3; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8"
markdown==3.3.3; python_version >= "3.6" importlib-metadata==4.10.0; python_version < "3.10" and python_version >= "3.7"
orjson==3.4.6; python_version >= "3.8" markdown==3.3.6; python_version >= "3.6"
protobuf==3.14.0; python_version >= "3.8" orjson==3.6.5; platform_machine == "x86_64" and python_version >= "3.8"
psutil==5.8.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") protobuf==3.19.3; python_version >= "3.8"
pyaudio==0.2.11 psutil==5.9.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
pysocks==1.7.1; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.8" pysocks==1.7.1; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8"
requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
send2trash==1.5.0 send2trash==1.8.0
six==1.15.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.8" soupsieve==2.3.1; python_full_version > "3.0.0" and python_version >= "3.8"
soupsieve==2.1; python_version >= "3.8" urllib3==1.26.8; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.8"
urllib3==1.26.2; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.8" webob==1.8.7; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0")
webob==1.8.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") zipp==3.7.0; python_version < "3.10" and python_version >= "3.7"