1- # when run inside Anki, __name__ variable would be numeric value (addon ID)
2- if __name__ != "addon" :
3- import atexit
4- import logging
5- import threading
6- from pathlib import Path
1+ from concurrent .futures import Future
2+ from contextlib import contextmanager
3+ from http import HTTPStatus
4+ import logging
5+ import os
6+ from pathlib import Path
7+ import tempfile
8+ from threading import Thread
9+ from typing import Iterator
10+
11+ from anki .collection import (
12+ NotetypeDict ,
13+ ImportCsvRequest ,
14+ Delimiter ,
15+ ImportLogWithChanges ,
16+ CsvMetadata ,
17+ )
18+ from aqt import mw , gui_hooks , appVersion
19+ from flask import Flask , jsonify , request , Response , Blueprint
20+ from waitress .server import create_server
21+
22+
23+ MODEL_NAME = "Cloze Multi Choice Audio"
24+
25+ DEFAULT_ADDRESS = "127.0.0.1"
26+ DEFAULT_PORT = 5555
27+
28+
29+ @contextmanager
30+ def temp_csv_file (csv_data : str ) -> Iterator [str ]:
31+ tmp = tempfile .NamedTemporaryFile (mode = "w" , delete = False , suffix = ".csv" , newline = "" )
32+ try :
33+ tmp .write (csv_data )
34+ tmp .close ()
35+ yield tmp .name
36+ finally :
37+ os .unlink (tmp .name )
738
8- from aqt import mw
9- from flask import Flask , jsonify , request , Response
10- from waitress .server import create_server
1139
12- # Configure logging to file only, Anki shows stderr as error popups!
40+ # when run inside Anki, __name__ variable would be numeric value (addon ID)
41+ if __name__ != "addon" :
1342 addon_dir = Path (__file__ ).parent
1443 log_file = addon_dir / "addon.log"
1544 logging .basicConfig (
2049 force = True ,
2150 )
2251 logger = logging .getLogger (__name__ )
23- logger .info ("addon initializing..." )
52+
53+ cfg = mw .addonManager .getConfig (__name__ ) or {}
54+ if not cfg :
55+ logger .fatal ("config is empty" )
2456
2557 app = Flask (__name__ )
2658
@@ -29,24 +61,111 @@ def log_request_info() -> None:
2961 logger .info (f"{ request .method } { request .path } " )
3062
3163 @app .route ("/" )
32- def index () -> Response :
33- return jsonify ({"status" : "healthy" })
64+ def index () -> tuple [Response , HTTPStatus ]:
65+ res : dict = {
66+ "status" : "healthy" ,
67+ "minimum_required_anki_app_version" : appVersion ,
68+ }
69+ return jsonify (res ), HTTPStatus .OK
70+
71+ # API v1 Blueprint with version prefix
72+ api_v1 = Blueprint ("api_v1" , __name__ , url_prefix = "/v1" )
73+
74+ @api_v1 .route ("/import-csv" , methods = ["POST" ])
75+ def import_csv () -> tuple [Response , HTTPStatus ]:
76+ if request .content_type != "text/csv" :
77+ return jsonify (
78+ {"message" : f"Content-type '{ request .content_type } ' must be 'text/csv'" }
79+ ), HTTPStatus .BAD_REQUEST
80+
81+ profile = request .args .get ("profile" , default = None )
82+ if profile is None or profile .strip () == "" :
83+ return jsonify (
84+ {"message" : "Missing 'profile' query parameter" }
85+ ), HTTPStatus .BAD_REQUEST
86+
87+ deck_name = request .args .get ("deck" , default = None )
88+ if deck_name is None or deck_name .strip () == "" :
89+ return jsonify (
90+ {"message" : "Missing 'deck' query parameter" }
91+ ), HTTPStatus .BAD_REQUEST
3492
35- @app .route ("/cards/count" )
36- def card_count () -> Response :
37- count = mw .col .card_count ()
38- return jsonify ({"card_count" : count })
93+ raw : str = request .data .decode ()
94+ future : Future = Future ()
95+
96+ def execute () -> None :
97+ try :
98+ if profile not in mw .pm .profiles ():
99+ future .set_result ((None , f"Profile '{ profile } ' does not exist" ))
100+ return
101+
102+ mw .pm .load (profile )
103+ col = mw .col
104+ if col is None :
105+ future .set_result ((None , "Failed to load collection" ))
106+ return
107+
108+ model : NotetypeDict | None = col .models .by_name (MODEL_NAME )
109+ if model is None :
110+ # we should create the model for Lamia as a fallback and not fail here
111+ future .set_result ((None , f"Model '{ MODEL_NAME } ' does not exist" ))
112+ return
113+
114+ # just checking what kind of model we want to create so that I can delete note-type.apkg and change README.md
115+ for k , v in model .items ():
116+ logger .info (f"{ MODEL_NAME } = { k } - { v } " )
117+
118+ deck_id = col .decks .id (deck_name , create = True )
119+
120+ # think about existing notes and match scopes of CsvMetadata during import
121+ with temp_csv_file (raw ) as path :
122+ # metadata: CsvMetadata = col.get_csv_metadata(
123+ # path=path, delimiter=Delimiter.COMMA
124+ # )
125+ # import_request: ImportCsvRequest = ImportCsvRequest(
126+ # path=path, metadata=metadata
127+ # )
128+ # response: ImportLogWithChanges = col.import_csv(import_request)
129+
130+ # res = {
131+ # "found_notes": response.log.found_notes,
132+ # "updated_notes": list(response.log.updated),
133+ # "new_notes": list(response.log.new),
134+ # }
135+ pass
136+
137+ future .set_result ((None , None ))
138+ except Exception as e :
139+ logger .exception ("Error in execute_on_main" )
140+ # this freaking propagates exception to another toxic try/catch above stack level
141+ future .set_exception (e )
142+
143+ # run on main thread of Anki to avoid SQLite threading issues
144+ mw .taskman .run_on_main (execute )
145+
146+ # block and wait for the result
147+ (res , err ) = future .result ()
148+ if err is not None :
149+ return jsonify ({"message" : err }), HTTPStatus .INTERNAL_SERVER_ERROR
150+
151+ return jsonify ({"message" : "Not implemented yet" }), HTTPStatus .NOT_IMPLEMENTED
152+
153+ address : str = cfg .get ("address" , DEFAULT_ADDRESS )
154+ port : int = cfg .get ("port" , DEFAULT_PORT )
155+
156+ app .register_blueprint (api_v1 )
157+ srv = create_server (app , host = address , port = port )
39158
40- srv = create_server (app , host = "127.0.0.1" , port = 5000 )
41159 def run_srv () -> None :
42- logger .info ("HTTP server running on http://127.0.0.1:5000 " )
160+ logger .info (f "HTTP server running on http://{ address } : { port } " )
43161 srv .run ()
44162
45163 def shutdown_srv () -> None :
46164 srv .close ()
47165 logger .info ("HTTP server shutdown successfully" )
48- atexit .register (shutdown_srv )
49166
50- th = threading .Thread (target = run_srv , daemon = True )
167+ gui_hooks .profile_will_close .append (shutdown_srv )
168+
169+ th = Thread (target = run_srv , daemon = True )
51170 th .start ()
52171 logger .info ("addon initialized successfully" )
0 commit comments