Merge pull request #137 from ankicommunity/release/v2.4.0
This commit is contained in:
commit
150da07f66
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,6 +5,9 @@
|
|||||||
/collections
|
/collections
|
||||||
/venv
|
/venv
|
||||||
|
|
||||||
|
/config/*
|
||||||
|
!/config/.env.example
|
||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,python,jupyternotebooks
|
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,python,jupyternotebooks
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,python,jupyternotebooks
|
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,python,jupyternotebooks
|
||||||
|
|
||||||
|
|||||||
13
Makefile
13
Makefile
@ -31,13 +31,26 @@ run:
|
|||||||
@if [[ -f "scripts/${*}.sh" ]]; then \
|
@if [[ -f "scripts/${*}.sh" ]]; then \
|
||||||
${BASH} "scripts/${*}.sh"; fi
|
${BASH} "scripts/${*}.sh"; fi
|
||||||
|
|
||||||
|
.PHONY: config #: Create new config file.
|
||||||
|
config: config/.env.${ENV}
|
||||||
|
config/.env.%:
|
||||||
|
@cp -n config/.env.example config/.env.${ENV}
|
||||||
|
|
||||||
.PHONY: init #: Download Python dependencies.
|
.PHONY: init #: Download Python dependencies.
|
||||||
init:
|
init:
|
||||||
@${POETRY} install
|
@${POETRY} install
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
@${POETRY} build
|
||||||
|
|
||||||
.PHONY: release #: Create new Git release and tags.
|
.PHONY: release #: Create new Git release and tags.
|
||||||
release: release-branch release-tags
|
release: release-branch release-tags
|
||||||
|
|
||||||
|
.PHONY: publish
|
||||||
|
publish: build
|
||||||
|
@${POETRY} publish
|
||||||
|
|
||||||
.PHONY: open
|
.PHONY: open
|
||||||
open:
|
open:
|
||||||
@${OPEN} ${ANKISYNCD_URL}
|
@${OPEN} ${ANKISYNCD_URL}
|
||||||
45
README.md
45
README.md
@ -31,7 +31,9 @@ It supports Python 3 and Anki 2.1.
|
|||||||
- [AnkiDroid](#ankidroid)
|
- [AnkiDroid](#ankidroid)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [ENVVAR configuration overrides](#envvar-configuration-overrides)
|
- [Configuration](#configuration)
|
||||||
|
- [Environment Variables](#environment-variables-preferred)
|
||||||
|
- [Config File](#config-file-ankisyncdconf)
|
||||||
- [Support for other database backends](#support-for-other-database-backends)
|
- [Support for other database backends](#support-for-other-database-backends)
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@ -42,18 +44,17 @@ Installing
|
|||||||
|
|
||||||
$ pip install -r src/requirements.txt
|
$ pip install -r src/requirements.txt
|
||||||
|
|
||||||
2. Modify ankisyncd.conf according to your needs
|
2. Copy the default config file ([ankisyncd.conf](src/ankisyncd.conf)) to configure the server using the command below. Environment variables can be used instead, see: [Configuration](#configuration).
|
||||||
|
|
||||||
|
$ cp src/ankisyncd.conf src/ankisyncd/.
|
||||||
|
|
||||||
3. Create user:
|
3. Create user:
|
||||||
|
|
||||||
$ ./ankisyncctl.py adduser <username>
|
$ ./ankisyncctl.py adduser <username>
|
||||||
|
|
||||||
4. Setup a proxy to unchunk the requests.
|
4. Setup a proxy to trans-write the requests (Optional) .
|
||||||
|
Ankisyncd currently support the header "Transfer-Encoding: chunked" used by Anki.
|
||||||
Webob does not support the header "Transfer-Encoding: chunked" used by Anki
|
If you want to enable secure connection or have a better security,a proxy can be set up.
|
||||||
and therefore ankisyncd sees chunked requests as empty. To solve this problem
|
|
||||||
setup Nginx (or any other webserver of your choice) and configure it to
|
|
||||||
"unchunk" the requests for ankisyncd.
|
|
||||||
|
|
||||||
For example, if you use Nginx on the same machine as ankisyncd, you first
|
For example, if you use Nginx on the same machine as ankisyncd, you first
|
||||||
have to change the port in `ankisyncd.conf` to something other than `27701`.
|
have to change the port in `ankisyncd.conf` to something other than `27701`.
|
||||||
@ -61,7 +62,7 @@ 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 ([entire config template click me](https://github.com/ankicommunity/anki-sync-server/blob/develop/docs/nginx.conf)):
|
and listening on port `27702` may look like ([entire config template click me](docs/src/nginx/nginx.example.conf)):
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
@ -70,11 +71,14 @@ Installing
|
|||||||
location / {
|
location / {
|
||||||
proxy_http_version 1.0;
|
proxy_http_version 1.0;
|
||||||
proxy_pass http://127.0.0.1:27702/;
|
proxy_pass http://127.0.0.1:27702/;
|
||||||
|
client_max_body_size 222M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Adding the line `client_max_body_size 222M;` to Nginx prevents bigger collections from not being able to sync due to size limitations.
|
||||||
|
|
||||||
5. Run ankisyncd:
|
5. Run ankisyncd:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -88,6 +92,7 @@ Installing (Docker)
|
|||||||
|
|
||||||
Follow [these instructions](https://github.com/ankicommunity/anki-devops-services#about-this-docker-image).
|
Follow [these instructions](https://github.com/ankicommunity/anki-devops-services#about-this-docker-image).
|
||||||
|
|
||||||
|
|
||||||
Setting up Anki
|
Setting up Anki
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
@ -138,7 +143,7 @@ and put it in `~/Anki/addons`.
|
|||||||
anki.sync.SYNC_BASE = addr
|
anki.sync.SYNC_BASE = addr
|
||||||
anki.sync.SYNC_MEDIA_BASE = addr + "msync/"
|
anki.sync.SYNC_MEDIA_BASE = addr + "msync/"
|
||||||
|
|
||||||
[addons21]: https://addon-docs.ankiweb.net/#/getting-started?id=add-on-folders
|
[addons21]: https://addon-docs.ankiweb.net/addon-folders.html
|
||||||
|
|
||||||
### AnkiDroid
|
### AnkiDroid
|
||||||
|
|
||||||
@ -171,7 +176,7 @@ This project uses [GNU Make](https://www.gnu.org/software/make/) to simplify the
|
|||||||
$ cp config/.env.example config/.env.local
|
$ cp config/.env.example config/.env.local
|
||||||
```
|
```
|
||||||
|
|
||||||
See [ENVVAR configuration overrides](#envvar-configuration-overrides) for more information.
|
See [Configuration](#configuration) for more information.
|
||||||
|
|
||||||
2. Download Python dependencies.
|
2. Download Python dependencies.
|
||||||
|
|
||||||
@ -185,8 +190,9 @@ $ make init
|
|||||||
$ make tests
|
$ make tests
|
||||||
```
|
```
|
||||||
|
|
||||||
ENVVAR configuration overrides
|
## Configuration
|
||||||
------------------------------
|
|
||||||
|
### Environment Variables (preferred)
|
||||||
|
|
||||||
Configuration values can be set via environment variables using `ANKISYNCD_` prepended
|
Configuration values can be set via environment variables using `ANKISYNCD_` prepended
|
||||||
to the uppercase form of the configuration value. E.g. the environment variable,
|
to the uppercase form of the configuration value. E.g. the environment variable,
|
||||||
@ -194,6 +200,19 @@ to the uppercase form of the configuration value. E.g. the environment variable,
|
|||||||
|
|
||||||
Environment variables override the values set in the `ankisyncd.conf`.
|
Environment variables override the values set in the `ankisyncd.conf`.
|
||||||
|
|
||||||
|
* The environment variables can be found here: config/.env.example.
|
||||||
|
* The file also includes other development variables, but the notable ones are the ones with the prefix ANKISYNCD_
|
||||||
|
* Environment variables will override the config files values (which is why I recommend you use them)
|
||||||
|
* This is what we use in the Docker images (see: https://github.com/ankicommunity/anki-devops-services/blob/develop/services/anki-sync-server/examples/docker-compose.yml).
|
||||||
|
* Copying the config file from config/.env.example to config/.env.local will allow you to configure the server when using the make commands
|
||||||
|
* You can also set it when running the server e.g. ANKISYNCD_PORT=5001 make run
|
||||||
|
* The above two options are useful for development. But if you're only going for usage, you can also set it globally by adding it to your ~/.bashrc file e.g. export ANKISYNCD_PORT=50001
|
||||||
|
|
||||||
|
### Config File: ankisyncd.conf
|
||||||
|
|
||||||
|
A config file can be used to configuring the server. It can be found here: [src/ankisyncd.conf](src/ankisyncd.conf).
|
||||||
|
|
||||||
|
|
||||||
Support for other database backends
|
Support for other database backends
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,11 @@ MKDOCS_CMD=serve
|
|||||||
### JUPYTER_CMD
|
### JUPYTER_CMD
|
||||||
### JUPYTER_NOTEBOOK_DIR
|
### JUPYTER_NOTEBOOK_DIR
|
||||||
|
|
||||||
|
## Poetry
|
||||||
|
### POETRY_PYPI_TOKEN_PYPI=
|
||||||
|
### POETRY_HTTP_BASIC_PYPI_USERNAME=
|
||||||
|
### POETRY_HTTP_BASIC_PYPI_PASSWORD=
|
||||||
|
|
||||||
## Make
|
## Make
|
||||||
AWK=awk
|
AWK=awk
|
||||||
BASH=bash
|
BASH=bash
|
||||||
|
|||||||
2021
poetry.lock
generated
2021
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,12 +4,12 @@ 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 = [
|
packages = [
|
||||||
{ include = "ankisyncd", from = "src" }
|
{ include = "*", from = "src" }
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
anki = "2.1.43"
|
anki = "2.1.49"
|
||||||
beautifulsoup4 = "^4.9.1"
|
beautifulsoup4 = "^4.9.1"
|
||||||
requests = "^2.24.0"
|
requests = "^2.24.0"
|
||||||
markdown = "^3.2.2"
|
markdown = "^3.2.2"
|
||||||
|
|||||||
10
scripts/poetry-update.sh
Normal file
10
scripts/poetry-update.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# file: poetry-update.sh
|
||||||
|
# description: Update Python dependencies to latest.
|
||||||
|
|
||||||
|
# POETRY_UPDATE_OPTS=--dry-run
|
||||||
|
# POETRY_UPDATE_ARGS=anki
|
||||||
|
poetry update "${POETRY_UPDATE_OPTS}" "${POETRY_UPDATE_ARGS}"
|
||||||
|
|
||||||
|
# TODO: Run poetry export
|
||||||
|
# TODO: Create git branch and commit
|
||||||
@ -15,7 +15,7 @@ if [[ "${GIT_BRANCH}" != "main" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
## TODO: get package version from pyproject.toml
|
## TODO: get package version from pyproject.toml
|
||||||
CURRENT_VERSION=2.3.0
|
CURRENT_VERSION=v2.3.0
|
||||||
|
|
||||||
## Create GitHub Release
|
## Create GitHub Release
|
||||||
git tag -a ${CURRENT_VERSION} -m "v${CURRENT_VERSION}"
|
git tag -a ${CURRENT_VERSION} -m "v${CURRENT_VERSION}"
|
||||||
|
|||||||
@ -41,6 +41,21 @@ class Syncer(object):
|
|||||||
self.col = col
|
self.col = col
|
||||||
self.server = server
|
self.server = server
|
||||||
|
|
||||||
|
# new added functions related to Syncer:
|
||||||
|
# these are removed from latest anki module
|
||||||
|
########################################################################
|
||||||
|
def scm(self):
|
||||||
|
"""return schema"""
|
||||||
|
scm=self.col.db.scalar("select scm from col")
|
||||||
|
return scm
|
||||||
|
def increment_usn(self):
|
||||||
|
"""usn+1 in db"""
|
||||||
|
self.col.db.execute("update col set usn = usn + 1")
|
||||||
|
def set_modified_time(self,now:int):
|
||||||
|
self.col.db.execute("update col set mod=?", now)
|
||||||
|
def set_last_sync(self,now:int):
|
||||||
|
self.col.db.execute("update col set ls = ?", now)
|
||||||
|
#########################################################################
|
||||||
def meta(self):
|
def meta(self):
|
||||||
return dict(
|
return dict(
|
||||||
mod=self.col.mod,
|
mod=self.col.mod,
|
||||||
@ -58,7 +73,7 @@ class Syncer(object):
|
|||||||
decks=self.getDecks(),
|
decks=self.getDecks(),
|
||||||
tags=self.getTags())
|
tags=self.getTags())
|
||||||
if self.lnewer:
|
if self.lnewer:
|
||||||
d['conf'] = json.loads(self.col.backend.get_all_config())
|
d['conf'] = self.col.all_config()
|
||||||
d['crt'] = self.col.crt
|
d['crt'] = self.col.crt
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@ -66,7 +81,6 @@ class Syncer(object):
|
|||||||
# then the other objects
|
# then the other objects
|
||||||
self.mergeModels(rchg['models'])
|
self.mergeModels(rchg['models'])
|
||||||
self.mergeDecks(rchg['decks'])
|
self.mergeDecks(rchg['decks'])
|
||||||
self.mergeTags(rchg['tags'])
|
|
||||||
if 'conf' in rchg:
|
if 'conf' in rchg:
|
||||||
self.mergeConf(rchg['conf'])
|
self.mergeConf(rchg['conf'])
|
||||||
# this was left out of earlier betas
|
# this was left out of earlier betas
|
||||||
@ -105,50 +119,50 @@ select id from notes where mid = ?) limit 1"""
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def sanityCheck(self, full):
|
def sanityCheck(self):
|
||||||
if not self.basicCheck():
|
tables=["cards",
|
||||||
return "failed basic check"
|
"notes",
|
||||||
for t in "cards", "notes", "revlog", "graves":
|
"revlog",
|
||||||
if self.col.db.scalar(
|
"graves",
|
||||||
"select count() from %s where usn = -1" % t):
|
"decks",
|
||||||
return "%s had usn = -1" % t
|
"deck_config",
|
||||||
for g in self.col.decks.all():
|
"tags",
|
||||||
if g['usn'] == -1:
|
"notetypes",
|
||||||
return "deck had usn = -1"
|
]
|
||||||
for t, usn in self.allItems():
|
for tb in tables:
|
||||||
if usn == -1:
|
if self.col.db.scalar(f'select null from {tb} where usn=-1'):
|
||||||
return "tag had usn = -1"
|
return f'table had usn=-1: {tb}'
|
||||||
found = False
|
|
||||||
for m in self.col.models.all():
|
|
||||||
if m['usn'] == -1:
|
|
||||||
return "model had usn = -1"
|
|
||||||
if found:
|
|
||||||
self.col.models.save()
|
|
||||||
self.col.sched.reset()
|
self.col.sched.reset()
|
||||||
# check for missing parent decks
|
|
||||||
#self.col.sched.deckDueList()
|
|
||||||
# return summary of deck
|
# return summary of deck
|
||||||
|
# make sched.counts() equal to default [0,0,0]
|
||||||
|
# to make sure sync normally if sched.counts()
|
||||||
|
# are not equal between different clients due to
|
||||||
|
# different deck selection
|
||||||
return [
|
return [
|
||||||
list(self.col.sched.counts()),
|
list([0,0,0]),
|
||||||
self.col.db.scalar("select count() from cards"),
|
self.col.db.scalar("select count() from cards"),
|
||||||
self.col.db.scalar("select count() from notes"),
|
self.col.db.scalar("select count() from notes"),
|
||||||
self.col.db.scalar("select count() from revlog"),
|
self.col.db.scalar("select count() from revlog"),
|
||||||
self.col.db.scalar("select count() from graves"),
|
self.col.db.scalar("select count() from graves"),
|
||||||
len(self.col.models.all()),
|
len(self.col.models.all()),
|
||||||
len(self.col.decks.all()),
|
len(self.col.decks.all()),
|
||||||
len(self.col.decks.allConf()),
|
len(self.col.decks.all_config()),
|
||||||
]
|
]
|
||||||
|
|
||||||
def usnLim(self):
|
def usnLim(self):
|
||||||
return "usn = -1"
|
return "usn = -1"
|
||||||
|
|
||||||
def finish(self, mod=None):
|
def finish(self, now=None):
|
||||||
self.col.ls = mod
|
if now is not None:
|
||||||
self.col._usn = self.maxUsn + 1
|
|
||||||
# ensure we save the mod time even if no changes made
|
# ensure we save the mod time even if no changes made
|
||||||
self.col.db.mod = True
|
self.set_modified_time(now)
|
||||||
self.col.save(mod=mod)
|
self.set_last_sync(now)
|
||||||
return mod
|
self.increment_usn()
|
||||||
|
self.col.save()
|
||||||
|
return now
|
||||||
|
# even though that now is None will not happen,have to match a gurad case
|
||||||
|
return None
|
||||||
|
|
||||||
# Chunked syncing
|
# Chunked syncing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@ -195,67 +209,26 @@ from notes where %s""" % lim, self.maxUsn)
|
|||||||
# Deletions
|
# Deletions
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def removed(self):
|
def add_grave(self, ids: List[int], type: int,usn: int):
|
||||||
cards = []
|
items=[(id,type,usn) for id in ids]
|
||||||
notes = []
|
# make sure table graves fields order and schema version match
|
||||||
decks = []
|
# query sql1='pragma table_info(graves)' version query schema='select ver from col'
|
||||||
|
self.col.db.executemany(
|
||||||
|
"INSERT OR IGNORE INTO graves (oid, type, usn) VALUES (?, ?, ?)" ,
|
||||||
|
items)
|
||||||
|
|
||||||
curs = self.col.db.execute(
|
def apply_graves(self, graves,latest_usn: int):
|
||||||
"select oid, type from graves where usn = -1")
|
|
||||||
|
|
||||||
for oid, type in curs:
|
|
||||||
if type == REM_CARD:
|
|
||||||
cards.append(oid)
|
|
||||||
elif type == REM_NOTE:
|
|
||||||
notes.append(oid)
|
|
||||||
else:
|
|
||||||
decks.append(oid)
|
|
||||||
|
|
||||||
self.col.db.execute("update graves set usn=? where usn=-1",
|
|
||||||
self.maxUsn)
|
|
||||||
|
|
||||||
return dict(cards=cards, notes=notes, decks=decks)
|
|
||||||
|
|
||||||
def remove(self, graves):
|
|
||||||
# remove card and the card's orphaned notes
|
# remove card and the card's orphaned notes
|
||||||
self.col.remove_cards_and_orphaned_notes(graves['cards'])
|
self.col.remove_cards_and_orphaned_notes(graves['cards'])
|
||||||
|
self.add_grave(graves['cards'], REM_CARD,latest_usn)
|
||||||
# only notes
|
# only notes
|
||||||
self.col.remove_notes(graves['notes'])
|
self.col.remove_notes(graves['notes'])
|
||||||
|
self.add_grave(graves['notes'], REM_NOTE,latest_usn)
|
||||||
|
|
||||||
# since level 0 deck ,we only remove deck ,but backend will delete child,it is ok, the delete
|
# since level 0 deck ,we only remove deck ,but backend will delete child,it is ok, the delete
|
||||||
# will have once effect
|
# will have once effect
|
||||||
for oid in graves['decks']:
|
self.col.decks.remove(graves['decks'])
|
||||||
self.col.decks.rem(oid)
|
self.add_grave(graves['decks'], REM_DECK,latest_usn)
|
||||||
|
|
||||||
|
|
||||||
# we can place non-exist grave after above delete.
|
|
||||||
localgcards = []
|
|
||||||
localgnotes = []
|
|
||||||
localgdecks = []
|
|
||||||
curs = self.col.db.execute(
|
|
||||||
"select oid, type from graves where usn = %d" % self.col.usn())
|
|
||||||
|
|
||||||
for oid, type in curs:
|
|
||||||
if type == REM_CARD:
|
|
||||||
localgcards.append(oid)
|
|
||||||
elif type == REM_NOTE:
|
|
||||||
localgnotes.append(oid)
|
|
||||||
else:
|
|
||||||
localgdecks.append(oid)
|
|
||||||
|
|
||||||
# n meaning non-exsiting grave in the server compared to client
|
|
||||||
ncards = [ oid for oid in graves['cards'] if oid not in localgcards]
|
|
||||||
for oid in ncards:
|
|
||||||
self.col._logRem([oid], REM_CARD)
|
|
||||||
|
|
||||||
nnotes = [ oid for oid in graves['notes'] if oid not in localgnotes]
|
|
||||||
for oid in nnotes:
|
|
||||||
self.col._logRem([oid], REM_NOTE)
|
|
||||||
|
|
||||||
ndecks = [ oid for oid in graves['decks'] if oid not in localgdecks]
|
|
||||||
for oid in ndecks:
|
|
||||||
self.col._logRem([oid], REM_DECK)
|
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@ -342,7 +315,8 @@ from notes where %s""" % lim, self.maxUsn)
|
|||||||
for r in data:
|
for r in data:
|
||||||
if r[0] not in lmods or lmods[r[0]] < r[modIdx]:
|
if r[0] not in lmods or lmods[r[0]] < r[modIdx]:
|
||||||
update.append(r)
|
update.append(r)
|
||||||
self.col.log(table, data)
|
# replace col.log by just using print
|
||||||
|
print(table, data)
|
||||||
return update
|
return update
|
||||||
|
|
||||||
def mergeCards(self, cards):
|
def mergeCards(self, cards):
|
||||||
@ -356,7 +330,7 @@ from notes where %s""" % lim, self.maxUsn)
|
|||||||
self.col.db.executemany(
|
self.col.db.executemany(
|
||||||
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
rows)
|
rows)
|
||||||
self.col.updateFieldCache([f[0] for f in rows])
|
self.col.after_note_updates([f[0] for f in rows], mark_modified=False, generate_cards=False)
|
||||||
|
|
||||||
# Col config
|
# Col config
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
@ -15,29 +15,25 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import gzip
|
import gzip
|
||||||
import hashlib
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import zipfile
|
import zipfile
|
||||||
from configparser import ConfigParser
|
import types
|
||||||
from sqlite3 import dbapi2 as sqlite
|
|
||||||
|
|
||||||
from webob import Response
|
from webob import Response
|
||||||
from webob.dec import wsgify
|
|
||||||
from webob.exc import *
|
from webob.exc import *
|
||||||
|
import urllib.parse
|
||||||
|
from functools import wraps
|
||||||
|
from anki.collection import Collection
|
||||||
import anki.db
|
import anki.db
|
||||||
import anki.utils
|
import anki.utils
|
||||||
from anki.consts import REM_CARD, REM_NOTE
|
from anki.consts import REM_CARD, REM_NOTE
|
||||||
|
|
||||||
from ankisyncd.full_sync import get_full_sync_manager
|
from ankisyncd.full_sync import get_full_sync_manager
|
||||||
from ankisyncd.sessions import get_session_manager
|
from ankisyncd.sessions import get_session_manager
|
||||||
from ankisyncd.sync import Syncer, SYNC_VER, SYNC_ZIP_SIZE, SYNC_ZIP_COUNT
|
from ankisyncd.sync import Syncer, SYNC_VER, SYNC_ZIP_SIZE, SYNC_ZIP_COUNT
|
||||||
@ -97,8 +93,8 @@ class SyncCollectionHandler(Syncer):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'mod': self.col.mod,
|
'mod': self.col.mod,
|
||||||
'scm': self.col.scm,
|
'scm': self.scm(),
|
||||||
'usn': self.col._usn,
|
'usn': self.col.usn(),
|
||||||
'ts': anki.utils.intTime(),
|
'ts': anki.utils.intTime(),
|
||||||
'musn': self.col.media.lastUsn(),
|
'musn': self.col.media.lastUsn(),
|
||||||
'uname': self.session.name,
|
'uname': self.session.name,
|
||||||
@ -117,19 +113,20 @@ class SyncCollectionHandler(Syncer):
|
|||||||
# Since now have not thorougly test the V2 scheduler, we leave this comments here, and
|
# Since now have not thorougly test the V2 scheduler, we leave this comments here, and
|
||||||
# just enable the V2 scheduler in the serve code.
|
# just enable the V2 scheduler in the serve code.
|
||||||
|
|
||||||
self.maxUsn = self.col._usn
|
self.maxUsn = self.col.usn()
|
||||||
self.minUsn = minUsn
|
self.minUsn = minUsn
|
||||||
self.lnewer = not lnewer
|
self.lnewer = not lnewer
|
||||||
|
# fetch local/server graves
|
||||||
lgraves = self.removed()
|
lgraves = self.removed()
|
||||||
# convert grave:None to {'cards': [], 'notes': [], 'decks': []}
|
# handle AnkiDroid using old protocol
|
||||||
# because req.POST['data'] returned value of grave is None
|
# Only if Operations like deleting deck are performed on Ankidroid
|
||||||
if graves==None:
|
# can (client) graves is not None
|
||||||
graves={'cards': [], 'notes': [], 'decks': []}
|
if graves is not None:
|
||||||
self.remove(graves)
|
self.apply_graves(graves,self.maxUsn)
|
||||||
return lgraves
|
return lgraves
|
||||||
|
|
||||||
def applyGraves(self, chunk):
|
def applyGraves(self, chunk):
|
||||||
self.remove(chunk)
|
self.apply_graves(chunk,self.maxUsn)
|
||||||
|
|
||||||
def applyChanges(self, changes):
|
def applyChanges(self, changes):
|
||||||
self.rchg = changes
|
self.rchg = changes
|
||||||
@ -138,17 +135,18 @@ class SyncCollectionHandler(Syncer):
|
|||||||
self.mergeChanges(lchg, self.rchg)
|
self.mergeChanges(lchg, self.rchg)
|
||||||
return lchg
|
return lchg
|
||||||
|
|
||||||
def sanityCheck2(self, client, full=None):
|
def sanityCheck2(self, client):
|
||||||
server = self.sanityCheck(full)
|
client[0]=[0,0,0]
|
||||||
|
server = self.sanityCheck()
|
||||||
if client != server:
|
if client != server:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"sanity check failed with server: {server} client: {client}"
|
f"sanity check failed with server: {server} client: {client}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return dict(status="bad", c=client, s=server)
|
return dict(status="bad", c=client, s=server)
|
||||||
return dict(status="ok")
|
return dict(status="ok")
|
||||||
|
|
||||||
def finish(self, mod=None):
|
|
||||||
|
def finish(self):
|
||||||
return super().finish(anki.utils.intTime(1000))
|
return super().finish(anki.utils.intTime(1000))
|
||||||
|
|
||||||
# This function had to be put here in its entirety because Syncer.removed()
|
# This function had to be put here in its entirety because Syncer.removed()
|
||||||
@ -178,7 +176,7 @@ class SyncCollectionHandler(Syncer):
|
|||||||
def getDecks(self):
|
def getDecks(self):
|
||||||
return [
|
return [
|
||||||
[g for g in self.col.decks.all() if g['usn'] >= self.minUsn],
|
[g for g in self.col.decks.all() if g['usn'] >= self.minUsn],
|
||||||
[g for g in self.col.decks.allConf() if g['usn'] >= self.minUsn]
|
[g for g in self.col.decks.all_config() if g['usn'] >= self.minUsn]
|
||||||
]
|
]
|
||||||
|
|
||||||
def getTags(self):
|
def getTags(self):
|
||||||
@ -338,7 +336,6 @@ class SyncMediaHandler:
|
|||||||
if lastUsn < server_lastUsn or lastUsn == 0:
|
if lastUsn < server_lastUsn or lastUsn == 0:
|
||||||
for fname,usn,csum, in self.col.media.changes(lastUsn):
|
for fname,usn,csum, in self.col.media.changes(lastUsn):
|
||||||
result.append([fname, usn, csum])
|
result.append([fname, usn, csum])
|
||||||
|
|
||||||
# anki assumes server_lastUsn == result[-1][1]
|
# anki assumes server_lastUsn == result[-1][1]
|
||||||
# ref: anki/sync.py:720 (commit cca3fcb2418880d0430a5c5c2e6b81ba260065b7)
|
# ref: anki/sync.py:720 (commit cca3fcb2418880d0430a5c5c2e6b81ba260065b7)
|
||||||
result.reverse()
|
result.reverse()
|
||||||
@ -394,7 +391,137 @@ class SyncUserSession:
|
|||||||
# for inactivity and then later re-open it (creating a new Collection object).
|
# for inactivity and then later re-open it (creating a new Collection object).
|
||||||
handler.col = col
|
handler.col = col
|
||||||
return handler
|
return handler
|
||||||
|
class Requests(object):
|
||||||
|
def __init__(self,environ: dict):
|
||||||
|
self.environ=environ
|
||||||
|
@property
|
||||||
|
def params(self):
|
||||||
|
return self.request_items_dict
|
||||||
|
@params.setter
|
||||||
|
def params(self,value):
|
||||||
|
"""
|
||||||
|
A dictionary-like object containing both the parameters from
|
||||||
|
the query string and request body.
|
||||||
|
"""
|
||||||
|
self.request_items_dict= value
|
||||||
|
@property
|
||||||
|
def path(self)-> str:
|
||||||
|
return self.environ['PATH_INFO']
|
||||||
|
@property
|
||||||
|
def POST(self):
|
||||||
|
return self._request_items_dict
|
||||||
|
@POST.setter
|
||||||
|
def POST(self,value):
|
||||||
|
self._request_items_dict=value
|
||||||
|
@property
|
||||||
|
def parse(self):
|
||||||
|
'''Return a MultiDict containing all the variables from a form
|
||||||
|
request.\n
|
||||||
|
include not only post req,but also get'''
|
||||||
|
env = self.environ
|
||||||
|
query_string=env['QUERY_STRING']
|
||||||
|
content_len= env.get('CONTENT_LENGTH', '0')
|
||||||
|
input = env.get('wsgi.input')
|
||||||
|
length = 0 if content_len == '' else int(content_len)
|
||||||
|
body=b''
|
||||||
|
request_items_dict={}
|
||||||
|
if length == 0:
|
||||||
|
if input is None:
|
||||||
|
return request_items_dict
|
||||||
|
if env.get('HTTP_TRANSFER_ENCODING','0') == 'chunked':
|
||||||
|
# readlines and read(no argument) will block
|
||||||
|
# convert byte str to number base 16
|
||||||
|
leng=int(input.readline(),16)
|
||||||
|
c=0
|
||||||
|
bdry=b''
|
||||||
|
data=[]
|
||||||
|
data_other=[]
|
||||||
|
while leng >0:
|
||||||
|
c+=1
|
||||||
|
dt = input.read(leng+2)
|
||||||
|
if c==1:
|
||||||
|
bdry=dt
|
||||||
|
elif c>=3:
|
||||||
|
# data
|
||||||
|
data_other.append(dt)
|
||||||
|
leng = int(input.readline(),16)
|
||||||
|
data_other=[item for item in data_other if item!=b'\r\n\r\n']
|
||||||
|
for item in data_other:
|
||||||
|
if bdry in item:
|
||||||
|
break
|
||||||
|
# only strip \r\n if there are extra \n
|
||||||
|
# eg b'?V\xc1\x8f>\xf9\xb1\n\r\n'
|
||||||
|
data.append(item[:-2])
|
||||||
|
request_items_dict['data']=b''.join(data)
|
||||||
|
others=data_other[len(data):]
|
||||||
|
boundary=others[0]
|
||||||
|
others=b''.join(others).split(boundary.strip())
|
||||||
|
others.pop()
|
||||||
|
others.pop(0)
|
||||||
|
for i in others:
|
||||||
|
i=i.splitlines()
|
||||||
|
key=re.findall(b'name="(.*?)"',i[2],flags=re.M)[0].decode('utf-8')
|
||||||
|
v=i[-1].decode('utf-8')
|
||||||
|
request_items_dict[key]=v
|
||||||
|
return request_items_dict
|
||||||
|
|
||||||
|
if query_string !='':
|
||||||
|
# GET method
|
||||||
|
body=query_string
|
||||||
|
request_items_dict=urllib.parse.parse_qs(body)
|
||||||
|
for k,v in request_items_dict.items():
|
||||||
|
request_items_dict[k]=''.join(v)
|
||||||
|
return request_items_dict
|
||||||
|
|
||||||
|
else:
|
||||||
|
body = env['wsgi.input'].read(length)
|
||||||
|
|
||||||
|
if body is None or body ==b'':
|
||||||
|
return request_items_dict
|
||||||
|
# process body to dict
|
||||||
|
repeat=body.splitlines()[0]
|
||||||
|
items=re.split(repeat,body)
|
||||||
|
# del first ,last item
|
||||||
|
items.pop()
|
||||||
|
items.pop(0)
|
||||||
|
for item in items:
|
||||||
|
if b'name="data"' in item:
|
||||||
|
data_field=None
|
||||||
|
# remove \r\n
|
||||||
|
if b'application/octet-stream' in item:
|
||||||
|
# Ankidroid case
|
||||||
|
item=re.sub(b'Content-Disposition: form-data; name="data"; filename="data"',b'',item)
|
||||||
|
item=re.sub(b'Content-Type: application/octet-stream',b'',item)
|
||||||
|
data_field=item.strip()
|
||||||
|
else:
|
||||||
|
# PKzip file stream and others
|
||||||
|
item=re.sub(b'Content-Disposition: form-data; name="data"; filename="data"',b'',item)
|
||||||
|
data_field=item.strip()
|
||||||
|
request_items_dict['data']=data_field
|
||||||
|
continue
|
||||||
|
item=re.sub(b'\r\n',b'',item,flags=re.MULTILINE)
|
||||||
|
key=re.findall(b'name="(.*?)"',item)[0].decode('utf-8')
|
||||||
|
v=item[item.rfind(b'"')+1:].decode('utf-8')
|
||||||
|
request_items_dict[key]=v
|
||||||
|
return request_items_dict
|
||||||
|
class chunked(object):
|
||||||
|
'''decorator'''
|
||||||
|
def __init__(self, func):
|
||||||
|
wraps(func)(self)
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
clss=args[0]
|
||||||
|
environ=args[1]
|
||||||
|
start_response = args[2]
|
||||||
|
b=Requests(environ)
|
||||||
|
args=(clss,b,)
|
||||||
|
w= self.__wrapped__(*args, **kwargs)
|
||||||
|
resp=Response(w)
|
||||||
|
return resp(environ, start_response)
|
||||||
|
def __get__(self, instance, cls):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return types.MethodType(self, instance)
|
||||||
class SyncApp:
|
class SyncApp:
|
||||||
valid_urls = SyncCollectionHandler.operations + SyncMediaHandler.operations + ['hostKey', 'upload', 'download']
|
valid_urls = SyncCollectionHandler.operations + SyncMediaHandler.operations + ['hostKey', 'upload', 'download']
|
||||||
|
|
||||||
@ -467,16 +594,18 @@ class SyncApp:
|
|||||||
# local copy in Anki
|
# local copy in Anki
|
||||||
return self.full_sync_manager.download(col, session)
|
return self.full_sync_manager.download(col, session)
|
||||||
|
|
||||||
@wsgify
|
@chunked
|
||||||
def __call__(self, req):
|
def __call__(self, req):
|
||||||
# Get and verify the session
|
# cgi file can only be read once,and will be blocked after being read once more
|
||||||
|
# so i call Requests.parse only once,and bind its return result to properties
|
||||||
|
# POST and params (set return result as property values)
|
||||||
|
req.params=req.parse
|
||||||
|
req.POST=req.params
|
||||||
try:
|
try:
|
||||||
hkey = req.params['k']
|
hkey = req.params['k']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
hkey = None
|
hkey = None
|
||||||
|
|
||||||
session = self.session_manager.load(hkey, self.create_session)
|
session = self.session_manager.load(hkey, self.create_session)
|
||||||
|
|
||||||
if session is None:
|
if session is None:
|
||||||
try:
|
try:
|
||||||
skey = req.POST['sk']
|
skey = req.POST['sk']
|
||||||
@ -490,7 +619,7 @@ class SyncApp:
|
|||||||
compression = 0
|
compression = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = req.POST['data'].file.read()
|
data = req.POST['data']
|
||||||
data = self._decode_data(data, compression)
|
data = self._decode_data(data, compression)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
data = {}
|
data = {}
|
||||||
@ -631,4 +760,5 @@ def main():
|
|||||||
finally:
|
finally:
|
||||||
shutdown()
|
shutdown()
|
||||||
|
|
||||||
if __name__ == '__main__': main()
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|||||||
0
src/ankisyncd_cli/__init__.py
Normal file
0
src/ankisyncd_cli/__init__.py
Normal file
4
src/ankisyncd_cli/__main__.py
Normal file
4
src/ankisyncd_cli/__main__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from ankisyncd_cli import ankisyncctl
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ankisyncctl.main()
|
||||||
6
src/ankisyncctl.py → src/ankisyncd_cli/ankisyncctl.py
Executable file → Normal file
6
src/ankisyncctl.py → src/ankisyncd_cli/ankisyncctl.py
Executable file → Normal file
@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
import ankisyncd.config
|
from ankisyncd import config
|
||||||
from ankisyncd.users import get_user_manager
|
from ankisyncd.users import get_user_manager
|
||||||
|
|
||||||
|
|
||||||
config = ankisyncd.config.load()
|
config = config.load()
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print("usage: {} <command> [<args>]".format(sys.argv[0]))
|
print("usage: {} <command> [<args>]".format(sys.argv[0]))
|
||||||
@ -1,113 +1,109 @@
|
|||||||
# THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!
|
# THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!
|
||||||
|
|
||||||
|
|
||||||
anki==2.1.43; python_version >= "3.8"
|
anki==2.1.49
|
||||||
appnope==0.1.2; platform_system == "Darwin" and python_version >= "3.8" and sys_platform == "darwin"
|
appnope==0.1.3; platform_system == "Darwin" or sys_platform == "darwin" or python_version >= "3.3" and sys_platform == "darwin"
|
||||||
argon2-cffi-bindings==21.2.0; python_version >= "3.6"
|
argon2-cffi==21.3.0
|
||||||
argon2-cffi==21.3.0; python_version >= "3.6"
|
argon2-cffi-bindings==21.2.0
|
||||||
asttokens==2.0.5; python_version >= "3.8"
|
asttokens==2.0.5
|
||||||
attrs==21.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
attrs==21.4.0
|
||||||
backcall==0.2.0; python_version >= "3.8"
|
backcall==0.2.0
|
||||||
beautifulsoup4==4.10.0; python_full_version > "3.0.0"
|
beautifulsoup4==4.11.1
|
||||||
black==21.12b0; python_full_version >= "3.6.2" and python_version >= "3.8"
|
bleach==5.0.1
|
||||||
bleach==4.1.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
certifi==2022.6.15
|
||||||
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"
|
cffi==1.15.1
|
||||||
cffi==1.15.0; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
|
charset-normalizer==2.1.0
|
||||||
charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3.8"
|
click==8.1.3
|
||||||
click==8.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8"
|
colorama==0.4.5; python_version >= "3.3" and sys_platform == "win32" or sys_platform == "win32" or platform_system == "Windows"
|
||||||
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.6.0
|
||||||
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
|
||||||
decorator==4.4.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.2.0")
|
defusedxml==0.7.1
|
||||||
defusedxml==0.7.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
distro==1.7.0
|
||||||
distro==1.6.0
|
entrypoints==0.4
|
||||||
entrypoints==0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
executing==0.8.3
|
||||||
executing==0.8.2; python_version >= "3.8"
|
fastjsonschema==2.15.3
|
||||||
ghp-import==2.0.2; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
ghp-import==2.1.0
|
||||||
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"
|
idna==3.3
|
||||||
importlib-metadata==4.10.0; python_version < "3.10" and python_version >= "3.7" and python_full_version >= "3.7.1"
|
importlib-metadata==4.12.0
|
||||||
importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.7" and python_full_version >= "3.7.1"
|
importlib-resources==5.8.0; python_version < "3.9"
|
||||||
ipykernel==6.7.0; python_version >= "3.7"
|
ipykernel==6.15.0
|
||||||
ipython-genutils==0.2.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
ipython==8.4.0
|
||||||
ipython==8.0.0; python_version >= "3.8"
|
ipython-genutils==0.2.0
|
||||||
ipywidgets==7.6.5
|
ipywidgets==7.7.1
|
||||||
jedi==0.18.1; python_version >= "3.8"
|
jedi==0.18.1
|
||||||
jinja2==3.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
jinja2==3.1.2
|
||||||
json5==0.9.6; python_version >= "3.5"
|
json5==0.9.8
|
||||||
jsonschema==4.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
jsonschema==4.6.1
|
||||||
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_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
jupyter-client==7.3.4
|
||||||
jupyterlab-server==1.2.0; python_version >= "3.5"
|
jupyter-console==6.4.4
|
||||||
jupyterlab-widgets==1.0.2; python_version >= "3.6"
|
jupyter-core==4.10.0
|
||||||
jupyterlab==2.3.2; python_version >= "3.5"
|
jupyterlab==2.3.2
|
||||||
jupytext==1.13.6; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
jupyterlab-pygments==0.2.2
|
||||||
markdown-it-py==1.1.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
jupyterlab-server==1.2.0
|
||||||
markdown==3.3.6; python_version >= "3.6"
|
jupyterlab-widgets==1.1.1; python_version >= "3.6"
|
||||||
markupsafe==2.0.1; python_version >= "3.6"
|
jupytext==1.13.8
|
||||||
matplotlib-inline==0.1.3; python_version >= "3.8"
|
markdown==3.3.7
|
||||||
mdit-py-plugins==0.3.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
markdown-it-py==2.1.0
|
||||||
mergedeep==1.3.4; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
markupsafe==2.1.1
|
||||||
mistune==0.8.4; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
matplotlib-inline==0.1.3
|
||||||
mkdocs-jupyter==0.19.0; python_full_version >= "3.7.1" and python_version < "4"
|
mdit-py-plugins==0.3.0
|
||||||
mkdocs-material-extensions==1.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
mdurl==0.1.1
|
||||||
mkdocs-material==8.1.7; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
mergedeep==1.3.4
|
||||||
mkdocs==1.2.3; python_version >= "3.6"
|
mistune==0.8.4
|
||||||
mypy-extensions==0.4.3; python_full_version >= "3.6.2" and python_version >= "3.8"
|
mkdocs==1.3.0
|
||||||
nbclient==0.5.10; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
mkdocs-jupyter==0.19.0
|
||||||
nbconvert==6.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
mkdocs-material==8.3.8
|
||||||
nbformat==5.1.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
mkdocs-material-extensions==1.0.3
|
||||||
nest-asyncio==1.5.4; python_full_version >= "3.7.1" and python_version >= "3.7" and python_version < "4"
|
nbclient==0.6.6
|
||||||
notebook==6.4.7; python_version >= "3.6"
|
nbconvert==6.5.0
|
||||||
orjson==3.6.5; platform_machine == "x86_64" and python_version >= "3.8"
|
nbformat==5.4.0
|
||||||
packaging==21.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
nest-asyncio==1.5.5
|
||||||
pandocfilters==1.5.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
notebook==6.4.12
|
||||||
parso==0.8.3; python_version >= "3.8"
|
orjson==3.7.5; platform_machine == "x86_64"
|
||||||
pathspec==0.9.0; python_full_version >= "3.6.2" and python_version >= "3.8"
|
packaging==21.3
|
||||||
pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.8"
|
pandocfilters==1.5.0
|
||||||
pickleshare==0.7.5; python_version >= "3.8"
|
parso==0.8.3
|
||||||
platformdirs==2.4.1; python_full_version >= "3.6.2" and python_version >= "3.8"
|
pexpect==4.8.0; python_version >= "3.3" and sys_platform != "win32" or sys_platform != "win32"
|
||||||
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"
|
pickleshare==0.7.5
|
||||||
prompt-toolkit==3.0.24; python_full_version >= "3.6.2" and python_version >= "3.8"
|
prometheus-client==0.14.1
|
||||||
protobuf==3.19.3; python_version >= "3.8"
|
prompt-toolkit==3.0.30
|
||||||
psutil==5.9.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
|
protobuf==4.21.2
|
||||||
ptyprocess==0.7.0; os_name != "nt" and python_version >= "3.8" and sys_platform != "win32"
|
psutil==5.9.1
|
||||||
pure-eval==0.2.1; python_version >= "3.8"
|
ptyprocess==0.7.0; sys_platform != "win32" or os_name != "nt" or python_version >= "3.3" and sys_platform != "win32"
|
||||||
py==1.11.0; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
|
pure-eval==0.2.2
|
||||||
pycparser==2.21; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
|
py==1.11.0; implementation_name == "pypy"
|
||||||
pygments==2.11.2; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8"
|
pycparser==2.21
|
||||||
pymdown-extensions==9.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
pygments==2.12.0
|
||||||
pyparsing==3.0.6; python_version >= "3.6"
|
pymdown-extensions==9.5
|
||||||
pyrsistent==0.18.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
pyparsing==3.0.9
|
||||||
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"
|
pyrsistent==0.18.1
|
||||||
python-dateutil==2.8.2; python_full_version >= "3.6.1" and python_version >= "3.7"
|
python-dateutil==2.8.2
|
||||||
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"
|
pywin32==304; sys_platform == "win32" and platform_python_implementation != "PyPy"
|
||||||
pywinpty==1.1.6; os_name == "nt" and python_version >= "3.6"
|
pywinpty==2.0.5; os_name == "nt"
|
||||||
pyyaml-env-tag==0.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
pyyaml==6.0
|
||||||
pyyaml==6.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
pyyaml-env-tag==0.1
|
||||||
pyzmq==22.3.0; python_full_version >= "3.6.1" and python_version >= "3.7"
|
pyzmq==23.2.0
|
||||||
qtconsole==5.2.2; python_version >= "3.6"
|
qtconsole==5.3.1
|
||||||
qtpy==2.0.0; python_version >= "3.6"
|
qtpy==2.1.0
|
||||||
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
|
requests==2.28.1
|
||||||
send2trash==1.8.0
|
send2trash==1.8.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")
|
six==1.16.0
|
||||||
soupsieve==2.3.1; python_full_version > "3.0.0" and python_version >= "3.8"
|
soupsieve==2.3.2.post1
|
||||||
stack-data==0.1.4; python_version >= "3.8"
|
stack-data==0.3.0
|
||||||
terminado==0.12.1; python_version >= "3.6"
|
stringcase==1.2.0
|
||||||
testpath==0.5.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
terminado==0.15.0
|
||||||
toml==0.10.2; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
tinycss2==1.1.1
|
||||||
tomli==1.2.3; python_full_version >= "3.6.2" and python_version >= "3.8"
|
toml==0.10.2
|
||||||
tornado==6.1; python_full_version >= "3.6.1" and python_version >= "3.7"
|
tornado==6.1
|
||||||
traitlets==5.1.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8"
|
traitlets==5.3.0
|
||||||
typing-extensions==4.0.1
|
urllib3==1.26.9
|
||||||
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.1.2
|
||||||
waitress==2.0.0; python_full_version >= "3.6.0"
|
watchdog==2.1.9
|
||||||
watchdog==2.1.6; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
wcwidth==0.2.5
|
||||||
wcwidth==0.2.5; python_full_version >= "3.6.2" and python_version >= "3.8"
|
webencodings==0.5.1
|
||||||
webencodings==0.5.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
webob==1.8.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
|
webtest==2.0.35
|
||||||
widgetsnbextension==3.5.2
|
widgetsnbextension==3.6.1
|
||||||
zipp==3.7.0; python_version < "3.9" and python_version >= "3.7" and python_full_version >= "3.7.1"
|
zipp==3.8.0
|
||||||
-e src/.
|
-e src/.
|
||||||
|
|||||||
@ -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.43; python_version >= "3.8"
|
anki==2.1.49
|
||||||
beautifulsoup4==4.10.0; python_full_version > "3.0.0"
|
beautifulsoup4==4.11.1
|
||||||
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"
|
certifi==2022.6.15
|
||||||
charset-normalizer==2.0.10; python_full_version >= "3.6.0" and python_version >= "3.8"
|
charset-normalizer==2.1.0
|
||||||
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
|
||||||
distro==1.6.0
|
distro==1.7.0
|
||||||
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"
|
idna==3.3
|
||||||
importlib-metadata==4.10.0; python_version < "3.10" and python_version >= "3.7"
|
importlib-metadata==4.12.0
|
||||||
markdown==3.3.6; python_version >= "3.6"
|
markdown==3.3.7
|
||||||
orjson==3.6.5; platform_machine == "x86_64" and python_version >= "3.8"
|
orjson==3.7.5; platform_machine == "x86_64"
|
||||||
protobuf==3.19.3; python_version >= "3.8"
|
protobuf==4.21.2
|
||||||
psutil==5.9.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
|
psutil==5.9.1
|
||||||
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.28.1
|
||||||
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
|
|
||||||
send2trash==1.8.0
|
send2trash==1.8.0
|
||||||
soupsieve==2.3.1; python_full_version > "3.0.0" and python_version >= "3.8"
|
soupsieve==2.3.2.post1
|
||||||
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"
|
stringcase==1.2.0
|
||||||
webob==1.8.7; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0")
|
urllib3==1.26.9
|
||||||
zipp==3.7.0; python_version < "3.10" and python_version >= "3.7"
|
webob==1.8.7
|
||||||
|
zipp==3.8.0
|
||||||
|
|||||||
12
src/setup.py
Normal file
12
src/setup.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
# TODO: Generate from or parse values from pyproject.toml.
|
||||||
|
setup(
|
||||||
|
name="anki-sync-server",
|
||||||
|
version="2.3.0",
|
||||||
|
description="Self-hosted Anki Sync Server.",
|
||||||
|
author="Anki Community",
|
||||||
|
author_email="kothary.vikash+ankicommunity@gmail.com",
|
||||||
|
packages=find_packages(),
|
||||||
|
url='https://ankicommunity.github.io/'
|
||||||
|
)
|
||||||
@ -3,7 +3,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from anki import Collection
|
from anki.collection import Collection
|
||||||
|
|
||||||
class CollectionUtils:
|
class CollectionUtils:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user