deckops 1.0.0__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.
Files changed (34) hide show
  1. deckops-1.0.0/PKG-INFO +224 -0
  2. deckops-1.0.0/README.md +199 -0
  3. deckops-1.0.0/deckops/__init__.py +4 -0
  4. deckops-1.0.0/deckops/anki_client.py +32 -0
  5. deckops-1.0.0/deckops/anki_to_markdown.py +350 -0
  6. deckops-1.0.0/deckops/cli.py +265 -0
  7. deckops-1.0.0/deckops/config.py +113 -0
  8. deckops-1.0.0/deckops/data/DeckOps Tutorial.md +54 -0
  9. deckops-1.0.0/deckops/data/__init__.py +0 -0
  10. deckops-1.0.0/deckops/ensure_models.py +99 -0
  11. deckops-1.0.0/deckops/git.py +63 -0
  12. deckops-1.0.0/deckops/html_converter.py +163 -0
  13. deckops-1.0.0/deckops/init.py +173 -0
  14. deckops-1.0.0/deckops/log.py +161 -0
  15. deckops-1.0.0/deckops/markdown_converter.py +194 -0
  16. deckops-1.0.0/deckops/markdown_helpers.py +269 -0
  17. deckops-1.0.0/deckops/markdown_to_anki.py +870 -0
  18. deckops-1.0.0/deckops/models/DeckOpsClozeBack.template.anki +16 -0
  19. deckops-1.0.0/deckops/models/DeckOpsClozeFront.template.anki +7 -0
  20. deckops-1.0.0/deckops/models/DeckOpsQABack.template.anki +20 -0
  21. deckops-1.0.0/deckops/models/DeckOpsQAFront.template.anki +7 -0
  22. deckops-1.0.0/deckops/models/Styling.css +980 -0
  23. deckops-1.0.0/deckops/models/__init__.py +0 -0
  24. deckops-1.0.0/deckops.egg-info/PKG-INFO +224 -0
  25. deckops-1.0.0/deckops.egg-info/SOURCES.txt +32 -0
  26. deckops-1.0.0/deckops.egg-info/dependency_links.txt +1 -0
  27. deckops-1.0.0/deckops.egg-info/entry_points.txt +2 -0
  28. deckops-1.0.0/deckops.egg-info/requires.txt +8 -0
  29. deckops-1.0.0/deckops.egg-info/top_level.txt +1 -0
  30. deckops-1.0.0/pyproject.toml +58 -0
  31. deckops-1.0.0/setup.cfg +4 -0
  32. deckops-1.0.0/tests/test_cloze.py +256 -0
  33. deckops-1.0.0/tests/test_html_converter.py +620 -0
  34. deckops-1.0.0/tests/test_markdown_converter.py +730 -0
deckops-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: deckops
3
+ Version: 1.0.0
4
+ Summary: Manage Anki decks as Markdown files.
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/visserle/DeckOps
7
+ Project-URL: Issues, https://github.com/visserle/DeckOps/issues
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: End Users/Desktop
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Education
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.28.0
19
+ Requires-Dist: html-to-markdown>=2.24.5
20
+ Requires-Dist: mistune>=3.2.0
21
+ Requires-Dist: pygments>=2.14.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
24
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
25
+
26
+ # DeckOps
27
+
28
+ Manage your Anki decks as Markdown files.
29
+
30
+ ## The Problem
31
+
32
+ Managing Anki decks through the UI can feel complex and slow, especially with many cards. Working with decks as plain text files would offer a simpler alternative, enabling batch editing, AI-friendliness, version control, easy data sharing, and tool independence.
33
+
34
+ ## The Solution
35
+
36
+ DeckOps is an Anki ↔ Markdown bridge that solves exactly this problem. Each Anki deck is represented by a Markdown file, and edits in either place can be synced to the other. This enables a hybrid approach between your Anki database and filesystem, allowing for faster editing and easier maintenance.
37
+
38
+ ## Features
39
+
40
+ - Fully round-trip, bidirectional sync that handles identity, moves, drifts, conflicts, and deletions
41
+ - Markdown support with nearly all features (including syntax-highlighted code blocks, supported on desktop and mobile)
42
+ - Built-in Git integration with autocommit for never losing your data
43
+ - Image support via VS Code where images are directly copied into your Anki media folder (automatically set up)
44
+ - Support for Base Type (Q&A) and Cloze notes
45
+ - Simple CLI interface—after setup, only two commands are needed for daily drive
46
+
47
+ ## Getting Started
48
+
49
+ 1. **Install DeckOps via [pipx](https://github.com/pypa/pipx)**:
50
+ ```bash
51
+ pipx install deckops
52
+ ```
53
+ 2. **Initialize DeckOps**: Make sure that Anki is running, with the [AnkiConnect add-on](https://ankiweb.net/shared/info/2055492159) enabled. Initialize DeckOps in any empty directory of your choosing. This is where your text-based decks will live. The additional tutorial flag creates a sample Markdown deck with further information.
54
+
55
+ ```bash
56
+ deckops init --tutorial
57
+ ```
58
+
59
+ 3. **Execute DeckOps**: Import the tutorial deck into Anki. The tutorial deck provides a good starting point for exploring DeckOps features. Import it into Anki using:
60
+ ```bash
61
+ deckops ma # markdown to anki (import)
62
+ ```
63
+
64
+ 4. **Keep everything in sync**: Each sync is unidirectional—the last sync direction overwrites the other (sync in the same direction per session to avoid losing edits). The CLI provides additional flags for syncing single files or decks. To export changes after reviewing and editing the tutorial in Anki, use:
65
+
66
+ ```bash
67
+ deckops am # anki to markdown (export)
68
+ ```
69
+
70
+ ## FAQ
71
+
72
+ ### Is it safe to use?
73
+
74
+ Yes! DeckOps only acts on its own note types, so your existing collection is never affected. You can mix managed and unmanaged cards in the same deck without issues. Before every DeckOps sync, it automatically creates a Git commit of your Markdown folder so you can always roll your files back if needed. For Anki, additional safety features are implemented. DeckOps only syncs if the activated profiles matches the one it was initialized with. When orpahned DeckOps cards are detected, you will be prompted to confirm their deletion.
75
+
76
+ ### How do I create new cards?
77
+
78
+ Create a new Markdown file in your initialized DeckOps folder. For the first import, the file name will act as the deck name. Subdecks are supported via two underscores `__` (Anki's `::` is not supported in the file system). Start by writing your cards in Markdown. For each card, you can decide whether to use the QA or cloze format. Cards must be separated by a new line, three dashes `---`, and another new line.
79
+
80
+ ```markdown
81
+ Q: Question text here
82
+ A: Answer text here
83
+ E: Extra information (optional)
84
+ M: Content behind a "more" button (optional)
85
+
86
+ ---
87
+
88
+ T: Text with {{c1::multiple}} {{c2::cloze deletions}}.
89
+ E: ![image with set width](im.png){width=700}
90
+
91
+ ---
92
+
93
+ And so on…
94
+ ```
95
+
96
+ ### Which characters or symbols cannot be used?
97
+
98
+ Since cards are separated by horizontal lines (`---`), they cannot be used within the content fields of your cards. This includes all special Markdown characters that render these lines (`***`, `___`), and `<hr>`.
99
+
100
+ ### How does it work?
101
+
102
+ On first import, DeckOps assigns IDs from Anki to each deck/note/card for tracking. They are represented by a single-line HTML tag (e.g., `<!-- card_id: 1770487991522 -->`) above a card in the Markdown. With the IDs in place, we can track what is new, changed, moved between decks, or deleted, and DeckOps will sync accordingly. Note that one DeckOps folder represents an entire Anki profile.
103
+
104
+ ### Why do some cards have a `card_id` and others a `note_id`?
105
+
106
+ `DeckOpsQA` cards have a `card_id`, while `DeckOpsCloze` cards have a `note_id` HTML tag. This is because cloze notes can generate multiple cards. If you want to transform a Cloze note into a QA card in Markdown, make sure to change the prefix from `T:` to `Q:` & `A:` and delete the old `note_id`. DeckOps will assign a new `card_id` in the next import.
107
+
108
+ ### What is the recommended workflow?
109
+
110
+ We recommend using VS Code. It has excellent AI integration, a great [add-on](https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced) for Markdown previews, and supports image pasting (which will be saved in your Anki media folder by default).
111
+
112
+ ### How can I migrate my existing cards into DeckOps?
113
+
114
+ While it is doable, the migration can be tricky. If you convert your cards into DeckOps' note types, then all you need to do is export your cards from Anki to Markdown. In the first re-import, some formatting may be changed because the original HTML from Anki may not follow the CommonMark standard; however, all changes are easily trackable via Git. If your note format does not work with the DeckOps format, you will have to adapt the code to your needs.
115
+
116
+ ### How can I develop DeckOps locally?
117
+
118
+ Fork this repository and initialize the tutorial in your root folder (make sure Anki is running). This will create a folder called `collection` with the sample Markdown in it. Paths will adapt automatically to the development environment. You can run DeckOps using the main script:
119
+ ```bash
120
+ python -m main init --tutorial
121
+ python -m main ma
122
+ ```
123
+
124
+ ### How does DeckOps solve bidirectional sync? (Claude's answer)
125
+
126
+ DeckOps handles the core challenges of bidirectional synchronization between markdown and Anki:
127
+
128
+ #### 1. Identity
129
+
130
+ **Solution**: Embed immutable IDs directly in markdown as HTML comments
131
+
132
+ - **Deck Identity**: `<!-- deck_id: 1234567890 -->` on first line of file
133
+ - **Card Identity** (QA cards): `<!-- card_id: 1770487991522 -->` before each card
134
+ - **Note Identity** (Cloze): `<!-- note_id: 1770487991521 -->` before each note
135
+
136
+ IDs are Anki's native IDs (timestamps in milliseconds), written to Markdown on first sync and persisting across all future syncs, enabling bidirectional linking.
137
+
138
+
139
+ #### 2. Moves (Between Decks)
140
+
141
+ **Solution**: Move detection + automatic deck correction
142
+
143
+ **Import (Markdown → Anki)**:
144
+ When you move a card between markdown files:
145
+ 1. Cut card from `DeckA.md` (keeping its ID)
146
+ 2. Paste into `DeckB.md`
147
+ 3. Import detects card in wrong deck → **auto-moves to DeckB**
148
+ 4. **Review history preserved**
149
+
150
+
151
+ **Export (Anki → Markdown)**:
152
+ When you move a card between decks in Anki:
153
+ 1. Export detects card disappeared from DeckA, appeared in DeckB
154
+ 2. Reports as move (not deletion + creation)
155
+ 3. Card appears in correct Markdown file
156
+
157
+ Note: Deck renaming is only possible via export (Anki → Markdown). While import (Markdown → Anki) can be used to create new decks named after the file, renaming decks should always happen via export. Since the `deck_id` is not dependent on the file name, there is no conflict when the Markdown file name differs from a deck's name in Anki.
158
+
159
+ #### 3. Conflict Resolution
160
+
161
+ **Solution**: **Last sync direction wins** (no merging)
162
+
163
+ **Import (Markdown → Anki)**:
164
+ - Markdown content **overwrites** Anki content
165
+ - Updates existing cards with markdown content
166
+ - If fields match → skip (optimization)
167
+ - If fields differ → markdown wins
168
+
169
+ **Export (Anki → Markdown)**:
170
+ - Anki content **overwrites** Markdown content
171
+ - Existing blocks replaced with Anki's current state
172
+ - Deck renames reflected in file renames
173
+
174
+ This simple approach requires discipline: always sync in the same direction for a given edit session.
175
+
176
+ #### 4. Drift Detection & Recovery
177
+
178
+ **Solution**: "Stale card" detection with automatic re-creation
179
+
180
+ **What is drift?**
181
+ - Card exists in Markdown with `card_id: 123`
182
+ - But ID 123 no longer exists in Anki (manually deleted)
183
+
184
+ **How it's resolved**:
185
+ 1. Phase 1: Try to update card 123 → fails
186
+ 2. Mark as "stale"
187
+ 3. Phase 3: Re-create in Anki with new ID (e.g., 456)
188
+ 4. Phase 4: Update Markdown: `<!-- card_id: 123 -->` → `<!-- card_id: 456 -->`
189
+
190
+ **Result**: Drift is automatically healed. Content preserved, but review history lost (new card).
191
+
192
+ #### 5. Deletions
193
+
194
+ **Solution**: Bidirectional orphan cleanup
195
+
196
+ **Markdown → Anki (Import)**:
197
+ - Cards in Anki deck but NOT in Markdown file → deleted from Anki
198
+ - Exception: Cards claimed by other files are moved, not deleted
199
+
200
+ **Anki → Markdown (Export)**:
201
+ - **Orphaned decks**: File has `deck_id` but deck doesn't exist → delete file
202
+ - **Orphaned cards**: Card has `card_id` but card doesn't exist → remove block
203
+
204
+ Deletions propagate in both directions to maintain consistency.
205
+
206
+ #### Summary
207
+
208
+ | Challenge | Solution | Preserves History? |
209
+ |-----------|----------|-------------------|
210
+ | **Identity** | Embed Anki IDs in Markdown | Yes |
211
+ | **Moves** | Auto-move + global tracking | Yes |
212
+ | **Conflicts** | Last sync wins (no merge) | Yes |
213
+ | **Drift** | Stale card detection + re-creation | No |
214
+ | **Deletions** | Bidirectional orphan cleanup | N/A |
215
+
216
+ ---
217
+
218
+ ## Support This Project
219
+
220
+ If DeckOps saves you time or makes your workflow better, consider buying me a coffee—it would make my day!
221
+
222
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/visserle)
223
+
224
+ MIT License
@@ -0,0 +1,199 @@
1
+ # DeckOps
2
+
3
+ Manage your Anki decks as Markdown files.
4
+
5
+ ## The Problem
6
+
7
+ Managing Anki decks through the UI can feel complex and slow, especially with many cards. Working with decks as plain text files would offer a simpler alternative, enabling batch editing, AI-friendliness, version control, easy data sharing, and tool independence.
8
+
9
+ ## The Solution
10
+
11
+ DeckOps is an Anki ↔ Markdown bridge that solves exactly this problem. Each Anki deck is represented by a Markdown file, and edits in either place can be synced to the other. This enables a hybrid approach between your Anki database and filesystem, allowing for faster editing and easier maintenance.
12
+
13
+ ## Features
14
+
15
+ - Fully round-trip, bidirectional sync that handles identity, moves, drifts, conflicts, and deletions
16
+ - Markdown support with nearly all features (including syntax-highlighted code blocks, supported on desktop and mobile)
17
+ - Built-in Git integration with autocommit for never losing your data
18
+ - Image support via VS Code where images are directly copied into your Anki media folder (automatically set up)
19
+ - Support for Base Type (Q&A) and Cloze notes
20
+ - Simple CLI interface—after setup, only two commands are needed for daily drive
21
+
22
+ ## Getting Started
23
+
24
+ 1. **Install DeckOps via [pipx](https://github.com/pypa/pipx)**:
25
+ ```bash
26
+ pipx install deckops
27
+ ```
28
+ 2. **Initialize DeckOps**: Make sure that Anki is running, with the [AnkiConnect add-on](https://ankiweb.net/shared/info/2055492159) enabled. Initialize DeckOps in any empty directory of your choosing. This is where your text-based decks will live. The additional tutorial flag creates a sample Markdown deck with further information.
29
+
30
+ ```bash
31
+ deckops init --tutorial
32
+ ```
33
+
34
+ 3. **Execute DeckOps**: Import the tutorial deck into Anki. The tutorial deck provides a good starting point for exploring DeckOps features. Import it into Anki using:
35
+ ```bash
36
+ deckops ma # markdown to anki (import)
37
+ ```
38
+
39
+ 4. **Keep everything in sync**: Each sync is unidirectional—the last sync direction overwrites the other (sync in the same direction per session to avoid losing edits). The CLI provides additional flags for syncing single files or decks. To export changes after reviewing and editing the tutorial in Anki, use:
40
+
41
+ ```bash
42
+ deckops am # anki to markdown (export)
43
+ ```
44
+
45
+ ## FAQ
46
+
47
+ ### Is it safe to use?
48
+
49
+ Yes! DeckOps only acts on its own note types, so your existing collection is never affected. You can mix managed and unmanaged cards in the same deck without issues. Before every DeckOps sync, it automatically creates a Git commit of your Markdown folder so you can always roll your files back if needed. For Anki, additional safety features are implemented. DeckOps only syncs if the activated profiles matches the one it was initialized with. When orpahned DeckOps cards are detected, you will be prompted to confirm their deletion.
50
+
51
+ ### How do I create new cards?
52
+
53
+ Create a new Markdown file in your initialized DeckOps folder. For the first import, the file name will act as the deck name. Subdecks are supported via two underscores `__` (Anki's `::` is not supported in the file system). Start by writing your cards in Markdown. For each card, you can decide whether to use the QA or cloze format. Cards must be separated by a new line, three dashes `---`, and another new line.
54
+
55
+ ```markdown
56
+ Q: Question text here
57
+ A: Answer text here
58
+ E: Extra information (optional)
59
+ M: Content behind a "more" button (optional)
60
+
61
+ ---
62
+
63
+ T: Text with {{c1::multiple}} {{c2::cloze deletions}}.
64
+ E: ![image with set width](im.png){width=700}
65
+
66
+ ---
67
+
68
+ And so on…
69
+ ```
70
+
71
+ ### Which characters or symbols cannot be used?
72
+
73
+ Since cards are separated by horizontal lines (`---`), they cannot be used within the content fields of your cards. This includes all special Markdown characters that render these lines (`***`, `___`), and `<hr>`.
74
+
75
+ ### How does it work?
76
+
77
+ On first import, DeckOps assigns IDs from Anki to each deck/note/card for tracking. They are represented by a single-line HTML tag (e.g., `<!-- card_id: 1770487991522 -->`) above a card in the Markdown. With the IDs in place, we can track what is new, changed, moved between decks, or deleted, and DeckOps will sync accordingly. Note that one DeckOps folder represents an entire Anki profile.
78
+
79
+ ### Why do some cards have a `card_id` and others a `note_id`?
80
+
81
+ `DeckOpsQA` cards have a `card_id`, while `DeckOpsCloze` cards have a `note_id` HTML tag. This is because cloze notes can generate multiple cards. If you want to transform a Cloze note into a QA card in Markdown, make sure to change the prefix from `T:` to `Q:` & `A:` and delete the old `note_id`. DeckOps will assign a new `card_id` in the next import.
82
+
83
+ ### What is the recommended workflow?
84
+
85
+ We recommend using VS Code. It has excellent AI integration, a great [add-on](https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced) for Markdown previews, and supports image pasting (which will be saved in your Anki media folder by default).
86
+
87
+ ### How can I migrate my existing cards into DeckOps?
88
+
89
+ While it is doable, the migration can be tricky. If you convert your cards into DeckOps' note types, then all you need to do is export your cards from Anki to Markdown. In the first re-import, some formatting may be changed because the original HTML from Anki may not follow the CommonMark standard; however, all changes are easily trackable via Git. If your note format does not work with the DeckOps format, you will have to adapt the code to your needs.
90
+
91
+ ### How can I develop DeckOps locally?
92
+
93
+ Fork this repository and initialize the tutorial in your root folder (make sure Anki is running). This will create a folder called `collection` with the sample Markdown in it. Paths will adapt automatically to the development environment. You can run DeckOps using the main script:
94
+ ```bash
95
+ python -m main init --tutorial
96
+ python -m main ma
97
+ ```
98
+
99
+ ### How does DeckOps solve bidirectional sync? (Claude's answer)
100
+
101
+ DeckOps handles the core challenges of bidirectional synchronization between markdown and Anki:
102
+
103
+ #### 1. Identity
104
+
105
+ **Solution**: Embed immutable IDs directly in markdown as HTML comments
106
+
107
+ - **Deck Identity**: `<!-- deck_id: 1234567890 -->` on first line of file
108
+ - **Card Identity** (QA cards): `<!-- card_id: 1770487991522 -->` before each card
109
+ - **Note Identity** (Cloze): `<!-- note_id: 1770487991521 -->` before each note
110
+
111
+ IDs are Anki's native IDs (timestamps in milliseconds), written to Markdown on first sync and persisting across all future syncs, enabling bidirectional linking.
112
+
113
+
114
+ #### 2. Moves (Between Decks)
115
+
116
+ **Solution**: Move detection + automatic deck correction
117
+
118
+ **Import (Markdown → Anki)**:
119
+ When you move a card between markdown files:
120
+ 1. Cut card from `DeckA.md` (keeping its ID)
121
+ 2. Paste into `DeckB.md`
122
+ 3. Import detects card in wrong deck → **auto-moves to DeckB**
123
+ 4. **Review history preserved**
124
+
125
+
126
+ **Export (Anki → Markdown)**:
127
+ When you move a card between decks in Anki:
128
+ 1. Export detects card disappeared from DeckA, appeared in DeckB
129
+ 2. Reports as move (not deletion + creation)
130
+ 3. Card appears in correct Markdown file
131
+
132
+ Note: Deck renaming is only possible via export (Anki → Markdown). While import (Markdown → Anki) can be used to create new decks named after the file, renaming decks should always happen via export. Since the `deck_id` is not dependent on the file name, there is no conflict when the Markdown file name differs from a deck's name in Anki.
133
+
134
+ #### 3. Conflict Resolution
135
+
136
+ **Solution**: **Last sync direction wins** (no merging)
137
+
138
+ **Import (Markdown → Anki)**:
139
+ - Markdown content **overwrites** Anki content
140
+ - Updates existing cards with markdown content
141
+ - If fields match → skip (optimization)
142
+ - If fields differ → markdown wins
143
+
144
+ **Export (Anki → Markdown)**:
145
+ - Anki content **overwrites** Markdown content
146
+ - Existing blocks replaced with Anki's current state
147
+ - Deck renames reflected in file renames
148
+
149
+ This simple approach requires discipline: always sync in the same direction for a given edit session.
150
+
151
+ #### 4. Drift Detection & Recovery
152
+
153
+ **Solution**: "Stale card" detection with automatic re-creation
154
+
155
+ **What is drift?**
156
+ - Card exists in Markdown with `card_id: 123`
157
+ - But ID 123 no longer exists in Anki (manually deleted)
158
+
159
+ **How it's resolved**:
160
+ 1. Phase 1: Try to update card 123 → fails
161
+ 2. Mark as "stale"
162
+ 3. Phase 3: Re-create in Anki with new ID (e.g., 456)
163
+ 4. Phase 4: Update Markdown: `<!-- card_id: 123 -->` → `<!-- card_id: 456 -->`
164
+
165
+ **Result**: Drift is automatically healed. Content preserved, but review history lost (new card).
166
+
167
+ #### 5. Deletions
168
+
169
+ **Solution**: Bidirectional orphan cleanup
170
+
171
+ **Markdown → Anki (Import)**:
172
+ - Cards in Anki deck but NOT in Markdown file → deleted from Anki
173
+ - Exception: Cards claimed by other files are moved, not deleted
174
+
175
+ **Anki → Markdown (Export)**:
176
+ - **Orphaned decks**: File has `deck_id` but deck doesn't exist → delete file
177
+ - **Orphaned cards**: Card has `card_id` but card doesn't exist → remove block
178
+
179
+ Deletions propagate in both directions to maintain consistency.
180
+
181
+ #### Summary
182
+
183
+ | Challenge | Solution | Preserves History? |
184
+ |-----------|----------|-------------------|
185
+ | **Identity** | Embed Anki IDs in Markdown | Yes |
186
+ | **Moves** | Auto-move + global tracking | Yes |
187
+ | **Conflicts** | Last sync wins (no merge) | Yes |
188
+ | **Drift** | Stale card detection + re-creation | No |
189
+ | **Deletions** | Bidirectional orphan cleanup | N/A |
190
+
191
+ ---
192
+
193
+ ## Support This Project
194
+
195
+ If DeckOps saves you time or makes your workflow better, consider buying me a coffee—it would make my day!
196
+
197
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/visserle)
198
+
199
+ MIT License
@@ -0,0 +1,4 @@
1
+ from deckops.cli import main
2
+
3
+ __version__ = "1.0.0"
4
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,32 @@
1
+ """Shared AnkiConnect client and helpers used by both import/export scripts."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from deckops.config import ANKI_CONNECT_URL
9
+
10
+
11
+ def invoke(action: str, **params) -> Any:
12
+ """Send a request to AnkiConnect and return the result.
13
+
14
+ Raises an Exception when AnkiConnect returns an error.
15
+ """
16
+ response = requests.post(
17
+ ANKI_CONNECT_URL,
18
+ json={"action": action, "version": 6, "params": params},
19
+ timeout=10,
20
+ )
21
+ result = response.json()
22
+ if result.get("error"):
23
+ raise Exception(f"AnkiConnect error: {result['error']}")
24
+ return result["result"]
25
+
26
+
27
+ def extract_deck_id(content: str) -> tuple[int | None, str]:
28
+ """Extract deck_id from the first line and return (deck_id, remaining content)."""
29
+ match = re.match(r"<!--\s*deck_id:\s*(\d+)\s*-->\n?", content)
30
+ if match:
31
+ return int(match.group(1)), content[match.end() :]
32
+ return None, content