Function bodies 373 total
_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:
passGarminExtractApp 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 = verboseon_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 FalseRepobility · 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 Truebuild_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="Increasemain 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 credscheck_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.gooupload_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["sprerun 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.staAbout: 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(selfset_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 btnAutomationPage 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 ›