22# Gramps Web API - A RESTful API for the Gramps genealogy program
33#
44# Copyright (C) 2020 David Straub
5+ # Copyright (C) 2024 Doug Blank
56#
67# This program is free software; you can redistribute it and/or modify
78# it under the terms of the GNU Affero General Public License as published by
2728import sys
2829import time
2930import warnings
31+ from threading import Thread
3032
3133import click
34+ import waitress
35+ import webbrowser
3236
3337from .api .search import get_search_indexer
3438from .api .util import get_db_manager , list_trees , close_db
3539from .app import create_app
3640from .auth import add_user , delete_user , fill_tree , user_db
3741from .const import ENV_CONFIG_FILE , TREE_MULTI
3842from .dbmanager import WebDbManager
39-
40- logging .basicConfig ()
41- LOG = logging .getLogger ("gramps_webapi" )
43+ from .translogger import TransLogger
4244
4345
4446@click .group ("cli" )
@@ -58,12 +60,80 @@ def cli(ctx, config):
5860
5961@cli .command ("run" )
6062@click .option ("-p" , "--port" , help = "Port to use (default: 5000)" , default = 5000 )
61- @click .option ("--tree" , help = "Tree ID" , default = None )
63+ @click .option ("-t" , "--tree" , help = "Tree ID: '*' for multi-trees" , default = None )
64+ @click .option (
65+ "-o" ,
66+ "--open-browser" ,
67+ help = "Open gramps-web in browser: 'tab', 'window', or 'no'" ,
68+ default = "no" ,
69+ type = click .Choice (["tab" , "window" , "no" ], case_sensitive = False ),
70+ )
71+ @click .option (
72+ "-d" ,
73+ "--debug-level" ,
74+ help = "Debug level: 'info', 'debug', 'warning', 'critical'" ,
75+ default = "info" ,
76+ type = click .Choice (["info" , "debug" , "warning" , "critical" ], case_sensitive = False ),
77+ )
78+ @click .option ("-l" , "--log-file" , help = "Set logging file to this path" , default = None )
79+ @click .option (
80+ "--host" , help = "Set the host address for server to listen on" , default = "127.0.0.1"
81+ )
82+ @click .option (
83+ "--max-workers" ,
84+ help = "Maximum number of workers for frontend; requires --use-wsgi" ,
85+ default = None ,
86+ )
87+ @click .option ("--use-wsgi" , is_flag = True , help = "Add a wsgi wrapper around server" )
6288@click .pass_context
63- def run (ctx , port , tree ):
89+ def run (
90+ ctx , port , tree , host , open_browser , debug_level , log_file , max_workers , use_wsgi
91+ ):
6492 """Run the app."""
6593 app = ctx .obj ["app" ]
66- app .run (port = port , threaded = True )
94+ debug_level = debug_level .upper ()
95+ open_browser = open_browser .lower ()
96+
97+ if max_workers is None :
98+ max_workers = min (32 , os .cpu_count () + 4 )
99+
100+ def open_webbrowser_after_start ():
101+ # Wait a bit for for server to start:
102+ time .sleep (1.0 )
103+ new = {"tab" : 2 , "window" : 1 }[open_browser ]
104+ webbrowser .open ("http://%s:%s" % (host , port ), new = 0 , autoraise = True )
105+
106+ if open_browser != "no" :
107+ thread = Thread (target = open_webbrowser_after_start )
108+ thread .start ()
109+
110+ if log_file :
111+ file_handler = logging .FileHandler (log_file , "w+" )
112+ app .logger .addHandler (file_handler )
113+ app .logger .setLevel (debug_level )
114+
115+ print ("Running gramps-web-api server..." )
116+ if open_browser != "no" :
117+ print (
118+ f" Opening gramps-web in browser { open_browser } on http://{ host } :{ port } ..."
119+ )
120+
121+ print (" Control+C to quit" )
122+ if use_wsgi :
123+ waitress .serve (
124+ TransLogger (
125+ app ,
126+ setup_console_handler = False ,
127+ set_logger_level = debug_level ,
128+ ),
129+ host = host ,
130+ port = port ,
131+ threads = max_workers ,
132+ )
133+ else :
134+ app .run (port = port , threaded = True )
135+ print ()
136+ print ("Stopping gramps-web-api server..." )
67137
68138
69139@cli .group ("user" , help = "Manage users." )
@@ -82,8 +152,8 @@ def user(ctx):
82152@click .pass_context
83153def user_add (ctx , name , password , fullname , email , role , tree ):
84154 """Add a user."""
85- LOG .error (f"Adding user { name } ..." )
86155 app = ctx .obj ["app" ]
156+ app .logger .info (f"Adding user { name } ..." )
87157 with app .app_context ():
88158 user_db .create_all ()
89159 add_user (name , password , fullname , email , role , tree )
@@ -94,8 +164,8 @@ def user_add(ctx, name, password, fullname, email, role, tree):
94164@click .pass_context
95165def user_del (ctx , name ):
96166 """Delete a user."""
97- LOG .info (f"Deleting user { name } ..." )
98167 app = ctx .obj ["app" ]
168+ app .logger .info (f"Deleting user { name } ..." )
99169 with app .app_context ():
100170 delete_user (name )
101171
@@ -146,51 +216,55 @@ def search(ctx, tree, semantic):
146216 ctx .obj ["search_indexer" ] = get_search_indexer (tree = tree , semantic = semantic )
147217
148218
149- def progress_callback_count (current : int , total : int , prev : int | None = None ) -> None :
219+ def progress_callback_count (
220+ app , current : int , total : int , prev : int | None = None
221+ ) -> None :
150222 if total == 0 :
151223 return
152224 pct = int (100 * current / total )
153225 if prev is None :
154226 prev = current - 1
155227 pct_prev = int (100 * prev / total )
156228 if current == 0 or pct != pct_prev :
157- LOG .info (f"Progress: { pct } %" )
229+ app . logger .info (f"Progress: { pct } %" )
158230
159231
160232@search .command ("index-full" )
161233@click .pass_context
162234def index_full (ctx ):
163235 """Perform a full reindex."""
164- LOG .info ("Rebuilding search index ..." )
236+ app = ctx .obj ["app" ]
237+ app .logger .info ("Rebuilding search index ..." )
165238 db_manager = ctx .obj ["db_manager" ]
166239 indexer = ctx .obj ["search_indexer" ]
167240 db = db_manager .get_db ().db
168241
169242 t0 = time .time ()
170243 try :
171- indexer .reindex_full (db , progress_cb = progress_callback_count )
244+ indexer .reindex_full (app , db , progress_cb = progress_callback_count )
172245 except :
173- LOG .exception ("Error during indexing" )
246+ app . logger .exception ("Error during indexing" )
174247 finally :
175248 close_db (db )
176- LOG .info (f"Done building search index in { time .time () - t0 :.0f} seconds." )
249+ app . logger .info (f"Done building search index in { time .time () - t0 :.0f} seconds." )
177250
178251
179252@search .command ("index-incremental" )
180253@click .pass_context
181254def index_incremental (ctx ):
182255 """Perform an incremental reindex."""
256+ app = ctx .obj ["app" ]
183257 db_manager = ctx .obj ["db_manager" ]
184258 indexer = ctx .obj ["search_indexer" ]
185259 db = db_manager .get_db ().db
186260
187261 try :
188262 indexer .reindex_incremental (db , progress_cb = progress_callback_count )
189263 except Exception :
190- LOG .exception ("Error during indexing" )
264+ app . logger .exception ("Error during indexing" )
191265 finally :
192266 close_db (db )
193- LOG .info ("Done updating search index." )
267+ app . logger .info ("Done updating search index." )
194268
195269
196270@cli .group ("tree" , help = "Manage trees." )
@@ -212,7 +286,7 @@ def tree_list(ctx):
212286 print (f"{ dirname :>36} { name :<} " )
213287
214288
215- @cli .group ("grampsdb" , help = "Manage a Gramps daabase ." )
289+ @cli .group ("grampsdb" , help = "Manage a Gramps database ." )
216290@click .option ("--tree" , help = "Tree ID" , default = None )
217291@click .pass_context
218292def grampsdb (ctx , tree ):
@@ -236,8 +310,10 @@ def migrate_gramps_db(ctx):
236310
237311
238312if __name__ == "__main__" :
239- LOG .setLevel (logging .INFO )
240-
241- cli (
242- prog_name = "python3 -m gramps_webapi"
243- ) # pylint:disable=no-value-for-parameter,unexpected-keyword-arg
313+ try :
314+ cli (
315+ prog_name = "python3 -m gramps_webapi"
316+ ) # pylint:disable=no-value-for-parameter,unexpected-keyword-arg
317+ except SystemExit as e :
318+ if e .code != 0 :
319+ raise
0 commit comments