← back to ColonelPanicX__garmin-extract

Function bodies 373 total

All specs Real LLM only Function bodies
_load_app_config function · python · L15-L21 (7 LOC)
garmin_extract/app.py
def _load_app_config() -> dict:
    if _CONFIG_FILE.exists():
        try:
            return json.loads(_CONFIG_FILE.read_text())
        except Exception:
            return {}
    return {}
_save_app_config function · python · L24-L28 (5 LOC)
garmin_extract/app.py
def _save_app_config(cfg: dict) -> None:
    try:
        _CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
    except Exception:
        pass
GarminExtractApp class · python · L31-L54 (24 LOC)
garmin_extract/app.py
class GarminExtractApp(App[None]):
    """garmin-extract TUI — boots to the main menu."""

    TITLE = f"garmin-extract  v{__version__}"
    SUB_TITLE = "Automated Garmin Connect data pipeline"

    def __init__(self, dry_run: bool = False, verbose: int = 0) -> None:
        super().__init__()
        self.dry_run = dry_run
        self.verbose = verbose

    def on_mount(self) -> None:
        cfg = _load_app_config()
        if saved_theme := cfg.get("theme"):
            self.theme = saved_theme
        from garmin_extract.screens.main_menu import MainMenuScreen

        self.push_screen(MainMenuScreen())

    def watch_theme(self, theme: str) -> None:
        """Persist theme selection across restarts."""
        cfg = _load_app_config()
        cfg["theme"] = theme
        _save_app_config(cfg)
__init__ method · python · L37-L40 (4 LOC)
garmin_extract/app.py
    def __init__(self, dry_run: bool = False, verbose: int = 0) -> None:
        super().__init__()
        self.dry_run = dry_run
        self.verbose = verbose
on_mount method · python · L42-L48 (7 LOC)
garmin_extract/app.py
    def on_mount(self) -> None:
        cfg = _load_app_config()
        if saved_theme := cfg.get("theme"):
            self.theme = saved_theme
        from garmin_extract.screens.main_menu import MainMenuScreen

        self.push_screen(MainMenuScreen())
watch_theme method · python · L50-L54 (5 LOC)
garmin_extract/app.py
    def watch_theme(self, theme: str) -> None:
        """Persist theme selection across restarts."""
        cfg = _load_app_config()
        cfg["theme"] = theme
        _save_app_config(cfg)
_is_gui_available function · python · L18-L27 (10 LOC)
garmin_extract/cli.py
def _is_gui_available() -> bool:
    """Return True if running on Windows with PySide6 installed."""
    if sys.platform != "win32":
        return False
    try:
        import PySide6  # noqa: F401

        return True
    except ImportError:
        return False
Repobility · open methodology · https://repobility.com/research/
_is_tui_capable function · python · L30-L42 (13 LOC)
garmin_extract/cli.py
def _is_tui_capable() -> bool:
    """Return True if the current environment can support a Textual TUI."""
    # Explicit opt-outs
    if os.environ.get("CI") or os.environ.get("GARMIN_NO_TUI"):
        return False
    # Dumb / missing terminal
    term = os.environ.get("TERM", "")
    if term in ("dumb", ""):
        return False
    # No TTY (e.g. piped output, cron)
    if not sys.stdout.isatty():
        return False
    return True
build_parser function · python · L45-L82 (38 LOC)
garmin_extract/cli.py
def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="garmin-extract",
        description="Automated Garmin Connect data pipeline.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Run without flags to launch the interactive interface.\n"
            "Use --no-tui for headless, CI, or cron environments."
        ),
    )
    parser.add_argument(
        "--no-tui",
        action="store_true",
        help="Use the print menu instead of the TUI",
    )
    parser.add_argument(
        "--no-gui",
        action="store_true",
        help="Skip the PySide6 GUI on Windows — use the TUI or print menu",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Preview actions without making changes (print menu only — Phase 2 for TUI)",
    )
    parser.add_argument(
        "--verbose",
        "-v",
        action="count",
        default=0,
        help="Increase
main function · python · L85-L103 (19 LOC)
garmin_extract/cli.py
def main() -> None:
    args = build_parser().parse_args()

    if args.no_tui:
        from garmin_extract.menu import main as run_menu

        run_menu(dry_run=args.dry_run, verbose=args.verbose)
    elif not args.no_gui and _is_gui_available():
        from garmin_extract.gui.app import run

        run(dry_run=args.dry_run, verbose=args.verbose)
    elif _is_tui_capable():
        from garmin_extract.app import GarminExtractApp

        GarminExtractApp(dry_run=args.dry_run, verbose=args.verbose).run()
    else:
        from garmin_extract.menu import main as run_menu

        run_menu(dry_run=args.dry_run, verbose=args.verbose)
detect_keyring function · python · L15-L44 (30 LOC)
garmin_extract/_credentials.py
def detect_keyring() -> tuple[bool, str]:
    """
    Returns (available, detail).
    Performs a write/read/delete smoke-test to confirm the backend actually works.
    """
    try:
        import keyring  # noqa: PLC0415

        backend = keyring.get_keyring()
        name = type(backend).__name__

        # Backends that can never store a secret
        if any(x in name for x in ("Fail", "Null", "PlainText")):
            return False, f"No secure backend ({name})"

        # Smoke-test: write, read, delete
        keyring.set_password(_SERVICE, _PROBE_KEY, "1")
        val = keyring.get_password(_SERVICE, _PROBE_KEY)
        try:
            keyring.delete_password(_SERVICE, _PROBE_KEY)
        except Exception:
            pass
        if val == "1":
            return True, name
        return False, "Keyring probe failed"

    except ImportError:
        return False, "keyring package not installed"
    except Exception as exc:
        return False, str(exc)
load_credentials function · python · L47-L62 (16 LOC)
garmin_extract/_credentials.py
def load_credentials() -> tuple[str, str]:
    """
    Returns (email, password).  Either may be empty string.
    Priority: keyring → .env → ('', '').
    """
    try:
        import keyring  # noqa: PLC0415

        email = keyring.get_password(_SERVICE, _EMAIL_KEY) or ""
        password = keyring.get_password(_SERVICE, _PASSWORD_KEY) or ""
        if email or password:
            return email, password
    except Exception:
        pass

    return _load_from_env()
save_to_keyring function · python · L65-L75 (11 LOC)
garmin_extract/_credentials.py
def save_to_keyring(email: str, password: str) -> tuple[bool, str]:
    """Save credentials to OS keyring.  Returns (ok, detail)."""
    try:
        import keyring  # noqa: PLC0415

        keyring.set_password(_SERVICE, _EMAIL_KEY, email)
        keyring.set_password(_SERVICE, _PASSWORD_KEY, password)
        _scrub_env()
        return True, "Saved to keyring"
    except Exception as exc:
        return False, str(exc)
save_to_env function · python · L78-L85 (8 LOC)
garmin_extract/_credentials.py
def save_to_env(email: str, password: str) -> None:
    """Write credentials to .env (plaintext)."""
    lines = [
        "# Garmin Connect credentials",
        f"GARMIN_EMAIL={email}",
        f"GARMIN_PASSWORD={password}",
    ]
    ENV_FILE.write_text("\n".join(lines) + "\n")
check_credentials function · python · L88-L114 (27 LOC)
garmin_extract/_credentials.py
def check_credentials() -> tuple[bool, str]:
    """
    Returns (ok, detail) for status display in SetupScreen.
    Shows where creds came from, or surfaces keyring errors if lookup fails.
    """
    keyring_error: str | None = None
    try:
        import keyring  # noqa: PLC0415

        email = keyring.get_password(_SERVICE, _EMAIL_KEY) or ""
        password = keyring.get_password(_SERVICE, _PASSWORD_KEY) or ""
        if email and password:
            return True, f"{email}  [dim](keyring)[/]"
        if email:
            return False, f"{email}  [dim](no password in keyring)[/]"
    except Exception as exc:
        keyring_error = str(exc)

    email, password = _load_from_env()
    if email and password:
        return True, f"{email}  [dim](.env)[/]"
    if email:
        return False, f"{email}  [dim](no password in .env)[/]"

    if keyring_error:
        return False, f"Keyring error: {keyring_error}"
    return False, "Not configured"
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
_scrub_env function · python · L117-L131 (15 LOC)
garmin_extract/_credentials.py
def _scrub_env() -> None:
    """Remove Garmin credentials from .env after a successful keyring save."""
    if not ENV_FILE.exists():
        return
    kept = []
    for line in ENV_FILE.read_text().splitlines():
        stripped = line.strip()
        if stripped.startswith(("GARMIN_EMAIL=", "GARMIN_PASSWORD=")):
            continue
        kept.append(line)
    # If nothing meaningful remains, delete the file entirely
    if all(not ln.strip() or ln.strip().startswith("#") for ln in kept):
        ENV_FILE.unlink()
    else:
        ENV_FILE.write_text("\n".join(kept) + "\n")
_load_from_env function · python · L134-L148 (15 LOC)
garmin_extract/_credentials.py
def _load_from_env() -> tuple[str, str]:
    email = ""
    password = ""
    if ENV_FILE.exists():
        for line in ENV_FILE.read_text().splitlines():
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            k, _, v = line.partition("=")
            k, v = k.strip(), v.strip()
            if k == "GARMIN_EMAIL":
                email = v
            elif k == "GARMIN_PASSWORD":
                password = v
    return email, password
_load_credentials function · python · L41-L64 (24 LOC)
garmin_extract/_google_drive.py
def _load_credentials():
    """Return refreshed google.oauth2.credentials.Credentials, or raise."""
    from google.auth.transport.requests import Request
    from google.oauth2.credentials import Credentials

    if not TOKEN_FILE.exists():
        raise FileNotFoundError("No OAuth token found — run Gmail OAuth setup first.")

    tok = json.loads(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"),
        scopes=tok.get("scopes"),
    )

    if creds.expired and creds.refresh_token:
        creds.refresh(Request())
        tok["token"] = creds.token
        TOKEN_FILE.write_text(json.dumps(tok, indent=2))

    return creds
check_auth function · python · L67-L86 (20 LOC)
garmin_extract/_google_drive.py
def check_auth() -> tuple[str, str]:
    """Return (status, detail) — status is 'ok' | 'missing_scopes' | 'no_token' | 'error'."""
    if not TOKEN_FILE.exists():
        return "no_token", "No OAuth token — run Gmail OAuth setup first."

    try:
        tok = json.loads(TOKEN_FILE.read_text())
        token_scopes = set(tok.get("scopes") or [])
        missing = REQUIRED_SCOPES - token_scopes
        if missing:
            return (
                "missing_scopes",
                "Token lacks Drive/Sheets scopes — re-run Gmail OAuth setup to upgrade.",
            )
        creds = _load_credentials()
        if not creds.valid and not creds.refresh_token:
            return "error", "Token is expired and cannot be refreshed."
        return "ok", "Drive and Sheets access authorized."
    except Exception as exc:
        return "error", str(exc)
load_config function · python · L94-L101 (8 LOC)
garmin_extract/_google_drive.py
def load_config() -> dict[str, Any]:
    """Load .drive_config.json, returning {} if not found."""
    if not CONFIG_FILE.exists():
        return {}
    try:
        return json.loads(CONFIG_FILE.read_text())
    except Exception:
        return {}
save_config function · python · L104-L106 (3 LOC)
garmin_extract/_google_drive.py
def save_config(cfg: dict[str, Any]) -> None:
    """Write config dict to .drive_config.json."""
    CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
_drive_service function · python · L114-L117 (4 LOC)
garmin_extract/_google_drive.py
def _drive_service(creds):
    from googleapiclient.discovery import build

    return build("drive", "v3", credentials=creds, cache_discovery=False)
_sheets_service function · python · L120-L123 (4 LOC)
garmin_extract/_google_drive.py
def _sheets_service(creds):
    from googleapiclient.discovery import build

    return build("sheets", "v4", credentials=creds, cache_discovery=False)
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
get_or_create_folder function · python · L126-L140 (15 LOC)
garmin_extract/_google_drive.py
def get_or_create_folder(creds, name: str = "Garmin Extract") -> str:
    """Return the Drive folder ID for *name*, creating it if absent."""
    svc = _drive_service(creds)

    # Search for existing folder by name
    q = f"name='{name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
    results = svc.files().list(q=q, fields="files(id, name)", pageSize=1).execute()
    files = results.get("files", [])
    if files:
        return files[0]["id"]

    # Create it
    meta = {"name": name, "mimeType": "application/vnd.google-apps.folder"}
    folder = svc.files().create(body=meta, fields="id").execute()
    return folder["id"]
upload_csv function · python · L143-L171 (29 LOC)
garmin_extract/_google_drive.py
def upload_csv(creds, csv_path: Path, folder_id: str) -> tuple[str, str]:
    """
    Upload *csv_path* to *folder_id*, updating in-place if it already exists.
    Returns (file_id, web_link).
    """
    from googleapiclient.http import MediaFileUpload

    svc = _drive_service(creds)
    name = csv_path.name
    mime = "text/csv"

    # Check if file already exists in folder
    q = f"name='{name}' and '{folder_id}' in parents and trashed=false"
    results = svc.files().list(q=q, fields="files(id)", pageSize=1).execute()
    existing = results.get("files", [])

    media = MediaFileUpload(str(csv_path), mimetype=mime, resumable=False)

    if existing:
        file_id = existing[0]["id"]
        svc.files().update(fileId=file_id, media_body=media).execute()
    else:
        meta = {"name": name, "parents": [folder_id]}
        f = svc.files().create(body=meta, media_body=media, fields="id").execute()
        file_id = f["id"]

    # Make readable link
    link = f"https://drive.goo
upload_csvs_to_drive function · python · L174-L202 (29 LOC)
garmin_extract/_google_drive.py
def upload_csvs_to_drive() -> dict[str, Any]:
    """
    Upload garmin_daily.csv and garmin_activities.csv to Drive.
    Returns a result dict with keys: ok, folder_link, files, error.
    """
    missing = [p.name for p in (DAILY_CSV, ACTIVITIES_CSV) if not p.exists()]
    if missing:
        joined = ", ".join(missing)
        return {"ok": False, "error": f"CSVs not found: {joined}. Run 'Pull Data' first."}

    try:
        creds = _load_credentials()
        cfg = load_config()

        folder_id = cfg.get("folder_id") or get_or_create_folder(creds)
        cfg["folder_id"] = folder_id

        files = []
        for csv_path in (DAILY_CSV, ACTIVITIES_CSV):
            file_id, link = upload_csv(creds, csv_path, folder_id)
            files.append({"name": csv_path.name, "id": file_id, "link": link})

        cfg["last_export"] = datetime.now(timezone.utc).isoformat()
        save_config(cfg)

        folder_link = f"https://drive.google.com/drive/folders/{folder_id}"
        ret
_csv_to_values function · python · L210-L213 (4 LOC)
garmin_extract/_google_drive.py
def _csv_to_values(csv_path: Path) -> list[list[str]]:
    """Read a CSV file and return it as a list-of-lists for the Sheets API."""
    with open(csv_path, newline="") as f:
        return [row for row in csv.reader(f)]
_ensure_sheet_tab function · python · L216-L222 (7 LOC)
garmin_extract/_google_drive.py
def _ensure_sheet_tab(sheets_svc, spreadsheet_id: str, tab_name: str) -> None:
    """Add a tab named *tab_name* if it doesn't already exist."""
    meta = sheets_svc.spreadsheets().get(spreadsheetId=spreadsheet_id).execute()
    existing = {s["properties"]["title"] for s in meta.get("sheets", [])}
    if tab_name not in existing:
        body = {"requests": [{"addSheet": {"properties": {"title": tab_name}}}]}
        sheets_svc.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body=body).execute()
sync_to_sheets function · python · L225-L283 (59 LOC)
garmin_extract/_google_drive.py
def sync_to_sheets() -> dict[str, Any]:
    """
    Create or update a Google Sheet with Daily and Activities tabs.
    Returns a result dict with keys: ok, sheet_url, error.
    """
    missing = [p.name for p in (DAILY_CSV, ACTIVITIES_CSV) if not p.exists()]
    if missing:
        joined = ", ".join(missing)
        return {"ok": False, "error": f"CSVs not found: {joined}. Run 'Pull Data' first."}

    try:
        creds = _load_credentials()
        svc = _sheets_service(creds)
        cfg = load_config()

        sheet_id = cfg.get("sheet_id")

        if not sheet_id:
            # Create new spreadsheet
            body = {
                "properties": {"title": "Garmin Data"},
                "sheets": [
                    {"properties": {"title": "Daily"}},
                    {"properties": {"title": "Activities"}},
                ],
            }
            result = svc.spreadsheets().create(body=body, fields="spreadsheetId").execute()
            sheet_id = result["spre
run function · python · L14-L24 (11 LOC)
garmin_extract/gui/app.py
def run(dry_run: bool = False, verbose: int = 0) -> None:
    """Launch the PySide6 GUI."""
    app = QApplication(sys.argv)
    app.setApplicationName("garmin-extract")
    app.setApplicationVersion(__version__)
    app.setStyleSheet(DARK_STYLESHEET)

    window = MainWindow()
    window.show()

    sys.exit(app.exec())
MainWindow class · python · L19-L50 (32 LOC)
garmin_extract/gui/main_window.py
class MainWindow(QMainWindow):
    """Top-level window with a left sidebar and swappable content panels."""

    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle(f"garmin-extract  v{__version__}")
        self.setMinimumSize(800, 600)
        self.resize(1000, 700)

        central = QWidget()
        self.setCentralWidget(central)
        layout = QHBoxLayout(central)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        # ── Sidebar ───────────────────────────────────
        self.sidebar = QListWidget()
        self.sidebar.setObjectName("sidebar")
        self.sidebar.setFixedWidth(200)
        for label in ("Initial Setup", "Pull Data", "Automation"):
            self.sidebar.addItem(label)
        self.sidebar.setCurrentRow(0)
        layout.addWidget(self.sidebar)

        # ── Content area ──────────────────────────────
        self.stack = QStackedWidget()
        self.stack.addWidget(SetupPage())
        self.sta
About: code-quality intelligence by Repobility · https://repobility.com
__init__ method · python · L22-L50 (29 LOC)
garmin_extract/gui/main_window.py
    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle(f"garmin-extract  v{__version__}")
        self.setMinimumSize(800, 600)
        self.resize(1000, 700)

        central = QWidget()
        self.setCentralWidget(central)
        layout = QHBoxLayout(central)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        # ── Sidebar ───────────────────────────────────
        self.sidebar = QListWidget()
        self.sidebar.setObjectName("sidebar")
        self.sidebar.setFixedWidth(200)
        for label in ("Initial Setup", "Pull Data", "Automation"):
            self.sidebar.addItem(label)
        self.sidebar.setCurrentRow(0)
        layout.addWidget(self.sidebar)

        # ── Content area ──────────────────────────────
        self.stack = QStackedWidget()
        self.stack.addWidget(SetupPage())
        self.stack.addWidget(PullDataPage())
        self.stack.addWidget(AutomationPage())
        layout.addWidget(self.sta
_check_gmail_automation function · python · L28-L57 (30 LOC)
garmin_extract/gui/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"
    if not has_token:
        return "partial", "Not yet authorized"

    try:
        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"),
            scopes=tok.get("scopes"),
        )
        if creds.expired and creds.refresh_token:
       
_check_drive_auth function · python · L60-L67 (8 LOC)
garmin_extract/gui/screens/automation.py
def _check_drive_auth() -> tuple[str, str]:
    """Return (status, detail) for Drive/Sheets auth."""
    try:
        from garmin_extract._google_drive import check_auth

        return check_auth()
    except Exception as exc:
        return "error", str(exc)
_load_drive_config function · python · L70-L74 (5 LOC)
garmin_extract/gui/screens/automation.py
def _load_drive_config() -> dict:
    try:
        return json.loads(DRIVE_CONFIG_FILE.read_text()) if DRIVE_CONFIG_FILE.exists() else {}
    except Exception:
        return {}
_AutoSignals class · python · L80-L83 (4 LOC)
garmin_extract/gui/screens/automation.py
class _AutoSignals(QObject):
    gmail_done = Signal(str, str, str)  # status, detail, icon_color
    drive_auth_done = Signal(str, str)  # auth_text, last_export_text
    drive_op_done = Signal(str)  # result text
_SectionCard class · python · L89-L150 (62 LOC)
garmin_extract/gui/screens/automation.py
class _SectionCard(QFrame):
    """A card for displaying automation section status."""

    def __init__(self, title: str, subtitle: str, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self.setStyleSheet("""
            QFrame {
                background-color: #313244;
                border: 1px solid #45475a;
                border-radius: 8px;
                padding: 16px;
            }
            """)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(16, 12, 16, 12)
        layout.setSpacing(6)

        self._title = QLabel(title)
        self._title.setStyleSheet("font-size: 16px; font-weight: bold; background: transparent;")
        layout.addWidget(self._title)

        self._subtitle = QLabel(subtitle)
        self._subtitle.setStyleSheet("font-size: 13px; color: #6c7086; background: transparent;")
        layout.addWidget(self._subtitle)

        self._status = QLabel("Checking...")
        self._status.setStyleSheet("f
__init__ method · python · L92-L121 (30 LOC)
garmin_extract/gui/screens/automation.py
    def __init__(self, title: str, subtitle: str, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self.setStyleSheet("""
            QFrame {
                background-color: #313244;
                border: 1px solid #45475a;
                border-radius: 8px;
                padding: 16px;
            }
            """)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(16, 12, 16, 12)
        layout.setSpacing(6)

        self._title = QLabel(title)
        self._title.setStyleSheet("font-size: 16px; font-weight: bold; background: transparent;")
        layout.addWidget(self._title)

        self._subtitle = QLabel(subtitle)
        self._subtitle.setStyleSheet("font-size: 13px; color: #6c7086; background: transparent;")
        layout.addWidget(self._subtitle)

        self._status = QLabel("Checking...")
        self._status.setStyleSheet("font-size: 13px; color: #6c7086; background: transparent;")
        layout.addWidget(self
set_status method · python · L123-L125 (3 LOC)
garmin_extract/gui/screens/automation.py
    def set_status(self, text: str, color: str = "#6c7086") -> None:
        self._status.setText(text)
        self._status.setStyleSheet(f"font-size: 13px; color: {color}; background: transparent;")
Repobility · open methodology · https://repobility.com/research/
add_action_button method · python · L127-L150 (24 LOC)
garmin_extract/gui/screens/automation.py
    def add_action_button(self, label: str, callback: object) -> QPushButton:
        btn = QPushButton(label)
        btn.setCursor(Qt.CursorShape.PointingHandCursor)
        btn.setStyleSheet("""
            QPushButton {
                padding: 6px 14px;
                font-size: 13px;
                background-color: #45475a;
                border: 1px solid #585b70;
                border-radius: 4px;
                color: #cdd6f4;
            }
            QPushButton:hover {
                background-color: #585b70;
                border-color: #89b4fa;
            }
            QPushButton:pressed {
                background-color: #89b4fa;
                color: #1e1e2e;
            }
            """)
        btn.clicked.connect(callback)
        self._actions_layout.addWidget(btn)
        return btn
AutomationPage class · python · L156-L340 (185 LOC)
garmin_extract/gui/screens/automation.py
class AutomationPage(QWidget):
    """The Automation page shown in the main window's stacked widget."""

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(40, 32, 40, 32)
        layout.setSpacing(8)

        heading = QLabel("Automation")
        heading.setObjectName("heading")
        layout.addWidget(heading)

        sub = QLabel("Gmail MFA, scheduled pulls, Drive / Sheets")
        sub.setObjectName("subheading")
        layout.addWidget(sub)

        layout.addSpacing(16)

        # ── Gmail MFA card ────────────────────────────
        self._gmail_card = _SectionCard("Gmail MFA", "Automatic MFA code retrieval from Gmail")
        layout.addWidget(self._gmail_card)

        layout.addSpacing(4)

        # ── Scheduled Pulls card ──────────────────────
        self._sched_card = _SectionCard(
            "Scheduled Pulls",
            "Windows Task Scheduler — comin
__init__ method · python · L159-L216 (58 LOC)
garmin_extract/gui/screens/automation.py
    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(40, 32, 40, 32)
        layout.setSpacing(8)

        heading = QLabel("Automation")
        heading.setObjectName("heading")
        layout.addWidget(heading)

        sub = QLabel("Gmail MFA, scheduled pulls, Drive / Sheets")
        sub.setObjectName("subheading")
        layout.addWidget(sub)

        layout.addSpacing(16)

        # ── Gmail MFA card ────────────────────────────
        self._gmail_card = _SectionCard("Gmail MFA", "Automatic MFA code retrieval from Gmail")
        layout.addWidget(self._gmail_card)

        layout.addSpacing(4)

        # ── Scheduled Pulls card ──────────────────────
        self._sched_card = _SectionCard(
            "Scheduled Pulls",
            "Windows Task Scheduler — coming in issue #31",
        )
        self._sched_card.set_status("Not yet implemented", "#6c7086")
        
refresh_status method · python · L218-L220 (3 LOC)
garmin_extract/gui/screens/automation.py
    def refresh_status(self) -> None:
        Thread(target=self._check_gmail, daemon=True).start()
        Thread(target=self._check_drive, daemon=True).start()
_check_gmail method · python · L222-L229 (8 LOC)
garmin_extract/gui/screens/automation.py
    def _check_gmail(self) -> None:
        status, detail = _check_gmail_automation()
        if status == "ok":
            self._signals.gmail_done.emit(status, detail, "#a6e3a1")
        elif status == "partial":
            self._signals.gmail_done.emit(status, detail, "#f9e2af")
        else:
            self._signals.gmail_done.emit(status, detail, "#6c7086")
_on_gmail_done method · python · L231-L237 (7 LOC)
garmin_extract/gui/screens/automation.py
    def _on_gmail_done(self, status: str, detail: str, color: str) -> None:
        if status == "ok":
            self._gmail_card.set_status("\u2713 " + detail, color)
        elif status == "partial":
            self._gmail_card.set_status("\u26a0 " + detail, color)
        else:
            self._gmail_card.set_status(detail, color)
_check_drive method · python · L239-L260 (22 LOC)
garmin_extract/gui/screens/automation.py
    def _check_drive(self) -> None:
        status, detail = _check_drive_auth()
        cfg = _load_drive_config()
        last = cfg.get("last_export", "")
        last_text = ""
        if last:
            try:
                from datetime import datetime

                dt = datetime.fromisoformat(last).astimezone(None)
                last_text = f"Last export: {dt.strftime('%Y-%m-%d %H:%M')}"
            except Exception:
                last_text = f"Last export: {last[:19]}"

        if status == "ok":
            auth_text = "\u2713 Drive and Sheets authorized"
        elif status == "missing_scopes":
            auth_text = f"\u26a0 {detail}"
        else:
            auth_text = detail

        self._signals.drive_auth_done.emit(auth_text, last_text)
_on_drive_auth_done method · python · L262-L266 (5 LOC)
garmin_extract/gui/screens/automation.py
    def _on_drive_auth_done(self, auth_text: str, last_text: str) -> None:
        combined = auth_text
        if last_text:
            combined += f"  |  {last_text}"
        self._drive_card.set_status(combined)
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
_set_buttons_enabled method · python · L270-L273 (4 LOC)
garmin_extract/gui/screens/automation.py
    def _set_buttons_enabled(self, enabled: bool) -> None:
        self._drive_upload_btn.setEnabled(enabled)
        self._drive_sheets_btn.setEnabled(enabled)
        self._drive_both_btn.setEnabled(enabled)
_do_drive method · python · L275-L278 (4 LOC)
garmin_extract/gui/screens/automation.py
    def _do_drive(self) -> None:
        self._set_buttons_enabled(False)
        self._feedback.setText("Uploading CSVs to Drive...")
        Thread(target=self._run_drive, daemon=True).start()
_do_sheets method · python · L280-L283 (4 LOC)
garmin_extract/gui/screens/automation.py
    def _do_sheets(self) -> None:
        self._set_buttons_enabled(False)
        self._feedback.setText("Syncing to Google Sheets...")
        Thread(target=self._run_sheets, daemon=True).start()
page 1 / 8next ›