hanky 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hanky-0.0.1/.gitignore +129 -0
- hanky-0.0.1/LICENSE +21 -0
- hanky-0.0.1/PKG-INFO +157 -0
- hanky-0.0.1/README.md +136 -0
- hanky-0.0.1/pyproject.toml +57 -0
- hanky-0.0.1/src/hanky/__about__.py +1 -0
- hanky-0.0.1/src/hanky/__init__.py +1 -0
- hanky-0.0.1/src/hanky/__main__.py +5 -0
- hanky-0.0.1/src/hanky/cli.py +135 -0
- hanky-0.0.1/src/hanky/config.py +56 -0
- hanky-0.0.1/src/hanky/fs.py +35 -0
- hanky-0.0.1/src/hanky/hanky.py +351 -0
- hanky-0.0.1/src/hanky/media.py +21 -0
hanky-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
target/
|
|
76
|
+
|
|
77
|
+
# Jupyter Notebook
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
|
|
80
|
+
# IPython
|
|
81
|
+
profile_default/
|
|
82
|
+
ipython_config.py
|
|
83
|
+
|
|
84
|
+
# pyenv
|
|
85
|
+
.python-version
|
|
86
|
+
|
|
87
|
+
# pipenv
|
|
88
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
89
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
90
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
91
|
+
# install all needed dependencies.
|
|
92
|
+
#Pipfile.lock
|
|
93
|
+
|
|
94
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
95
|
+
__pypackages__/
|
|
96
|
+
|
|
97
|
+
# Celery stuff
|
|
98
|
+
celerybeat-schedule
|
|
99
|
+
celerybeat.pid
|
|
100
|
+
|
|
101
|
+
# SageMath parsed files
|
|
102
|
+
*.sage.py
|
|
103
|
+
|
|
104
|
+
# Environments
|
|
105
|
+
.env
|
|
106
|
+
.venv
|
|
107
|
+
env/
|
|
108
|
+
venv/
|
|
109
|
+
ENV/
|
|
110
|
+
env.bak/
|
|
111
|
+
venv.bak/
|
|
112
|
+
|
|
113
|
+
# Spyder project settings
|
|
114
|
+
.spyderproject
|
|
115
|
+
.spyproject
|
|
116
|
+
|
|
117
|
+
# Rope project settings
|
|
118
|
+
.ropeproject
|
|
119
|
+
|
|
120
|
+
# mkdocs documentation
|
|
121
|
+
/site
|
|
122
|
+
|
|
123
|
+
# mypy
|
|
124
|
+
.mypy_cache/
|
|
125
|
+
.dmypy.json
|
|
126
|
+
dmypy.json
|
|
127
|
+
|
|
128
|
+
# Pyre type checker
|
|
129
|
+
.pyre/
|
hanky-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hanky-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hanky
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Simple library and command line tool for loading flash cards into anki.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Haeata-Ash/hanky
|
|
6
|
+
Project-URL: Issues, https://github.com/Haeata-Ash/hanky/issues
|
|
7
|
+
Author-email: HBA <hanky-pypi.8ebs0@simplelogin.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: anki
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: Unix
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Requires-Dist: anki
|
|
16
|
+
Requires-Dist: psutil
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# hanky
|
|
23
|
+
|
|
24
|
+
[](https://github.com/pypa/hatch)
|
|
25
|
+
|
|
26
|
+
Library and command line application for loading flash cards into anki.
|
|
27
|
+
|
|
28
|
+
> **:information_source: Note:**
|
|
29
|
+
> This project is currently in alpha and is not stable.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install via pip:
|
|
34
|
+
|
|
35
|
+
`pip install hanky`
|
|
36
|
+
|
|
37
|
+
Optionally install text to speech code seen in tutorial:
|
|
38
|
+
|
|
39
|
+
` pip install hanky[toc]`
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
Currently two configuration options are exposed:
|
|
44
|
+
|
|
45
|
+
- `anki_database`: tells hanky where to find the anki collection (an sqlite database where anki stores flash cards and other data). The normal locations at the time of writing are as follows:
|
|
46
|
+
- MAC OS
|
|
47
|
+
- `~/Library/Application Support/Anki2/User 1/collection.anki2`
|
|
48
|
+
- Linux
|
|
49
|
+
- `~/.local/share/Anki2/User 1/collection.anki2`
|
|
50
|
+
|
|
51
|
+
- `database_safety_check`: a boolean which when set to `true` will check for any running processes using the anki collection.
|
|
52
|
+
> **:warning: Caution:**
|
|
53
|
+
> Setting this option to false may result in database corruption. Always ensure your anki is backed up.
|
|
54
|
+
|
|
55
|
+
- `allow_duplicates`: a boolean which when set to `true` allows duplicate cards (all field values match another cards field values) to be added.
|
|
56
|
+
s
|
|
57
|
+
Example configuration:
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
# where to find the anki collection (sqlite db where anki stores data)
|
|
61
|
+
# Usual system lo
|
|
62
|
+
anki_database = "~/.local/share/Anki2/User 1/collection.anki2"
|
|
63
|
+
|
|
64
|
+
# whether or not to check for other processes using the anki database
|
|
65
|
+
database_safety_check = true
|
|
66
|
+
|
|
67
|
+
# whether or not to allow duplicate cards to be added
|
|
68
|
+
allow_duplicates = false
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
Hanky can be used as both a command line application and a library.
|
|
74
|
+
|
|
75
|
+
- If you want to do something more complex than simply adding cards directly from files, such as generating speech, querying an api or performing other operations at runtime, see the [Library Tutorial](#library-tutorial)
|
|
76
|
+
|
|
77
|
+
- If you just want to load flash cards from files, jump to the [command line usage](#command-line-usage)
|
|
78
|
+
|
|
79
|
+
## Library Tutorial
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Command Line Usage
|
|
83
|
+
|
|
84
|
+
Hanky can be used out of the box as a command line application. If running your own hanky script, omit the `-m` seen in the below examples.
|
|
85
|
+
|
|
86
|
+
### Recursively load decks from files in a folder
|
|
87
|
+
|
|
88
|
+
Recursively load all csv files as decks of cards using the 'basic' anki model/note type. The relative path from the specified folder will be used as the deck name.
|
|
89
|
+
|
|
90
|
+
`python3 -m hanky load "basic" "~/french/" "*.csv" -r`
|
|
91
|
+
|
|
92
|
+
For example, given the following folder structure:
|
|
93
|
+
```
|
|
94
|
+
french
|
|
95
|
+
├── animals.csv
|
|
96
|
+
├── bodies.csv
|
|
97
|
+
├── clothing.csv
|
|
98
|
+
└── grammar
|
|
99
|
+
└── passe_compose.csv
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The following decks will be created:
|
|
103
|
+
- `french`: top level deck
|
|
104
|
+
- `french::animals`: nested animal vocab deck
|
|
105
|
+
- `french::bodies`: nested bodies vocab deck
|
|
106
|
+
- `french::clothing`: nested clothing vocab deck
|
|
107
|
+
- `french::grammar`: nested container deck for grammar
|
|
108
|
+
- `french::grammar::passe_compose`: doubly nested deck for passe compose rules
|
|
109
|
+
|
|
110
|
+
The created anki decks will have the following structure:
|
|
111
|
+
```
|
|
112
|
+
french
|
|
113
|
+
├── animals
|
|
114
|
+
├── bodies
|
|
115
|
+
├── clothing
|
|
116
|
+
└── grammar
|
|
117
|
+
└── passe_compose
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Load decks from files from a folder
|
|
121
|
+
|
|
122
|
+
Load all csv files in a folder as decks of cards using the 'basic' anki model/note type. The relative path from the specified folder will be used as the deck name.
|
|
123
|
+
|
|
124
|
+
`python3 -m hanky load "basic" "~/french/" "*.csv"`
|
|
125
|
+
|
|
126
|
+
For example, given the following folder structure:
|
|
127
|
+
```
|
|
128
|
+
french
|
|
129
|
+
├── animals.csv
|
|
130
|
+
├── bodies.csv
|
|
131
|
+
├── clothing.csv
|
|
132
|
+
└── grammar
|
|
133
|
+
└── passe_compose.csv
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The following decks will be created:
|
|
137
|
+
- `french`: top level deck
|
|
138
|
+
- `french::animals`: nested animal vocab deck
|
|
139
|
+
- `french::bodies`: nested bodies vocab deck
|
|
140
|
+
- `french::clothing`: nested clothing vocab deck
|
|
141
|
+
|
|
142
|
+
The created anki decks will have the following structure:
|
|
143
|
+
```
|
|
144
|
+
french
|
|
145
|
+
├── animals
|
|
146
|
+
├── bodies
|
|
147
|
+
├── clothing
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Load a deck from a file
|
|
151
|
+
|
|
152
|
+
Load a single deck using the 'basic' anki model/note type from a file
|
|
153
|
+
|
|
154
|
+
`python3 -m hanky load-deck "basic" ~/my-folder/countries.csv`
|
|
155
|
+
|
|
156
|
+
The following deck will be created:
|
|
157
|
+
- `countries`
|
hanky-0.0.1/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# hanky
|
|
2
|
+
|
|
3
|
+
[](https://github.com/pypa/hatch)
|
|
4
|
+
|
|
5
|
+
Library and command line application for loading flash cards into anki.
|
|
6
|
+
|
|
7
|
+
> **:information_source: Note:**
|
|
8
|
+
> This project is currently in alpha and is not stable.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Install via pip:
|
|
13
|
+
|
|
14
|
+
`pip install hanky`
|
|
15
|
+
|
|
16
|
+
Optionally install text to speech code seen in tutorial:
|
|
17
|
+
|
|
18
|
+
` pip install hanky[toc]`
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
Currently two configuration options are exposed:
|
|
23
|
+
|
|
24
|
+
- `anki_database`: tells hanky where to find the anki collection (an sqlite database where anki stores flash cards and other data). The normal locations at the time of writing are as follows:
|
|
25
|
+
- MAC OS
|
|
26
|
+
- `~/Library/Application Support/Anki2/User 1/collection.anki2`
|
|
27
|
+
- Linux
|
|
28
|
+
- `~/.local/share/Anki2/User 1/collection.anki2`
|
|
29
|
+
|
|
30
|
+
- `database_safety_check`: a boolean which when set to `true` will check for any running processes using the anki collection.
|
|
31
|
+
> **:warning: Caution:**
|
|
32
|
+
> Setting this option to false may result in database corruption. Always ensure your anki is backed up.
|
|
33
|
+
|
|
34
|
+
- `allow_duplicates`: a boolean which when set to `true` allows duplicate cards (all field values match another cards field values) to be added.
|
|
35
|
+
s
|
|
36
|
+
Example configuration:
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
# where to find the anki collection (sqlite db where anki stores data)
|
|
40
|
+
# Usual system lo
|
|
41
|
+
anki_database = "~/.local/share/Anki2/User 1/collection.anki2"
|
|
42
|
+
|
|
43
|
+
# whether or not to check for other processes using the anki database
|
|
44
|
+
database_safety_check = true
|
|
45
|
+
|
|
46
|
+
# whether or not to allow duplicate cards to be added
|
|
47
|
+
allow_duplicates = false
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
Hanky can be used as both a command line application and a library.
|
|
53
|
+
|
|
54
|
+
- If you want to do something more complex than simply adding cards directly from files, such as generating speech, querying an api or performing other operations at runtime, see the [Library Tutorial](#library-tutorial)
|
|
55
|
+
|
|
56
|
+
- If you just want to load flash cards from files, jump to the [command line usage](#command-line-usage)
|
|
57
|
+
|
|
58
|
+
## Library Tutorial
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Command Line Usage
|
|
62
|
+
|
|
63
|
+
Hanky can be used out of the box as a command line application. If running your own hanky script, omit the `-m` seen in the below examples.
|
|
64
|
+
|
|
65
|
+
### Recursively load decks from files in a folder
|
|
66
|
+
|
|
67
|
+
Recursively load all csv files as decks of cards using the 'basic' anki model/note type. The relative path from the specified folder will be used as the deck name.
|
|
68
|
+
|
|
69
|
+
`python3 -m hanky load "basic" "~/french/" "*.csv" -r`
|
|
70
|
+
|
|
71
|
+
For example, given the following folder structure:
|
|
72
|
+
```
|
|
73
|
+
french
|
|
74
|
+
├── animals.csv
|
|
75
|
+
├── bodies.csv
|
|
76
|
+
├── clothing.csv
|
|
77
|
+
└── grammar
|
|
78
|
+
└── passe_compose.csv
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The following decks will be created:
|
|
82
|
+
- `french`: top level deck
|
|
83
|
+
- `french::animals`: nested animal vocab deck
|
|
84
|
+
- `french::bodies`: nested bodies vocab deck
|
|
85
|
+
- `french::clothing`: nested clothing vocab deck
|
|
86
|
+
- `french::grammar`: nested container deck for grammar
|
|
87
|
+
- `french::grammar::passe_compose`: doubly nested deck for passe compose rules
|
|
88
|
+
|
|
89
|
+
The created anki decks will have the following structure:
|
|
90
|
+
```
|
|
91
|
+
french
|
|
92
|
+
├── animals
|
|
93
|
+
├── bodies
|
|
94
|
+
├── clothing
|
|
95
|
+
└── grammar
|
|
96
|
+
└── passe_compose
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Load decks from files from a folder
|
|
100
|
+
|
|
101
|
+
Load all csv files in a folder as decks of cards using the 'basic' anki model/note type. The relative path from the specified folder will be used as the deck name.
|
|
102
|
+
|
|
103
|
+
`python3 -m hanky load "basic" "~/french/" "*.csv"`
|
|
104
|
+
|
|
105
|
+
For example, given the following folder structure:
|
|
106
|
+
```
|
|
107
|
+
french
|
|
108
|
+
├── animals.csv
|
|
109
|
+
├── bodies.csv
|
|
110
|
+
├── clothing.csv
|
|
111
|
+
└── grammar
|
|
112
|
+
└── passe_compose.csv
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The following decks will be created:
|
|
116
|
+
- `french`: top level deck
|
|
117
|
+
- `french::animals`: nested animal vocab deck
|
|
118
|
+
- `french::bodies`: nested bodies vocab deck
|
|
119
|
+
- `french::clothing`: nested clothing vocab deck
|
|
120
|
+
|
|
121
|
+
The created anki decks will have the following structure:
|
|
122
|
+
```
|
|
123
|
+
french
|
|
124
|
+
├── animals
|
|
125
|
+
├── bodies
|
|
126
|
+
├── clothing
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Load a deck from a file
|
|
130
|
+
|
|
131
|
+
Load a single deck using the 'basic' anki model/note type from a file
|
|
132
|
+
|
|
133
|
+
`python3 -m hanky load-deck "basic" ~/my-folder/countries.csv`
|
|
134
|
+
|
|
135
|
+
The following deck will be created:
|
|
136
|
+
- `countries`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hanky"
|
|
7
|
+
authors = [{ name = "HBA", email = "hanky-pypi.8ebs0@simplelogin.com" }]
|
|
8
|
+
description = "Simple library and command line tool for loading flash cards into anki."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Operating System :: Unix",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
keywords = ["anki"]
|
|
19
|
+
dynamic = ["version"]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"anki",
|
|
23
|
+
"psutil"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# tos = [
|
|
28
|
+
# "boto3"
|
|
29
|
+
# ]
|
|
30
|
+
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest",
|
|
33
|
+
"mypy"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/Haeata-Ash/hanky"
|
|
38
|
+
Issues = "https://github.com/Haeata-Ash/hanky/issues"
|
|
39
|
+
|
|
40
|
+
[tool]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
path = "src/hanky/__about__.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
exclude = [
|
|
47
|
+
"/.github",
|
|
48
|
+
"/docs",
|
|
49
|
+
"/tests",
|
|
50
|
+
"/.venv",
|
|
51
|
+
"/.mypy_cache",
|
|
52
|
+
"/demo.py",
|
|
53
|
+
"/.*"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/hanky"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .hanky import Hanky as Hanky
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# def make_parser():
|
|
5
|
+
# parser = argparse.ArgumentParser(
|
|
6
|
+
# "hanky",
|
|
7
|
+
# description="Simple program to allow programatic management of anki cards",
|
|
8
|
+
# )
|
|
9
|
+
# parser.add_argument(
|
|
10
|
+
# "--config",
|
|
11
|
+
# dest="config",
|
|
12
|
+
# nargs=1,
|
|
13
|
+
# help="Path to hanky json configuration file",
|
|
14
|
+
# )
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# object_subparsers = parser.add_subparsers(
|
|
18
|
+
# dest = "object",
|
|
19
|
+
# help="Type of anki object to manage",
|
|
20
|
+
# required = True
|
|
21
|
+
# )
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# card_obj = object_subparsers.add_parser(
|
|
25
|
+
# "card", help="Perform an operation on anki cards."
|
|
26
|
+
# )
|
|
27
|
+
|
|
28
|
+
# card_cmds = card_obj.add_subparsers(
|
|
29
|
+
# dest="action",
|
|
30
|
+
# required=True,
|
|
31
|
+
# help="Action to perform against anki cards."
|
|
32
|
+
# )
|
|
33
|
+
|
|
34
|
+
# # add anki cards to a deck using model
|
|
35
|
+
# add_card = card_cmds.add_parser("add", help="Add card(s) to an anki deck.")
|
|
36
|
+
# add_card.add_argument("-i", "--input-file", required=True, help="Source file to add the cards from.")
|
|
37
|
+
# add_card.add_argument("-m", "--model", required=True, help="Anki model to create the card from.")
|
|
38
|
+
# add_card.add_argument("-d", "--deck", required=True, help="Anki deck add the card to")
|
|
39
|
+
# add_card.add_argument("--model-args", help="Arguments to pass to the model.")
|
|
40
|
+
# # list anki cards in a deck
|
|
41
|
+
# list_card = card_cmds.add_parser("list", help="List card(s) in an anki deck.")
|
|
42
|
+
# list_card.add_argument("-d", "--deck", required=True, help="List anki cards from deck")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# deck_obj = object_subparsers.add_parser(
|
|
46
|
+
# "deck", help="Perform an operation on an anki deck."
|
|
47
|
+
# )
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# deck_cmds = deck_obj.add_subparsers(
|
|
51
|
+
# dest="action",
|
|
52
|
+
# required=True,
|
|
53
|
+
# help="Action to perform against an anki deck."
|
|
54
|
+
# )
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# add_deck = deck_cmds.add_parser("add", help="Add deck to the anki collection.")
|
|
58
|
+
|
|
59
|
+
# # add anki deck
|
|
60
|
+
# add_deck.add_argument("name", help="Full name of the anki deck, including '::' for nesting.")
|
|
61
|
+
# add_deck.add_argument("-p", "--parent", help="Optionally specify parent deck to avoid manually adding '::' for nesting.")
|
|
62
|
+
|
|
63
|
+
# # list anki decks
|
|
64
|
+
# add_deck = deck_cmds.add_parser("list", help="Add deck to the anki collection.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# model_obj = object_subparsers.add_parser(
|
|
68
|
+
# "model", help="Perform an operation on an anki model."
|
|
69
|
+
# )
|
|
70
|
+
|
|
71
|
+
# model_cmds = model_obj.add_subparsers(
|
|
72
|
+
# dest="action",
|
|
73
|
+
# required=True,
|
|
74
|
+
# help="Action to perform against an anki model."
|
|
75
|
+
# )
|
|
76
|
+
|
|
77
|
+
# # list anki cards in a deck
|
|
78
|
+
# add_card = model_cmds.add_parser("list", help="List models in anki collection.")
|
|
79
|
+
|
|
80
|
+
# return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class KeyValueArg(argparse.Action):
|
|
84
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
85
|
+
setattr(namespace, self.dest, dict())
|
|
86
|
+
|
|
87
|
+
for v in values:
|
|
88
|
+
print(v)
|
|
89
|
+
key, value = v.split("=")
|
|
90
|
+
getattr(namespace, self.dest)[key] = value
|
|
91
|
+
|
|
92
|
+
def make_parser():
|
|
93
|
+
parser = argparse.ArgumentParser(
|
|
94
|
+
"hanky",
|
|
95
|
+
description="Simple program to allow programatic management of anki cards",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--config",
|
|
99
|
+
dest="config",
|
|
100
|
+
help="Path to hanky json configuration file",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
op_parser = parser.add_subparsers(
|
|
104
|
+
dest = "operation",
|
|
105
|
+
help="Type of operation to perform",
|
|
106
|
+
required = True
|
|
107
|
+
)
|
|
108
|
+
load_file = op_parser.add_parser(
|
|
109
|
+
"load-deck",
|
|
110
|
+
help="Load cards into an anki deck from a file"
|
|
111
|
+
)
|
|
112
|
+
load_file.add_argument(
|
|
113
|
+
"model",
|
|
114
|
+
help="Name of the anki model to create cards with."
|
|
115
|
+
)
|
|
116
|
+
load_file.add_argument("file", help="Path of the file to load from")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
load_file.add_argument("-d", "--deck", dest="deck", default=None, help="Name of the deck to load cards into. If not specified, defaults to the filename without the extension.")
|
|
120
|
+
load_file.add_argument("--args", dest="args", default={}, nargs="*", action=KeyValueArg, help="Key value arguments to pass to registered transformers.")
|
|
121
|
+
|
|
122
|
+
load_dir = op_parser.add_parser(
|
|
123
|
+
"load",
|
|
124
|
+
help="Load cards into anki deck(s) from files in a directory, using the filenames as deck names."
|
|
125
|
+
)
|
|
126
|
+
load_dir.add_argument("-r", "--recursive", dest="is_rec", action="store_true", default=False, help="If loading files from a directory, recursively load from files in sub directories as well.")
|
|
127
|
+
load_dir.add_argument(
|
|
128
|
+
"model",
|
|
129
|
+
help="Name of the anki model to create cards with."
|
|
130
|
+
)
|
|
131
|
+
load_dir.add_argument("dir", help="Path of the file to load from")
|
|
132
|
+
load_dir.add_argument("pattern", help="Glob pattern used to decide which files to load. For example, '*.csv'")
|
|
133
|
+
|
|
134
|
+
load_dir.add_argument("--args", dest="args", default={}, nargs="*", action=KeyValueArg, help="Key value arguments to pass to registered transformers.")
|
|
135
|
+
return parser
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Callable, Union
|
|
4
|
+
|
|
5
|
+
def _get_default_anki_db_path() -> str:
|
|
6
|
+
"""Choose a default path for the anki sqlite collection database based on
|
|
7
|
+
the OS.
|
|
8
|
+
|
|
9
|
+
Defaults:
|
|
10
|
+
Linux: "~/.local/share/Anki2/User 1/collection.anki2"
|
|
11
|
+
MacOS: "~/Library/Application Support/Anki2/User 1/collection.anki2"
|
|
12
|
+
"""
|
|
13
|
+
if platform.system() == "Linux":
|
|
14
|
+
return "~/.local/share/Anki2/User 1/collection.anki2"
|
|
15
|
+
elif platform.system() == "Darwin":
|
|
16
|
+
return "~/Library/Application Support/Anki2/User 1/collection.anki2"
|
|
17
|
+
else:
|
|
18
|
+
return ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ANKI_DB_PATH = "anki_database"
|
|
22
|
+
DO_SAFET_CHECK = "database_safety_check"
|
|
23
|
+
ALLOW_DUPLICATES = "allow_duplicates"
|
|
24
|
+
|
|
25
|
+
DEFAULT_CONFIG = {
|
|
26
|
+
ANKI_DB_PATH: _get_default_anki_db_path(),
|
|
27
|
+
DO_SAFET_CHECK: True,
|
|
28
|
+
ALLOW_DUPLICATES: False
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class Config(dict):
|
|
32
|
+
"""Configuration object"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def __init__(self, **kwargs):
|
|
36
|
+
self._config = None
|
|
37
|
+
self.default_path = Path("~/.config/hanky/hanky.toml").expanduser()
|
|
38
|
+
super().__init__(kwargs)
|
|
39
|
+
|
|
40
|
+
def from_file(
|
|
41
|
+
self,
|
|
42
|
+
file: Union[Path, str],
|
|
43
|
+
loader: Callable[[Union[str, Path], dict], dict],
|
|
44
|
+
text=False,
|
|
45
|
+
**kwargs,
|
|
46
|
+
):
|
|
47
|
+
with open(file, "r" if text else "rb") as f:
|
|
48
|
+
cfg = loader(f, **kwargs)
|
|
49
|
+
if not isinstance(cfg, dict):
|
|
50
|
+
raise TypeError(
|
|
51
|
+
f"Received type '{type(cfg)}' but expected '{type(dict)}' from loader function."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
for k, v in cfg.items():
|
|
55
|
+
self[k] = v
|
|
56
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Generator, Iterator, Union, TextIO, Callable
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import json
|
|
4
|
+
import csv
|
|
5
|
+
import psutil
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def read_file(
|
|
9
|
+
path: str, loader: Callable[[TextIO], Union[Iterator, Generator]]
|
|
10
|
+
) -> Union[Iterator, Generator]:
|
|
11
|
+
path = Path(path)
|
|
12
|
+
if not path.is_file():
|
|
13
|
+
raise IOError(f"{path} is not a file.")
|
|
14
|
+
with open(path, "r") as f:
|
|
15
|
+
for item in loader(f):
|
|
16
|
+
yield item
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_LOADERS = {".json": json.load, ".csv": csv.DictReader}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def has_handle(fpath):
|
|
25
|
+
fpath = Path(fpath).expanduser().absolute()
|
|
26
|
+
for proc in psutil.process_iter():
|
|
27
|
+
try:
|
|
28
|
+
for item in proc.open_files():
|
|
29
|
+
if str(fpath) == str(item.path):
|
|
30
|
+
return True
|
|
31
|
+
except psutil.Error:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
return False
|
|
35
|
+
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import hashlib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Callable, Generator, Iterator, List, Union
|
|
5
|
+
|
|
6
|
+
from anki.collection import Collection, SearchNode
|
|
7
|
+
from tomllib import load as toml_load
|
|
8
|
+
|
|
9
|
+
from hanky.cli import make_parser
|
|
10
|
+
from hanky.config import (
|
|
11
|
+
ALLOW_DUPLICATES,
|
|
12
|
+
ANKI_DB_PATH,
|
|
13
|
+
DEFAULT_CONFIG,
|
|
14
|
+
DO_SAFET_CHECK,
|
|
15
|
+
Config,
|
|
16
|
+
)
|
|
17
|
+
from hanky.fs import DEFAULT_LOADERS, has_handle, read_file
|
|
18
|
+
from hanky.media import is_audio_ext, make_anki_sound_ref
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ModelProcessor:
|
|
22
|
+
def __init__(self, model_name: str, func, expected_args, required_fields):
|
|
23
|
+
self.f = func
|
|
24
|
+
self.model = model_name
|
|
25
|
+
self.expected_args = expected_args
|
|
26
|
+
self.required_fields = required_fields
|
|
27
|
+
|
|
28
|
+
if not isinstance(self.expected_args, list):
|
|
29
|
+
raise TypeError("'expected_args' must be a list of strings")
|
|
30
|
+
if not isinstance(self.required_fields, list):
|
|
31
|
+
raise TypeError("'required_fields' must be a list of strings")
|
|
32
|
+
|
|
33
|
+
def __call__(self, card: dict, **kwargs) -> dict:
|
|
34
|
+
for k in self.required_fields:
|
|
35
|
+
if k not in card:
|
|
36
|
+
raise KeyError(
|
|
37
|
+
f"Processor requires '{k}' to be present in card. \n {str(card)}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
for k in self.expected_args:
|
|
41
|
+
if k not in kwargs:
|
|
42
|
+
raise KeyError(
|
|
43
|
+
f"Processor for {self.model} expects key word argument '{k}'. Ensure it is passed in via the --model-args option"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
ret = self.f(card, **kwargs)
|
|
47
|
+
if not isinstance(ret, dict):
|
|
48
|
+
raise TypeError(
|
|
49
|
+
f"Processor function did not return a dictionary like object, returned type {type(ret)}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return ret
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Hanky:
|
|
56
|
+
def __init__(self, **kwargs):
|
|
57
|
+
# set default config and then overwrite with config object provided via constructor
|
|
58
|
+
# ensures default keys are present
|
|
59
|
+
self.config: Config = Config(**DEFAULT_CONFIG)
|
|
60
|
+
if kwargs:
|
|
61
|
+
self.config.update(kwargs)
|
|
62
|
+
|
|
63
|
+
self._col: Collection = None
|
|
64
|
+
|
|
65
|
+
self.processors = dict()
|
|
66
|
+
self.loaders = dict(DEFAULT_LOADERS)
|
|
67
|
+
|
|
68
|
+
def run(self):
|
|
69
|
+
parser = make_parser()
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
|
|
72
|
+
if args.config:
|
|
73
|
+
self.config.from_file(args.config, toml_load)
|
|
74
|
+
|
|
75
|
+
if args.operation == "load-deck":
|
|
76
|
+
self.load_deck(
|
|
77
|
+
args.file,
|
|
78
|
+
args.model,
|
|
79
|
+
deck_name=args.deck,
|
|
80
|
+
**(args.args) if args.args else {},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
elif args.operation == "load":
|
|
84
|
+
self.load_dir(
|
|
85
|
+
args.model,
|
|
86
|
+
args.dir,
|
|
87
|
+
args.pattern,
|
|
88
|
+
recursive=args.is_rec,
|
|
89
|
+
*(args.args) if args.args else {},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def col(self):
|
|
94
|
+
if not self._col:
|
|
95
|
+
if not self.config[ANKI_DB_PATH]:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
"""Path to anki sqlite collection database was
|
|
98
|
+
not provided in config and no suitable default known."""
|
|
99
|
+
)
|
|
100
|
+
db_path = Path(self.config[ANKI_DB_PATH]).expanduser().absolute()
|
|
101
|
+
|
|
102
|
+
if not db_path.exists() or not db_path.is_file():
|
|
103
|
+
raise FileNotFoundError(
|
|
104
|
+
f"'{db_path}' either does not exist or is not a file. Please check the provided path to the anki collection."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if self.config[DO_SAFET_CHECK]:
|
|
108
|
+
if has_handle(self.config[ANKI_DB_PATH]):
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
"""At least one other process is using the anki database. Ensure the Anki application is closed before using Hanky to avoid possible corruption."""
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._col = Collection(db_path)
|
|
114
|
+
return self._col
|
|
115
|
+
|
|
116
|
+
def add_card(
|
|
117
|
+
self,
|
|
118
|
+
deck_name,
|
|
119
|
+
model_name,
|
|
120
|
+
filter_query: str = None,
|
|
121
|
+
allow_duplicates=False,
|
|
122
|
+
**fields,
|
|
123
|
+
) -> bool:
|
|
124
|
+
model = self.col.models.by_name(model_name)
|
|
125
|
+
if not model:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Model '{model_name}' does not exist in your anki collection. Ensure it has been added before using it with hanky."
|
|
128
|
+
)
|
|
129
|
+
deck_id = self.col.decks.id(deck_name, create=False)
|
|
130
|
+
if not deck_id:
|
|
131
|
+
ValueError(
|
|
132
|
+
f"Deck '{deck_name}' does not exist in your anki collection. Ensure it has been created before using it with hanky."
|
|
133
|
+
)
|
|
134
|
+
expected_fields = self.col.models.field_names(model)
|
|
135
|
+
for k in expected_fields:
|
|
136
|
+
if k not in fields:
|
|
137
|
+
raise KeyError(f"Expected field '{k}' is missing.")
|
|
138
|
+
|
|
139
|
+
new_card = self.col.new_note(model)
|
|
140
|
+
|
|
141
|
+
for k, v in fields.items():
|
|
142
|
+
new_card[k] = str(v)
|
|
143
|
+
|
|
144
|
+
if filter_query:
|
|
145
|
+
matches = self.col.find_cards(filter_query)
|
|
146
|
+
if len(matches):
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
allow_duplicates = allow_duplicates if allow_duplicates else self.config[ALLOW_DUPLICATES]
|
|
150
|
+
|
|
151
|
+
if not allow_duplicates:
|
|
152
|
+
rets = [True]
|
|
153
|
+
|
|
154
|
+
for field in fields:
|
|
155
|
+
if self.col.find_notes(
|
|
156
|
+
self.col.build_search_string(
|
|
157
|
+
new_card[field], SearchNode(field_name=field)
|
|
158
|
+
)
|
|
159
|
+
):
|
|
160
|
+
rets.append(True)
|
|
161
|
+
else:
|
|
162
|
+
rets.append(False)
|
|
163
|
+
|
|
164
|
+
is_dupe = functools.reduce(lambda x, y: x and y, rets)
|
|
165
|
+
|
|
166
|
+
if is_dupe:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
self.col.add_note(new_card, deck_id)
|
|
170
|
+
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def add_deck(self, deck_name) -> bool:
|
|
174
|
+
self.col.decks.id(deck_name)
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
def register_loader(
|
|
178
|
+
self, file_ext: str, loader: Callable[[str], Union[Iterator, Generator]]
|
|
179
|
+
):
|
|
180
|
+
self.loaders[file_ext] = loader
|
|
181
|
+
|
|
182
|
+
def register_card_processor(
|
|
183
|
+
self,
|
|
184
|
+
model_name: str,
|
|
185
|
+
handler: Callable[[dict], dict],
|
|
186
|
+
expected_args: List[str] = [],
|
|
187
|
+
expected_fields: List[str] = [],
|
|
188
|
+
):
|
|
189
|
+
if model_name not in self.processors:
|
|
190
|
+
self.processors[model_name] = []
|
|
191
|
+
self.processors[model_name].append(
|
|
192
|
+
ModelProcessor(model_name, handler, expected_args, expected_fields)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def card_processor(
|
|
196
|
+
self, model: str, expected_args: List[str], expected_fields: List[str]
|
|
197
|
+
):
|
|
198
|
+
def decorator(func):
|
|
199
|
+
self.register_card_processor(model, func, expected_args, expected_fields)
|
|
200
|
+
return func
|
|
201
|
+
|
|
202
|
+
return decorator
|
|
203
|
+
|
|
204
|
+
def get_model_processors(self, model_name: str) -> List[ModelProcessor]:
|
|
205
|
+
if model_name in self.processors:
|
|
206
|
+
return self.processors[model_name]
|
|
207
|
+
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
def get_loader(self, suffix) -> Callable:
|
|
211
|
+
return self.loaders[suffix]
|
|
212
|
+
|
|
213
|
+
def load_deck(
|
|
214
|
+
self,
|
|
215
|
+
fpath: str,
|
|
216
|
+
model_name: str,
|
|
217
|
+
deck_name: str = None,
|
|
218
|
+
loader=None,
|
|
219
|
+
parent_deck="",
|
|
220
|
+
**model_args,
|
|
221
|
+
):
|
|
222
|
+
print(f"Loading into deck {deck_name}")
|
|
223
|
+
fpath = Path(fpath).absolute()
|
|
224
|
+
|
|
225
|
+
transformers = self.get_model_processors(model_name)
|
|
226
|
+
loader = loader if loader else self.get_loader(fpath.suffix)
|
|
227
|
+
|
|
228
|
+
model = self.col.models.by_name(model_name)
|
|
229
|
+
if not model:
|
|
230
|
+
raise KeyError(
|
|
231
|
+
f"Model '{model_name}' does not exist in your anki collection. Ensure it has been added before using it with hanky."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# deck is the specified name or filename without extension
|
|
235
|
+
deck_name = deck_name if deck_name else fpath.stem
|
|
236
|
+
|
|
237
|
+
self.add_deck(deck_name)
|
|
238
|
+
|
|
239
|
+
count = 0
|
|
240
|
+
total = 0
|
|
241
|
+
for item in read_file(fpath, loader):
|
|
242
|
+
card = dict(item)
|
|
243
|
+
for t in transformers:
|
|
244
|
+
card = t(card, **model_args)
|
|
245
|
+
|
|
246
|
+
ret = self.add_card(
|
|
247
|
+
deck_name,
|
|
248
|
+
model_name,
|
|
249
|
+
**card,
|
|
250
|
+
)
|
|
251
|
+
total += 1
|
|
252
|
+
if ret:
|
|
253
|
+
count += 1
|
|
254
|
+
|
|
255
|
+
print(f"Added {count} out of {total} cards.")
|
|
256
|
+
|
|
257
|
+
def add_media(
|
|
258
|
+
self, data, anki_media_filename: str = None, file_ext: str = None
|
|
259
|
+
) -> str:
|
|
260
|
+
ext = None
|
|
261
|
+
if anki_media_filename:
|
|
262
|
+
path = Path(anki_media_filename)
|
|
263
|
+
ext = path.suffix
|
|
264
|
+
elif file_ext:
|
|
265
|
+
ext = file_ext
|
|
266
|
+
else:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
"If argument 'anki_media_filename' is not provided then 'file_ext' must be present"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if isinstance(data, str):
|
|
272
|
+
data = data.encode()
|
|
273
|
+
|
|
274
|
+
desired_name = anki_media_filename
|
|
275
|
+
|
|
276
|
+
# no filename given, use hash of the data plus file_ext
|
|
277
|
+
if not desired_name:
|
|
278
|
+
m = hashlib.sha256()
|
|
279
|
+
m.update(data)
|
|
280
|
+
desired_name = m.hexdigest() + ext
|
|
281
|
+
|
|
282
|
+
# write media to anki database
|
|
283
|
+
actual_name = self.col.media.write_data(
|
|
284
|
+
desired_name,
|
|
285
|
+
data,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
anki_ref = self.col.media.escape_media_filenames(actual_name)
|
|
289
|
+
|
|
290
|
+
if is_audio_ext(actual_name):
|
|
291
|
+
anki_ref = make_anki_sound_ref(anki_ref)
|
|
292
|
+
|
|
293
|
+
return anki_ref
|
|
294
|
+
|
|
295
|
+
def add_media_file(self, local_path) -> str:
|
|
296
|
+
anki_ref = self.col.media.escape_media_filenames(
|
|
297
|
+
self.col.media.add_file(local_path)
|
|
298
|
+
)
|
|
299
|
+
if is_audio_ext(anki_ref):
|
|
300
|
+
anki_ref = make_anki_sound_ref(anki_ref)
|
|
301
|
+
|
|
302
|
+
return anki_ref
|
|
303
|
+
|
|
304
|
+
def load_dir(
|
|
305
|
+
self,
|
|
306
|
+
model: str,
|
|
307
|
+
root_dir: str,
|
|
308
|
+
glob_pattern: str,
|
|
309
|
+
recursive=False,
|
|
310
|
+
parent_deck: str = "",
|
|
311
|
+
loader=None,
|
|
312
|
+
**model_args,
|
|
313
|
+
):
|
|
314
|
+
parent_deck = ""
|
|
315
|
+
|
|
316
|
+
root = Path(root_dir).expanduser()
|
|
317
|
+
|
|
318
|
+
root_deck = parent_deck if parent_deck else root.name
|
|
319
|
+
|
|
320
|
+
def _glob(root, pattern, recursive):
|
|
321
|
+
if recursive:
|
|
322
|
+
for path in root.rglob(pattern):
|
|
323
|
+
yield path
|
|
324
|
+
else:
|
|
325
|
+
for path in root.glob(pattern):
|
|
326
|
+
yield path
|
|
327
|
+
|
|
328
|
+
for path in _glob(root, glob_pattern, recursive):
|
|
329
|
+
if path.is_file():
|
|
330
|
+
path = path.relative_to(root)
|
|
331
|
+
abs_path = root.joinpath(path)
|
|
332
|
+
parents = [p.name for p in reversed(path.parents)]
|
|
333
|
+
|
|
334
|
+
# don't need the first empty entry for the current directory
|
|
335
|
+
parents.pop(0)
|
|
336
|
+
deck_list = [root_deck]
|
|
337
|
+
|
|
338
|
+
i = 0
|
|
339
|
+
while i < len(parents):
|
|
340
|
+
deck_list.append(parents[i])
|
|
341
|
+
i += 1
|
|
342
|
+
deck_list.append(path.stem)
|
|
343
|
+
full_deck = "::".join(deck_list)
|
|
344
|
+
|
|
345
|
+
self.load_deck(
|
|
346
|
+
abs_path,
|
|
347
|
+
model,
|
|
348
|
+
deck_name=full_deck,
|
|
349
|
+
loader=loader,
|
|
350
|
+
**model_args,
|
|
351
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_audio_ext(filename: str) -> bool:
|
|
5
|
+
AUDIO_EXT = set(
|
|
6
|
+
".mp3",
|
|
7
|
+
".oga",
|
|
8
|
+
".opus",
|
|
9
|
+
".wav",
|
|
10
|
+
".weba",
|
|
11
|
+
".aac",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if Path(filename).suffix in AUDIO_EXT:
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def make_anki_sound_ref(media_ref: str) -> str:
|
|
21
|
+
return f"[sound:{media_ref}]"
|