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
|
||||
/venv
|
||||
|
||||
/config/*
|
||||
!/config/.env.example
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
13
Makefile
13
Makefile
@ -31,13 +31,26 @@ run:
|
||||
@if [[ -f "scripts/${*}.sh" ]]; then \
|
||||
${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.
|
||||
init:
|
||||
@${POETRY} install
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@${POETRY} build
|
||||
|
||||
.PHONY: release #: Create new Git release and tags.
|
||||
release: release-branch release-tags
|
||||
|
||||
.PHONY: publish
|
||||
publish: build
|
||||
@${POETRY} publish
|
||||
|
||||
.PHONY: open
|
||||
open:
|
||||
@${OPEN} ${ANKISYNCD_URL}
|
||||
59
README.md
59
README.md
@ -31,7 +31,9 @@ It supports Python 3 and Anki 2.1.
|
||||
- [AnkiDroid](#ankidroid)
|
||||
- [Development](#development)
|
||||
- [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)
|
||||
</details>
|
||||
|
||||
@ -42,39 +44,41 @@ Installing
|
||||
|
||||
$ 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:
|
||||
|
||||
$ ./ankisyncctl.py adduser <username>
|
||||
|
||||
4. Setup a proxy to unchunk the requests.
|
||||
|
||||
Webob does not support the header "Transfer-Encoding: chunked" used by Anki
|
||||
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.
|
||||
|
||||
4. Setup a proxy to trans-write the requests (Optional) .
|
||||
Ankisyncd currently 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.
|
||||
|
||||
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`.
|
||||
Then configure Nginx to listen on port `27701` and forward the unchunked
|
||||
requests to ankisyncd.
|
||||
|
||||
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
|
||||
server {
|
||||
listen 27701;
|
||||
server_name default;
|
||||
location / {
|
||||
proxy_http_version 1.0;
|
||||
proxy_pass http://127.0.0.1:27702/;
|
||||
}
|
||||
listen 27701;
|
||||
server_name default;
|
||||
location / {
|
||||
proxy_http_version 1.0;
|
||||
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:
|
||||
|
||||
```
|
||||
@ -88,6 +92,7 @@ Installing (Docker)
|
||||
|
||||
Follow [these instructions](https://github.com/ankicommunity/anki-devops-services#about-this-docker-image).
|
||||
|
||||
|
||||
Setting up Anki
|
||||
---------------
|
||||
|
||||
@ -138,7 +143,7 @@ and put it in `~/Anki/addons`.
|
||||
anki.sync.SYNC_BASE = addr
|
||||
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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
See [ENVVAR configuration overrides](#envvar-configuration-overrides) for more information.
|
||||
See [Configuration](#configuration) for more information.
|
||||
|
||||
2. Download Python dependencies.
|
||||
|
||||
@ -185,8 +190,9 @@ $ make init
|
||||
$ make tests
|
||||
```
|
||||
|
||||
ENVVAR configuration overrides
|
||||
------------------------------
|
||||
## Configuration
|
||||
|
||||
### Environment Variables (preferred)
|
||||
|
||||
Configuration values can be set via environment variables using `ANKISYNCD_` prepended
|
||||
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`.
|
||||
|
||||
* 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
|
||||
-----------------------------------
|
||||
|
||||
|
||||
@ -25,6 +25,11 @@ MKDOCS_CMD=serve
|
||||
### JUPYTER_CMD
|
||||
### JUPYTER_NOTEBOOK_DIR
|
||||
|
||||
## Poetry
|
||||
### POETRY_PYPI_TOKEN_PYPI=
|
||||
### POETRY_HTTP_BASIC_PYPI_USERNAME=
|
||||
### POETRY_HTTP_BASIC_PYPI_PASSWORD=
|
||||
|
||||
## Make
|
||||
AWK=awk
|
||||
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."
|
||||
authors = ["Vikash Kothary <kothary.vikash@gmail.com>"]
|
||||
packages = [
|
||||
{ include = "ankisyncd", from = "src" }
|
||||
{ include = "*", from = "src" }
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
anki = "2.1.43"
|
||||
anki = "2.1.49"
|
||||
beautifulsoup4 = "^4.9.1"
|
||||
requests = "^2.24.0"
|
||||
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
|
||||
|
||||
## TODO: get package version from pyproject.toml
|
||||
CURRENT_VERSION=2.3.0
|
||||
CURRENT_VERSION=v2.3.0
|
||||
|
||||
## Create GitHub Release
|
||||
git tag -a ${CURRENT_VERSION} -m "v${CURRENT_VERSION}"
|
||||
|
||||
@ -41,6 +41,21 @@ class Syncer(object):
|
||||
self.col = col
|
||||
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):
|
||||
return dict(
|
||||
mod=self.col.mod,
|
||||
@ -58,7 +73,7 @@ class Syncer(object):
|
||||
decks=self.getDecks(),
|
||||
tags=self.getTags())
|
||||
if self.lnewer:
|
||||
d['conf'] = json.loads(self.col.backend.get_all_config())
|
||||
d['conf'] = self.col.all_config()
|
||||
d['crt'] = self.col.crt
|
||||
return d
|
||||
|
||||
@ -66,7 +81,6 @@ class Syncer(object):
|
||||
# then the other objects
|
||||
self.mergeModels(rchg['models'])
|
||||
self.mergeDecks(rchg['decks'])
|
||||
self.mergeTags(rchg['tags'])
|
||||
if 'conf' in rchg:
|
||||
self.mergeConf(rchg['conf'])
|
||||
# this was left out of earlier betas
|
||||
@ -105,50 +119,50 @@ select id from notes where mid = ?) limit 1"""
|
||||
return False
|
||||
return True
|
||||
|
||||
def sanityCheck(self, full):
|
||||
if not self.basicCheck():
|
||||
return "failed basic check"
|
||||
for t in "cards", "notes", "revlog", "graves":
|
||||
if self.col.db.scalar(
|
||||
"select count() from %s where usn = -1" % t):
|
||||
return "%s had usn = -1" % t
|
||||
for g in self.col.decks.all():
|
||||
if g['usn'] == -1:
|
||||
return "deck had usn = -1"
|
||||
for t, usn in self.allItems():
|
||||
if usn == -1:
|
||||
return "tag had usn = -1"
|
||||
found = False
|
||||
for m in self.col.models.all():
|
||||
if m['usn'] == -1:
|
||||
return "model had usn = -1"
|
||||
if found:
|
||||
self.col.models.save()
|
||||
def sanityCheck(self):
|
||||
tables=["cards",
|
||||
"notes",
|
||||
"revlog",
|
||||
"graves",
|
||||
"decks",
|
||||
"deck_config",
|
||||
"tags",
|
||||
"notetypes",
|
||||
]
|
||||
for tb in tables:
|
||||
if self.col.db.scalar(f'select null from {tb} where usn=-1'):
|
||||
return f'table had usn=-1: {tb}'
|
||||
self.col.sched.reset()
|
||||
# check for missing parent decks
|
||||
#self.col.sched.deckDueList()
|
||||
|
||||
# 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 [
|
||||
list(self.col.sched.counts()),
|
||||
list([0,0,0]),
|
||||
self.col.db.scalar("select count() from cards"),
|
||||
self.col.db.scalar("select count() from notes"),
|
||||
self.col.db.scalar("select count() from revlog"),
|
||||
self.col.db.scalar("select count() from graves"),
|
||||
len(self.col.models.all()),
|
||||
len(self.col.decks.all()),
|
||||
len(self.col.decks.allConf()),
|
||||
len(self.col.decks.all_config()),
|
||||
]
|
||||
|
||||
def usnLim(self):
|
||||
return "usn = -1"
|
||||
|
||||
def finish(self, mod=None):
|
||||
self.col.ls = mod
|
||||
self.col._usn = self.maxUsn + 1
|
||||
def finish(self, now=None):
|
||||
if now is not None:
|
||||
# ensure we save the mod time even if no changes made
|
||||
self.col.db.mod = True
|
||||
self.col.save(mod=mod)
|
||||
return mod
|
||||
self.set_modified_time(now)
|
||||
self.set_last_sync(now)
|
||||
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
|
||||
##########################################################################
|
||||
@ -195,67 +209,26 @@ from notes where %s""" % lim, self.maxUsn)
|
||||
# Deletions
|
||||
##########################################################################
|
||||
|
||||
def removed(self):
|
||||
cards = []
|
||||
notes = []
|
||||
decks = []
|
||||
|
||||
curs = self.col.db.execute(
|
||||
"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
|
||||
def add_grave(self, ids: List[int], type: int,usn: int):
|
||||
items=[(id,type,usn) for id in ids]
|
||||
# make sure table graves fields order and schema version match
|
||||
# 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)
|
||||
|
||||
def apply_graves(self, graves,latest_usn: int):
|
||||
# remove card and the card's orphaned notes
|
||||
self.col.remove_cards_and_orphaned_notes(graves['cards'])
|
||||
|
||||
self.add_grave(graves['cards'], REM_CARD,latest_usn)
|
||||
# only 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
|
||||
# will have once effect
|
||||
for oid in graves['decks']:
|
||||
self.col.decks.rem(oid)
|
||||
|
||||
|
||||
# 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)
|
||||
self.col.decks.remove(graves['decks'])
|
||||
self.add_grave(graves['decks'], REM_DECK,latest_usn)
|
||||
|
||||
# Models
|
||||
##########################################################################
|
||||
@ -342,7 +315,8 @@ from notes where %s""" % lim, self.maxUsn)
|
||||
for r in data:
|
||||
if r[0] not in lmods or lmods[r[0]] < r[modIdx]:
|
||||
update.append(r)
|
||||
self.col.log(table, data)
|
||||
# replace col.log by just using print
|
||||
print(table, data)
|
||||
return update
|
||||
|
||||
def mergeCards(self, cards):
|
||||
@ -356,7 +330,7 @@ from notes where %s""" % lim, self.maxUsn)
|
||||
self.col.db.executemany(
|
||||
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
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
|
||||
##########################################################################
|
||||
|
||||
@ -15,29 +15,25 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
import zipfile
|
||||
from configparser import ConfigParser
|
||||
from sqlite3 import dbapi2 as sqlite
|
||||
|
||||
import types
|
||||
from webob import Response
|
||||
from webob.dec import wsgify
|
||||
from webob.exc import *
|
||||
|
||||
import urllib.parse
|
||||
from functools import wraps
|
||||
from anki.collection import Collection
|
||||
import anki.db
|
||||
import anki.utils
|
||||
from anki.consts import REM_CARD, REM_NOTE
|
||||
|
||||
from ankisyncd.full_sync import get_full_sync_manager
|
||||
from ankisyncd.sessions import get_session_manager
|
||||
from ankisyncd.sync import Syncer, SYNC_VER, SYNC_ZIP_SIZE, SYNC_ZIP_COUNT
|
||||
@ -97,8 +93,8 @@ class SyncCollectionHandler(Syncer):
|
||||
|
||||
return {
|
||||
'mod': self.col.mod,
|
||||
'scm': self.col.scm,
|
||||
'usn': self.col._usn,
|
||||
'scm': self.scm(),
|
||||
'usn': self.col.usn(),
|
||||
'ts': anki.utils.intTime(),
|
||||
'musn': self.col.media.lastUsn(),
|
||||
'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
|
||||
# just enable the V2 scheduler in the serve code.
|
||||
|
||||
self.maxUsn = self.col._usn
|
||||
self.maxUsn = self.col.usn()
|
||||
self.minUsn = minUsn
|
||||
self.lnewer = not lnewer
|
||||
# fetch local/server graves
|
||||
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)
|
||||
# handle AnkiDroid using old protocol
|
||||
# Only if Operations like deleting deck are performed on Ankidroid
|
||||
# can (client) graves is not None
|
||||
if graves is not None:
|
||||
self.apply_graves(graves,self.maxUsn)
|
||||
return lgraves
|
||||
|
||||
def applyGraves(self, chunk):
|
||||
self.remove(chunk)
|
||||
self.apply_graves(chunk,self.maxUsn)
|
||||
|
||||
def applyChanges(self, changes):
|
||||
self.rchg = changes
|
||||
@ -138,17 +135,18 @@ class SyncCollectionHandler(Syncer):
|
||||
self.mergeChanges(lchg, self.rchg)
|
||||
return lchg
|
||||
|
||||
def sanityCheck2(self, client, full=None):
|
||||
server = self.sanityCheck(full)
|
||||
def sanityCheck2(self, client):
|
||||
client[0]=[0,0,0]
|
||||
server = self.sanityCheck()
|
||||
if client != server:
|
||||
logger.info(
|
||||
f"sanity check failed with server: {server} client: {client}"
|
||||
)
|
||||
|
||||
return dict(status="bad", c=client, s=server)
|
||||
return dict(status="ok")
|
||||
|
||||
def finish(self, mod=None):
|
||||
|
||||
def finish(self):
|
||||
return super().finish(anki.utils.intTime(1000))
|
||||
|
||||
# This function had to be put here in its entirety because Syncer.removed()
|
||||
@ -178,7 +176,7 @@ class SyncCollectionHandler(Syncer):
|
||||
def getDecks(self):
|
||||
return [
|
||||
[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):
|
||||
@ -338,7 +336,6 @@ class SyncMediaHandler:
|
||||
if lastUsn < server_lastUsn or lastUsn == 0:
|
||||
for fname,usn,csum, in self.col.media.changes(lastUsn):
|
||||
result.append([fname, usn, csum])
|
||||
|
||||
# anki assumes server_lastUsn == result[-1][1]
|
||||
# ref: anki/sync.py:720 (commit cca3fcb2418880d0430a5c5c2e6b81ba260065b7)
|
||||
result.reverse()
|
||||
@ -394,7 +391,137 @@ class SyncUserSession:
|
||||
# for inactivity and then later re-open it (creating a new Collection object).
|
||||
handler.col = col
|
||||
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:
|
||||
valid_urls = SyncCollectionHandler.operations + SyncMediaHandler.operations + ['hostKey', 'upload', 'download']
|
||||
|
||||
@ -467,16 +594,18 @@ class SyncApp:
|
||||
# local copy in Anki
|
||||
return self.full_sync_manager.download(col, session)
|
||||
|
||||
@wsgify
|
||||
@chunked
|
||||
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:
|
||||
hkey = req.params['k']
|
||||
except KeyError:
|
||||
hkey = None
|
||||
|
||||
session = self.session_manager.load(hkey, self.create_session)
|
||||
|
||||
if session is None:
|
||||
try:
|
||||
skey = req.POST['sk']
|
||||
@ -490,7 +619,7 @@ class SyncApp:
|
||||
compression = 0
|
||||
|
||||
try:
|
||||
data = req.POST['data'].file.read()
|
||||
data = req.POST['data']
|
||||
data = self._decode_data(data, compression)
|
||||
except KeyError:
|
||||
data = {}
|
||||
@ -631,4 +760,5 @@ def main():
|
||||
finally:
|
||||
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
|
||||
import os
|
||||
|
||||
import sys
|
||||
import getpass
|
||||
|
||||
import ankisyncd.config
|
||||
from ankisyncd import config
|
||||
from ankisyncd.users import get_user_manager
|
||||
|
||||
|
||||
config = ankisyncd.config.load()
|
||||
config = config.load()
|
||||
|
||||
def usage():
|
||||
print("usage: {} <command> [<args>]".format(sys.argv[0]))
|
||||
@ -1,113 +1,109 @@
|
||||
# THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!
|
||||
|
||||
|
||||
anki==2.1.43; python_version >= "3.8"
|
||||
appnope==0.1.2; platform_system == "Darwin" and python_version >= "3.8" and sys_platform == "darwin"
|
||||
argon2-cffi-bindings==21.2.0; python_version >= "3.6"
|
||||
argon2-cffi==21.3.0; python_version >= "3.6"
|
||||
asttokens==2.0.5; python_version >= "3.8"
|
||||
attrs==21.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
backcall==0.2.0; python_version >= "3.8"
|
||||
beautifulsoup4==4.10.0; python_full_version > "3.0.0"
|
||||
black==21.12b0; python_full_version >= "3.6.2" and python_version >= "3.8"
|
||||
bleach==4.1.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
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.0; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
|
||||
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")
|
||||
defusedxml==0.7.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
distro==1.6.0
|
||||
entrypoints==0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
executing==0.8.2; python_version >= "3.8"
|
||||
ghp-import==2.0.2; python_full_version >= "3.7.1" and python_version < "4" and 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"
|
||||
importlib-metadata==4.10.0; python_version < "3.10" and python_version >= "3.7" and python_full_version >= "3.7.1"
|
||||
importlib-resources==5.4.0; python_version < "3.9" and python_version >= "3.7" and python_full_version >= "3.7.1"
|
||||
ipykernel==6.7.0; python_version >= "3.7"
|
||||
ipython-genutils==0.2.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
ipython==8.0.0; python_version >= "3.8"
|
||||
ipywidgets==7.6.5
|
||||
jedi==0.18.1; python_version >= "3.8"
|
||||
jinja2==3.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
json5==0.9.6; python_version >= "3.5"
|
||||
jsonschema==4.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
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"
|
||||
anki==2.1.49
|
||||
appnope==0.1.3; platform_system == "Darwin" or sys_platform == "darwin" or python_version >= "3.3" and sys_platform == "darwin"
|
||||
argon2-cffi==21.3.0
|
||||
argon2-cffi-bindings==21.2.0
|
||||
asttokens==2.0.5
|
||||
attrs==21.4.0
|
||||
backcall==0.2.0
|
||||
beautifulsoup4==4.11.1
|
||||
bleach==5.0.1
|
||||
certifi==2022.6.15
|
||||
cffi==1.15.1
|
||||
charset-normalizer==2.1.0
|
||||
click==8.1.3
|
||||
colorama==0.4.5; python_version >= "3.3" and sys_platform == "win32" or sys_platform == "win32" or platform_system == "Windows"
|
||||
debugpy==1.6.0
|
||||
decorator==4.4.2
|
||||
defusedxml==0.7.1
|
||||
distro==1.7.0
|
||||
entrypoints==0.4
|
||||
executing==0.8.3
|
||||
fastjsonschema==2.15.3
|
||||
ghp-import==2.1.0
|
||||
idna==3.3
|
||||
importlib-metadata==4.12.0
|
||||
importlib-resources==5.8.0; python_version < "3.9"
|
||||
ipykernel==6.15.0
|
||||
ipython==8.4.0
|
||||
ipython-genutils==0.2.0
|
||||
ipywidgets==7.7.1
|
||||
jedi==0.18.1
|
||||
jinja2==3.1.2
|
||||
json5==0.9.8
|
||||
jsonschema==4.6.1
|
||||
jupyter==1.0.0
|
||||
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-widgets==1.0.2; python_version >= "3.6"
|
||||
jupyterlab==2.3.2; 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-it-py==1.1.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
||||
markdown==3.3.6; python_version >= "3.6"
|
||||
markupsafe==2.0.1; python_version >= "3.6"
|
||||
matplotlib-inline==0.1.3; python_version >= "3.8"
|
||||
mdit-py-plugins==0.3.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
||||
mergedeep==1.3.4; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
||||
mistune==0.8.4; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
mkdocs-jupyter==0.19.0; python_full_version >= "3.7.1" and python_version < "4"
|
||||
mkdocs-material-extensions==1.0.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
||||
mkdocs-material==8.1.7; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
||||
mkdocs==1.2.3; python_version >= "3.6"
|
||||
mypy-extensions==0.4.3; python_full_version >= "3.6.2" and python_version >= "3.8"
|
||||
nbclient==0.5.10; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
nbconvert==6.4.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
nbformat==5.1.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
nest-asyncio==1.5.4; python_full_version >= "3.7.1" and python_version >= "3.7" and python_version < "4"
|
||||
notebook==6.4.7; python_version >= "3.6"
|
||||
orjson==3.6.5; platform_machine == "x86_64" and python_version >= "3.8"
|
||||
packaging==21.3; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
pandocfilters==1.5.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
parso==0.8.3; python_version >= "3.8"
|
||||
pathspec==0.9.0; python_full_version >= "3.6.2" and python_version >= "3.8"
|
||||
pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.8"
|
||||
pickleshare==0.7.5; python_version >= "3.8"
|
||||
platformdirs==2.4.1; python_full_version >= "3.6.2" and python_version >= "3.8"
|
||||
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"
|
||||
prompt-toolkit==3.0.24; python_full_version >= "3.6.2" and python_version >= "3.8"
|
||||
protobuf==3.19.3; python_version >= "3.8"
|
||||
psutil==5.9.0; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
|
||||
ptyprocess==0.7.0; os_name != "nt" and python_version >= "3.8" and sys_platform != "win32"
|
||||
pure-eval==0.2.1; python_version >= "3.8"
|
||||
py==1.11.0; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
|
||||
pycparser==2.21; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.6.1"
|
||||
pygments==2.11.2; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8"
|
||||
pymdown-extensions==9.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
||||
pyparsing==3.0.6; python_version >= "3.6"
|
||||
pyrsistent==0.18.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7"
|
||||
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"
|
||||
python-dateutil==2.8.2; python_full_version >= "3.6.1" and python_version >= "3.7"
|
||||
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"
|
||||
pywinpty==1.1.6; os_name == "nt" and python_version >= "3.6"
|
||||
pyyaml-env-tag==0.1; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.6"
|
||||
pyyaml==6.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.1"
|
||||
pyzmq==22.3.0; python_full_version >= "3.6.1" and python_version >= "3.7"
|
||||
qtconsole==5.2.2; python_version >= "3.6"
|
||||
qtpy==2.0.0; 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")
|
||||
jupyter-client==7.3.4
|
||||
jupyter-console==6.4.4
|
||||
jupyter-core==4.10.0
|
||||
jupyterlab==2.3.2
|
||||
jupyterlab-pygments==0.2.2
|
||||
jupyterlab-server==1.2.0
|
||||
jupyterlab-widgets==1.1.1; python_version >= "3.6"
|
||||
jupytext==1.13.8
|
||||
markdown==3.3.7
|
||||
markdown-it-py==2.1.0
|
||||
markupsafe==2.1.1
|
||||
matplotlib-inline==0.1.3
|
||||
mdit-py-plugins==0.3.0
|
||||
mdurl==0.1.1
|
||||
mergedeep==1.3.4
|
||||
mistune==0.8.4
|
||||
mkdocs==1.3.0
|
||||
mkdocs-jupyter==0.19.0
|
||||
mkdocs-material==8.3.8
|
||||
mkdocs-material-extensions==1.0.3
|
||||
nbclient==0.6.6
|
||||
nbconvert==6.5.0
|
||||
nbformat==5.4.0
|
||||
nest-asyncio==1.5.5
|
||||
notebook==6.4.12
|
||||
orjson==3.7.5; platform_machine == "x86_64"
|
||||
packaging==21.3
|
||||
pandocfilters==1.5.0
|
||||
parso==0.8.3
|
||||
pexpect==4.8.0; python_version >= "3.3" and sys_platform != "win32" or sys_platform != "win32"
|
||||
pickleshare==0.7.5
|
||||
prometheus-client==0.14.1
|
||||
prompt-toolkit==3.0.30
|
||||
protobuf==4.21.2
|
||||
psutil==5.9.1
|
||||
ptyprocess==0.7.0; sys_platform != "win32" or os_name != "nt" or python_version >= "3.3" and sys_platform != "win32"
|
||||
pure-eval==0.2.2
|
||||
py==1.11.0; implementation_name == "pypy"
|
||||
pycparser==2.21
|
||||
pygments==2.12.0
|
||||
pymdown-extensions==9.5
|
||||
pyparsing==3.0.9
|
||||
pyrsistent==0.18.1
|
||||
python-dateutil==2.8.2
|
||||
pywin32==304; sys_platform == "win32" and platform_python_implementation != "PyPy"
|
||||
pywinpty==2.0.5; os_name == "nt"
|
||||
pyyaml==6.0
|
||||
pyyaml-env-tag==0.1
|
||||
pyzmq==23.2.0
|
||||
qtconsole==5.3.1
|
||||
qtpy==2.1.0
|
||||
requests==2.28.1
|
||||
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")
|
||||
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")
|
||||
six==1.16.0
|
||||
soupsieve==2.3.2.post1
|
||||
stack-data==0.3.0
|
||||
stringcase==1.2.0
|
||||
terminado==0.15.0
|
||||
tinycss2==1.1.1
|
||||
toml==0.10.2
|
||||
tornado==6.1
|
||||
traitlets==5.3.0
|
||||
urllib3==1.26.9
|
||||
waitress==2.1.2
|
||||
watchdog==2.1.9
|
||||
wcwidth==0.2.5
|
||||
webencodings==0.5.1
|
||||
webob==1.8.7
|
||||
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"
|
||||
widgetsnbextension==3.6.1
|
||||
zipp==3.8.0
|
||||
-e src/.
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
# THE FILE WAS GENERATED BY POETRY, DO NOT EDIT!
|
||||
|
||||
|
||||
anki==2.1.43; python_version >= "3.8"
|
||||
beautifulsoup4==4.10.0; python_full_version > "3.0.0"
|
||||
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"
|
||||
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")
|
||||
distro==1.6.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"
|
||||
importlib-metadata==4.10.0; python_version < "3.10" and python_version >= "3.7"
|
||||
markdown==3.3.6; python_version >= "3.6"
|
||||
orjson==3.6.5; platform_machine == "x86_64" and python_version >= "3.8"
|
||||
protobuf==3.19.3; python_version >= "3.8"
|
||||
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.6.0" and python_version >= "3.8"
|
||||
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
|
||||
anki==2.1.49
|
||||
beautifulsoup4==4.11.1
|
||||
certifi==2022.6.15
|
||||
charset-normalizer==2.1.0
|
||||
decorator==4.4.2
|
||||
distro==1.7.0
|
||||
idna==3.3
|
||||
importlib-metadata==4.12.0
|
||||
markdown==3.3.7
|
||||
orjson==3.7.5; platform_machine == "x86_64"
|
||||
protobuf==4.21.2
|
||||
psutil==5.9.1
|
||||
requests==2.28.1
|
||||
send2trash==1.8.0
|
||||
soupsieve==2.3.1; python_full_version > "3.0.0" and 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"
|
||||
webob==1.8.7; (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"
|
||||
soupsieve==2.3.2.post1
|
||||
stringcase==1.2.0
|
||||
urllib3==1.26.9
|
||||
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 tempfile
|
||||
|
||||
from anki import Collection
|
||||
from anki.collection import Collection
|
||||
|
||||
class CollectionUtils:
|
||||
"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user