snapshot of https://github.com/ActivityWatch/aw-core.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / Test on ubuntu-latest, py-3.8 (push) Successful in 4m21s
Lint / lint (push) Successful in 25s
Build / Test on macOS-latest, py-3.8 (push) Has been cancelled
Build / Test on windows-latest, py-3.8 (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
Some checks failed
Build / Test on ubuntu-latest, py-3.8 (push) Successful in 4m21s
Lint / lint (push) Successful in 25s
Build / Test on macOS-latest, py-3.8 (push) Has been cancelled
Build / Test on windows-latest, py-3.8 (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
This commit is contained in:
107
aw_cli/__main__.py
Normal file
107
aw_cli/__main__.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
The idea behind this `aw` or `aw-cli` wrapper script is to act as a collection of helper tools,
|
||||
and perhaps even as a way to list and run ActivityWatch modules on a system (a bit like aw-qt, but without the GUI).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
|
||||
from aw_cli.log import find_oldest_log, print_log, LOGLEVELS
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def _resolve_profile(profile: Optional[str], testing: bool) -> str:
|
||||
"""Return the active profile name, resolving --testing as an alias for 'testing'."""
|
||||
if profile:
|
||||
return profile
|
||||
if testing:
|
||||
return "testing"
|
||||
return ""
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option(
|
||||
"--profile",
|
||||
default=None,
|
||||
help="Named instance profile (e.g. 'testing', 'research').",
|
||||
)
|
||||
@click.option("--testing", is_flag=True, help="Alias for --profile testing.")
|
||||
def main(profile: Optional[str] = None, testing: bool = False):
|
||||
active = _resolve_profile(profile, testing)
|
||||
if active:
|
||||
os.environ["AW_PROFILE"] = active
|
||||
# If no explicit flag, preserve any inherited AW_PROFILE from the environment.
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.pass_context
|
||||
def qt(ctx):
|
||||
profile = ctx.parent.params.get("profile")
|
||||
testing = ctx.parent.params.get("testing")
|
||||
active = _resolve_profile(profile, testing)
|
||||
args = ["aw-qt"]
|
||||
if active:
|
||||
args += ["--profile", active]
|
||||
return subprocess.call(args)
|
||||
|
||||
|
||||
@main.command()
|
||||
def directories():
|
||||
# Print all directories (respects AW_PROFILE set by the group callback)
|
||||
from aw_core.dirs import get_data_dir, get_config_dir, get_cache_dir, get_log_dir
|
||||
|
||||
print("Directory paths used")
|
||||
print(" - config: ", get_config_dir(None))
|
||||
print(" - data: ", get_data_dir(None))
|
||||
print(" - logs: ", get_log_dir(None))
|
||||
print(" - cache: ", get_cache_dir(None))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.pass_context
|
||||
@click.argument("module_name", type=str, required=False)
|
||||
@click.option(
|
||||
"--since",
|
||||
type=click.DateTime(formats=["%Y-%m-%d"]),
|
||||
help="Only show logs since this date",
|
||||
)
|
||||
@click.option(
|
||||
"--level",
|
||||
type=click.Choice(LOGLEVELS),
|
||||
help="Only show logs of this level, or higher.",
|
||||
)
|
||||
def logs(
|
||||
ctx,
|
||||
module_name: Optional[str] = None,
|
||||
since: Optional[datetime] = None,
|
||||
level: Optional[str] = None,
|
||||
):
|
||||
from aw_core.dirs import get_log_dir
|
||||
|
||||
# AW_PROFILE was set by the group callback, so get_log_dir returns the profile-specific dir.
|
||||
logdir: Path = Path(get_log_dir(None))
|
||||
|
||||
if module_name:
|
||||
_print_newest_log(logdir / module_name, since, level)
|
||||
else:
|
||||
for subdir in sorted(logdir.iterdir()):
|
||||
if subdir.is_dir():
|
||||
_print_newest_log(subdir, since, level)
|
||||
|
||||
|
||||
def _print_newest_log(
|
||||
path: Path, since: Optional[datetime], level: Optional[str]
|
||||
) -> None:
|
||||
logfile = find_oldest_log(path)
|
||||
if logfile:
|
||||
print_log(logfile, since, level)
|
||||
else:
|
||||
print(f"No logfile found in {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
57
aw_cli/log.py
Normal file
57
aw_cli/log.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
LOGLEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
|
||||
|
||||
def print_log(
|
||||
path: Path, since: Optional[datetime] = None, level: Optional[str] = None
|
||||
):
|
||||
if not path.is_file():
|
||||
return
|
||||
|
||||
show_levels = LOGLEVELS[LOGLEVELS.index(level) :] if level else None
|
||||
|
||||
lines_printed = 0
|
||||
with path.open("r") as f:
|
||||
lines = f.readlines()
|
||||
print(f"Logs for module {path.parent.name} ({path.name}, {len(lines)} lines)")
|
||||
for line in lines:
|
||||
if since:
|
||||
try:
|
||||
linedate = datetime.strptime(line.split(" ")[0], "%Y-%m-%d")
|
||||
except ValueError:
|
||||
# Could not parse the date, so skip this line
|
||||
# NOTE: Just because the date could not be parsed, doesn't mean there isn't meaningful info there.
|
||||
# Would be better to find the first line after the cutoff, and then just print everything past that.
|
||||
continue
|
||||
# Skip lines before the date
|
||||
if linedate < since:
|
||||
continue
|
||||
if level:
|
||||
if not any(level in line for level in show_levels):
|
||||
continue
|
||||
print(line, end="")
|
||||
lines_printed += 1
|
||||
|
||||
print(f" (Filtered {lines_printed}/{len(lines)} lines)")
|
||||
|
||||
|
||||
def find_oldest_log(path: Path) -> Optional[Path]:
|
||||
"""Return the most-recently-modified log file under *path*.
|
||||
|
||||
Since aw-core #149 each profile runs in its own platform directory
|
||||
(``activitywatch-<profile>``), so no filename-based profile filtering is
|
||||
needed here — every ``.log`` file in *path* belongs to the active profile.
|
||||
"""
|
||||
if not path.is_dir():
|
||||
return None
|
||||
|
||||
logfiles = [f for f in path.iterdir() if f.is_file() and f.name.endswith(".log")]
|
||||
if not logfiles:
|
||||
return None
|
||||
|
||||
logfiles.sort(key=lambda f: f.stat().st_mtime)
|
||||
return logfiles[-1]
|
||||
Reference in New Issue
Block a user