Skip to content

Commit 7520fc9

Browse files
Introduce flask-less CLI interface (e.g. for programmatic DB access) (#1909)
Resolves #1690. (Successor of tiny-pilot/tinypilot-pro#1602.) This PR introduces a new, internal Flask-less CLI entrypoint to our app, for programmatic access of certain features (mainly DB-related). As a first use-case, it migrates the `streaming-mode` CLI command, mainly to demonstrate how this is intended to be used. We’ll soon [add another command for retrieving the serial settings](tiny-pilot/tinypilot-pro#1332 (comment)). In a subsequent step, we can also migrate [the last remaining command for activating a license](https://github.com/tiny-pilot/tinypilot-pro/blob/b93dc93f4f2581118325ffe9eaa30d7f5fefae26/app/flask_cli.py#L13-L21) and thereby sunset the Flask CLI entrypoint altogether. (I’d create a separate ticket for that.) ## Notes - Although the footprint of the CLI functionality is still quite small (and is unlikely to grow substantially in the near-to-mid future), I thought that a separate Python module (directory) was warranted (`app/cli`). That way, we hopefully have a clearer separation from the web app, and we are also a bit more flexible to structure the code nicely. - I introduced a `@command` annotation to make it convenient to register commands. My thinking is not to go overboard with this, though – e.g., I’d not expand this mechanism to include arg-parsing etc., as it would then probably make more sense to rather keep the Flask CLI or introduce another framework. The main intention of `@command` is to have all command-related code closely together, as opposed to having to define the function in one place and then “register” it in a second. Since the whole CLI is more of an internal helper utility, I think it’s fair to keep the whole structure basic. <a data-ca-tag href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1909"><img src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review on CodeApprove" /></a> --------- Co-authored-by: Jan Heuermann <[email protected]>
1 parent 8c2bbdd commit 7520fc9

File tree

9 files changed

+85
-35
lines changed

9 files changed

+85
-35
lines changed

app/cli.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

app/cli/__init__.py

Whitespace-only changes.

app/cli/commands.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import db.settings
2+
from cli.registry import command
3+
4+
5+
@command('streaming-mode')
6+
def streaming_mode(_args):
7+
"""Prints the currently applicable streaming mode."""
8+
mode = db.settings.Settings().get_streaming_mode().value
9+
print(mode)

app/cli/main.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Internal entrypoint for accessing certain app features from the CLI.
2+
3+
This package is an internal utility that we use to programmatically access
4+
certain features of our app without needing to initialize a full Flask
5+
application context. (The latter would be quite cumbersome and slow.)
6+
7+
The structure of the CLI interface is deliberately minimal, as we don’t expose
8+
it to end-users directly, but if at all only via dedicated wrapper scripts.
9+
"""
10+
11+
import sys
12+
13+
import cli.commands # noqa: F401
14+
import cli.registry
15+
16+
17+
def main():
18+
if len(sys.argv) < 2:
19+
print('Missing required subcommand!', file=sys.stderr)
20+
sys.exit(1)
21+
22+
try:
23+
command = cli.registry.COMMANDS[sys.argv[1]]
24+
except KeyError:
25+
print('No such subcommand!', file=sys.stderr)
26+
sys.exit(1)
27+
28+
command(sys.argv[1:])
29+
30+
31+
if __name__ == '__main__':
32+
main()

app/cli/registry.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
COMMANDS = {}
2+
3+
4+
def command(command_name):
5+
6+
def decorator(func):
7+
COMMANDS[command_name] = func
8+
return func
9+
10+
return decorator

app/db_connection.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,42 @@
77

88

99
def get():
10-
"""Returns a database connection (sqlite3.dbapi2.connection)."""
11-
# Keep in mind that Flask only caches the connection object on a per-request
12-
# basis, and not throughout the entire runtime of the server.
13-
connection = _get_flask_db()
10+
"""Returns a connection to the SQlite database.
11+
12+
Within a Flask app context, this function applies a caching mechanism to
13+
avoid redundant initialization overhead. Outside a Flask app context, it
14+
creates a fresh DB connection on every invocation.
15+
16+
Returns:
17+
sqlite3.dbapi2.connection
18+
"""
19+
if not flask.has_app_context():
20+
return db.store.create_or_open(_DB_PATH)
21+
22+
connection = _get_cached_flask_db()
1423
if connection is None:
1524
connection = db.store.create_or_open(_DB_PATH)
16-
_set_flask_db(connection)
25+
_cache_flask_db(connection)
1726
return connection
1827

1928

2029
def close():
21-
connection = _get_flask_db()
30+
"""Closes a potentially cached DB connection.
31+
32+
Noop if no DB connection was cached or if invocation context is not within
33+
Flask.
34+
"""
35+
if not flask.has_app_context():
36+
return
37+
38+
connection = _get_cached_flask_db()
2239
if connection is not None:
2340
connection.close()
2441

2542

26-
def _get_flask_db():
43+
def _get_cached_flask_db():
2744
return getattr(flask.g, '_database', None)
2845

2946

30-
def _set_flask_db(flask_db):
47+
def _cache_flask_db(flask_db):
3148
flask.g._database = flask_db # pylint: disable=protected-access

app/main.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# app-wide logger class before any other module loads it.
1212
import log
1313
import api
14-
import cli
1514
import json_response
1615
import license_notice
1716
import secret_key
@@ -49,7 +48,6 @@
4948
app.register_blueprint(api.api_blueprint)
5049
app.register_blueprint(license_notice.blueprint)
5150
app.register_blueprint(views.views_blueprint)
52-
app.register_blueprint(cli.cli_blueprint)
5351

5452

5553
@app.errorhandler(flask_wtf.csrf.CSRFError)

debian-pkg/opt/tinypilot-privileged/scripts/collect-debug-logs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ print_info "Checking for voltage issues..."
166166
print_info "Checking TinyPilot streaming mode..."
167167
{
168168
echo "Streaming mode"
169-
echo "Selected mode: $(runuser tinypilot --command '/opt/tinypilot/scripts/streaming-mode')"
169+
echo "Selected mode: $(runuser tinypilot --command '/opt/tinypilot/scripts/cli streaming-mode')"
170170
printf "Current mode: "
171171
# H264 mode is considered active when the last Janus video log line contains
172172
# "Memsink opened; reading frames".
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
#!/bin/bash
22

3-
# Prints TinyPilot's preferred video streaming mode, either H264 or MJPEG.
3+
# Helper script to invoke the CLI entrypoint of the TinyPilot app.
4+
# See `app/cli/main.py` for more info, including the available subcommands.
5+
#
6+
# Example of usage:
7+
# ./scripts/cli streaming-mode
8+
49

510
# Exit on first failure.
611
set -e
@@ -18,7 +23,6 @@ fi
1823
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
1924
readonly SCRIPT_DIR
2025
cd "${SCRIPT_DIR}/.."
21-
. ./venv/bin/activate
2226

23-
export FLASK_APP='app/main.py'
24-
flask cli streaming-mode
27+
. venv/bin/activate
28+
python -m app.cli.main "${@:1}"

0 commit comments

Comments
 (0)