diff --git a/bincio/serve/routers/activities.py b/bincio/serve/routers/activities.py index e4835f3..77f3879 100644 --- a/bincio/serve/routers/activities.py +++ b/bincio/serve/routers/activities.py @@ -192,7 +192,7 @@ async def delete_activity( idx = json.loads(index_path.read_text(encoding="utf-8")) idx["activities"] = [a for a in idx.get("activities", []) if a.get("id") != activity_id] index_path.write_text(json.dumps(idx, indent=2, ensure_ascii=False)) - except Exception: + except (OSError, json.JSONDecodeError): pass # corrupt index — merge_all will clean up on next run # Remove from dedup cache so the file can be re-uploaded if needed @@ -205,7 +205,7 @@ async def delete_activity( a for a in cache["activities"] if a.get("id") != activity_id ] cache_path.write_text(json.dumps(cache, indent=2, ensure_ascii=False)) - except Exception: + except (OSError, json.JSONDecodeError): pass # corrupt cache — leave it; next extract will rebuild # Full merge needed: activity removed from index @@ -289,13 +289,13 @@ async def get_athlete(bincio_session: str | None = Cookie(default=None)) -> JSON # Layer edits/athlete.yaml on top edits_path = dd / "edits" / "athlete.yaml" if edits_path.exists(): + import yaml try: - import yaml edits = yaml.safe_load(edits_path.read_text(encoding="utf-8")) or {} for k in ("max_hr", "ftp_w", "hr_zones", "power_zones", "seasons", "gear"): if k in edits: data[k] = edits[k] - except Exception: + except (OSError, yaml.YAMLError): pass return JSONResponse(data) diff --git a/bincio/serve/routers/admin.py b/bincio/serve/routers/admin.py index c9b10ec..25687bc 100644 --- a/bincio/serve/routers/admin.py +++ b/bincio/serve/routers/admin.py @@ -331,7 +331,7 @@ async def admin_reextract_originals( total_imported += evt.get("imported", 0) total_skipped += evt.get("skipped", 0) total_errors += evt.get("errors", 0) - except Exception: + except json.JSONDecodeError: pass await proc.wait() @@ -390,7 +390,7 @@ async def admin_diag( try: idx = json.loads(merged_index.read_text()) merged_activity_count = len(idx.get("activities", [])) - except Exception: + except (OSError, json.JSONDecodeError): merged_activity_count = -1 root_activity_count: int | None = None @@ -398,7 +398,7 @@ async def admin_diag( try: idx = json.loads(root_index.read_text()) root_activity_count = len(idx.get("activities", [])) - except Exception: + except (OSError, json.JSONDecodeError): root_activity_count = -1 # Peek at a few filenames in activities/ to understand the actual state @@ -497,7 +497,7 @@ async def admin_delete_user_directory( from bincio.render.cli import _write_root_manifest try: _write_root_manifest(deps._get_data_dir()) - except Exception: + except (OSError, json.JSONDecodeError): pass return JSONResponse({"ok": True}) @@ -523,7 +523,7 @@ async def admin_strava_sync_status( sc = json.loads(sync_path.read_text(encoding="utf-8")) last_sync = sc.get("last_sync") total_imported = len(sc.get("imported_ids", [])) - except Exception: + except (OSError, json.JSONDecodeError): pass run_status: str | None = None @@ -540,7 +540,7 @@ async def admin_strava_sync_status( run_errors = ss.get("errors", 0) run_error_message = ss.get("error_message") last_run = ss.get("last_run") - except Exception: + except (OSError, json.JSONDecodeError): pass users.append({ diff --git a/bincio/serve/routers/ideas.py b/bincio/serve/routers/ideas.py index fb2c7d6..3a3c38e 100644 --- a/bincio/serve/routers/ideas.py +++ b/bincio/serve/routers/ideas.py @@ -37,7 +37,7 @@ async def list_ideas( for path in sorted(_ideas_dir(dd).glob("*.json")): try: idea = json.loads(path.read_text(encoding="utf-8")) - except Exception: + except (OSError, json.JSONDecodeError): continue votes = idea.get("votes", []) idea["vote_count"] = len(votes) @@ -161,7 +161,7 @@ async def delete_idea( raise HTTPException(404, "Not found") try: idea = json.loads(path.read_text(encoding="utf-8")) - except Exception: + except (OSError, json.JSONDecodeError): raise HTTPException(500, "Could not read idea") if not user.is_admin and idea.get("author") != user.handle: raise HTTPException(403, "Forbidden") @@ -221,7 +221,7 @@ async def submit_feedback( if log_file.exists(): try: existing = json.loads(log_file.read_text()) - except Exception: + except (OSError, json.JSONDecodeError): existing = [] existing.append(entry) log_file.write_text(json.dumps(existing, indent=2)) diff --git a/bincio/serve/routers/me.py b/bincio/serve/routers/me.py index bb2f04f..8c55956 100644 --- a/bincio/serve/routers/me.py +++ b/bincio/serve/routers/me.py @@ -151,7 +151,7 @@ async def me_delete_account( from bincio.render.cli import _write_root_manifest try: _write_root_manifest(deps._get_data_dir()) - except Exception: + except (OSError, json.JSONDecodeError): pass resp = JSONResponse({"ok": True}) @@ -214,7 +214,7 @@ async def me_get_strava_credentials(bincio_session: str | None = Cookie(default= if cid and csec: has_user_creds = True client_id_hint = cid - except Exception: + except (OSError, json.JSONDecodeError): pass return JSONResponse({ "has_user_creds": has_user_creds, @@ -242,7 +242,7 @@ async def me_set_strava_credentials( try: existing = json.loads(creds_path.read_text(encoding="utf-8")) csec = str(existing.get("client_secret", "")).strip() - except Exception: + except (OSError, json.JSONDecodeError): pass if not csec: raise HTTPException(400, "client_secret is required (no existing secret to preserve)") @@ -255,7 +255,7 @@ async def me_set_strava_credentials( old_cid = str(json.loads(creds_path.read_text(encoding="utf-8")).get("client_id", "")).strip() if old_cid and old_cid != cid: token_path.unlink(missing_ok=True) - except Exception: + except (OSError, json.JSONDecodeError): pass creds_path.write_text( diff --git a/bincio/serve/routers/segments.py b/bincio/serve/routers/segments.py index f740a8a..79e6c28 100644 --- a/bincio/serve/routers/segments.py +++ b/bincio/serve/routers/segments.py @@ -32,7 +32,7 @@ def _scan_segment_for_user(dd: Path, handle: str, segment_id: str) -> int: continue try: detail = json.loads(detail_path.read_text(encoding="utf-8")) - except Exception: + except (OSError, json.JSONDecodeError, ValueError): continue ts_url = detail.get("timeseries_url") if not ts_url: @@ -42,14 +42,14 @@ def _scan_segment_for_user(dd: Path, handle: str, segment_id: str) -> int: continue try: ts = json.loads(ts_path.read_text(encoding="utf-8")) - except Exception: + except (OSError, json.JSONDecodeError, ValueError): continue started_raw = detail.get("started_at") if not started_raw: continue try: started_at = _datetime.fromisoformat(started_raw.replace("Z", "+00:00")) - except Exception: + except ValueError: continue track = track_from_timeseries_json(ts, detail.get("id", detail_path.stem), detail.get("sport", "other"), started_at) @@ -228,7 +228,7 @@ async def me_segment_rescan( continue try: detail = _json.loads(detail_path.read_text(encoding="utf-8")) - except Exception: + except (OSError, _json.JSONDecodeError, ValueError): continue ts_url = detail.get("timeseries_url") if not ts_url: @@ -238,14 +238,14 @@ async def me_segment_rescan( continue try: ts = _json.loads(ts_path.read_text(encoding="utf-8")) - except Exception: + except (OSError, _json.JSONDecodeError, ValueError): continue started_raw = detail.get("started_at") if not started_raw: continue try: started_at = _datetime.fromisoformat(started_raw.replace("Z", "+00:00")) - except Exception: + except ValueError: continue track = track_from_timeseries_json( ts, detail.get("id", detail_path.stem), diff --git a/bincio/serve/routers/strava.py b/bincio/serve/routers/strava.py index 72ce5f2..fd46047 100644 --- a/bincio/serve/routers/strava.py +++ b/bincio/serve/routers/strava.py @@ -82,7 +82,7 @@ async def strava_reset(request: Request, bincio_session: Optional[str] = Cookie( dt = datetime.fromisoformat(latest.replace("Z", "+00:00")) last_ts = int(dt.astimezone(timezone.utc).timestamp()) break - except Exception: + except (OSError, json.JSONDecodeError, ValueError): continue if last_ts is None: diff --git a/bincio/serve/routers/uploads.py b/bincio/serve/routers/uploads.py index 7b52fed..5b78e7f 100644 --- a/bincio/serve/routers/uploads.py +++ b/bincio/serve/routers/uploads.py @@ -42,7 +42,7 @@ def _upsert_index_summary(user_dir: Path, activity_id: str, activity: dict, geoj if coords: step = max(1, len(coords) // 9) preview = [[c[1], c[0]] for c in coords[::step]][:9] - except Exception: + except (TypeError, IndexError, AttributeError): pass has_track = (user_dir / "activities" / f"{activity_id}.geojson").exists() @@ -183,7 +183,7 @@ async def upload_raw_activity( try: raw = _b64.b64decode(b64) - except Exception: + except ValueError: raise HTTPException(400, "Invalid base64 encoding") source_hash = hashlib.sha256(raw).hexdigest() @@ -378,7 +378,7 @@ async def upload_activity( cache = json.loads(cache_path.read_text(encoding="utf-8")) cache.pop(activity_id, None) cache_path.write_text(json.dumps(cache, ensure_ascii=False)) - except Exception: + except (OSError, json.JSONDecodeError): pass # Remove merged copies (merge_all will regenerate them after ingest) merged_acts = dd / "_merged" / "activities" diff --git a/bincio/serve/server.py b/bincio/serve/server.py index dd44678..56094f5 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -39,7 +39,7 @@ async def _on_startup() -> None: for p in _glob.glob(str(data_dir / "*" / "tmp*.zip")): try: Path(p).unlink() - except Exception: + except OSError: pass if deps.webroot is not None: threading.Thread(target=tasks._site_rebuild_worker, daemon=True, name="site-rebuild").start() diff --git a/refactoring.md b/refactoring.md index f134de2..bd2fa39 100644 --- a/refactoring.md +++ b/refactoring.md @@ -492,6 +492,6 @@ def test_activity_geojson_missing_geometry(client, tmp_path, authenticated_sessi | 1 | Extract shared image utilities → `bincio/shared/images.py` | Done | | 2 | Extract HTML template → `bincio/edit/templates/edit.html` | Done | | 3 | Split `serve/server.py` into `deps.py` + `routers/*` | Done | -| 4 | Narrow broad `except Exception:` catches | Not started | +| 4 | Narrow broad `except Exception:` catches | Done | > **Note on dependency pinning**: not included. `uv.lock` already pins every dependency (including transitives) to exact versions, which is strictly stronger than switching `>=` to `~=` in `pyproject.toml`. The lockfile is the right mechanism for this concern.