Fix CI: refresh uv.lock, resolve ruff findings, pin ruff 0.16.1
Lint Python / lint (push) Successful in 7s
Typecheck Python / typecheck (push) Successful in 24s
Test Python / test (push) Successful in 1m52s

Also point the README CI badge at the public GitHub mirror workflow.
This commit is contained in:
flan
2026-08-03 19:39:40 +00:00
parent 8b11e85c2c
commit 6f2aaa7d5b
28 changed files with 119 additions and 81 deletions
+3
View File
@@ -19,3 +19,6 @@ jobs:
uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
with:
args: "check"
# Pinned: an unpinned ruff means any upstream release can turn main red
# with no code change (exactly what happened 2026-07 → 2026-08).
version: "0.16.1"
+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- CI: refreshed the stale `uv.lock` (unblocks the `uv lock --check` gate), fixed all
outstanding ruff findings, and pinned ruff to 0.16.1 in the lint workflow so an
upstream ruff release can no longer turn `main` red without a code change.
- README CI badge now points at the public GitHub mirror's workflow instead of the
forge outsiders can't browse.
## [0.35.0] - 2026-07-24
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
# hark
[![test](https://git.onetick.ninja/flan/hark/actions/workflows/test.yml/badge.svg?branch=main)](https://git.onetick.ninja/flan/hark/actions) [![Python 3.12+](https://img.shields.io/badge/Python-3.12%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue)](LICENSE) [![GitHub Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-%E2%9D%A4-EA4AAA?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/sudolulo) [![Ko-fi](https://img.shields.io/badge/Ko--fi-support-FF5E5B?logo=kofi&logoColor=white)](https://ko-fi.com/sudolulo)
[![test](https://github.com/sudolulo/hark/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/sudolulo/hark/actions) [![Python 3.12+](https://img.shields.io/badge/Python-3.12%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue)](LICENSE) [![GitHub Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-%E2%9D%A4-EA4AAA?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/sudolulo) [![Ko-fi](https://img.shields.io/badge/Ko--fi-support-FF5E5B?logo=kofi&logoColor=white)](https://ko-fi.com/sudolulo)
Cross-podcast topic index and discovery service for subject-per-episode genres
(true crime, history, disasters, and the like). The goal: resolve episodes to the
+3 -3
View File
@@ -12,7 +12,7 @@ import contextlib
import hashlib
import secrets
import sqlite3
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
PW_ITERS = 120_000
@@ -75,7 +75,7 @@ def constant_eq(a: str, b: str) -> bool:
def utcnow() -> datetime:
return datetime.now(timezone.utc)
return datetime.now(UTC)
def iso(dt: datetime) -> str:
@@ -83,7 +83,7 @@ def iso(dt: datetime) -> str:
def parse_iso(value: str) -> datetime:
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
class Auth:
+2 -1
View File
@@ -30,9 +30,10 @@ from __future__ import annotations
import json
import os
import sqlite3
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Protocol
from typing import Protocol
from pydantic import BaseModel
+24 -9
View File
@@ -22,11 +22,11 @@ import argparse
import os
import sys
import xml.etree.ElementTree as ET
from collections.abc import Callable
from datetime import UTC
from pathlib import Path
from typing import Callable
import httpx
from adscrub import chapters as ad_chapters
from adscrub import cut as ad_cut
from adscrub import dai as ad_dai
@@ -36,9 +36,24 @@ from adscrub import repeats as ad_repeats
from adscrub import transcribe as ad_transcribe
from . import (
__version__, claims, dai_probe, db, discover, extract, gpodder_server,
hosting, ingest, llm_budget, nextcloud, opml, orchestrator, pipeline,
ratings, resolve, transcript_search, wikidata,
__version__,
claims,
dai_probe,
db,
discover,
extract,
gpodder_server,
hosting,
ingest,
llm_budget,
nextcloud,
opml,
orchestrator,
pipeline,
ratings,
resolve,
transcript_search,
wikidata,
)
DEFAULT_DB = os.environ.get("HARK_DB", "hark.db")
@@ -724,8 +739,8 @@ def cmd_seeds(args: argparse.Namespace) -> int:
# /pipeline can show EXACTLY which episodes detect-ads would read — and that it is these
# few, campaign-deduplicated, not the thousands merely "pending". select_seed_episodes
# writes via ensure_schema, so the web can't recompute this itself.
from datetime import datetime, timezone
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
from datetime import datetime
now_iso = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
by_id = {e["id"]: e for e in all_pending}
conn.execute(
"CREATE TABLE IF NOT EXISTS llm_ad_seeds (episode_id INTEGER PRIMARY KEY, title TEXT, "
@@ -756,7 +771,7 @@ def cmd_seeds(args: argparse.Namespace) -> int:
return 1
by_id = {e["id"]: e for e in all_pending}
out = open(args.out, "w", encoding="utf-8") if args.out else sys.stdout
out = open(args.out, "w", encoding="utf-8") if args.out else sys.stdout # noqa: SIM115 — the sys.stdout branch must not be closed by a context manager
try:
print(f"# {len(seeds)} episode(s) to read — chosen to cover every unread ad campaign.",
file=out)
@@ -1223,7 +1238,7 @@ def cmd_dai_probe(args: argparse.Namespace) -> int:
# adscrub.dai's own docstring for why a shared client's cookie jar
# silently defeats the comparison. User-Agent is set per-fetch by
# probe_variance() itself, so no default is needed here.
client_factory = lambda: httpx.Client(timeout=60.0) # noqa: E731
client_factory = lambda: httpx.Client(timeout=60.0)
for ep in sample:
r = dai_probe.run_probe(client_factory, conn, ep, ep["hosting_platform"])
if r.error or r.result is None: # run_probe sets exactly one of the two
+1 -1
View File
@@ -7,8 +7,8 @@ which platforms actually support this technique, not just running it once.
from __future__ import annotations
import sqlite3
from collections.abc import Callable
from dataclasses import dataclass
from typing import Callable
import httpx
from adscrub import dai
+2 -2
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import secrets
import sqlite3
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
# topics / topic_genres / episode_topics are created now but only populated by
@@ -372,4 +372,4 @@ def connect(path: str | Path) -> sqlite3.Connection:
def utcnow() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
+2 -2
View File
@@ -21,7 +21,7 @@ from __future__ import annotations
import sqlite3
import time
from datetime import datetime, timezone
from datetime import UTC, datetime
from . import resolve
@@ -36,7 +36,7 @@ MAX_SHOWS_PER_USER = 10
def _format_ts(epoch: float) -> str:
return datetime.fromtimestamp(epoch, tz=timezone.utc).strftime(_ACTION_TS_FORMAT)
return datetime.fromtimestamp(epoch, tz=UTC).strftime(_ACTION_TS_FORMAT)
def record_subscription_changes(
+2 -2
View File
@@ -22,7 +22,7 @@ from __future__ import annotations
import os
import sqlite3
from datetime import datetime, timezone
from datetime import UTC, datetime
# Opus 4.8 input price, $/1M tokens — the model hark's ad/topic extraction defaults to. Output is
# tiny for these tasks (span-index lists, short topic labels) so only input is metered.
@@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS llm_spend (
def _today() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
return datetime.now(UTC).strftime("%Y-%m-%d")
def ensure_schema(conn: sqlite3.Connection) -> None:
+9 -7
View File
@@ -20,13 +20,13 @@ from __future__ import annotations
import os
import shutil
import subprocess
import sqlite3
import subprocess
import sys
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Callable
from collections.abc import Callable
from dataclasses import dataclass
from datetime import UTC, datetime
from . import alert, llm_budget
@@ -165,13 +165,15 @@ def stage_meta() -> list[dict]:
def _log(msg: str) -> None:
print(f"[{datetime.now(timezone.utc):%Y-%m-%dT%H:%M:%SZ}] pipeline: {msg}", flush=True)
print(f"[{datetime.now(UTC):%Y-%m-%dT%H:%M:%SZ}] pipeline: {msg}", flush=True)
def _default_run(db_path: str, argv: list[str]) -> int:
"""Run one `hark` subcommand as its own process, so a crash is isolated."""
try:
return subprocess.run([sys.executable, "-m", "hark", "--db", db_path, *argv]).returncode
return subprocess.run(
[sys.executable, "-m", "hark", "--db", db_path, *argv], check=False
).returncode
except Exception as exc: # noqa: BLE001
_log(f"stage {' '.join(argv)} raised {exc}")
return 1
@@ -231,7 +233,7 @@ def run_cycle(
path = os.path.join(data_dir, fname)
if os.path.exists(path):
if run(argv + [path]) == 0:
stamp = datetime.now(timezone.utc).strftime("%s")
stamp = datetime.now(UTC).strftime("%s")
loaded = os.path.join(data_dir, fname.replace("pending-", f"loaded-{stamp}-"))
try:
os.replace(path, loaded)
+1 -1
View File
@@ -15,8 +15,8 @@ store path.
from __future__ import annotations
import sqlite3
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Callable
from .db import utcnow
from .extract import GENRES, ExtractedTopic, TopicExtractor
+3 -4
View File
@@ -19,12 +19,11 @@ from __future__ import annotations
import sqlite3
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from feedgen.feed import FeedGenerator
from adscrub.cut import CUT_SOURCES
from feedgen.feed import FeedGenerator
# Podcasting 2.0 namespace — for <podcast:chapters> in "mark, don't cut" mode.
_PODCAST_NS = "https://podcastindex.org/namespace/1.0"
@@ -64,7 +63,7 @@ def _add_chapters_links(rss: bytes, chapters_url_by_guid: dict[str, str]) -> byt
def _parse_pubdate(value: str | None) -> datetime | None:
if not value:
return None
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
def feed_url(show: sqlite3.Row, base_url: str) -> str:
+2 -2
View File
@@ -7,7 +7,7 @@ from __future__ import annotations
import json
import sqlite3
from datetime import datetime, timezone
from datetime import UTC, datetime
PAGE_SIZE = 50
@@ -173,7 +173,7 @@ def pipeline_status(conn: sqlite3.Connection) -> dict:
out: dict = {"stages": {}, "spans": [], "library": 0, "quarantined": 0, "held": 0,
"spend": {"ads": 0.0, "comparisons": 0.0}}
try:
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
today = datetime.now(UTC).strftime("%Y-%m-%d")
for r in conn.execute("SELECT category, dollars FROM llm_spend WHERE day = ?", (today,)):
out["spend"][r["category"]] = r["dollars"] # read-only: never touches llm_budget's ensure_schema
except sqlite3.OperationalError:
+2 -2
View File
@@ -31,7 +31,7 @@ import math
import re
import sqlite3
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Protocol
import httpx
@@ -236,7 +236,7 @@ class TaddyRatingsSource:
def _cutoff(days: int) -> str:
return (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
return (datetime.now(UTC) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
def _chunked(items: list, size: int):
-1
View File
@@ -13,7 +13,6 @@ from __future__ import annotations
import json
import sqlite3
from pathlib import Path
from . import queries
+18 -8
View File
@@ -10,14 +10,24 @@ import secrets
import sqlite3
import urllib.parse
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
import httpx
from . import (claims, gpodder_server, llm_budget, orchestrator, podcast_feed, ratings, resolve,
scoring, series, transcript_search)
from .auth import BASE_URL_SETTING, Auth, parse_iso, utcnow
from . import (
claims,
gpodder_server,
llm_budget,
orchestrator,
podcast_feed,
ratings,
resolve,
scoring,
series,
transcript_search,
)
from .auth import BASE_URL_SETTING, Auth, parse_iso
from .extract import GENRES as GENRES_FILTER
from .queries import (
PAGE_SIZE,
@@ -46,8 +56,8 @@ from .templates import (
plural,
relative_time,
stage_status_badge,
topic_table,
topic_pills,
topic_table,
)
# MAX_SHOWS_PER_USER lives in gpodder_server.py — shared with the
@@ -568,7 +578,7 @@ class App:
status = row["last_status"] if row else None
exit_code = row["last_exit"] if row else None
last_run = (row["last_run"] if row else None) or 0.0
when = relative_time(datetime.fromtimestamp(last_run, timezone.utc)) if last_run > 0 else ""
when = relative_time(datetime.fromtimestamp(last_run, UTC)) if last_run > 0 else ""
stage_rows.append(
f"<tr class='stagerow'><td>{esc(meta['name'])}</td>"
f"<td class='dim'>{esc(meta['cadence'])}</td>"
@@ -913,7 +923,7 @@ class App:
),
"rare": (
"<h2>Rare coverage</h2>" +
(f'<p class="dim">Top 15 episodes covering hark\'s least-common genres — '
('<p class="dim">Top 15 episodes covering hark\'s least-common genres — '
+ " and ".join(
f'<a href="/topics?genre={esc(g)}">{esc(g)}</a>' for g in rare_genres
) + ".</p>"
@@ -1006,7 +1016,7 @@ class App:
)
related_html = f"<h2>Related topics</h2><p>{related_pills}</p>"
comparison_html = (
f'<h2 id="comparison">what each show said</h2>'
'<h2 id="comparison">what each show said</h2>'
+ claims_html(comparison, shows_transcribed)
) if comparison is not None or shows_transcribed >= 2 else ""
span_html = ""
+2 -1
View File
@@ -84,7 +84,8 @@ from .templates import ( # noqa: F401 — re-exported for callers (cli.py, test
topic_pills,
topic_table,
)
from .views import COOKIE, App # noqa: F401 — re-exported for callers (cli.py, tests)
from .views import COOKIE, App
def _safe_next(value: str, default: str) -> str:
"""Validate a same-origin-relative redirect target from a form's `next`
+2 -2
View File
@@ -11,7 +11,7 @@ from __future__ import annotations
import email.utils
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import UTC, datetime
import httpx
@@ -32,7 +32,7 @@ def _retry_after_seconds(value: str | None, default: float = 5.0) -> float:
when = email.utils.parsedate_to_datetime(value)
except (TypeError, ValueError):
return default
seconds = (when - datetime.now(timezone.utc)).total_seconds()
seconds = (when - datetime.now(UTC)).total_seconds()
return max(0.0, min(seconds, MAX_BACKOFF))
+3 -4
View File
@@ -1,7 +1,6 @@
import json
import httpx
from adscrub import cut as ad_cut
from adscrub import detect as ad_detect
from adscrub import transcribe as ad_transcribe
@@ -983,7 +982,7 @@ def test_user_add_admin_flag(tmp_path, capsys):
from hark import web
auth = web.Auth(auth_db, admin_token=None)
row = [u for u in auth.list_users() if u["username"] == "bob"][0]
row = next(u for u in auth.list_users() if u["username"] == "bob")
assert row["is_admin"] == 1
@@ -1054,7 +1053,7 @@ def test_user_invite_creates_account_and_prints_link(tmp_path, capsys):
from hark import web
auth = web.Auth(auth_db, admin_token=None)
row = [u for u in auth.list_users() if u["username"] == "alice"][0]
row = next(u for u in auth.list_users() if u["username"] == "alice")
assert row["invite_pending"] == 1
@@ -1065,7 +1064,7 @@ def test_user_invite_admin_flag(tmp_path, capsys):
from hark import web
auth = web.Auth(auth_db, admin_token=None)
row = [u for u in auth.list_users() if u["username"] == "bob"][0]
row = next(u for u in auth.list_users() if u["username"] == "bob")
assert row["is_admin"] == 1
+2 -2
View File
@@ -1,7 +1,7 @@
import httpx
import pytest
from adscrub import dai
from hark import dai_probe, db
@@ -178,7 +178,7 @@ def test_run_probe_records_an_attempt_on_fetch_failure(conn):
def handler(request):
return httpx.Response(404)
factory = lambda: httpx.Client(transport=httpx.MockTransport(handler)) # noqa: E731
factory = lambda: httpx.Client(transport=httpx.MockTransport(handler))
result = dai_probe.run_probe(factory, conn, ep, "acast.com")
assert result.error is not None
+1 -1
View File
@@ -31,7 +31,7 @@ def test_subscription_changes_since_only_returns_later_events(tmp_path):
("https://b.example/feed", cursor_after_first + 10),
)
conn.commit()
add, remove, _ = gpodder_server.subscription_changes_since(conn, 1, cursor_after_first)
add, _remove, _ = gpodder_server.subscription_changes_since(conn, 1, cursor_after_first)
assert add == ["https://b.example/feed"]
+3 -1
View File
@@ -1,6 +1,8 @@
import sqlite3
import pytest
from hark import orchestrator, llm_budget
from hark import llm_budget, orchestrator
def _db(tmp_path):
-1
View File
@@ -274,7 +274,6 @@ def test_recanonicalize_limit_caps_batch_size(tmp_path):
def canonicalize(label):
calls.append(label)
return None
results = pipeline.recanonicalize(conn, canonicalize, limit=2)
assert results == [] # canonicalize returns None for all, no upgrades
+1 -1
View File
@@ -110,7 +110,7 @@ def test_chapters_mode_serves_original_audio_with_chapter_links(server, tmp_path
assert doc["version"] == "1.2.0"
assert {"startTime": 30.0, "title": "Advertisement"} in doc["chapters"]
assert {"startTime": 60.0, "title": "Content"} in doc["chapters"]
assert request(server, f"/chapters/2/wrong.json")[0].status == 404 # wrong token
assert request(server, "/chapters/2/wrong.json")[0].status == 404 # wrong token
def test_feed_route_uses_admin_base_url_override(server, tmp_path):
+9 -8
View File
@@ -1,5 +1,5 @@
import json
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
import httpx
import pytest
@@ -114,18 +114,19 @@ def test_fetch_raises_taddy_error_on_graphql_error_body():
def handler(variables):
return httpx.Response(200, json={"errors": [{"message": "bad argument"}]})
with graphql_client(handler) as client:
with pytest.raises(ratings.TaddyError):
make_source(client).fetch("https://feeds.example.com/x", None)
with graphql_client(handler) as client, pytest.raises(ratings.TaddyError):
make_source(client).fetch("https://feeds.example.com/x", None)
def test_fetch_raises_on_http_error_status():
def handler(request):
return httpx.Response(401, json={"message": "unauthorized"})
with httpx.Client(transport=httpx.MockTransport(handler)) as client:
with pytest.raises(httpx.HTTPStatusError):
make_source(client).fetch("https://feeds.example.com/x", None)
with (
httpx.Client(transport=httpx.MockTransport(handler)) as client,
pytest.raises(httpx.HTTPStatusError),
):
make_source(client).fetch("https://feeds.example.com/x", None)
# --- refresh_ratings ---
@@ -290,7 +291,7 @@ def test_refresh_ratings_known_match_uses_shorter_stale_window_than_a_miss(tmp_p
conn = db.connect(tmp_path / "t.db")
# 100 days old: stale for a known match (90-day window) but not yet due
# for a confirmed miss (180-day window).
cutoff = (datetime.now(timezone.utc) - timedelta(days=100)).strftime("%Y-%m-%dT%H:%M:%SZ")
cutoff = (datetime.now(UTC) - timedelta(days=100)).strftime("%Y-%m-%dT%H:%M:%SZ")
known_id = seed_existing_rating(conn, "https://feeds.example.com/known", "uuid-known", cutoff, 4.0)
seed_existing_rating(conn, "https://feeds.example.com/miss", None, cutoff)
+13 -13
View File
@@ -280,7 +280,7 @@ def test_episode_ad_transparency_and_admin_marking(tmp_path):
marked = [r for r in rows if r["source"] == "manual"]
assert marked and marked[0]["start_second"] == 120.0 and marked[0]["end_second"] == 150.0
assert conn.execute("SELECT cut_path FROM episodes WHERE id = 1").fetchone()[0] is None
fp_id = [r["id"] for r in rows if r["source"] == "fpmatch"][0]
fp_id = next(r["id"] for r in rows if r["source"] == "fpmatch")
conn.close()
# (#4) remove a false positive
@@ -1169,7 +1169,7 @@ def test_adblock_toggle_is_atomic_under_concurrent_requests(server):
t.join()
assert not errors
resp, body = request(server, "GET", "/show/1", cookie=cookie)
_resp, body = request(server, "GET", "/show/1", cookie=cookie)
assert "<strong>enabled</strong>" in body # 20 (even) toggles from enabled -> enabled
@@ -1704,7 +1704,7 @@ def test_bootstrap_admin_is_admin(tmp_path):
def test_create_user_defaults_to_non_admin(tmp_path):
auth = web.Auth(tmp_path / "auth.db", admin_token="t")
auth.create_user("alice")
row = [u for u in auth.list_users() if u["username"] == "alice"][0]
row = next(u for u in auth.list_users() if u["username"] == "alice")
assert row["is_admin"] == 0
assert row["has_password"] == 0
@@ -1712,14 +1712,14 @@ def test_create_user_defaults_to_non_admin(tmp_path):
def test_create_user_can_grant_admin(tmp_path):
auth = web.Auth(tmp_path / "auth.db", admin_token="t")
auth.create_user("bob", is_admin=True)
row = [u for u in auth.list_users() if u["username"] == "bob"][0]
row = next(u for u in auth.list_users() if u["username"] == "bob")
assert row["is_admin"] == 1
def test_create_user_rejects_duplicate_username(tmp_path):
auth = web.Auth(tmp_path / "auth.db", admin_token="t")
auth.create_user("alice")
with pytest.raises(Exception):
with pytest.raises(sqlite3.IntegrityError):
auth.create_user("alice")
@@ -1854,7 +1854,7 @@ def test_create_invite_returns_working_token(tmp_path):
def test_create_invite_defaults_to_non_admin(tmp_path):
auth = web.Auth(tmp_path / "auth.db", admin_token="t")
auth.create_invite("alice")
row = [u for u in auth.list_users() if u["username"] == "alice"][0]
row = next(u for u in auth.list_users() if u["username"] == "alice")
assert row["is_admin"] == 0
assert row["invite_pending"] == 1
@@ -1862,7 +1862,7 @@ def test_create_invite_defaults_to_non_admin(tmp_path):
def test_create_invite_can_grant_admin(tmp_path):
auth = web.Auth(tmp_path / "auth.db", admin_token="t")
auth.create_invite("alice", is_admin=True)
row = [u for u in auth.list_users() if u["username"] == "alice"][0]
row = next(u for u in auth.list_users() if u["username"] == "alice")
assert row["is_admin"] == 1
@@ -2008,7 +2008,7 @@ def test_subscribe_unknown_show_404s(server):
def test_show_page_offers_subscribe_toggle(server):
cookie = login(server)
resp, body = request(server, "GET", "/show/1", cookie=cookie)
_resp, body = request(server, "GET", "/show/1", cookie=cookie)
assert "In your list." in body
assert 'action="/show/1/unsubscribe"' in body
@@ -2059,10 +2059,10 @@ def test_gpodder_sync_isolated_between_two_accounts(server, tmp_path):
request(server, "POST", "/index.php/apps/gpoddersync/subscription_change/create",
auth=alice_auth, json_body={"add": ["https://alice-only.example/feed"], "remove": []})
resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
auth=admin_auth, json_body=None)
_resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
auth=admin_auth, json_body=None)
admin_add = json.loads(body)["add"]
resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
_resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
auth=alice_auth, json_body=None)
alice_add = json.loads(body)["add"]
@@ -2514,7 +2514,7 @@ def test_gpodder_sync_quota_enforced_for_non_admin(server, tmp_path):
urls = [f"https://quota-test-{i}.example/feed" for i in range(15)]
request(server, "POST", "/index.php/apps/gpoddersync/subscription_change/create",
auth=auth, json_body={"add": urls, "remove": []})
resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
_resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
auth=auth, json_body=None)
data = json.loads(body)
assert len(data["add"]) == 10
@@ -2525,7 +2525,7 @@ def test_gpodder_sync_quota_not_enforced_for_admin(server, tmp_path):
urls = [f"https://admin-quota-test-{i}.example/feed" for i in range(15)]
request(server, "POST", "/index.php/apps/gpoddersync/subscription_change/create",
auth=admin_auth, json_body={"add": urls, "remove": []})
resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
_resp, body = request(server, "GET", "/index.php/apps/gpoddersync/subscriptions",
auth=admin_auth, json_body=None)
data = json.loads(body)
assert len([u for u in urls if u in data["add"]]) == 15
Generated
+1 -1
View File
@@ -242,7 +242,7 @@ wheels = [
[[package]]
name = "hark"
version = "0.34.1"
version = "0.35.0"
source = { editable = "." }
dependencies = [
{ name = "adscrub" },