← back to ColonelPanicX__garmin-extract

Function bodies 373 total

All specs Real LLM only Function bodies
setup_gmail_mfa function · python · L532-L560 (29 LOC)
garmin_extract/menu.py
def setup_gmail_mfa() -> None:
    header("Gmail MFA Automation Setup")
    print("  When Garmin requires a security code (~every 30 days), this module")
    print("  polls your Gmail inbox and submits it automatically. Without it,")
    print("  you'll be prompted to paste the code manually.\n")
    hr()

    creds_file = ROOT / "google_credentials.json"
    if not creds_file.exists():
        print()
        console.print("  [red]✗[/]  google_credentials.json not found.\n")
        print("  To create it:")
        print("    1. Go to console.cloud.google.com")
        print("    2. Create a project")
        print("    3. Enable the Gmail API")
        print("    4. Credentials → Create → OAuth 2.0 Client ID → Desktop app")
        print("    5. Download JSON → save as google_credentials.json in this directory")
        print()
        print("  Then run this option again to complete authorization.")
        _continue()
        return

    print("  google_credentials.json found. Start
_parse_date function · python · L568-L575 (8 LOC)
garmin_extract/menu.py
def _parse_date(s: str) -> str | None:
    """Accept YYYY-MM-DD, MM/DD/YYYY, MM/DD/YY, MM-DD-YYYY, MM-DD-YY."""
    for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y", "%m-%d-%Y", "%m-%d-%y"):
        try:
            return datetime.strptime(s.strip(), fmt).strftime("%Y-%m-%d")
        except ValueError:
            continue
    return None
_run_pull function · python · L578-L599 (22 LOC)
garmin_extract/menu.py
def _run_pull(start_date: str, days: int, no_skip: bool = False) -> None:
    """Run the Garmin puller, then immediately rebuild CSVs."""
    cmd = [
        PYTHON,
        str(ROOT / "pullers" / "garmin.py"),
        "--date",
        start_date,
        "--days",
        str(days),
    ]
    if no_skip:
        cmd.append("--no-skip")

    subprocess.run(cmd)

    print()
    print("  Building CSV reports...")
    subprocess.run([PYTHON, str(ROOT / "reports" / "build_garmin_csvs.py")])
    print()
    console.print("  [green]✓[/]  Done.")
    print("  · reports/garmin_daily.csv")
    print("  · reports/garmin_activities.csv")
_pull_yesterday function · python · L607-L611 (5 LOC)
garmin_extract/menu.py
def _pull_yesterday() -> None:
    yesterday = (date.today() - timedelta(days=1)).isoformat()
    print(f"\n  Pulling {yesterday}...\n")
    _run_pull(yesterday, 1)
    _continue()
_pull_last_n function · python · L614-L618 (5 LOC)
garmin_extract/menu.py
def _pull_last_n(n: int, label: str) -> None:
    start = (date.today() - timedelta(days=n)).isoformat()
    print(f"\n  Pulling {label} ({start} → yesterday)...\n")
    _run_pull(start, n)
    _continue()
_pull_custom function · python · L621-L644 (24 LOC)
garmin_extract/menu.py
def _pull_custom() -> None:
    header("Custom Date Pull")
    print("  Accepted date formats: YYYY-MM-DD  |  MM/DD/YYYY  |  MM/DD/YY\n")

    raw = prompt_with_navigation("  Start date: ")
    if not raw:
        return
    start = _parse_date(raw)
    if not start:
        print(f"\n  Could not parse '{raw}' as a date. Try: 2025-04-07 or 04/07/2025")
        _continue()
        return

    days_in = prompt_with_navigation("  Number of days to pull [Enter for 1]: ")
    days = int(days_in) if days_in.isdigit() and int(days_in) > 0 else 1

    end = (datetime.strptime(start, "%Y-%m-%d").date() + timedelta(days=days - 1)).isoformat()
    range_label = start if days == 1 else f"{start} → {end}"
    print(f"\n  Pulling {days} day(s): {range_label}")

    reskip = prompt_with_navigation("  Re-pull dates that already have data? [y/N]: ")
    print()
    _run_pull(start, days, no_skip=(reskip.lower() == "y"))
    _continue()
_pull_everything function · python · L647-L696 (50 LOC)
garmin_extract/menu.py
def _pull_everything() -> None:
    header("Pull Full History")
    print("  Pulls every day from a start date through yesterday.")
    print("  Use this to build a complete data history via the live API.\n")
    print("  Each day takes ~15 seconds to pull.")
    print("  · 1 month  ≈  7 minutes")
    print("  · 6 months ≈  45 minutes")
    print("  · 1 year   ≈  90 minutes\n")
    print("  For very large history pulls, the Garmin bulk export (option 6)")
    print("  is faster — request it from Garmin and import the .zip.\n")
    print("  Accepted date formats: YYYY-MM-DD  |  MM/DD/YYYY  |  MM/DD/YY\n")

    raw = prompt_with_navigation("  Pull data starting from: ")
    if not raw:
        return
    start_str = _parse_date(raw)
    if not start_str:
        print(f"\n  Could not parse '{raw}'. Try: 2023-01-01 or 01/01/2023")
        _continue()
        return

    start_dt = datetime.strptime(start_str, "%Y-%m-%d").date()
    yesterday = date.today() - timedelta(days=1)
    days = (
All rows scored by the Repobility analyzer (https://repobility.com)
import_export function · python · L704-L737 (34 LOC)
garmin_extract/menu.py
def import_export() -> None:
    header("Import from Garmin Bulk Export")
    print("  The Garmin bulk export is the fastest way to load years of")
    print("  historical data. Request it at:")
    print("  Garmin Connect → Profile → Account → Your Garmin Data")
    print("  (The .zip file arrives within 24–48 hours.)\n")

    zip_path = prompt_with_navigation("  Path to export .zip: ").strip('"').strip("'")
    if not zip_path:
        print("  Cancelled.")
        _continue()
        return

    if not Path(zip_path).exists():
        print(f"\n  File not found: {zip_path}")
        _continue()
        return

    reskip = prompt_with_navigation("  Overwrite dates that already have data? [y/N]: ")
    cmd = [PYTHON, str(ROOT / "pullers" / "garmin_import_export.py"), zip_path]
    if reskip.lower() == "y":
        cmd.append("--no-skip")

    print()
    subprocess.run(cmd)

    print()
    print("  Building CSV reports...")
    subprocess.run([PYTHON, str(ROOT / "reports" / "build_g
build_csvs function · python · L745-L758 (14 LOC)
garmin_extract/menu.py
def build_csvs() -> None:
    header("Build CSV Reports")
    print("  Flattens all JSON data files into:")
    print("  · reports/garmin_daily.csv       — one row per day")
    print("  · reports/garmin_activities.csv  — one row per workout\n")

    since = prompt_with_navigation("  Include data from [Enter for all time, or YYYY-MM-DD]: ")
    cmd = [PYTHON, str(ROOT / "reports" / "build_garmin_csvs.py")]
    if since:
        cmd += ["--since", since]

    print()
    run(cmd)
    _continue()
_first_run_notice function · python · L769-L776 (8 LOC)
garmin_extract/menu.py
def _first_run_notice() -> None:
    if not ENV.exists():
        header("garmin-extract")
        print("  Looks like your first run — .env not found.")
        print()
        print("  Start with option 1 (Initial Setup) to get everything")
        print("  installed and configured before pulling data.")
        _continue("\n  Press Enter to open the menu...")
_submenu function · python · L779-L806 (28 LOC)
garmin_extract/menu.py
def _submenu(title: str, options: list[MenuOption]) -> None:
    """Generic sub-menu loop. BackSignal from prompt exits; others propagate up."""
    keys: dict[str, Callable[[], None]] = {k: fn for k, label, fn in options if fn is not None}
    while True:
        header(title)
        for key, label, fn in options:
            if fn is None:
                print(f"  {label}")
            else:
                print(f"    {key}  {label}")
        print()
        hr()
        print("    b  Back    x  Main menu    q  Quit")
        hr()
        try:
            choice = prompt_with_navigation("\n  Choice: ").lower()
        except BackSignal:
            return
        # ExitToMainSignal and QuitSignal propagate naturally

        if choice in keys:
            try:
                keys[choice]()
            except BackSignal:
                pass  # stay in this submenu; action was aborted
            # ExitToMainSignal and QuitSignal propagate
        else:
            print("  Unreco
menu_initial_setup function · python · L809-L816 (8 LOC)
garmin_extract/menu.py
def menu_initial_setup() -> None:
    _submenu(
        "Initial Setup",
        [
            ("1", "Setup wizard  (prerequisites + credentials)", check_prerequisites),
            ("2", "Update Garmin credentials", configure_credentials),
        ],
    )
menu_pull_data function · python · L819-L835 (17 LOC)
garmin_extract/menu.py
def menu_pull_data() -> None:
    _submenu(
        "Pull Data",
        [
            ("", "─── Recent ──────────────────────────────────────────", None),
            ("1", "Yesterday", _pull_yesterday),
            ("2", "Last 7 days", lambda: _pull_last_n(7, "last 7 days")),
            ("3", "Last 30 days", lambda: _pull_last_n(30, "last 30 days")),
            ("", "─── Custom ──────────────────────────────────────────", None),
            ("4", "Specific date or date range", _pull_custom),
            ("5", "Full history  (from a date you choose to today)", _pull_everything),
            ("", "─── Historical import ───────────────────────────────", None),
            ("6", "Import from Garmin bulk export (.zip)", import_export),
            ("", "─── Reports ─────────────────────────────────────────", None),
            ("7", "Rebuild CSV reports  (from existing pulled data)", build_csvs),
        ],
    )
menu_automation function · python · L838-L845 (8 LOC)
garmin_extract/menu.py
def menu_automation() -> None:
    _submenu(
        "Configure Automation",
        [
            ("", "─── Unattended MFA ─────────────────────────────────", None),
            ("1", "Set up Gmail MFA  (auto-handle Garmin security codes)", setup_gmail_mfa),
        ],
    )
main function · python · L848-L885 (38 LOC)
garmin_extract/menu.py
def main(dry_run: bool = False, verbose: int = 0) -> None:
    # TODO Phase 2: thread dry_run through action functions
    _first_run_notice()

    actions: dict[str, Callable[[], None]] = {
        "1": menu_initial_setup,
        "2": menu_pull_data,
        "3": menu_automation,
    }

    while True:
        header("garmin-extract")
        print("    1  Initial Setup")
        print("    2  Pull Data")
        print("    3  Configure Automation")
        print()
        hr()
        print("    q  Quit")
        hr()

        try:
            choice = prompt_with_navigation("\n  Choice: ").lower()
        except (BackSignal, ExitToMainSignal):
            continue  # no-op at main menu level
        except QuitSignal:
            break

        if choice in actions:
            try:
                actions[choice]()
            except ExitToMainSignal:
                pass  # return to main menu loop
            except QuitSignal:
                break
        else:
            pri
If a scraper extracted this row, it came from Repobility (https://repobility.com)
app_root function · python · L20-L29 (10 LOC)
garmin_extract/_paths.py
def app_root() -> Path:
    """Return the directory where user-facing files (config, data, scripts) live.

    - Dev install: project root (containing pyproject.toml)
    - Frozen (PyInstaller onedir): the directory containing the .exe
    """
    if getattr(sys, "frozen", False):
        return Path(sys.executable).parent
    # Dev install: this file is at <root>/garmin_extract/_paths.py
    return Path(__file__).resolve().parent.parent
bundle_root function · python · L32-L43 (12 LOC)
garmin_extract/_paths.py
def bundle_root() -> Path:
    """Return the directory containing bundled data files (pullers/, reports/, scripts/).

    - Dev install: same as app_root() — the project root
    - Frozen: `sys._MEIPASS` (PyInstaller's _internal directory) where data files land
    """
    if getattr(sys, "frozen", False):
        meipass = getattr(sys, "_MEIPASS", None)
        if meipass:
            return Path(meipass)
        return Path(sys.executable).parent / "_internal"
    return Path(__file__).resolve().parent.parent
_build_menu function · python · L28-L43 (16 LOC)
garmin_extract/screens/automation.py
def _build_menu(cursor: int = 0) -> str:
    top = "  ┌" + "─" * _W + "┐"
    bottom = "  └" + "─" * _W + "┘"
    rows = ["\n" + top, _EMPTY_ROW]
    for i, (key, label, hint) in enumerate(_ITEMS):
        sel = i == cursor
        cur = "❯" if sel else " "
        lbl = f"[bold]{label}[/]" if sel else label
        h = f"[bold]{hint}[/]" if sel else hint
        lbl_pad = " " * (_W - 8 - len(label))
        hint_pad = " " * (_W - 8 - len(hint))
        rows.append(f"  │ {cur} [bold cyan][{key}][/]  {lbl}{lbl_pad}│")
        rows.append(f"  │        [dim]{h}[/]{hint_pad}│")
        rows.append(_EMPTY_ROW)
    rows.append(bottom)
    return "\n".join(rows)
_read_crontab function · python · L49-L59 (11 LOC)
garmin_extract/screens/automation.py
def _read_crontab() -> str:
    """Return current crontab contents, or empty string if none set."""
    try:
        result = subprocess.run(
            ["crontab", "-l"],
            capture_output=True,
            text=True,
        )
        return result.stdout if result.returncode == 0 else ""
    except Exception:
        return ""
_write_crontab function · python · L62-L73 (12 LOC)
garmin_extract/screens/automation.py
def _write_crontab(contents: str) -> bool:
    """Write new crontab contents. Returns True on success."""
    try:
        result = subprocess.run(
            ["crontab", "-"],
            input=contents,
            capture_output=True,
            text=True,
        )
        return result.returncode == 0
    except Exception:
        return False
_find_cron_entry function · python · L76-L81 (6 LOC)
garmin_extract/screens/automation.py
def _find_cron_entry(crontab: str) -> str | None:
    """Return the garmin-extract cron line, or None if not installed."""
    for line in crontab.splitlines():
        if _CRON_MARKER in line:
            return line
    return None
_install_cron function · python · L88-L95 (8 LOC)
garmin_extract/screens/automation.py
def _install_cron(hour: int) -> tuple[bool, str]:
    """Install or replace the garmin-extract cron entry."""
    crontab = _read_crontab()
    lines = [ln for ln in crontab.splitlines() if _CRON_MARKER not in ln]
    lines.append(_build_cron_entry(hour))
    new_contents = "\n".join(lines) + "\n"
    ok = _write_crontab(new_contents)
    return ok, new_contents if ok else "Failed to write crontab"
_remove_cron function · python · L98-L104 (7 LOC)
garmin_extract/screens/automation.py
def _remove_cron() -> tuple[bool, str]:
    """Remove the garmin-extract cron entry."""
    crontab = _read_crontab()
    lines = [ln for ln in crontab.splitlines() if _CRON_MARKER not in ln]
    new_contents = ("\n".join(lines) + "\n") if lines else ""
    ok = _write_crontab(new_contents)
    return ok, "Removed" if ok else "Failed to remove"
Repobility · severity-and-effort ranking · https://repobility.com
_check_gmail_automation function · python · L110-L141 (32 LOC)
garmin_extract/screens/automation.py
def _check_gmail_automation() -> tuple[str, str]:
    """Return (status, detail) where status is 'ok' | 'partial' | 'unconfigured'."""
    has_creds = GMAIL_CREDS_FILE.exists()
    has_token = GMAIL_TOKEN_FILE.exists()

    if not has_creds and not has_token:
        return "unconfigured", "Not set up"
    if not has_creds:
        return "partial", "google_credentials.json missing — re-run Gmail OAuth setup"
    if not has_token:
        return "partial", "Not yet authorized — run Gmail OAuth setup to generate token"

    try:
        import json as _json

        from google.auth.transport.requests import Request
        from google.oauth2.credentials import Credentials

        tok = _json.loads(GMAIL_TOKEN_FILE.read_text())
        creds = Credentials(
            token=tok.get("token"),
            refresh_token=tok.get("refresh_token"),
            token_uri=tok.get("token_uri"),
            client_id=tok.get("client_id"),
            client_secret=tok.get("client_secret"),
     
AutomationScreen class · python · L147-L238 (92 LOC)
garmin_extract/screens/automation.py
class AutomationScreen(Screen[None]):
    """Automation landing — Gmail MFA status, cron schedule, Drive/Sheets."""

    _ITEM_COUNT = 3

    BINDINGS = [
        Binding("1", "go_gmail", show=False),
        Binding("2", "go_cron", show=False),
        Binding("3", "go_sheets", show=False),
        Binding("up", "cursor_up", show=False),
        Binding("k", "cursor_up", show=False),
        Binding("down", "cursor_down", show=False),
        Binding("j", "cursor_down", show=False),
        Binding("enter", "cursor_select", show=False),
        Binding("b", "back", "Back", show=True),
        Binding("q", "quit", "Quit", show=True),
    ]

    CSS = """
    AutomationScreen {
        align: center middle;
    }

    #auto-header {
        width: 57;
        text-align: center;
        color: $accent;
        text-style: bold;
        margin-bottom: 1;
    }

    #auto-options {
        width: 57;
        color: $text;
    }

    #auto-hint {
        width: 57;
        text-align: cent
compose method · python · L191-L199 (9 LOC)
garmin_extract/screens/automation.py
    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
        yield Static("Automation", id="auto-header")
        yield Static(_build_menu(0), id="auto-options")
        yield Static(
            "↑↓  j/k  navigate  ·  enter  select  ·  1–3  direct  ·  b  back",
            id="auto-hint",
        )
        yield Footer()
action_cursor_up method · python · L207-L210 (4 LOC)
garmin_extract/screens/automation.py
    def action_cursor_up(self) -> None:
        if self._cursor > 0:
            self._cursor -= 1
            self._refresh_menu()
action_cursor_down method · python · L212-L215 (4 LOC)
garmin_extract/screens/automation.py
    def action_cursor_down(self) -> None:
        if self._cursor < self._ITEM_COUNT - 1:
            self._cursor += 1
            self._refresh_menu()
action_go_gmail method · python · L220-L222 (3 LOC)
garmin_extract/screens/automation.py
    def action_go_gmail(self) -> None:
        self._cursor = 0
        self.app.push_screen(GmailMfaScreen())
action_go_cron method · python · L224-L226 (3 LOC)
garmin_extract/screens/automation.py
    def action_go_cron(self) -> None:
        self._cursor = 1
        self.app.push_screen(CronScreen())
action_go_sheets method · python · L228-L232 (5 LOC)
garmin_extract/screens/automation.py
    def action_go_sheets(self) -> None:
        self._cursor = 2
        from garmin_extract.screens.drive_sheets import DriveSheetsScreen

        self.app.push_screen(DriveSheetsScreen())
Repobility · code-quality intelligence · https://repobility.com
GmailMfaScreen class · python · L244-L360 (117 LOC)
garmin_extract/screens/automation.py
class GmailMfaScreen(Screen[None]):
    """Gmail MFA automation status screen."""

    BINDINGS = [
        Binding("s", "go_setup", "Go to Setup", show=False),
        Binding("b", "back", "Back", show=True),
        Binding("q", "quit", "Quit", show=True),
    ]

    CSS = """
    GmailMfaScreen {
        layout: vertical;
        padding: 2 4;
    }

    #gmail-mfa-header {
        text-style: bold;
        color: $accent;
        margin-bottom: 2;
    }

    #gmail-mfa-creds {
        height: auto;
        margin-bottom: 1;
    }

    #gmail-mfa-token {
        height: auto;
        margin-bottom: 1;
    }

    #gmail-mfa-status {
        height: auto;
        margin-bottom: 2;
    }

    #gmail-mfa-hint {
        color: $text-muted;
        height: auto;
    }
    """

    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
        yield Static("Gmail MFA Automation", id="gmail-mfa-header")
        yield Static("Checking…", id="gmail-mfa-creds")
        yield
compose method · python · L286-L293 (8 LOC)
garmin_extract/screens/automation.py
    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
        yield Static("Gmail MFA Automation", id="gmail-mfa-header")
        yield Static("Checking…", id="gmail-mfa-creds")
        yield Static("", id="gmail-mfa-token")
        yield Static("", id="gmail-mfa-status")
        yield Static("", id="gmail-mfa-hint")
        yield Footer()
_check method · python · L298-L330 (33 LOC)
garmin_extract/screens/automation.py
    def _check(self) -> None:
        has_creds = GMAIL_CREDS_FILE.exists()
        has_token = GMAIL_TOKEN_FILE.exists()
        status, detail = _check_gmail_automation()

        creds_text = (
            "[green]✓[/] google_credentials.json found"
            if has_creds
            else "[red]✗[/] google_credentials.json not found"
        )
        token_text = (
            "[green]✓[/] Authorization token present"
            if has_token
            else "[yellow]○[/] Not yet authorized"
        )

        if status == "ok":
            status_text = (
                "[green]● Gmail automation is active[/]"
                "  —  MFA codes will be fetched automatically"
            )
            hint_text = ""
            show_setup = False
        else:
            status_text = (
                "[dim]Not configured[/]" if status == "unconfigured" else f"[yellow]⚠[/]  {detail}"
            )
            hint_text = "Press  [bold]s[/]  to go to Initial Setup → Gmail OAuth"
_apply_state method · python · L332-L349 (18 LOC)
garmin_extract/screens/automation.py
    def _apply_state(
        self,
        creds_text: str,
        token_text: str,
        status_text: str,
        hint_text: str,
        show_setup: bool,
    ) -> None:
        self.query_one("#gmail-mfa-creds", Static).update(creds_text)
        self.query_one("#gmail-mfa-token", Static).update(token_text)
        self.query_one("#gmail-mfa-status", Static).update(status_text)
        self.query_one("#gmail-mfa-hint", Static).update(hint_text)
        self.BINDINGS = [
            Binding("s", "go_setup", "Go to Setup", show=show_setup),
            Binding("b", "back", "Back", show=True),
            Binding("q", "quit", "Quit", show=True),
        ]
        self.refresh_bindings()
action_go_setup method · python · L351-L354 (4 LOC)
garmin_extract/screens/automation.py
    def action_go_setup(self) -> None:
        from garmin_extract.screens.setup import SetupScreen

        self.app.push_screen(SetupScreen())
_EditTimeModal class · python · L366-L432 (67 LOC)
garmin_extract/screens/automation.py
class _EditTimeModal(ModalScreen[int | None]):
    """Modal to pick cron hour (0–23)."""

    BINDINGS = [Binding("escape", "dismiss", "Cancel", show=True)]

    CSS = """
    _EditTimeModal {
        align: center middle;
    }

    #edit-time-box {
        width: 52;
        height: auto;
        border: round $accent;
        padding: 1 2;
    }

    #edit-time-title {
        text-style: bold;
        color: $accent;
        margin-bottom: 1;
    }

    #edit-time-hint {
        color: $text-muted;
        margin-bottom: 1;
    }

    #edit-time-error {
        color: $error;
        height: 1;
    }
    """

    def __init__(self, current_hour: int = 6) -> None:
        super().__init__()
        self._current_hour = current_hour

    def compose(self) -> ComposeResult:
        with Static(id="edit-time-box"):
            yield Static("Set Pull Time", id="edit-time-title")
            yield Static(
                f"Enter hour in 24h format (0–23).  Current: {self._current_hour:02
__init__ method · python · L400-L402 (3 LOC)
garmin_extract/screens/automation.py
    def __init__(self, current_hour: int = 6) -> None:
        super().__init__()
        self._current_hour = current_hour
compose method · python · L404-L417 (14 LOC)
garmin_extract/screens/automation.py
    def compose(self) -> ComposeResult:
        with Static(id="edit-time-box"):
            yield Static("Set Pull Time", id="edit-time-title")
            yield Static(
                f"Enter hour in 24h format (0–23).  Current: {self._current_hour:02d}:00",
                id="edit-time-hint",
            )
            yield Input(
                placeholder="6",
                value=str(self._current_hour),
                id="edit-time-input",
                max_length=2,
            )
            yield Static("", id="edit-time-error")
All rows scored by the Repobility analyzer (https://repobility.com)
on_input_submitted method · python · L422-L432 (11 LOC)
garmin_extract/screens/automation.py
    def on_input_submitted(self, event: Input.Submitted) -> None:
        try:
            hour = int(event.value.strip())
            if not 0 <= hour <= 23:
                raise ValueError
        except ValueError:
            self.query_one("#edit-time-error", Static).update(
                "Enter a whole number between 0 and 23."
            )
            return
        self.dismiss(hour)
CronScreen class · python · L438-L576 (139 LOC)
garmin_extract/screens/automation.py
class CronScreen(Screen[None]):
    """Cron schedule management screen."""

    BINDINGS = [
        Binding("i", "install", "Enable / Re-enable", show=True),
        Binding("e", "edit_time", "Change Time", show=True),
        Binding("r", "remove", "Disable", show=True),
        Binding("b", "back", "Back", show=True),
        Binding("q", "quit", "Quit", show=True),
    ]

    CSS = """
    CronScreen {
        layout: vertical;
        padding: 2 4;
    }

    #cron-header {
        text-style: bold;
        color: $accent;
        margin-bottom: 2;
    }

    #cron-status {
        height: auto;
        margin-bottom: 1;
    }

    #cron-entry {
        color: $text-muted;
        height: auto;
        margin-bottom: 2;
    }

    #cron-feedback {
        height: 1;
    }
    """

    def __init__(self) -> None:
        super().__init__()
        self._installed = False
        self._current_hour = 6

    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
  
__init__ method · python · L477-L480 (4 LOC)
garmin_extract/screens/automation.py
    def __init__(self) -> None:
        super().__init__()
        self._installed = False
        self._current_hour = 6
compose method · python · L482-L488 (7 LOC)
garmin_extract/screens/automation.py
    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
        yield Static("Scheduled Pulls", id="cron-header")
        yield Static("Checking schedule…", id="cron-status")
        yield Static("", id="cron-entry")
        yield Static("", id="cron-feedback")
        yield Footer()
_check_cron method · python · L499-L510 (12 LOC)
garmin_extract/screens/automation.py
    def _check_cron(self) -> None:
        crontab = _read_crontab()
        entry = _find_cron_entry(crontab)

        if entry:
            try:
                hour = int(entry.split()[1])
            except (IndexError, ValueError):
                hour = 6
            self.app.call_from_thread(self._apply_state, True, hour, entry.strip())
        else:
            self.app.call_from_thread(self._apply_state, False, 6, "")
_apply_state method · python · L512-L525 (14 LOC)
garmin_extract/screens/automation.py
    def _apply_state(self, installed: bool, hour: int, entry: str) -> None:
        self._installed = installed
        self._current_hour = hour

        if installed:
            self.query_one("#cron-status", Static).update(
                f"[green]● Active[/]  —  pulls data every day at [bold]{hour:02d}:00[/]"
            )
            self.query_one("#cron-entry", Static).update(
                "[dim]Output is logged to  /tmp/garmin-pull.log[/]"
            )
        else:
            self.query_one("#cron-status", Static).update("[dim]Not scheduled[/]")
            self.query_one("#cron-entry", Static).update("[dim]Default pull time: 6:00 AM daily[/]")
action_install method · python · L527-L535 (9 LOC)
garmin_extract/screens/automation.py
    def action_install(self) -> None:
        ok, _ = _install_cron(self._current_hour)
        feedback = (
            f"[green]Scheduled — will pull data every day at {self._current_hour:02d}:00[/]"
            if ok
            else "[red]Failed to save schedule — check system permissions[/]"
        )
        self.query_one("#cron-feedback", Static).update(feedback)
        self._refresh_cron_display()
action_remove method · python · L537-L550 (14 LOC)
garmin_extract/screens/automation.py
    def action_remove(self) -> None:
        if not self._installed:
            self.query_one("#cron-feedback", Static).update(
                "[dim]No schedule is set — nothing to disable.[/]"
            )
            return
        ok, _ = _remove_cron()
        feedback = (
            "[dim]Schedule disabled — automatic pulls are turned off.[/]"
            if ok
            else "[red]Failed to remove schedule — check system permissions[/]"
        )
        self.query_one("#cron-feedback", Static).update(feedback)
        self._refresh_cron_display()
If a scraper extracted this row, it came from Repobility (https://repobility.com)
action_edit_time method · python · L552-L558 (7 LOC)
garmin_extract/screens/automation.py
    def action_edit_time(self) -> None:
        if not self._installed:
            self.query_one("#cron-feedback", Static).update(
                "[dim]Enable a schedule first, then use  e  to change the time.[/]"
            )
            return
        self.app.push_screen(_EditTimeModal(self._current_hour), self._on_hour_selected)
_on_hour_selected method · python · L560-L570 (11 LOC)
garmin_extract/screens/automation.py
    def _on_hour_selected(self, hour: int | None) -> None:
        if hour is None:
            return
        ok, _ = _install_cron(hour)
        feedback = (
            f"[green]Updated — will pull data every day at {hour:02d}:00[/]"
            if ok
            else "[red]Failed to update schedule — check system permissions[/]"
        )
        self.query_one("#cron-feedback", Static).update(feedback)
        self._refresh_cron_display()
_date_range_label function · python · L16-L20 (5 LOC)
garmin_extract/screens/data_pull.py
def _date_range_label(days: int) -> str:
    today = date.today()
    start = today - timedelta(days=days)
    end = today - timedelta(days=1)
    return f"{start.isoformat()}  →  {end.isoformat()}"
‹ prevpage 4 / 8next ›