Scaffold v0.1.0: corpus collector, methodology spec, docs-enforcement CI
- ingest: 20 outlets' politics feeds -> full text -> SQLite w/ SHA-256 fingerprints - METHODOLOGY.md: 10 decision blocks (decision/rationale+sources/alternatives/failure modes) - docs: research foundations, plain-language walkthrough (CI-gated <= grade 10), glossary, ADR-0001, generated outlets table - CI: pytest+ruff, docs gates (readability, glossary coverage, decision-block lint, outlets-table freshness), code/docs sync gate - systemd user timer ingests every 6h (repo-external, documented in README quickstart)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- run: uv sync --dev
|
||||
- run: uv run ruff check .
|
||||
- name: tests (includes docs gates - readability, glossary, decision-block lint)
|
||||
run: uv run pytest -q
|
||||
|
||||
docs-sync:
|
||||
# Code and config may not change without a docs change in the same range.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: determine range and check
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
BEFORE: ${{ github.event.before }}
|
||||
SHA: ${{ github.sha }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
if [[ "$EVENT" == "pull_request" ]]; then
|
||||
range="origin/${BASE_REF}...HEAD"
|
||||
elif [[ "$BEFORE" =~ ^0+$ ]]; then
|
||||
echo "new branch push; skipping docs-sync"; exit 0
|
||||
else
|
||||
range="${BEFORE}..${SHA}"
|
||||
fi
|
||||
bash scripts/check_docs_sync.sh "$range"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# collected corpus stays local (copyright: we publish manifests, not text)
|
||||
data/
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
dist/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
@@ -0,0 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project are documented here. Format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
|
||||
[SemVer](https://semver.org). Any change that could alter published ratings
|
||||
requires a version bump and, if it changes methodology, a decision record in
|
||||
`docs/decisions/`.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-07-10
|
||||
|
||||
### Added
|
||||
|
||||
- Project scaffold: uv-managed Python 3.13 package, MIT license, CI.
|
||||
- Corpus collector (`tiltmeter ingest`): polls 20 US outlets' politics RSS
|
||||
feeds, fetches full article text once per URL, stores to SQLite with SHA-256
|
||||
content fingerprints. `tiltmeter status` reports per-outlet counts.
|
||||
- Methodology specification (`METHODOLOGY.md`): ten decision blocks
|
||||
(D1–D10), each with decision, sourced rationale, alternatives, and failure
|
||||
modes; format machine-checked in CI.
|
||||
- Foundational literature review (`docs/research.md`), venue-verified.
|
||||
- Plain-language walkthrough (`docs/how-it-works.md`), CI-gated to ≤ grade-10
|
||||
reading level; glossary (`docs/glossary.md`).
|
||||
- ADR-0001: story-selection ideal points with congressional-language anchor.
|
||||
- Docs enforcement in CI: readability gate, glossary coverage, decision-block
|
||||
and ADR format lint, generated outlets table freshness, code/docs sync gate.
|
||||
|
||||
### Changed
|
||||
|
||||
- Outlet sample swaps against the planned list (feed availability,
|
||||
2026-07-10): AP → Christian Science Monitor (AP has no public RSS; 401),
|
||||
WSJ politics feed → WSJ world news feed (politics feed dead; Opinion feed
|
||||
deliberately not used — news and opinion are rated separately by every
|
||||
incumbent rater).
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 flan <flan@arch.fyi>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
# Methodology
|
||||
|
||||
This document is the technical specification of how tiltmeter computes political-lean
|
||||
ratings. It is the product: the numbers are only as good as this document is honest.
|
||||
|
||||
Every design choice below is a numbered decision block with four mandatory fields —
|
||||
**Decision**, **Rationale** (with sources), **Alternatives considered**, and **Failure
|
||||
modes**. This format is machine-checked by `tests/test_docs.py`. Changing any decision
|
||||
requires a new record in `docs/decisions/`, a CHANGELOG entry, and a version bump — the
|
||||
methodology cannot move quietly.
|
||||
|
||||
Plain-language version: [docs/how-it-works.md](docs/how-it-works.md).
|
||||
Literature foundations: [docs/research.md](docs/research.md).
|
||||
Term definitions: [docs/glossary.md](docs/glossary.md).
|
||||
|
||||
Choices that are pragmatic rather than literature-backed are explicitly labeled
|
||||
**Tunable** and are covered by the published parameter sensitivity analysis (D7)
|
||||
rather than dressed up as principled.
|
||||
|
||||
---
|
||||
|
||||
## D1. Ratings come from a reproducible pipeline, not editorial judgment
|
||||
|
||||
**Decision**: Every rating is computed by open code running on an open, content-hashed
|
||||
corpus snapshot, with pinned dependencies and fixed random seeds. Anyone can rerun the
|
||||
pipeline and obtain the identical output. No human judgment enters the scoring path.
|
||||
|
||||
**Rationale**: The incumbent raters (AllSides, Ad Fontes, MBFC) all rest on structured
|
||||
human judgment — balanced panels, blind surveys, rubrics (see
|
||||
[docs/research.md §2](docs/research.md)). Their answer to bias is balancing the panel;
|
||||
ours is removing it. A reproducible method shifts disputes from "you are biased" to
|
||||
"here is the code and data" — the dispute becomes checkable. Validation discipline for
|
||||
text-based measures follows Grimmer & Stewart (2013), *Text as Data*, Political
|
||||
Analysis.
|
||||
|
||||
**Alternatives considered**: Human ratings with published evidence worksheets
|
||||
(AllSides-style transparency) — scales poorly and invites governance brigading. Hybrid
|
||||
algorithmic-plus-override — inherits the governance problem the moment the first
|
||||
override lands.
|
||||
|
||||
**Failure modes**: Reproducible is not the same as correct — a deterministic pipeline
|
||||
can be deterministically wrong, which is why validation (D7) is a gate, not a
|
||||
formality. Method choices themselves can encode bias; that risk is handled by
|
||||
citing each choice to prior literature and publishing sensitivity analyses.
|
||||
|
||||
## D2. Version 1 rates political lean only
|
||||
|
||||
**Decision**: One axis: political lean, expressed on a [-1, +1] scale. Reliability
|
||||
and factuality are out of scope for v1.
|
||||
|
||||
**Rationale**: Lean is the axis with the strongest measurement tradition (Puglisi &
|
||||
Snyder 2015, *Empirical Studies of Media Bias*, Handbook of Media Economics) and the
|
||||
best available validation targets (both AllSides and Ad Fontes publish lean ratings).
|
||||
Reliability requires corrections tracking and fact-check cross-referencing — a
|
||||
different, harder pipeline.
|
||||
|
||||
**Alternatives considered**: Two-axis lean+reliability (Ad Fontes chart) — doubles v1
|
||||
scope. Reliability only — less contentious but hardest to compute and not what
|
||||
blindspot-style analysis needs.
|
||||
|
||||
**Failure modes**: A lean-only score can be misread as a quality score. Every published
|
||||
artifact states that lean ≠ reliability.
|
||||
|
||||
## D3. The v1 signal is story selection, framed as ideal-point estimation
|
||||
|
||||
**Decision**: Measure *what outlets choose to cover*. Articles from all outlets are
|
||||
embedded (all-MiniLM-L6-v2 sentence embeddings of headline + lede) and grouped into
|
||||
cross-outlet story clusters. The outlet×story coverage matrix is scaled by
|
||||
correspondence analysis; the first principal axis is the candidate lean dimension.
|
||||
Framing: this is roll-call analysis where outlets are legislators and coverage
|
||||
decisions are votes.
|
||||
|
||||
**Rationale**: Selection (gatekeeping) bias is a real and measurable channel:
|
||||
D'Alessio & Allen (2000, J. Communication) meta-analytic taxonomy; Budak, Goel & Rao
|
||||
(2016, Public Opinion Quarterly) found outlet bias manifests largely through issue
|
||||
selection; Rönnback, Emmery & Brighton (2025, PLOS One) found coverage features the
|
||||
most informative for outlet-level bias prediction. The scaling method sits in the
|
||||
ideal-point tradition (Poole & Rosenthal 1985, AJPS; unsupervised text scaling:
|
||||
Slapin & Proksch 2008, AJPS "Wordfish"). It requires no training data, no lexicon,
|
||||
and no LLM judgment.
|
||||
|
||||
**Alternatives considered**: Language/framing similarity to congressional speech
|
||||
(Gentzkow & Shapiro 2010, Econometrica) — planned as signal S2 in v0.2, not v1,
|
||||
because it requires the corrected high-dimensional estimator of Gentzkow, Shapiro &
|
||||
Taddy (2019, Econometrica) to avoid severe finite-sample bias. Audience-based scores
|
||||
(Bakshy, Messing & Adamic 2015, Science; Barberá 2015) — platform data access is now
|
||||
gated. LLM-as-judge — excluded by D8.
|
||||
|
||||
**Failure modes**: The first principal axis may capture topic mix (business vs.
|
||||
lifestyle) rather than politics — mitigated by politics-section feeds (D9) and by
|
||||
publishing the story clusters behind every score so the axis is inspectable. Small
|
||||
outlet samples make the axis unstable — mitigated by the snapshot window length and
|
||||
bootstrap confidence intervals (D6). **Tunable**: embedding model, clustering
|
||||
threshold — both in the sensitivity sweep (D7).
|
||||
|
||||
## D4. Twenty outlets, deliberately spread, chosen once and openly
|
||||
|
||||
**Decision**: v1 covers the 20 US outlets in `config/outlets.yaml`, selected to span
|
||||
the spectrum from Mother Jones/The Nation to Breitbart/Newsmax, with
|
||||
politics-section RSS feeds. Incumbent-rater ratings were consulted for *sample
|
||||
selection only* — they are never an input to scoring.
|
||||
|
||||
**Rationale**: A proof of concept needs a sample where the expected ordering is
|
||||
uncontroversial enough that failure to recover it falsifies the method. Feed
|
||||
availability was verified 2026-07-10; two swaps from the original list (AP → CS
|
||||
Monitor, dead feed; WSJ politics → WSJ world news feed) are recorded in CHANGELOG.md.
|
||||
|
||||
**Alternatives considered**: ~100-outlet US national coverage — more infrastructure
|
||||
before the method is proven. Including international outlets — the left-right axis is
|
||||
US-calibrated and ground truth thins out.
|
||||
|
||||
**Failure modes**: Sample selection is itself a choice with bias potential; the
|
||||
selection criteria are stated in `config/outlets.yaml` comments, and the sample is
|
||||
frozen for v1 so results cannot be cherry-picked by adding/removing outlets
|
||||
post hoc.
|
||||
|
||||
## D5. The axis is oriented by congressional language, not by assumed outlet leans
|
||||
|
||||
**Decision**: No outlet's lean is assumed anywhere in the pipeline. The unsupervised
|
||||
axis from D3 gets its sign (which end is "left") from an external reference corpus:
|
||||
recent Congressional Record speeches (govinfo.gov bulk data), with party membership
|
||||
and DW-NOMINATE scores from voteview.com. Whichever axis pole is closer to Democratic
|
||||
vs. Republican language determines orientation. The published reference frame is
|
||||
explicit: *lean relative to contemporary US congressional party discourse*.
|
||||
|
||||
**Rationale**: Lean is relational — every method in the literature defines it against
|
||||
a reference population. Politicians are the standard anchor because their ideology is
|
||||
a matter of public record (roll-call votes), not a rating: Gentzkow & Shapiro (2010,
|
||||
Econometrica); Poole & Rosenthal's DW-NOMINATE. Anchoring to politicians also keeps
|
||||
validation (D7) non-circular: no rater data enters the pipeline.
|
||||
|
||||
**Alternatives considered**: Declared anchor outlets ("Mother Jones = left") — one
|
||||
editorial bit, but exactly the kind of judgment this project exists to remove.
|
||||
Audience-based orientation — gated platform data.
|
||||
|
||||
**Failure modes**: Congressional discourse defines the center as the US two-party
|
||||
midpoint; positions outside that frame (or drift of the frame itself over time) are
|
||||
invisible to the measure. This is stated, not hidden: the frame is part of every
|
||||
release's metadata.
|
||||
|
||||
## D6. Scores are time series with uncertainty, never permanent labels
|
||||
|
||||
**Decision**: Every published score is attached to a corpus snapshot window and
|
||||
carries a 95% bootstrap confidence interval (resampling over story clusters).
|
||||
tiltmeter publishes no undated, uncertainty-free outlet labels.
|
||||
|
||||
**Rationale**: Outlet lean drifts, and even incumbent raters disagree with each other
|
||||
sharply — MBFC vs. audience-derived labels agree only 46%, two human-annotated
|
||||
sources 57% (Rönnback et al. 2025, PLOS One). Penn's Media Bias Detector (CHI 2025)
|
||||
adopts the same dynamic-view stance. Publishing uncertainty is what distinguishes a
|
||||
measurement from a verdict.
|
||||
|
||||
**Alternatives considered**: Static labels (the incumbent-rater product shape) —
|
||||
misrepresents both drift and measurement error.
|
||||
|
||||
**Failure modes**: Time series with CIs are harder for laymen to read than a single
|
||||
label — mitigated by the evidence pages (D10) and the plain-language docs layer.
|
||||
|
||||
## D7. Validation against incumbent raters is a gate, with published sensitivity
|
||||
|
||||
**Decision**: `config/reference_ratings.yaml` holds published AllSides (5-point,
|
||||
mapped to −2..+2) and Ad Fontes (numeric) ratings with retrieval dates, used for
|
||||
validation only. The v1 gate: Spearman rank correlation ρ ≥ 0.7 against both raters
|
||||
over the 20-outlet sample. Every release also publishes a parameter sensitivity
|
||||
sweep: how much ratings move under alternative tunables (clustering threshold,
|
||||
embedding model, window length).
|
||||
|
||||
**Rationale**: Agreement with independent human raters is the standard external check
|
||||
for text-based measures (Grimmer & Stewart 2013). The bar is deliberately below
|
||||
perfect: given 46–57% inter-rater agreement in adjacent work, near-perfect
|
||||
correlation with any one rater would indicate overfitting to that rater, not truth.
|
||||
Fair-use note: reference ratings are facts (a rating value on a date), stored with
|
||||
retrieval dates, used only for validation.
|
||||
|
||||
**Alternatives considered**: No external validation ("the method is principled,
|
||||
trust it") — indistinguishable from the incumbents' posture. Validating against
|
||||
audience data — gated.
|
||||
|
||||
**Failure modes**: ρ ≥ 0.7 on 20 outlets has wide confidence bounds itself; the
|
||||
validation report states the n and the CI on ρ. Failing the gate stops scaling — the
|
||||
documented response is to iterate the method, not the sample.
|
||||
|
||||
## D8. No LLM judgment in the scoring path
|
||||
|
||||
**Decision**: Large language models are never asked to judge bias, lean, framing, or
|
||||
quality anywhere in the pipeline. Embedding models are permitted for similarity
|
||||
computation only (D3), pinned by exact version.
|
||||
|
||||
**Rationale**: LLMs carry measurable political lean themselves: Rozado (2024, PLOS
|
||||
One, "The political preferences of LLMs" — 11 orientation tests × 24 models);
|
||||
Santurkar et al. (2023, ICML, "Whose Opinions Do Language Models Reflect?").
|
||||
Piping the news through a judge with its own lean would relocate the bias problem,
|
||||
not solve it. Small pinned embedding models keep runs reproducible in a way
|
||||
API-served LLM judgments are not.
|
||||
|
||||
**Alternatives considered**: LLM-as-judge with audited prompts — cheaper per article
|
||||
than any alternative, but unreproducible across model versions and lean-contaminated.
|
||||
Penn's Media Bias Detector accepts this trade; we don't.
|
||||
|
||||
**Failure modes**: Embeddings are not judgment-free either — they encode training
|
||||
distribution biases. The mitigation is that embeddings only *group similar text*
|
||||
here; the lean axis and its orientation come from coverage structure and public
|
||||
political records, and the embedding model is a published tunable in the
|
||||
sensitivity sweep (D7).
|
||||
|
||||
## D9. The corpus is politics-section RSS, stored locally, published as manifests
|
||||
|
||||
**Decision**: Ingestion polls each outlet's politics-section RSS feed (general feed
|
||||
where none exists), fetches full article text once per URL, and stores it locally.
|
||||
Published corpus artifacts are manifests — URL, headline, outlet, timestamp, SHA-256
|
||||
content hash per article — not full text.
|
||||
|
||||
**Rationale**: Politics-section feeds reduce the topic-mix confound in D3. Manifests
|
||||
let anyone re-fetch and hash-verify the exact corpus without tiltmeter
|
||||
redistributing copyrighted article text. Headline+link is the same posture
|
||||
aggregators take, and readers are always sent to the source.
|
||||
|
||||
**Alternatives considered**: Redistributing full text (clean reproducibility, clear
|
||||
copyright violation). Third-party corpora like GDELT (Rönnback et al. 2025 use it) —
|
||||
convenient but inserts an unauditable dependency between the outlet and our data.
|
||||
|
||||
**Failure modes**: Articles edited after our fetch won't match our stored hash —
|
||||
manifests carry the fetch timestamp, and hash mismatches on re-fetch are reported as
|
||||
exactly that. Feeds die (two already did, see D4) — per-outlet ingest counts are
|
||||
monitored and chronic failures trigger a documented swap.
|
||||
|
||||
## D10. Every number is traceable to evidence a layman can read
|
||||
|
||||
**Decision**: Every published score links to an evidence page: the story clusters
|
||||
that drove the outlet's position with example headlines side by side, its
|
||||
nearest-neighbor outlets, the confidence interval, the corpus snapshot ID, and the
|
||||
pipeline version. Reproducing any release must be a documented ≤3-command path ending
|
||||
in a hash-verified `ratings.json`.
|
||||
|
||||
**Rationale**: This is the project's core value made concrete (see README). A layman
|
||||
asking "why is this outlet here?" should see actual headlines, not eigenvectors.
|
||||
Auditability that requires a statistics degree is not auditability for the public
|
||||
this project serves.
|
||||
|
||||
**Alternatives considered**: Publishing scores + code only — auditable in principle,
|
||||
by almost nobody in practice.
|
||||
|
||||
**Failure modes**: Evidence pages could be cherry-picked — they are generated by the
|
||||
same deterministic pipeline as the scores (`report` module), never hand-edited, and
|
||||
regenerating them is part of the ≤3-command reproduction path.
|
||||
@@ -0,0 +1,10 @@
|
||||
tiltmeter — development disclosure
|
||||
|
||||
This project is developed with substantial AI assistance (Anthropic Claude),
|
||||
under human direction and review. Methodology decisions are recorded in
|
||||
docs/decisions/ with their rationale and sources regardless of who or what
|
||||
drafted them; the auditability guarantees in METHODOLOGY.md are enforced by
|
||||
CI, not by trust in any author, human or otherwise.
|
||||
|
||||
Consistent with the project's disclosure policy, AI use is declared here in
|
||||
the repository rather than in individual commit messages.
|
||||
@@ -0,0 +1,66 @@
|
||||
# tiltmeter
|
||||
|
||||
**Auditable political-lean ratings for news outlets.** A tiltmeter is an instrument
|
||||
that measures the tilt of the ground — this one measures the tilt of the news.
|
||||
|
||||
Commercial bias raters (AllSides, Ad Fontes, Media Bias/Fact Check) sell outlet
|
||||
ratings produced by human panels behind closed doors. You can't check their work.
|
||||
tiltmeter is the opposite bet: ratings computed by open code from open data, where
|
||||
every number can be re-derived, every methodology choice is cited to published
|
||||
research, and every score links to the actual headlines that produced it.
|
||||
|
||||
**Do not trust us. Check.**
|
||||
|
||||
## How it works (one paragraph)
|
||||
|
||||
tiltmeter watches which stories each of 20 US outlets chooses to cover, via their
|
||||
public politics feeds. Outlets making similar coverage choices land near each other
|
||||
on a map — a seating chart built from the outlets' own behavior, with no one voting
|
||||
on it. Which side of the map is "left" is decided by public records, not opinion:
|
||||
one pole's coverage sits closer to Democratic congressional speech, the other to
|
||||
Republican. Each outlet gets a dated score from −1 to +1 with an honest error range.
|
||||
Plain-language walkthrough: [docs/how-it-works.md](docs/how-it-works.md). Full
|
||||
technical spec with sources: [METHODOLOGY.md](METHODOLOGY.md).
|
||||
|
||||
## Status
|
||||
|
||||
Pre-alpha (v0.1, milestone M1). The corpus collector is running; scoring lands in
|
||||
M2; the first validated ratings release requires ≥2 weeks of corpus (M3). Nothing
|
||||
here is a usable rating yet.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```sh
|
||||
uv sync # install
|
||||
uv run tiltmeter ingest # poll all 20 outlet feeds once
|
||||
uv run tiltmeter status # article counts per outlet
|
||||
```
|
||||
|
||||
## The auditability contract
|
||||
|
||||
- **Reproducible**: same snapshot + same version ⇒ byte-identical ratings. Anyone
|
||||
can verify a release in ≤3 commands.
|
||||
- **Sourced**: every methodology decision in [METHODOLOGY.md](METHODOLOGY.md) cites
|
||||
the research behind it ([literature review](docs/research.md)).
|
||||
- **Traceable**: every score links to an evidence page of real headlines.
|
||||
- **Versioned**: methodology changes require a [decision record](docs/decisions/),
|
||||
a CHANGELOG entry, and a version bump. The method cannot move quietly.
|
||||
- **Readable**: the plain-language docs are CI-gated to a grade-10 reading level.
|
||||
Jargon must be defined in the [glossary](docs/glossary.md).
|
||||
|
||||
## Project layout
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `METHODOLOGY.md` | The product: every design decision, with sources |
|
||||
| `docs/how-it-works.md` | The same thing, for humans in a hurry |
|
||||
| `docs/research.md` | The literature this stands on |
|
||||
| `docs/decisions/` | Numbered methodology decision records (ADRs) |
|
||||
| `config/outlets.yaml` | The 20 outlets and their feeds |
|
||||
| `src/tiltmeter/` | The pipeline (each module states its question in plain language) |
|
||||
| `releases/` | Corpus manifests and ratings, per release |
|
||||
|
||||
## License
|
||||
|
||||
Code: [MIT](LICENSE). Published ratings data: CC-BY-4.0. See [NOTICE](NOTICE) for
|
||||
development disclosure.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Outlets tiltmeter collects from.
|
||||
#
|
||||
# Selection criteria (see METHODOLOGY.md, decision D4):
|
||||
# - ~20 US outlets deliberately spread across the spectrum as rated by the
|
||||
# incumbent raters (used for SELECTION only, never as pipeline input)
|
||||
# - politics-section feed preferred where one exists, to reduce the
|
||||
# topic-mix confound (sports/lifestyle coverage polluting the signal)
|
||||
# - feed must actually work; broken outlets get swapped for a
|
||||
# same-region-of-spectrum alternative, recorded in CHANGELOG.md
|
||||
#
|
||||
# Fields: name (canonical id), feed (RSS/Atom URL), homepage.
|
||||
|
||||
outlets:
|
||||
- name: cs-monitor
|
||||
homepage: https://www.csmonitor.com
|
||||
feed: https://rss.csmonitor.com/feeds/politics
|
||||
- name: pbs-newshour
|
||||
homepage: https://www.pbs.org/newshour
|
||||
feed: https://www.pbs.org/newshour/feeds/rss/politics
|
||||
- name: npr
|
||||
homepage: https://www.npr.org
|
||||
feed: https://feeds.npr.org/1014/rss.xml
|
||||
- name: axios
|
||||
homepage: https://www.axios.com
|
||||
feed: https://api.axios.com/feed/
|
||||
- name: the-hill
|
||||
homepage: https://thehill.com
|
||||
feed: https://thehill.com/homenews/feed/
|
||||
- name: nytimes
|
||||
homepage: https://www.nytimes.com
|
||||
feed: https://rss.nytimes.com/services/xml/rss/nyt/Politics.xml
|
||||
- name: washington-post
|
||||
homepage: https://www.washingtonpost.com
|
||||
feed: https://feeds.washingtonpost.com/rss/politics
|
||||
- name: cnn
|
||||
homepage: https://www.cnn.com
|
||||
feed: http://rss.cnn.com/rss/cnn_allpolitics.rss
|
||||
- name: nbc-news
|
||||
homepage: https://www.nbcnews.com
|
||||
feed: https://feeds.nbcnews.com/nbcnews/public/politics
|
||||
- name: huffpost
|
||||
homepage: https://www.huffpost.com
|
||||
feed: https://www.huffpost.com/section/politics/feed
|
||||
- name: mother-jones
|
||||
homepage: https://www.motherjones.com
|
||||
feed: https://www.motherjones.com/politics/feed/
|
||||
- name: the-nation
|
||||
homepage: https://www.thenation.com
|
||||
feed: https://www.thenation.com/feed/?post_type=article
|
||||
# WSJ's politics feed is dead; the working feed is general news (not the
|
||||
# Opinion feed — news and opinion are rated separately by every incumbent
|
||||
# rater, and we collect news). Topic-mix tradeoff noted in METHODOLOGY D9.
|
||||
- name: wsj
|
||||
homepage: https://www.wsj.com
|
||||
feed: https://feeds.a.dj.com/rss/RSSWorldNews.xml
|
||||
- name: national-review
|
||||
homepage: https://www.nationalreview.com
|
||||
feed: https://www.nationalreview.com/feed/
|
||||
- name: washington-examiner
|
||||
homepage: https://www.washingtonexaminer.com
|
||||
feed: https://www.washingtonexaminer.com/feed
|
||||
- name: ny-post
|
||||
homepage: https://nypost.com
|
||||
feed: https://nypost.com/politics/feed/
|
||||
- name: fox-news
|
||||
homepage: https://www.foxnews.com
|
||||
feed: https://moxie.foxnews.com/google-publisher/politics.xml
|
||||
- name: daily-wire
|
||||
homepage: https://www.dailywire.com
|
||||
feed: https://www.dailywire.com/feeds/rss.xml
|
||||
- name: breitbart
|
||||
homepage: https://www.breitbart.com
|
||||
feed: https://feeds.feedburner.com/breitbart
|
||||
- name: newsmax
|
||||
homepage: https://www.newsmax.com
|
||||
feed: https://www.newsmax.com/rss/Politics/1/
|
||||
@@ -0,0 +1,55 @@
|
||||
# ADR-0001: Story-selection ideal points, anchored to congressional language
|
||||
|
||||
- **Status**: accepted
|
||||
- **Date**: 2026-07-10
|
||||
- **Supersedes**: none
|
||||
|
||||
## Decision
|
||||
|
||||
tiltmeter v0.1 measures outlet political lean from **story selection**: outlets are
|
||||
placed on a latent axis by correspondence analysis of the outlet×story coverage
|
||||
matrix (ideal-point estimation — outlets as legislators, coverage decisions as
|
||||
votes). The axis is **oriented by an external political reference** — similarity of
|
||||
each pole to Democratic vs. Republican language in recent Congressional Record
|
||||
speeches — so no outlet's lean is assumed anywhere in the pipeline. Scores are
|
||||
per-window time series with bootstrap confidence intervals. No editorial or LLM
|
||||
judgment exists in the scoring path.
|
||||
|
||||
## Rationale (sources)
|
||||
|
||||
- Selection is a real, strong bias channel: D'Alessio & Allen 2000 (taxonomy);
|
||||
Budak, Goel & Rao 2016 (bias operates largely via issue selection); Rönnback,
|
||||
Emmery & Brighton 2025 (coverage features most informative at outlet level).
|
||||
- The estimation tradition is proven: Poole & Rosenthal 1985 (ideal points);
|
||||
Slapin & Proksch 2008 (unsupervised political scaling from text).
|
||||
- Anchoring to politicians keeps the reference frame factual and the validation
|
||||
non-circular: Gentzkow & Shapiro 2010; DW-NOMINATE (voteview.com).
|
||||
- No judge in the loop: LLMs measurably lean (Rozado 2024; Santurkar et al. 2023);
|
||||
human panels are the incumbents' admission of the same problem.
|
||||
|
||||
Full citations: [docs/research.md](../research.md) §6.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Declared anchor outlets** ("Mother Jones = left, Breitbart = right") — one
|
||||
editorial bit, but precisely the judgment this project exists to remove. Rejected
|
||||
after methodology review, 2026-07-10.
|
||||
2. **Language-similarity scoring as the primary signal** (Gentzkow–Shapiro style) —
|
||||
deferred to v0.2 (S2); requires the Gentzkow, Shapiro & Taddy 2019 estimator to
|
||||
avoid finite-sample bias. In v0.1 congressional language is used only to orient
|
||||
the axis sign.
|
||||
3. **Audience-based scoring** — platform data access is gated; dead end since ~2018.
|
||||
4. **LLM-as-judge** — unreproducible across model versions and lean-contaminated;
|
||||
rejected on principle (METHODOLOGY.md D8).
|
||||
5. **Human panels with published worksheets** — transparent but unscalable and
|
||||
governance-fragile; this is the incumbents' territory anyway.
|
||||
|
||||
## Failure modes / risks accepted
|
||||
|
||||
- The first principal axis may capture topic mix rather than politics; mitigated by
|
||||
politics-section feeds and published evidence pages; checked by the D7 validation
|
||||
gate (Spearman ρ ≥ 0.7 vs. AllSides and Ad Fontes orderings).
|
||||
- 20 outlets over a 2–4 week window may yield an unstable axis; bootstrap CIs make
|
||||
the instability visible rather than hidden.
|
||||
- The congressional reference frame defines "center" as the US two-party midpoint;
|
||||
drift or positions outside that frame are invisible. Stated in every release.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Glossary
|
||||
|
||||
Plain definitions of every technical term used in this project's docs. If you
|
||||
hit a term anywhere in tiltmeter that isn't here, that's a bug — file it.
|
||||
|
||||
### RSS feed
|
||||
A machine-readable list an outlet publishes of its latest articles: headline,
|
||||
link, timestamp. It's how tiltmeter knows what each outlet published without
|
||||
scraping their whole site.
|
||||
|
||||
### Corpus
|
||||
The full collection of articles tiltmeter has gathered. Scores are always
|
||||
computed from a stated slice of it, never from "the web" loosely.
|
||||
|
||||
### Snapshot
|
||||
A frozen slice of the corpus: all articles from a stated time window, pinned
|
||||
by their fingerprints. Ratings are computed from snapshots so results can be
|
||||
checked and re-run later.
|
||||
|
||||
### Fingerprint (hash)
|
||||
A short code (SHA-256) computed from an article's exact text. Change one
|
||||
character of the article and the code changes completely. Fingerprints prove
|
||||
that a rating was computed from exactly these articles and no others.
|
||||
|
||||
### Manifest
|
||||
The published list of what's in a snapshot: each article's link, headline,
|
||||
outlet, timestamp, and fingerprint — but not its full text (which stays with
|
||||
the outlet). Anyone can re-download the articles and verify the fingerprints.
|
||||
|
||||
### Embedding
|
||||
A way of turning a piece of text into a list of numbers so that similar texts
|
||||
get similar numbers. tiltmeter uses embeddings only to spot when two articles
|
||||
cover the same story — never to judge them.
|
||||
|
||||
### Clustering
|
||||
Automatically grouping similar things. Here: grouping all outlets' articles
|
||||
about the same news event into one "story."
|
||||
|
||||
### Story cluster
|
||||
One news event and all the articles about it, across outlets. The unit of
|
||||
tiltmeter's analysis: for each story, which outlets covered it?
|
||||
|
||||
### Coverage matrix
|
||||
A big grid: one row per outlet, one column per story, marking who covered
|
||||
what. All of tiltmeter's scoring reads off this grid.
|
||||
|
||||
### Correspondence analysis
|
||||
A standard statistical method that turns a grid like the coverage matrix into
|
||||
a map: things with similar patterns end up near each other. The "seating
|
||||
chart" in the plain-language docs.
|
||||
|
||||
### Principal axis
|
||||
The single strongest direction of difference in that map — the line along
|
||||
which outlets' coverage choices differ most. tiltmeter checks whether that
|
||||
line matches political lean, rather than assuming it.
|
||||
|
||||
### Anchor
|
||||
The external reference that tells the map which end is politically left and
|
||||
which is right. tiltmeter's anchor is language from congressional speeches —
|
||||
public records, not anyone's opinion of an outlet.
|
||||
|
||||
### DW-NOMINATE
|
||||
A long-running political science measure that places members of Congress on
|
||||
a left–right scale using their actual voting records. Public data, published
|
||||
at voteview.com.
|
||||
|
||||
### Bootstrap
|
||||
A standard way to measure how sure we are of a number: recompute it many
|
||||
times on random re-samples of the data and watch how much it wobbles.
|
||||
|
||||
### Confidence interval
|
||||
The honest error range around a score, produced by the bootstrap. "−0.3 ± 0.1"
|
||||
means: don't read anything into differences smaller than that.
|
||||
|
||||
### Spearman rank correlation
|
||||
A statistic (written ρ, "rho") measuring how similarly two lists rank the same
|
||||
items, from −1 to +1. We use it to compare our outlet ordering with the
|
||||
commercial raters' ordering.
|
||||
|
||||
### Sensitivity analysis
|
||||
Re-running the pipeline with the judgment-call settings changed (time window,
|
||||
grouping threshold) and publishing how much the scores move. If a small
|
||||
setting change swings the results, that's disclosed, not hidden.
|
||||
|
||||
### Tunable
|
||||
A pipeline setting that is a practical judgment call rather than something the
|
||||
research literature dictates. Every tunable is labeled as such in
|
||||
METHODOLOGY.md and covered by the sensitivity analysis.
|
||||
|
||||
### ADR (decision record)
|
||||
A short numbered document recording one methodology decision: what was
|
||||
decided, why, what the alternatives were, and what could go wrong. Decisions
|
||||
are never edited in place — a new record supersedes an old one, so the
|
||||
method's history stays visible.
|
||||
|
||||
### SemVer (semantic versioning)
|
||||
Version numbers with meaning (MAJOR.MINOR.PATCH). Any change that could alter
|
||||
published ratings bumps the version, so every ratings file names the exact
|
||||
method that produced it.
|
||||
|
||||
### Reproducibility
|
||||
The property that anyone can re-run the pipeline on the same snapshot and get
|
||||
the same output, byte for byte. The project's core promise.
|
||||
|
||||
### Ideal-point estimation
|
||||
The political science technique of placing actors on a scale from their
|
||||
observed choices — classically, placing legislators by their votes. tiltmeter
|
||||
places outlets by their coverage choices: outlets are the legislators, stories
|
||||
are the votes.
|
||||
|
||||
### Lede
|
||||
The opening sentences of a news article, summarizing the story. tiltmeter
|
||||
embeds headline + lede to group articles into story clusters.
|
||||
@@ -0,0 +1,79 @@
|
||||
# How tiltmeter works
|
||||
|
||||
This page explains the whole system in plain language. No math. Every claim
|
||||
here links to the technical spec, [METHODOLOGY.md](../METHODOLOGY.md), which
|
||||
has the full details and sources. Words in *italics* are defined in the
|
||||
[glossary](glossary.md).
|
||||
|
||||
## The problem
|
||||
|
||||
Some news outlets lean left. Some lean right. Companies already sell ratings
|
||||
that say which is which. But those ratings come from panels of people voting
|
||||
behind closed doors. You cannot check their work. You just have to trust them.
|
||||
|
||||
tiltmeter is different. It is a measuring tool. Anyone can run it, check every
|
||||
step, and get the same numbers. If you think a rating is wrong, you can look
|
||||
at the exact news stories behind it and see for yourself.
|
||||
|
||||
## The idea: a seating chart for the news
|
||||
|
||||
Picture every big news event of the month as a party. Not every outlet shows
|
||||
up to every party. Each outlet picks which stories it covers. Those picks are
|
||||
choices, and choices reveal leanings.
|
||||
|
||||
tiltmeter watches those choices. Outlets that keep covering the same kinds of
|
||||
stories get seated near each other on a map. Outlets that make very different
|
||||
picks sit far apart. That map is built from the outlets' own behavior. Nobody
|
||||
votes on it. ([D3](../METHODOLOGY.md#d3-the-v1-signal-is-story-selection-framed-as-ideal-point-estimation))
|
||||
|
||||
One question is left: which side of the map is "left" and which is "right"?
|
||||
We do not decide that by opinion either. We compare the language of each side
|
||||
of the map to speeches from Congress. One pole will sound more like Democrats.
|
||||
The other will sound more like Republicans. Party membership is a public fact,
|
||||
not a rating. So the whole map is anchored to public records. ([D5](../METHODOLOGY.md#d5-the-axis-is-oriented-by-congressional-language-not-by-assumed-outlet-leans))
|
||||
|
||||
## The steps
|
||||
|
||||
1. **Collect.** Several times a day, tiltmeter reads the public article feeds
|
||||
of 20 US outlets, from far left to far right. It saves each new article
|
||||
with a *fingerprint* — a code that proves the text has not been changed.
|
||||
([D9](../METHODOLOGY.md#d9-the-corpus-is-politics-section-rss-stored-locally-published-as-manifests))
|
||||
2. **Group.** Articles about the same event are grouped into one story. Now we
|
||||
can see which outlets covered each story and which stayed silent.
|
||||
3. **Map.** The pattern of "covered it / skipped it" choices places each
|
||||
outlet on the seating chart.
|
||||
4. **Label the sides.** Speeches from Congress tell us which end of the chart
|
||||
is left and which is right.
|
||||
5. **Score.** Each outlet gets a number from −1 (left) to +1 (right), plus an
|
||||
honest error range. Lean can drift, so every score has a date. There are no
|
||||
permanent labels. ([D6](../METHODOLOGY.md#d6-scores-are-time-series-with-uncertainty-never-permanent-labels))
|
||||
|
||||
## How we check ourselves
|
||||
|
||||
We compare our numbers to the big commercial ratings. If our order of outlets
|
||||
is close to theirs, the method works. If not, we fix the method — in public,
|
||||
with a written record of what changed and why. One thing worth knowing: the
|
||||
commercial raters often disagree with *each other*. So matching them exactly
|
||||
is not even the goal. ([D7](../METHODOLOGY.md#d7-validation-against-incumbent-raters-is-a-gate-with-published-sensitivity))
|
||||
|
||||
We also publish "what if" checks. What if we group stories a bit differently?
|
||||
What if we use a different time window? If small choices like these swing the
|
||||
scores a lot, you deserve to know. ([D7](../METHODOLOGY.md#d7-validation-against-incumbent-raters-is-a-gate-with-published-sensitivity))
|
||||
|
||||
## What tiltmeter does not do
|
||||
|
||||
- It does not rate truth or quality. A score near zero does not mean "good".
|
||||
It means "picks stories like the middle of Congress talks". ([D2](../METHODOLOGY.md#d2-version-1-rates-political-lean-only))
|
||||
- It does not use AI models to judge the news. Studies show those models have
|
||||
leans of their own. ([D8](../METHODOLOGY.md#d8-no-llm-judgment-in-the-scoring-path))
|
||||
- It does not let anyone — including us — nudge a rating by hand. There is no
|
||||
hand. ([D1](../METHODOLOGY.md#d1-ratings-come-from-a-reproducible-pipeline-not-editorial-judgment))
|
||||
|
||||
## Check our work
|
||||
|
||||
Every published score links to an evidence page: the real headlines that drove
|
||||
it, the outlets that sat nearest, the error range, and the exact data batch
|
||||
used. And anyone can rerun the whole thing with three commands and get the
|
||||
same file, byte for byte. ([D10](../METHODOLOGY.md#d10-every-number-is-traceable-to-evidence-a-layman-can-read))
|
||||
|
||||
That is the whole point. Do not trust us. Check.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Outlets collected
|
||||
|
||||
<!-- GENERATED FILE — do not edit by hand.
|
||||
Source: config/outlets.yaml
|
||||
Regenerate: uv run python scripts/gen_outlets_doc.py -->
|
||||
|
||||
The outlets tiltmeter currently collects from, straight from
|
||||
[`config/outlets.yaml`](../config/outlets.yaml). Selection criteria and the
|
||||
swap policy for dead feeds are documented in
|
||||
[METHODOLOGY.md, decision D4](../METHODOLOGY.md#d4-twenty-outlets-deliberately-spread-chosen-once-and-openly);
|
||||
individual swaps are recorded in the [CHANGELOG](../CHANGELOG.md).
|
||||
|
||||
| Outlet | Homepage | Feed |
|
||||
|---|---|---|
|
||||
| axios | <https://www.axios.com> | <https://api.axios.com/feed/> |
|
||||
| breitbart | <https://www.breitbart.com> | <https://feeds.feedburner.com/breitbart> |
|
||||
| cnn | <https://www.cnn.com> | <http://rss.cnn.com/rss/cnn_allpolitics.rss> |
|
||||
| cs-monitor | <https://www.csmonitor.com> | <https://rss.csmonitor.com/feeds/politics> |
|
||||
| daily-wire | <https://www.dailywire.com> | <https://www.dailywire.com/feeds/rss.xml> |
|
||||
| fox-news | <https://www.foxnews.com> | <https://moxie.foxnews.com/google-publisher/politics.xml> |
|
||||
| huffpost | <https://www.huffpost.com> | <https://www.huffpost.com/section/politics/feed> |
|
||||
| mother-jones | <https://www.motherjones.com> | <https://www.motherjones.com/politics/feed/> |
|
||||
| national-review | <https://www.nationalreview.com> | <https://www.nationalreview.com/feed/> |
|
||||
| nbc-news | <https://www.nbcnews.com> | <https://feeds.nbcnews.com/nbcnews/public/politics> |
|
||||
| newsmax | <https://www.newsmax.com> | <https://www.newsmax.com/rss/Politics/1/> |
|
||||
| npr | <https://www.npr.org> | <https://feeds.npr.org/1014/rss.xml> |
|
||||
| ny-post | <https://nypost.com> | <https://nypost.com/politics/feed/> |
|
||||
| nytimes | <https://www.nytimes.com> | <https://rss.nytimes.com/services/xml/rss/nyt/Politics.xml> |
|
||||
| pbs-newshour | <https://www.pbs.org/newshour> | <https://www.pbs.org/newshour/feeds/rss/politics> |
|
||||
| the-hill | <https://thehill.com> | <https://thehill.com/homenews/feed/> |
|
||||
| the-nation | <https://www.thenation.com> | <https://www.thenation.com/feed/?post_type=article> |
|
||||
| washington-examiner | <https://www.washingtonexaminer.com> | <https://www.washingtonexaminer.com/feed> |
|
||||
| washington-post | <https://www.washingtonpost.com> | <https://feeds.washingtonpost.com/rss/politics> |
|
||||
| wsj | <https://www.wsj.com> | <https://feeds.a.dj.com/rss/RSSWorldNews.xml> |
|
||||
|
||||
Total: 20 outlets.
|
||||
@@ -0,0 +1,158 @@
|
||||
# Research Foundations — Measuring Media Lean
|
||||
|
||||
tiltmeter descends from two research traditions: the **commercial rating industry**
|
||||
(AllSides, Ad Fontes, MBFC — structured human judgment behind closed data) and four
|
||||
decades of **academic measurement** of media bias (open methods, mostly one-off
|
||||
studies that never became living datasets). This document surveys both, extracts what
|
||||
is and is not measurable, and records the design implications. It informs
|
||||
[METHODOLOGY.md](../METHODOLOGY.md); where the two disagree, reconcile deliberately.
|
||||
|
||||
All venues verified 2026-07-10. Links in §6.
|
||||
|
||||
---
|
||||
|
||||
## 1. Lineage
|
||||
|
||||
### 1.a The rating industry: balanced panels, closed data
|
||||
|
||||
- **AllSides** (2012–) — Blind Bias Surveys™: cross-spectrum respondents rate content
|
||||
with branding stripped. Editorial reviews: multipartisan panels of ≥6 (or trios)
|
||||
from left/center/right deliberate to consensus. Output: 5-point scale plus a
|
||||
numeric meter. Community feedback as a secondary signal.
|
||||
- **Ad Fontes Media** (2017–) — trios of trained analysts (one self-identified left,
|
||||
center, right each; ≥20 hours training) score individual articles on an 8-part
|
||||
rubric (veracity, expression, headline/graphics, political position, language,
|
||||
comparison…). Article bias −42..+42, aggregated to outlet scores. Methodology
|
||||
white paper published 2021.
|
||||
- **Media Bias/Fact Check** (2015–) — small team applying published criteria
|
||||
(wording, sourcing, story choice, endorsements). Widest coverage, least rigorous;
|
||||
ironically the most-used ground truth in academic work because it covers the most
|
||||
sources.
|
||||
|
||||
The industry's shared premise: bias in the rater is unavoidable, so *balance the
|
||||
raters*. The panels are the method. None of it is reproducible — you cannot rerun a
|
||||
panel — and the per-rating evidence is not published. That gap, not the ratings
|
||||
themselves, is what tiltmeter fills: **remove the panel instead of balancing it.**
|
||||
|
||||
### 1.b The academic tradition: open methods, dead datasets
|
||||
|
||||
Economists and political scientists have measured outlet lean reproducibly since the
|
||||
1980s, but almost every study was a one-off: a method, a paper, a static replication
|
||||
archive. Nothing compounds. The methods, however, are exactly what a living open
|
||||
dataset needs (see §2).
|
||||
|
||||
A partial exception and the closest living relative: **Penn's Media Bias Detector**
|
||||
(CHI 2025) — LLM-annotated articles aggregated into dynamic per-publisher views,
|
||||
deliberately refusing static outlet labels. It trades reproducibility for coverage by
|
||||
putting an LLM in the judgment loop (see METHODOLOGY.md D8 for why we refuse that
|
||||
trade).
|
||||
|
||||
### 1.c Why re-run the experiment now
|
||||
|
||||
Three things changed. Sentence embeddings made cross-outlet story clustering cheap
|
||||
and good — the expensive part of selection-bias measurement is now a commodity.
|
||||
Congressional text and ideology data (govinfo.gov bulk data, voteview.com) are
|
||||
freely downloadable, so the classic anchoring strategy no longer requires
|
||||
institutional data access. And the incumbent raters have demonstrated durable demand
|
||||
for outlet-lean ratings while leaving methodology transparency entirely unserved.
|
||||
|
||||
## 2. Measurement traditions
|
||||
|
||||
The canonical survey — **Puglisi & Snyder, "Empirical Studies of Media Bias"
|
||||
(Handbook of Media Economics 1B, 2015)** — divides implicit-bias measures into three
|
||||
families. They map onto tiltmeter's roadmap directly:
|
||||
|
||||
| Family (Puglisi & Snyder) | Canonical works | tiltmeter |
|
||||
|---|---|---|
|
||||
| Intensity of coverage (what gets covered, how much) | D'Alessio & Allen 2000 (gatekeeping); Larcinese, Puglisi & Snyder 2011; Budak, Goel & Rao 2016 | **S1, v0.1** — story-selection ideal points |
|
||||
| Comparison with other political actors | Groseclose & Milyo 2005 (contested); **Gentzkow & Shapiro 2010**; Gentzkow, Shapiro & Taddy 2019 | **S2, v0.2** — congressional-language similarity; v0.1 uses it for axis orientation only |
|
||||
| Tone | sentiment/valence studies; BABE dataset (EMNLP Findings 2021) for sentence-level bias | future work |
|
||||
|
||||
Supporting traditions:
|
||||
|
||||
- **Ideal-point estimation** (Poole & Rosenthal 1985 → DW-NOMINATE): place actors on
|
||||
a latent dimension from their observed choices. tiltmeter's outlet×story
|
||||
correspondence analysis is roll-call analysis where outlets are legislators and
|
||||
coverage decisions are votes. Ho & Quinn (2008) already placed outlets by ideal
|
||||
points, using editorial positions.
|
||||
- **Unsupervised text scaling** (Slapin & Proksch 2008, "Wordfish"; supervised
|
||||
variant: Laver, Benoit & Garry 2003, "Wordscores"): latent political dimensions
|
||||
recovered from text without labeled outlets. S1's closest methodological cousin.
|
||||
- **Audience-based scores** (Bakshy, Messing & Adamic 2015, Science; Barberá 2015):
|
||||
infer outlet lean from who consumes/shares it. Methodologically attractive,
|
||||
practically dead — platform data access is gated. Included here because its logic
|
||||
(lean is relational, anchored in an external population) survives in our
|
||||
congressional anchoring.
|
||||
- **Theory of selection** — why S1 measures something real: White 1950
|
||||
(gatekeeping), Galtung & Ruge 1965 (news values), McCombs & Shaw 1972
|
||||
(agenda-setting: media tell you what to think *about*), Entman 1993 (framing).
|
||||
Political economy — why outlets lean at all: Mullainathan & Shleifer 2005 (AER,
|
||||
demand-side); Gentzkow & Shapiro 2006 (JPE, reputation); DellaVigna & Kaplan 2007
|
||||
(QJE, the Fox News effect — why any of this matters).
|
||||
|
||||
## 3. What is and is not measurable
|
||||
|
||||
Lessons the literature forces on us:
|
||||
|
||||
1. **Lean is relational.** Every credible measure anchors to an external population
|
||||
with independently known politics — Congress (roll-call votes), voters, audiences.
|
||||
A "view from nowhere" rating is an unstated anchor, not a neutral one.
|
||||
2. **The method chosen shapes the structure found.** MBFC labels vs. audience-derived
|
||||
labels agree only 46%; two human-annotated label sources agree 57% (Rönnback,
|
||||
Emmery & Brighton 2025, PLOS One). Measurement choice is itself a finding — which
|
||||
is the argument for publishing the method, not just the numbers.
|
||||
3. **Selection carries signal.** Budak, Goel & Rao (2016) found outlet bias operates
|
||||
largely through issue selection rather than explicit slant; Rönnback et al. (2025)
|
||||
found coverage features the most informative outlet-level predictors. The cheapest
|
||||
honest signal is *what outlets choose to cover*.
|
||||
4. **Judges have lean too.** LLMs measurably lean (Rozado 2024, PLOS One; Santurkar
|
||||
et al. 2023, ICML); so do human panels — the industry's entire architecture is an
|
||||
admission of that. Any judgment component relocates the bias problem.
|
||||
5. **Perfect agreement with any one rater would be a bug.** Given 46–57% inter-rater
|
||||
agreement, a method that matched AllSides at ρ ≈ 1 would have learned AllSides,
|
||||
not lean.
|
||||
|
||||
## 4. Design implications
|
||||
|
||||
| Lesson (§3) | Design response (METHODOLOGY.md) |
|
||||
|---|---|
|
||||
| Lean is relational | D5: explicit anchor — congressional party discourse; frame stated in every release |
|
||||
| Method shapes findings | D1: open reproducible pipeline; D7: published sensitivity sweeps |
|
||||
| Selection carries signal | D3: story-selection ideal points as the v1 signal |
|
||||
| Judges have lean | D1: no editorial judgment; D8: no LLM judgment |
|
||||
| Perfect agreement = overfit | D7: validation gate at ρ ≥ 0.7, not ρ → 1 |
|
||||
| Raters disagree; lean drifts | D6: time series with confidence intervals, never static labels |
|
||||
| One-off studies don't compound | Corpus manifests + versioned releases: a living dataset others can extend |
|
||||
|
||||
## 5. Reading list (in argument order)
|
||||
|
||||
1. Puglisi & Snyder 2015 — the map of the field; read first.
|
||||
2. D'Alessio & Allen 2000 — the bias taxonomy (gatekeeping / coverage / statement).
|
||||
3. McCombs & Shaw 1972 — agenda-setting; the theoretical charter for selection bias.
|
||||
4. Poole & Rosenthal 1985 — ideal points; the estimation tradition S1 sits in.
|
||||
5. Slapin & Proksch 2008 — unsupervised political scaling from text.
|
||||
6. Gentzkow & Shapiro 2010 — congressional-language anchoring; our D5 and future S2.
|
||||
7. Gentzkow, Shapiro & Taddy 2019 — read before implementing S2; the naive estimator
|
||||
is severely biased in finite samples.
|
||||
8. Budak, Goel & Rao 2016 — selection is where the bias lives.
|
||||
9. Grimmer & Stewart 2013 — validation discipline for all of the above.
|
||||
10. Rönnback, Emmery & Brighton 2025 — the modern end-to-end attempt; ground-truth
|
||||
disagreement numbers.
|
||||
11. Rozado 2024; Santurkar et al. 2023 — why no LLM judges.
|
||||
12. Spinde et al., ACM Computing Surveys 2023 — the NLP field map (3,140 papers).
|
||||
|
||||
## 6. Sources
|
||||
|
||||
- Puglisi & Snyder 2015: <https://www.sciencedirect.com/science/article/abs/pii/B9780444636850000152>
|
||||
- Gentzkow & Shapiro 2010: <https://onlinelibrary.wiley.com/doi/abs/10.3982/ECTA7195>
|
||||
- Gentzkow, Shapiro & Taddy 2019: <https://onlinelibrary.wiley.com/doi/abs/10.3982/ECTA16566>
|
||||
- Budak, Goel & Rao 2016: <https://doi.org/10.1093/poq/nfw007>
|
||||
- Rönnback, Emmery & Brighton 2025: <https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0321418>
|
||||
- Media Bias Detector (CHI 2025): <https://dl.acm.org/doi/10.1145/3706598.3713716>
|
||||
- Rozado 2024: <https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0306621>
|
||||
- Spinde et al. 2023 (ACM CS): <https://arxiv.org/abs/2312.16148>
|
||||
- BABE (Findings of EMNLP 2021): <https://aclanthology.org/2021.findings-emnlp.101/>
|
||||
- AllSides methodology: <https://www.allsides.com/about/media-bias-rating-methods>
|
||||
- Ad Fontes white paper: <https://adfontesmedia.com/white-paper-2021/>
|
||||
- DW-NOMINATE / voteview: <https://voteview.com>
|
||||
- Congressional Record bulk data: <https://www.govinfo.gov/bulkdata>
|
||||
@@ -0,0 +1,37 @@
|
||||
[project]
|
||||
name = "tiltmeter"
|
||||
version = "0.1.0"
|
||||
description = "Auditable, reproducible political-lean ratings for news outlets"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "flan", email = "flan@arch.fyi" }]
|
||||
dependencies = [
|
||||
"feedparser>=6.0",
|
||||
"trafilatura>=1.12",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
tiltmeter = "tiltmeter.cli:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"ruff>=0.8",
|
||||
"textstat>=0.7",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/tiltmeter"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py313"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Docs move with the code, enforced: if pipeline code or config changed in a
|
||||
# commit range but no documentation did, fail. Used by CI (ci.yml) and usable
|
||||
# locally: scripts/check_docs_sync.sh <base>..<head>
|
||||
set -euo pipefail
|
||||
|
||||
range="${1:?usage: check_docs_sync.sh <base>..<head>}"
|
||||
|
||||
changed="$(git diff --name-only "$range")"
|
||||
|
||||
code_changed="$(echo "$changed" | grep -E '^(src/|config/)' || true)"
|
||||
docs_changed="$(echo "$changed" | grep -E '^(docs/|METHODOLOGY\.md|CHANGELOG\.md|README\.md)' || true)"
|
||||
|
||||
if [[ -n "$code_changed" && -z "$docs_changed" ]]; then
|
||||
echo "FAIL: code/config changed without any docs change in $range"
|
||||
echo
|
||||
echo "Changed code/config files:"
|
||||
echo "$code_changed" | sed 's/^/ /'
|
||||
echo
|
||||
echo "Update the relevant docs (METHODOLOGY.md decision block, CHANGELOG.md"
|
||||
echo "entry, docs/*) in the same change. Docs are part of the product here."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "docs-sync ok"
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Keep docs/outlets.md automatically in sync with config/outlets.yaml.
|
||||
|
||||
The outlet table in the docs is generated, never hand-edited: this script
|
||||
renders it from the config, and tests/test_docs.py fails CI whenever the
|
||||
committed file differs from what the config would generate. To update docs
|
||||
after changing outlets: uv run python scripts/gen_outlets_doc.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CONFIG = ROOT / "config" / "outlets.yaml"
|
||||
TARGET = ROOT / "docs" / "outlets.md"
|
||||
|
||||
HEADER = """\
|
||||
# Outlets collected
|
||||
|
||||
<!-- GENERATED FILE — do not edit by hand.
|
||||
Source: config/outlets.yaml
|
||||
Regenerate: uv run python scripts/gen_outlets_doc.py -->
|
||||
|
||||
The outlets tiltmeter currently collects from, straight from
|
||||
[`config/outlets.yaml`](../config/outlets.yaml). Selection criteria and the
|
||||
swap policy for dead feeds are documented in
|
||||
[METHODOLOGY.md, decision D4](../METHODOLOGY.md#d4-twenty-outlets-deliberately-spread-chosen-once-and-openly);
|
||||
individual swaps are recorded in the [CHANGELOG](../CHANGELOG.md).
|
||||
|
||||
| Outlet | Homepage | Feed |
|
||||
|---|---|---|
|
||||
"""
|
||||
|
||||
|
||||
def render() -> str:
|
||||
with open(CONFIG) as f:
|
||||
outlets = yaml.safe_load(f)["outlets"]
|
||||
rows = [
|
||||
f"| {o['name']} | <{o['homepage']}> | <{o['feed']}> |"
|
||||
for o in sorted(outlets, key=lambda o: o["name"])
|
||||
]
|
||||
return HEADER + "\n".join(rows) + f"\n\nTotal: {len(outlets)} outlets.\n"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
content = render()
|
||||
if "--check" in sys.argv:
|
||||
current = TARGET.read_text() if TARGET.exists() else ""
|
||||
if current != content:
|
||||
print("docs/outlets.md is stale; run: uv run python scripts/gen_outlets_doc.py")
|
||||
sys.exit(1)
|
||||
print("docs/outlets.md is in sync")
|
||||
else:
|
||||
TARGET.write_text(content)
|
||||
print(f"wrote {TARGET}")
|
||||
@@ -0,0 +1,7 @@
|
||||
"""tiltmeter: auditable, reproducible political-lean ratings for news outlets.
|
||||
|
||||
Every module in this package opens with a plain-language docstring stating the
|
||||
question it answers. See docs/how-it-works.md for the full plain-language tour.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,66 @@
|
||||
"""How does a person drive this thing?
|
||||
|
||||
The command-line interface. Two commands exist so far:
|
||||
|
||||
tiltmeter ingest — poll every outlet's feed once and store new articles
|
||||
tiltmeter status — show how many articles we hold per outlet
|
||||
|
||||
Later milestones add: snapshot, run, validate, report.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from tiltmeter import db, ingest
|
||||
|
||||
DEFAULT_CONFIG = "config/outlets.yaml"
|
||||
DEFAULT_DB = "data/tiltmeter.db"
|
||||
|
||||
|
||||
def cmd_ingest(args: argparse.Namespace) -> int:
|
||||
results = ingest.ingest_all(args.config, args.db, fetch_text=not args.no_text)
|
||||
failed = [r for r in results if "error" in r]
|
||||
for r in results:
|
||||
if "error" in r:
|
||||
print(f" FAIL {r['outlet']}: {r['error']}")
|
||||
else:
|
||||
print(f" ok {r['outlet']}: {r['seen']} in feed, {r['new']} new")
|
||||
return 1 if len(failed) == len(results) and results else 0
|
||||
|
||||
|
||||
def cmd_status(args: argparse.Namespace) -> int:
|
||||
conn = db.connect(args.db)
|
||||
rows = db.outlet_counts(conn)
|
||||
if not rows:
|
||||
print("no articles collected yet")
|
||||
return 0
|
||||
width = max(len(o) for o, _ in rows)
|
||||
for outlet, count in rows:
|
||||
print(f" {outlet:<{width}} {count}")
|
||||
print(f" {'TOTAL':<{width}} {sum(c for _, c in rows)}")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(prog="tiltmeter", description=__doc__)
|
||||
parser.add_argument("--db", default=DEFAULT_DB, help="path to the article database")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_ingest = sub.add_parser("ingest", help="poll all outlet feeds once")
|
||||
p_ingest.add_argument("--config", default=DEFAULT_CONFIG)
|
||||
p_ingest.add_argument(
|
||||
"--no-text", action="store_true", help="skip full-text fetch (feed metadata only)"
|
||||
)
|
||||
p_ingest.set_defaults(func=cmd_ingest)
|
||||
|
||||
p_status = sub.add_parser("status", help="article counts per outlet")
|
||||
p_status.set_defaults(func=cmd_status)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Where do collected articles live?
|
||||
|
||||
One SQLite file holds every article we have ever collected: which outlet
|
||||
published it, when we saw it, its headline, its text, and a fingerprint (hash)
|
||||
of its content. The fingerprint is what later lets anyone verify that a
|
||||
published rating was computed from exactly these articles and no others.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
outlet TEXT NOT NULL,
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
published TEXT,
|
||||
fetched_at TEXT NOT NULL,
|
||||
summary TEXT,
|
||||
text TEXT,
|
||||
content_hash TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_outlet ON articles (outlet);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_fetched ON articles (fetched_at);
|
||||
"""
|
||||
|
||||
|
||||
def connect(db_path: str | Path) -> sqlite3.Connection:
|
||||
"""Open (creating if needed) the article database."""
|
||||
path = Path(db_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(path)
|
||||
conn.executescript(SCHEMA)
|
||||
return conn
|
||||
|
||||
|
||||
def content_hash(title: str, text: str) -> str:
|
||||
"""Fingerprint an article's content.
|
||||
|
||||
SHA-256 over headline + body. Two articles with the same fingerprint have
|
||||
identical content; a snapshot manifest of fingerprints pins a corpus.
|
||||
"""
|
||||
return hashlib.sha256((title + "\x1f" + text).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def have_url(conn: sqlite3.Connection, url: str) -> bool:
|
||||
"""Have we already collected this article? (Dedup key is the URL.)"""
|
||||
row = conn.execute("SELECT 1 FROM articles WHERE url = ? LIMIT 1", (url,)).fetchone()
|
||||
return row is not None
|
||||
|
||||
|
||||
def insert_article(
|
||||
conn: sqlite3.Connection,
|
||||
*,
|
||||
outlet: str,
|
||||
url: str,
|
||||
title: str,
|
||||
published: str | None,
|
||||
fetched_at: str,
|
||||
summary: str | None,
|
||||
text: str | None,
|
||||
) -> None:
|
||||
"""Store one newly collected article."""
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO articles"
|
||||
" (outlet, url, title, published, fetched_at, summary, text, content_hash)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(outlet, url, title, published, fetched_at, summary, text, content_hash(title, text or "")),
|
||||
)
|
||||
|
||||
|
||||
def outlet_counts(conn: sqlite3.Connection) -> list[tuple[str, int]]:
|
||||
"""How many articles do we have per outlet? (The health check for ingestion.)"""
|
||||
return conn.execute(
|
||||
"SELECT outlet, COUNT(*) FROM articles GROUP BY outlet ORDER BY outlet"
|
||||
).fetchall()
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Which articles did each outlet publish today?
|
||||
|
||||
This module polls each outlet's RSS feed (a machine-readable list of its
|
||||
latest articles), fetches the full text of any article we haven't seen
|
||||
before, and stores it. It never rates anything — it only collects, so that
|
||||
every later step works from the same recorded evidence.
|
||||
|
||||
Politeness rules: we identify ourselves with an honest User-Agent, fetch each
|
||||
article at most once ever (URLs are deduplicated), and pause between fetches.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import feedparser
|
||||
import trafilatura
|
||||
import yaml
|
||||
|
||||
from tiltmeter import db
|
||||
|
||||
log = logging.getLogger("tiltmeter.ingest")
|
||||
|
||||
USER_AGENT = "tiltmeter/0.1 (+https://github.com/sudolulo/tiltmeter; research corpus builder)"
|
||||
# Floor chosen to stay under WAF/rate-limit radar on a single domain; a block
|
||||
# would cost us corpus coverage, which is worth more than ingest speed.
|
||||
FETCH_DELAY_SECONDS = 0.2
|
||||
|
||||
|
||||
def load_outlets(config_path: str) -> list[dict]:
|
||||
"""Read the outlet list (name, feed URL) from config/outlets.yaml."""
|
||||
with open(config_path) as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
return cfg["outlets"]
|
||||
|
||||
|
||||
def fetch_article_text(url: str) -> str | None:
|
||||
"""Download one article page and extract its readable text."""
|
||||
html = trafilatura.fetch_url(url)
|
||||
if html is None:
|
||||
return None
|
||||
return trafilatura.extract(html, include_comments=False, include_tables=False)
|
||||
|
||||
|
||||
def ingest_outlet(conn, outlet: dict, *, fetch_text: bool = True) -> tuple[int, int]:
|
||||
"""Poll one outlet's feed; store unseen articles. Returns (seen, new)."""
|
||||
parsed = feedparser.parse(outlet["feed"], agent=USER_AGENT)
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
seen, new = 0, 0
|
||||
for entry in parsed.entries:
|
||||
url = entry.get("link")
|
||||
title = (entry.get("title") or "").strip()
|
||||
if not url or not title:
|
||||
continue
|
||||
seen += 1
|
||||
if db.have_url(conn, url):
|
||||
continue
|
||||
text = None
|
||||
if fetch_text:
|
||||
try:
|
||||
text = fetch_article_text(url)
|
||||
except Exception as exc: # noqa: BLE001 - one bad page must not stop the run
|
||||
log.warning("text fetch failed for %s: %s", url, exc)
|
||||
time.sleep(FETCH_DELAY_SECONDS)
|
||||
db.insert_article(
|
||||
conn,
|
||||
outlet=outlet["name"],
|
||||
url=url,
|
||||
title=title,
|
||||
published=entry.get("published") or entry.get("updated"),
|
||||
fetched_at=now,
|
||||
summary=(entry.get("summary") or "").strip() or None,
|
||||
text=text,
|
||||
)
|
||||
new += 1
|
||||
conn.commit()
|
||||
return seen, new
|
||||
|
||||
|
||||
def ingest_all(config_path: str, db_path: str, *, fetch_text: bool = True) -> list[dict]:
|
||||
"""Poll every configured outlet once. Returns a per-outlet result report."""
|
||||
conn = db.connect(db_path)
|
||||
results = []
|
||||
for outlet in load_outlets(config_path):
|
||||
try:
|
||||
seen, new = ingest_outlet(conn, outlet, fetch_text=fetch_text)
|
||||
results.append({"outlet": outlet["name"], "seen": seen, "new": new})
|
||||
log.info("%s: %d entries in feed, %d new", outlet["name"], seen, new)
|
||||
except Exception as exc: # noqa: BLE001 - one bad feed must not stop the run
|
||||
results.append({"outlet": outlet["name"], "error": str(exc)})
|
||||
log.error("%s: feed failed: %s", outlet["name"], exc)
|
||||
conn.close()
|
||||
return results
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Do the docs keep the promises the README makes?
|
||||
|
||||
These tests are the enforcement arm of the auditability contract:
|
||||
- the plain-language walkthrough stays at or below a grade-10 reading level
|
||||
- every required jargon term has a glossary entry, and no glossary entry is dead
|
||||
- every METHODOLOGY decision block carries its four mandatory fields
|
||||
- decision records (ADRs) are sequential and complete
|
||||
- the generated outlets table matches the config it is generated from
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import textstat
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
HOW_IT_WORKS = ROOT / "docs" / "how-it-works.md"
|
||||
GLOSSARY = ROOT / "docs" / "glossary.md"
|
||||
METHODOLOGY = ROOT / "METHODOLOGY.md"
|
||||
DECISIONS = ROOT / "docs" / "decisions"
|
||||
|
||||
MAX_GRADE = 10.0
|
||||
|
||||
|
||||
def strip_markdown(text: str) -> str:
|
||||
text = re.sub(r"\]\([^)]*\)", "]", text) # link targets are not prose
|
||||
text = re.sub(r"<https?://[^>]*>", "", text)
|
||||
return re.sub(r"[#*`>\[\]]", "", text)
|
||||
|
||||
|
||||
def test_how_it_works_readability():
|
||||
grade = textstat.flesch_kincaid_grade(strip_markdown(HOW_IT_WORKS.read_text()))
|
||||
assert grade <= MAX_GRADE, (
|
||||
f"docs/how-it-works.md reads at grade {grade:.1f}, above the promised "
|
||||
f"grade-{MAX_GRADE:.0f} ceiling. Shorten sentences, use simpler words."
|
||||
)
|
||||
|
||||
|
||||
# Jargon that project docs are allowed to use only because the glossary defines it.
|
||||
# Add a term here whenever a new one enters the docs.
|
||||
REQUIRED_GLOSSARY_TERMS = [
|
||||
"RSS feed",
|
||||
"Corpus",
|
||||
"Snapshot",
|
||||
"Fingerprint (hash)",
|
||||
"Manifest",
|
||||
"Embedding",
|
||||
"Clustering",
|
||||
"Story cluster",
|
||||
"Coverage matrix",
|
||||
"Correspondence analysis",
|
||||
"Principal axis",
|
||||
"Anchor",
|
||||
"DW-NOMINATE",
|
||||
"Bootstrap",
|
||||
"Confidence interval",
|
||||
"Spearman rank correlation",
|
||||
"Sensitivity analysis",
|
||||
"Tunable",
|
||||
"ADR (decision record)",
|
||||
"SemVer (semantic versioning)",
|
||||
"Reproducibility",
|
||||
"Ideal-point estimation",
|
||||
"Lede",
|
||||
]
|
||||
|
||||
|
||||
def glossary_terms() -> list[str]:
|
||||
return re.findall(r"^### (.+)$", GLOSSARY.read_text(), flags=re.MULTILINE)
|
||||
|
||||
|
||||
def test_glossary_covers_required_terms():
|
||||
missing = set(REQUIRED_GLOSSARY_TERMS) - set(glossary_terms())
|
||||
assert not missing, f"glossary is missing required terms: {sorted(missing)}"
|
||||
|
||||
|
||||
def test_glossary_has_no_dead_entries():
|
||||
"""Every glossary entry must actually be used somewhere in the docs."""
|
||||
docs_text = " ".join(
|
||||
p.read_text().lower()
|
||||
for p in [
|
||||
HOW_IT_WORKS,
|
||||
METHODOLOGY,
|
||||
ROOT / "README.md",
|
||||
ROOT / "CHANGELOG.md",
|
||||
ROOT / "docs" / "research.md",
|
||||
]
|
||||
)
|
||||
dead = []
|
||||
for term in glossary_terms():
|
||||
# match on the head word(s) before any parenthetical, e.g. "Fingerprint (hash)"
|
||||
head = re.sub(r"\s*\(.*\)$", "", term).lower()
|
||||
stem = head[:-1] if head.endswith("y") else head # reproducibility/reproducible
|
||||
if stem not in docs_text:
|
||||
dead.append(term)
|
||||
assert not dead, f"glossary entries never used in any doc: {dead}"
|
||||
|
||||
|
||||
DECISION_FIELDS = ["**Decision**", "**Rationale**", "**Alternatives considered**",
|
||||
"**Failure modes**"]
|
||||
|
||||
|
||||
def test_methodology_decision_blocks_complete():
|
||||
text = METHODOLOGY.read_text()
|
||||
blocks = re.split(r"^## D(\d+)\. ", text, flags=re.MULTILINE)
|
||||
assert len(blocks) > 1, "METHODOLOGY.md contains no decision blocks"
|
||||
numbers = [int(n) for n in blocks[1::2]]
|
||||
assert numbers == list(range(1, len(numbers) + 1)), (
|
||||
f"decision blocks must be numbered sequentially from D1, got {numbers}"
|
||||
)
|
||||
for number, body in zip(blocks[1::2], blocks[2::2]):
|
||||
missing = [f for f in DECISION_FIELDS if f not in body]
|
||||
assert not missing, f"METHODOLOGY.md D{number} is missing fields: {missing}"
|
||||
|
||||
|
||||
ADR_SECTIONS = ["## Decision", "## Rationale", "## Alternatives considered", "## Failure modes"]
|
||||
ADR_METADATA = ["**Status**", "**Date**", "**Supersedes**"]
|
||||
|
||||
|
||||
def test_adrs_sequential_and_complete():
|
||||
adrs = sorted(DECISIONS.glob("*.md"))
|
||||
assert adrs, "docs/decisions/ contains no decision records"
|
||||
numbers = []
|
||||
for path in adrs:
|
||||
m = re.match(r"^(\d{4})-[a-z0-9-]+\.md$", path.name)
|
||||
assert m, f"ADR filename must be NNNN-kebab-title.md, got {path.name}"
|
||||
numbers.append(int(m.group(1)))
|
||||
text = path.read_text()
|
||||
for section in ADR_SECTIONS:
|
||||
assert re.search(f"^{re.escape(section)}", text, flags=re.MULTILINE), (
|
||||
f"{path.name} is missing a section starting with '{section}'"
|
||||
)
|
||||
for field in ADR_METADATA:
|
||||
assert field in text, f"{path.name} is missing metadata field {field}"
|
||||
assert numbers == list(range(1, len(numbers) + 1)), (
|
||||
f"ADRs must be numbered sequentially from 0001, got {numbers}"
|
||||
)
|
||||
|
||||
|
||||
def test_outlets_doc_in_sync_with_config():
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"gen_outlets_doc", ROOT / "scripts" / "gen_outlets_doc.py"
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
committed = (ROOT / "docs" / "outlets.md").read_text()
|
||||
assert committed == mod.render(), (
|
||||
"docs/outlets.md is stale. Regenerate: uv run python scripts/gen_outlets_doc.py"
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Does the collector store, deduplicate, and fingerprint articles correctly?
|
||||
|
||||
Pure unit tests — no network. Feed parsing and article fetching are exercised
|
||||
by the live cron run; what must never regress silently is the storage
|
||||
contract: one row per URL, stable fingerprints, accurate per-outlet counts.
|
||||
"""
|
||||
|
||||
from tiltmeter import db
|
||||
|
||||
|
||||
def test_insert_and_dedup_by_url():
|
||||
conn = db.connect(":memory:")
|
||||
kwargs = dict(
|
||||
outlet="example",
|
||||
url="https://example.com/a",
|
||||
title="Headline",
|
||||
published=None,
|
||||
fetched_at="2026-07-10T00:00:00+00:00",
|
||||
summary=None,
|
||||
text="Body text.",
|
||||
)
|
||||
db.insert_article(conn, **kwargs)
|
||||
db.insert_article(conn, **kwargs) # same URL again: must not duplicate
|
||||
assert db.outlet_counts(conn) == [("example", 1)]
|
||||
assert db.have_url(conn, "https://example.com/a")
|
||||
assert not db.have_url(conn, "https://example.com/b")
|
||||
|
||||
|
||||
def test_content_hash_is_stable_and_content_sensitive():
|
||||
a = db.content_hash("Headline", "Body text.")
|
||||
assert a == db.content_hash("Headline", "Body text.") # deterministic
|
||||
assert a != db.content_hash("Headline", "Body text!") # any edit changes it
|
||||
assert a != db.content_hash("Headline2", "Body text.")
|
||||
# title/text boundary is unambiguous: moving a word across it changes the hash
|
||||
assert db.content_hash("A B", "C") != db.content_hash("A", "B C")
|
||||
|
||||
|
||||
def test_counts_group_by_outlet():
|
||||
conn = db.connect(":memory:")
|
||||
for i, outlet in enumerate(["left-times", "right-post", "left-times"]):
|
||||
db.insert_article(
|
||||
conn,
|
||||
outlet=outlet,
|
||||
url=f"https://example.com/{i}",
|
||||
title=f"T{i}",
|
||||
published=None,
|
||||
fetched_at="2026-07-10T00:00:00+00:00",
|
||||
summary=None,
|
||||
text="x",
|
||||
)
|
||||
assert db.outlet_counts(conn) == [("left-times", 2), ("right-post", 1)]
|
||||
@@ -0,0 +1,625 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.6.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "courlan"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "babel" },
|
||||
{ name = "tld" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/16/2a771612ee0b3acaa95ac21cc7e8a3319e815d6360f8ffc5987d1ce28499/courlan-1.4.0.tar.gz", hash = "sha256:fbbac7b7fcde2195ea08e707609503c81cf39c891e8d26cdb1fed4585782d63d", size = 208997, upload-time = "2026-06-01T17:30:17.306Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/38/ce65091ff20a16e06d17418c4353af5f56d3190821b1a06983c79ae79274/courlan-1.4.0-py3-none-any.whl", hash = "sha256:ad1dbdefd912ca7238d4607dc855df5df097f56bac175dd662c84eed3802f49e", size = 34193, upload-time = "2026-06-01T17:30:14.984Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dateparser"
|
||||
version = "1.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
{ name = "regex" },
|
||||
{ name = "tzlocal" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/7c/2e5dcf53909deddd0bf38cbe277ad9806be038276b1c6c436561b4d9b2e2/dateparser-1.4.1-py3-none-any.whl", hash = "sha256:f25d4e051a84be27a35bd297e3e1dc59ff78373701b89be352ba80372d22d0d0", size = 300503, upload-time = "2026-06-15T08:45:45.951Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feedparser"
|
||||
version = "6.0.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "sgmllib3k" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmldate"
|
||||
version = "1.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "dateparser" },
|
||||
{ name = "lxml" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/e7cf83e23d7b68105de8b874a8b36ba23b450d6f71388583e4ca3ce475ca/htmldate-1.10.0.tar.gz", hash = "sha256:a38df10772ab5d7dbb11896e3f6a852a8491fb1b0965465bc174e23fc2baae58", size = 44455, upload-time = "2026-06-01T17:43:53.437Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/17/d3356233c826c641f940983d9479eab27faec59d49f4070bc58e80fcc021/htmldate-1.10.0-py3-none-any.whl", hash = "sha256:9211dae35ab94147c8ed9e5fc2c9287a5cf31d2394cb7857e7f5dd814eb2aad6", size = 31561, upload-time = "2026-06-01T17:43:51.797Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "joblib"
|
||||
version = "1.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "justext"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml", extra = ["html-clean"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
html-clean = [
|
||||
{ name = "lxml-html-clean" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml-html-clean"
|
||||
version = "0.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/63/195dfdde380a84df309e3bccf4384b034b745dba43426886f7ae623b4fba/lxml_html_clean-0.4.5.tar.gz", hash = "sha256:e2a4c7d5beedd17cd7b484d848a0571e54baa239a4f9df5546e3acba7f990560", size = 24142, upload-time = "2026-05-20T12:17:53.574Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/bd/6e2b76a6c5dee10397db9c929f0c5066766ec1036046f0335b7ca7ca08b8/lxml_html_clean-0.4.5-py3-none-any.whl", hash = "sha256:c76fcadd1e5bfb9b8bafc2200d51e4e78eb0dad67f56881c21dfb6484c7e7746", size = 14573, upload-time = "2026-05-20T12:17:52.215Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nltk"
|
||||
version = "3.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "defusedxml" },
|
||||
{ name = "joblib" },
|
||||
{ name = "regex" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244, upload-time = "2026-07-08T02:39:13.562Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144, upload-time = "2026-07-08T02:39:09.753Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyphen"
|
||||
version = "0.17.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2026.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2026.6.28"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/05/e4f219230e11e774a6c9987d2ab0d0c6b8573e13a17e143d0015bee710ef/regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342", size = 416101, upload-time = "2026-06-28T19:56:55.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/53/d5c1b3cc0b5a0c985563ad6fac93d73ff2b300cb84342d89f044625d6bc7/regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca", size = 490329, upload-time = "2026-06-28T19:54:35.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/9f/0c3503e819e91ca0e7a901a8e989ebf840ac7c7aea20b1fc7f31b6759f77/regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c", size = 292039, upload-time = "2026-06-28T19:54:37.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/7f/cd004e13fcad23b3794a82307dfd222e6365eb7f598bd3caab148a830bff/regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94", size = 289488, upload-time = "2026-06-28T19:54:39.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/4c/293fb34586fbcdc47eac436069e9c11f71fae5dadfd4889b475d7d2e5f7a/regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04", size = 796772, upload-time = "2026-06-28T19:54:41.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/fa/c0cd1a90b7d12d9dc155cfc8bdea8df9720988ea5b07e8fa1eccbd0ab2dd/regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762", size = 865467, upload-time = "2026-06-28T19:54:43.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/db/0b479973046d005a1eaea299d5d536aeecb9488a16d9cbb8286338102e2d/regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba", size = 912345, upload-time = "2026-06-28T19:54:46.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5", size = 801291, upload-time = "2026-06-28T19:54:48.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/09/2103686defaf9a0a31c1663782359d5b45f42524c64cca681f5481e44a5e/regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe", size = 777106, upload-time = "2026-06-28T19:54:50.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/5a/b57593c0aa23ed269ec332fbcf07852abcb6b746e811d9464e0d09b4e25f/regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20", size = 785175, upload-time = "2026-06-28T19:54:52.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/59/c36e756ad29bf14d7b6c6d7138952476b21f6160286cedb98ac13481c993/regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4", size = 860186, upload-time = "2026-06-28T19:54:54.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/66/49808aea0da9649c300139360708fb91b7144be1f962fcebf96755fde948/regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c", size = 765754, upload-time = "2026-06-28T19:54:56.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c5/52bbd436cf2200decdf48825fa38363eaaeebb77011ea9928a1ef9e0b9f2/regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854", size = 852085, upload-time = "2026-06-28T19:54:57.988Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c3/0390b66e3019497143fe768b3ba567b64d8b24f3812d09506deb86f4a0f0/regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b", size = 789600, upload-time = "2026-06-28T19:54:59.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/fd/ab5b03653a244975069fed93d73f4f5f7484c03a84cedb238292510d7182/regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5", size = 267088, upload-time = "2026-06-28T19:55:02.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/55/21022f7d3143210ae8d4ff905c45306237b657375cc0b97883f49db3d423/regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3", size = 277680, upload-time = "2026-06-28T19:55:04.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/99/7f664804f1aef924542b0b233996b78b3e4d0a52d9951358aac99f129f51/regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767", size = 277017, upload-time = "2026-06-28T19:55:06.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/e1/9eb83518e159d719fd681c4932dc2aaff855ce72451e1d05d69466f25a96/regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c", size = 494195, upload-time = "2026-06-28T19:55:08.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e2/e259c5f2f7be269d0e2fb54275c1fa6a13fb47019f389c3f3ae457447825/regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1", size = 293976, upload-time = "2026-06-28T19:55:10.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/4e/9bdf444014d22b045d0c82ca114fac7e07a597b5b5331b7c4ce6328426e2/regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97", size = 292340, upload-time = "2026-06-28T19:55:11.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/3a/f49b11e59cbfe187ace0053a460bd72a0169b8cd52e7db9421a074ce7a43/regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d", size = 811704, upload-time = "2026-06-28T19:55:13.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/fb/ad04c39e149bf8b6cf357df5fff78341733ec366780a00c803a36735818c/regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475", size = 871157, upload-time = "2026-06-28T19:55:15.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/64/0e5ba31c11eb8ef7aac19a690c1211fc9aa9990caf09565785ebb0081b9a/regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb", size = 917287, upload-time = "2026-06-28T19:55:18.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/75/6b78df2b858c2fcbbc4858fdc3f2975cf2703be374b2842db7d2c32591a7/regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e", size = 816333, upload-time = "2026-06-28T19:55:20.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/01/ecfe665a3694d5eda9f3ec686c856438ada0943947b6005e90556a1e2cdf/regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27", size = 785518, upload-time = "2026-06-28T19:55:23.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/0a/88f9cd88ff1e82881605c4ffd62d77ee67d051232cfe6f8e9a64b86cf0e8/regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3", size = 801371, upload-time = "2026-06-28T19:55:24.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/97/601483732f93275482ceb9fed57813dfed7c47d3a019db6ec4a3bb6e23e0/regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7", size = 866517, upload-time = "2026-06-28T19:55:27.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/ed/385c2a0351b994a693453c1d1a6e9af9eb35db3c9460d76b5078acd70c62/regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb", size = 772834, upload-time = "2026-06-28T19:55:29.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/bc/bbf4a5b3b29770d7f307d3c28b5b1bca0105b0cb424be0a4eb1339bc92cf/regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816", size = 856606, upload-time = "2026-06-28T19:55:32.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/26/51d74fff82f682819979249f8d700267108ba5dc4eb284b0e11b9c85e4b3/regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb", size = 803475, upload-time = "2026-06-28T19:55:34.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/3e/6be10cefdc813533fe604dbf5d3c77d2638e7ee658b2749ebadc113b6b2e/regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d", size = 269126, upload-time = "2026-06-28T19:55:36.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/3c/32cda905ea1a6eeeb798291c294d8ec66ee0efe0cdba28b061e248b1d396/regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab", size = 279961, upload-time = "2026-06-28T19:55:38.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/b9/69f4e5cd6fbe0bb420cb2dbae441ca118f2495bdda522a74da75aa9829e7/regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d", size = 279266, upload-time = "2026-06-28T19:55:40.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/fb/fad3b810a5bb1e09b9e5d6913fc6ba88cab738fdf283196827a3c59a4c10/regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c", size = 490407, upload-time = "2026-06-28T19:55:42.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/52/b8c79d12276d93e90e707e939b396034c04980caf1235312ef790f8e11fc/regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3", size = 291988, upload-time = "2026-06-28T19:55:44.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/d2/6a911f18279daa8d7bb8b20d771ddb6ef31fabd35f5921f9d3ba21640e80/regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba", size = 289704, upload-time = "2026-06-28T19:55:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/22/ad1955c47c669291a05804d53d7071cc0732dfdf166857be38003cedc2d1/regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878", size = 797017, upload-time = "2026-06-28T19:55:48.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/67/a83159ff8703ab4d0c2cf99e76ebf289b7b4a501623241d09f88f3614f80/regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26", size = 866112, upload-time = "2026-06-28T19:55:51.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/09/7bff2d6dbbd77421b3274aa51db1c887381cbc5b6eda93598c3e882ea345/regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0", size = 911554, upload-time = "2026-06-28T19:55:53.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/44/ae59c3826e7ba492e56795cdf74ea2a7b5b7c5ea116afb79ee4956a5dff1/regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a", size = 800665, upload-time = "2026-06-28T19:55:55.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/19/6fd033d2ab00f35d445aaeaf3307c1e721424dcbfd48f6f65c857cb939cf/regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581", size = 777243, upload-time = "2026-06-28T19:55:57.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/9d/99730f26df4938049ab1e652ca75e967b4c6739444e18d9707bfdb8af20c/regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b", size = 785784, upload-time = "2026-06-28T19:56:00.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/49/105cd57162f5fc5c04cc917a1388a060cf8427e5c14353cd9044660fbf4d/regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc", size = 860914, upload-time = "2026-06-28T19:56:02.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/a5/788245a95b69018f58bff2f4fd27d007cacaea088cdb390979743f1b2571/regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9", size = 765915, upload-time = "2026-06-28T19:56:05.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/01/292065a39a004b05e67a337b18213670a7cb919d6856ac2d7df7f1a10dbb/regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db", size = 851404, upload-time = "2026-06-28T19:56:07.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/9e/a93d865db0e13483ae1a01d81e2ce16d4a7fe2f9b9fe4aac4cc08590b136/regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc", size = 789373, upload-time = "2026-06-28T19:56:09.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/0c/38b1685ad4017d78efbc8fa7dbbf96d8113b53750c8aa2d3609defd46605/regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03", size = 272496, upload-time = "2026-06-28T19:56:11.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/50/e19f261ff9ba9b50722a529e09b1743ecf65eb348be99d0fd2cd7fcede1c/regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a", size = 280754, upload-time = "2026-06-28T19:56:13.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/b8/c9e68f3a9e33be73f20990b2c065b144ff2d0aa242608a950d8c4f3b56e8/regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7", size = 280979, upload-time = "2026-06-28T19:56:15.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e6/21c425a37880c650d007c4171c6a80325446d830d85f5fbf335e7205b1e7/regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c", size = 494282, upload-time = "2026-06-28T19:56:18.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/50/6647a7ccf5ffff995ba955a0b7d766440f4e58ce1666549c8ee998f2b972/regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5", size = 293977, upload-time = "2026-06-28T19:56:20.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/dc/a3e141a4eaf125e50f63105570c01fa477c06ac5259dcfa95e9b90760e84/regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820", size = 292432, upload-time = "2026-06-28T19:56:22.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/ee/2ac1a6b9f167f8ff69f5a789938cc103b60cff41b24a6990daced8b88e34/regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2", size = 811877, upload-time = "2026-06-28T19:56:25.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/7b/9a5505ee92180bcae300b1018b9ff3d3c19962436e66f2505f255e9fde35/regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1", size = 871212, upload-time = "2026-06-28T19:56:27.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/4d/d61a702a9f9d1bd29b22cbef1aed6d477baa961232a7eb4d91b7775b0b3e/regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f", size = 917507, upload-time = "2026-06-28T19:56:29.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/60/1308066f5966b65fbb6905b99ba37e9f1cd753dd0ac08485f8257334ee92/regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546", size = 816389, upload-time = "2026-06-28T19:56:32.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/5c/57ce2cb8d714ee0b7f11c7ee4cfe2af66df2b90f147feadcb538609a3a02/regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400", size = 785890, upload-time = "2026-06-28T19:56:34.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/fd/1d5350d3a8a327bff0fccacb911732baf7b5b6f5529c0e3fa602a23e7dad/regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387", size = 801451, upload-time = "2026-06-28T19:56:36.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/79/3c9e4f8a0306e030ad5a43bbbc01625fb28d58a813bc52d42fd1cc63fb2e/regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06", size = 866504, upload-time = "2026-06-28T19:56:38.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/12/f747de475b54f4709efb24dd0fbc8467c64cec91f5db0d047b079646ee78/regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19", size = 773047, upload-time = "2026-06-28T19:56:41.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/3c/f02f860e0500c1b2d61a79dec7e214b37fb9656281dcddc92397edf96678/regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858", size = 856665, upload-time = "2026-06-28T19:56:43.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/6c/28b3fa222513484be9dee26b7222bda109056c43ea28aa2314262ca48816/regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34", size = 803573, upload-time = "2026-06-28T19:56:45.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/8f86cf1a1fd85c5ab0c503c9fe4607ad4ad48978b2d8b435d94465e134c7/regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493", size = 274515, upload-time = "2026-06-28T19:56:47.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/de/f8613c03b36786ddef2c930d28f9bcae861fcd541cc9203a870956cf1e83/regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d", size = 283650, upload-time = "2026-06-28T19:56:50.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/f3/f5ec86839bbabe33b6dee649b62ff9a445d43de6b0ad780cf6b83c56f61e/regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8", size = 283338, upload-time = "2026-06-28T19:56:52.879Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "83.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sgmllib3k"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" }
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textstat"
|
||||
version = "0.7.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nltk" },
|
||||
{ name = "pyphen" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/0f/b673fcec5ad6e976b2e8368ef3651fe0fea3348a1191bacfcd41a17ddec6/textstat-0.7.13.tar.gz", hash = "sha256:a88d1da76287cd27ca4ce7bcba1ebaf2890544a5f0bb6a5758fa84cef3bceccb", size = 138932, upload-time = "2026-02-18T21:07:39.525Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/31/0eb4cc5bb021b4ceaaa602c59ba16ce99256b9dd30981bef3f3a53d8555f/textstat-0.7.13-py3-none-any.whl", hash = "sha256:04b1ec995d1e8b2e628759497e6b23204a9ec91dcd652447d8cbba9478f25471", size = 177050, upload-time = "2026-02-18T21:07:38.163Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiltmeter"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "feedparser" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "trafilatura" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
{ name = "textstat" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "feedparser", specifier = ">=6.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "trafilatura", specifier = ">=1.12" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pytest", specifier = ">=8.0" },
|
||||
{ name = "ruff", specifier = ">=0.8" },
|
||||
{ name = "textstat", specifier = ">=0.7" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tld"
|
||||
version = "0.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/5d/76b4383ac4e5b5e254e50c09807b3e13820bed6d6c11cd540264988d6802/tld-0.13.2.tar.gz", hash = "sha256:d983fa92b9d717400742fca844e29d5e18271079c7bcfabf66d01b39b4a14345", size = 467175, upload-time = "2026-03-06T23:50:34.498Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/90/39a85a4b63c84213e78b3c17d22e1bf45328acf8ebb33ef93be30d0a3911/tld-0.13.2-py2.py3-none-any.whl", hash = "sha256:9b8fdbdb880e7ba65b216a4937f2c94c49a7226723783d5838fc958ac76f4e0c", size = 296743, upload-time = "2026-03-06T23:50:32.465Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.68.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trafilatura"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "courlan" },
|
||||
{ name = "htmldate" },
|
||||
{ name = "justext" },
|
||||
{ name = "lxml" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/19/24833e905df2d80e3bb67424f95febcc17709a1f61a522120bc438afca70/trafilatura-2.1.0.tar.gz", hash = "sha256:f689e2116fc89c7bc0b9a296d01dcfe2eb0b5455f8c371a77dc0db1f06a05643", size = 263876, upload-time = "2026-06-07T17:43:31.829Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/78/4ad99d79aee2784f49f20fd0a29058ce4c032fe4439047924c43521cd211/trafilatura-2.1.0-py3-none-any.whl", hash = "sha256:0eded5207a806445ddebbe36eae30b9035fe6a2f233c36f6fe82663fca8b9d30", size = 134600, upload-time = "2026-06-07T17:43:28.404Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2026.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user