mavlab-logbook-client 0.1.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.
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: mavlab-logbook-client
3
+ Version: 0.1.1
4
+ Summary: Download and upload drone flight logs of a MAVLab flight log database (Paparazzi, ArduPilot, PX4, Betaflight)
5
+ License-Expression: GPL-3.0-only
6
+ Project-URL: Source, https://github.com/tudelft/paparazzi_log_parsing/tree/master/python/mavlab-logbook-client
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Scientific/Engineering
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # mavlab-logbook-client
14
+
15
+ Download and upload drone flight logs of a [MAVLab flight log database](https://github.com/tudelft/paparazzi_log_parsing)
16
+ from Python, without a browser download or drag and drop. Works with every log the server stores (Paparazzi
17
+ `.data`/`.log`, ArduPilot `.bin`/`.log`/`.tlog`, PX4 `.ulg`, Betaflight `.BFL`, …). Pure Python 3.10+, no dependencies.
18
+ The API follows the [MATLAB tools](https://github.com/tudelft/paparazzi_log_parsing/tree/master/matlab), which share
19
+ the login and the cache.
20
+
21
+ ```sh
22
+ pip install mavlab-logbook-client
23
+ ```
24
+
25
+ ```python
26
+ import mavlab_logbook_client as ml
27
+
28
+ ml.connect("logs.example.org") # or ml.connect("logs.example.org", key="logdb_…")
29
+ rows = ml.logbook() # the flights you may see, newest first (as pprz_logbook)
30
+ numbers = [r["flight"] for r in rows if "Hex" in r["aircraft"]]
31
+ mats = ml.fetch([1234, 1240]) # flight numbers, as in /flight/1234: MATLAB exports, one per log
32
+ mats = ml.fetch([1234, 1240], scope="flight") # one .mat per number instead (as pprz_fetch)
33
+ logs = ml.fetch_raw([1234, 1240]) # the original logs, untouched: one path per log
34
+ logs[0] # e.g. ~/.cache/mavlab-logbook-client/logs/<sha256>/24_10_30__16_27_37_SD.data (.log next to it)
35
+ receipts = ml.upload(["flight.data", "LOG00030.BFL"], wait=True)
36
+ receipts[0]["flight_ids"] # the flights the server found, for ml.fetch
37
+ ```
38
+
39
+ `connect(server, key)` does the authentication. The server is required (`https://` is added to a bare host name;
40
+ `http://` is refused except for `localhost`, so keys never travel unencrypted);
41
+ the key defaults to `LOGDB_API_KEY` (a key from your account page, as for `upload_logs.py`; not used when
42
+ `LOGDB_SERVER` names another server), else the key of the last browser login to that server. Values taken from the environment are announced and named
43
+ in errors. Without a key, `connect` opens the browser: sign in, check that the page shows the same code as the
44
+ terminal and press **Allow**. That key acts as you for a week and is stored for the next `connect`.
45
+ `connect(..., login=False)` raises `AuthError` instead (for unattended scripts); `insecure=True` accepts a
46
+ self-signed certificate. `connect` returns a `Client` with the same methods, for using several servers at once.
47
+
48
+ `ml.revoke_all()` revokes your browser-login keys on every computer (Python and MATLAB) and forgets the stored one.
49
+ Keys made on the account page are revoked there.
50
+
51
+ **The logbook** (`ml.logbook()`, as MATLAB's `pprz_logbook`) is a list of dicts, one per flight, newest first:
52
+ `flight` (the number for `fetch`), `date` (takeoff, UTC `datetime`; `None` when unknown), `date_ok` (`False` when
53
+ the log's clock was not set and no date was entered), `aircraft`, `duration_min`, `pic`, `project`, `goal`, `verdict`,
54
+ `simulation` (`True` for a simulated flight) and `log`. It holds the flights the parser judged real; `all=True` adds uncertain and ground
55
+ records. Paper records are left out. `pandas.DataFrame(rows)` makes a table of it.
56
+
57
+ **Fetching** works as MATLAB's `pprz_fetch`: `fetch` downloads the MATLAB exports (`.mat`, v7.3 = HDF5, e.g. for
58
+ `h5py`; the format is in the [MATLAB README](https://github.com/tudelft/paparazzi_log_parsing/tree/master/matlab))
59
+ and exists for Paparazzi logs. `scope` chooses what each file holds: `"selected"` (default) the given flights of
60
+ each log, 10 s before takeoff to 10 s after landing; `"all"` every flight of those logs; `"log"` the whole log; these
61
+ return one file per log. `"flight"` returns one file per number. Exports are cached (shared with MATLAB) and
62
+ reused while they would be identical; a change of the log, parser or logbook data fetches them again.
63
+
64
+ `fetch_raw` downloads the original log files that hold the flights (`.data` with its `.log`, `.BFL`, `.ulg`, …),
65
+ whole and untouched, one path per log. One never changes (it is named by its SHA-256, which is checked after each
66
+ download). For both, `force=True` downloads again; without a connection or when the server fails (HTTP 5xx) the
67
+ cached copies are used, with a warning.
68
+
69
+ **Uploads** pair a `.data` with the `.log` next to it (whichever of the two you pass). A log already on the server
70
+ is skipped. `name="Hex 1/LOG00030.BFL"` sets the stored name of a single file, which can link it to a fleet
71
+ aircraft. With `wait=True` each call returns once the server has parsed the log, else as soon as it accepted it.
72
+
73
+ ## Command line
74
+
75
+ ```sh
76
+ mavlab-logbook-client --server logs.example.org status # signs in if needed; user, key and cache
77
+ mavlab-logbook-client --server logs.example.org logbook # tab-separated flights; --all
78
+ mavlab-logbook-client --server logs.example.org fetch 1234 1240 # prints the .mat paths; --scope flight
79
+ mavlab-logbook-client --server logs.example.org fetch-raw 1234 1240 # prints the paths of the original logs
80
+ mavlab-logbook-client --server logs.example.org upload flight.data LOG00030.BFL --wait
81
+ mavlab-logbook-client --server logs.example.org revoke-all # keys of browser logins, all computers
82
+ mavlab-logbook-client --server logs.example.org --key logdb_… fetch 1234 # as connect(server, key)
83
+ ```
84
+
85
+ ## Where files are kept
86
+
87
+ | | Linux | macOS | Windows |
88
+ |---|---|---|---|
89
+ | Login (`config.json`) | `~/.config/mavlab-logbook-client` | `~/Library/Application Support/mavlab-logbook-client` | `%APPDATA%\mavlab-logbook-client` |
90
+ | Cache | `~/.cache/mavlab-logbook-client` | `~/Library/Caches/mavlab-logbook-client` | `%LOCALAPPDATA%\mavlab-logbook-client\Cache` |
91
+
92
+ `config.json` holds API keys and is readable by you only; `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` are respected on
93
+ Linux. `MAVLAB_LOGBOOK_CACHE` (or `ml.set_cache_dir(folder)`, also seen by MATLAB) and `MAVLAB_LOGBOOK_CONFIG` choose
94
+ other folders. The cache holds `mat/<server>/` (exports, from `fetch` and MATLAB), `logs/<sha256>/` (original files) and
95
+ `flights/<server>/` (which log each flight is in, for offline use); delete it at any time to free space.
@@ -0,0 +1,83 @@
1
+ # mavlab-logbook-client
2
+
3
+ Download and upload drone flight logs of a [MAVLab flight log database](https://github.com/tudelft/paparazzi_log_parsing)
4
+ from Python, without a browser download or drag and drop. Works with every log the server stores (Paparazzi
5
+ `.data`/`.log`, ArduPilot `.bin`/`.log`/`.tlog`, PX4 `.ulg`, Betaflight `.BFL`, …). Pure Python 3.10+, no dependencies.
6
+ The API follows the [MATLAB tools](https://github.com/tudelft/paparazzi_log_parsing/tree/master/matlab), which share
7
+ the login and the cache.
8
+
9
+ ```sh
10
+ pip install mavlab-logbook-client
11
+ ```
12
+
13
+ ```python
14
+ import mavlab_logbook_client as ml
15
+
16
+ ml.connect("logs.example.org") # or ml.connect("logs.example.org", key="logdb_…")
17
+ rows = ml.logbook() # the flights you may see, newest first (as pprz_logbook)
18
+ numbers = [r["flight"] for r in rows if "Hex" in r["aircraft"]]
19
+ mats = ml.fetch([1234, 1240]) # flight numbers, as in /flight/1234: MATLAB exports, one per log
20
+ mats = ml.fetch([1234, 1240], scope="flight") # one .mat per number instead (as pprz_fetch)
21
+ logs = ml.fetch_raw([1234, 1240]) # the original logs, untouched: one path per log
22
+ logs[0] # e.g. ~/.cache/mavlab-logbook-client/logs/<sha256>/24_10_30__16_27_37_SD.data (.log next to it)
23
+ receipts = ml.upload(["flight.data", "LOG00030.BFL"], wait=True)
24
+ receipts[0]["flight_ids"] # the flights the server found, for ml.fetch
25
+ ```
26
+
27
+ `connect(server, key)` does the authentication. The server is required (`https://` is added to a bare host name;
28
+ `http://` is refused except for `localhost`, so keys never travel unencrypted);
29
+ the key defaults to `LOGDB_API_KEY` (a key from your account page, as for `upload_logs.py`; not used when
30
+ `LOGDB_SERVER` names another server), else the key of the last browser login to that server. Values taken from the environment are announced and named
31
+ in errors. Without a key, `connect` opens the browser: sign in, check that the page shows the same code as the
32
+ terminal and press **Allow**. That key acts as you for a week and is stored for the next `connect`.
33
+ `connect(..., login=False)` raises `AuthError` instead (for unattended scripts); `insecure=True` accepts a
34
+ self-signed certificate. `connect` returns a `Client` with the same methods, for using several servers at once.
35
+
36
+ `ml.revoke_all()` revokes your browser-login keys on every computer (Python and MATLAB) and forgets the stored one.
37
+ Keys made on the account page are revoked there.
38
+
39
+ **The logbook** (`ml.logbook()`, as MATLAB's `pprz_logbook`) is a list of dicts, one per flight, newest first:
40
+ `flight` (the number for `fetch`), `date` (takeoff, UTC `datetime`; `None` when unknown), `date_ok` (`False` when
41
+ the log's clock was not set and no date was entered), `aircraft`, `duration_min`, `pic`, `project`, `goal`, `verdict`,
42
+ `simulation` (`True` for a simulated flight) and `log`. It holds the flights the parser judged real; `all=True` adds uncertain and ground
43
+ records. Paper records are left out. `pandas.DataFrame(rows)` makes a table of it.
44
+
45
+ **Fetching** works as MATLAB's `pprz_fetch`: `fetch` downloads the MATLAB exports (`.mat`, v7.3 = HDF5, e.g. for
46
+ `h5py`; the format is in the [MATLAB README](https://github.com/tudelft/paparazzi_log_parsing/tree/master/matlab))
47
+ and exists for Paparazzi logs. `scope` chooses what each file holds: `"selected"` (default) the given flights of
48
+ each log, 10 s before takeoff to 10 s after landing; `"all"` every flight of those logs; `"log"` the whole log; these
49
+ return one file per log. `"flight"` returns one file per number. Exports are cached (shared with MATLAB) and
50
+ reused while they would be identical; a change of the log, parser or logbook data fetches them again.
51
+
52
+ `fetch_raw` downloads the original log files that hold the flights (`.data` with its `.log`, `.BFL`, `.ulg`, …),
53
+ whole and untouched, one path per log. One never changes (it is named by its SHA-256, which is checked after each
54
+ download). For both, `force=True` downloads again; without a connection or when the server fails (HTTP 5xx) the
55
+ cached copies are used, with a warning.
56
+
57
+ **Uploads** pair a `.data` with the `.log` next to it (whichever of the two you pass). A log already on the server
58
+ is skipped. `name="Hex 1/LOG00030.BFL"` sets the stored name of a single file, which can link it to a fleet
59
+ aircraft. With `wait=True` each call returns once the server has parsed the log, else as soon as it accepted it.
60
+
61
+ ## Command line
62
+
63
+ ```sh
64
+ mavlab-logbook-client --server logs.example.org status # signs in if needed; user, key and cache
65
+ mavlab-logbook-client --server logs.example.org logbook # tab-separated flights; --all
66
+ mavlab-logbook-client --server logs.example.org fetch 1234 1240 # prints the .mat paths; --scope flight
67
+ mavlab-logbook-client --server logs.example.org fetch-raw 1234 1240 # prints the paths of the original logs
68
+ mavlab-logbook-client --server logs.example.org upload flight.data LOG00030.BFL --wait
69
+ mavlab-logbook-client --server logs.example.org revoke-all # keys of browser logins, all computers
70
+ mavlab-logbook-client --server logs.example.org --key logdb_… fetch 1234 # as connect(server, key)
71
+ ```
72
+
73
+ ## Where files are kept
74
+
75
+ | | Linux | macOS | Windows |
76
+ |---|---|---|---|
77
+ | Login (`config.json`) | `~/.config/mavlab-logbook-client` | `~/Library/Application Support/mavlab-logbook-client` | `%APPDATA%\mavlab-logbook-client` |
78
+ | Cache | `~/.cache/mavlab-logbook-client` | `~/Library/Caches/mavlab-logbook-client` | `%LOCALAPPDATA%\mavlab-logbook-client\Cache` |
79
+
80
+ `config.json` holds API keys and is readable by you only; `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` are respected on
81
+ Linux. `MAVLAB_LOGBOOK_CACHE` (or `ml.set_cache_dir(folder)`, also seen by MATLAB) and `MAVLAB_LOGBOOK_CONFIG` choose
82
+ other folders. The cache holds `mat/<server>/` (exports, from `fetch` and MATLAB), `logs/<sha256>/` (original files) and
83
+ `flights/<server>/` (which log each flight is in, for offline use); delete it at any time to free space.
@@ -0,0 +1,70 @@
1
+ """Download and upload flight logs of a MAVLab flight log database from Python.
2
+
3
+ import mavlab_logbook_client as ml
4
+ ml.connect("logs.example.org") # signs in through the browser when needed
5
+ rows = ml.logbook() # the flights you may see, newest first
6
+ mats = ml.fetch([1234, 1240]) # flight numbers, as in /flight/1234; MATLAB exports, as pprz_fetch
7
+ logs = ml.fetch_raw([1234, 1240]) # the original logs (.data with .log, .BFL, .ulg, ...)
8
+ ml.upload("LOG00030.BFL", wait=True)
9
+
10
+ See connect() and Client for the details; the command line tool is ``mavlab-logbook-client``.
11
+ """
12
+ from .client import AuthError, Client, Error, HTTPError, ProcessingFailed
13
+ from .settings import cache_dir, set_cache_dir
14
+
15
+ __version__ = "0.1.1"
16
+ __all__ = ["Client", "connect", "logbook", "fetch", "fetch_raw", "upload", "revoke_all", "cache_dir", "set_cache_dir",
17
+ "Error", "AuthError", "HTTPError", "ProcessingFailed"]
18
+
19
+ _client = None
20
+
21
+
22
+ def connect(server, key=None, *, login=True, insecure=False):
23
+ """Connect to the logbook server for logbook(), fetch(), fetch_raw(), upload() and revoke_all(), and return the
24
+ Client.
25
+
26
+ server: URL or host name, e.g. "logs.example.org" (https:// is added; http:// only for localhost).
27
+ key: API key from the account page; default LOGDB_API_KEY, else the key of the last browser login to this
28
+ server, else a new browser login (sign in, compare the code, press Allow; the key lasts a week).
29
+ Values taken from the environment are announced. login=False raises AuthError instead of opening the browser.
30
+ """
31
+ global _client
32
+ client = Client(server, key, login=login, insecure=insecure)
33
+ try:
34
+ user = client.me()
35
+ except OSError as exc:
36
+ raise Error(f"cannot reach {client._origin()}: {exc}") from exc
37
+ print(f"Connected to {client.server} as {user.get('display_name', '?')}.", flush=True)
38
+ _client = client
39
+ return client
40
+
41
+
42
+ def _connected():
43
+ if _client is None:
44
+ raise Error("not connected: call mavlab_logbook_client.connect() first")
45
+ return _client
46
+
47
+
48
+ def logbook(all=False):
49
+ """The logbook flights you may see, newest first (dicts with flight, date, aircraft, …); see Client.logbook."""
50
+ return _connected().logbook(all=all)
51
+
52
+
53
+ def fetch(flights, scope="selected", force=False):
54
+ """Local paths of the MATLAB exports (.mat) of flights by number, as MATLAB's pprz_fetch; see Client.fetch."""
55
+ return _connected().fetch(flights, scope=scope, force=force)
56
+
57
+
58
+ def fetch_raw(flights, force=False):
59
+ """Local paths of the original logs of flights by number, one per log; see Client.fetch_raw."""
60
+ return _connected().fetch_raw(flights, force=force)
61
+
62
+
63
+ def upload(paths, name=None, wait=False, skip_existing=True):
64
+ """Upload log files; see Client.upload."""
65
+ return _connected().upload(paths, name=name, wait=wait, skip_existing=skip_existing)
66
+
67
+
68
+ def revoke_all():
69
+ """Revoke your API keys from browser logins (all computers); see Client.revoke_all."""
70
+ return _connected().revoke_all()
@@ -0,0 +1,75 @@
1
+ """Command line: mavlab-logbook-client [--server URL] [--key KEY] status | logbook | fetch | fetch-raw | upload |
2
+ revoke-all."""
3
+ import argparse
4
+ import sys
5
+
6
+ from . import __version__, settings
7
+ from .client import Client, Error
8
+
9
+
10
+ def main(argv=None):
11
+ ap = argparse.ArgumentParser(prog="mavlab-logbook-client",
12
+ description="Download and upload logs of a flight log database.")
13
+ ap.add_argument("--version", action="version", version=__version__)
14
+ ap.add_argument("--server", required=True, help="server URL or host name")
15
+ ap.add_argument("--key", help="API key (default: LOGDB_API_KEY, else the stored browser login, else a new one)")
16
+ ap.add_argument("--insecure", action="store_true", help="do not verify the https certificate (self-signed)")
17
+ sub = ap.add_subparsers(dest="command", required=True)
18
+ sub.add_parser("status", help="sign in if needed and show the server, user, key and cache folder")
19
+ p = sub.add_parser("logbook", help="list the flights you may see, newest first, tab-separated")
20
+ p.add_argument("--all", action="store_true", help="also flights judged uncertain or ground")
21
+ p = sub.add_parser("fetch", help="download the MATLAB exports (.mat) of flights by number and print their paths")
22
+ p.add_argument("flights", nargs="+", type=int, help="flight numbers (the number in /flight/<number>)")
23
+ p.add_argument("--scope", choices=("selected", "all", "log", "flight"), default="selected",
24
+ help="the given flights of each log, all its flights, the whole log, or one file per flight")
25
+ p.add_argument("--force", action="store_true", help="download again even when cached")
26
+ p = sub.add_parser("fetch-raw", help="download the original logs of flights by number and print their paths")
27
+ p.add_argument("flights", nargs="+", type=int, help="flight numbers (the number in /flight/<number>)")
28
+ p.add_argument("--force", action="store_true", help="download again even when cached")
29
+ p = sub.add_parser("upload", help="upload log files")
30
+ p.add_argument("files", nargs="+", help="log files (.data with its .log next to it, .BFL, .ulg, .bin, ...)")
31
+ p.add_argument("--name", help="stored name of a single file, e.g. 'Hex 1/LOG00030.BFL'")
32
+ p.add_argument("--wait", action="store_true", help="wait until the server has parsed each log")
33
+ p.add_argument("--again", action="store_true", help="upload even when the server already has the log")
34
+ sub.add_parser("revoke-all", help="revoke your keys from browser logins (all computers)")
35
+ a = ap.parse_args(argv)
36
+ try:
37
+ client = Client(a.server, a.key, insecure=a.insecure)
38
+ if a.command == "status":
39
+ stored = settings.entry(client.server) or {}
40
+ user = client.me()
41
+ print(f"Server: {client.server}")
42
+ print(f"User: {user.get('display_name')}")
43
+ print(f"Key: {client.key_source}" + (f", until {stored['expires_at']}"
44
+ if client.key_source == "browser login" and stored.get("expires_at") else ""))
45
+ print(f"Cache: {settings.cache_dir()}")
46
+ elif a.command == "logbook":
47
+ columns = ("flight", "date", "date_ok", "aircraft", "duration_min", "pic", "project", "goal", "verdict",
48
+ "simulation", "log")
49
+ print("\t".join(columns))
50
+ for r in client.logbook(all=a.all):
51
+ r["date"] = r["date"].strftime("%Y-%m-%d %H:%M:%S") if r["date"] else ""
52
+ r["duration_min"] = f"{r['duration_min']:.1f}" if r["duration_min"] is not None else ""
53
+ print("\t".join("" if r[c] is None else str(r[c]).replace("\t", " ") for c in columns))
54
+ elif a.command == "fetch":
55
+ for path in client.fetch(a.flights, scope=a.scope, force=a.force):
56
+ print(path)
57
+ elif a.command == "fetch-raw":
58
+ for path in client.fetch_raw(a.flights, force=a.force):
59
+ print(path)
60
+ elif a.command == "upload":
61
+ for r in client.upload(a.files, name=a.name, wait=a.wait, skip_existing=not a.again):
62
+ extra = f" log {r['log_id']}, flights {r.get('flight_ids')}" if r.get("log_id") else ""
63
+ print(f"{r.get('name')}: {r['status']}{extra}")
64
+ elif a.command == "revoke-all":
65
+ client.revoke_all()
66
+ except (Error, OSError, ValueError) as exc:
67
+ print(f"mavlab-logbook-client: {exc}", file=sys.stderr)
68
+ return 1
69
+ except KeyboardInterrupt:
70
+ return 130
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ sys.exit(main())