diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..73febfbf3b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "Bash(gh api:*)", + "Bash(gh pr close:*)", + "Bash(gh pr comment:*)", + "Bash(gh pr diff:*)", + "Bash(gh pr edit:*)", + "Bash(gh pr list:*)", + "Bash(gh pr view:*)", + "Bash(gh run list:*)", + "Bash(gh run rerun:*)", + "Bash(gh run view:*)", + "Bash(gh search:*)" + ], + "deny": [] + } +} diff --git a/.claude/skills/audit-the-list/SKILL.md b/.claude/skills/audit-the-list/SKILL.md new file mode 100644 index 0000000000..a7a7cc7ef7 --- /dev/null +++ b/.claude/skills/audit-the-list/SKILL.md @@ -0,0 +1,39 @@ +--- +name: audit-the-list +description: Audit README.md sections against the shortlist rules — re-verify every entry's verdict with live data, promote or demote challengers, restructure oversized use cases, prune, and evaluate proposed additions. Fire when the maintainer asks to audit, sweep, prune, re-check, or maintain sections, or asks whether an entry (or a proposed one) deserves its slot. +argument-hint: [all or specific sections] +--- + +# Audit the list + +An Audit re-runs the admission test over live sections of README.md: every entry re-verified against current data, tiers reassessed, structure reconsidered, then the maintainer adjudicates and the result is committed. The rules live in CONTRIBUTING.md (Admission, Evidence, Entry Ordering) and CONTEXT.md (vocabulary) — this skill is the process that applies them, not a second copy of them. Entry changes land only on the maintainer's explicit go. + +## 1. Scope + +Resolve the scope from the arguments. Named sections mean exactly those, whether or not they were audited before (a re-run is how a past verdict gets rechecked). `all` is ambiguous — `AskUserQuestion` whether it means every section or only never-audited ones; prior audits are recorded in git history (`git log --oneline --grep="sweep\|audit"`). With no arguments, AskUserQuestion listing the never-audited Thematic Groups. Batch the work one Thematic Group per sitting. Done when the section list is settled. + +## 2. Evidence + +Fetch live evidence for every entry in scope before judging anything (CLAUDE.md verification rule): + +- **Downloads/month**: `cd website && uv run python fetch_pypi_downloads_via_clickpy.py` — free keyless ClickPy sweep of the full README, sole writer of `data/pypi_downloads.tsv` (rewritten from scratch each run; header row, every row stamped with its `fetched_at` date). A cache whose `fetched_at` is within the last 7 days is current enough for verdicts — skip the sweep; older than that, re-run it (costs ~1s). Cross-checks print to stdout, take explicit names, and never touch the cache: `fetch_pypi_downloads_via_bigquery.py ...` (canonical source, maintainer's own GCP account, `--dry-run` first — the docstring carries the cost constraints; full-README sweeps exceed the free tier, keep name lists small), `fetch_pypi_downloads_via_pepy.py ...` (needs `PEPY_TECH_API_KEY` in repo-root `.env`, throttled to 5 requests/minute), or `https://pypistats.org/api/packages/{name}/recent` paced 8s or slower. pypistats excludes mirror/CI traffic; ClickPy, BigQuery, and pepy include it — never mix sources within one comparison. +- **Repo state**: archived flag, last push, created date, stars, description — `gh api repos/{owner}/{repo}`, GitLab API for GitLab-hosted projects. +- **PyPI metadata** (`https://pypi.org/pypi/{name}/json`) wherever a name might not be the canonical package — ownership collisions and wrong display names surface here. Wrong-package cache rows are common: the sweep looks up README display names, so when the display name differs from the canonical package, the row silently measures an unrelated squatter or a dead predecessor. When a famous entry shows a count that looks off (too small, or absurdly round), verify identity before citing it, and fetch the canonical package's count via pepy. + +Done when every entry in scope has downloads (or a stated no-signal reason), repo state, and a confirmed PyPI name. + +## 3. Verdicts + +Draft a verdict and reason for every entry, restructure before cap: decide Splits, mints, and re-homes first — cutting before restructuring destroys entries — then apply the cap tier by tier, including promotions and demotions between obvious choice and challenger. Ground every reason in the fetched evidence; label anything unverifiable as a judgment call. Where fresh evidence contradicts the standing verdict, say so in the reason instead of silently keeping the seed. + +## 4. Review and go + +Run the `preview-verdicts` skill: it generates the interactive review page and defines how the maintainer's feedback JSON comes back and gets processed. Their verdicts are final. Before touching README.md, surface what the feedback implies but does not decide — cap overflows, homeless entries after a Split, tier for a tierless flip — as named assumptions or questions, then get an explicit go. + +## 5. Execute + +One commit per section: body lists each removal with its reason and downloads figure; restructures, tier moves, and reorders ride the same commit. Format-only outcomes (no removals) are a single style commit. `make test` before every commit, `make build` after the last one. Generic commit helpers tend to split a section audit into structural and per-subcategory commits — if that happens, squash back to one commit per section. Done when the tree is clean, tests passed before each commit, and the build count reconciles with the adjudicated changes. + +## 6. Record + +A conclusion that outlives the sitting goes into the repo before the audit ends: admission or evidence rules into CONTRIBUTING.md, repo process and environment facts into CLAUDE.md and AGENTS.md (kept in sync), vocabulary into CONTEXT.md. Done when nothing the next audit needs is stranded in conversation. diff --git a/.claude/skills/preview-verdicts/SKILL.md b/.claude/skills/preview-verdicts/SKILL.md new file mode 100644 index 0000000000..938a02d51a --- /dev/null +++ b/.claude/skills/preview-verdicts/SKILL.md @@ -0,0 +1,24 @@ +--- +name: preview-verdicts +description: Generate the interactive keep/drop verdict preview (HTML page with per-row feedback controls) whenever a prune sweep, batch entry edit, or restructure needs maintainer review before touching README.md — and process the feedback JSON the maintainer pastes back. +--- + +# Verdict preview + +Maintainer review happens through an interactive HTML page: one row per entry with your seeded verdict and reason, a Keep/Drop toggle and a reason field for the maintainer, and a **Copy feedback** button that exports only changed or commented rows as JSON. Generate the page, wait for the pasted JSON, then apply it. Entry changes land in README.md only after the review — and only on an explicit go. + +## Generate the preview + +1. Build the `DATA` array. A group is `[section, subcategory, rows]`; a row is `[entry, url, downloads, verdict, reason]`. + - `subcategory` may carry a note after ` — ` (rendered muted): use it for proposed splits, re-homes, or anything the maintainer should weigh for the whole group. + - `downloads` is PyPI last-month as a comma-formatted string; use `—` when no signal exists (e.g. agent skill packs), `stdlib` for standard-library modules, `fetch failed` when the lookup failed. State the fetch date in the sub-header. + - `verdict` is `keep` or `drop`, seeded from the current adjudication or dry-run. + - `reason` is plain language the maintainer reads cold — no invented shorthand. When fresh evidence contradicts the seeded verdict (a big download count on a drop, a dead repo on a keep), say so in that row's reason instead of silently changing the seed. +2. Copy `template.html` (sibling of this file) and replace the placeholders: `__TITLE__` (page title), `__SUB__` (sub-header: scope, seed provenance, fetch date, and the standing instruction to flip/comment then Copy feedback), `__KEY__` (localStorage key), `__DATA__` (the array). `__KEY__` must be unique per review — slug plus date, e.g. `awesome-python-science-2026-09-01` — because saved state under a reused key bleeds a previous review's flips into rows with the same section and entry name. +3. Write the page to `tmp/awesome-python--preview.html` in the repo root (git-ignored; create the directory if needed), `open` it, and tell the maintainer the path and the return path: flip or comment rows (they highlight yellow), press **Copy feedback**, paste the JSON into the chat. Done when the page is open and the return path is stated. + +## Process the pasted feedback + +Each JSON row is `{section, subcategory, entry, my_verdict, your_verdict, reason}`. The maintainer's verdict is final — apply it, never re-argue it. An empty reason means the verdict stands unexplained; that is enough. + +Before executing, surface anything the flips imply that the maintainer has not decided: a use case pushed past its cap, an entry left homeless by a proposed split, a request that is already satisfied (a no-op). Ask, then execute on their go. Done when every pasted row is either applied or surfaced back — none silently dropped. diff --git a/.claude/skills/preview-verdicts/template.html b/.claude/skills/preview-verdicts/template.html new file mode 100644 index 0000000000..91324e82f4 --- /dev/null +++ b/.claude/skills/preview-verdicts/template.html @@ -0,0 +1,160 @@ + + + + + +__TITLE__ + + + +
+

__TITLE__

+

__SUB__

+
+ +
+
+ + + Copies only rows you changed or commented, as JSON. +
+ + + diff --git a/.claude/skills/review-prs/SKILL.md b/.claude/skills/review-prs/SKILL.md new file mode 100644 index 0000000000..bcc854e046 --- /dev/null +++ b/.claude/skills/review-prs/SKILL.md @@ -0,0 +1,35 @@ +--- +name: review-prs +description: Triage open PRs — screen from the diff, delegate the admission judgment to audit-the-list, then merge or close on GitHub. Fire when the maintainer asks to review PRs, process the PR queue, or judge whether a specific PR should be merged. +argument-hint: [PR numbers] +--- + +# Review PRs + +A PR review turns the open queue into terminal states: merged, closed, or explicitly parked. The rules live in CONTRIBUTING.md (Quality Requirements, Admission, Review Process, Automatic Rejection) — this skill is the workflow that applies them, not a second copy. + +## 1. Fetch + +Arguments name specific PRs; otherwise take the queue: `gh pr list --repo vinta/awesome-python --limit 10 --json number,title,author,url,body,files,mergeable,mergeStateStatus`. Fetch all diffs in parallel: `gh pr diff --repo vinta/awesome-python`. + +Sort the batch: entry additions continue; anything else (typo fixes, website changes, docs) is out of scope — report it under "needs human" and touch nothing, so it resurfaces every run until a human acts. Done when every PR is sorted and has its diff. + +## 2. Screen + +Apply the Automatic Rejection rules that the diff and PR metadata answer without judgment. The recently-closed-duplicate rule needs a lookup: `gh pr list --repo vinta/awesome-python --state closed --search "" --limit 10`. A screened-out PR goes straight to step 4 as a close, with the rule it broke as its reason. + +Resolve each survivor's target use case against the current README — a diff's context lines show the base the PR was written on, which may have changed since. A merge conflict rides to the Merge arm, which absorbs it. Done when every surviving PR names its target use case. + +## 3. Judge + +Group survivors by target use case — PRs proposing entries for the same use case compete for the same slots, so they ride one invocation. Per group, invoke the `audit-the-list` skill with arguments in this shape: "Judge proposed entries for the
use case. Evidence and Verdicts steps only; report the verdicts back. No preview page, no README changes, no commits." The verdict for each PR is merge or close, grounded in the fetched evidence; when the use case is at cap, a merge verdict names the entry that leaves. An entry no current use case fits is a third outcome — a structure question: carry it to Act with evidence attached, and the maintainer decides there (mint the subcategory and merge, close, or leave open). Done when every survivor holds a verdict with a reason. + +## 4. Act + +- **Close**: per closing PR, AskUserQuestion presenting the draft closing comment — the comment states the reason and links CONTRIBUTING.md. Batch up to 4 PRs per call and keep a checklist of which verdicts have been asked; answers often arrive as custom text, and that text is the decision. Arms: close with this comment, close without a comment, keep open. Then `gh pr close --repo vinta/awesome-python --comment ""`, or a plain close. +- **Merge**: a clean PR merges with `gh pr merge --repo vinta/awesome-python --merge`. A conflicted one merges locally: `git fetch origin pull//head`, `git merge FETCH_HEAD` with the standard `Merge pull request # from /` message, resolving the conflict by placing the entry correctly — GitHub still marks the PR merged and the contributor keeps credit. Either way, reconcile the section per CONTRIBUTING before pushing: remove the entry the verdict displaced, fix the new entry's display name and Entry Ordering position, `make test`, commit. An add-only diff that displaces is the normal case — the removal is this step's job, not the contributor's. +Done when every verdict has been adjudicated by the maintainer and its action executed. A kept-open PR resurfaces next run — that is its point. + +## 5. Report + +Summary table: PR, verdict, action taken, plus the needs-human list. Done when every fetched PR ends in exactly one state — merged with its section reconciled, closed, or left open (kept open, needs-human, or structure question pending). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3a77974e24..41890f4696 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,26 @@ -## Why this framework/library/software/resource is awesome? +## Project -A few sentences describing the reason. +[project-name](url) -## Vote for this pull request +## Checklist -Who agrees that this change should be merged could add your reactions (e.g. :+1:) to this pull request. +- [ ] I read [CONTRIBUTING.md](https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md) - awesome-python is a shortlist, not a catalog +- [ ] One project per PR +- [ ] PR title format: `Add project-name` +- [ ] Entry format: `- [pypi-name](https://github.com/owner/repo) - Description ending with period.` +- [ ] Display name is the PyPI package name +- [ ] Placed in an existing use case (new sections and subcategories are maintainer-only) +- [ ] Meets all Quality Requirements: active, stable, documented, at least 1 month old + +## Which Tier + +Pick one: + +- [ ] **Obvious choice** - a tool an experienced Python developer would name when asked "what do I use for this?" +- [ ] **Challenger** - not yet the obvious choice, but a credible successor to one. Give adoption-trajectory evidence, not popularity alone. + +Explain: + +## Displacement + +If the use case is at its cap, name the entry your project replaces and argue that yours does that entry's job better. One in, one out. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..02898b64ca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --group build --locked + + - name: Run tests + run: make test + + - name: Build website + run: make build diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml new file mode 100644 index 0000000000..0c23761c62 --- /dev/null +++ b/.github/workflows/deploy-website.yml @@ -0,0 +1,102 @@ +name: Deploy Website + +on: + push: + branches: [master] + schedule: + - cron: "0 0 * * *" + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + if: github.repository == 'vinta/awesome-python' + runs-on: ubuntu-latest + environment: + name: github-pages + url: https://awesome-python.com/ + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --group build --locked + + - name: Run tests + if: github.event_name == 'schedule' + run: make test + + - name: Get date + id: date + run: echo "today=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" + + - name: Restore star data cache + id: cache-stars + uses: actions/cache/restore@v4 + with: + path: website/data/github_stars.json + key: github-stars-${{ steps.date.outputs.today }} + restore-keys: github-stars- + + - name: Fetch GitHub stars + id: fetch-stars + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: make fetch_github_stars + + - name: Save star data cache + if: steps.fetch-stars.outcome == 'success' + uses: actions/cache/save@v4 + with: + path: website/data/github_stars.json + key: github-stars-${{ steps.date.outputs.today }} + + - name: Verify star data exists + run: | + if [ ! -f website/data/github_stars.json ]; then + echo "::error::github_stars.json not found. No cache and fetch failed or was skipped." + exit 1 + fi + python -m json.tool website/data/github_stars.json > /dev/null + + - name: Restore PyPI download data cache + uses: actions/cache/restore@v4 + with: + path: website/data/pypi_downloads.tsv + key: pypi-downloads-${{ steps.date.outputs.today }} + restore-keys: pypi-downloads- + + - name: Fetch PyPI downloads + id: fetch-downloads + continue-on-error: true + run: make fetch_pypi_downloads + + - name: Save PyPI download data cache + if: steps.fetch-downloads.outcome == 'success' + uses: actions/cache/save@v4 + with: + path: website/data/pypi_downloads.tsv + key: pypi-downloads-${{ steps.date.outputs.today }} + + - name: Build website + run: make build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: website/output/ + + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 4046e40f0f..ba30760958 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,19 @@ +# secrets +.env* + +# macOS .DS_Store +# python +.venv/ +__pycache__/ *.py[co] -docs/index.md -site/ +# website +website/output/ +website/data/* +!website/data/pypi_name_overrides.json + +# agents +.playwright-cli/ +tmp/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 44c97353bd..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: ruby - -rvm: - - 2.2 - -before_script: - - gem install awesome_bot - -script: - - awesome_bot README.md --allow-dupe --white-list pyparsing,graphviz.org diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..49023b5d58 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,14 @@ +# AGENTS.md + +An opinionated guide to the best Python frameworks, libraries, and tools. + +[README.md](README.md) is the single source of content truth; `website/` renders it into the static site: [awesome-python.com](https://awesome-python.com/). + +## Entry Rules + +[CONTRIBUTING.md](CONTRIBUTING.md) holds the admission rules, quality requirements, rejection rules, entry format, and ordering. Apply it whenever adding or removing an entry — direct commits included, not only PR reviews. + +- Every keep/drop reason must be verified against current online data at decision time — download counts, repo activity and archived status, PyPI metadata, project docs. Judging tiers — obvious choice vs challenger — also requires WebSearch evidence (adoption trajectory, community sentiment), not download counts alone. Training-data recollections are not evidence; label anything unverifiable as a judgment call. +- One entry per commit when adding or deleting entries. Exceptions: a prune sweep is one commit per section, its body listing each removal with its reason; format, wording, or categorization changes may be bundled. Cross-section re-homes ride the originating audit's commit (both sides of the move in one diff). +- Resources sections are not project entries: out of audit scope, and the website never parses them. +- Sponsor placement never influences which projects get listed — see [SPONSORSHIP.md](SPONSORSHIP.md). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..16e15f9b57 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,14 @@ +# CLAUDE.md + +An opinionated guide to the best Python frameworks, libraries, and tools. + +[README.md](README.md) is the single source of content truth; `website/` renders it into the static site: [awesome-python.com](https://awesome-python.com/). + +## Entry Rules + +[CONTRIBUTING.md](CONTRIBUTING.md) holds the admission rules, quality requirements, rejection rules, entry format, and ordering. Apply it whenever adding or removing an entry — direct commits included, not only PR reviews. + +- Every keep/drop reason must be verified against current online data at decision time — download counts, repo activity and archived status, PyPI metadata, project docs. Judging tiers — obvious choice vs challenger — also requires WebSearch evidence (adoption trajectory, community sentiment), not download counts alone. Training-data recollections are not evidence; label anything unverifiable as a judgment call. +- One entry per commit when adding or deleting entries. Exceptions: a prune sweep is one commit per section, its body listing each removal with its reason; format, wording, or categorization changes may be bundled. Cross-section re-homes ride the originating audit's commit (both sides of the move in one diff). +- Resources sections are not project entries: out of audit scope, and the website never parses them. +- Sponsor placement never influences which projects get listed — see [SPONSORSHIP.md](SPONSORSHIP.md). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..f13d736d08 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,40 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery +- Trolling, insulting or derogatory comments +- Public or private harassment +- Publishing others' private information without permission +- Other conduct which could reasonably be considered inappropriate + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project maintainers. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), +version 2.0. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000000..434afbc77b --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,63 @@ +# Awesome Python Curation + +The editorial domain of awesome-python: which projects the list carries, how they are admitted, and how they are displaced. The list is a curated shortlist of obvious choices, not a catalog of everything good. + +## Language + +### List structure + +**Entry**: +A single listed project: `- [name](url) - Description.` The unit that is admitted, displaced, or pruned. Named by PyPI package name when one exists, else by repository name. + +**Sub-item**: +An indented awesome-* also-see link under an Entry (awesome-django under django, awesome-pytest under pytest). Not an Entry: it holds no slot, does not count against the Cap, and rides its parent — re-homed or removed with it. Maintainer-only placement, and only for awesome-* lists: a companion project either earns a full Entry in its proper Use Case or is not listed. + +**Thematic Group**: +A bold group line (e.g. "AI & ML", "Web Development") that clusters Sections in the TOC and the Projects body. + +**Section**: +A `###` heading in README.md (e.g. "Testing", "AI and Agents"). Sections group entries and live under a Thematic Group. +_Avoid_: Category (overloaded — the TOC calls them categories, but rules bind to Use Cases, not Sections) + +**Subcategory**: +A named bullet inside a Section with indented entries under it (e.g. "Mock" inside "Testing"). Where present, each Subcategory is one Use Case. + +**Use Case**: +The unit of "it" in "one obvious way to do it": one distinct job a reader needs done. Defined by the list's structure — each Subcategory is a Use Case; a flat Section is a single Use Case. Submitters cannot define Use Cases; only the maintainer changes the structure (see Split). +_Avoid_: Niche, micro-category + +### Admission + +**Serves Python Developers**: +The scope test for what belongs on the list at all: Python developers use it in their Python work. Implementation language and packaging are irrelevant — uv and ty are Rust and belong; a pure-Python library nobody uses in Python work does not. +_Avoid_: Python-first, written-in-Python (old requirement — removed) + +**Obvious Choice**: +An entry an experienced Python developer would name unprompted when asked "what do I use for [Use Case]?". Certified by maintainer editorial judgment, informed primarily by PyPI download counts rather than GitHub stars; judgment overrides the signal's known failure modes (CI-inflated counts, model releases consumed as weights rather than pip installs — extended to any project consumed outside pip: SDK downloads like renpy, deployed services like thumbor — and large-but-specific audiences misread as "niche"). A standard-library module holds a slot only when it is itself the Obvious Choice for the Use Case, not merely relevant to it. +_Avoid_: Industry Standard (old lane name), awesome (unfalsifiable) + +**Cap**: +The per-Use-Case entry limit: up to 3 Obvious Choices plus up to 2 Challengers, hard maximum 5. A qualitative bar first, a numeric backstop second — and a ceiling, not a floor: a minted Use Case may hold a single entry. + +**Displacement**: +The only admission path into a full Use Case: the PR names the entry it replaces and argues the newcomer does that entry's job better. One in, one out. +_Avoid_: One-in-one-out (informal alias) + +**Challenger**: +An entry (at most two per Use Case) that is not yet the Obvious Choice but is a credible successor to one. Marked by ordering, not description: within a Use Case, Obvious Choices are listed first, then Challengers, each tier by PyPI downloads/month descending (standard-library modules first in the Use Case outright — always Obvious Choices by the stdlib rule — other no-signal entries last in tier, alphabetically). Admission requires adoption-trajectory evidence, not popularity alone. +_Avoid_: Rising Star (old lane name), Hidden Gem (old lane name — concept removed entirely) + +**Second Tier**: +A Challenger slot holding a demoted incumbent instead of a rising successor — a former Obvious Choice or fading community standard an Audit kept below the current Obvious Choices (clickhouse-driver behind the official client, dogpile.cache and django-cacheops behind cachetools and diskcache, django-haystack in Search). It counts against the two Challenger slots and is marked by position like any Challenger; the adoption-trajectory bar gates only new admissions, not demotions. + +**Override**: +A maintainer decision, made in an Audit and explicitly recorded, that keeps an entry or use-case shape past a written limit — the activity line, the stability bar, a cap, or a structure rule. Case-by-case: an Override never loosens the written rule for any other entry, and submitters cannot cite one. + +**Split**: +Maintainer-only restructuring of an oversized Use Case into finer Use Cases (new Subcategories), considered before any trimming when the size reflects genuinely distinct jobs. A submitter's entry PR can never perform a Split. + +### Maintenance + +**Audit**: +The recurring maintenance pass over one or more Sections: every Entry's verdict re-verified against current evidence, Challengers promoted or demoted, oversized Use Cases Split, drops and admissions re-decided. The maintainer adjudicates through an interactive preview, and entry changes land only on their explicit go. The 2026-08 shortlist-reform sweeps were the first Audits. +_Avoid_: Sweep (reform-era name for a Section's first Audit) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcb24bfa63..e90db05a23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,116 @@ # Contributing -Your contributions are always welcome! - -## Guidelines - -* Add one link per Pull Request. -* Add the link: `* [project-name](http://example.com/) - A short description ends with a period.` - * Keep descriptions concise. -* Add a section if needed. - * Add the section description. - * Add the section title to Table of Contents. -* Search previous suggestions before making a new one, as yours may be a duplicate. -* Don't mention `Python` in the description as it's implied. -* Check your spelling and grammar. -* Remove any trailing whitespace. -* Send a Pull Request with the reason why the library is awesome. +awesome-python is a shortlist, not a catalog. Each use case lists only its obvious choices, and most rejections mean "the use case is full", not "your project is bad". Read this whole page before opening a PR. + +## Quality Requirements + +All submissions must satisfy **ALL** of these: + +1. **Serves Python Developers**: Python developers use it in their Python work. Implementation language and packaging are irrelevant — uv and ty are written in Rust, and agent skill packs are markdown, yet all belong; a pure-Python project nobody uses in Python work does not. +2. **Active**: Commits within the last 12 months +3. **Stable**: Production-ready, not alpha/beta/experimental +4. **Documented**: Clear README with examples and use cases +5. **Established**: Repository at least 1 month old + +## Admission + +A **use case** is one distinct job a reader needs done. Use cases are defined by the list's structure: each subcategory is a use case, and a section without subcategories is a single use case. Structure changes — new sections, new subcategories, splitting an oversized use case into finer ones — are made only by the maintainer; an entry PR can never create the subcategory it needs. + +Each use case lists at most: + +- **Up to 3 obvious choices** — tools an experienced Python developer would name when asked "what do I use for this?" +- **Up to 2 challengers** — tools that are not yet the obvious choice but are credible successors to one. Admission as a challenger requires adoption-trajectory evidence, not popularity alone. + +Hard maximum: 5 entries per use case. This is a qualitative bar first and a numeric backstop second — most use cases should carry fewer. + +**Overrides**: the maintainer may exceed any limit on this page — the caps, the activity requirement, the stability requirement — for a specific entry or use case by explicit decision. An override is case-by-case; it does not loosen these rules for submissions, and citing one in a PR carries no weight. + +**Displacement**: once a use case is at its cap, the only way in is to name the entry your project replaces and argue that yours does that entry's job better. One in, one out. + +**Dual-listing**: a tool may hold entries in multiple use cases, but only when it earns its slot in each independently. List the full entry in each home with identical lines; never a "see X above" note, since the website only renders list items. Description edits update every copy in the same commit. Each slot is audited on its own: dropping one home keeps the other, and dropping the tool entirely removes all copies in one commit. Dual-listing is a maintainer decision; a PR adding a second home for an existing entry is treated as a duplicate. + +**Standard library**: a standard-library module is listed only where the stdlib is itself the obvious choice for the use case (tomllib yes, unittest no). + +**Evidence**: admission is decided by maintainer editorial judgment, informed primarily by PyPI download counts rather than GitHub stars. Judgment overrides the signal's known failure modes (CI-inflated counts, model releases consumed as weights rather than pip installs, large-but-specific audiences misread as "niche"). The maintainer's decision is final. + +Looking for an exhaustive catalog instead? Follow the awesome-\* lists linked under individual entries (for example awesome-python-testing) — they exist precisely so this list doesn't have to be one. + +## Entry Format Reference + +**Use GitHub repository URLs** whenever possible. Projects linked to a GitHub repo are ranked higher on [awesome-python.com](https://awesome-python.com/). + +### Naming Convention + +Use the **PyPI package name** as the display name so developers can copy it directly to `pip install`. Check the canonical name at `https://pypi.org/pypi/{package}/json`. If the project is not on PyPI, use the GitHub repository name instead. + +### Standard Entry + +```markdown +- [pypi-name](https://github.com/owner/repo) - Description ending with period. +``` + +### Standard Library Module + +```markdown +- [module](https://docs.python.org/3/library/module.html) - (Python standard library) Description. +``` + +### Fork of Another Project + +```markdown +- [new-name](https://github.com/owner/new-name) - Description ([original-name](original-url) fork). +``` + +### Entry with Related Awesome List + +```markdown +- [project](https://github.com/owner/project) - Description. + - [awesome-project](https://github.com/someone/awesome-project) +``` + +### Subcategory Format + +```markdown +- Subcategory Name + - [project](url) - Description. +``` + +### Entry Ordering + +Within a use case, the obvious choices are listed first, ordered by PyPI downloads per month from high to low; challengers follow, in the same order. Standard-library modules sort first in the use case — built-ins lead, before everything else — alphabetically when there are several. (This never conflicts with tier order: a standard-library module is only listed where it is itself the obvious choice, so it always belongs to the first tier.) Other entries without a download signal (agent skill packs, projects distributed outside PyPI) sort last within their tier, alphabetically. There is no marker in the entry text — position is the marker, so the last entries of a use case may be its challengers. + +## Changing the Structure + +Adding sections or subcategories is maintainer-only (see Admission). For maintainer reference: + +1. Add the section description in italics: `*Libraries for doing X.*` +2. Add the section under the appropriate thematic group (e.g., **AI & ML**, **Web**, **Data & Science**) +3. Add the section title to the Table of Contents under its group +4. Order entries per Entry Ordering above + +## Review Process + +PRs are reviewed by automated tools and maintainers: + +1. **Format Check**: Entry follows the correct format +2. **Category Check**: Placed in the appropriate use case +3. **Duplicate Check**: Not already listed or previously rejected +4. **Activity Check**: Project shows recent activity +5. **Admission Check**: Meets the Admission rules above, including Displacement when the use case is at its cap + +Search previous Pull Requests and Issues before submitting, as yours may be a duplicate. + +## Automatic Rejection + +PRs will be **closed** if: + +- Adding multiple projects in one PR +- The use case is at its cap and the PR makes no Displacement argument +- The PR creates a new section or subcategory and fills it (structure changes are maintainer-only) +- Coordinated multi-entry self-promotion: multiple related projects from the same organization or author, across one or several PRs +- Duplicate of existing entry or recently-closed PR +- Empty or placeholder PR descriptions +- Placed under an inappropriate category +- Project is archived or abandoned (no commits in 12+ months) +- No documentation or unclear use case +- Repository less than 1 month old diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000000..1e678f2b14 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,223 @@ +--- +version: alpha +name: awesome-python.com +description: Warm editorial Python index. Light cream canvas, brown-red interactive accent, Cormorant Garamond plus Manrope, table-driven single-page reference. +--- + +# awesome-python.com DESIGN.md + +awesome-python.com is a searchable, filterable index of ~650 curated Python projects. It is a reference tool, not a landing page and not a GitHub README mirror. + +This file follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/overview/). The source of truth for token values lives in `website/static/style.css`. Color tokens here are written in OKLCH because the project mandates OKLCH over hex, which is a deliberate divergence from the spec's hex-only token requirement. + +## Overview + +Three words: **opinionated, confident, dense**. + +Working Python developers (mid to senior) are the target reader. They write Python daily and arrive with a specific question in mind: "what's a good HTTP client these days", "is there still a maintained ORM for X", "what are people using for task queues now". Secondary readers: polyglot developers evaluating Python's ecosystem, and curious browsers. + +Jobs to be done: + +1. Find a library for a specific need fast (search + tag filter). +2. Compare candidates at a glance (stars, last commit, tags, one-line description). +3. Confirm a project is alive before clicking through. + +These users skim. They reward density and terse copy. They penalize marketing fluff. + +Voice: + +- Editorial. Every word earns its place. +- Confident, not combative. "This is the list" energy, not "check out these cool projects". +- No hype. The content is what's interesting. +- Calm authority. Closer to a well-edited technical reference (O'Reilly index, The Economist briefing, a good man page) than a blog or product site. + +Emotional goals: trust, efficiency, craft. The reader should feel the list was edited by someone with taste, find what they need in seconds, and notice the typographic care as a signal that the curation is careful too. + +Reference points (stay close to these): + +- **https://www.placestoread.xyz** is the primary visual model for the table, expand row, sorting, and footer. "Like placestoread" means dense single-page list, inline click-to-expand rows that indent under the Name column, sortable headers, minimum decoration. When in doubt about a table or row treatment, check placestoread first. +- Magazine reference pages (The Economist, FT Weekend, Monocle). +- Field-guide books. Curated, functional, hand-made. +- Library card catalogs. Dense tabular information, excellent typography, no decoration for decoration's sake. + +Anti-references (avoid strictly): + +- Generic dark developer-tool look. No cyan on near-black, neon gradients, VSCode-palette dashboards, terminal-green monospace branding. +- Other awesome-\* sites. No plain README dumps, bare lists of links, no voice. +- SaaS marketing pages. No big metric counters, testimonial cards, feature grids, pricing tiers, or "join 10,000+ developers" social proof bands. + +Design principles: + +1. **The list is the hero.** Hero, sponsor band, and CTA exist, but they must not compete with the table for attention. +2. **Density is a feature.** Prefer tables and tight rhythm over giant cards with one fact each. Mid-senior developers want to see more at once. +3. **Editorial typography over decoration.** Visual interest comes from the serif/sans pairing, type hierarchy, and whitespace. Not from gradients, shadows, badges, or icon boxes above headings. +4. **Warm, not cool.** Neutrals tint toward warm hues (roughly 55 to 80 in OKLCH). Pure grays and cool blues do not belong. +5. **One point of view.** No dark mode, no theme picker, no alternate palettes. Consistency signals curation. + +## Colors + +Warm editorial palette. Light theme only (`color-scheme: light`). OKLCH only. + +Each token below shows the OKLCH value (canonical, lives in `style.css`) followed by an approximate hex sRGB equivalent for spec linters and any tool that expects hex. + +Surfaces: + +- `--bg-page` `oklch(96.8% 0.018 80)` ≈ `#FBF3E7`. Cream/ivory canvas, the body floor. The body uses a vertical gradient between `--bg-page-top` `oklch(95.2% 0.018 78)` ≈ `#F7EFE3`, `--bg-page` at 24rem, and `--bg-page-end` `oklch(98.4% 0.01 80)` ≈ `#FCF8F0`, with a soft radial highlight in the top-left corner. +- `--bg-paper` `oklch(98.6% 0.01 80)` ≈ `#FEFAF3`. Warm white for the content shell. +- `--bg-paper-strong` `oklch(95.7% 0.016 76)` ≈ `#F7F0E5`. Tinted paper for sponsor band, CTA backgrounds, static decoration. +- `--hero-bg-start` `oklch(14% 0.03 32)` ≈ `#130503` through `--hero-bg-mid` `oklch(19% 0.035 35)` ≈ `#22120B` to `--hero-bg-end` `oklch(28% 0.05 42)` ≈ `#3D2014`. Dark earthy hero gradient. +- `--footer-bg` `oklch(16% 0.025 35)` ≈ `#170906`. Dark warm charcoal footer, part of the same system. + +Ink: + +- `--ink` `oklch(22% 0.02 55)` ≈ `#221812`. Body text. +- `--ink-soft` `oklch(38% 0.018 55)` ≈ `#4A4039`. Secondary copy. +- `--ink-muted` `oklch(52% 0.02 55)` ≈ `#72665E`. Meta rows, captions, static labels. +- `--line` / `--line-strong`. Hairlines and dividers. + +Accent (warm brown-red, reserved for interactive): + +- `--accent` `oklch(58% 0.16 45)` ≈ `#C4530F`. Primary accent. +- `--accent-deep` `oklch(44% 0.15 42)` ≈ `#922900`. Link text, hover. +- `--accent-soft` `oklch(92% 0.045 55)` ≈ `#FDDDC9`. Tinted background for filter tags. +- `--accent-underline` `oklch(58% 0.16 45 / 0.4)` ≈ `#C4530F66`. Subtle text-decoration-color. + +Rules: + +- Use OKLCH for any new color. Not HSL, not hex. +- Accent tokens (`--accent`, `--accent-deep`, `--accent-soft`) are reserved for interactive elements. Clickable filter tags (`.tag`) correctly use `--accent-soft` background with `--accent-deep` text. Interactive link states (`.col-name > a:hover`, `.sponsor-link:hover`, `.hero-action-primary`, `.back-to-top`, CTAs) use accent tokens. +- Non-interactive elements (inline code, `.source-badge`, static labels, decorative pills) must use ink tokens (`--ink`, `--ink-soft`, `--ink-muted`) on `--bg-paper-strong` or `--bg-paper`, never the accent. `.source-badge` uses `--ink-soft`; `.sponsorship-body code` uses `--ink`. Users should not mistake static decoration for something clickable. +- Same role uses the same token everywhere. No one-off inline `color: oklch(...)` buried in a rule. + +Aversions: + +- **No green.** The user rejected it when picking the palette. Warm brown-red, ivory, and dark earthy tones are the established system. Do not introduce green even for success states or ancillary accents. +- No cyan, no neon gradients, no pure grays, no cool blues. + +## Typography + +Pairing (do not swap): + +- **Display**: `Cormorant Garamond` (serif, 600 only). +- **Body**: `Manrope` (sans, 400 / 600 / 700 / 800). + +Scale: + +| Role | Token | Size | Family | Weight | Notes | +| ---------------- | ----------------- | ----------------------------- | ------------------ | --------- | ---------------------------------------------- | +| Hero headline | (literal `clamp`) | `clamp(4.5rem, 11vw, 8.5rem)` | Cormorant Garamond | 600 | Magazine-cover scale, single use on the hero | +| Body large | `--text-lg` | `1.125rem` | Manrope | 400 | Content-heavy passages | +| Body | `--text-base` | `1rem` (16px) | Manrope | 400 | Body floor, do not go smaller | +| Meta / secondary | `--text-sm` | `0.95rem` | Manrope | 400 / 600 | Meta rows, secondary copy | +| Caption / pill | `--text-xs` | `0.8rem` (12.8px) | Manrope | 600 / 700 | Smallest token, pills, badges, tags, footnotes | + +Hard-won sizing rules (do not relax): + +- **Body type floor is 16px.** Do not go smaller. +- **Absolute minimum font size is 12px (`0.75rem`) for ANY text**, including pills, badges, tags, captions, footnotes. Anything smaller hits Chrome's default minimum-font-size floor and renders inconsistently across browsers and user accessibility settings. Use `var(--text-xs)` (`0.8rem`) as the smallest token in code. +- **When in doubt, pick one step larger** than what generic scale references suggest. The user has corrected sizes upward 11+ times across 8 sessions. Footer, meta rows, expand content, labels, and headings all trend too small by default. **Never reduce an existing size unprompted.** +- Adjacent heading levels differ by at least 0.25rem of rendered size. +- Row numbers in the table: left-align, no leading zeros. Zero-padding was tried and rejected. +- **Never `text-transform`.** Write the casing in the markup. + +## Layout + +- **Single width cap: `--shell-max: 84rem` (~1344px) applied via `.section-shell`.** This is the ONLY width cap in the project. Widescreen monitors are the default viewing context. +- **Do NOT add `max-width`** to sections, cards, table cells, table rows, expanded rows, CTA backgrounds, sponsor descriptions, hero subcopy, paragraphs, or list items. The user has removed narrow inner caps repeatedly (`56ch`, `65-75ch`, etc.). Default is no inner cap. +- The "cap line length at ~65-75ch" rule does NOT apply here. Ignore it. Readability at wide widths is carried by vertical rhythm, leading, and the modular type scale instead. +- If a width cap is genuinely necessary for a specific element, ask first with a concrete reason before adding it. +- Shell padding: `--shell-pad: clamp(1.25rem, 3vw, 2.5rem)`. Symmetric gutters: logo left-gap equals logo right-gap, column paddings match across header and body. +- `gap` over child margins in flex and grid. +- Logical properties (`margin-inline`, `padding-block`) over physical (`margin-left`, `padding-top`). +- `rem` for spacing and type. `px` only for borders and shadows. +- CSS custom properties for all colors and repeated values. +- Sibling components (card lists, grid items) share identical spacing. +- Use flexbox or grid for layout. Avoid floats and absolute positioning except for genuine overlay cases (focus rings, sticky headers). +- Never `!important`. Fix specificity instead. + +## Elevation & Depth + +Depth comes from **tonal layers**, not heavy shadows. + +- The page is a quiet warm canvas (`--bg-page`). The content shell is slightly brighter paper (`--bg-paper`). The sponsor band, CTA backgrounds, and inline decorative blocks step up to `--bg-paper-strong`. +- The hero is the one place that uses real atmosphere: subtle grid, slow sheen, warm radial gradients on a dark earthy ground (`--hero-bg-start` → `--hero-bg-mid` → `--hero-bg-end`). The sheen and any other motion respect `prefers-reduced-motion`. +- The footer is a single tonal block in `--footer-bg`, no internal gradients. +- Two depth treatments are allowed and only these two. The search input combines a 1px inset highlight (`--search-inset`) with a soft warm drop shadow (`--search-shadow`, intensified by `--search-focus-shadow` on focus). The primary CTA button (`.hero-action-primary`) carries a warm drop shadow for press affordance. Both shadows are soft, warm-tinted, and tied to interactive elements. No new drop shadows on cards, panels, rows, or static decoration. +- No glassmorphism as default decoration. +- No bounce or elastic easing. Real objects decelerate smoothly. + +## Shapes + +The shape language is overwhelmingly **pill on small, zero radius on large**. + +- **Pills** (`border-radius: 999px`) for tags, search, sponsor logo chip, source badges, back-to-top, and primary CTA buttons. +- **`0.4rem`** is used in exactly one place: inline `` inside `.sponsorship-body`. Do not introduce a tokenized radius scale. The project does not need one. +- Containers use the page surface itself, not rounded panels. When a panel is needed, prefer pill on small chips and zero radius on large surfaces. +- **No `border-left` or `border-right` greater than 1px as a colored accent stripe** on cards, list items, callouts, or alerts. Use a different structure. + +## Components + +The component vocabulary is small and table-led. Source of truth: `website/static/style.css`. + +- **Table-driven index** (the hero of the page). Sticky header, sortable columns, click-to-expand rows that indent under the Name column. Modeled on placestoread.xyz. Not a card grid. +- **Filter tags** (`.tag`). `--accent-soft` background with `--accent-deep` text. Pill shape. Hover swaps to `--highlight` background with `--tag-hover-border` border and ink text. Active state uses the warm `--tag-active-start` → `--tag-active-end` gradient with hero-ink text. Tag variants (`tag-group`, `tag-source`) inherit the base `.tag` style today and differ only at narrow widths (`tag-group` hides under 960px). Add a new variant only when a real visual difference is needed. +- **Hero**. Magazine-cover headline, dark earthy ground, kicker and proof microcopy, primary CTA button using `--hero-btn-start` / `--hero-btn-end`. Subtle grid plus slow sheen. Respects `prefers-reduced-motion`. +- **Sponsor band**. Sits in the README header on `--bg-paper-strong`. Editorial layout, not a logo wall. Sponsor links share the global accent treatment. +- **CTA**. Warm `--cta-bg`, full-bleed within shell. The button itself uses accent tokens. +- **Footer**. Dark warm charcoal, part of the same system. Footer links share the global hover and focus treatment. +- **Search**. Pill input with `--search-inset` interior and `--search-focus-ring` focus ring. Focus shadow uses `--search-focus-shadow`. +- **Source badge / inline code**. Static decoration on `--bg-paper-strong`. `.source-badge` uses `--ink-soft` text in pill shape; `.sponsorship-body code` uses `--ink` text with the lone `0.4rem` radius. Never the accent. + +Peer-consistency check (run before shipping any visual change): + +- Hover and focus states: if one link type gets a treatment, peer links (hero topbar, footer, project names, sponsor names, expand-meta) share it. +- Tag variants inherit the base `.tag` style. Differ only where a real difference is needed. +- Typography tiers: labels that play the same role share size, weight, and letter-spacing. +- Symmetric gutters: logo left-gap equals logo right-gap, column paddings match across header and body. +- Role-based color tokens: same role uses the same token everywhere. + +## Do's and Don'ts + +- **Do** keep the table the focal point. Hero, sponsor band, and CTA must not compete. +- **Do** use accent tokens only on interactive elements. +- **Do** prefer density over whitespace expansion. +- **Do** check peer elements before shipping a visual change. +- **Do** use OKLCH for every new color. +- **Don't** add inner `max-width` to anything. The shell handles width. +- **Don't** introduce green, cyan, neon, pure gray, or cool blue. +- **Don't** add a dark mode, theme picker, or alternate palette. +- **Don't** use gradient text (`background-clip: text` on gradients). Solid color only. +- **Don't** use `!important`. Fix specificity instead. +- **Don't** use `text-transform`. Write the casing in markup. +- **Don't** use a `border-left` or `border-right` greater than 1px as an accent stripe. +- **Don't** use bounce or elastic easing. +- **Don't** use glassmorphism as default decoration. +- **Don't** mimic generic dark developer-tool sites, other awesome-\* sites, or SaaS marketing pages. + +## Narrow-Screen Behavior + +The user actively tests `< 960px` and `< 680px`. Narrow screens must stay functional. + +- Do not drop features the user might want (sort affordance, filter chips, sticky header where reasonable). Hiding is a last resort and requires justification. +- Always run the `playwright-cli` skill at a narrow viewport after any layout change. + +## Iteration Guide + +Run this audit after generating or modifying a screen. Failure on any item means revise before moving on. + +1. **Width caps.** Inspect every section, card, paragraph, table cell, expanded row, CTA, sponsor description, hero subcopy. Only `.section-shell` (`--shell-max: 84rem`) may cap width. Anything else with a `max-width` is wrong. +2. **Accent reservation.** Grep the changed CSS for `--accent`, `--accent-deep`, `--accent-soft`. Each match must back an interactive element (link, button, focus ring, filter tag). Static decoration must use ink tokens (`--ink`, `--ink-soft`, `--ink-muted`) on `--bg-paper-strong` or `--bg-paper`. +3. **Shape language.** Containers are square or pill. Anything in the 4px-to-16px radius range is suspect. The lone `0.4rem` on `.sponsorship-body code` is the only allowed exception. +4. **Type sizes.** Confirm no rendered text falls below 12px. If a size feels small to a mid-senior reader on a 27-inch display, bump one step up. Never reduce an existing size. +5. **Peer consistency.** Compare against the closest peer element (sibling link type, sibling tag variant, sibling label). Hover, focus, color token, and gutter must match unless there is a stated reason to differ. +6. **Narrow viewport.** Run the `playwright-cli` skill at `< 960px` and `< 680px`. Sort affordance, filter chips, and sticky header must remain functional. + +## Known Gaps + +- **Color format diverges from the Stitch spec.** The official linter requires hex sRGB (`/^#([0-9a-fA-F]{3,8})$/`). The project mandates OKLCH in `style.css`. The Colors section above resolves this by showing both: OKLCH is canonical, hex is the linter-friendly approximation. +- **YAML frontmatter is minimal.** Only `version`, `name`, and `description` are encoded. The project has no JSON / Figma export pipeline that would consume token-level frontmatter, so the prose-led format is preferred for everything else. +- **No formal spacing or radius scale.** The codebase uses `clamp()` and ad-hoc rem values rather than a tokenized scale. Adding one would be invention, not documentation. + +## Verification + +After any frontend change, use the `playwright-cli` skill to visually verify in a real browser. Check layout, responsiveness, and interactive behavior. Do not claim a UI change works based on code alone. diff --git a/Makefile b/Makefile index 0c42fe4f1f..5a07ad9119 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,32 @@ -BASEDIR=$(CURDIR) -DOCDIR=$(BASEDIR)/docs +-include .env +export install: - pip install mkdocs + uv sync --locked -link: - ln -sf $(BASEDIR)/README.md $(DOCDIR)/index.md +fetch_github_stars: + uv run python website/fetch_github_stars.py -preview: - $(MAKE) link - mkdocs serve +fetch_pypi_downloads: + uv run python website/fetch_pypi_downloads_via_clickpy.py -deploy: - $(MAKE) link - mkdocs gh-deploy --clean +test: + uv run pytest website/tests/ -v + +lint: + uv run ruff check . + +format: + uv run ruff format . + +typecheck: + uv run ty check website + +build: + uv run python website/build.py + +preview: build + uv run watchfiles \ + 'uv run python website/build.py' \ + README.md website/templates website/static website/data & \ + python -m http.server -b 127.0.0.1 -d website/output/ 8000 diff --git a/README.md b/README.md index 16df7591a8..70b1e44561 100644 --- a/README.md +++ b/README.md @@ -1,1239 +1,1180 @@ -# Awesome Python [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) - -A curated list of awesome Python frameworks, libraries, software and resources. - -Inspired by [awesome-php](https://github.com/ziadoz/awesome-php). - -- [Awesome Python](#awesome-python) - - [Environment Management](#environment-management) - - [Package Management](#package-management) - - [Package Repositories](#package-repositories) - - [Distribution](#distribution) - - [Build Tools](#build-tools) - - [Interactive Interpreter](#interactive-interpreter) - - [Files](#files) - - [Date and Time](#date-and-time) - - [Text Processing](#text-processing) - - [Specific Formats Processing](#specific-formats-processing) - - [Natural Language Processing](#natural-language-processing) - - [Documentation](#documentation) - - [Configuration](#configuration) - - [Command-line Tools](#command-line-tools) - - [Downloader](#downloader) - - [Imagery](#imagery) - - [OCR](#ocr) - - [Audio](#audio) - - [Video](#video) - - [Geolocation](#geolocation) - - [HTTP](#http) - - [Database](#database) - - [Database Drivers](#database-drivers) - - [ORM](#orm) - - [Web Frameworks](#web-frameworks) - - [Serverless Frameworks](#serverless-frameworks) - - [Permissions](#permissions) - - [CMS](#cms) - - [E-commerce](#e-commerce) - - [RESTful API](#restful-api) - - [Serialization](#serialization) - - [Authentication](#authentication) - - [Template Engine](#template-engine) - - [Queue](#queue) - - [Search](#search) - - [News Feed](#news-feed) - - [Asset Management](#asset-management) - - [Caching](#caching) - - [Email](#email) - - [Internationalization](#internationalization) - - [URL Manipulation](#url-manipulation) - - [HTML Manipulation](#html-manipulation) - - [Web Crawling](#web-crawling) - - [Web Content Extracting](#web-content-extracting) - - [Forms](#forms) - - [Data Validation](#data-validation) - - [Anti-spam](#anti-spam) - - [Tagging](#tagging) - - [Admin Panels](#admin-panels) - - [Static Site Generator](#static-site-generator) - - [Processes](#processes) - - [Concurrency and Parallelism](#concurrency-and-parallelism) - - [Networking](#networking) - - [WebSocket](#websocket) - - [WSGI Servers](#wsgi-servers) - - [RPC Servers](#rpc-servers) - - [Cryptography](#cryptography) - - [GUI](#gui) - - [Game Development](#game-development) - - [Logging](#logging) - - [Testing](#testing) - - [Code Analysis and Linter](#code-analysis-and-linter) - - [Debugging Tools](#debugging-tools) - - [Science and Data Analysis](#science-and-data-analysis) - - [Data Visualization](#data-visualization) - - [Computer Vision](#computer-vision) - - [Machine Learning](#machine-learning) - - [Functional Programming](#functional-programming) - - [MapReduce](#mapreduce) - - [Third-party APIs](#third-party-apis) - - [DevOps Tools](#devops-tools) - - [Job Scheduler](#job-scheduler) - - [Foreign Function Interface](#foreign-function-interface) - - [High Performance](#high-performance) - - [Network Virtualization and SDN](#network-virtualization-and-sdn) - - [Hardware](#hardware) - - [Compatibility](#compatibility) - - [Miscellaneous](#miscellaneous) - - [Algorithms and Design Patterns](#algorithms-and-design-patterns) - - [Editor Plugins](#editor-plugins) - - [IDEs](#ides) -- [Services](#services) - - [Continuous Integration](#continuous-integration) - - [Code Quality](#code-quality) -- [Resources](#resources) - - [Websites](#websites) - - [Weekly](#weekly) - - [Twitter](#twitter) -- [Other Awesome Lists](#other-awesome-lists) -- [Contributing](#contributing) - -- - - - -## Environment Management - -*Libraries for Python version and environment management.* - -* [p](https://github.com/qw3rtman/p) - Dead simple interactive Python version management. -* [pyenv](https://github.com/yyuu/pyenv) - Simple Python version management. -* [venv](https://docs.python.org/3/library/venv.html) - (Python standard library in Python 3.3+) Creating lightweight virtual environments. -* [virtualenv](https://pypi.python.org/pypi/virtualenv) - A tool to create isolated Python environments. -* [virtualenvwrapper](https://pypi.python.org/pypi/virtualenvwrapper) - A set of extensions to virtualenv. - -## Package Management - -*Libraries for package and dependency management.* - -* [pip](https://pip.pypa.io/en/stable/) - The Python package and dependency manager. - * [Python Package Index](https://pypi.python.org/pypi) -* [pip-tools](https://github.com/nvie/pip-tools) - A set of tools to keep your pinned Python dependencies fresh. -* [conda](https://github.com/conda/conda/) - Cross-platform, Python-agnostic binary package manager. -* [Curdling](http://clarete.li/curdling/) - Curdling is a command line tool for managing Python packages. -* [wheel](http://pythonwheels.com/) - The new standard of Python distribution and are intended to replace eggs. - -## Package Repositories - -*Local PyPI repository server and proxies.* - -* [warehouse](https://github.com/pypa/warehouse) - Next generation Python Package Repository (PyPI). - * [Warehouse](https://warehouse.python.org/) -* [bandersnatch](https://bitbucket.org/pypa/bandersnatch) - PyPI mirroring tool provided by Python Packaging Authority (PyPA). -* [devpi](http://doc.devpi.net/latest/) - PyPI server and packaging/testing/release tool. -* [localshop](https://github.com/mvantellingen/localshop) - Local PyPI server (custom packages and auto-mirroring of pypi). - -## Distribution - -*Libraries to create packaged executables for release distribution.* - -* [PyInstaller](https://github.com/pyinstaller/pyinstaller) - Converts Python programs into stand-alone executables (cross-platform). -* [dh-virtualenv](http://dh-virtualenv.readthedocs.org/en/latest/) - Build and distribute a virtualenv as a Debian package. -* [Nuitka](http://nuitka.net/) - Compile scripts, modules, packages to an executable or extension module. -* [py2app](http://pythonhosted.org/py2app/) - Freezes Python scripts (Mac OS X). -* [py2exe](http://www.py2exe.org/) - Freezes Python scripts (Windows). -* [pynsist](http://pynsist.readthedocs.org/en/latest/) - A tool to build Windows installers, installers bundle Python itself. - -## Build Tools - -*Compile software from source code.* - -* [buildout](http://www.buildout.org/en/latest/) - A build system for creating, assembling and deploying applications from multiple parts. -* [BitBake](http://www.yoctoproject.org/docs/1.6/bitbake-user-manual/bitbake-user-manual.html) - A make-like build tool for embedded Linux. -* [PlatformIO](https://github.com/platformio/platformio) - A console tool to build code with different development platforms. -* [PyBuilder](https://github.com/pybuilder/pybuilder) - A continuous build tool written in pure Python. -* [SCons](http://www.scons.org/) - A software construction tool. - -## Interactive Interpreter - -*Interactive Python interpreters (REPL).* - -* [Jupyter Notebook (IPython)](https://jupyter.org) - A rich toolkit to help you make the most out of using Python interactively. -* [bpython](https://github.com/bpython/bpython) – A fancy interface to the Python interpreter. -* [ptpython](https://github.com/jonathanslenders/ptpython) - Advanced Python REPL built on top of the [python-prompt-toolkit](https://github.com/jonathanslenders/python-prompt-toolkit). - -## Files - -*Libraries for file manipulation and MIME type detection.* - -* [imghdr](https://docs.python.org/2/library/imghdr.html) - (Python standard library) Determine the type of an image. -* [mimetypes](https://docs.python.org/2/library/mimetypes.html) - (Python standard library) Map filenames to MIME types. -* [path.py](https://github.com/jaraco/path.py) - A module wrapper for [os.path](https://docs.python.org/2/library/os.path.html). -* [pathlib](https://pathlib.readthedocs.org/en/pep428/) - (Python standard library in Python 3.4+) An cross-platform, object-oriented path library. -* [python-magic](https://github.com/ahupp/python-magic) - A Python interface to the libmagic file type identification library. -* [Unipath](https://github.com/mikeorr/Unipath) - An object-oriented approach to file/directory operations. -* [watchdog](https://github.com/gorakhargosh/watchdog) - API and shell utilities to monitor file system events. - -## Date and Time - -*Libraries for working with dates and times.* - -* [arrow](https://github.com/crsmithdev/arrow) - Better dates & times for Python. -* [Chronyk](https://github.com/KoffeinFlummi/Chronyk) - A Python 3 library for parsing human-written times and dates. -* [dateutil](https://github.com/dateutil/dateutil) - Extensions to the standard Python [datetime](https://docs.python.org/2/library/datetime.html) module. -* [delorean](https://github.com/myusuf3/delorean/) - A library for clearing up the inconvenient truths that arise dealing with datetimes. -* [moment](https://github.com/zachwill/moment) - A Python library for dealing with dates/times. Inspired by [Moment.js](http://momentjs.com/). -* [PyTime](https://github.com/shinux/PyTime) - A easy-use Python module which aims to operate date/time/datetime by string. -* [pytz](https://launchpad.net/pytz) - World timezone definitions, modern and historical. Brings the [tz database](https://en.wikipedia.org/wiki/Tz_database) into Python. -* [when.py](https://github.com/dirn/When.py) - Providing user-friendly functions to help perform common date and time actions. - -## Text Processing - -*Libraries for parsing and manipulating plain texts.* - -* General - * [chardet](https://github.com/chardet/chardet) - Python 2/3 compatible character encoding detector. - * [difflib](https://docs.python.org/2/library/difflib.html) - (Python standard library) Helpers for computing deltas. - * [ftfy](https://github.com/LuminosoInsight/python-ftfy) - Makes Unicode text less broken and more consistent automagically. - * [fuzzywuzzy](https://github.com/seatgeek/fuzzywuzzy) - Fuzzy String Matching. - * [Levenshtein](https://github.com/ztane/python-Levenshtein/) - Fast computation of Levenshtein distance and string similarity. - * [pangu.py](https://github.com/vinta/pangu.py) - Spacing texts for CJK and alphanumerics. - * [pyfiglet](https://github.com/pwaller/pyfiglet) - An implementation of figlet written in Python. - * [shortuuid](https://github.com/stochastic-technologies/shortuuid) - A generator library for concise, unambiguous and URL-safe UUIDs. - * [unidecode](https://pypi.python.org/pypi/Unidecode) - ASCII transliterations of Unicode text. - * [uniout](https://github.com/moskytw/uniout) - Print readable chars instead of the escaped string. - * [xpinyin](https://github.com/lxneng/xpinyin) - A library to translate Chinese hanzi (漢字) to pinyin (拼音). -* Slugify - * [awesome-slugify](https://github.com/dimka665/awesome-slugify) - A Python slugify library that can preserve unicode. - * [python-slugify](https://github.com/un33k/python-slugify) - A Python slugify library that translates unicode to ASCII. - * [unicode-slugify](https://github.com/mozilla/unicode-slugify) - A slugifier that generates unicode slugs with Django as a dependency. -* Parser - * [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) - Parsing, formatting, storing and validating international phone numbers. - * [PLY](http://www.dabeaz.com/ply/) - Implementation of lex and yacc parsing tools for Python - * [Pygments](http://pygments.org/) - A generic syntax highlighter. - * [pyparsing](http://pyparsing.wikispaces.com/) - A general purpose framework for generating parsers. - * [python-nameparser](https://github.com/derek73/python-nameparser) - Parsing human names into their individual components. - * [python-user-agents](https://github.com/selwin/python-user-agents) - Browser user agent parser. - * [sqlparse](https://sqlparse.readthedocs.org/en/latest/) - A non-validating SQL parser. - -## Specific Formats Processing - -*Libraries for parsing and manipulating specific text formats.* - -* General - * [tablib](https://github.com/kennethreitz/tablib) - A module for Tabular Datasets in XLS, CSV, JSON, YAML. -* Office - * [Marmir](https://github.com/brianray/mm) - Takes Python data structures and turns them into spreadsheets. - * [openpyxl](https://openpyxl.readthedocs.org/en/latest/) - A library for reading and writing Excel 2010 xlsx/xlsm/xltx/xltm files. - * [pyexcel](http://pyexcel.readthedocs.org/en/latest/) - Providing one API for reading, manipulating and writing csv, ods, xls, xlsx and xlsm files. - * [python-docx](https://github.com/python-openxml/python-docx) - Reads, queries and modifies Microsoft Word 2007/2008 docx files. - * [relatorio](http://relatorio.tryton.org/) - Templating OpenDocument files. - * [unoconv](https://github.com/dagwieers/unoconv) - Convert between any document format supported by LibreOffice/OpenOffice. - * [XlsxWriter](https://xlsxwriter.readthedocs.org/en/latest/) - A Python module for creating Excel .xlsx files. - * [xlwings](http://xlwings.org/) - A BSD-licensed library that makes it easy to call Python from Excel and vice versa. - * [xlwt](https://github.com/python-excel/xlwt) / [xlrd](https://github.com/python-excel/xlrd) - Writing and reading data and formatting information from Excel files. -* PDF - * [PDFMiner](https://github.com/euske/pdfminer) - A tool for extracting information from PDF documents. - * [PyPDF2](https://github.com/mstamy2/PyPDF2) - A library capable of splitting, merging and transforming PDF pages. - * [ReportLab](http://www.reportlab.com/opensource/) - Allowing Rapid creation of rich PDF documents. -* Markdown - * [Mistune](https://github.com/lepture/mistune) - Fastest and full featured pure Python parsers of Markdown. - * [Python-Markdown](https://github.com/waylan/Python-Markdown) - A Python implementation of John Gruber’s Markdown. -* YAML - * [PyYAML](http://pyyaml.org/) - YAML implementations for Python. -* CSV - * [csvkit](https://github.com/wireservice/csvkit) - Utilities for converting to and working with CSV. -* Archive - * [unp](https://github.com/mitsuhiko/unp) - A command line tool that can unpack archives easily. - -## Natural Language Processing - -*Libraries for working with human languages.* - -* [NLTK](http://www.nltk.org/) - A leading platform for building Python programs to work with human language data. -* [Pattern](http://www.clips.ua.ac.be/pattern) - A web mining module for the Python. -* [Jieba](https://github.com/fxsjy/jieba) - Chinese text segmentation. -* [SnowNLP](https://github.com/isnowfy/snownlp) - A library for processing Chinese text. -* [spaCy](https://spacy.io/) - A library for industrial-strength natural language processing in Python and Cython. -* [TextBlob](http://textblob.readthedocs.org/en/latest/) - Providing a consistent API for diving into common NLP tasks. -* [TextGrocery](https://github.com/2shou/TextGrocery) - A simple, efficient short-text classification tool based on LibLinear and Jieba. -* [langid.py](https://github.com/saffsd/langid.py) - Stand-alone language identification system. - -## Documentation - -*Libraries for generating project documentation.* - -* [Sphinx](http://www.sphinx-doc.org/en/latest/) - Python Documentation generator. - * [awesome-sphinxdoc](https://github.com/yoloseem/awesome-sphinxdoc) -* [MkDocs](http://www.mkdocs.org/) - Markdown friendly documentation generator. -* [pdoc](https://github.com/BurntSushi/pdoc) - Epydoc replacement to auto generate API documentation for Python libraries. -* [Pycco](https://github.com/pycco-docs/pycco) - The literate-programming-style documentation generator. - -## Configuration - -*Libraries for storing and parsing configuration options.* - -* [config](https://www.red-dove.com/config-doc/) - Hierarchical config from the author of [logging](https://docs.python.org/2/library/logging.html). -* [ConfigObj](http://www.voidspace.org.uk/python/configobj.html) - INI file parser with validation. -* [ConfigParser](https://docs.python.org/2/library/configparser.html) - (Python standard library) INI file parser. -* [profig](http://profig.readthedocs.org/en/default/) - Config from multiple formats with value conversion. -* [python-decouple](https://github.com/henriquebastos/python-decouple) - Strict separation of settings from code. - -## Command-line Tools - -*Libraries for building command-line application.* - -* Command-line Application Development - * [asciimatics](https://github.com/peterbrittain/asciimatics) - Cross-platform, full-screen terminal package (i.e. mouse/keyboard input and coloured, positioned text output) complete with high-level API for complex animations and special effects. - * [cement](http://builtoncement.com/) - CLI Application Framework for Python. - * [click](http://click.pocoo.org/dev/) - A package for creating beautiful command line interfaces in a composable way. - * [cliff](http://docs.openstack.org/developer/cliff/) - A framework for creating command-line programs with multi-level commands. - * [clint](https://github.com/kennethreitz/clint) - Python Command-line Application Tools. - * [colorama](https://pypi.python.org/pypi/colorama) - Cross-platform colored terminal text. - * [docopt](http://docopt.org/) - Pythonic command line arguments parser. - * [Gooey](https://github.com/chriskiehl/Gooey) - Turn command line programs into a full GUI application with one line - * [python-prompt-toolkit](https://github.com/jonathanslenders/python-prompt-toolkit) - A Library for building powerful interactive command lines. -* Productivity Tools - * [aws-cli](https://github.com/aws/aws-cli) - A universal command-line interface for Amazon Web Services. - * [bashplotlib](https://github.com/glamp/bashplotlib) - Making basic plots in the terminal. - * [caniusepython3](https://github.com/brettcannon/caniusepython3) - Determine what projects are blocking you from porting to Python 3. - * [cookiecutter](https://github.com/audreyr/cookiecutter) - A command-line utility that creates projects from cookiecutters (project templates). - * [doitlive](https://github.com/sloria/doitlive) - A tool for live presentations in the terminal. - * [howdoi](https://github.com/gleitz/howdoi) - Instant coding answers via the command line. - * [httpie](https://github.com/jkbrzt/httpie) - A command line HTTP client, a user-friendly cURL replacement. - * [PathPicker](https://github.com/facebook/PathPicker) - Select files out of bash output. - * [percol](https://github.com/mooz/percol) - Adds flavor of interactive selection to the traditional pipe concept on UNIX. - * [SAWS](https://github.com/donnemartin/saws) - A Supercharged AWS CLI. - * [thefuck](https://github.com/nvbn/thefuck) - Correcting your previous console command. - * [try](https://github.com/timofurrer/try) - A dead simple CLI to try out python packages - It's never been easier. - * [mycli](https://github.com/dbcli/mycli) - A Terminal Client for MySQL with AutoCompletion and Syntax Highlighting. - * [pgcli](https://github.com/dbcli/pgcli) - Postgres CLI with autocompletion and syntax highlighting. - -## Downloader - -*Libraries for downloading.* - -* [s3cmd](https://github.com/s3tools/s3cmd) - A command line tool for managing Amazon S3 and CloudFront. -* [s4cmd](https://github.com/bloomreach/s4cmd) - Super S3 command line tool, good for higher performance. -* [you-get](https://www.soimort.org/you-get/) - A YouTube/Youku/Niconico video downloader written in Python 3. -* [youtube-dl](http://rg3.github.io/youtube-dl/) - A small command-line program to download videos from YouTube. - -## Imagery - -*Libraries for manipulating images.* - -* [pillow](http://pillow.readthedocs.org/en/latest/) - Pillow is the friendly [PIL](http://www.pythonware.com/products/pil/) fork. -* [hmap](https://github.com/rossgoodwin/hmap) - Image histogram remapping. -* [imgSeek](https://sourceforge.net/projects/imgseek/) - A project for searching a collection of images using visual similarity. -* [nude.py](https://github.com/hhatto/nude.py) - Nudity detection. -* [pyBarcode](https://pythonhosted.org/pyBarcode/) - Create barcodes in Python without needing PIL. -* [pygram](https://github.com/ajkumar25/pygram) - Instagram-like image filters. -* [python-qrcode](https://github.com/lincolnloop/python-qrcode) - A pure Python QR Code generator. -* [Quads](https://github.com/fogleman/Quads) - Computer art based on quadtrees. -* [scikit-image](http://scikit-image.org/) - A Python library for (scientific) image processing. -* [thumbor](https://github.com/thumbor/thumbor) - A smart imaging service. It enables on-demand crop, re-sizing and flipping of images. -* [wand](https://github.com/dahlia/wand) - Python bindings for [MagickWand](http://www.imagemagick.org/script/magick-wand.php), C API for ImageMagick. - -## OCR - -*Libraries for Optical Character Recognition.* - -* [pyocr](https://github.com/jflesch/pyocr) - A wrapper for Tesseract and Cuneiform. -* [pytesseract](https://github.com/madmaze/pytesseract) - Another wrapper for [Google Tesseract OCR](https://github.com/tesseract-ocr). - -## Audio - -*Libraries for manipulating audio.* - -* [audiolazy](https://github.com/danilobellini/audiolazy) - Expressive Digital Signal Processing (DSP) package for Python. -* [audioread](https://github.com/beetbox/audioread) - Cross-library (GStreamer + Core Audio + MAD + FFmpeg) audio decoding. -* [beets](http://beets.io/) - A music library manager and [MusicBrainz](https://musicbrainz.org/) tagger. -* [dejavu](https://github.com/worldveil/dejavu) - Audio fingerprinting and recognition. -* [django-elastic-transcoder](https://github.com/StreetVoice/django-elastic-transcoder) - Django + [Amazon Elastic Transcoder](http://aws.amazon.com/elastictranscoder/). -* [eyeD3](http://eyed3.nicfit.net/) - A tool for working with audio files, specifically MP3 files containing ID3 metadata. -* [id3reader](http://nedbatchelder.com/code/modules/id3reader.py) - A Python module for reading MP3 meta data. -* [m3u8](https://github.com/globocom/m3u8) - A module for parsing m3u8 file. -* [mutagen](https://bitbucket.org/lazka/mutagen) - A Python module to handle audio metadata. -* [pydub](https://github.com/jiaaro/pydub) - Manipulate audio with a simple and easy high level interface. -* [pyechonest](https://github.com/echonest/pyechonest) - Python client for the [Echo Nest](http://developer.echonest.com/) API. -* [talkbox](http://scikits.appspot.com/talkbox) - A Python library for speech/signal processing. -* [TimeSide](https://github.com/Parisson/TimeSide) - Open web audio processing framework. -* [tinytag](https://github.com/devsnd/tinytag) - A library for reading music meta data of MP3, OGG, FLAC and Wave files. -* [mingus](http://bspaans.github.io/python-mingus/) - An advanced music theory and notation package with MIDI file and playback support. - -## Video - -*Libraries for manipulating video and GIFs.* - -* [moviepy](http://zulko.github.io/moviepy/) - A module for script-based movie editing with many formats, including animated GIFs. -* [scikit-video](https://github.com/aizvorski/scikit-video) - Video processing routines for SciPy. - -## Geolocation - -*Libraries for geocoding addresses and working with latitudes and longitudes.* - -* [GeoDjango](https://docs.djangoproject.com/en/dev/ref/contrib/gis/) - A world-class geographic web framework. -* [GeoIP](https://github.com/maxmind/geoip-api-python) - Python API for MaxMind GeoIP Legacy Database. -* [geojson](https://github.com/frewsxcv/python-geojson) - Python bindings and utilities for GeoJSON. -* [geopy](https://github.com/geopy/geopy) - Python Geocoding Toolbox. -* [pygeoip](https://github.com/appliedsec/pygeoip) - Pure Python GeoIP API. -* [django-countries](https://github.com/SmileyChris/django-countries) - A Django app that provides country choices for use with forms, flag icons static files, and a country field for models. - -## HTTP - -*Libraries for working with HTTP.* - -* [requests](http://docs.python-requests.org/en/latest/) - HTTP Requests for Humans™. -* [grequests](https://github.com/kennethreitz/grequests) - requests + gevent for asynchronous HTTP requests. -* [httplib2](https://github.com/jcgregorio/httplib2) - Comprehensive HTTP client library. -* [treq](https://github.com/twisted/treq) - Python requests like API built on top of Twisted's HTTP client. -* [urllib3](https://github.com/shazow/urllib3) - A HTTP library with thread-safe connection pooling, file post support, sanity friendly. - -## Database - -*Databases implemented in Python.* - -* [pickleDB](https://pythonhosted.org/pickleDB/) - A simple and lightweight key-value store for Python. -* [PipelineDB](https://www.pipelinedb.com/) - The Streaming SQL Database. -* [TinyDB](https://github.com/msiemens/tinydb) - A tiny, document-oriented database. -* [ZODB](http://www.zodb.org/en/latest/) - A native object database for Python. A key-value and object graph database. - -## Database Drivers - -*Libraries for connecting and operating databases.* - -* MySQL - [awesome-mysql](http://shlomi-noach.github.io/awesome-mysql/) - * [mysql-python](https://sourceforge.net/projects/mysql-python/) - The MySQL database connector for Python. - * [mysqlclient](https://github.com/PyMySQL/mysqlclient-python) - mysql-python fork supporting Python 3. - * [oursql](https://pythonhosted.org/oursql/) - A better MySQL connector with support for native prepared statements and BLOBs. - * [PyMySQL](https://github.com/PyMySQL/PyMySQL) - Pure Python MySQL driver compatible to mysql-python. -* PostgreSQL - * [psycopg2](http://initd.org/psycopg/) - The most popular PostgreSQL adapter for Python. - * [queries](https://github.com/gmr/queries) - A wrapper of the psycopg2 library for interacting with PostgreSQL. - * [txpostgres](http://txpostgres.readthedocs.org/en/latest/) - Twisted based asynchronous driver for PostgreSQL. -* Other Relational Databases - * [apsw](http://rogerbinns.github.io/apsw/) - Another Python SQLite wrapper. - * [dataset](https://github.com/pudo/dataset) - Store Python dicts in a database - works with SQLite, MySQL, and PostgreSQL. - * [pymssql](http://www.pymssql.org/en/latest/) - A simple database interface to Microsoft SQL Server. -* NoSQL Databases - * [cassandra-python-driver](https://github.com/datastax/python-driver) - Python driver for Cassandra. - * [HappyBase](http://happybase.readthedocs.org/en/latest/) - A developer-friendly library for Apache HBase. - * [Plyvel](https://plyvel.readthedocs.org/en/latest/) - A fast and feature-rich Python interface to LevelDB. - * [py2neo](http://py2neo.org/2.0/) - Python wrapper client for Neo4j's restful interface. - * [pycassa](https://github.com/pycassa/pycassa) - Python Thrift driver for Cassandra. - * [PyMongo](https://docs.mongodb.org/ecosystem/drivers/python/) - The official Python client for MongoDB. - * [redis-py](https://github.com/andymccurdy/redis-py) - The Redis Python Client. - * [telephus](https://github.com/driftx/Telephus) - Twisted based client for Cassandra. - * [txRedis](https://github.com/deldotdr/txRedis) - Twisted based client for Redis. - -## ORM - -*Libraries that implement Object-Relational Mapping or data mapping techniques.* - -* Relational Databases - * [Django Models](https://docs.djangoproject.com/en/dev/topics/db/models/) - A part of Django. - * [SQLAlchemy](http://www.sqlalchemy.org/) - The Python SQL Toolkit and Object Relational Mapper. - * [awesome-sqlalchemy](https://github.com/dahlia/awesome-sqlalchemy) - * [Peewee](https://github.com/coleifer/peewee) - A small, expressive ORM. - * [PonyORM](https://ponyorm.com/) - ORM that provides a generator-oriented interface to SQL. - * [python-sql](https://pypi.python.org/pypi/python-sql) - Write SQL queries pythonically. -* NoSQL Databases - * [django-mongodb-engine](https://github.com/django-nonrel/mongodb-engine) - Django MongoDB Backend. - * [PynamoDB](https://github.com/jlafon/PynamoDB) - A Pythonic interface for [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). - * [flywheel](https://github.com/mathcamp/flywheel) - Object mapper for Amazon DynamoDB. - * [MongoEngine](http://mongoengine.org/) - A Python Object-Document-Mapper for working with MongoDB. - * [hot-redis](https://github.com/stephenmcd/hot-redis) - Rich Python data types for Redis. - * [redisco](https://github.com/kiddouk/redisco) - A Python Library for Simple Models and Containers Persisted in Redis. -* Others - * [butterdb](https://github.com/Widdershin/butterdb) - A Python ORM for Google Drive Spreadsheets. - -## Web Frameworks - -*Full stack web frameworks.* - -* [Django](https://www.djangoproject.com/) - The most popular web framework in Python. - * [awesome-django](https://github.com/rosarior/awesome-django) -* [Flask](http://flask.pocoo.org/) - A microframework for Python. - * [awesome-flask](https://github.com/humiaozuzu/awesome-flask) -* [Pyramid](http://www.pylonsproject.org/) - A small, fast, down-to-earth, open source Python web framework. - * [awesome-pyramid](https://github.com/uralbash/awesome-pyramid) -* [Bottle](http://bottlepy.org/docs/dev/index.html) - A fast, simple and lightweight WSGI micro web-framework. -* [CherryPy](http://www.cherrypy.org/) - A minimalist Python web framework, HTTP/1.1-compliant and WSGI thread-pooled. -* [TurboGears](http://www.turbogears.org/) - A microframework that can scale up to a full stack solution. -* [web.py](http://webpy.org/) - A web framework for Python that is as simple as it is powerful. -* [web2py](http://www.web2py.com) - A full stack web framework and platform focused in the ease of use. -* [Tornado](http://www.tornadoweb.org/en/latest/) - A Web framework and asynchronous networking library. - -## Serverless Frameworks - -*Frameworks for developing serverless Python code.* - -* [apex](https://github.com/apex/apex) - Build, deploy, and manage [AWS Lambda](https://aws.amazon.com/lambda/) functions with ease. -* [Zappa](https://github.com/Miserlou/Zappa) - A tool for deploying WSGI applications on AWS Lambda and API Gateway. -* [python-lambda](https://github.com/nficano/python-lambda) - A toolkit for developing and deploying Python code in AWS Lambda. - -## Permissions - -*Libraries that allow or deny users access to data or functionality.* - -* [Carteblanche](https://github.com/neuman/python-carteblanche/) - Module to align code with thoughts of users and designers. Also magically handles navigation and permissions. -* [django-guardian](https://github.com/django-guardian/django-guardian) - Implementation of per object permissions for Django 1.2+ -* [django-rules](https://github.com/dfunckt/django-rules) - A tiny but powerful app providing object-level permissions to Django, without requiring a database. - -## CMS - -*Content Management Systems.* - -* [django-cms](http://www.django-cms.org/en/) - An Open source enterprise CMS based on the Django. -* [djedi-cms](http://djedi-cms.org/) - A lightweight but yet powerful Django CMS with plugins, inline editing and performance in mind. -* [FeinCMS](http://www.feincms.org/) - One of the most advanced Content Management Systems built on Django. -* [Kotti](http://kotti.pylonsproject.org/) - A high-level, Pythonic web application framework built on Pyramid. -* [Mezzanine](http://mezzanine.jupo.org/) - A powerful, consistent, and flexible content management platform. -* [Opps](http://opps.github.io/opps/) - A Django-based CMS for magazines, newspapers websites and portals with high-traffic. -* [Plone](https://plone.org/) - A CMS built on top of the open source application server Zope. -* [Quokka](http://quokkaproject.org/) - Flexible, extensible, small CMS powered by Flask and MongoDB. -* [Wagtail](https://wagtail.io/) - A Django content management system. -* [Widgy](https://wid.gy/) - Last CMS framework, based on Django. - -## E-commerce - -*Frameworks and libraries for e-commerce and payments.* - -* [django-oscar](http://oscarcommerce.com/) - An open-source e-commerce framework for Django. -* [django-shop](https://github.com/awesto/django-shop) - A Django based shop system. -* [Cartridge](https://github.com/stephenmcd/cartridge) - A shopping cart app built using the Mezzanine. -* [shoop](https://www.shoop.io/en/) - An open source E-Commerce platform based on Django. -* [alipay](https://github.com/lxneng/alipay) - Unofficial Alipay API for Python. -* [merchant](https://github.com/agiliq/merchant) - A Django app to accept payments from various payment processors. -* [money](https://github.com/carlospalol/money) - Money class with optional CLDR-backed locale-aware formatting and an extensible currency exchange solution. -* [python-currencies](https://github.com/Alir3z4/python-currencies) - Display money format and its filthy currencies. - -## RESTful API - -*Libraries for developing RESTful APIs.* - -* Django - * [django-rest-framework](http://www.django-rest-framework.org/) - A powerful and flexible toolkit to build web APIs. - * [django-tastypie](http://tastypieapi.org/) - Creating delicious APIs for Django apps. - * [django-formapi](https://github.com/5monkeys/django-formapi) - Create JSON APIs with Django's form validation. -* Flask - * [flask-api](http://www.flaskapi.org/) - Browsable Web APIs for Flask. - * [flask-restful](http://flask-restful.readthedocs.org/en/latest/) - Quickly building REST APIs for Flask. - * [flask-restless](https://flask-restless.readthedocs.org/en/latest/) - Generating RESTful APIs for database models defined with SQLAlchemy. - * [flask-api-utils](https://github.com/marselester/flask-api-utils) - Taking care of API representation and authentication for Flask. - * [eve](https://github.com/nicolaiarocci/eve) - REST API framework powered by Flask, MongoDB and good intentions. -* Pyramid - * [cornice](https://cornice.readthedocs.org/en/latest/) - A REST framework for Pyramid. -* Framework agnostic - * [falcon](http://falconframework.org/) - A high-performance framework for building cloud APIs and web app backends. - * [hug](https://github.com/timothycrosley/hug) - A Python3 framework for cleanly exposing APIs over HTTP and the Command Line with automatic documentation and validation. - * [sandman](https://github.com/jeffknupp/sandman) - Automated REST APIs for existing database-driven systems. - * [restless](http://restless.readthedocs.org/en/latest/) - Framework agnostic REST framework based on lessons learned from Tastypie. - * [ripozo](https://github.com/vertical-knowledge/ripozo) - Quickly creating REST/HATEOAS/Hypermedia APIs. - -## Serialization - -*Libraries for serializing complex data types* - -* [marshmallow](http://marshmallow.readthedocs.org/en/latest/) - marshmallow is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes. - -## Authentication - -*Libraries for implementing authentications schemes.* - -* OAuth - * [Authomatic](http://peterhudec.github.io/authomatic/) - Simple but powerful framework agnostic authentication/authorization client. - * [django-allauth](https://github.com/pennersr/django-allauth) - Authentication app for Django that "just works." - * [django-oauth-toolkit](https://github.com/evonove/django-oauth-toolkit) - OAuth2 goodies for the Djangonauts. - * [django-oauth2-provider](https://github.com/caffeinehit/django-oauth2-provider) - Providing OAuth2 access to Django app. - * [Flask-OAuthlib](https://github.com/lepture/flask-oauthlib) - OAuth 1.0/a, 2.0 implementation of client and provider for Flask. - * [OAuthLib](https://github.com/idan/oauthlib) - A generic and thorough implementation of the OAuth request-signing logic. - * [python-oauth2](https://github.com/joestump/python-oauth2) - A fully tested, abstract interface to creating OAuth clients and servers. - * [python-social-auth](https://github.com/omab/python-social-auth) - An easy-to-setup social authentication mechanism. - * [rauth](https://github.com/litl/rauth) - A Python library for OAuth 1.0/a, 2.0, and Ofly. - * [sanction](https://github.com/demianbrecht/sanction) - A dead simple OAuth2 client implementation. -* Others - * [jose](https://github.com/demonware/jose) - JavaScript Object Signing and Encryption draft implementation. - * [PyJWT](https://github.com/jpadilla/pyjwt) - Implementation of the JSON Web Token draft 01. - * [python-jws](https://github.com/brianloveswords/python-jws) - Implementation of JSON Web Signatures draft 02. - * [python-jwt](https://github.com/davedoesdev/python-jwt) - Module for generating and verifying JSON Web Tokens. +# [Awesome Python](https://awesome-python.com/) -## Template Engine +An opinionated guide to the best Python frameworks, libraries, and tools. -*Libraries and tools for templating and lexing.* +**Visit the [website](https://awesome-python.com/) to search and filter projects more easily.** -* [Jinja2](https://github.com/pallets/jinja) - A modern and designer friendly templating language. -* [Chameleon](https://chameleon.readthedocs.org/en/latest/) - An HTML/XML template engine. Modeled after ZPT, optimized for speed. -* [Genshi](https://genshi.edgewall.org/) - Python templating toolkit for generation of web-aware output. -* [Mako](http://www.makotemplates.org/) - Hyperfast and lightweight templating for the Python platform. +## **Sponsors** -## Queue +> The **#10 most-starred repo on GitHub**. Put your product in front of Python developers. [Become a sponsor](SPONSORSHIP.md). -*Libraries for working with event and task queues.* +## Categories -* [celery](http://www.celeryproject.org/) - An asynchronous task queue/job queue based on distributed message passing. -* [huey](https://github.com/coleifer/huey) - Little multi-threaded task queue. -* [mrq](https://github.com/pricingassistant/mrq) - Mr. Queue - A distributed worker task queue in Python using Redis & gevent. -* [rq](http://python-rq.org/) - Simple job queues for Python. -* [simpleq](https://github.com/rdegges/simpleq) - A simple, infinitely scalable, Amazon SQS based queue. - -## Search - -*Libraries and software for indexing and performing search queries on data.* - -* [django-haystack](https://github.com/django-haystack/django-haystack) - Modular search for Django. -* [elasticsearch-py](https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html) - The official low-level Python client for [Elasticsearch](https://www.elastic.co/products/elasticsearch). -* [elasticsearch-dsl-py](https://github.com/elastic/elasticsearch-dsl-py) - The official high-level Python client for Elasticsearch. -* [esengine](https://github.com/catholabs/esengine) - ElasticSearch ODM (Object Document Mapper) for Python. -* [solrpy](https://github.com/edsu/solrpy) - A Python client for [solr](http://lucene.apache.org/solr/). -* [Whoosh](http://whoosh.readthedocs.org/en/latest/) - A fast, pure Python search engine library. - -## News Feed - -*Libraries for building user's activities.* - -* [django-activity-stream](https://github.com/justquick/django-activity-stream) - Generating generic activity streams from the actions on your site. -* [Stream-Framework](https://github.com/tschellenbach/Stream-Framework) - Building newsfeed and notification systems using Cassandra and Redis. - -## Asset Management - -*Tools for managing, compressing and minifying website assets.* - -* [django-compressor](https://github.com/django-compressor/django-compressor) - Compresses linked and inline JavaScript or CSS into a single cached file. -* [django-storages](http://django-storages.readthedocs.org/en/latest/) - A collection of custom storage back ends for Django. -* [fanstatic](http://www.fanstatic.org/en/latest/) - Packages, optimizes, and serves static file dependencies as Python packages. -* [File Conveyor](http://fileconveyor.org/) - A daemon to detect and sync files to CDNs, S3 and FTP. -* [Flask-Assets](http://flask-assets.readthedocs.org/en/latest/) - Helps you integrate webassets into your Flask app. -* [jinja-assets-compressor](https://github.com/jaysonsantos/jinja-assets-compressor) - A Jinja extension to compile and compress your assets. -* [webassets](http://webassets.readthedocs.org/en/latest/) - Bundles, optimizes, and manages unique cache-busting URLs for static resources. - -## Caching - -*Libraries for caching data.* - -* [Beaker](http://beaker.readthedocs.org/en/latest/) - A library for caching and sessions for use with web applications and stand-alone Python scripts and applications. -* [django-cache-machine](https://github.com/django-cache-machine/django-cache-machine) - Automatic caching and invalidation for Django models. -* [django-cacheops](https://github.com/Suor/django-cacheops) - A slick ORM cache with automatic granular event-driven invalidation. -* [django-viewlet](https://github.com/5monkeys/django-viewlet) - Render template parts with extended cache control. -* [dogpile.cache](http://dogpilecache.readthedocs.org/en/latest/) - dogpile.cache is next generation replacement for Beaker made by same authors. -* [HermesCache](https://pypi.python.org/pypi/HermesCache) - Python caching library with tag-based invalidation and dogpile effect prevention. -* [johnny-cache](https://github.com/jmoiron/johnny-cache) - A caching framework for django applications. -* [pylibmc](https://github.com/lericson/pylibmc) - A Python wrapper around the [libmemcached](http://libmemcached.org/libMemcached.html) interface. -* [DiskCache](http://www.grantjenks.com/docs/diskcache/) - SQLite and file backed cache backend with faster lookups than memcached and redis. - -## Email +**AI & ML** -*Libraries for sending and parsing email.* +- [AI and Agents](#ai-and-agents) +- [Deep Learning](#deep-learning) +- [Machine Learning](#machine-learning) +- [Natural Language Processing](#natural-language-processing) +- [Computer Vision](#computer-vision) +- [Recommender Systems](#recommender-systems) -* [envelopes](http://tomekwojcik.github.io/envelopes/) - Mailing for human beings. -* [flanker](https://github.com/mailgun/flanker) - A email address and Mime parsing library. -* [imbox](https://github.com/martinrusev/imbox) - Python IMAP for Humans. -* [inbox.py](https://github.com/kennethreitz/inbox.py) - Python SMTP Server for Humans. -* [lamson](https://github.com/zedshaw/lamson) - Pythonic SMTP Application Server. -* [Marrow Mailer](https://github.com/marrow/mailer) - High-performance extensible mail delivery framework. -* [modoboa](https://github.com/tonioo/modoboa) - A mail hosting and management platform including a modern and simplified Web UI. -* [Nylas Sync Engine](https://github.com/nylas/sync-engine) - Providing a RESTful API on top of a powerful email sync platform. -* [yagmail](https://github.com/kootenpv/yagmail) - Yet another Gmail/SMTP client. +**Web Development** -## Internationalization +- [Web Frameworks](#web-frameworks) +- [Web APIs](#web-apis) +- [Web Servers](#web-servers) +- [WebSocket](#websocket) +- [Template Engines](#template-engines) +- [Web Asset Management](#web-asset-management) +- [Authentication](#authentication) +- [Admin Panels](#admin-panels) +- [CMS](#cms) +- [ERP](#erp) +- [Static Site Generators](#static-site-generators) -*Libraries for working with i18n.* +**HTTP & Scraping** -* [Babel](http://babel.pocoo.org/en/latest/) - An internationalization library for Python. -* [Korean](https://korean.readthedocs.org/en/latest/) - A library for [Korean](https://en.wikipedia.org/wiki/Korean_language) morphology. +- [HTTP Clients](#http-clients) +- [Web Scraping](#web-scraping) +- [Email](#email) -## URL Manipulation +**Database & Storage** -*Libraries for parsing URLs.* +- [ORM](#orm) +- [Database Drivers](#database-drivers) +- [Database](#database) +- [Caching](#caching) +- [Search](#search) +- [Serialization](#serialization) -* [furl](https://github.com/gruns/furl) - A small Python library that makes manipulating URLs simple. -* [purl](https://github.com/codeinthehole/purl) - A simple, immutable URL class with a clean API for interrogation and manipulation. -* [pyshorteners](https://github.com/ellisonleao/pyshorteners) - A pure Python URL shortening lib. -* [short_url](https://github.com/Alir3z4/python-short_url) - Python implementation for generating Tiny URL and bit.ly-like URLs. -* [webargs](https://github.com/sloria/webargs) - A friendly library for parsing HTTP request arguments, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, and Pyramid. +**Data & Science** -## HTML Manipulation +- [Data Analysis](#data-analysis) +- [Data Ingestion / ETL](#data-ingestion--etl) +- [Data Validation](#data-validation) +- [Data Visualization](#data-visualization) +- [Geolocation](#geolocation) +- [Science](#science) +- [Quantum Computing](#quantum-computing) + +**Developer Tools** + +- [Algorithms and Design Patterns](#algorithms-and-design-patterns) +- [Interactive Interpreter](#interactive-interpreter) +- [Code Analysis](#code-analysis) +- [Testing](#testing) +- [Debugging Tools](#debugging-tools) +- [Build Tools](#build-tools) +- [Documentation](#documentation) + +**DevOps** + +- [DevOps Tools](#devops-tools) +- [Distributed Computing](#distributed-computing) +- [Task Queues](#task-queues) +- [Messaging](#messaging) +- [Job Schedulers](#job-schedulers) +- [Logging](#logging) +- [Network Virtualization](#network-virtualization) + +**CLI & GUI** + +- [CLI Development](#cli-development) +- [CLI Tools](#cli-tools) +- [GUI Development](#gui-development) + +**Text & Documents** + +- [Text Processing](#text-processing) +- [HTML Manipulation](#html-manipulation) +- [File Format Processing](#file-format-processing) +- [File Manipulation](#file-manipulation) + +**Media** + +- [Image Processing](#image-processing) +- [Audio & Video Processing](#audio--video-processing) +- [Game Development](#game-development) + +**Python Language** + +- [Implementations](#implementations) +- [Built-in Classes Enhancement](#built-in-classes-enhancement) +- [Functional Programming](#functional-programming) +- [Asynchronous Programming](#asynchronous-programming) +- [Date and Time](#date-and-time) + +**Python Toolchain** + +- [Environment Management](#environment-management) +- [Package Management](#package-management) +- [Package Repositories](#package-repositories) +- [Distribution](#distribution) +- [Configuration Files](#configuration-files) + +**Security** + +- [Cryptography](#cryptography) +- [Penetration Testing](#penetration-testing) +- [Supply Chain Security](#supply-chain-security) +- [Web Security](#web-security) + +**Other** + +- [Hardware](#hardware) +- [Microsoft Windows](#microsoft-windows) +- [Miscellaneous](#miscellaneous) + +## Projects + +**AI & ML** + +### AI and Agents + +_Libraries for building AI applications, LLM integrations, and autonomous agents._ + +- Agent Skills + - [django-ai-plugins](https://github.com/vintasoftware/django-ai-plugins) - Django backend agent skills for Django, DRF, Celery, and Django-specific code review. + - [sentry-skills](https://github.com/getsentry/skills) - Python-focused engineering skills for code review, debugging, and backend workflows. + - [trailofbits-skills](https://github.com/trailofbits/skills) - Python-friendly security skills for auditing, testing, and safer backend development. +- Orchestration + - [langchain](https://github.com/langchain-ai/langchain) - Building applications with LLMs through composability. + - [langgraph](https://github.com/langchain-ai/langgraph) - Low-level orchestration framework for building stateful, long-running LLM agents. + - [crewai](https://github.com/crewAIInc/crewAI) - A framework for orchestrating role-playing autonomous AI agents for collaborative task solving. + - [pydantic-ai](https://github.com/pydantic/pydantic-ai) - A Python agent framework for building generative AI applications with structured schemas. +- Vendor Agent SDKs + - [openai-agents](https://github.com/openai/openai-agents-python) - OpenAI's framework for building and managing AI agents. + - [claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-python) - Anthropic's Python SDK for building AI agents on Claude Code's harness — custom tools, in-process MCP servers, hooks. +- Personal Assistants + - [hermes-agent](https://github.com/nousresearch/hermes-agent) - An adaptive personal AI assistant that grows with you. +- Prompt Optimization + - [dspy](https://github.com/stanfordnlp/dspy) - A framework for programming, not prompting, language models. +- Data Layer + - [instructor](https://github.com/567-labs/instructor) - A library for extracting structured data from LLMs, powered by Pydantic. + - [llama-index](https://github.com/run-llama/llama_index) - A data framework for your LLM application. + - [mem0](https://github.com/mem0ai/mem0) - An intelligent memory layer for AI agents enabling personalized interactions. +- Pre-trained Models + - [transformers](https://github.com/huggingface/transformers) - A framework that lets you easily use pre-trained transformer models for NLP, vision, and audio tasks. +- LLM Inference and Serving + - [sglang](https://github.com/sgl-project/sglang) - A high-performance serving framework for large language models and multimodal models. + - [vllm](https://github.com/vllm-project/vllm) - A high-throughput and memory-efficient inference and serving engine for LLMs. + - [mlx-lm](https://github.com/ml-explore/mlx-lm) - Run and fine-tune large language models on Apple Silicon with MLX. +- LLM Gateways + - [LiteLLM](https://github.com/BerriAI/litellm) - Call 100+ LLMs using OpenAI format. +- Image and Video Generation + - [diffusers](https://github.com/huggingface/diffusers) - A library that provides pre-trained diffusion models for generating and editing images, audio, and video. +- Fine-tuning + - [unsloth](https://github.com/unslothai/unsloth) - A library for faster LLM fine-tuning and training with reduced memory usage. +- Speech + - [openai-whisper](https://github.com/openai/whisper) - A general-purpose automatic speech recognition model trained on 680k hours of multilingual and multitask supervised data. + - [funasr](https://github.com/modelscope/FunASR) - Industrial-grade speech recognition toolkit with 170x realtime speed, 50+ languages, speaker diarization, and emotion detection. + - [vibevoice](https://github.com/microsoft/VibeVoice) - A family of open-source voice AI models from Microsoft for text-to-speech and long-form speech recognition. + - [gTTS](https://github.com/pndurette/gTTS) - Python library and CLI tool for converting text to speech using Google Translate TTS. + - [kittentts](https://github.com/KittenML/KittenTTS) - Lightweight ONNX text-to-speech library with small CPU-friendly models. + +### Deep Learning + +_Frameworks for Neural Networks and Deep Learning. Also see [awesome-deep-learning](https://github.com/ChristosChristofidis/awesome-deep-learning)._ + +- Frameworks + - [pytorch](https://github.com/pytorch/pytorch) - Tensors and Dynamic neural networks in Python with strong GPU acceleration. + - [tensorflow](https://github.com/tensorflow/tensorflow) - The most popular Deep Learning framework created by Google. + - [keras](https://github.com/keras-team/keras) - A high-level deep learning library with support for JAX, TensorFlow, and PyTorch backends. + - [jax](https://github.com/jax-ml/jax) - A library for high-performance numerical computing with automatic differentiation and JIT compilation. + - [pytorch-lightning](https://github.com/Lightning-AI/pytorch-lightning) - Deep learning framework to train, deploy, and ship AI products Lightning fast. +- Reinforcement Learning + - [gymnasium](https://github.com/Farama-Foundation/Gymnasium) - A standard API for reinforcement learning environments with popular reference environments ([gym](https://github.com/openai/gym) successor). + - [stable-baselines3](https://github.com/DLR-RM/stable-baselines3) - PyTorch implementations of Stable Baselines (deep) reinforcement learning algorithms. + +### Machine Learning + +_Libraries for Machine Learning. Also see [awesome-machine-learning](https://github.com/josephmisiti/awesome-machine-learning#python)._ + +- General + - [scikit-learn](https://github.com/scikit-learn/scikit-learn) - The most popular Python library for Machine Learning with extensive documentation and community support. + - [pgmpy](https://github.com/pgmpy/pgmpy) - A Python library for probabilistic graphical models and Bayesian networks. + - [feature-engine](https://github.com/feature-engine/feature_engine) - sklearn compatible API with the widest toolset for feature engineering and selection. +- Gradient Boosting + - [xgboost](https://github.com/dmlc/xgboost) - A scalable, portable, and distributed gradient boosting library. + - [lightgbm](https://github.com/lightgbm-org/LightGBM) - A fast, distributed, high performance gradient boosting framework. + - [catboost](https://github.com/catboost/catboost) - A fast, scalable, high performance gradient boosting on decision trees library. +- Time Series Forecasting + - [timesfm](https://github.com/google-research/timesfm) - A pretrained foundation model from Google Research for time-series forecasting. + +### Natural Language Processing + +_Libraries for working with human languages._ + +- General + - [nltk](https://github.com/nltk/nltk) - A leading platform for building Python programs to work with human language data. + - [spacy](https://github.com/explosion/spaCy) - A library for industrial-strength natural language processing in Python and Cython. + - [gensim](https://github.com/piskvorky/gensim) - Topic Modeling for Humans. + - [stanza](https://github.com/stanfordnlp/stanza) - The Stanford NLP Group's official Python library, supporting 60+ languages. +- Chinese + - [jieba](https://github.com/fxsjy/jieba) - The most popular Chinese text segmentation library. + - [pypinyin](https://github.com/mozillazg/python-pinyin) - Convert Chinese hanzi (漢字) to pinyin (拼音). + - [pangu.py](https://github.com/vinta/pangu.py) - Paranoid text spacing. + +### Computer Vision + +_Libraries for Computer Vision._ + +- General + - [opencv-python](https://github.com/opencv/opencv-python) - Open Source Computer Vision Library. + - [ultralytics](https://github.com/ultralytics/ultralytics) - Ultralytics YOLO for object detection, segmentation, pose estimation, and classification with state-of-the-art accuracy and speed. + - [kornia](https://github.com/kornia/kornia/) - Open Source Differentiable Computer Vision Library for PyTorch. + - [fiftyone](https://github.com/voxel51/fiftyone) - The open-source tool for building high-quality datasets and computer vision models. +- OCR + - [pytesseract](https://github.com/madmaze/pytesseract) - A wrapper for [Google Tesseract OCR](https://github.com/tesseract-ocr). + - [easyocr](https://github.com/JaidedAI/EasyOCR) - Ready-to-use OCR with 40+ languages supported. + +### Recommender Systems + +_Libraries for building recommender systems._ + +- [annoy](https://github.com/spotify/annoy) - Approximate Nearest Neighbors in C++/Python optimized for memory usage. +- [implicit](https://github.com/benfred/implicit) - A fast Python implementation of collaborative filtering for implicit datasets. +- [scikit-surprise](https://github.com/NicolasHug/Surprise) - A scikit for building and analyzing recommender systems. + +**Web Development** + +### Web Frameworks + +_Traditional full stack web frameworks. Also see [Web APIs](#web-apis)._ + +- Synchronous + - [flask](https://github.com/pallets/flask) - A microframework for Python. + - [awesome-flask](https://github.com/humiaozuzu/awesome-flask) + - [django](https://github.com/django/django) - The most popular web framework in Python. + - [awesome-django](https://github.com/wsvincent/awesome-django) + - [bottle](https://github.com/bottlepy/bottle) - A fast and simple micro-framework distributed as a single file with no dependencies. + - [pyramid](https://github.com/Pylons/pyramid) - A small, fast, down-to-earth, open source Python web framework. + - [awesome-pyramid](https://github.com/uralbash/awesome-pyramid) + - [fasthtml](https://github.com/AnswerDotAI/fasthtml) - The fastest way to create an HTML app. + - [awesome-fasthtml](https://github.com/amosgyamfi/awesome-fasthtml) +- Asynchronous + - [starlette](https://github.com/Kludex/starlette) - A lightweight ASGI framework and toolkit for building high-performance async services. + - [tornado](https://github.com/tornadoweb/tornado) - A web framework and asynchronous networking library. + - [litestar](https://github.com/litestar-org/litestar) - Production-ready, capable and extensible ASGI Web framework. + - [reflex](https://github.com/reflex-dev/reflex) - A framework for building reactive, full-stack web applications entirely with Python. -*Libraries for working with HTML and XML.* +### Web APIs -* [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) - Providing Pythonic idioms for iterating, searching, and modifying HTML or XML. -* [bleach](http://bleach.readthedocs.org/en/latest/) - A whitelist-based HTML sanitization and text linkification library. -* [cssutils](https://pypi.python.org/pypi/cssutils/) - A CSS library for Python. -* [html5lib](https://github.com/html5lib/html5lib-python) - A standards-compliant library for parsing and serializing HTML documents and fragments. -* [lxml](http://lxml.de/) - A very fast, easy-to-use and versatile library for handling HTML and XML. -* [MarkupSafe](https://github.com/pallets/markupsafe) - Implements a XML/HTML/XHTML Markup safe string for Python. -* [pyquery](https://github.com/gawel/pyquery) - A jQuery-like library for parsing HTML. -* [untangle](https://github.com/stchris/untangle) - Converts XML documents to Python objects for easy access. -* [xhtml2pdf](https://github.com/xhtml2pdf/xhtml2pdf) - HTML/CSS to PDF converter. -* [xmltodict](https://github.com/martinblech/xmltodict) - Working with XML feel like you are working with JSON. +_Libraries for building RESTful, GraphQL, and RPC APIs._ -## Web Crawling +- Django + - [django-rest-framework](https://github.com/encode/django-rest-framework) - A powerful and flexible toolkit to build web APIs. + - [django-ninja](https://github.com/vitalik/django-ninja) - Fast, Django REST framework based on type hints and Pydantic. + - [strawberry-django](https://github.com/strawberry-graphql/strawberry-django) - Strawberry GraphQL integration with Django. + - [django-modern-rest](https://github.com/wemake-services/django-modern-rest) - Modern REST with speed, types, async, `msgspec`, `pydantic` and other goodies! +- Flask + - [apiflask](https://github.com/apiflask/apiflask) - A lightweight Python web API framework based on Flask and Marshmallow. +- Framework Agnostic + - [fastapi](https://github.com/fastapi/fastapi) - A modern, fast, web framework for building APIs with standard Python type hints. + - [connexion](https://github.com/spec-first/connexion) - A spec-first framework that automatically handles requests based on your OpenAPI specification. + - [strawberry](https://github.com/strawberry-graphql/strawberry) - A GraphQL library that leverages Python type annotations for schema definition. +- RPC + - [grpcio](https://github.com/grpc/grpc) - HTTP/2-based RPC framework with Python bindings, built by Google. -*Libraries for scraping websites.* +### Web Servers -* [Scrapy](http://scrapy.org/) - A fast high-level screen scraping and web crawling framework. -* [cola](https://github.com/chineking/cola) - A distributed crawling framework. -* [Demiurge](https://github.com/matiasb/demiurge) - PyQuery-based scraping micro-framework. -* [feedparser](http://pythonhosted.org/feedparser/) - Universal feed parser. -* [Grab](http://grablib.org/) - Site scraping framework. -* [MechanicalSoup](https://github.com/hickford/MechanicalSoup) - A Python library for automating interaction with websites. -* [portia](https://github.com/scrapinghub/portia) - Visual scraping for Scrapy. -* [pyspider](https://github.com/binux/pyspider) - A powerful spider system. -* [RoboBrowser](https://github.com/jmcarp/robobrowser) - A simple, Pythonic library for browsing the web without a standalone web browser. +_ASGI and WSGI compatible web servers._ -## Web Content Extracting +- ASGI + - [uvicorn](https://github.com/Kludex/uvicorn) - A lightning-fast ASGI server implementation, using uvloop and httptools. + - [granian](https://github.com/emmett-framework/granian) - A Rust HTTP server for Python applications built on top of Hyper and Tokio, supporting WSGI/ASGI/RSGI. + - [hypercorn](https://github.com/pgjones/hypercorn) - An ASGI and WSGI Server based on Hyper libraries and inspired by Gunicorn. +- WSGI + - [gunicorn](https://github.com/benoitc/gunicorn) - Pre-forked, ported from Ruby's Unicorn project. + - [waitress](https://github.com/Pylons/waitress) - Multi-threaded, powers Pyramid. -*Libraries for extracting web contents.* +### WebSocket -* [Haul](https://github.com/vinta/Haul) - An Extensible Image Crawler. -* [html2text](https://github.com/Alir3z4/html2text) - Convert HTML to Markdown-formatted text. -* [lassie](https://github.com/michaelhelmick/lassie) - Web Content Retrieval for Humans. -* [micawber](https://github.com/coleifer/micawber) - A small library for extracting rich content from URLs. -* [newspaper](https://github.com/codelucas/newspaper) - News extraction, article extraction and content curation in Python. -* [opengraph](https://github.com/erikriver/opengraph) - A Python module to parse the Open Graph Protocol -* [python-goose](https://github.com/grangier/python-goose) - HTML Content/Article Extractor. -* [python-readability](https://github.com/buriy/python-readability) - Fast Python port of arc90's readability tool. -* [sanitize](https://github.com/Alir3z4/python-sanitize) - Bringing sanity to world of messed-up data. -* [sumy](https://github.com/miso-belica/sumy) - A module for automatic summarization of text documents and HTML pages. -* [textract](https://github.com/deanmalmgren/textract) - Extract text from any document, Word, PowerPoint, PDFs, etc. +_Libraries for working with WebSocket._ -## Forms +- [websockets](https://github.com/python-websockets/websockets) - A library for building WebSocket servers and clients with a focus on correctness and simplicity. +- [channels](https://github.com/django/channels) - Developer-friendly asynchrony for Django. +- [flask-socketio](https://github.com/miguelgrinberg/Flask-SocketIO) - Socket.IO integration for Flask applications. +- [autobahn-python](https://github.com/crossbario/autobahn-python) - WebSocket & WAMP for Python on Twisted and [asyncio](https://docs.python.org/3/library/asyncio.html). -*Libraries for working with forms.* +### Template Engines -* [Deform](http://deform.readthedocs.org/en/latest/) - Python HTML form generation library influenced by the formish form generation library. -* [django-bootstrap3](https://github.com/dyve/django-bootstrap3) - Bootstrap 3 integration with Django. -* [django-crispy-forms](http://django-crispy-forms.readthedocs.org/en/latest/) - A Django app which lets you create beautiful forms in a very elegant and DRY way. -* [django-remote-forms](https://github.com/WiserTogether/django-remote-forms) - A platform independent Django form serializer. -* [WTForms](http://wtforms.readthedocs.org/en/latest/) - A flexible forms validation and rendering library. -* [WTForms-JSON](http://wtforms-json.readthedocs.org/en/latest/) - A WTForms extension for JSON data handling. +_Libraries and tools for templating and lexing._ -## Data Validation +- [jinja](https://github.com/pallets/jinja) - A modern and designer friendly templating language. +- [mako](https://github.com/sqlalchemy/mako) - Hyperfast and lightweight templating for the Python platform. -*Libraries for validating data. Used for forms in many cases.* +### Web Asset Management -* [Schematics](https://github.com/schematics/schematics) - Data Structure Validation. -* [schema](https://github.com/keleshev/schema) - A library for validating Python data structures. -* [jsonschema](https://github.com/Julian/jsonschema) - An implementation of [JSON Schema](http://json-schema.org/) for Python. -* [Cerberus](https://github.com/nicolaiarocci/cerberus/) - A lightweight and extensible data validation library. -* [colander](http://docs.pylonsproject.org/projects/colander/en/latest/) - Validating and deserializing data obtained via XML, JSON, an HTML form post. -* [voluptuous](https://github.com/alecthomas/voluptuous) - A Python data validation library. -* [valideer](https://github.com/podio/valideer) - Lightweight extensible data validation and adaptation library. +_Tools for managing, storing, compressing and minifying website assets._ -## Anti-spam +- [django-storages](https://github.com/jschneier/django-storages) - A collection of custom storage back ends for Django. +- [django-compressor](https://github.com/django-compressor/django-compressor) - Compresses linked and inline JavaScript or CSS into a single cached file. -*Libraries for fighting spam.* +### Authentication -* [django-simple-captcha](https://github.com/mbi/django-simple-captcha) - A simple and highly customizable Django app to add captcha images to any Django form. -* [django-simple-spam-blocker](https://github.com/moqada/django-simple-spam-blocker) - Simple spam blocker for Django. +_Libraries for implementing authentication schemes._ -## Tagging +- OAuth + - [oauthlib](https://github.com/oauthlib/oauthlib) - A generic and thorough implementation of the OAuth request-signing logic. + - [authlib](https://github.com/authlib/authlib) - A comprehensive library for building OAuth, OpenID Connect, and JWT/JWS/JWE/JWK/JWA. + - [django-allauth](https://github.com/pennersr/django-allauth) - Authentication app for Django that "just works." + - [django-oauth-toolkit](https://github.com/django-oauth/django-oauth-toolkit) - OAuth 2 goodies for Django. +- JWT + - [pyjwt](https://github.com/jpadilla/pyjwt) - JSON Web Token implementation in Python. +- Permissions + - [django-guardian](https://github.com/django-guardian/django-guardian) - Implementation of per-object permissions for Django. + - [django-rules](https://github.com/dfunckt/django-rules) - A tiny but powerful app providing object-level permissions to Django, without requiring a database. -*Libraries for tagging items.* +### Admin Panels -* [django-taggit](https://github.com/alex/django-taggit) - Simple tagging for Django. +_Libraries for administrative interfaces._ -## Admin Panels +- [flask-admin](https://github.com/pallets-eco/flask-admin) - Simple and extensible administrative interface framework for Flask. +- [django-unfold](https://github.com/unfoldadmin/django-unfold) - Elevate your Django admin with a stunning modern interface, powerful features, and seamless user experience. +- [django-grappelli](https://github.com/sehmaschine/django-grappelli) - A jazzy skin for the Django Admin-Interface. -*Libraries for administrative interfaces.* +### CMS -* [Ajenti](https://github.com/Eugeny/ajenti) - The admin panel your servers deserve. -* [django-suit](http://djangosuit.com/) - Alternative Django Admin-Interface (free only for Non-commercial use). -* [django-xadmin](https://github.com/sshwsfc/xadmin) - Drop-in replacement of Django admin comes with lots of goodies. -* [flask-admin](https://github.com/flask-admin/flask-admin) - Simple and extensible administrative interface framework for Flask. -* [flower](https://github.com/mher/flower) - Real-time monitor and web admin for Celery. -* [Grappelli](http://grappelliproject.com) – A jazzy skin for the Django Admin-Interface. -* [Wooey](https://github.com/wooey/wooey) - A Django app which creates automatic web UIs for Python scripts. +_Content Management Systems._ -## Static Site Generator +- [wagtail](https://github.com/wagtail/wagtail) - A Django content management system. +- [django-cms](https://github.com/django-cms/django-cms) - The easy-to-use and developer-friendly enterprise CMS powered by Django. -*Static site generator is a software that takes some text + templates as input and produces HTML files on the output.* +### ERP -* [Pelican](http://blog.getpelican.com/) - Uses Markdown or ReST for content and Jinja 2 for themes. Supports DVCS, Disqus. AGPL. -* [Cactus](https://github.com/koenbok/Cactus/) – Static site generator for designers. -* [Hyde](http://hyde.github.io/) - Jinja2-based static web site generator. -* [Nikola](https://www.getnikola.com/) - A static website and blog generator. -* [Tinkerer](http://tinkerer.me/) - Tinkerer is a blogging engine/.static website generator powered by Sphinx. -* [Lektor](https://www.getlektor.com/) - An easy to use static CMS and blog engine. +_Enterprise resource planning frameworks._ -## Processes +- [odoo](https://github.com/odoo/odoo) - A suite of open source business apps: CRM, e-commerce, accounting, inventory, and thousands of community modules. -*Libraries for starting and communicating with OS processes.* +### Static Site Generators -* [envoy](https://github.com/kennethreitz/envoy) - Python [subprocess](https://docs.python.org/2/library/subprocess.html) for Humans™. -* [sarge](http://sarge.readthedocs.org/en/latest/) - Yet another wrapper for subprocess. -* [sh](https://github.com/amoffat/sh) - A full-fledged subprocess replacement for Python. +_Static site generator is a software that takes some text + templates as input and produces HTML files on the output._ -## Concurrency and Parallelism +- [pelican](https://github.com/getpelican/pelican) - Static site generator that supports Markdown and reST syntax. +- [nikola](https://github.com/getnikola/nikola) - A static website and blog generator. -*Libraries for concurrent and parallel execution.* +**HTTP & Scraping** -* [multiprocessing](https://docs.python.org/2/library/multiprocessing.html) - (Python standard library) Process-based "threading" interface. -* [threading](https://docs.python.org/2/library/threading.html) - (Python standard library) Higher-level threading interface. -* [eventlet](http://eventlet.net/) - Asynchronous framework with WSGI support. -* [gevent](http://www.gevent.org/) - A coroutine-based Python networking library that uses [greenlet](https://github.com/python-greenlet/greenlet). -* [Tomorrow](https://github.com/madisonmay/Tomorrow) - Magic decorator syntax for asynchronous code. +### HTTP Clients -## Networking +_Libraries for working with HTTP._ -*Libraries for networking programming.* +- Clients + - [requests](https://github.com/psf/requests) - HTTP Requests for Humans. + - [httpx](https://github.com/encode/httpx) - A next generation HTTP client for Python. + - [aiohttp](https://github.com/aio-libs/aiohttp) - Asynchronous HTTP client/server framework for asyncio and Python. + - [urllib3](https://github.com/urllib3/urllib3) - A HTTP library with thread-safe connection pooling, file post support, sanity friendly. +- URL Manipulation + - [yarl](https://github.com/aio-libs/yarl) - Yet another URL library. + - [httpx.URL](https://www.python-httpx.org/api/) - The immutable URL class bundled with HTTPX. -* [asyncio](https://docs.python.org/3/library/asyncio.html) - (Python standard library) Asynchronous I/O, event loop, coroutines and tasks. -* [Twisted](https://twistedmatrix.com/trac/) - An event-driven networking engine. -* [pulsar](https://github.com/quantmind/pulsar) - Event-driven concurrent framework for Python. -* [diesel](https://github.com/dieseldev/diesel) - Greenlet-based event I/O Framework for Python. -* [pyzmq](http://zeromq.github.io/pyzmq/) - A Python wrapper for the ZeroMQ message library. -* [txZMQ](https://github.com/smira/txZMQ) - Twisted based wrapper for the ZeroMQ message library. +### Web Scraping -## WebSocket +_Libraries to automate web scraping and extract web content._ -*Libraries for working with WebSocket.* +- Frameworks + - [browser-use](https://github.com/browser-use/browser-use) - Make websites accessible for AI agents with easy browser automation. + - [scrapy](https://github.com/scrapy/scrapy) - A fast high-level screen scraping and web crawling framework. + - [crawl4ai](https://github.com/unclecode/crawl4ai) - An open-source, LLM-friendly web crawler that provides lightning-fast, structured data extraction specifically designed for AI agents. +- Content Extraction + - [feedparser](https://github.com/kurtmckee/feedparser) - Universal feed parser. + - [html2text](https://github.com/Alir3z4/html2text) - Convert HTML to Markdown-formatted text. + - [trafilatura](https://github.com/adbar/trafilatura) - A tool for gathering text and metadata from the web, with built-in content filtering. -* [AutobahnPython](https://github.com/crossbario/autobahn-python) - WebSocket & WAMP for Python on Twisted and [asyncio](https://docs.python.org/3/library/asyncio.html). -* [Crossbar](https://github.com/crossbario/crossbar/) - Open-source Unified Application Router (Websocket & WAMP for Python on Autobahn). -* [django-socketio](https://github.com/stephenmcd/django-socketio) - WebSockets for Django. -* [WebSocket-for-Python](https://github.com/Lawouach/WebSocket-for-Python) - WebSocket client and server library for Python 2 and 3 as well as PyPy. +### Email -## WSGI Servers +_Libraries for sending and parsing email, and mail server management._ -*WSGI-compatible web servers.* - -* [gunicorn](https://pypi.python.org/pypi/gunicorn) - Pre-forked, partly written in C. -* [uwsgi](https://uwsgi-docs.readthedocs.org/en/latest/) - A project aims at developing a full stack for building hosting services, written in C. -* [bjoern](https://pypi.python.org/pypi/bjoern) - Asynchronous, very fast and written in C. -* [fapws3](http://www.fapws.org/) - Asynchronous (network side only), written in C. -* [meinheld](https://pypi.python.org/pypi/meinheld) - Asynchronous, partly written in C. -* [netius](https://github.com/hivesolutions/netius) - Asynchronous, very fast. -* [paste](http://pythonpaste.org/) - Multi-threaded, stable, tried and tested. -* [rocket](https://pypi.python.org/pypi/rocket) - Multi-threaded. -* [waitress](https://waitress.readthedocs.org/en/latest/) - Multi-threaded, poweres Pyramid. -* [Werkzeug](http://werkzeug.pocoo.org/) - A WSGI utility library for Python that powers Flask and can easily be embedded into your own projects. - -## RPC Servers - -*RPC-compatible servers.* - -* [SimpleJSONRPCServer](https://github.com/joshmarshall/jsonrpclib/) - This library is an implementation of the JSON-RPC specification. -* [SimpleXMLRPCServer](https://docs.python.org/2/library/simplexmlrpcserver.html) - (Python standard library) Simple XML-RPC server implementation, single-threaded. -* [zeroRPC](https://github.com/0rpc/zerorpc-python) - zerorpc is a flexible RPC implementation based on [ZeroMQ](http://zeromq.org/) and [MessagePack](http://msgpack.org/). - -## Cryptography - -* [cryptography](https://cryptography.io/en/latest/) - A package designed to expose cryptographic primitives and recipes to Python developers. -* [hashids](https://github.com/davidaurelio/hashids-python) - Implementation of [hashids](http://hashids.org) in Python. -* [Paramiko](http://www.paramiko.org/) - A Python (2.6+, 3.3+) implementation of the SSHv2 protocol, providing both client and server functionality. -* [Passlib](https://pythonhosted.org/passlib/) - Secure password storage/hashing library, very high level. -* [PyCrypto](https://www.dlitz.net/software/pycrypto/) - The Python Cryptography Toolkit. -* [PyNacl](https://github.com/pyca/pynacl) - Python binding to the Networking and Cryptography (NaCl) library. - -## GUI - -*Libraries for working with graphical user interface applications.* - -* [curses](https://docs.python.org/2/library/curses.html#module-curses) - Built-in wrapper for [ncurses](http://www.gnu.org/software/ncurses/) used to create terminal GUI applications. -* [enaml](https://github.com/nucleic/enaml) - Creating beautiful user-interfaces with Declaratic Syntax like QML. -* [kivy](https://kivy.org/) - A library for creating NUI applications, running on Windows, Linux, Mac OS X, Android and iOS. -* [pyglet](https://bitbucket.org/pyglet/pyglet/wiki/Home) - A cross-platform windowing and multimedia library for Python. -* [PyQt](https://riverbankcomputing.com/software/pyqt/intro) - Python bindings for the [Qt](http://www.qt.io/) cross-platform application and UI framework, with support for both Qt v4 and Qt v5 frameworks. -* [PySide](https://wiki.qt.io/PySide) - Python bindings for the [Qt](http://www.qt.io/) cross-platform application and UI framework, supporting the Qt v4 framework. -* [Tkinter](https://wiki.python.org/moin/TkInter) - Tkinter is Python's de-facto standard GUI package. -* [Toga](https://github.com/pybee/toga) - A Python native, OS native GUI toolkit. -* [urwid](http://urwid.org/) - A library for creating terminal GUI applications with strong support for widgets, events, rich colors, etc. -* [wxPython](http://wxpython.org/) - A blending of the wxWidgets C++ class library with the Python. -* [PyGObject](https://wiki.gnome.org/Projects/PyGObject) - Python Bindings for GLib/GObject/GIO/GTK+ (GTK+3) -* [Flexx](https://github.com/zoofIO/flexx) - Flexx is a pure Python toolkit for creating GUI's, that uses web technology for its rendering. - -## Game Development - -*Awesome game development libraries.* - -* [Cocos2d](http://cocos2d.org/) - cocos2d is a framework for building 2D games, demos, and other graphical/interactive applications. It is based on pyglet. -* [Panda3D](https://www.panda3d.org/) - 3D game engine developed by Disney and maintained by Carnegie Mellon's Entertainment Technology Center. Written in C++, completely wrapped in Python. -* [Pygame](http://www.pygame.org/news.html) - Pygame is a set of Python modules designed for writing games. -* [PyOgre](http://www.ogre3d.org/tikiwiki/PyOgre) - Python bindings for the Ogre 3D render engine, can be used for games, simulations, anything 3D. -* [PyOpenGL](http://pyopengl.sourceforge.net/) - Python ctypes bindings for OpenGL and it's related APIs. -* [PySDL2](http://pysdl2.readthedocs.org/en/latest/) - A ctypes based wrapper for the SDL2 library. -* [RenPy](https://www.renpy.org/) - A Visual Novel engine. - -## Logging - -*Libraries for generating and working with logs.* - -* [logging](https://docs.python.org/2/library/logging.html) - (Python standard library) Logging facility for Python. -* [logbook](http://pythonhosted.org/Logbook/) - Logging replacement for Python. -* [Eliot](https://eliot.readthedocs.org/en/latest/) - Logging for complex & distributed systems. -* [Raven](http://raven.readthedocs.org/en/latest/) - The Python client for Sentry. -* [Sentry](https://pypi.python.org/pypi/sentry) - A realtime logging and aggregation server. - -## Testing - -*Libraries for testing codebases and generating test data.* - -* Testing Frameworks - * [unittest](https://docs.python.org/2/library/unittest.html) - (Python standard library) Unit testing framework. - * [nose](https://nose.readthedocs.org/en/latest/) - nose extends unittest. - * [pytest](http://pytest.org/latest/) - A mature full-featured Python testing tool. - * [hypothesis](https://github.com/HypothesisWorks/hypothesis-python) - Hypothesis is an advanced Quickcheck style property based testing library. - * [mamba](http://nestorsalceda.github.io/mamba/) - The definitive testing tool for Python. Born under the banner of BDD. - * [Robot Framework](https://github.com/robotframework/robotframework) - A generic test automation framework. -* Test Runners - * [tox](https://tox.readthedocs.org/en/latest/) - Auto builds and tests distributions in multiple Python versions - * [green](https://github.com/CleanCut/green) - A clean, colorful test runner. -* GUI / Web Testing - * [Selenium](https://pypi.python.org/pypi/selenium) - Python bindings for [Selenium](http://www.seleniumhq.org/) WebDriver. - * [locust](https://github.com/locustio/locust) - Scalable user load testing tool written in Python. - * [sixpack](https://github.com/seatgeek/sixpack) - A language-agnostic A/B Testing framework. - * [splinter](https://splinter.readthedocs.org/en/latest/) - Open source tool for testing web applications. - * [PyAutoGUI](https://github.com/asweigart/pyautogui) - PyAutoGUI is a cross-platform GUI automation Python module for human beings. -* Mock - * [mock](https://docs.python.org/3/library/unittest.mock.html) - (Python standard library) A mocking and patching library. - * [doublex](https://pypi.python.org/pypi/doublex) - Powerful test doubles framework for Python. - * [freezegun](https://github.com/spulec/freezegun) - Travel through time by mocking the datetime module. - * [httmock](https://github.com/patrys/httmock) - A mocking library for requests for Python 2.6+ and 3.2+. - * [httpretty](http://falcao.it/HTTPretty/) - HTTP request mock tool for Python. - * [responses](https://github.com/getsentry/responses) - A utility library for mocking out the requests Python library. - * [VCR.py](https://github.com/kevin1024/vcrpy) - Record and replay HTTP interactions on your tests. -* Object Factories - * [factory_boy](https://github.com/rbarrois/factory_boy) - A test fixtures replacement for Python. - * [mixer](https://github.com/klen/mixer) - Another fixtures replacement. Supported Django, Flask, SQLAlchemy, Peewee and etc. - * [model_mommy](https://github.com/vandersonmota/model_mommy) - Creating random fixtures for testing in Django. -* Code Coverage - * [coverage](https://pypi.python.org/pypi/coverage) - Code coverage measurement. -* Fake Data - * [faker](http://www.joke2k.net/faker/) - A Python package that generates fake data. - * [fake2db](https://github.com/emirozer/fake2db) - Fake database generator. - * [radar](https://pypi.python.org/pypi/radar) - Generate random datetime / time. -* Error Handler - * [FuckIt.py](https://github.com/ajalt/fuckitpy) - FuckIt.py uses state-of-the-art technology to make sure your Python code runs whether it has any right to or not. - - -## Code Analysis and Linter - -*Libraries and tools for analysing, parsing and manipulation codebases.* - -* Code Analysis - * [code2flow](https://github.com/scottrogowski/code2flow) - Turn your Python and JavaScript code into DOT flowcharts. - * [pycallgraph](https://github.com/gak/pycallgraph) - A library that visualises the flow (call graph) of your Python application. - * [pysonar2](https://github.com/yinwang0/pysonar2) - A type inferencer and indexer for Python. - * [coala](http://coala-analyzer.org/) - Language independent and easily extendable code analysis application. -* Linter - * [Flake8](https://pypi.python.org/pypi/flake8) - The modular source code checker: pep8, pyflakes and co. - * [Pylint](https://www.pylint.org/) - A Fully customizable source code analyzer. - * [pylama](https://pylama.readthedocs.org/en/latest/) - Code audit tool for Python and JavaScript. - -## Debugging Tools - -*Libraries for debugging code.* - -* Debugger - * [ipdb](https://pypi.python.org/pypi/ipdb) - IPython-enabled [pdb](https://docs.python.org/2/library/pdb.html). - * [pudb](https://pypi.python.org/pypi/pudb) – A full-screen, console-based Python debugger. - * [pyringe](https://github.com/google/pyringe) - Debugger capable of attaching to and injecting code into Python processes. - * [wdb](https://github.com/Kozea/wdb) - An improbable web debugger through WebSockets. - * [winpdb](http://winpdb.org/) - A Python Debugger with GUI, capable of remote debugging based on `rpdb2`. - * [django-debug-toolbar](https://github.com/django-debug-toolbar/django-debug-toolbar) - Display various debug information for Django. - * [django-devserver](https://github.com/dcramer/django-devserver) - A drop-in replacement for Django's runserver. - * [flask-debugtoolbar](https://github.com/mgood/flask-debugtoolbar) - A port of the django-debug-toolbar to flask. -* Profiler - * [line_profiler](https://github.com/rkern/line_profiler) - Line-by-line profiling. - * [memory_profiler](https://github.com/fabianp/memory_profiler) - Monitor Memory usage of Python code. - * [profiling](https://github.com/what-studio/profiling) - An interactive Python profiler. -* Others - * [pyelftools](https://github.com/eliben/pyelftools) - Parsing and analyzing ELF files and DWARF debugging information. - * [python-statsd](https://github.com/WoLpH/python-statsd) - Python Client for the [statsd](https://github.com/etsy/statsd/) server. - -## Science and Data Analysis - -*Libraries for scientific computing and data analyzing.* - -* [astropy](http://www.astropy.org/) - A community Python library for Astronomy. -* [bcbio-nextgen](https://github.com/chapmanb/bcbio-nextgen) - A toolkit providing best-practice pipelines for fully automated high throughput sequencing analysis. -* [bccb](https://github.com/chapmanb/bcbb) - Collection of useful code related to biological analysis. -* [Biopython](http://biopython.org/wiki/Main_Page) - Biopython is a set of freely available tools for biological computation. -* [blaze](http://blaze.readthedocs.org/en/latest/index.html) - NumPy and Pandas interface to Big Data. -* [cclib](http://cclib.github.io/) - A library for parsing and interpreting the results of computational chemistry packages. -* [NetworkX](https://networkx.github.io/) - A high-productivity software for complex networks. -* [Neupy](http://neupy.com/pages/home.html) - Running and testing different Artificial Neural Networks algorithms. -* [NIPY](http://nipy.org) - A collection of neuroimaging toolkits. -* [Numba](http://numba.pydata.org/) - Python JIT (just in time) complier to LLVM aimed at scientific Python by the developers of Cython and NumPy. -* [NumPy](http://www.numpy.org/) - A fundamental package for scientific computing with Python. -* [Open Babel](http://openbabel.org/wiki/Main_Page) - A chemical toolbox designed to speak the many languages of chemical data. -* [Open Mining](https://github.com/mining/mining) - Business Intelligence (BI) in Python (Pandas web interface) -* [orange](http://orange.biolab.si/) - Data mining, data visualization, analysis and machine learning through visual programming or Python scripting. -* [Pandas](http://pandas.pydata.org/) - A library providing high-performance, easy-to-use data structures and data analysis tools. -* [PyDy](http://www.pydy.org/) - Short for Python Dynamics, used to assist with workflow in the modeling of dynamic motion based around NumPy, SciPy, IPython, and matplotlib. -* [PyMC](https://github.com/pymc-devs/pymc3) - Markov Chain Monte Carlo sampling toolkit. -* [RDKit](http://www.rdkit.org/) - Cheminformatics and Machine Learning Software. -* [SciPy](http://www.scipy.org/) - A Python-based ecosystem of open-source software for mathematics, science, and engineering. -* [statsmodels](https://github.com/statsmodels/statsmodels) - Statistical modeling and econometrics in Python. -* [SymPy](https://github.com/sympy/sympy) - A Python library for symbolic mathematics. -* [zipline](https://github.com/quantopian/zipline) - A Pythonic algorithmic trading library. - -## Data Visualization - -*Libraries for visualizing data. See: [awesome-javascript](https://github.com/sorrycc/awesome-javascript#data-visualization).* - -* [matplotlib](http://matplotlib.org/) - A Python 2D plotting library. -* [bokeh](https://github.com/bokeh/bokeh) - Interactive Web Plotting for Python. -* [ggplot](https://github.com/yhat/ggplot) - Same API as ggplot2 for R. -* [plotly](https://plot.ly/python/) - Collaborative web plotting for Python and matplotlib. -* [pygal](http://www.pygal.org/en/latest/) - A Python SVG Charts Creator. -* [pygraphviz](https://pypi.python.org/pypi/pygraphviz) - Python interface to [Graphviz](http://www.graphviz.org/). -* [PyQtGraph](http://www.pyqtgraph.org/) - Interactive and realtime 2D/3D/Image plotting and science/engineering widgets. -* [SnakeViz](http://jiffyclub.github.io/snakeviz/) - A browser based graphical viewer for the output of Python's cProfile module. -* [vincent](https://github.com/wrobstory/vincent) - A Python to Vega translator. -* [VisPy](http://vispy.org/) - High-performance scientific visualization based on OpenGL. - -## Computer Vision - -*Libraries for computer vision.* - -* [OpenCV](http://opencv.org/) - Open Source Computer Vision Library. -* [SimpleCV](http://simplecv.org/) - An open source framework for building computer vision applications. - -## Machine Learning - -*Libraries for Machine Learning. See: [awesome-machine-learning](https://github.com/josephmisiti/awesome-machine-learning#python).* - -* [Crab](https://github.com/muricoca/crab) - A flexible, fast recommender engine. -* [gensim](https://github.com/piskvorky/gensim) - Topic Modelling for Humans. -* [hebel](https://github.com/hannes-brt/hebel) - GPU-Accelerated Deep Learning Library in Python. -* [NuPIC](https://github.com/numenta/nupic) - Numenta Platform for Intelligent Computing. -* [pattern](https://github.com/clips/pattern) - Web mining module for Python. -* [PyBrain](https://github.com/pybrain/pybrain) - Another Python Machine Learning Library. -* [Pylearn2](https://github.com/lisa-lab/pylearn2) - A Machine Learning library based on [Theano](https://github.com/Theano/Theano). -* [python-recsys](https://github.com/ocelma/python-recsys) - A Python library for implementing a Recommender System. -* [scikit-learn](http://scikit-learn.org/) - A Python module for machine learning built on top of SciPy. -* [pydeep](https://github.com/andersbll/deeppy)-Deep learning in python -* [vowpal_porpoise](https://github.com/josephreisinger/vowpal_porpoise) - A lightweight Python wrapper for [Vowpal Wabbit](https://github.com/JohnLangford/vowpal_wabbit/). -* [skflow](https://github.com/tensorflow/skflow) - A simplified interface for [TensorFlow](https://github.com/tensorflow/tensorflow) (mimicking scikit-learn). - -## MapReduce - -*Frameworks and libraries for MapReduce.* - -* [dpark](https://github.com/douban/dpark) - Python clone of Spark, a MapReduce alike framework in Python. -* [dumbo](https://github.com/klbostee/dumbo) - Python module that allows one to easily write and run Hadoop programs. -* [luigi](https://github.com/spotify/luigi) - A module that helps you build complex pipelines of batch jobs. -* [mrjob](https://github.com/Yelp/mrjob) - Run MapReduce jobs on Hadoop or Amazon Web Services. -* [PySpark](http://spark.apache.org/docs/latest/programming-guide.html) - The Spark Python API. -* [streamparse](https://github.com/Parsely/streamparse) - Run Python code against real-time streams of data. Integrates with [Apache Storm](http://storm.apache.org/). - -## Functional Programming - -*Functional Programming with Python.* - -* [CyToolz](https://github.com/pytoolz/cytoolz/) - Cython implementation of Toolz: High performance functional utilities. -* [fn.py](https://github.com/kachayev/fn.py) - Functional programming in Python: implementation of missing features to enjoy FP. -* [funcy](https://github.com/Suor/funcy) - A fancy and practical functional tools. -* [Toolz](https://github.com/pytoolz/toolz) - A collection of functional utilities for iterators, functions, and dictionaries. - -## Third-party APIs - -*Libraries for accessing third party services APIs. See: [List of Python API Wrappers and Libraries](https://github.com/realpython/list-of-python-api-wrappers).* - -* [apache-libcloud](https://libcloud.apache.org/) - One Python library for all clouds. -* [boto](https://github.com/boto/boto) - Python interface to Amazon Web Services. -* [django-wordpress](https://github.com/sunlightlabs/django-wordpress/) - WordPress models and views for Django. -* [facebook-sdk](https://github.com/mobolic/facebook-sdk) - Facebook Platform Python SDK. -* [facepy](https://github.com/jgorset/facepy) - Facepy makes it really easy to interact with Facebook's Graph API -* [gmail](https://github.com/charlierguo/gmail) - A Pythonic interface for Gmail. -* [google-api-python-client](https://github.com/google/google-api-python-client) - Google APIs Client Library for Python. -* [gspread](https://github.com/burnash/gspread) - Google Spreadsheets Python API. -* [twython](https://github.com/ryanmcgrath/twython) - A Python wrapper for the Twitter API. +- [yagmail](https://github.com/kootenpv/yagmail) - Yet another Gmail/SMTP client. -## DevOps Tools +**Database & Storage** -*Software and libraries for DevOps.* - -* [Ansible](https://github.com/ansible/ansible) - A radically simple IT automation platform. -* [SaltStack](https://github.com/saltstack/salt) - Infrastructure automation and management system. -* [OpenStack](http://www.openstack.org/) - Open source software for building private and public clouds. -* [Docker Compose](https://docs.docker.com/compose/) - Fast, isolated development environments using [Docker](https://www.docker.com/). -* [Cloud-Init](http://cloudinit.readthedocs.org/en/latest/) - A multi-distribution package that handles early initialization of a cloud instance. -* [cuisine](https://github.com/sebastien/cuisine) - Chef-like functionality for Fabric. -* [Fabric](http://www.fabfile.org/) - A simple, Pythonic tool for remote execution and deployment. -* [Fabtools](https://github.com/ronnix/fabtools) - Tools for writing awesome Fabric files. -* [honcho](https://github.com/nickstenning/honcho) - A Python clone of [Foreman](https://github.com/ddollar/foreman), for managing Procfile-based applications. -* [pexpect](https://github.com/pexpect/pexpect) - Controlling interactive programs in a pseudo-terminal like GNU expect. -* [psutil](https://github.com/giampaolo/psutil) - A cross-platform process and system utilities module. -* [supervisor](https://github.com/Supervisor/supervisor) - Supervisor process control system for UNIX. +### ORM -## Job Scheduler +_Libraries that implement Object-Relational Mapping or data mapping techniques._ -*Libraries for scheduling jobs.* +- Relational Databases + - [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) - The Python SQL Toolkit and Object Relational Mapper. + - [awesome-sqlalchemy](https://github.com/dahlia/awesome-sqlalchemy) + - [django.db.models](https://github.com/django/django) - The Django [ORM](https://docs.djangoproject.com/en/dev/topics/db/models/). + - [peewee](https://github.com/coleifer/peewee) - A small, expressive ORM. + - [sqlmodel](https://github.com/fastapi/sqlmodel) - SQLModel is based on Python type annotations, and powered by Pydantic and SQLAlchemy. +- NoSQL Databases + - [pynamodb](https://github.com/pynamodb/PynamoDB) - A Pythonic interface for [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). + - [mongoengine](https://github.com/MongoEngine/mongoengine) - A Python Object-Document-Mapper for working with MongoDB. + - [beanie](https://github.com/BeanieODM/beanie) - An asynchronous Python object-document mapper (ODM) for MongoDB. -* [APScheduler](http://apscheduler.readthedocs.org/en/latest/) - A light but powerful in-process task scheduler that lets you schedule functions. -* [django-schedule](https://github.com/thauber/django-schedule) - A calendaring app for Django. -* [doit](http://pydoit.org/) - A task runner and build tool. -* [gunnery](https://github.com/gunnery/gunnery) - Multipurpose task execution tool for distributed systems with web-based interface. -* [Joblib](http://pythonhosted.org/joblib/index.html) - A set of tools to provide lightweight pipelining in Python. -* [Plan](https://github.com/fengsp/plan) - Writing crontab file in Python like a charm. -* [schedule](https://github.com/dbader/schedule) - Python job scheduling for humans. -* [Spiff](https://github.com/knipknap/SpiffWorkflow) - A powerful workflow engine implemented in pure Python. -* [TaskFlow](http://docs.openstack.org/developer/taskflow/) - A Python library that helps to make task execution easy, consistent and reliable. +### Database Drivers -## Foreign Function Interface +_Libraries for connecting and operating databases._ -*Libraries for providing foreign function interface.* +- MySQL - [awesome-mysql](https://github.com/shlomi-noach/awesome-mysql) + - [pymysql](https://github.com/PyMySQL/PyMySQL) - A pure Python MySQL driver compatible to mysql-python. + - [mysqlclient](https://github.com/PyMySQL/mysqlclient) - MySQL connector with Python 3 support ([mysql-python](https://sourceforge.net/projects/mysql-python/) fork). +- PostgreSQL - [awesome-postgres](https://github.com/dhamaniasad/awesome-postgres) + - [psycopg](https://github.com/psycopg/psycopg) - The most popular PostgreSQL adapter for Python. + - [asyncpg](https://github.com/MagicStack/asyncpg) - A fast PostgreSQL Database Client Library for Python/asyncio. +- SQLite - [awesome-sqlite](https://github.com/planetopendata/awesome-sqlite) + - [sqlite3](https://docs.python.org/3/library/sqlite3.html) - (Python standard library) SQLite interface compliant with DB-API 2.0. + - [sqlite-utils](https://github.com/simonw/sqlite-utils) - Python CLI utility and library for manipulating SQLite databases. +- ClickHouse + - [clickhouse-connect](https://github.com/ClickHouse/clickhouse-connect) - The official ClickHouse client, with SQLAlchemy and Superset connectors. + - [clickhouse-driver](https://github.com/mymarilyn/clickhouse-driver) - Python driver with native interface for ClickHouse. +- Other Relational Databases + - [pyodbc](https://github.com/mkleehammer/pyodbc) - An ODBC bridge for connecting to SQL Server and any other ODBC-accessible database. + - [oracledb](https://github.com/oracle/python-oracledb) - The official Python driver for Oracle Database, successor to cx_Oracle. + - [mssql-python](https://github.com/microsoft/mssql-python) - Official Microsoft driver for SQL Server and Azure SQL, built on ODBC for high performance and low memory usage. +- NoSQL Databases + - [redis](https://github.com/redis/redis-py) - The Python client for Redis. + - [pymongo](https://github.com/mongodb/mongo-python-driver) - The official Python client for MongoDB. + - [cassandra-driver](https://github.com/apache/cassandra-python-driver) - The Python Driver for Apache Cassandra. + - [django-mongodb-backend](https://github.com/mongodb/django-mongodb-backend) - Official MongoDB database backend for Django. -* [cffi](https://pypi.python.org/pypi/cffi) - Foreign Function Interface for Python calling C code. -* [ctypes](https://docs.python.org/2/library/ctypes.html) - (Python standard library) Foreign Function Interface for Python calling C code. -* [PyCUDA](https://mathema.tician.de/software/pycuda/) - A Python wrapper for Nvidia's CUDA API. -* [SWIG](http://www.swig.org/Doc1.3/Python.html) - Simplified Wrapper and Interface Generator. +### Database -## High Performance +_In-process databases usable directly from Python._ -*Libraries for making Python faster.* +- Analytical + - [duckdb](https://github.com/duckdb/duckdb) - An in-process SQL OLAP database management system; optimized for analytics and fast queries, similar to SQLite but for analytical workloads. + - [chdb](https://github.com/chdb-io/chdb) - In-process OLAP SQL engine with the full ClickHouse dialect, zero-copy pandas/Arrow interop, and federation to remote ClickHouse clusters via `remoteSecure()`. +- Vector + - [chromadb](https://github.com/chroma-core/chroma) - An open-source embedding database for building AI applications with embeddings and semantic search. + - [lancedb](https://github.com/lancedb/lancedb) - A developer-friendly embedded retrieval database for multimodal AI. + - [zvec](https://github.com/alibaba/zvec) - An embedded vector database for on-device RAG and edge AI, the SQLite of vector databases. +- Key-Value & Document + - [tinydb](https://github.com/msiemens/tinydb) - A tiny, document-oriented database. -* [Cython](http://cython.org/) - Optimizing Static Compiler for Python. Uses type mixins to compile Python into C or C++ modules resulting in large performance gains. -* [PeachPy](https://github.com/Maratyszcza/PeachPy) - x86-64 assembler embedded in Python. Can be used as inline assembler for Python or as a stand-alone assembler for Windows, Linux, OS X, Native Client and Go. -* [PyPy](http://pypy.org/) - An implementation of Python in Python. The interpreter uses black magic to make Python very fast without having to add in additional type information. -* [Pyston](https://github.com/dropbox/pyston) - A Python implementation built using LLVM and modern JIT techniques with the goal of achieving good performance. -* [Stackless Python](https://bitbucket.org/stackless-dev/stackless/overview) - An enhanced version of the Python. +### Caching -## Microsoft Windows +_Libraries for caching data._ -*Python programming on Microsoft Windows.* +- [cachetools](https://github.com/tkem/cachetools) - Extensible memoizing collections and decorators. +- [diskcache](https://github.com/grantjenks/python-diskcache) - SQLite and file backed cache backend with faster lookups than memcached and redis. +- [dogpile.cache](https://github.com/sqlalchemy/dogpile.cache) - dogpile.cache is a next generation replacement for Beaker made by the same authors. +- [django-cacheops](https://github.com/Suor/django-cacheops) - A slick ORM cache with automatic granular event-driven invalidation. + +### Search + +_Libraries and software for indexing and performing search queries on data._ + +- [elasticsearch](https://github.com/elastic/elasticsearch-py) - The official low-level Python client for [Elasticsearch](https://www.elastic.co/products/elasticsearch). +- [opensearch-py](https://github.com/opensearch-project/opensearch-py) - The official low-level Python client for [OpenSearch](https://opensearch.org/). +- [meilisearch](https://github.com/meilisearch/meilisearch-python) - The official Python client for the [Meilisearch](https://www.meilisearch.com/) search engine. +- [django-haystack](https://github.com/django-haystack/django-haystack) - Modular search for Django. + +### Serialization + +_Libraries for serializing complex data types._ + +- [msgpack](https://github.com/msgpack/msgpack-python) - MessagePack serializer implementation for Python. +- [orjson](https://github.com/ijl/orjson) - Fast, correct JSON library. +- [marshmallow](https://github.com/marshmallow-code/marshmallow) - A lightweight library for converting complex objects to and from simple Python datatypes. +- [msgspec](https://github.com/msgspec/msgspec) - A fast serialization and validation library with built-in support for JSON, MessagePack, YAML, and TOML. + +**Data & Science** + +### Data Analysis + +_Libraries for data analysis._ + +- [pandas](https://github.com/pandas-dev/pandas) - A library providing high-performance, easy-to-use data structures and data analysis tools. +- [polars](https://github.com/pola-rs/polars) - A fast DataFrame library implemented in Rust with a Python API. +- [ibis-framework](https://github.com/ibis-project/ibis) - A portable Python dataframe library with a single API for 20+ backends. + +### Data Ingestion / ETL + +_Libraries for data extraction, transformation, and loading pipelines across multiple sources and destinations._ + +- General + - [awswrangler](https://github.com/aws/aws-sdk-pandas) - Pandas integration with AWS services like Athena, Glue, Redshift, S3, and DynamoDB. + - [dlt](https://github.com/dlt-hub/dlt) - A Python library for building data pipelines with automatic schema inference, incremental loading, and support for multiple sources and destinations. + - [pathway](https://github.com/pathwaycom/pathway) - Python ETL framework for stream processing, real-time analytics, LLM pipelines, and RAG. +- Financial Data + - [yfinance](https://github.com/ranaroussi/yfinance) - Easy Pythonic way to download market and financial data from Yahoo Finance. + - [akshare](https://github.com/akfamily/akshare) - A financial data interface library, built for human beings! + - [edgartools](https://github.com/dgunning/edgartools) - Library for downloading structured data from SEC EDGAR filings and XBRL financial statements. + - [openbb](https://github.com/OpenBB-finance/OpenBB) - A financial data platform for analysts, quants and AI agents. + +### Data Validation + +_Libraries for validating data. Used for forms in many cases._ + +- [pydantic](https://github.com/pydantic/pydantic) - Data validation using Python type hints. +- [jsonschema](https://github.com/python-jsonschema/jsonschema) - An implementation of [JSON Schema](https://json-schema.org/) for Python. +- [pandera](https://github.com/unionai-oss/pandera) - A data validation library for dataframes, with support for pandas, polars, and Spark. + +### Data Visualization + +_Libraries for visualizing data. Also see [awesome-javascript](https://github.com/sorrycc/awesome-javascript#data-visualization)._ + +- Plotting + - [matplotlib](https://github.com/matplotlib/matplotlib) - A Python 2D plotting library. + - [plotly](https://github.com/plotly/plotly.py) - Interactive graphing library for Python. + - [seaborn](https://github.com/mwaskom/seaborn) - Statistical data visualization using Matplotlib. + - [altair](https://github.com/vega/altair) - Declarative statistical visualization library for Python. + - [bokeh](https://github.com/bokeh/bokeh) - Interactive Web Plotting for Python. +- Specialized + - [cartopy](https://github.com/SciTools/cartopy) - A cartographic python library with matplotlib support. + - [pygraphviz](https://github.com/pygraphviz/pygraphviz/) - Python interface to [Graphviz](https://www.graphviz.org/). + - [graphify](https://github.com/Graphify-Labs/graphify) - Turn any folder of code, SQL schemas, docs, papers, images, or videos into a queryable knowledge graph. +- Dashboards and Apps + - [streamlit](https://github.com/streamlit/streamlit) - A framework which lets you build dashboards, generate reports, or create chat apps in minutes. + - [gradio](https://github.com/gradio-app/gradio) - Build and share machine learning apps, all in Python. + +### Geolocation + +_Libraries for geocoding addresses and working with latitudes and longitudes._ + +- [geopandas](https://github.com/geopandas/geopandas) - Python tools for geographic data (GeoSeries/GeoDataFrame) built on pandas. +- [geopy](https://github.com/geopy/geopy) - Python Geocoding Toolbox. +- [geojson](https://github.com/jazzband/geojson) - Python bindings and utilities for GeoJSON. +- [geodjango](https://github.com/django/django) - A world-class geographic web framework that is part of [Django](https://docs.djangoproject.com/en/dev/ref/contrib/gis/). + +### Science + +_Libraries for scientific computing. Also see [Python-for-Scientists](https://github.com/TomNicholas/Python-for-Scientists)._ + +- Core + - [numpy](https://github.com/numpy/numpy) - A fundamental package for scientific computing with Python. + - [scipy](https://github.com/scipy/scipy) - A Python-based ecosystem of open-source software for mathematics, science, and engineering. + - [numba](https://github.com/numba/numba) - Python JIT compiler to LLVM aimed at scientific Python. +- Symbolic Mathematics + - [sympy](https://github.com/sympy/sympy) - A Python library for symbolic mathematics. +- Statistics + - [statsmodels](https://github.com/statsmodels/statsmodels) - Statistical modeling and econometrics in Python. +- Biology and Chemistry + - [biopython](https://github.com/biopython/biopython) - Biopython is a set of freely available tools for biological computation. + - [rdkit](https://github.com/rdkit/rdkit) - Cheminformatics and Machine Learning Software. +- Physics and Engineering + - [astropy](https://github.com/astropy/astropy) - A community Python library for Astronomy. + - [obspy](https://github.com/obspy/obspy) - A Python toolbox for seismology. +- Simulation and Modeling + - [pymc](https://github.com/pymc-devs/pymc) - Probabilistic programming and Bayesian modeling in Python. + - [simpy](https://gitlab.com/team-simpy/simpy) - A process-based discrete-event simulation framework. + - [mesa](https://github.com/mesa/mesa) - An agent-based modeling framework for building, analyzing, and visualizing complex system simulations. +- Graphs and Networks + - [networkx](https://github.com/networkx/networkx) - A high-productivity software for complex networks. +- Computational Geometry + - [shapely](https://github.com/shapely/shapely) - Manipulation and analysis of geometric objects in the Cartesian plane. +- Other + - [colour-science](https://github.com/colour-science/colour) - Implementing a comprehensive number of colour theory transformations and algorithms. + - [manim](https://github.com/ManimCommunity/manim) - An animation engine for explanatory math videos. + +### Quantum Computing + +_Libraries for quantum computing._ + +- [qiskit](https://github.com/Qiskit/qiskit) - An IBM-backed quantum SDK for building, simulating, and running circuits on real quantum hardware. +- [qutip](https://github.com/qutip/qutip) - Quantum Toolbox in Python. +- [pennylane](https://github.com/PennyLaneAI/pennylane) - A hybrid quantum-classical machine learning library with automatic differentiation support. +- [cirq](https://github.com/quantumlib/Cirq) - A Google-developed framework focused on hardware-aware quantum circuit design for NISQ devices. + +**Developer Tools** + +### Algorithms and Design Patterns + +_Python implementation of data structures, algorithms and design patterns. Also see [awesome-algorithms](https://github.com/tayllan/awesome-algorithms)._ + +- Algorithms + - [sortedcontainers](https://github.com/grantjenks/python-sortedcontainers) - Fast and pure-Python implementation of sorted collections. + - [algorithms](https://github.com/keon/algorithms) - Minimal examples of data structures and algorithms. + - [thealgorithms](https://github.com/TheAlgorithms/Python) - All Algorithms implemented in Python. +- Design Patterns + - [transitions](https://github.com/pytransitions/transitions) - A lightweight, object-oriented finite state machine implementation. + - [python-patterns](https://github.com/faif/python-patterns) - A collection of design patterns in Python. + +### Interactive Interpreter + +_Interactive Python interpreters (REPL)._ + +- [ipython](https://github.com/ipython/ipython) - A powerful interactive Python shell, and the kernel behind Jupyter notebooks. +- [jupyter](https://github.com/jupyter/notebook) - A rich toolkit to help you make the most out of using Python interactively. + - [awesome-jupyter](https://github.com/markusschanta/awesome-jupyter) +- [marimo](https://github.com/marimo-team/marimo) - Transform data and train models, feels like a next-gen notebook, stored as Git-friendly Python. +- [ptpython](https://github.com/prompt-toolkit/ptpython) - Advanced Python REPL built on top of the [python-prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit). + +### Code Analysis + +_Tools of static analysis, linters and code quality checkers. Also see [awesome-static-analysis](https://github.com/analysis-tools-dev/static-analysis)._ + +- Code Analysis + - [vulture](https://github.com/jendrikseipp/vulture) - A tool for finding and analyzing dead Python code. + - [prospector](https://github.com/prospector-dev/prospector) - A tool to analyze Python code. + - [repowise](https://github.com/repowise-dev/repowise) - Codebase intelligence that indexes repos into dependency graphs, git history, and auto-generated docs with dead code detection. +- Git Hooks + - [pre-commit](https://github.com/pre-commit/pre-commit) - A framework for managing and maintaining multi-language pre-commit hooks. +- Linters and Formatters + - [ruff](https://github.com/astral-sh/ruff) - An extremely fast Python linter and code formatter. + - [black](https://github.com/psf/black) - The uncompromising Python code formatter. + - [isort](https://github.com/PyCQA/isort) - A Python utility / library to sort imports. + - [pylint](https://github.com/pylint-dev/pylint) - A fully customizable source code analyzer. + - [flake8](https://github.com/PyCQA/flake8) - A wrapper around `pycodestyle`, `pyflakes` and McCabe. + - [awesome-flake8-extensions](https://github.com/DmytroLitvinov/awesome-flake8-extensions) + - [bandit](https://github.com/PyCQA/bandit) - A tool designed to find common security issues in Python code. +- Refactoring + - [rope](https://github.com/python-rope/rope) - Rope is a python refactoring library. +- Type Checkers - [awesome-python-typing](https://github.com/typeddjango/awesome-python-typing) + - [mypy](https://github.com/python/mypy) - Check variable types during compile time. + - [ty](https://github.com/astral-sh/ty) - An extremely fast Python type checker and language server. + - [pyright](https://github.com/microsoft/pyright) - Full-featured static type checker for Python from Microsoft, the engine behind Pylance. + - [pyrefly](https://github.com/facebook/pyrefly) - A fast type checker and language server for Python. +- Type Annotations Generators + - [monkeytype](https://github.com/Instagram/MonkeyType) - A system for Python that generates static type annotations by collecting runtime types. + +### Testing + +_Libraries for testing codebases and generating test data. Also see [awesome-python-testing](https://github.com/cleder/awesome-python-testing)._ + +- Frameworks + - [pytest](https://github.com/pytest-dev/pytest) - A mature full-featured Python testing tool. + - [awesome-pytest](https://github.com/augustogoulart/awesome-pytest) + - [hypothesis](https://github.com/HypothesisWorks/hypothesis) - Hypothesis is an advanced Quickcheck style property based testing library. + - [robotframework](https://github.com/robotframework/robotframework) - A generic test automation framework. +- Test Runners + - [tox](https://github.com/tox-dev/tox) - Auto builds and tests distributions in multiple Python versions + - [nox](https://github.com/wntrblm/nox) - Flexible test automation for Python. +- Browser Automation + - [playwright-python](https://github.com/microsoft/playwright-python) - Python version of the Playwright testing and automation library. + - [selenium](https://github.com/SeleniumHQ/selenium) - Python bindings for [Selenium](https://selenium.dev/) [WebDriver](https://selenium.dev/documentation/webdriver/). + - [seleniumbase](https://github.com/seleniumbase/SeleniumBase) - Python framework for web automation & testing, with stealth options. +- Load Testing + - [locust](https://github.com/locustio/locust) - Scalable user load testing tool written in Python. +- API Testing + - [schemathesis](https://github.com/schemathesis/schemathesis) - A tool for automatic property-based testing of web applications built with Open API / Swagger specifications. +- Mock + - [mock](https://docs.python.org/3/library/unittest.mock.html) - (Python standard library) A mocking and patching library. + - [responses](https://github.com/getsentry/responses) - A utility library for mocking out the requests Python library. + - [freezegun](https://github.com/spulec/freezegun) - Travel through time by mocking the datetime module. + - [vcrpy](https://github.com/kevin1024/vcrpy) - Record and replay HTTP interactions on your tests. + - [respx](https://github.com/lundberg/respx) - Mock HTTPX with awesome request patterns and response side effects. +- Object Factories + - [factory_boy](https://github.com/FactoryBoy/factory_boy) - A test fixtures replacement for Python. + - [polyfactory](https://github.com/litestar-org/polyfactory) - mock data generation library with support to classes (continuation of `pydantic-factories`) +- Code Coverage + - [coverage](https://github.com/coveragepy/coveragepy) - Code coverage measurement. +- Fake Data + - [faker](https://github.com/joke2k/faker) - A Python package that generates fake data. + - [mimesis](https://github.com/lk-geimfari/mimesis) - is a Python library that help you generate fake data. + +### Debugging Tools + +_Libraries for debugging code._ + +- pdb-like Debugger + - [ipdb](https://github.com/gotcha/ipdb) - IPython-enabled [pdb](https://docs.python.org/3/library/pdb.html). + - [pudb](https://github.com/inducer/pudb) - A full-screen, console-based Python debugger. +- Tracing + - [hunter](https://github.com/ionelmc/python-hunter) - A flexible code tracing toolkit. +- Profiler + - [py-spy](https://github.com/benfred/py-spy) - A sampling profiler for Python programs. Written in Rust. + - [memray](https://github.com/bloomberg/memray) - A memory profiler that tracks allocations in Python code, native extensions, and the interpreter itself. + - [pyinstrument](https://github.com/joerick/pyinstrument) - A statistical wall-clock profiler with low overhead and readable call-tree output. + - [scalene](https://github.com/plasma-umass/scalene) - A high-performance, high-precision CPU, GPU, and memory profiler for Python. +- Others + - [django-debug-toolbar](https://github.com/django-commons/django-debug-toolbar) - Display various debug information for Django. + - [icecream](https://github.com/gruns/icecream) - Inspect variables, expressions, and program execution with a single, simple function call. + - [flask-debugtoolbar](https://github.com/pallets-eco/flask-debugtoolbar) - A port of the django-debug-toolbar to flask. + +### Build Tools + +_Compile software from source code. If you're looking for Python packaging/build tools, see [Package Management](#package-management)._ + +- [invoke](https://github.com/pyinvoke/invoke) - A tool for managing shell-oriented subprocesses and organizing executable Python code into CLI-invokable tasks. +- [scons](https://github.com/SCons/scons) - A software construction tool. +- [doit](https://github.com/pydoit/doit) - A task runner and build tool. + +### Documentation + +_Libraries for generating project documentation._ + +- [sphinx](https://github.com/sphinx-doc/sphinx/) - Python Documentation generator. + - [awesome-sphinxdoc](https://github.com/ygzgxyz/awesome-sphinxdoc) +- [mkdocs-material](https://github.com/squidfunk/mkdocs-material) - A documentation framework and Material Design theme built on MkDocs. +- [mkdocs](https://github.com/mkdocs/mkdocs/) - Markdown friendly documentation generator. +- [diagrams](https://github.com/mingrammer/diagrams) - Diagram as Code. +- [pdoc](https://github.com/mitmproxy/pdoc) - Epydoc replacement to auto generate API documentation for Python libraries. + +**DevOps** + +### DevOps Tools + +_Software and libraries for DevOps._ + +- Cloud Providers + - [boto3](https://github.com/boto/boto3) - Python interface to Amazon Web Services. + - [awscli](https://github.com/aws/aws-cli) - Universal Command Line Interface for Amazon Web Services. + - [azure-sdk-for-python](https://github.com/Azure/azure-sdk-for-python) - Microsoft Azure SDK for Python, published as per-service packages. + - [google-cloud-python](https://github.com/googleapis/google-cloud-python) - Google Cloud client libraries for Python, published as per-service packages. +- Configuration Management + - [ansible](https://github.com/ansible/ansible) - A radically simple IT automation platform. + - [cloud-init](https://github.com/canonical/cloud-init) - A multi-distribution package that handles early initialization of a cloud instance. + - [pyinfra](https://github.com/pyinfra-dev/pyinfra) - A versatile CLI tools and python libraries to automate infrastructure. + - [salt](https://github.com/saltstack/salt) - Infrastructure automation and management system. +- Deployment + - [fabric](https://github.com/fabric/fabric) - A simple, Pythonic tool for remote execution and deployment. + - [chalice](https://github.com/aws/chalice) - A Python serverless microframework for AWS. +- Monitoring and Processes + - [psutil](https://github.com/giampaolo/psutil) - A cross-platform process and system utilities module. + - [sentry-sdk](https://github.com/getsentry/sentry-python) - Sentry SDK for Python. + - [supervisor](https://github.com/Supervisor/supervisor) - Supervisor process control system for UNIX. + - [flower](https://github.com/mher/flower) - A real-time monitor and web admin for Celery task queues. + - [sh](https://github.com/amoffat/sh) - A full-fledged subprocess replacement for Python. +- Other + - [borgbackup](https://github.com/borgbackup/borg) - A deduplicating archiver with compression and encryption. + - [chaostoolkit](https://github.com/chaostoolkit/chaostoolkit) - A Chaos Engineering toolkit & Orchestration for Developers. + +### Distributed Computing + +_Frameworks and libraries for Distributed Computing._ + +- [ray](https://github.com/ray-project/ray/) - A system for parallel and distributed Python that unifies the machine learning ecosystem. +- [pyspark](https://github.com/apache/spark) - [Apache Spark](https://spark.apache.org/) Python API. +- [dask](https://github.com/dask/dask) - A flexible parallel computing library for analytic computing. +- [joblib](https://github.com/joblib/joblib) - A set of tools to provide lightweight pipelining in Python. +- [mpi4py](https://github.com/mpi4py/mpi4py) - Python bindings for MPI. + +### Task Queues + +_Libraries for working with task queues._ + +- [celery](https://github.com/celery/celery) - An asynchronous task queue/job queue based on distributed message passing. +- [rq](https://github.com/rq/rq) - Simple job queues for Python. +- [dramatiq](https://github.com/Bogdanp/dramatiq) - A fast and reliable background task processing library for Python 3. +- [huey](https://github.com/coleifer/huey) - Little multi-threaded task queue. +- [taskiq](https://github.com/taskiq-python/taskiq) - Distributed task queue with native asyncio support and pluggable brokers. + +### Messaging + +_Libraries for working with message brokers and event streaming._ + +- [confluent-kafka](https://github.com/confluentinc/confluent-kafka-python) - Confluent's Python client for Apache Kafka, built on librdkafka. +- [pika](https://github.com/pika/pika) - Pure-Python RabbitMQ/AMQP 0-9-1 client library. +- [paho-mqtt](https://github.com/eclipse-paho/paho.mqtt.python) - The Eclipse Paho MQTT client for Python. +- [faststream](https://github.com/ag2ai/faststream) - A framework for building asynchronous services over Apache Kafka, RabbitMQ, NATS, MQTT and Redis. + +### Job Schedulers + +_Libraries for scheduling jobs._ + +- Task Scheduling + - [apscheduler](https://github.com/agronholm/apscheduler) - A light but powerful in-process task scheduler that lets you schedule functions. + - [schedule](https://github.com/dbader/schedule) - Python job scheduling for humans. +- Workflow Orchestration + - [apache-airflow](https://github.com/apache/airflow) - Airflow is a platform to programmatically author, schedule and monitor workflows. + - [prefect](https://github.com/PrefectHQ/prefect) - A modern workflow orchestration framework that makes it easy to build, schedule and monitor robust data pipelines. + - [dagster](https://github.com/dagster-io/dagster) - An orchestration platform for the development, production, and observation of data assets. + +### Logging + +_Libraries for generating and working with logs._ + +- [logging](https://docs.python.org/3/library/logging.html) - (Python standard library) Logging facility for Python. +- [structlog](https://github.com/hynek/structlog) - Structured logging made easy. +- [loguru](https://github.com/Delgan/loguru) - Library which aims to bring enjoyable logging in Python. + +### Network Virtualization + +_Tools and libraries for Virtual Networking and SDN (Software Defined Networking)._ + +- [scapy](https://github.com/secdev/scapy) - A brilliant packet manipulation library. +- [napalm](https://github.com/napalm-automation/napalm) - Cross-vendor API to manipulate network devices. + +**CLI & GUI** + +### CLI Development + +_Libraries for building command-line applications._ + +- CLI Development + - [argparse](https://docs.python.org/3/library/argparse.html) - (Python standard library) Command-line option and argument parsing. + - [click](https://github.com/pallets/click/) - A package for creating beautiful command line interfaces in a composable way. + - [typer](https://github.com/fastapi/typer) - Modern CLI framework that uses Python type hints. Built on Click and Pydantic. + - [prompt_toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) - A library for building powerful interactive command lines. + - [fire](https://github.com/google/python-fire) - A library for creating command line interfaces from absolutely any Python object. +- Terminal Rendering + - [tqdm](https://github.com/tqdm/tqdm) - Fast, extensible progress bar for loops and CLI. + - [rich](https://github.com/Textualize/rich) - Python library for rich text and beautiful formatting in the terminal. Also provides a great `RichHandler` log handler. + - [colorama](https://github.com/tartley/colorama) - Cross-platform colored terminal text. + - [alive-progress](https://github.com/rsalmei/alive-progress) - A new kind of Progress Bar, with real-time throughput, eta and very cool animations. +- TUI Frameworks + - [textual](https://github.com/Textualize/textual) - A framework for building interactive user interfaces that run in the terminal and the browser. + - [urwid](https://github.com/urwid/urwid) - A library for creating terminal GUI applications with strong support for widgets, events, rich colors, etc. + - [asciimatics](https://github.com/peterbrittain/asciimatics) - A package to create full-screen text UIs (from interactive forms to ASCII animations). + +### CLI Tools + +_Useful CLI-based tools._ + +- Database CLIs + - [pgcli](https://github.com/dbcli/pgcli) - PostgreSQL CLI with autocompletion and syntax highlighting. + - [mycli](https://github.com/dbcli/mycli) - MySQL CLI with autocompletion and syntax highlighting. + - [litecli](https://github.com/dbcli/litecli) - SQLite CLI with autocompletion and syntax highlighting. + - [iredis](https://github.com/laixintao/iredis) - Redis CLI with autocompletion and syntax highlighting. +- Downloaders + - [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A command-line program to download videos from YouTube and other video sites, a fork of youtube-dl. +- HTTP Clients + - [httpie](https://github.com/httpie/cli) - A command line HTTP client, a user-friendly cURL replacement. +- Project Scaffolding + - [cookiecutter](https://github.com/cookiecutter/cookiecutter) - A command-line utility that creates projects from cookiecutters (project templates). + - [copier](https://github.com/copier-org/copier) - A library and command-line utility for rendering projects templates. +- Shells + - [xonsh](https://github.com/xonsh/xonsh/) - A Python-powered shell. Full-featured and cross-platform. +- Terminal Workflow + - [tmuxp](https://github.com/tmux-python/tmuxp) - A [tmux](https://github.com/tmux/tmux) session manager. + +### GUI Development + +_Libraries for working with graphical user interface applications._ + +- Desktop + - [pygobject](https://github.com/GNOME/pygobject) - Python Bindings for GLib/GObject/GIO/GTK+ (GTK+3). + - [wxPython](https://github.com/wxWidgets/Phoenix) - A blending of the wxWidgets C++ class library with the Python. + - [kivy](https://github.com/kivy/kivy) - A library for creating NUI applications, running on Windows, Linux, Mac OS X, Android and iOS. + - [dearpygui](https://github.com/hoffstadt/DearPyGui) - A Simple GPU accelerated Python GUI framework + - [toga](https://github.com/beeware/toga) - A Python native, OS native GUI toolkit. +- Qt + - [PySide6](https://github.com/pyside/pyside-setup) - Qt for Python offers the official Python bindings for [Qt](https://www.qt.io/), same as PyQt6 but it's the official binding with different licensing. + - [PyQt6](https://www.riverbankcomputing.com/static/Docs/PyQt6/) - Python bindings for the [Qt](https://www.qt.io/) cross-platform application and UI framework. +- Tkinter + - [tkinter](https://docs.python.org/3/library/tkinter.html) - (Python standard library) The standard Python interface to the Tcl/Tk GUI toolkit. + - [customtkinter](https://github.com/tomschimansky/customtkinter) - A modern and customizable python UI-library based on Tkinter. + - [tkdesigner](https://github.com/ParthJadhav/Tkinter-Designer) - Generates Tkinter interfaces from Figma designs using the Figma API. +- Web-based + - [pywebview](https://github.com/r0x0r/pywebview/) - A lightweight cross-platform native wrapper around a webview component. + - [nicegui](https://github.com/zauberzeug/nicegui) - An easy-to-use, Python-based UI framework, which shows up in your web browser. + - [flet](https://github.com/flet-dev/flet) - Cross-platform GUI framework for building modern apps in pure Python. +- Wrappers + - [gooey](https://github.com/chriskiehl/Gooey) - Turn command line programs into a full GUI application with one line. + +**Text & Documents** + +### Text Processing + +_Libraries for parsing and manipulating plain texts._ + +- Encoding and Unicode + - [charset-normalizer](https://github.com/jawah/charset_normalizer) - Universal character encoding detector, the default of the requests ecosystem. + - [chardet](https://github.com/chardet/chardet) - Python character encoding detector. + - [ftfy](https://github.com/rspeer/python-ftfy) - Makes Unicode text less broken and more consistent automagically. +- Fuzzy Matching + - [rapidfuzz](https://github.com/rapidfuzz/RapidFuzz) - Rapid fuzzy string matching using various string metrics, with a C++ core. +- General + - [difflib](https://docs.python.org/3/library/difflib.html) - (Python standard library) Helpers for computing deltas. + - [pyfiglet](https://github.com/pwaller/pyfiglet) - An implementation of figlet written in Python. +- Internationalization + - [babel](https://github.com/python-babel/babel) - An internationalization library for Python. +- Parser + - [pygments](https://github.com/pygments/pygments) - A generic syntax highlighter. + - [pyparsing](https://github.com/pyparsing/pyparsing) - A general purpose framework for generating parsers. + - [sqlparse](https://github.com/andialbrecht/sqlparse) - A non-validating SQL parser. + - [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) - Parsing, formatting, storing and validating international phone numbers. + - [parsy](https://github.com/python-parsy/parsy) - Easy, generic parser combinator library for creating parsers. +- Transliteration and Slugs + - [python-slugify](https://github.com/un33k/python-slugify) - A Python slugify library that translates unicode to ASCII. + - [unidecode](https://github.com/avian2/unidecode) - ASCII transliterations of Unicode text. +- Unique identifiers + - [shortuuid](https://github.com/skorokithakis/shortuuid) - A generator library for concise, unambiguous and URL-safe UUIDs. + - [sqids](https://github.com/sqids/sqids-python) - A library for generating short unique IDs from numbers. + +### HTML Manipulation + +_Libraries for working with HTML and XML._ + +- [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) - Providing Pythonic idioms for iterating, searching, and modifying HTML or XML. +- [lxml](https://github.com/lxml/lxml) - A very fast, easy-to-use and versatile library for handling HTML and XML. +- [xmltodict](https://github.com/martinblech/xmltodict) - Working with XML feel like you are working with JSON. +- [markupsafe](https://github.com/pallets/markupsafe) - Implements a XML/HTML/XHTML Markup safe string for Python. +- [justhtml](https://github.com/EmilStenstrom/justhtml/) - A pure Python HTML5 parser that just works. + +### File Format Processing + +_Libraries for parsing and manipulating specific text formats._ + +- General + - [pyelftools](https://github.com/eliben/pyelftools) - Parsing and analyzing ELF files and DWARF debugging information. + - [tablib](https://github.com/jazzband/tablib) - A module for Tabular Datasets in XLS, CSV, JSON, YAML. +- File Conversion + - [markitdown](https://github.com/microsoft/markitdown) - Python tool for converting files and office documents to Markdown. + - [docling](https://github.com/docling-project/docling) - Library for converting documents into structured data. +- Excel + - [openpyxl](https://openpyxl.readthedocs.io/en/stable/) - A library for reading and writing Excel 2010 xlsx/xlsm/xltx/xltm files. + - [xlsxwriter](https://github.com/jmcnamara/XlsxWriter) - A Python module for creating Excel .xlsx files. +- Word + - [python-docx](https://github.com/python-openxml/python-docx) - Reads, queries and modifies Microsoft Word 2007/2008 docx files. +- PowerPoint + - [python-pptx](https://github.com/scanny/python-pptx) - Python library for creating and updating PowerPoint (.pptx) files. +- PDF + - [pypdf](https://github.com/py-pdf/pypdf) - A library capable of splitting, merging, cropping, and transforming PDF pages. + - [reportlab](https://www.reportlab.com/opensource/) - Allowing Rapid creation of rich PDF documents. + - [pdfminer.six](https://github.com/pdfminer/pdfminer.six) - Pdfminer.six is a community maintained fork of the original PDFMiner. +- HTML-to-PDF + - [weasyprint](https://github.com/Kozea/WeasyPrint) - A visual rendering engine for HTML and CSS that can export to PDF. +- Markdown + - [markdown-it-py](https://github.com/executablebooks/markdown-it-py) - Markdown parser with 100% CommonMark support, extensions, and syntax plugins. + - [markdown](https://github.com/Python-Markdown/markdown) - A Python implementation of John Gruber’s Markdown. + - [mistune](https://github.com/lepture/mistune) - Fastest and full featured pure Python parsers of Markdown. +- Data Formats + - [tomllib](https://docs.python.org/3/library/tomllib.html) - (Python standard library) Parse TOML files. + - [pyyaml](https://github.com/yaml/pyyaml) - YAML implementations for Python. + +### File Manipulation + +_Libraries for file manipulation._ + +- [mimetypes](https://docs.python.org/3/library/mimetypes.html) - (Python standard library) Map filenames to MIME types. +- [pathlib](https://docs.python.org/3/library/pathlib.html) - (Python standard library) A cross-platform, object-oriented path library. +- [watchfiles](https://github.com/samuelcolvin/watchfiles) - Simple, modern and fast file watching and code reload in python. +- [watchdog](https://github.com/gorakhargosh/watchdog) - API and shell utilities to monitor file system events. +- [python-magic](https://github.com/ahupp/python-magic) - A Python interface to the libmagic file type identification library. + +**Media** + +### Image Processing + +_Libraries for manipulating images._ + +- Barcodes and QR Codes + - [qrcode](https://github.com/lincolnloop/python-qrcode) - A pure Python QR Code generator. + - [python-barcode](https://github.com/WhyNotHugo/python-barcode) - Create barcodes in Python with no extra dependencies. +- General + - [pillow](https://github.com/python-pillow/Pillow) - Pillow is the friendly [PIL](https://www.pythonware.com/products/pil/) fork. + - [scikit-image](https://github.com/scikit-image/scikit-image) - A Python library for (scientific) image processing. + - [rembg](https://github.com/danielgatis/rembg) - A tool to remove image backgrounds. + - [wand](https://github.com/emcconville/wand) - Python bindings for [MagickWand](https://www.imagemagick.org/script/magick-wand.php), C API for ImageMagick. + - [pyvips](https://github.com/libvips/pyvips) - A fast image processing library with low memory needs. +- Image Serving + - [thumbor](https://github.com/thumbor/thumbor) - A smart imaging service. It enables on-demand crop, re-sizing and flipping of images. + +### Audio & Video Processing + +_Libraries for manipulating audio, video, and their metadata._ + +- Audio + - [pydub](https://github.com/jiaaro/pydub) - Manipulate audio with a simple and easy high level interface. + - [librosa](https://github.com/librosa/librosa) - Python library for audio and music analysis. +- Video + - [moviepy](https://github.com/Zulko/moviepy) - A module for script-based movie editing with many formats, including animated GIFs. + - [vidgear](https://github.com/abhiTronix/vidgear) - Most Powerful multi-threaded Video Processing framework. +- Metadata + - [mutagen](https://github.com/quodlibet/mutagen) - A Python module to handle audio metadata. + - [tinytag](https://github.com/tinytag/tinytag) - A library for reading music meta data of MP3, OGG, FLAC and Wave files. + - [beets](https://github.com/beetbox/beets) - A music library manager and [MusicBrainz](https://musicbrainz.org/) tagger. + +### Game Development + +_Awesome game development libraries._ + +- 3D Engines + - [panda3d](https://github.com/panda3d/panda3d) - 3D game engine developed by Disney. +- Game Frameworks + - [pygame](https://github.com/pygame/pygame) - Pygame is a set of Python modules designed for writing games. + - [pyglet](https://github.com/pyglet/pyglet) - A cross-platform windowing and multimedia library for Python. + - [pygame-ce](https://github.com/pygame-community/pygame-ce) - An actively developed drop-in replacement with new features and performance improvements ([pygame](https://github.com/pygame/pygame) fork). + - [arcade](https://github.com/pythonarcade/arcade) - Arcade is a modern Python framework for crafting games with compelling graphics and sound. +- Visual Novels + - [renpy](https://github.com/renpy/renpy) - A Visual Novel engine. + +**Python Language** + +### Implementations + +_Implementations of Python._ + +- [cpython](https://github.com/python/cpython) - Default, most widely used implementation of the Python programming language written in C. +- [micropython](https://github.com/micropython/micropython) - A lean and efficient Python programming language implementation. +- [pypy](https://github.com/pypy/pypy) - A very fast and compliant implementation of the Python language. +- [Cython](https://github.com/cython/cython) - Optimizing Static Compiler for Python. +- [pyodide](https://github.com/pyodide/pyodide) - Python distribution for the browser and Node.js based on WebAssembly. + +### Built-in Classes Enhancement + +_Libraries for enhancing Python built-in classes._ + +- [attrs](https://github.com/python-attrs/attrs) - Replacement for `__init__`, `__eq__`, `__repr__`, etc. boilerplate in class definitions. +- [bidict](https://github.com/jab/bidict) - Efficient, Pythonic bidirectional map data structures and related functionality. +- [uuid-utils](https://github.com/aminalaee/uuid-utils) - A fast, Rust-backed drop-in replacement for Python's built-in `uuid` module, supporting RFC 9562 (UUIDv6, UUIDv7, and UUIDv8). +- [python-box](https://github.com/cdgriffith/Box) - Python dictionaries with advanced dot notation access. + +### Functional Programming + +_Functional Programming with Python._ -* [Python(x,y)](http://python-xy.github.io/) - Scientific-applications-oriented Python Distribution based on Qt and Spyder. -* [pythonlibs](http://www.lfd.uci.edu/~gohlke/pythonlibs/) - Unofficial Windows binaries for Python extension packages. -* [PythonNet](https://github.com/pythonnet/pythonnet) - Python Integration with the .NET Common Language Runtime (CLR). -* [PyWin32](https://sourceforge.net/projects/pywin32/) - Python Extensions for Windows. -* [WinPython](https://winpython.github.io/) - Portable development environment for Windows 7/8. +- [functools](https://docs.python.org/3/library/functools.html) - (Python standard library) Higher-order functions and operations on callable objects. +- [more-itertools](https://github.com/more-itertools/more-itertools) - More routines for operating on iterables, beyond `itertools`. +- [toolz](https://github.com/pytoolz/toolz) - A collection of functional utilities for iterators, functions, and dictionaries. Also available as [cytoolz](https://github.com/pytoolz/cytoolz/) for Cython-accelerated performance. +- [funcy](https://github.com/Suor/funcy) - A fancy and practical functional tools. +- [returns](https://github.com/dry-python/returns) - A set of type-safe monads, transformers, and composition utilities. -## Network Virtualization and SDN +### Asynchronous Programming -*Tools and libraries for Virtual Networking and SDN (Software Defined Networking).* +_Libraries for asynchronous, concurrent and parallel execution. Also see [awesome-asyncio](https://github.com/timofurrer/awesome-asyncio)._ -* [Mininet](http://mininet.org/) - A popular network emulator and API written in Python. -* [POX](https://github.com/noxrepo/pox) - An open source development platform for Python-based Software Defined Networking (SDN) control applications, such as OpenFlow SDN controllers. -* [Pyretic](http://frenetic-lang.org/pyretic/) - A member of the Frenetic family of SDN programming languages that provides powerful abstractions over network switches or emulators. -* [SDX Platform](https://github.com/sdn-ixp/internet2award) - SDN based IXP implementation that leverages Mininet, POX and Pyretic. +- Async I/O + - [asyncio](https://docs.python.org/3/library/asyncio.html) - (Python standard library) Asynchronous I/O, event loop, coroutines and tasks. + - [awesome-asyncio](https://github.com/timofurrer/awesome-asyncio) + - [anyio](https://github.com/agronholm/anyio) - A high-level async concurrency and networking framework that works on top of asyncio or trio. + - [uvloop](https://github.com/MagicStack/uvloop) - Ultra fast asyncio event loop. + - [trio](https://github.com/python-trio/trio) - A friendly library for async concurrency and I/O. + - [gevent](https://github.com/gevent/gevent) - A coroutine-based Python networking library that uses [greenlet](https://github.com/python-greenlet/greenlet). + - [Twisted](https://github.com/twisted/twisted) - An event-driven networking engine. +- Parallelism + - [concurrent.futures](https://docs.python.org/3/library/concurrent.futures.html) - (Python standard library) A high-level interface for asynchronously executing callables. + - [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) - (Python standard library) Process-based parallelism. -## Hardware +### Date and Time -*Libraries for programming with hardware.* +_Libraries for working with dates and times._ -* [ino](http://inotool.org/) - Command line toolkit for working with [Arduino](https://www.arduino.cc/). -* [Pyro](http://pyrorobotics.com/) - Python Robotics. -* [PyUserInput](https://github.com/SavinaRoja/PyUserInput) - A module for cross-platform control of the mouse and keyboard. -* [scapy](https://github.com/secdev/scapy) - A brilliant packet manipulation library. -* [wifi](https://wifi.readthedocs.org/en/latest/) - A Python library and command line tool for working with WiFi on Linux. -* [Pingo](http://www.pingo.io/) - Pingo provides a uniform API to program devices like the Raspberry Pi, pcDuino, Intel Galileo, etc. +- [zoneinfo](https://docs.python.org/3/library/zoneinfo.html) - (Python standard library) IANA time zone support. Brings the [tz database](https://en.wikipedia.org/wiki/Tz_database) into Python. +- [python-dateutil](https://github.com/dateutil/dateutil) - Extensions to the standard Python [datetime](https://docs.python.org/3/library/datetime.html) module. +- [dateparser](https://github.com/scrapinghub/dateparser) - A Python parser for human-readable dates in dozens of languages. +- [pendulum](https://github.com/python-pendulum/pendulum) - Python datetimes made easy. +- [whenever](https://github.com/ariebovenberg/whenever) - A modern datetime library, type-safe and DST-safe, backed by Rust. -## Compatibility +**Python Toolchain** -*Libraries for migrating from Python 2 to 3.* +### Environment Management -* [Python-Future](http://python-future.org/index.html) - The missing compatibility layer between Python 2 and Python 3. -* [Python-Modernize](https://github.com/mitsuhiko/python-modernize) - Modernizes Python code for eventual Python 3 migration. -* [Six](https://pypi.python.org/pypi/six) - Python 2 and 3 compatibility utilities. +_Libraries for Python version and virtual environment management._ -## Miscellaneous +- [virtualenv](https://github.com/pypa/virtualenv) - A tool to create isolated Python environments. +- [uv](https://github.com/astral-sh/uv) - An extremely fast Python version, package and project manager, written in Rust. +- [pyenv](https://github.com/pyenv/pyenv) - Simple Python version management. -*Useful libraries or tools that don't fit in the categories above.* +### Package Management -* [blinker](https://github.com/jek/blinker) - A fast Python in-process signal/event dispatching system. -* [itsdangerous](https://github.com/pallets/itsdangerous) - Various helpers to pass trusted data to untrusted environments. -* [pluginbase](https://github.com/mitsuhiko/pluginbase) - A simple but flexible plugin system for Python. -* [Pychievements](https://github.com/PacketPerception/pychievements) - A framework for creating and tracking achievements. -* [Tryton](http://www.tryton.org/) - A general purpose business framework. +_Libraries for package and dependency management._ -## Algorithms and Design Patterns +- Package Managers + - [pip](https://github.com/pypa/pip) - The package installer for Python. + - [uv](https://github.com/astral-sh/uv) - An extremely fast Python version, package and project manager, written in Rust. + - [poetry](https://github.com/python-poetry/poetry) - Python dependency management and packaging made easy. + - [hatch](https://github.com/pypa/hatch) - Modern, extensible Python project manager for environments, builds, and publishing. + - [pipx](https://github.com/pypa/pipx) - Install and Run Python Applications in Isolated Environments. Like `npx` in Node.js. + - [conda](https://github.com/conda/conda/) - Cross-platform, Python-agnostic binary package manager. +- Build Backends + - [setuptools](https://github.com/pypa/setuptools) - The historical and still most widely used pyproject build backend. + - [hatchling](https://github.com/pypa/hatch) - Modern, extensible build backend from the hatch project. + - [uv-build](https://github.com/astral-sh/uv) - uv's fast, minimal build backend for pure-Python projects. -*Python implementation of algorithms and design patterns.* +### Package Repositories -* [algorithms](https://github.com/nryoung/algorithms) - A module of algorithms for Python. -* [python-patterns](https://github.com/faif/python-patterns) - A collection of design patterns in Python. -* [sortedcontainers](http://www.grantjenks.com/docs/sortedcontainers/) - Fast, pure-Python implementation of SortedList, SortedDict, and SortedSet types. +_Local PyPI repository server and proxies._ -## Editor Plugins +- [bandersnatch](https://github.com/pypa/bandersnatch/) - PyPI mirroring tool provided by Python Packaging Authority (PyPA). +- [devpi](https://github.com/devpi/devpi) - PyPI server and packaging/testing/release tool. +- [warehouse](https://github.com/pypi/warehouse) - Next generation Python Package Repository (PyPI). -*Plugins for editors and IDEs.* +### Distribution -* Emacs - * [Elpy](https://github.com/jorgenschaefer/elpy) - Emacs Python Development Environment. -* Sublime Text - * [SublimeJEDI](https://github.com/srusskih/SublimeJEDI) - A Sublime Text plugin to the awesome auto-complete library Jedi. - * [Anaconda](https://github.com/DamnWidget/anaconda) - Anaconda turns your Sublime Text 3 in a full featured Python development IDE. -* Vim - * [YouCompleteMe](https://github.com/Valloric/YouCompleteMe) - Includes [Jedi](https://github.com/davidhalter/jedi)-based completion engine for Python. - * [Jedi-vim](https://github.com/davidhalter/jedi-vim) - Vim bindings for the Jedi auto-completion library for Python. - * [Python-mode](https://github.com/klen/python-mode) - An all in one plugin for turning Vim into a Python IDE. -* Visual Studio - * [PTVS](https://github.com/Microsoft/PTVS) - Python Tools for Visual Studio. +_Libraries to create packaged executables for release distribution._ -## IDEs +- Executables + - [pyinstaller](https://github.com/pyinstaller/pyinstaller) - Converts Python programs into stand-alone executables (cross-platform). + - [Nuitka](https://github.com/Nuitka/Nuitka) - Compiles Python programs into high-performance standalone executables (cross-platform, supports all Python versions). + - [shiv](https://github.com/linkedin/shiv) - A command line utility for building fully self-contained zipapps (PEP 441), but with all their dependencies included. + - [cx-Freeze](https://github.com/marcelotduarte/cx_Freeze) - It is a Python tool that converts Python scripts into standalone executables and installers for Windows, macOS, and Linux. +- Obfuscation + - [pyarmor](https://github.com/dashingsoft/pyarmor) - A tool used to obfuscate python scripts, bind obfuscated scripts to fixed machine or expire obfuscated scripts. -*Popular Python IDEs.* +### Configuration Files -* [PyCharm](https://www.jetbrains.com/pycharm/) - Commercial Python IDE by JetBrains. Has free community edition available. -* [LiClipse](http://www.liclipse.com/) - Free polyglot IDE based on Eclipse. Uses PyDev for Python support. -* [Spyder](https://github.com/spyder-ide/spyder) - Open Source Python IDE. +_Libraries for storing and parsing configuration options._ -# Services +- [configparser](https://docs.python.org/3/library/configparser.html) - (Python standard library) INI file parser. +- [python-dotenv](https://github.com/theskumar/python-dotenv) - Reads key-value pairs from a `.env` file and sets them as environment variables. +- [pydantic-settings](https://github.com/pydantic/pydantic-settings) - Settings management using Pydantic models with validation, loading from environment variables and secrets files. +- [hydra-core](https://github.com/facebookresearch/hydra) - Hydra is a framework for elegantly configuring complex applications. +- [dynaconf](https://github.com/dynaconf/dynaconf) - Dynaconf is a configuration manager with plugins for Django, Flask and FastAPI. -Online tools and APIs to simplify development. +**Security** -## Continuous Integration +### Cryptography -*See: [awesome-CIandCD](https://github.com/ciandcd/awesome-ciandcd#online-build-system).* +_Libraries for cryptographic primitives and secure protocols._ -* [Travis CI](https://travis-ci.org) - A popular CI service for your open source and [private](https://travis-ci.com) projects. (GitHub only) -* [CircleCI](https://circleci.com/) - A CI service that can run very fast parallel testing. (GitHub only) -* [Vexor CI](https://vexor.io) - A continuous integration tool for private apps with pay-per-minute billing model. -* [Wercker](http://wercker.com/) - A Docker-based platform for building and deploying applications and microservices. +- [cryptography](https://github.com/pyca/cryptography) - A package designed to expose cryptographic primitives and recipes to Python developers. +- [pynacl](https://github.com/pyca/pynacl) - Python binding to the Networking and Cryptography (NaCl) library. +- [paramiko](https://github.com/paramiko/paramiko) - The leading native Python SSHv2 protocol library. +- [itsdangerous](https://github.com/pallets/itsdangerous) - Various helpers to pass trusted data to untrusted environments. -## Code Quality +### Penetration Testing -* [Codacy](https://www.codacy.com/) - Automated Code Review to ship better code, faster. Free for Open Source. -* [QuantifiedCode](https://www.quantifiedcode.com/) - A data-driven, automated, continuous code review tool. +_Frameworks and tools for penetration testing._ -# Resources +- [mitmproxy](https://github.com/mitmproxy/mitmproxy) - An interactive TLS-capable intercepting HTTP proxy for penetration testers and software developers. +- [sqlmap](https://github.com/sqlmapproject/sqlmap) - Automatic SQL injection and database takeover tool. +- [sherlock-project](https://github.com/sherlock-project/sherlock) - Hunt down social media accounts by username across social networks. +- [social-engineer-toolkit](https://github.com/trustedsec/social-engineer-toolkit) - A toolkit for social engineering. -Where to discover new Python libraries. +### Supply Chain Security -## Websites +_Tools for auditing dependencies against known vulnerabilities._ -* [r/Python](https://www.reddit.com/r/python) -* [CoolGithubProjects](https://www.coolgithubprojects.com/) -* [Django Packages](https://www.djangopackages.com/) -* [Full Stack Python](https://www.fullstackpython.com/) -* [Python 3 Wall of Superpowers](http://python3wos.appspot.com/) -* [Python Hackers](http://pythonhackers.com/open-source/) -* [Python ZEEF](https://python.zeef.com/alan.richmond) -* [Trending Python repositories on GitHub today](https://github.com/trending?l=python) -* [PyPI Ranking](http://pypi-ranking.info/alltime) +- [pip-audit](https://github.com/pypa/pip-audit) - Audits Python environments and dependency trees for known vulnerabilities, using the PyPI Advisory Database and OSV. +- [uv audit](https://docs.astral.sh/uv/reference/cli/#uv-audit) - uv's built-in dependency vulnerability and malware scanning backed by OSV. -## Weekly +### Web Security -* [Import Python Newsletter](http://importpython.com/newsletter/) -* [Pycoder's Weekly](http://pycoders.com/) -* [Python Weekly](http://www.pythonweekly.com/) +_Libraries for application-layer web security._ -## Twitter +- [secure](https://github.com/TypeError/secure) - HTTP security headers for Python web applications with ASGI and WSGI middleware. -* [@codetengu](https://twitter.com/codetengu) -* [@getpy](https://twitter.com/getpy) -* [@importpython](https://twitter.com/importpython) -* [@planetpython](https://twitter.com/planetpython) -* [@pycoders](https://twitter.com/pycoders) -* [@pypi](https://twitter.com/pypi) -* [@pythontrending](https://twitter.com/pythontrending) -* [@PythonWeekly](https://twitter.com/PythonWeekly) +**Other** -# Other Awesome Lists +### Hardware -List of lists. +_Libraries for programming with hardware._ -* Python - * [pycrumbs](https://github.com/kirang89/pycrumbs/blob/master/pycrumbs.md) - * [python-github-projects](https://github.com/checkcheckzz/python-github-projects) - * [python_reference](https://github.com/rasbt/python_reference) - * [pythonidae](https://github.com/svaksha/pythonidae) -* Monty - * [awesome](https://github.com/sindresorhus/awesome) - * [lists](https://github.com/jnv/lists) +- [bleak](https://github.com/hbldh/bleak) - A cross platform Bluetooth Low Energy Client for Python using asyncio. +- [pynput](https://github.com/moses-palmer/pynput) - A library to control and monitor input devices. +- [jumpstarter](https://github.com/jumpstarter-dev/jumpstarter) - A hardware-in-the-loop testing framework with a Python client library for automated testing on real and virtual hardware. -# Contributing +### Microsoft Windows + +_Python programming on Microsoft Windows._ + +- [pythonnet](https://github.com/pythonnet/pythonnet) - Python Integration with the .NET Common Language Runtime (CLR). +- [pywin32](https://github.com/mhammond/pywin32) - Python Extensions for Windows. +- [pyenv-win](https://github.com/pyenv-win/pyenv-win) - A Python version manager for Windows ([pyenv](https://github.com/pyenv/pyenv) fork). +- [winpython](https://github.com/winpython/winpython) - Portable development environment for Windows 10/11. + +### Miscellaneous + +_Useful libraries or tools that don't fit in the categories above._ + +- [blinker](https://github.com/pallets-eco/blinker) - A fast Python in-process signal/event dispatching system. +- [boltons](https://github.com/mahmoud/boltons) - A set of pure-Python utilities. + +## Resources + +Where to discover learning resources or new Python libraries. + +### Newsletters + +- [Awesome Python Newsletter](https://python.libhunt.com/newsletter) +- [Pycoder's Weekly](https://pycoders.com/) +- [Python Tricks](https://realpython.com/python-tricks/) +- [Python Weekly](https://www.pythonweekly.com/) + +### Podcasts + +- [Django Chat](https://djangochat.com/) +- [PyPodcats](https://pypodcats.live) +- [Python Bytes](https://pythonbytes.fm) +- [Talk Python To Me](https://talkpython.fm/) +- [The Real Python Podcast](https://realpython.com/podcasts/rpp/) + +### Websites + +- [Python Developer Tooling Handbook](https://pydevtools.com/) - Comprehensive guide to modern Python developer tools covering package management, linting, type checking, testing, and more. + +## Contributing Your contributions are always welcome! Please take a look at the [contribution guidelines](https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md) first. -I would keep some pull requests open if I'm not sure whether the libraries are awesome, you could [vote for them](https://github.com/vinta/awesome-python/pulls) by adding :+1: to them. +--- + +If you have any question about this opinionated list, do not hesitate to contact [@vinta](https://x.com/vinta) on X (Twitter). diff --git a/SPONSORSHIP.md b/SPONSORSHIP.md new file mode 100644 index 0000000000..e61b9955ee --- /dev/null +++ b/SPONSORSHIP.md @@ -0,0 +1,62 @@ +# Sponsor awesome-python + +**The #10 most-starred repository on all of GitHub.** + +awesome-python is where Python developers go to discover tools. It ranks on the first page of Google for "best Python libraries," is referenced by ChatGPT and other LLMs when recommending Python tools, and is the list developers check when evaluating frameworks. + +Your sponsorship puts your product in front of developers at the exact moment they're choosing what to use. + +## Audience + +| Metric | Value | +| ------------ | ---------------------------------------------------------------------------------------------------- | +| Stars | ![Stars](https://img.shields.io/github/stars/vinta/awesome-python?style=for-the-badge) | +| Forks | ![Forks](https://img.shields.io/github/forks/vinta/awesome-python?style=for-the-badge) | +| Watchers | ![Watchers](https://img.shields.io/github/watchers/vinta/awesome-python?style=for-the-badge) | +| Contributors | ![Contributors](https://img.shields.io/github/contributors/vinta/awesome-python?style=for-the-badge) | + +**Who visits:** Professional Python developers evaluating libraries and tools for production use. Not beginners browsing tutorials. People making adoption decisions. + +**Where they come from:** Google Search, GitHub, Reddit, YouTube, ChatGPT/LLMs, Hacker News. + +## Sponsorship Tiers + +### Headline Sponsor - $500/month + +- Large logo and one-line description (max 120 characters) pinned at the very top of the README, above all project entries +- Logo link in the sponsor section of [awesome-python.com](https://awesome-python.com/) + +### Featured Sponsor - $200/month + +- Text entry (`[Name](URL) - Description.`, max 120 characters) pinned at the top of the README, directly below Headline sponsors +- Text link in the sponsor section of [awesome-python.com](https://awesome-python.com/) + +### Indie Dev Sponsor - $99/month or $249/quarter + +- Lower-cost tier for indie developers, solo founders, and one-person companies only +- Text entry (`[Name](URL) - Description.`, max 120 characters) in the README sponsor section, directly below Featured sponsors +- Text link in the sponsor section of [awesome-python.com](https://awesome-python.com/) + +Indie Dev Sponsor is available only to founder-led (one-person company) products. Funded startups, larger companies, and products with enterprise sales teams should use Featured or Headline Sponsor. + +## Get Started + +Email [sponsorship@awesome-python.com](mailto:sponsorship@awesome-python.com?subject=Awesome%20Python%20Sponsorship) with: + +- **Tier:** Headline Sponsor ($500/mo), Featured Sponsor ($200/mo), or Indie Dev Sponsor ($99/mo or $249/quarter) +- **Content:** Product name, URL, logo, and description (Headline tier) or `[Name](URL) - Description.` entry (Featured or Indie Dev tier) +- **Duration:** 1, 3, 6 months, or longer +- **Payment method:** US bank transfer (ACH/wire) or PayPal + +One upfront payment per term. Setup takes less than 24 hours. + +## Previously Sponsored By + +- [Warp](https://www.warp.dev/) - The terminal for modern developers. +- [pyr](https://pyrun.dev) - Zero-config Python project manager. + +## Editorial Independence + +Sponsorship is logo or link placement in the README header. It does not influence which projects are listed. Listings are curated on merit through the normal [contribution process](CONTRIBUTING.md). + +We reserve the right to request changes to sponsor text, logos, or links that are misleading, off-topic, or incompatible with the README formatting. diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 0f6ced6634..0000000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -awesome-python.com \ No newline at end of file diff --git a/docs/adr/0001-shortlist-not-catalog.md b/docs/adr/0001-shortlist-not-catalog.md new file mode 100644 index 0000000000..095fad3866 --- /dev/null +++ b/docs/adr/0001-shortlist-not-catalog.md @@ -0,0 +1,26 @@ +--- +status: accepted +--- + +# awesome-python is a shortlist of obvious choices, not a catalog + +By mid-2026 the list held 576 entries across 75 sections, with entry inflow up 24x year over year (96 additions in the trailing 12 months vs 4 the year before), concentrated in sections like AI and Agents (41 entries). The old acceptance model — three lanes (Industry Standard, Rising Star, Hidden Gem), only the first capped — admitted any project good enough in isolation, so categories grew without bound and stopped answering the reader's actual question: "what do I use for X?" We decided to reposition the list as a curated shortlist, per the Zen of Python: there should be one — and preferably only one — obvious way to do it. + +## The decision + +The list's scope test is "serves Python developers", replacing the old "primarily written in Python (>50%)" requirement: implementation language and packaging are irrelevant when Python developers use the thing in their Python work (uv and ty are Rust; agent skill packs are markdown), while a pure-Python project nobody uses in Python work does not belong. Each Use Case (a subcategory, or a flat section) lists at most its Obvious Choices — up to 3, plus up to 2 marked Challengers, hard maximum 5 (numbers provisional, to be reviewed after the prune). Admission is by maintainer editorial judgment, informed primarily by PyPI download counts rather than GitHub stars, and stated as final; judgment overrides the signal's known failure modes (CI/dependency-inflated counts, model releases consumed as weights rather than pip installs, large-but-specific audiences misread as "niche"). Once a Use Case is at cap, the only way in is Displacement: the PR names the entry it replaces and argues the newcomer does that job better. Use Cases are defined by the list's existing structure; an entry PR can never create the subcategory it needs. Standard-library entries hold a slot only where the stdlib module is itself the obvious choice. The existing stock gets the same test retroactively: a staged, worst-first prune (per-section sweep commits), with removed entries deleted outright — git history is the archive. Resources sections (Newsletters, Podcasts, Websites) are out of scope for now. + +## Considered options + +- Keep the three-lane model with caps on every lane: rejected because the lanes answer the wrong question ("is this good enough to enter?") once admission is comparative; rising-star momentum becomes evidence for a Challenger slot or a Displacement rather than an admission ticket of its own, and Hidden Gem is definitionally incompatible with "obvious". +- Rules-only, no retroactive prune: rejected because every rejection would face "but X is listed" precedent arguments, and readers would see no change. +- Archive removed entries in a separate file: rejected because it recreates the catalog one click away and dilutes the identity the change exists to restore. + +## Consequences + +- The list shrinks substantially: the maintainer-reviewed preview of the three largest sections kept 45 of 80 entries. Most future PRs will be rejected for fullness, not badness. +- awesome-python.com loses long-tail search traffic for the hundreds of niche tool names it will no longer carry. Accepted deliberately: reader trust over search surface. +- Fast-moving domains (e.g. AI and Agents) list current leaders by usage and absorb churn through Displacement; an oversized Use Case is either trimmed or Split into finer Use Cases by the maintainer. +- Linked awesome-* lists (e.g. awesome-python-testing) remain as the escape valve for readers who want exhaustive catalogs. + +See CONTEXT.md for the vocabulary (Use Case, Obvious Choice, Challenger, Displacement, Split). diff --git a/docs/audit-logs.md b/docs/audit-logs.md new file mode 100644 index 0000000000..80407a2ff7 --- /dev/null +++ b/docs/audit-logs.md @@ -0,0 +1,33 @@ +# Audit Log + +awesome-python is audited section by section. Every entry gets re-verified against live data, and every removal lands in a commit whose body carries the reason. Git history is the archive. This file is the at-a-glance register of maintainer decisions that a single commit can't show. + +## Overrides + +[CONTRIBUTING.md](../CONTRIBUTING.md) allows the maintainer to exceed any limit for a specific entry or use case. Each override is recorded here. + +### Naming Exceptions + +Display names follow the canonical PyPI package name. These entries keep a different name by maintainer decision: + +- autobahn-python -- `autobahn`. +- django-rest-framework -- `djangorestframework`. +- django-rules -- `rules`. +- fasthtml -- `python-fasthtml`. +- jinja -- `Jinja2`. +- mem0 -- `mem0ai`. +- pangu.py -- `pangu`. +- playwright-python -- `playwright`. +- pytorch -- `torch`. +- strawberry -- `strawberry-graphql`. +- strawberry-django -- `strawberry-graphql-django`. + +### Mature-stable Keeps + +These entries sit past the 12-month activity requirement without an override. Each one is kept by editorial judgment: mature, stable, and no successor exists. + +- ftfy +- itsdangerous +- jieba +- jinja +- sortedcontainers diff --git a/docs/extra.css b/docs/extra.css deleted file mode 100644 index 2f58f224ab..0000000000 --- a/docs/extra.css +++ /dev/null @@ -1,37 +0,0 @@ -.bs-sidebar.well { - /* margin-bottom: 0; */ -} - -.bs-sidebar .nav > li > a:hover, -.bs-sidebar .nav > li > a:focus { - border: none; - border-left: 2px solid; -} - -.bs-sidebar .nav > .active > a, -.bs-sidebar .nav > .active:hover > a, -.bs-sidebar .nav > .active:focus > a { - border: none; - border-left: 2px solid; -} - -@media (min-width: 992px) { - .bs-sidebar.affix { - /* top: 60px; */ - /* bottom: 0px; */ - top: 80px; - bottom: 23px; - overflow: auto; - } -} - -@media (min-width: 1200px) { - .bs-sidebar.affix-bottom, - .bs-sidebar.affix { - width: 280px; - } - - .container > .col-md-9 { - padding-left: 40px; - } -} diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 6d2dd3937e..0000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,9 +0,0 @@ -site_name: Awesome Python -site_url: http://awesome-python.com -site_description: A curated list of awesome Python frameworks, libraries and software -site_author: Vinta Chen -repo_url: https://github.com/vinta/awesome-python -theme: flatly -google_analytics: ['UA-510626-7', 'auto'] -pages: - - "Life is short, you need Python.": "index.md" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..6f82506806 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[project] +name = "awesome-python" +version = "0.1.0" +description = "An opinionated guide to the best Python frameworks, libraries, and tools." +authors = [{ name = "Vinta Chen", email = "vinta.chen@gmail.com" }] +readme = "README.md" +license = "MIT" +requires-python = ">=3.14" +dependencies = [] + +[project.urls] +Homepage = "https://awesome-python.com/" +Repository = "https://github.com/vinta/awesome-python" + +[dependency-groups] +build = ["httpx>=0.28.1", "jinja2>=3.1.6", "markdown-it-py>=4.0.0"] +lint = ["ruff>=0.15.6", "ty>=0.0.33"] +test = ["pytest>=9.0.3"] +preview = ["watchfiles>=1.2.0"] +dev = [ + { include-group = "build" }, + { include-group = "lint" }, + { include-group = "test" }, + { include-group = "preview" }, +] + +[tool.uv] +exclude-newer = "3 days" +no-build = true + +[tool.uv.pip] +only-binary = [":all:"] + +[tool.ruff] +line-length = 200 + +[tool.ty.environment] +python-version = "3.14" +root = ["website"] + +[tool.ty.terminal] +error-on-warning = true + +[tool.ty.rules] +division-by-zero = "error" +possibly-missing-attribute = "error" +possibly-missing-import = "error" +possibly-unresolved-reference = "error" +unused-ignore-comment = "error" + +[tool.pytest.ini_options] +testpaths = ["website/tests"] +pythonpath = ["website"] diff --git a/sort.py b/sort.py deleted file mode 100644 index d8091009ca..0000000000 --- a/sort.py +++ /dev/null @@ -1,50 +0,0 @@ -# coding: utf-8 - -""" - The approach taken is explained below. I decided to do it simply. - Initially I was considering parsing the data into some sort of - structure and then generating an appropriate README. I am still - considering doing it - but for now this should work. The only issue - I see is that it only sorts the entries at the lowest level, and that - the order of the top-level contents do not match the order of the actual - entries. - - This could be extended by having nested blocks, sorting them recursively - and flattening the end structure into a list of lines. Revision 2 maybe ^.^. -""" - - -def main(): - # First, we load the current README into memory as an array of lines - with open('README.md', 'r') as read_me_file: - read_me = read_me_file.readlines() - - # Then we cluster the lines together as blocks - # Each block represents a collection of lines that should be sorted - # This was done by assuming only links ([...](...)) are meant to be sorted - # Clustering is done by indentation - blocks = [] - last_indent = None - for line in read_me: - s_line = line.lstrip() - indent = len(line) - len(s_line) - - if any([s_line.startswith(s) for s in ['* [', '- [']]): - if indent == last_indent: - blocks[-1].append(line) - else: - blocks.append([line]) - last_indent = indent - else: - blocks.append([line]) - last_indent = None - - with open('README.md', 'w+') as sorted_file: - # Then all of the blocks are sorted individually - blocks = [''.join(sorted(block, key=lambda s: s.lower())) for block in blocks] - # And the result is written back to README.md - sorted_file.write(''.join(blocks)) - - -if __name__ == "__main__": - main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..a3d6793cb8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,349 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P3D" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "awesome-python" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +build = [ + { name = "httpx" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, +] +dev = [ + { name = "httpx" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "ty" }, + { name = "watchfiles" }, +] +lint = [ + { name = "ruff" }, + { name = "ty" }, +] +preview = [ + { name = "watchfiles" }, +] +test = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +build = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "markdown-it-py", specifier = ">=4.0.0" }, +] +dev = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "markdown-it-py", specifier = ">=4.0.0" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "ruff", specifier = ">=0.15.6" }, + { name = "ty", specifier = ">=0.0.33" }, + { name = "watchfiles", specifier = ">=1.2.0" }, +] +lint = [ + { name = "ruff", specifier = ">=0.15.6" }, + { name = "ty", specifier = ">=0.0.33" }, +] +preview = [{ name = "watchfiles", specifier = ">=1.2.0" }] +test = [{ name = "pytest", specifier = ">=9.0.3" }] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[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 = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[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 = "pytest" +version = "9.0.3" +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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +] + +[[package]] +name = "ty" +version = "0.0.33" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/44/9478c50c266826c1bf30d1692e589755bffa8f1c0a3eb7af8a346c255991/ty-0.0.33.tar.gz", hash = "sha256:46d63bda07403322cb6c28ccfdd5536be916e13df725c29f7ccd0a21f06bd9e8", size = 5559373, upload-time = "2026-04-28T10:45:13.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/24/e287388c63a19191be26b32ff4dbd06029834068150ebe2532939bc4c851/ty-0.0.33-py3-none-linux_armv6l.whl", hash = "sha256:94d0a9d2234261a8911396d59e506b5923fe0971dbda43b9dcea287936887fcc", size = 11021308, upload-time = "2026-04-28T10:45:43.34Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/ba1eed819895bd239fba8ee35dfcd5fcb266c203b0914a17a59579096bb5/ty-0.0.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4a2b5ba078f90de342f56b5f7979bb77c9b9b1d8625a041352ffc6ee93c4073", size = 10777272, upload-time = "2026-04-28T10:45:32.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/a8/c3131d37b44b3fea1d6654a1c929a0cd0873822f77a90482b8ec28f6fbbd/ty-0.0.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84ff5707825e9af9668d2bcf66975f93e520a63b524ab494e3a8265735be2563", size = 10201078, upload-time = "2026-04-28T10:45:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/7b/db/d8e37ff0045810cc65e1ff36aa0da0a2253c05659787ac987df8a16c7897/ty-0.0.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e375285736f57886868e7af0b11c7b0ec5b6543fa15e7ad2a714fed9f077d4e0", size = 10732347, upload-time = "2026-04-28T10:45:21.444Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1a/20e83a412506a918e4684fc67b567cf7cc13b105470b3428cb23c3d5aa13/ty-0.0.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5680f6350c3b4e46b8bff6d7bb132366ea239463d6cad4892725d06046e65464", size = 10808238, upload-time = "2026-04-28T10:45:38.565Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4b/d0a39f4464dc6cb4cc2c159473ce216bd1846bfb684c0323a3cb36dce5c6/ty-0.0.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5535538bad8d0f7e62bcdff02197cdb30e41451d80b35d27e17d128f2e1dc5d", size = 11288348, upload-time = "2026-04-28T10:45:08.419Z" }, + { url = "https://files.pythonhosted.org/packages/35/7e/f1745e0f9583363d7a83d9a4990fc244f76ecc30840ddad83dc16a33c52d/ty-0.0.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da196c42bbbc069e1e21e3e52107c061aa9660352dae57a41930690b56e2c02d", size = 11789907, upload-time = "2026-04-28T10:45:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/25f39f46a12d662859d45bc648555d0661044eb43db6b5648c9947487da9/ty-0.0.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9281672921ef6d4460e03146b5e6c18cb1a3e3a3b8a1a88f6f33226d05a469b7", size = 11500774, upload-time = "2026-04-28T10:45:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/ec/136959ecbb7c71cb90537f5aea441c73f4ab24612868a6ecdc9d7444d32d/ty-0.0.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1b8f303f82da64e878108e764be3ecbcd7c9903ac0a7f7031614ed00b97ab", size = 11360314, upload-time = "2026-04-28T10:45:05.402Z" }, + { url = "https://files.pythonhosted.org/packages/cf/95/32809575c222f00beed498cb728e9290a0f5009f930025381bb7253b2206/ty-0.0.33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:efe3af412c9ff67bce5fa37d0a2b0d8555c24072b145a5bac6c79637f1c83abe", size = 10707785, upload-time = "2026-04-28T10:45:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/13/89/c8e9531f7aa4a093359e15fa32c8e1277fbbe90d16894d7c6032d29f4b34/ty-0.0.33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aeec29c91ea768601747da546c3efc20b72c2fb1bd52bcc786a5c6eeff51d27b", size = 10834987, upload-time = "2026-04-28T10:45:40.738Z" }, + { url = "https://files.pythonhosted.org/packages/31/16/9835fbcf5338af1a1917bd28fdb8a7193c210b83f243aa286fa9f79cb3ad/ty-0.0.33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a535977c52bbb5f7e96b8b70a6ad375ad077f4a9ff2492508ea3816a2b403819", size = 10968968, upload-time = "2026-04-28T10:45:30.26Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/64c76aabc1bc70c7f24b686cd93c3407f8ea430905e395f59bf9603ef571/ty-0.0.33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d732facf39fcb221ba279d469c5040d37883e964f123b1563888efd34818180", size = 11458077, upload-time = "2026-04-28T10:45:45.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/fae27b0c4718776a298690d31ca4cc1995f2e3e1c63a7b59e84c41498e9a/ty-0.0.33-py3-none-win32.whl", hash = "sha256:d90960b574428dc252f85e8598ec5fcb7f619794196b2fc95a90da075ed4681c", size = 10345364, upload-time = "2026-04-28T10:45:16.836Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a0/a2938b23ae3e1a09a2d7c189e2ac5f7113676bae4e0e23948b568e18e5f8/ty-0.0.33-py3-none-win_amd64.whl", hash = "sha256:c1c3aec62c44de610c6e95f0a4e97ac3dbc07934bfdbf1fd90d758c9ff72f48e", size = 11342470, upload-time = "2026-04-28T10:45:26.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/62/7fb948aace38d2f6329261bb33c035a8484549c74f1db28649c7a4c6fed9/ty-0.0.33-py3-none-win_arm64.whl", hash = "sha256:0d44f99ba1b441e55e2aa301b2ac0a21112784931b46a5f66f4ea9efe5620d97", size = 10742673, upload-time = "2026-04-28T10:45:35.555Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] diff --git a/website/build.py b/website/build.py new file mode 100644 index 0000000000..611c3952fd --- /dev/null +++ b/website/build.py @@ -0,0 +1,793 @@ +#!/usr/bin/env python3 +"""Build a single-page HTML site from README.md for the awesome-python website.""" + +import json +import re +import shutil +import xml.etree.ElementTree as ET +from collections import Counter +from collections.abc import Sequence +from datetime import UTC, datetime +from pathlib import Path +from typing import TypedDict + +from fetch_pypi_downloads_via_clickpy import normalize +from jinja2 import Environment, FileSystemLoader +from readme_parser import AlsoSee, ParsedGroup, ParsedSection, parse_readme, parse_sponsors, slugify + +GITHUB_REPO_URL_RE = re.compile(r"^https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$") +MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)\s]+)\)") +BULLET_LINE_RE = re.compile(r"^\s*-\s") +SITE_URL = "https://awesome-python.com/" +SITEMAP_URL = f"{SITE_URL}sitemap.xml" +SITEMAP_NS = "http://www.sitemaps.org/schemas/sitemap/0.9" + +BUILTIN_FILTER = "Built-in" +BUILTIN_SLUG = "built-in" +BUILTIN_PATH = f"/categories/{BUILTIN_SLUG}/" +BUILTIN_PUBLIC_URL = f"{SITE_URL}categories/{BUILTIN_SLUG}/" + +SPONSORSHIP_PATH = "/sponsorship/" +SPONSORSHIP_PUBLIC_URL = f"{SITE_URL}sponsorship/" +SPONSORSHIP_DESCRIPTION = "Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use." + +SOURCE_TYPE_DOMAINS = { + "docs.python.org": "Built-in", + "gitlab.com": "GitLab", + "bitbucket.org": "Bitbucket", +} + + +class TemplateSubcategory(TypedDict): + name: str + value: str + slug: str + url: str + + +class TemplateEntry(TypedDict): + name: str + url: str + description: str + categories: list[str] + groups: list[str] + subcategories: list[TemplateSubcategory] + stars: int | None + downloads: int | None + owner: str | None + last_commit_at: str | None + source_type: str | None + also_see: list[AlsoSee] + + +class SyntheticCategory(TypedDict): + name: str + slug: str + description: str + description_html: str + + +TemplateCategory = ParsedSection | SyntheticCategory + + +def detect_source_type(url: str) -> str | None: + """Detect source type from URL domain. Returns None for GitHub URLs.""" + if GITHUB_REPO_URL_RE.match(url): + return None + for domain, source_type in SOURCE_TYPE_DOMAINS.items(): + if domain in url: + return source_type + if "github.com" not in url: + return "External" + return None + + +def extract_github_repo(url: str) -> str | None: + """Extract owner/repo from a GitHub repo URL. Returns None for non-GitHub URLs.""" + m = GITHUB_REPO_URL_RE.match(url) + return m.group(1) if m else None + + +def load_stars(path: Path) -> dict[str, dict]: + """Load star data from JSON. Returns empty dict if file doesn't exist or is corrupt.""" + if path.exists(): + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + return {} + + +def load_downloads(path: Path) -> dict[str, int]: + """Load last-30-day download counts from the TSV cache, keyed by normalized README name. + + Columns: name, package, downloads, fetched_at. Skips the header and + NOT_FOUND rows. Returns empty dict if the file doesn't exist. + """ + if not path.exists(): + return {} + downloads: dict[str, int] = {} + for line in path.read_text(encoding="utf-8").splitlines()[1:]: + parts = line.split("\t") + if len(parts) >= 3 and parts[2].isdigit(): + downloads[parts[0]] = int(parts[2]) + return downloads + + +def sort_entries(entries: Sequence[TemplateEntry]) -> list[TemplateEntry]: + """Sort entries by downloads descending, then name ascending. + + Four tiers: entries with download counts first, then starred entries by + stars descending, then stdlib, then the rest. + """ + + def sort_key(entry: TemplateEntry) -> tuple[int, int, int, str]: + downloads = entry["downloads"] + stars = entry["stars"] + name = entry["name"].lower() + if downloads is not None: + return (0, -downloads, 0, name) + if stars is not None: + builtin = 1 if entry.get("source_type") == "Built-in" else 0 + return (1, -stars, builtin, name) + if entry.get("source_type") == "Built-in": + return (2, 0, 0, name) + return (3, 0, 0, name) + + return sorted(entries, key=sort_key) + + +def build_robots_txt() -> str: + return f"User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: /\n\nSitemap: {SITEMAP_URL}\n" + + +WEBSITE_ID = f"{SITE_URL}#website" +ISPARTOF_WEBSITE = {"@type": "WebSite", "@id": WEBSITE_ID} + + +def _website_node() -> dict: + return { + "@type": "WebSite", + "@id": WEBSITE_ID, + "name": "Awesome Python", + "url": SITE_URL, + "inLanguage": "en", + "sameAs": "https://github.com/vinta/awesome-python", + } + + +def _item_list_payload(entries: Sequence[TemplateEntry]) -> dict: + return { + "@type": "ItemList", + "numberOfItems": len(entries), + "itemListElement": [ + { + "@type": "ListItem", + "position": i, + "name": entry["name"], + "url": entry["url"], + } + for i, entry in enumerate(entries, start=1) + ], + } + + +def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: int) -> dict: + description = ( + "An opinionated guide to the best Python frameworks, libraries, and tools. " + f"Explore {len(entries)} curated projects across {total_categories} categories, " + "from AI and agents to data science and web development." + ) + return { + "@context": "https://schema.org", + "@graph": [ + _website_node(), + { + "@type": "CollectionPage", + "@id": SITE_URL, + "name": "Awesome Python", + "url": SITE_URL, + "description": description, + "isPartOf": ISPARTOF_WEBSITE, + "inLanguage": "en", + "mainEntity": _item_list_payload(entries), + }, + ], + } + + +def category_meta_title(name: str, parent_name: str | None = None) -> str: + if parent_name: + title = f"{name} for {parent_name} - Awesome Python" + if len(title) <= 60: + return title + title = f"{parent_name}: {name} - Awesome Python" + if len(title) <= 60: + return title + return f"{name} - Awesome Python" + title = f"{name} Python Libraries - Awesome Python" + if len(title) <= 60: + return title + return f"{name} - Awesome Python" + + +def category_meta_description(name: str, entry_count: int, description: str, parent_name: str | None = None) -> str: + target = f"{name} for {parent_name}" if parent_name else name + count_sentence = f"Explore {entry_count} curated Python projects in {target}." + if description: + lead = description if description.endswith((".", "!", "?")) else f"{description}." + return f"{lead} {count_sentence}" + return f"{count_sentence} Part of the Awesome Python catalog." + + +def build_breadcrumb_json_ld(items: Sequence[tuple[str, str]]) -> dict: + return { + "@type": "BreadcrumbList", + "itemListElement": [ + { + "@type": "ListItem", + "position": i, + "name": name, + "item": url, + } + for i, (name, url) in enumerate(items, start=1) + ], + } + + +def build_category_json_ld( + name: str, + url: str, + description: str, + entries: Sequence[TemplateEntry], + breadcrumbs: Sequence[tuple[str, str]], +) -> dict: + return { + "@context": "https://schema.org", + "@graph": [ + _website_node(), + { + "@type": "CollectionPage", + "@id": url, + "name": name, + "url": url, + "description": description, + "isPartOf": ISPARTOF_WEBSITE, + "inLanguage": "en", + "mainEntity": _item_list_payload(entries), + }, + build_breadcrumb_json_ld(breadcrumbs), + ], + } + + +def build_sponsorship_json_ld() -> dict: + return { + "@context": "https://schema.org", + "@graph": [ + _website_node(), + { + "@type": "WebPage", + "@id": SPONSORSHIP_PUBLIC_URL, + "name": "Sponsor Awesome Python", + "url": SPONSORSHIP_PUBLIC_URL, + "description": SPONSORSHIP_DESCRIPTION, + "isPartOf": ISPARTOF_WEBSITE, + "inLanguage": "en", + }, + build_breadcrumb_json_ld( + [ + ("Awesome Python", SITE_URL), + ("Sponsorship", SPONSORSHIP_PUBLIC_URL), + ] + ), + ], + } + + +def category_path(category: ParsedSection) -> str: + return f"/categories/{category['slug']}/" + + +def category_public_url(category: ParsedSection) -> str: + return f"{SITE_URL}categories/{category['slug']}/" + + +def group_path(group_slug: str) -> str: + return f"/categories/{group_slug}/" + + +def group_public_url(group_slug: str) -> str: + return f"{SITE_URL}categories/{group_slug}/" + + +def subcategory_path(category_slug: str, subcategory_slug: str) -> str: + return f"/categories/{category_slug}/{subcategory_slug}/" + + +def subcategory_public_url(category_slug: str, subcategory_slug: str) -> str: + return f"{SITE_URL}categories/{category_slug}/{subcategory_slug}/" + + +def synthetic_category(name: str, slug: str) -> SyntheticCategory: + return {"name": name, "slug": slug, "description": "", "description_html": ""} + + +def write_sitemap_xml(path: Path, urls: Sequence[tuple[str, str]]) -> None: + ET.register_namespace("", SITEMAP_NS) + urlset = ET.Element(f"{{{SITEMAP_NS}}}urlset") + for url, lastmod in urls: + url_el = ET.SubElement(urlset, f"{{{SITEMAP_NS}}}url") + loc_el = ET.SubElement(url_el, f"{{{SITEMAP_NS}}}loc") + loc_el.text = url + lastmod_el = ET.SubElement(url_el, f"{{{SITEMAP_NS}}}lastmod") + lastmod_el.text = lastmod + + tree = ET.ElementTree(urlset) + ET.indent(tree, space=" ") + tree.write(path, encoding="utf-8", xml_declaration=True) + with path.open("ab") as f: + f.write(b"\n") + + +def top_level_heading_text(line: str) -> str | None: + stripped = line.strip() + match = re.match(r"^(#{1,2})\s+(.+)$", stripped) + if match is None: + return None + return match.group(2).strip().strip("#").strip().strip("*").strip() + + +def extract_categories_body(markdown: str) -> str: + """Return content from `Categories` through `Projects`, excluding later sections.""" + lines = markdown.splitlines(keepends=True) + start_idx = None + end_idx = len(lines) + for i, line in enumerate(lines): + heading = top_level_heading_text(line) + if heading is None: + continue + if start_idx is None and heading.lower() == "categories": + start_idx = i + 1 + while start_idx < len(lines) and lines[start_idx].strip() == "": + start_idx += 1 + elif start_idx is not None and heading.lower() in ("resources", "contributing"): + end_idx = i + break + if start_idx is None: + return "" + return "".join(lines[start_idx:end_idx]).rstrip() + "\n" + + +def github_markdown_anchor(text: str) -> str: + anchor = text.strip().lower() + anchor = re.sub(r"[^\w\s-]", "", anchor) + anchor = re.sub(r"\s", "-", anchor) + return f"#{anchor}" + + +def link_llms_category_index_to_canonical_pages(markdown: str, categories: Sequence[ParsedSection]) -> str: + """Point the README-derived category index at canonical category pages.""" + category_urls = {} + for category in categories: + public_url = category_public_url(category) + category_urls[f"#{category['slug']}"] = public_url + category_urls[github_markdown_anchor(category["name"])] = public_url + lines = markdown.splitlines(keepends=True) + out: list[str] = [] + + def replace_link(match: re.Match[str]) -> str: + target = match.group(2) + url = category_urls.get(target) + if url is None: + return match.group(0) + return match.group(0).replace(f"({target})", f"({url})", 1) + + for line in lines: + out.append(MARKDOWN_LINK_RE.sub(replace_link, line)) + + return "".join(out) + + +def build_llms_txt( + template_text: str, + *, + readme_text: str, + subtitle: str, + build_date: str, + stars_data: dict[str, dict], + downloads_data: dict[str, int], + categories: Sequence[ParsedSection], + total_entries: int, +) -> str: + """Render the llms.txt entry point with the curated category shortlist.""" + categories_md = annotate_entries_with_stats( + link_llms_category_index_to_canonical_pages( + extract_categories_body(readme_text).rstrip(), + categories, + ), + stars_data, + downloads_data, + ) + text_env = Environment(autoescape=False, trim_blocks=True, lstrip_blocks=True) + rendered = text_env.from_string(template_text).render( + subtitle=subtitle, + build_date=build_date, + site_url=SITE_URL, + github_repo_url="https://github.com/vinta/awesome-python", + contributing_url="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md", + sponsorship_url=SPONSORSHIP_PUBLIC_URL, + sitemap_url=SITEMAP_URL, + categories_md=categories_md, + total_entries=total_entries, + total_categories=len(categories), + ) + return rendered.rstrip() + "\n" + + +def annotate_entries_with_stats( + markdown: str, + stars_data: dict[str, dict], + downloads_data: dict[str, int], +) -> str: + """Append download and star counts to bullet entry lines. + + Downloads are looked up by the first link's display name (the same key the + TSV cache uses); stars by the first GitHub link with known star data. + """ + lines = markdown.splitlines(keepends=True) + out: list[str] = [] + for line in lines: + if not BULLET_LINE_RE.match(line): + out.append(line) + continue + links = MARKDOWN_LINK_RE.findall(line) + parts: list[str] = [] + if links: + name, url = links[0] + # Category-index bullets link into the site itself, and Built-in entries + # would hit same-named PyPI backports (e.g. logging) — no counts for either. + if not url.startswith((SITE_URL, "#")) and detect_source_type(url) != "Built-in": + downloads = downloads_data.get(normalize(name)) + if downloads is not None: + parts.append(f"PyPI downloads/month: {downloads}") + for _, url in links: + repo_key = extract_github_repo(url) + if not repo_key: + continue + entry = stars_data.get(repo_key) + if entry and "stars" in entry: + parts.append(f"GitHub stars: {entry['stars']}") + break + if not parts: + out.append(line) + continue + stripped = line.rstrip("\n") + ending = line[len(stripped) :] + out.append(f"{stripped} ({', '.join(parts)}){ending}") + return "".join(out) + + +def remove_sponsors_section(markdown: str) -> str: + lines = markdown.splitlines(keepends=True) + start_idx = None + for i, line in enumerate(lines): + heading = top_level_heading_text(line) + if heading and heading.lower() == "sponsors": + start_idx = i + break + + if start_idx is None: + return markdown + + end_idx = len(lines) + for i, line in enumerate(lines[start_idx + 1 :], start=start_idx + 1): + if top_level_heading_text(line): + end_idx = i + break + + return "".join(lines[:start_idx] + lines[end_idx:]) + + +def extract_entries( + categories: list[ParsedSection], + groups: list[ParsedGroup], +) -> list[TemplateEntry]: + """Flatten categories into individual library entries for table display. + + Entries appearing in multiple categories are merged into a single entry + with lists of categories and groups. + """ + cat_to_group = {cat["name"]: group["name"] for group in groups for cat in group["categories"]} + + seen: dict[tuple[str, str], TemplateEntry] = {} # (url, name) -> entry + entries: list[TemplateEntry] = [] + for cat in categories: + group_name = cat_to_group.get(cat["name"], "Other") + for entry in cat["entries"]: + key = (entry["url"], entry["name"]) + existing = seen.get(key) + if existing is None: + existing = TemplateEntry( + name=entry["name"], + url=entry["url"], + description=entry["description"], + categories=[], + groups=[], + subcategories=[], + stars=None, + downloads=None, + owner=None, + last_commit_at=None, + source_type=detect_source_type(entry["url"]), + also_see=entry["also_see"], + ) + seen[key] = existing + entries.append(existing) + if cat["name"] not in existing["categories"]: + existing["categories"].append(cat["name"]) + if group_name not in existing["groups"]: + existing["groups"].append(group_name) + subcat = entry["subcategory"] + if subcat: + scoped = f"{cat['name']} > {subcat}" + if not any(s["value"] == scoped for s in existing["subcategories"]): + sub_slug = slugify(subcat) + existing["subcategories"].append( + TemplateSubcategory( + name=subcat, + value=scoped, + slug=sub_slug, + url=f"/categories/{cat['slug']}/{sub_slug}/", + ) + ) + return entries + + +def build(repo_root: Path) -> None: + """Main build: parse README, render single-page HTML via Jinja2 templates.""" + website = repo_root / "website" + readme_text = (repo_root / "README.md").read_text(encoding="utf-8") + + subtitle = "" + for line in readme_text.split("\n"): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + subtitle = stripped + break + + parsed_groups = parse_readme(readme_text) + sponsors = parse_sponsors(readme_text) + + categories = [cat for g in parsed_groups for cat in g["categories"]] + cat_slugs = [cat["slug"] for cat in categories] + group_slugs = [g["slug"] for g in parsed_groups] + all_top_level_slugs = cat_slugs + group_slugs + [BUILTIN_SLUG] + duplicates = {s for s, n in Counter(all_top_level_slugs).items() if n > 1} + if duplicates: + raise ValueError(f"slug collision in /categories/ namespace: {sorted(duplicates)}. Rename a category or group so their slugs differ.") + total_entries = sum(c["entry_count"] for c in categories) + entries = extract_entries(categories, parsed_groups) + build_date = datetime.now(UTC) + + stars_data = load_stars(website / "data" / "github_stars.json") + downloads_data = load_downloads(website / "data" / "pypi_downloads.tsv") + + repo_self = stars_data.get("vinta/awesome-python", {}) + repo_stars = None + if "stars" in repo_self: + stars_val = repo_self["stars"] + repo_stars = f"{stars_val // 1000}k" if stars_val >= 1000 else str(stars_val) + + for entry in entries: + repo_key = extract_github_repo(entry["url"]) + if not repo_key and entry.get("source_type") == "Built-in": + repo_key = "python/cpython" + if repo_key and repo_key in stars_data: + sd = stars_data[repo_key] + entry["stars"] = sd["stars"] + entry["owner"] = sd["owner"] + entry["last_commit_at"] = sd.get("last_commit_at", "") + # Built-in entries would hit same-named PyPI backports (e.g. asyncio), not the stdlib. + if entry.get("source_type") != "Built-in": + entry["downloads"] = downloads_data.get(normalize(entry["name"])) + + entries = sort_entries(entries) + category_urls = {cat["name"]: category_path(cat) for cat in categories} + + filter_urls: dict[str, str] = dict(category_urls) + for group in parsed_groups: + filter_urls[group["name"]] = group_path(group["slug"]) + for entry in entries: + for sub in entry.get("subcategories", []): + filter_urls[sub["value"]] = sub["url"] + builtin_entries = [e for e in entries if e.get("source_type") == BUILTIN_FILTER] + if builtin_entries: + filter_urls[BUILTIN_FILTER] = BUILTIN_PATH + + env = Environment( + loader=FileSystemLoader(website / "templates"), + autoescape=True, + trim_blocks=True, + lstrip_blocks=True, + ) + site_dir = website / "output" + if site_dir.exists(): + shutil.rmtree(site_dir) + site_dir.mkdir(parents=True) + + filter_urls_json = json.dumps(filter_urls, sort_keys=True, ensure_ascii=False).replace(" None: + page_dir.mkdir(parents=True, exist_ok=True) + parent_name = parent_category["name"] if parent_category else None + category_title = category_meta_title(category["name"], parent_name) + category_description = category_meta_description(category["name"], len(entries), category["description"], parent_name) + breadcrumbs = [("Awesome Python", SITE_URL)] + if parent_category: + breadcrumbs.append((parent_category["name"], category_public_url(parent_category))) + breadcrumbs.append((category["name"], category_url)) + category_json_ld = json.dumps( + build_category_json_ld(category_title.removesuffix(" - Awesome Python"), category_url, category_description, entries, breadcrumbs), + ensure_ascii=False, + ).replace(" (cat_slug, sub_slug, sub_name) + cat_slug_by_url_prefix = {f"/categories/{c['slug']}/": c["slug"] for c in categories} + cat_by_slug = {c["slug"]: c for c in categories} + for entry in entries: + for sub in entry.get("subcategories", []): + value = sub["value"] + subcat_to_entries.setdefault(value, []).append(entry) + if value not in subcat_meta: + for prefix, cat_slug in cat_slug_by_url_prefix.items(): + if sub["url"].startswith(prefix): + subcat_meta[value] = (cat_slug, sub["slug"], sub["name"]) + break + + for value, (cat_slug, sub_slug, sub_name) in subcat_meta.items(): + render_category( + synthetic_category(sub_name, sub_slug), + category_url=subcategory_public_url(cat_slug, sub_slug), + entries=subcat_to_entries[value], + current_path=subcategory_path(cat_slug, sub_slug), + page_dir=categories_dir / cat_slug / sub_slug, + parent_category=cat_by_slug[cat_slug], + ) + + static_src = website / "static" + static_dst = site_dir / "static" + if static_src.exists(): + shutil.copytree(static_src, static_dst, dirs_exist_ok=True) + + sponsorship_md = repo_root / "SPONSORSHIP.md" + sponsorship_md_mtime = datetime.fromtimestamp(sponsorship_md.stat().st_mtime, tz=UTC).date().isoformat() + llms_template = (website / "templates" / "llms.txt").read_text(encoding="utf-8") + llms_txt = build_llms_txt( + llms_template, + readme_text=readme_text, + subtitle=subtitle, + build_date=build_date.strftime("%Y/%m/%d"), + stars_data=stars_data, + downloads_data=downloads_data, + categories=categories, + total_entries=total_entries, + ) + (site_dir / "robots.txt").write_text(build_robots_txt(), encoding="utf-8") + sitemap_date = build_date.date().isoformat() + sitemap_urls = [(SITE_URL, sitemap_date)] + sitemap_urls.extend((category_public_url(c), sitemap_date) for c in categories) + sitemap_urls.extend((group_public_url(g["slug"]), sitemap_date) for g in parsed_groups) + if builtin_entries: + sitemap_urls.append((BUILTIN_PUBLIC_URL, sitemap_date)) + for cat_slug, sub_slug, _ in sorted(subcat_meta.values()): + sitemap_urls.append((subcategory_public_url(cat_slug, sub_slug), sitemap_date)) + sitemap_urls.append((SPONSORSHIP_PUBLIC_URL, sponsorship_md_mtime)) + write_sitemap_xml(site_dir / "sitemap.xml", sitemap_urls) + (site_dir / "llms.txt").write_text(llms_txt, encoding="utf-8") + + print(f"Built site with {len(parsed_groups)} groups, {len(categories)} categories") + print(f"Total entries: {total_entries}") + print(f"Output: {site_dir}") + + +if __name__ == "__main__": + build(Path(__file__).parent.parent) diff --git a/website/data/pypi_name_overrides.json b/website/data/pypi_name_overrides.json new file mode 100644 index 0000000000..8c8a5f95f6 --- /dev/null +++ b/website/data/pypi_name_overrides.json @@ -0,0 +1,42 @@ +{ + "autobahn-python": { "package": "autobahn", "reason": null }, + "azure-sdk-for-python": { "package": null, "reason": "monorepo umbrella; ships as many azure-* packages, no single package represents it" }, + "cloud-init": { "package": null, "reason": "installed via distro images, not pip" }, + "concurrent-futures": { "package": null, "reason": "stdlib module" }, + "cpython": { "package": null, "reason": "not pip-installable; the cpython PyPI name was squatted" }, + "devpi": { "package": "devpi-server", "reason": "devpi is a deprecated metapackage; the server package carries real usage" }, + "difflib": { "package": null, "reason": "stdlib module" }, + "django-ai-plugins": { "package": null, "reason": "GitHub-only so far; young project, remap if it publishes" }, + "django-db-models": { "package": null, "reason": "part of Django, not a package" }, + "django-rest-framework": { "package": "djangorestframework", "reason": "django-rest-framework is a dead alias package" }, + "django-rules": { "package": "rules", "reason": "PyPI django-rules is an abandoned unrelated fork; dfunckt's project publishes as rules" }, + "fasthtml": { "package": "python-fasthtml", "reason": null }, + "geodjango": { "package": null, "reason": "part of Django (django.contrib.gis), not a package" }, + "google-cloud-python": { "package": null, "reason": "monorepo umbrella; ships as many google-cloud-* packages" }, + "graphify": { "package": null, "reason": "GitHub-only so far; young project, remap if it publishes" }, + "httpx-url": { "package": null, "reason": "class in the httpx package, not a package" }, + "jinja": { "package": "jinja2", "reason": "PyPI jinja is Jinja1; current releases publish as jinja2" }, + "mem0": { "package": "mem0ai", "reason": null }, + "micropython": { "package": null, "reason": "interpreter distributed as firmware builds, not pip" }, + "mimetypes": { "package": null, "reason": "stdlib module" }, + "odoo": { "package": null, "reason": "self-hosted suite installed from odoo.com, not pip" }, + "pangu-py": { "package": "pangu", "reason": null }, + "playwright-python": { "package": "playwright", "reason": null }, + "pyenv": { "package": null, "reason": "installed via git/homebrew; PyPI pyenv is a defunct shim" }, + "pypy": { "package": null, "reason": "interpreter distributed via pypy.org, not pip" }, + "python-patterns": { "package": null, "reason": "reference repo, not a package; PyPI name held by unrelated 2012 project" }, + "pytorch": { "package": "torch", "reason": "PyPI pytorch is a squatter; real package is torch" }, + "renpy": { "package": null, "reason": "distributed via renpy.org; PyPI name was squatted, now 404" }, + "sentry-skills": { "package": null, "reason": "agent skills repo, not a package" }, + "social-engineer-toolkit": { "package": null, "reason": "GitHub-only, installed from source" }, + "sqlite3": { "package": null, "reason": "stdlib module" }, + "strawberry": { "package": "strawberry-graphql", "reason": "PyPI strawberry is an unrelated bookmarking service" }, + "strawberry-django": { "package": "strawberry-graphql-django", "reason": null }, + "thealgorithms": { "package": null, "reason": "educational monorepo, not a package" }, + "tkinter": { "package": null, "reason": "stdlib module" }, + "tomllib": { "package": null, "reason": "stdlib module" }, + "trailofbits-skills": { "package": null, "reason": "agent skills repo, not a package" }, + "warehouse": { "package": null, "reason": "the PyPI server itself; deployed, not pip-installed" }, + "winpython": { "package": null, "reason": "distribution with its own installer; PyPI name is a 2012 relic" }, + "zoneinfo": { "package": null, "reason": "stdlib module" } +} diff --git a/website/fetch_github_stars.py b/website/fetch_github_stars.py new file mode 100644 index 0000000000..3f4518854e --- /dev/null +++ b/website/fetch_github_stars.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Fetch GitHub star counts and owner info for all GitHub repos in README.md. + +The output file (data/github_stars.json) is gitignored; CI fetches it at +deploy time, so local runs are for previews only — never commit the data. +Entries removed from README.md leave harmless orphan keys behind. +""" + +import json +import os +import re +import sys +from collections.abc import Sequence +from datetime import UTC, datetime, timedelta +from itertools import batched +from pathlib import Path + +import httpx +from build import extract_github_repo, load_stars + +CACHE_MAX_AGE_HOURS = 12 +DATA_DIR = Path(__file__).parent / "data" +CACHE_FILE = DATA_DIR / "github_stars.json" +README_PATH = Path(__file__).parent.parent / "README.md" +GRAPHQL_URL = "https://api.github.com/graphql" +BATCH_SIZE = 50 + +# GitHub usernames: alphanumeric and hyphens, must start/end with alphanumeric. +GITHUB_OWNER_RE = re.compile(r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$") +# GitHub repo names: alphanumeric, hyphens, underscores, dots, must start with alphanumeric. +GITHUB_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$") + + +def extract_github_repos(text: str) -> set[str]: + """Extract unique owner/repo pairs from GitHub URLs in markdown text.""" + repos = set() + for url in re.findall(r"https?://github\.com/[^\s)\]]+", text): + repo = extract_github_repo(url.split("#")[0].rstrip("/")) + if repo: + repos.add(repo) + return repos + + +def save_cache(cache: dict) -> None: + """Write the star cache to disk, creating data/ dir if needed.""" + DATA_DIR.mkdir(parents=True, exist_ok=True) + CACHE_FILE.write_text( + json.dumps(cache, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + +def build_graphql_query(repos: Sequence[str]) -> str: + """Build a GraphQL query with aliases for up to 100 repos.""" + parts = [] + for i, repo in enumerate(repos): + owner, name = repo.split("/", 1) + if not GITHUB_OWNER_RE.match(owner) or not GITHUB_NAME_RE.match(name): + continue + parts.append(f'repo_{i}: repository(owner: "{owner}", name: "{name}") {{ stargazerCount owner {{ login }} defaultBranchRef {{ target {{ ... on Commit {{ committedDate }} }} }} }}') + if not parts: + return "" + return "query { " + " ".join(parts) + " }" + + +def parse_graphql_response( + data: dict, + repos: Sequence[str], +) -> dict[str, dict]: + """Parse GraphQL response into {owner/repo: {stars, owner}} dict.""" + result = {} + for i, repo in enumerate(repos): + node = data.get(f"repo_{i}") + if node is None: + continue + default_branch = node.get("defaultBranchRef") or {} + target = default_branch.get("target") or {} + result[repo] = { + "stars": node.get("stargazerCount", 0), + "owner": node.get("owner", {}).get("login", ""), + "last_commit_at": target.get("committedDate", ""), + } + return result + + +def fetch_batch(repos: Sequence[str], client: httpx.Client) -> dict[str, dict]: + """Fetch star data for a batch of repos via GitHub GraphQL API.""" + query = build_graphql_query(repos) + if not query: + return {} + resp = client.post(GRAPHQL_URL, json={"query": query}) + resp.raise_for_status() + result = resp.json() + if "errors" in result: + for err in result["errors"]: + print(f" Warning: {err.get('message', err)}", file=sys.stderr) + data = result.get("data", {}) + return parse_graphql_response(data, repos) + + +def main() -> None: + """Fetch GitHub stars for all repos in README.md, updating the JSON cache.""" + token = os.environ.get("GITHUB_TOKEN", "") + if not token: + print("Error: GITHUB_TOKEN environment variable is required.", file=sys.stderr) + sys.exit(1) + + readme_text = README_PATH.read_text(encoding="utf-8") + current_repos = extract_github_repos(readme_text) + current_repos.add("vinta/awesome-python") + print(f"Found {len(current_repos)} GitHub repos in README.md") + + cache = load_stars(CACHE_FILE) + now = datetime.now(UTC) + + # Prune entries not in current README + pruned = {k: v for k, v in cache.items() if k in current_repos} + if len(pruned) < len(cache): + print(f"Pruned {len(cache) - len(pruned)} stale cache entries") + cache = pruned + + # Determine which repos need fetching (missing or stale) + max_age = timedelta(hours=CACHE_MAX_AGE_HOURS) + to_fetch = [] + for repo in sorted(current_repos): + entry = cache.get(repo) + if entry and "fetched_at" in entry: + fetched = datetime.fromisoformat(entry["fetched_at"]) + if now - fetched < max_age: + continue + to_fetch.append(repo) + + print(f"{len(to_fetch)} repos to fetch ({len(current_repos) - len(to_fetch)} cached)") + + if not to_fetch: + save_cache(cache) + print("Cache is up to date.") + return + + # Fetch in batches + fetched_count = 0 + skipped_repos: list[str] = [] + + now_iso = now.isoformat() + total_batches = (len(to_fetch) + BATCH_SIZE - 1) // BATCH_SIZE + + with httpx.Client( + headers={"Authorization": f"bearer {token}", "Content-Type": "application/json"}, + transport=httpx.HTTPTransport(retries=2), + timeout=30, + ) as client: + for batch_num, batch in enumerate(batched(to_fetch, BATCH_SIZE), 1): + print(f"Fetching batch {batch_num}/{total_batches} ({len(batch)} repos)...") + + try: + results = fetch_batch(batch, client) + except httpx.HTTPStatusError as e: + print(f"HTTP error {e.response.status_code}", file=sys.stderr) + if e.response.status_code == 401: + print("Error: Invalid GITHUB_TOKEN.", file=sys.stderr) + sys.exit(1) + print("Saving partial cache and exiting.", file=sys.stderr) + save_cache(cache) + sys.exit(1) + + for repo in batch: + if repo in results: + cache[repo] = {**results[repo], "fetched_at": now_iso} + fetched_count += 1 + else: + skipped_repos.append(repo) + + # Save after each batch in case of interruption + save_cache(cache) + + if skipped_repos: + print(f"Skipped {len(skipped_repos)} repos (deleted/private/renamed)") + print(f"Done. Fetched {fetched_count} repos, {len(cache)} total cached.") + + +if __name__ == "__main__": + main() diff --git a/website/fetch_pypi_downloads_via_bigquery.py b/website/fetch_pypi_downloads_via_bigquery.py new file mode 100644 index 0000000000..e8b5bbfc57 --- /dev/null +++ b/website/fetch_pypi_downloads_via_bigquery.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Cross-check last-30-day PyPI download counts for given names via BigQuery. + +Queries the canonical source ClickPy mirrors — +`bigquery-public-data.pypi.file_downloads` via the `bq` CLI — and prints +namecount TSV to stdout. Names resolve through +data/pypi_name_overrides.json, matching the cache sweep. Maintainer-local (needs a personal GCP +account) and print-only: data/pypi_downloads.tsv is written solely by +fetch_pypi_downloads_via_clickpy.py. + +Targeted cross-checks only — full-README sweeps belong to ClickPy. The +dry-run scan estimate scales with the IN list (measured 2026-08-16: +~38 GB for 2 names, ~275 GB for 53, ~1.2 TB for the full README — past +both MAX_BYTES_BILLED and the 1 TiB/month free tier), and BigQuery +enforces MAX_BYTES_BILLED against that pre-run estimate, so oversized +name lists fail before billing. Actual billed bytes come in far lower +via cluster pruning (33.7 GB for a single name). The table is +partitioned on timestamp and clustered on the top-level `project` +column — filter on `project`, never `file.project`: both hold identical +values (verified 2026-08-16, 408M rows on one day, zero mismatches), but +only a `project` filter gets cluster pruning, and the `file` record +costs ~4x more to scan. Always --dry-run first to check the estimate. + +Usage: python fetch_pypi_downloads_via_bigquery.py [--dry-run] NAME [NAME ...] +""" + +import subprocess +import sys +from json import loads + +from fetch_pypi_downloads_via_clickpy import resolve + +MAX_BYTES_BILLED = 400_000_000_000 + + +def fetch_bigquery(names: list[str], dry_run: bool) -> dict[str, int]: + in_list = ", ".join(f"'{name}'" for name in names) + query = ( + "SELECT project, COUNT(*) AS downloads " + "FROM `bigquery-public-data.pypi.file_downloads` " + "WHERE DATE(timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) " + "AND CURRENT_DATE() " + f"AND project IN ({in_list}) " + "GROUP BY project" + ) + cmd = ["bq", "query", "--use_legacy_sql=false", "--format=json", f"--maximum_bytes_billed={MAX_BYTES_BILLED}"] + if dry_run: + cmd.append("--dry_run") + cmd.append(query) + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(result.stderr, file=sys.stderr) + sys.exit(1) + if dry_run: + print(result.stdout.strip() or result.stderr.strip()) + sys.exit(0) + rows = loads(result.stdout) + return {row["project"]: int(row["downloads"]) for row in rows} + + +def main() -> None: + dry_run = "--dry-run" in sys.argv + names = set() + for arg in sys.argv[1:]: + if arg == "--dry-run": + continue + pkg = resolve(arg) + if pkg is None: + print(f"{arg}: not pip-installable per pypi_name_overrides.json, skipping", file=sys.stderr) + else: + names.add(pkg) + names = sorted(names) + if not names: + print("Usage: python fetch_pypi_downloads_via_bigquery.py [--dry-run] NAME [NAME ...]", file=sys.stderr) + sys.exit(1) + counts = fetch_bigquery(names, dry_run) + for name in names: + print(f"{name}\t{counts.get(name, 'NOT_FOUND')}") + + +if __name__ == "__main__": + main() diff --git a/website/fetch_pypi_downloads_via_clickpy.py b/website/fetch_pypi_downloads_via_clickpy.py new file mode 100644 index 0000000000..c7c979f2cf --- /dev/null +++ b/website/fetch_pypi_downloads_via_clickpy.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Fetch last-30-day PyPI download counts for all README entries via ClickPy. + +ClickPy is ClickHouse's public mirror of the PyPI download dataset +(sql-clickhouse.clickhouse.com, user `demo`, no key, no personal +account): one batched query covers the full README in under a second. +Playground limits — 60 queries/hour/IP, 1000 result rows — sit far above +a full-README sweep. The per-day table is a SummingMergeTree, so the +query must sum(count) GROUP BY, never count raw rows. Counts include +mirror/CI traffic; never mix these figures with pypistats.org, which +excludes mirrors by default. + +This script is the sole writer of data/pypi_downloads.tsv and rewrites it +from scratch each run, so entries removed from README.md drop out +naturally; names with no PyPI rows are written as NOT_FOUND. Counts are +looked up by README display name; when the display name differs from the +canonical PyPI package (a squatter or a dead predecessor would be +measured otherwise), add the mapping to the curated +data/pypi_name_overrides.json — normalized README name to +{"package": ..., "reason": ...}, where package is the real PyPI name +or null for projects that are not pip-installable so their row is +never queried, and reason records why. The file starts with a header row (name, package, +downloads, fetched_at) — package is the PyPI package the row actually +measured ("-" for null overrides) — and every row carries the sweep +date: a cache fetched within the last 7 days is current enough for +audit verdicts, so only re-run when older. Cross-checks against other +sources (fetch_pypi_downloads_via_bigquery.py, +fetch_pypi_downloads_via_pepy.py) print to stdout and never touch the +cache. + +Usage: python fetch_pypi_downloads_via_clickpy.py +""" + +import json +import re +from datetime import date +from pathlib import Path + +import httpx +from readme_parser import parse_readme + +DATA_DIR = Path(__file__).parent / "data" +OUT_FILE = DATA_DIR / "pypi_downloads.tsv" +OVERRIDES_FILE = DATA_DIR / "pypi_name_overrides.json" +README_PATH = Path(__file__).parent.parent / "README.md" +CLICKPY_URL = "https://sql-clickhouse.clickhouse.com/?user=demo" + +# PyPI normalizes names to lowercase with runs of -, _, . collapsed to -. +PYPI_NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$") + + +def normalize(name: str) -> str: + return re.sub(r"[-_.]+", "-", name.lower()) + + +def load_overrides() -> dict[str, str | None]: + """Load curated overrides: normalized README name -> PyPI package to measure (None = never query). + + Each file entry is {"package": str | null, "reason": str | null}; the reason is + documentation only — required for null packages, optional for remaps. + """ + raw = json.loads(OVERRIDES_FILE.read_text()) + return {name: entry["package"] for name, entry in raw.items()} + + +def resolve(name: str) -> str | None: + """Map a README display name to the PyPI package to measure. None = not pip-installable.""" + normalized = normalize(name) + return load_overrides().get(normalized, normalized) + + +def collect_names(readme_text: str) -> list[str]: + names = set() + for group in parse_readme(readme_text): + for section in group["categories"]: + for entry in section["entries"]: + normalized = normalize(entry["name"]) + if PYPI_NAME_RE.match(normalized): + names.add(normalized) + return sorted(names) + + +def fetch_clickpy(names: list[str]) -> dict[str, int]: + in_list = ", ".join(f"'{name}'" for name in names) + query = ( + "SELECT project, sum(count) AS downloads " + "FROM pypi.pypi_downloads_per_day " + f"WHERE project IN ({in_list}) AND date >= today() - 30 " + "GROUP BY project FORMAT JSON" + ) + resp = httpx.post(CLICKPY_URL, content=query, timeout=60) + resp.raise_for_status() + return {row["project"]: int(row["downloads"]) for row in resp.json()["data"]} + + +def main() -> None: + names = collect_names(README_PATH.read_text()) + overrides = load_overrides() + packages = {name: overrides.get(name, name) for name in names} + query_names = sorted({pkg for pkg in packages.values() if pkg}) + print(f"Querying {len(query_names)} package names...") + counts = fetch_clickpy(query_names) + fetched_at = date.today().isoformat() + rows = "\n".join(f"{name}\t{pkg or '-'}\t{counts.get(pkg, 'NOT_FOUND') if pkg else 'NOT_FOUND'}\t{fetched_at}" for name, pkg in packages.items()) + OUT_FILE.write_text(f"name\tpackage\tdownloads\tfetched_at\n{rows}\n") + print(f"Done. {len(counts)}/{len(query_names)} names found on PyPI. Cached to {OUT_FILE}") + + +if __name__ == "__main__": + main() diff --git a/website/fetch_pypi_downloads_via_pepy.py b/website/fetch_pypi_downloads_via_pepy.py new file mode 100644 index 0000000000..55eb75938e --- /dev/null +++ b/website/fetch_pypi_downloads_via_pepy.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Spot-check last-30-day PyPI download counts via the pepy.tech API. + +Cross-check for a handful of packages during audits — not for full-README +sweeps: the free API key is throttled to 5 requests/minute (10 burst), so +this script sleeps 12s between requests and 530 names would take ~2 hours +(use fetch_pypi_downloads_via_clickpy.py for bulk). Reads PEPY_TECH_API_KEY from the +environment, falling back to the repo-root .env. The v2 endpoint returns +~90 days of per-day per-version counts; this script sums the most recent +30 days present in the response across all versions. Names resolve +through data/pypi_name_overrides.json, matching the cache sweep. pepy counts include +mirror/CI traffic (CI filtering is a paid pepy feature), matching the +ClickPy/BigQuery figures; pypistats.org excludes mirrors, so never mix +the two in one comparison. Results print to stdout as TSV and are not +cached — data/pypi_downloads.tsv stays single-source. + +Usage: python fetch_pypi_downloads_via_pepy.py NAME [NAME ...] +""" + +import os +import sys +import time +from pathlib import Path + +import httpx +from fetch_pypi_downloads_via_clickpy import resolve + +ENV_FILE = Path(__file__).parent.parent / ".env" +PEPY_URL = "https://api.pepy.tech/api/v2/projects/{name}" +SECONDS_BETWEEN_REQUESTS = 12 + + +def load_api_key() -> str: + key = os.environ.get("PEPY_TECH_API_KEY", "") + if not key and ENV_FILE.exists(): + for line in ENV_FILE.read_text().splitlines(): + name, sep, value = line.partition("=") + if sep and name.strip() == "PEPY_TECH_API_KEY": + key = value.strip() + if not key: + print("Error: PEPY_TECH_API_KEY not set (environment or repo-root .env).", file=sys.stderr) + sys.exit(1) + return key + + +def last_30_day_total(downloads_per_day: dict[str, dict[str, int]]) -> int: + recent_days = sorted(downloads_per_day)[-30:] + return sum(sum(downloads_per_day[day].values()) for day in recent_days) + + +def main() -> None: + if len(sys.argv) < 2: + print("Usage: python fetch_pypi_downloads_via_pepy.py NAME [NAME ...]", file=sys.stderr) + sys.exit(1) + names = [] + for arg in sys.argv[1:]: + pkg = resolve(arg) + if pkg is None: + print(f"{arg}: not pip-installable per pypi_name_overrides.json, skipping", file=sys.stderr) + else: + names.append(pkg) + with httpx.Client(headers={"X-API-Key": load_api_key()}, timeout=30) as client: + for i, name in enumerate(names): + if i: + time.sleep(SECONDS_BETWEEN_REQUESTS) + resp = client.get(PEPY_URL.format(name=name)) + if resp.status_code == 404: + print(f"{name}\tNOT_FOUND") + continue + resp.raise_for_status() + print(f"{name}\t{last_30_day_total(resp.json()['downloads'])}") + + +if __name__ == "__main__": + main() diff --git a/website/readme_parser.py b/website/readme_parser.py new file mode 100644 index 0000000000..84887d0f77 --- /dev/null +++ b/website/readme_parser.py @@ -0,0 +1,471 @@ +"""Parse README.md into structured section data using markdown-it-py AST. + +Empirically verified behavior worth knowing before editing README.md: +everything above `## Projects` is ignored; new subcategories need no +parser change (a bullet without a leading link plus indented entries); +a standalone all-bold paragraph inside the Projects region becomes a +Thematic Group marker; prose placed right after `## Categories` leaks +into llms.txt; the build's "Total entries" figure counts sub-items +(indented links under an entry), not just entries. +""" + +from __future__ import annotations + +import re +from typing import TypedDict + +from markdown_it import MarkdownIt +from markdown_it.tree import SyntaxTreeNode +from markupsafe import escape + + +class AlsoSee(TypedDict): + name: str + url: str + + +class ParsedEntry(TypedDict): + name: str + url: str + description: str # inline HTML, properly escaped + also_see: list[AlsoSee] + subcategory: str # sub-category label, empty if none + + +class ParsedSection(TypedDict): + name: str + slug: str + description: str # plain text, links resolved to text + description_html: str # inline HTML, properly escaped + entries: list[ParsedEntry] + entry_count: int + + +class ParsedGroup(TypedDict): + name: str + slug: str + categories: list[ParsedSection] + + +class ParsedSponsor(TypedDict): + name: str + url: str + description: str # inline HTML, properly escaped + + +# --- Slugify ---------------------------------------------------------------- + +_SLUG_NON_ALNUM_RE = re.compile(r"[^a-z0-9\s-]") +_SLUG_WHITESPACE_RE = re.compile(r"[\s]+") +_SLUG_MULTI_DASH_RE = re.compile(r"-+") + + +def slugify(name: str) -> str: + """Convert a category name to a URL-friendly slug.""" + slug = name.lower() + slug = _SLUG_NON_ALNUM_RE.sub("", slug) + slug = _SLUG_WHITESPACE_RE.sub("-", slug.strip()) + slug = _SLUG_MULTI_DASH_RE.sub("-", slug) + return slug + + +# --- Inline renderers ------------------------------------------------------- + + +def _render_inline(children: list[SyntaxTreeNode], *, html: bool) -> str: + """Render inline AST nodes to HTML or plain text.""" + parts: list[str] = [] + for child in children: + match child.type: + case "text": + parts.append(str(escape(child.content)) if html else child.content) + case "html_inline": + if html: + parts.append(str(escape(child.content))) + case "softbreak": + parts.append(" ") + case "code_inline": + parts.append(f"{escape(child.content)}" if html else child.content) + case "link": + inner = _render_inline(child.children, html=html) + if html: + href = str(escape(_href(child))) + parts.append(f'{inner}') + else: + parts.append(inner) + case "em": + inner = _render_inline(child.children, html=html) + parts.append(f"{inner}" if html else inner) + case "strong": + inner = _render_inline(child.children, html=html) + parts.append(f"{inner}" if html else inner) + return "".join(parts) + + +def render_inline_html(children: list[SyntaxTreeNode]) -> str: + """Render inline AST nodes to HTML with proper escaping.""" + return _render_inline(children, html=True) + + +def render_inline_text(children: list[SyntaxTreeNode]) -> str: + """Render inline AST nodes to plain text (links become their text).""" + return _render_inline(children, html=False) + + +# --- AST helpers ------------------------------------------------------------- + + +def _heading_text(node: SyntaxTreeNode) -> str: + """Extract plain text from a heading node.""" + for child in node.children: + if child.type == "inline": + return render_inline_text(child.children) + return "" + + +def _heading_level(node: SyntaxTreeNode) -> int | None: + """Return the numeric level for a heading node.""" + if node.type != "heading" or not node.tag.startswith("h"): + return None + return int(node.tag[1:]) + + +def _extract_description_children(nodes: list[SyntaxTreeNode]) -> list[SyntaxTreeNode]: + """Extract description children from the first paragraph if it's a single block. + + Pattern: _Libraries for foo._ -> "Libraries for foo." + """ + if not nodes: + return [] + first = nodes[0] + if first.type != "paragraph": + return [] + for child in first.children: + if child.type == "inline" and len(child.children) == 1: + em = child.children[0] + if em.type == "em": + return em.children + return [] + + +# --- Entry extraction -------------------------------------------------------- + +_DESC_SEP_RE = re.compile(r"^\s*[-\u2013\u2014]\s*") +_SUBCAT_TRAILING_RE = re.compile(r"[\s,\-\u2013\u2014]+(also\s+see\s*)?$", re.IGNORECASE) + + +def _find_child(node: SyntaxTreeNode, child_type: str) -> SyntaxTreeNode | None: + """Find first direct child of a given type.""" + for child in node.children: + if child.type == child_type: + return child + return None + + +def _href(link: SyntaxTreeNode) -> str: + """Return the link's href attribute as a string, or '' if missing.""" + href = link.attrGet("href") + return href if isinstance(href, str) else "" + + +def _find_inline(node: SyntaxTreeNode) -> SyntaxTreeNode | None: + """Find the inline node in a list_item's paragraph.""" + para = _find_child(node, "paragraph") + if para is None: + return None + return _find_child(para, "inline") + + +def _extract_description_html(inline: SyntaxTreeNode, first_link: SyntaxTreeNode) -> str: + """Extract description HTML from inline content after the first link. + + AST: [link("name"), text(" - Description.")] -> "Description." + The separator (- / en-dash / em-dash) is stripped. + """ + link_idx = next((i for i, c in enumerate(inline.children) if c is first_link), None) + if link_idx is None: + return "" + desc_children = inline.children[link_idx + 1 :] + if not desc_children: + return "" + html = render_inline_html(desc_children) + return _DESC_SEP_RE.sub("", html) + + +def _parse_list_entries( + bullet_list: SyntaxTreeNode, + *, + subcategory: str = "", +) -> list[ParsedEntry]: + """Extract entries from a bullet_list AST node. + + Handles three patterns: + - Text-only list_item -> subcategory label -> recurse into nested list + - Link list_item with nested link-only items -> entry with also_see + - Link list_item without nesting -> simple entry + """ + entries: list[ParsedEntry] = [] + + for list_item in bullet_list.children: + if list_item.type != "list_item": + continue + + inline = _find_inline(list_item) + if inline is None: + continue + + first_link = _find_child(inline, "link") + + if first_link is None or inline.children[0] is not first_link: + # Subcategory label: take text before the first link, strip trailing separators + pre_link = [] + for child in inline.children: + if child.type == "link": + break + pre_link.append(child) + label = _SUBCAT_TRAILING_RE.sub("", render_inline_text(pre_link)) if pre_link else render_inline_text(inline.children) + nested = _find_child(list_item, "bullet_list") + if nested: + entries.extend(_parse_list_entries(nested, subcategory=label)) + continue + + # Entry with a link + name = render_inline_text(first_link.children) + url = _href(first_link) + desc_html = _extract_description_html(inline, first_link) + + # Collect also_see from nested bullet_list + also_see: list[AlsoSee] = [] + nested = _find_child(list_item, "bullet_list") + if nested: + for sub_item in nested.children: + if sub_item.type != "list_item": + continue + sub_inline = _find_inline(sub_item) + if sub_inline: + sub_link = _find_child(sub_inline, "link") + if sub_link: + also_see.append( + AlsoSee( + name=render_inline_text(sub_link.children), + url=_href(sub_link), + ) + ) + + entries.append( + ParsedEntry( + name=name, + url=url, + description=desc_html, + also_see=also_see, + subcategory=subcategory, + ) + ) + + return entries + + +def _parse_section_entries(content_nodes: list[SyntaxTreeNode]) -> list[ParsedEntry]: + """Extract all entries from a section's content nodes.""" + entries: list[ParsedEntry] = [] + for node in content_nodes: + if node.type == "bullet_list": + entries.extend(_parse_list_entries(node)) + return entries + + +# --- Section splitting ------------------------------------------------------- + + +def _build_section(name: str, body: list[SyntaxTreeNode]) -> ParsedSection: + """Build a ParsedSection from a heading name and its body nodes.""" + desc_children = _extract_description_children(body) + desc = render_inline_text(desc_children) if desc_children else "" + desc_html = render_inline_html(desc_children) if desc_children else "" + content_nodes = body[1:] if desc_children else body + entries = _parse_section_entries(content_nodes) + entry_count = len(entries) + sum(len(e["also_see"]) for e in entries) + return ParsedSection( + name=name, + slug=slugify(name), + description=desc, + description_html=desc_html, + entries=entries, + entry_count=entry_count, + ) + + +def _is_bold_marker(node: SyntaxTreeNode) -> str | None: + """Detect a bold-only paragraph used as a group marker. + + Pattern: a paragraph whose only content is **Group Name** (possibly + surrounded by empty text nodes in the AST). + Returns the group name text, or None if not a group marker. + """ + if node.type != "paragraph": + return None + for child in node.children: + if child.type != "inline": + continue + # Filter out empty text nodes that markdown-it inserts around strong + meaningful = [c for c in child.children if not (c.type == "text" and c.content == "")] + if len(meaningful) == 1 and meaningful[0].type == "strong": + return render_inline_text(meaningful[0].children) + return None + + +def _parse_grouped_sections( + nodes: list[SyntaxTreeNode], +) -> list[ParsedGroup]: + """Parse nodes into groups of categories using bold markers as group boundaries. + + Bold-only paragraphs (**Group Name**) delimit groups. H3 headings under each + bold marker become categories within that group. Categories appearing before + any bold marker go into an "Other" group. + """ + groups: list[ParsedGroup] = [] + current_group_name: str | None = None + current_group_cats: list[ParsedSection] = [] + current_cat_name: str | None = None + current_cat_body: list[SyntaxTreeNode] = [] + + def flush_cat() -> None: + nonlocal current_cat_name + if current_cat_name is None: + return + current_group_cats.append(_build_section(current_cat_name, current_cat_body)) + current_cat_name = None + + def flush_group() -> None: + nonlocal current_group_name, current_group_cats + if current_group_cats: + name = current_group_name or "Other" + groups.append( + ParsedGroup( + name=name, + slug=slugify(name), + categories=list(current_group_cats), + ) + ) + current_group_name = None + current_group_cats = [] + + for node in nodes: + bold_name = _is_bold_marker(node) + if bold_name is not None: + flush_cat() + flush_group() + current_group_name = bold_name + current_cat_body = [] + elif node.type == "heading" and node.tag in ("h2", "h3"): + flush_cat() + current_cat_name = _heading_text(node) + current_cat_body = [] + elif current_cat_name is not None: + current_cat_body.append(node) + + flush_cat() + flush_group() + return groups + + +_SPONSOR_SEP_RE = re.compile(r"^\s*[:\-\u2013\u2014]\s*") + + +def _find_link_deep(node: SyntaxTreeNode) -> SyntaxTreeNode | None: + """Find the first link anywhere in the subtree (including nested in strong/em).""" + for child in node.children: + if child.type == "link": + return child + found = _find_link_deep(child) + if found: + return found + return None + + +def _parse_sponsor_item(inline: SyntaxTreeNode) -> ParsedSponsor | None: + """Parse `**[name](url)**: description` (or `[name](url) - description`).""" + for split_idx, child in enumerate(inline.children): + link = child if child.type == "link" else _find_link_deep(child) + if link is None: + continue + desc_html = render_inline_html(inline.children[split_idx + 1 :]) + return ParsedSponsor( + name=render_inline_text(link.children), + url=_href(link), + description=_SPONSOR_SEP_RE.sub("", desc_html), + ) + return None + + +def parse_sponsors(text: str) -> list[ParsedSponsor]: + """Parse the `Sponsors` section of README.md into a list of sponsors. + + Expects bullets in the form `**[name](url)**: description`. + Returns [] if no Sponsors section exists. + """ + md = MarkdownIt("commonmark") + tokens = md.parse(text) + root = SyntaxTreeNode(tokens) + children = root.children + + start_idx = None + end_idx = len(children) + start_level = None + for i, node in enumerate(children): + level = _heading_level(node) + if level is None: + continue + title = _heading_text(node).strip().lower() + if start_idx is None and title == "sponsors": + start_idx = i + 1 + start_level = level + elif start_idx is not None and start_level is not None and level <= start_level: + end_idx = i + break + if start_idx is None: + return [] + + sponsors: list[ParsedSponsor] = [] + for node in children[start_idx:end_idx]: + if node.type != "bullet_list": + continue + for list_item in node.children: + if list_item.type != "list_item": + continue + inline = _find_inline(list_item) + if inline is None: + continue + sponsor = _parse_sponsor_item(inline) + if sponsor: + sponsors.append(sponsor) + return sponsors + + +def parse_readme(text: str) -> list[ParsedGroup]: + """Parse README.md text into grouped categories. + + Returns a list of ParsedGroup dicts containing nested categories. + Content between the Projects heading and Resources or Contributing is parsed + as categories grouped by bold markers (**Group Name**). + """ + md = MarkdownIt("commonmark") + tokens = md.parse(text) + root = SyntaxTreeNode(tokens) + children = root.children + + # Find Projects and section boundaries in one pass. + projects_idx = None + cat_end_idx = None + for i, node in enumerate(children): + if _heading_level(node) in (1, 2): + text_content = _heading_text(node) + if projects_idx is None and text_content == "Projects": + projects_idx = i + elif cat_end_idx is None and text_content in ("Resources", "Contributing"): + cat_end_idx = i + if projects_idx is None: + return [] + + cat_nodes = children[projects_idx + 1 : cat_end_idx or len(children)] + return _parse_grouped_sections(cat_nodes) diff --git a/website/static/favicon.svg b/website/static/favicon.svg new file mode 100644 index 0000000000..660a7c6f31 --- /dev/null +++ b/website/static/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/static/main.js b/website/static/main.js new file mode 100644 index 0000000000..11d438613e --- /dev/null +++ b/website/static/main.js @@ -0,0 +1,502 @@ +const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + +function getScrollBehavior() { + return reducedMotion.matches ? "auto" : "smooth"; +} + +let activeFilter = null; +let activeSort = { col: "downloads", order: "desc" }; +const searchInput = document.querySelector(".search"); +const filterBar = document.querySelector(".filter-bar"); +const filterValue = document.querySelector(".filter-value"); +const filterClear = document.querySelector(".filter-clear"); +const noResults = document.querySelector(".no-results"); +const rows = document.querySelectorAll(".table tbody tr.row"); +const tags = document.querySelectorAll(".tag"); +const tbody = document.querySelector(".table tbody"); + +function initRevealSections() { + const sections = document.querySelectorAll("[data-reveal]"); + if (!sections.length) return; + + if (!("IntersectionObserver" in window)) { + sections.forEach(function (section) { + section.classList.add("is-visible"); + }); + return; + } + + const observer = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (!entry.isIntersecting) return; + entry.target.classList.add("is-visible"); + observer.unobserve(entry.target); + }); + }, + { + threshold: 0.12, + rootMargin: "0px 0px -8% 0px", + }, + ); + + sections.forEach(function (section, index) { + section.classList.add("will-reveal"); + section.style.transitionDelay = Math.min(index * 70, 180) + "ms"; + observer.observe(section); + }); +} + +initRevealSections(); + +// Smooth scroll without hash in URL +document.querySelectorAll("[data-scroll-to]").forEach(function (link) { + link.addEventListener("click", function (e) { + const el = document.getElementById(link.dataset.scrollTo); + if (!el) return; + e.preventDefault(); + el.scrollIntoView({ behavior: getScrollBehavior() }); + }); +}); + +// Land at #library-index without leaving the hash in the URL +if (window.location.hash === "#library-index") { + const target = document.getElementById("library-index"); + if (target) { + target.scrollIntoView(); + } + history.replaceState( + null, + "", + window.location.pathname + window.location.search, + ); +} + +// Pause hero animations when scrolled out of view +(function () { + const hero = document.querySelector(".hero"); + if (!hero || !("IntersectionObserver" in window)) return; + const observer = new IntersectionObserver(function (entries) { + hero.classList.toggle("offscreen", !entries[0].isIntersecting); + }); + observer.observe(hero); +})(); + +function relativeTime(isoStr) { + const date = new Date(isoStr); + const now = new Date(); + const diffMs = now - date; + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + if (diffHours < 1) return "just now"; + if (diffHours < 24) + return diffHours === 1 ? "1 hour ago" : diffHours + " hours ago"; + if (diffDays === 1) return "yesterday"; + if (diffDays < 30) return diffDays + " days ago"; + const diffMonths = Math.floor(diffDays / 30); + if (diffMonths < 12) + return diffMonths === 1 ? "1 month ago" : diffMonths + " months ago"; + const diffYears = Math.floor(diffDays / 365); + return diffYears === 1 ? "1 year ago" : diffYears + " years ago"; +} + +document.querySelectorAll(".col-commit[data-commit]").forEach(function (td) { + const time = td.querySelector("time"); + if (time) time.textContent = relativeTime(td.dataset.commit); +}); + +document + .querySelectorAll(".expand-commit time[datetime]") + .forEach(function (time) { + time.textContent = relativeTime(time.getAttribute("datetime")); + }); + +rows.forEach(function (row, i) { + row._origIndex = i; + let next = row.nextElementSibling; + if (next && next.classList.contains("desc-row")) { + row._descRow = next; + next = next.nextElementSibling; + } + row._expandRow = next; +}); + +function collapseAll() { + if (!tbody) return; + const openRows = tbody.querySelectorAll("tr.row.open"); + openRows.forEach(function (row) { + row.classList.remove("open"); + row.setAttribute("aria-expanded", "false"); + }); +} + +function applyFilters() { + const query = searchInput ? searchInput.value.toLowerCase().trim() : ""; + const descRowsVisible = !isIndexDocument || activeFilter !== null; + let visibleCount = 0; + + collapseAll(); + + rows.forEach(function (row) { + let show = true; + + if (activeFilter) { + const rowTags = row.dataset.tags; + show = rowTags ? rowTags.split("||").includes(activeFilter) : false; + } + + if (show && query) { + if (!row._searchText) { + let text = row.textContent.toLowerCase(); + if (row._descRow) { + text += " " + row._descRow.textContent.toLowerCase(); + } + if (row._expandRow) { + text += " " + row._expandRow.textContent.toLowerCase(); + } + row._searchText = text; + } + show = row._searchText.includes(query); + } + + if (row.hidden !== !show) row.hidden = !show; + if (row._descRow) { + const descHidden = !show || !descRowsVisible; + if (row._descRow.hidden !== descHidden) { + row._descRow.hidden = descHidden; + } + } + + if (show) { + visibleCount++; + const numCell = row.cells[0]; + if (numCell.textContent !== String(visibleCount)) { + numCell.textContent = String(visibleCount); + } + } + }); + + if (noResults) noResults.hidden = visibleCount > 0; + + tags.forEach(function (tag) { + tag.classList.toggle("active", activeFilter === tag.dataset.value); + }); + + if (filterBar) { + if (activeFilter) { + filterBar.classList.add("visible"); + if (filterValue) filterValue.textContent = activeFilter; + } else { + filterBar.classList.remove("visible"); + } + } + + updateURL(); +} + +const filterUrlsScript = document.getElementById("filter-urls"); +const filterToUrl = filterUrlsScript + ? JSON.parse(filterUrlsScript.textContent) + : {}; + +const isIndexDocument = + location.pathname === "/" || location.pathname === "/index.html"; + +const urlToFilter = {}; +Object.keys(filterToUrl).forEach(function (k) { + urlToFilter[filterToUrl[k]] = k; +}); + +function buildQueryString() { + const params = new URLSearchParams(); + const query = searchInput ? searchInput.value.trim() : ""; + if (query) params.set("q", query); + if (activeSort.col !== "downloads" || activeSort.order !== "desc") { + params.set("sort", activeSort.col); + params.set("order", activeSort.order); + } + const qs = params.toString(); + return qs ? "?" + qs : ""; +} + +function updateURL() { + if (!isIndexDocument) return; + const path = + activeFilter && filterToUrl[activeFilter] ? filterToUrl[activeFilter] : "/"; + history.replaceState(null, "", path + buildQueryString()); +} + +function getSortValue(row, col) { + if (col === "name") { + return row.querySelector(".col-name a").textContent.trim().toLowerCase(); + } + if (col === "stars") { + const text = row + .querySelector(".col-stars") + .textContent.trim() + .replace(/,/g, ""); + const num = parseInt(text, 10); + return isNaN(num) ? -1 : num; + } + if (col === "downloads") { + const text = row + .querySelector(".col-downloads") + .textContent.trim() + .replace(/,/g, ""); + const num = parseInt(text, 10); + return isNaN(num) ? -1 : num; + } + if (col === "commit-time") { + const attr = row.querySelector(".col-commit").getAttribute("data-commit"); + return attr ? new Date(attr).getTime() : 0; + } + return 0; +} + +function sortRows() { + if (!tbody) return; + + const arr = Array.prototype.slice.call(rows); + const col = activeSort.col; + const order = activeSort.order; + + // Cache sort values once to avoid DOM queries per comparison + arr.forEach(function (row) { + row._sortVal = getSortValue(row, col); + }); + + arr.sort(function (a, b) { + const aVal = a._sortVal; + const bVal = b._sortVal; + if (col === "name") { + const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; + if (cmp === 0) return a._origIndex - b._origIndex; + return order === "desc" ? -cmp : cmp; + } + if (aVal <= 0 && bVal <= 0) return a._origIndex - b._origIndex; + if (aVal <= 0) return 1; + if (bVal <= 0) return -1; + const cmp = aVal - bVal; + if (cmp === 0) return a._origIndex - b._origIndex; + return order === "desc" ? -cmp : cmp; + }); + + const frag = document.createDocumentFragment(); + arr.forEach(function (row) { + frag.appendChild(row); + if (row._descRow) frag.appendChild(row._descRow); + if (row._expandRow) frag.appendChild(row._expandRow); + }); + tbody.appendChild(frag); + applyFilters(); +} + +const sortHeaders = document.querySelectorAll("th[data-sort]"); + +function updateSortIndicators() { + sortHeaders.forEach(function (th) { + th.classList.remove("sort-asc", "sort-desc"); + if (th.dataset.sort === activeSort.col) { + th.classList.add("sort-" + activeSort.order); + th.setAttribute( + "aria-sort", + activeSort.order === "asc" ? "ascending" : "descending", + ); + } else { + th.removeAttribute("aria-sort"); + } + }); +} + +// Expand/collapse: event delegation on tbody +if (tbody) { + tbody.addEventListener("click", function (e) { + // Don't toggle if clicking a link or tag button + if (e.target.closest("a") || e.target.closest(".tag")) return; + + let row = e.target.closest("tr.row"); + if (!row) { + const descRow = e.target.closest("tr.desc-row"); + if (descRow) row = descRow.previousElementSibling; + } + if (!row) return; + + const isOpen = row.classList.contains("open"); + if (isOpen) { + row.classList.remove("open"); + row.setAttribute("aria-expanded", "false"); + } else { + row.classList.add("open"); + row.setAttribute("aria-expanded", "true"); + } + }); + + // Keyboard: Enter or Space on focused .row toggles expand + tbody.addEventListener("keydown", function (e) { + if (e.key !== "Enter" && e.key !== " ") return; + const row = e.target.closest("tr.row"); + if (!row) return; + e.preventDefault(); + row.click(); + }); +} + +tags.forEach(function (tag) { + tag.addEventListener("click", function (e) { + e.preventDefault(); + const value = tag.dataset.value; + const url = tag.dataset.url; + if (isIndexDocument) { + activeFilter = activeFilter === value ? null : value; + if (activeFilter && url) { + history.pushState(null, "", url + buildQueryString()); + } else { + history.pushState(null, "", "/" + buildQueryString()); + } + applyFilters(); + } else if (url) { + window.location.href = url + "#library-index"; + } + }); +}); + +if (filterClear) { + filterClear.addEventListener("click", function () { + if (!isIndexDocument) { + window.location.href = "/#library-index"; + return; + } + activeFilter = null; + applyFilters(); + }); +} + +const noResultsClear = document.querySelector(".no-results-clear"); +if (noResultsClear) { + noResultsClear.addEventListener("click", function () { + if (!isIndexDocument) { + window.location.href = "/"; + return; + } + if (searchInput) searchInput.value = ""; + activeFilter = null; + applyFilters(); + }); +} + +sortHeaders.forEach(function (th) { + th.addEventListener("click", function () { + const col = th.dataset.sort; + const defaultOrder = col === "name" ? "asc" : "desc"; + const altOrder = defaultOrder === "asc" ? "desc" : "asc"; + if (activeSort.col === col) { + if (activeSort.order === defaultOrder) + activeSort = { col: col, order: altOrder }; + else activeSort = { col: "downloads", order: "desc" }; + } else { + activeSort = { col: col, order: defaultOrder }; + } + sortRows(); + updateSortIndicators(); + }); +}); + +if (searchInput) { + let searchTimer; + searchInput.addEventListener("input", function () { + clearTimeout(searchTimer); + searchTimer = setTimeout(applyFilters, 150); + }); + + document.addEventListener("keydown", function (e) { + if ( + e.key === "/" && + !["INPUT", "TEXTAREA", "SELECT"].includes( + document.activeElement.tagName, + ) && + !e.ctrlKey && + !e.metaKey + ) { + e.preventDefault(); + searchInput.focus(); + } + if (e.key === "Escape" && document.activeElement === searchInput) { + searchInput.value = ""; + activeFilter = null; + applyFilters(); + searchInput.blur(); + } + }); +} + +const backToTop = document.querySelector(".back-to-top"); +const resultsSection = document.querySelector("#library-index"); +const tableWrap = document.querySelector(".table-wrap"); +const stickyHeaderCell = backToTop ? backToTop.closest("th") : null; + +function updateBackToTopVisibility() { + if (!backToTop || !tableWrap || !stickyHeaderCell) return; + + const tableRect = tableWrap.getBoundingClientRect(); + const headRect = stickyHeaderCell.getBoundingClientRect(); + const hasPassedHeader = tableRect.top <= 0 && headRect.bottom > 0; + + backToTop.classList.toggle("visible", hasPassedHeader); +} + +if (backToTop) { + let scrollTicking = false; + window.addEventListener("scroll", function () { + if (!scrollTicking) { + requestAnimationFrame(function () { + updateBackToTopVisibility(); + scrollTicking = false; + }); + scrollTicking = true; + } + }); + + window.addEventListener("resize", updateBackToTopVisibility); + + backToTop.addEventListener("click", function () { + const target = searchInput || resultsSection; + if (!target) return; + target.scrollIntoView({ behavior: getScrollBehavior(), block: "center" }); + if (searchInput) searchInput.focus(); + }); + + updateBackToTopVisibility(); +} + +(function () { + const params = new URLSearchParams(location.search); + const q = params.get("q"); + const sort = params.get("sort"); + const order = params.get("order"); + if (q && searchInput) searchInput.value = q; + if ( + (sort === "name" || + sort === "stars" || + sort === "downloads" || + sort === "commit-time") && + (order === "desc" || order === "asc") + ) { + activeSort = { col: sort, order: order }; + } + const matched = urlToFilter[location.pathname]; + if (matched) activeFilter = matched; + if (q || activeFilter || sort) { + sortRows(); + } + if (activeFilter) { + applyFilters(); + } + updateSortIndicators(); +})(); + +window.addEventListener("popstate", function () { + if (!isIndexDocument) return; + const matched = urlToFilter[location.pathname]; + activeFilter = matched || null; + applyFilters(); +}); diff --git a/website/static/og-image.png b/website/static/og-image.png new file mode 100644 index 0000000000..954b5157da Binary files /dev/null and b/website/static/og-image.png differ diff --git a/website/static/og-image.svg b/website/static/og-image.svg new file mode 100644 index 0000000000..56492860ba --- /dev/null +++ b/website/static/og-image.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The definitive list that answers "I want to do X in Python, which tool should I use?" + + + Awesome Python + + + An opinionated guide to the best Python frameworks, libraries, and tools. + + + awesome-python.com + diff --git a/website/static/style.css b/website/static/style.css new file mode 100644 index 0000000000..2313c0104e --- /dev/null +++ b/website/static/style.css @@ -0,0 +1,1766 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + color-scheme: light; + --font-display: "Cormorant Garamond", Georgia, serif; + --font-body: "Manrope", "Avenir Next", "Segoe UI", sans-serif; + + --shell-max: 84rem; + --shell-pad: clamp(1.25rem, 3vw, 2.5rem); + + --bg-page: oklch(96.8% 0.018 80); + --bg-page-top: oklch(95.2% 0.018 78); + --bg-page-end: oklch(98.4% 0.01 80); + --bg-paper: oklch(98.6% 0.01 80); + --bg-paper-strong: oklch(95.7% 0.016 76); + --ink: oklch(22% 0.02 55); + --ink-soft: oklch(38% 0.018 55); + --ink-muted: oklch(52% 0.02 55); + --line: oklch(83% 0.02 70); + --line-strong: oklch(64% 0.035 62); + --accent: oklch(58% 0.16 45); + --accent-deep: oklch(44% 0.15 42); + --accent-soft: oklch(92% 0.045 55); + --accent-underline: oklch(58% 0.16 45 / 0.4); + --highlight: oklch(87% 0.08 78); + + --hero-ink: oklch(15% 0.02 40); + --hero-text: oklch(97% 0.012 85); + --hero-muted: oklch(88% 0.02 82); + --hero-line: oklch(100% 0 0 / 0.16); + --hero-kicker: oklch(82% 0.04 72); + --hero-proof: oklch(75% 0.02 72); + --hero-bg-start: oklch(14% 0.03 32); + --hero-bg-mid: oklch(19% 0.035 35); + --hero-bg-end: oklch(28% 0.05 42); + --hero-btn-start: oklch(83% 0.08 72); + --hero-btn-end: oklch(73% 0.14 58); + + --row-hover: oklch(96.2% 0.02 76); + --row-focus: oklch(95.7% 0.026 68); + --row-open-start: oklch(96.2% 0.03 76); + --row-open-end: oklch(95.4% 0.026 74); + --thead-bg: oklch(98.2% 0.012 80 / 0.97); + + --search-inset: oklch(100% 0 0 / 0.75); + --search-shadow: oklch(18% 0.03 45 / 0.24); + --search-focus-ring: oklch(61% 0.14 48 / 0.45); + --search-focus-shadow: oklch(34% 0.08 42 / 0.28); + + --tag-hover-border: oklch(71% 0.09 62 / 0.45); + --tag-active-start: oklch(82% 0.08 75); + --tag-active-end: oklch(74% 0.11 58); + + --cta-bg: oklch(94% 0.025 72); + + --text-xs: 0.8rem; + --text-sm: 0.95rem; + --text-base: 1rem; + --text-lg: 1.125rem; + + --footer-bg: oklch(16% 0.025 35); + --footer-text: oklch(72% 0.02 75); + --footer-link: oklch(82% 0.02 75); + --footer-link-hover: oklch(95% 0.01 80); + + --footer-sep: oklch(55% 0.02 55); +} + +html { + font-size: 100%; + scroll-behavior: smooth; +} + +body { + min-height: 100vh; + display: flex; + flex-direction: column; + font-family: var(--font-body); + line-height: 1.6; + color: var(--ink); + background: + radial-gradient(circle at top left, oklch(100% 0 0 / 0.72), transparent 28rem), + linear-gradient(180deg, var(--bg-page-top), var(--bg-page) 24rem, var(--bg-page-end)); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +main { + display: flex; + flex-direction: column; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input { + font: inherit; +} + +img, +svg { + display: block; + max-width: 100%; +} + +kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.8em; + padding: 0.08rem 0.38rem; + border: 1px solid var(--line); + border-bottom-width: 2px; + border-radius: 999px; + background: var(--bg-paper); + font-size: 0.85em; + line-height: 1.2; +} + +.section-shell { + width: min(100%, calc(var(--shell-max) + (var(--shell-pad) * 2))); + margin: 0 auto; + padding-inline: var(--shell-pad); +} + +.skip-link { + position: absolute; + left: -9999px; + top: 0; + z-index: 300; + padding: 0.75rem 1rem; + color: var(--hero-text); + background: var(--hero-ink); + font-size: var(--text-sm); + font-weight: 700; +} + +.skip-link:focus { + left: 1rem; + top: 1rem; +} + +.hero { + position: relative; + width: 100%; + min-height: 100svh; + overflow: clip; + background: + radial-gradient(circle at 18% 18%, oklch(55% 0.14 45 / 0.34), transparent 22rem), + radial-gradient(circle at 78% 32%, oklch(62% 0.17 70 / 0.17), transparent 24rem), + linear-gradient(140deg, var(--hero-bg-start) 0%, var(--hero-bg-mid) 52%, var(--hero-bg-end) 100%); + color: var(--hero-text); +} + +.hero::before { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(90deg, oklch(100% 0 0 / 0.03) 1px, transparent 1px), + linear-gradient(oklch(100% 0 0 / 0.03) 1px, transparent 1px); + background-size: 7rem 7rem; + mask-image: linear-gradient(180deg, oklch(0% 0 0 / 0.72), transparent 88%); + pointer-events: none; +} + +.hero-sheen, +.hero-noise { + position: absolute; + inset: 0; + pointer-events: none; +} + +.hero-sheen { + background: + linear-gradient(118deg, transparent 35%, oklch(100% 0 0 / 0.09) 49%, transparent 63%); + transform: translateX(-30%); + animation: sheen-drift 18s linear 3; +} + +.hero.offscreen .hero-sheen, +.hero.offscreen .hero-noise { + animation-play-state: paused; +} + +.hero-noise { + opacity: 0.1; + background-image: + radial-gradient(circle at 20% 20%, oklch(100% 0 0 / 0.65) 0.02rem, transparent 0.04rem), + radial-gradient(circle at 80% 30%, oklch(100% 0 0 / 0.55) 0.03rem, transparent 0.05rem); + background-size: 4rem 4rem, 5rem 5rem; +} + +.hero-shell { + position: relative; + z-index: 1; + width: min(100%, calc(var(--shell-max) + (var(--shell-pad) * 2))); + min-height: 100svh; + margin: 0 auto; + padding: 1.25rem var(--shell-pad) 2.4rem; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 2rem; +} + +.hero-topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.hero-brand-mini { + font-size: var(--text-xs); + font-weight: 800; + letter-spacing: 0.04em; + color: var(--hero-muted); +} + +.hero-topbar-link:hover { + color: var(--hero-text); +} + +.hero-sub a:hover { + color: var(--hero-text); + text-decoration-color: oklch(100% 0 0 / 0.6); +} + +.hero-topbar-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 0.75rem; +} + +.hero-topbar-link { + padding: 0.45rem 0.8rem; + border: 1px solid var(--hero-line); + border-radius: 999px; + color: var(--hero-muted); + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.02em; + transition: + color 180ms ease, + border-color 180ms ease, + background-color 180ms ease, + transform 180ms ease; +} + +.hero-topbar-link-strong, +.hero-topbar-link:hover { + border-color: oklch(100% 0 0 / 0.28); + background: oklch(100% 0 0 / 0.06); +} + +.hero-topbar-link:active, +.hero-action:active, +.tag:active, +.filter-clear:active { + transform: translateY(1px); +} + +.hero-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: center; + gap: 0; + flex: 1; +} + +.hero-copy { + width: 100%; + max-width: none; + animation: hero-rise 700ms cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.hero-kicker, +.section-label { + margin-bottom: 1.5rem; + font-size: var(--text-sm); + font-weight: 800; + letter-spacing: 0.04em; +} + +.hero-kicker { + color: var(--hero-kicker); +} + +.hero h1 { + font-family: var(--font-display); + font-size: clamp(4.5rem, 11vw, 8.5rem); + line-height: 0.9; + font-weight: 600; + letter-spacing: -0.03em; + text-wrap: balance; +} + +.hero-sub { + margin-top: 2rem; + color: var(--hero-muted); + font-size: clamp(1rem, 2vw, 1.18rem); + text-wrap: pretty; +} + +.hero-sub a { + color: var(--hero-text); + text-decoration: underline; + text-decoration-color: oklch(100% 0 0 / 0.25); + text-underline-offset: 0.2em; +} + +.hero-actions, +.final-cta-actions { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; +} + +.hero-actions { + margin-top: 1.75rem; +} + +.hero-proof { + margin-top: 0.5rem; + color: var(--hero-proof); + font-size: var(--text-sm); + letter-spacing: 0.02em; +} + +.hero-proof a { + color: var(--hero-text); + text-decoration: underline; + text-decoration-color: oklch(100% 0 0 / 0.25); + text-underline-offset: 0.2em; +} + +.hero-proof a:hover { + text-decoration-color: oklch(100% 0 0 / 0.6); +} + +.hero-category-nav { + display: grid; + grid-template-columns: minmax(10rem, 14rem) minmax(0, 1fr); + gap: clamp(1.5rem, 4vw, 3rem); + align-items: start; + padding-top: 1.35rem; + border-top: 1px solid var(--hero-line); + animation: hero-rise 820ms cubic-bezier(0.22, 1, 0.36, 1) 110ms both; +} + +.hero-category-meta h2 { + color: var(--hero-kicker); + font-size: var(--text-sm); + font-weight: 800; + letter-spacing: 0.04em; +} + +.hero-category-links { + list-style: none; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); + column-gap: clamp(1.25rem, 3vw, 2.5rem); + row-gap: 0.28rem; +} + +.hero-category-link { + color: var(--hero-muted); + font-size: 0.72rem; + font-weight: 700; + line-height: 1.35; + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 0.18em; + transition: + color 180ms ease, + text-decoration-color 180ms ease; +} + +.hero-category-link:hover { + color: var(--hero-text); + text-decoration-color: oklch(100% 0 0 / 0.42); +} + +.hero-action { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 3rem; + padding: 0.85rem 1.25rem; + border-radius: 999px; + border: 1px solid transparent; + font-size: var(--text-sm); + font-weight: 700; + letter-spacing: 0.01em; + transition: + transform 180ms ease, + color 180ms ease, + background-color 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease; +} + +.hero-action-primary { + color: var(--hero-ink); + background: linear-gradient(135deg, var(--hero-btn-start), var(--hero-btn-end)); + box-shadow: 0 1.2rem 2.5rem -1.5rem oklch(0% 0 0 / 0.65); +} + +.hero-action-primary:hover { + box-shadow: 0 1.5rem 2.7rem -1.4rem oklch(0% 0 0 / 0.8); +} + +.hero-action-secondary { + color: var(--hero-text); + border-color: var(--hero-line); + background: oklch(100% 0 0 / 0.04); +} + +.hero-action-secondary:hover { + background: oklch(100% 0 0 / 0.08); + border-color: oklch(100% 0 0 / 0.28); +} + +.hero-action:focus-visible, +.hero-brand-mini:focus-visible, +.hero-topbar-link:focus-visible, +.hero-category-link:focus-visible, +.search:focus-visible, +.filter-clear:focus-visible, +.tag:focus-visible, +.back-to-top:focus-visible, +.no-results-clear:focus-visible, +.table a:focus-visible, +.footer a:focus-visible, +.sort-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} + +.category-hero { + position: relative; + overflow: clip; + background: linear-gradient(140deg, var(--hero-bg-start) 0%, var(--hero-bg-mid) 58%, var(--hero-bg-end) 100%); + color: var(--hero-text); +} + +.category-hero-shell { + position: relative; + z-index: 1; + width: min(100%, calc(var(--shell-max) + (var(--shell-pad) * 2))); + margin: 0 auto; + padding: 1.25rem var(--shell-pad) clamp(3.75rem, 8vw, 6.75rem); + display: grid; + gap: clamp(3rem, 8vw, 5.5rem); +} + +.category-hero h1 { + font-family: var(--font-display); + font-size: clamp(3.6rem, 9vw, 7rem); + line-height: 0.9; + font-weight: 600; + text-wrap: balance; +} + +.category-breadcrumb { + margin: 0 0 1rem; + color: var(--hero-muted); + font-size: clamp(1rem, 1.5vw, 1.1rem); +} + +.category-breadcrumb a { + color: var(--hero-text); + text-decoration: underline; + text-decoration-color: oklch(100% 0 0 / 0.32); + text-underline-offset: 0.2em; +} + +.category-breadcrumb a:hover { + text-decoration-color: oklch(100% 0 0 / 0.7); +} + +.category-subtitle { + margin-top: 1.1rem; + color: var(--hero-muted); + font-size: clamp(1rem, 1.8vw, 1.18rem); + text-wrap: pretty; +} + +.category-subtitle a { + color: var(--hero-text); + text-decoration: underline; + text-decoration-color: oklch(100% 0 0 / 0.32); + text-underline-offset: 0.2em; +} + +.category-subtitle a:hover { + text-decoration-color: oklch(100% 0 0 / 0.7); +} + +.sponsor-band { + padding-block: clamp(2.5rem, 5.5vw, 4rem); + background: + linear-gradient(180deg, var(--bg-paper-strong), var(--bg-paper)); + border-bottom: 1px solid var(--line); +} + +.sponsor-shell { + display: grid; + grid-template-columns: minmax(0, 14rem) minmax(0, 1fr); + gap: clamp(1.5rem, 5vw, 3.5rem); + align-items: start; +} + +.sponsor-meta { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.sponsor-meta .section-label { + margin-bottom: 0; + font-size: var(--text-lg); +} + +.sponsor-become { + align-self: start; + color: var(--ink-soft); + font-size: var(--text-sm); + font-weight: 700; + letter-spacing: 0.01em; + text-decoration: underline; + text-decoration-color: var(--line-strong); + text-underline-offset: 0.2em; + transition: + color 180ms ease, + text-decoration-color 180ms ease; +} + +.sponsor-become:hover { + color: var(--accent-deep); + text-decoration-color: var(--accent-underline); +} + +.sponsor-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: clamp(1.5rem, 3vw, 2.25rem); +} + +.sponsor { + display: grid; + gap: 0.65rem; +} + +.sponsor-link { + display: inline-flex; + align-items: baseline; + gap: 0.5rem; + margin-block-start: -1rem; + color: var(--ink); + transition: color 180ms ease; +} + +.sponsor-link:hover { + color: var(--accent-deep); +} + +.sponsor-name { + font-family: var(--font-display); + font-size: clamp(2.25rem, 4.2vw, 3.25rem); + font-weight: 600; + line-height: 0.95; + letter-spacing: -0.025em; +} + +.sponsor-desc { + color: var(--ink-soft); + font-size: clamp(1rem, 1.5vw, 1.1rem); + line-height: 1.55; +} + +.sponsor-desc a { + color: var(--accent-deep); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.18em; +} + +.sponsor-desc a:hover { + color: var(--accent); +} + +.results-intro h2, +.final-cta h2 { + font-family: var(--font-display); + font-size: clamp(2.2rem, 4vw, 3.3rem); + font-weight: 600; + line-height: 0.94; + letter-spacing: -0.03em; +} + +.results-section { + padding-block: clamp(2.5rem, 6vw, 4.5rem) 0; + contain: layout style; +} + +.results-intro { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(18rem, 28rem); + gap: 1.25rem; + align-items: end; + padding-bottom: 1.75rem; +} + +.results-note { + color: var(--ink-soft); + font-size: var(--text-sm); + justify-self: end; + max-width: 28rem; +} + +.controls { + display: grid; + gap: 0.85rem; + padding-bottom: 1.35rem; +} + +.search-wrap { + position: relative; +} + +.search-icon { + position: absolute; + left: 1.15rem; + top: 50%; + transform: translateY(-50%); + color: var(--ink-muted); + pointer-events: none; +} + +.search { + width: 100%; + min-height: 4.1rem; + padding: 1rem 1.15rem 1rem 3.2rem; + border: 1px solid transparent; + border-radius: 999px; + background: linear-gradient(180deg, oklch(100% 0 0 / 0.82), var(--bg-paper)); + color: var(--ink); + font-size: clamp(1rem, 1.4vw, 1.06rem); + box-shadow: + inset 0 1px 0 var(--search-inset), + 0 1.4rem 2.6rem -2.1rem var(--search-shadow); + transition: + border-color 180ms ease, + box-shadow 180ms ease, + background-color 180ms ease; +} + +.search::placeholder { + color: var(--ink-muted); +} + +.search:focus { + border-color: var(--search-focus-ring); + box-shadow: + inset 0 1px 0 var(--search-inset), + 0 1.6rem 3rem -2rem var(--search-focus-shadow); +} + +.filter-bar { + display: flex; + align-items: center; + gap: 0.75rem; + min-height: 2.3rem; + font-size: var(--text-sm); + color: var(--ink-soft); + opacity: 0; + transform: translateY(-0.4rem); + pointer-events: none; + transition: + opacity 180ms ease, + transform 180ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.filter-bar.visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.filter-bar strong { + color: var(--ink); +} + +.filter-clear { + border: 1px solid var(--line); + border-radius: 999px; + background: var(--bg-paper); + color: var(--ink-soft); + padding: 0.42rem 0.82rem; + cursor: pointer; + transition: + border-color 180ms ease, + color 180ms ease, + background-color 180ms ease, + transform 180ms ease; +} + +.filter-clear:hover { + color: var(--ink); + background: var(--accent-soft); + border-color: oklch(68% 0.08 58 / 0.5); +} + +.table-wrap { + width: 100%; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); + scroll-margin-top: 1rem; +} + +.table-wrap:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + +.table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: var(--text-sm); +} + +.table thead th, +.table tbody td { + padding-inline: 0.9rem; +} + +.table thead th:first-child, +.table tbody td:first-child { + padding-left: max(var(--shell-pad), calc(50vw - (var(--shell-max) / 2) + var(--shell-pad))); +} + +.table thead th:last-child, +.table tbody td:last-child { + padding-right: max(var(--shell-pad), calc(50vw - (var(--shell-max) / 2) + var(--shell-pad))); +} + +.table thead th { + position: sticky; + top: 0; + z-index: 12; + padding-top: 1rem; + padding-bottom: 0.95rem; + text-align: left; + color: var(--ink); + font-size: var(--text-xs); + font-weight: 800; + letter-spacing: 0.03em; + white-space: nowrap; + border-bottom: 1px solid var(--line); + background: var(--thead-bg); +} + +.table tbody td { + padding-top: 1rem; + padding-bottom: 1rem; + vertical-align: middle; + border-bottom: 1px solid var(--line); + transition: + background-color 180ms ease, + border-color 180ms ease; +} + +.table tbody tr[hidden] { + display: none; +} + +.row { + cursor: pointer; +} + +.row:not(.open):hover td { + background: var(--row-hover); +} + +.row:focus-visible td { + background: var(--row-focus); + box-shadow: inset 3px 0 0 var(--accent); +} + +.row:has(+ .desc-row:not([hidden])) td { + border-bottom-color: transparent; + padding-bottom: 0.35rem; +} + +.row.open td { + background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end)); + border-bottom-color: transparent; +} + +.col-num { + width: 3.5rem; + color: var(--ink-muted); + font-variant-numeric: tabular-nums; +} + +.col-name { + white-space: nowrap; +} + +.mobile-cat { + display: none; +} + +.col-name > a { + color: var(--ink); + font-size: clamp(1rem, 1.5vw, 1.08rem); + font-weight: 700; + overflow-wrap: break-word; + word-break: break-word; +} + +.col-name > a:hover { + color: var(--accent-deep); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.2em; +} + +th[data-sort] { + cursor: pointer; + user-select: none; + transition: color 180ms ease; +} + +th[data-sort]:hover { + color: var(--accent-deep); +} + +.sort-btn { + background: none; + border: 0; + padding: 0; + color: inherit; + font: inherit; + cursor: inherit; +} + +th[data-sort]::after { + content: " \2193"; + opacity: 0; + transition: opacity 180ms ease; +} + +th[data-sort="name"]::after { + content: " \2191"; +} + +th[data-sort]:hover::after, +th[data-sort].sort-asc::after, +th[data-sort].sort-desc::after { + opacity: 1; +} + +th[data-sort].sort-desc::after { + content: " \2193"; +} + +th[data-sort].sort-asc::after { + content: " \2191"; +} + +.col-stars { + width: 7rem; + text-align: right; + white-space: nowrap; + font-variant-numeric: tabular-nums; + color: var(--ink-soft); +} + +.source-badge { + display: inline-flex; + align-items: center; + min-height: 1.8rem; + padding: 0.18rem 0.6rem; + border-radius: 999px; + background: var(--bg-paper-strong); + color: var(--ink-soft); + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.02em; +} + +.col-downloads { + width: 9.5rem; + text-align: right; + white-space: nowrap; + font-variant-numeric: tabular-nums; + color: var(--ink-soft); +} + +.col-commit { + width: 9rem; + white-space: nowrap; + color: var(--ink-muted); +} + +.col-cat { + white-space: nowrap; +} + +.col-arrow { + width: 3rem; + text-align: center; +} + +.arrow { + display: inline-block; + color: var(--accent-deep); + font-size: 0.9rem; + transition: + transform 180ms ease, + color 180ms ease; +} + +.row:hover .arrow, +.row.open .arrow { + color: var(--accent); +} + +.row.open .arrow { + transform: rotate(90deg); +} + +.desc-row td { + padding-top: 0; + padding-bottom: 1rem; + border-bottom: 1px solid var(--line); + color: var(--ink-soft); + font-size: var(--text-sm); + line-height: 1.6; + text-wrap: pretty; + overflow-wrap: break-word; + transition: + background-color 180ms ease, + border-color 180ms ease; +} + +.row:not(.open) + .desc-row td { + border-bottom: 1px solid var(--line); +} + +.row + .desc-row { + cursor: pointer; +} + +.row:not(.open):hover td, +.row:not(.open):hover + .desc-row td { + background: var(--row-hover); +} + +.row.open + .desc-row td { + background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end)); + border-bottom-color: transparent; +} + +.desc-text { + max-width: none; +} + +.desc-text a { + color: var(--accent-deep); +} + +.desc-text a:hover { + color: var(--accent); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.2em; +} + +.expand-row { + display: none; +} + +.row.open + .desc-row + .expand-row, +.row.open + .expand-row { + display: table-row; +} + +.desc-row:not([hidden]) + .expand-row .expand-desc { + display: none; +} + +.expand-row td { + padding-top: 0.1rem; + padding-bottom: 1.15rem; + background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end)); + border-bottom: 1px solid var(--line); +} + +.expand-content { + font-size: var(--text-base); + color: var(--ink-soft); + line-height: 1.7; + text-wrap: pretty; + overflow-wrap: break-word; + word-break: break-word; + contain: layout style paint; + animation: expand-in 220ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.expand-desc a, +.expand-also-see a, +.expand-meta a, +.footer a { + color: var(--accent-deep); +} + +.expand-desc a:hover, +.expand-also-see a:hover, +.expand-meta a:hover { + color: var(--accent); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.2em; +} + +.expand-also-see, +.expand-meta { + margin-top: 0.45rem; + font-size: var(--text-sm); + color: var(--ink-muted); +} + +.expand-meta a { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; +} + +.expand-sep { + margin-inline: 0.25rem; + color: var(--line-strong); +} + +.expand-commit, +.expand-downloads { + display: none; +} + +.tag { + position: relative; + border: 1px solid transparent; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent-deep); + padding: 0.14rem 0.48rem; + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.02em; + cursor: pointer; + transition: + color 180ms ease, + background-color 180ms ease, + border-color 180ms ease, + transform 180ms ease; +} + +.tag + .tag { + margin-left: 0.2rem; +} + +.tag::after { + content: ""; + position: absolute; + inset: -0.5rem -0.25rem; + min-height: 44px; + min-width: 44px; +} + +.tag:hover { + background: var(--highlight); + border-color: var(--tag-hover-border); + color: var(--ink); +} + +.tag.active { + background: linear-gradient(135deg, var(--tag-active-start), var(--tag-active-end)); + color: var(--hero-ink); +} + +.back-to-top { + border: 0; + background: none; + color: var(--accent-deep); + font-size: var(--text-xs); + font-weight: 800; + letter-spacing: 0.03em; + cursor: pointer; + opacity: 0; + pointer-events: none; + transition: + opacity 180ms ease, + color 180ms ease; +} + +.back-to-top.visible { + opacity: 1; + pointer-events: auto; +} + +.back-to-top:hover { + color: var(--accent); +} + +.no-results { + padding: 2.4rem var(--shell-pad); + text-align: center; + color: var(--ink-muted); + font-size: var(--text-lg); +} + +.no-results-hint { + margin-top: 0.5rem; + font-size: var(--text-sm); +} + +.no-results-clear { + background: none; + border: none; + color: var(--accent-deep); + font: inherit; + cursor: pointer; + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.2em; +} + +.no-results-clear:hover { + color: var(--accent); +} + +.sponsorship-hero .category-hero-shell { + padding-bottom: clamp(3.25rem, 6vw, 5rem); + gap: clamp(2rem, 5vw, 3.5rem); +} + +.sponsorship-hero-copy h1 { + font-size: clamp(3.4rem, 8.5vw, 6.5rem); +} + +.sponsorship-proof { + margin-top: 1.6rem; +} + +.sponsorship-proof .proof-sep { + color: oklch(100% 0 0 / 0.32); + margin-inline: 0.15rem; +} + +.sponsorship-hero .hero-actions { + margin-top: 1.9rem; +} + +.sponsorship-section { + padding-block: clamp(2.75rem, 5.5vw, 4.25rem); + border-bottom: 1px solid var(--line); +} + +.sponsorship-section:first-of-type { + padding-top: clamp(3.25rem, 6vw, 4.75rem); +} + +.sponsorship-section:last-of-type { + border-bottom: 0; + padding-bottom: clamp(3.5rem, 7vw, 5.5rem); +} + +.sponsorship-getstarted { + background: var(--cta-bg); + border-top: 1px solid var(--line); +} + +.sponsorship-shell { + display: grid; + grid-template-columns: minmax(0, 16rem) minmax(0, 1fr); + gap: clamp(1.75rem, 5vw, 4rem); + align-items: start; +} + +.sponsorship-meta { + display: flex; + flex-direction: column; + gap: 0.85rem; + position: sticky; + top: 1.5rem; +} + +.sponsorship-meta .section-label { + margin-bottom: 0; + font-size: var(--text-lg); +} + +.sponsorship-meta-note { + color: var(--ink-muted); + font-size: var(--text-sm); + line-height: 1.55; +} + +.sponsorship-body { + display: flex; + flex-direction: column; + gap: 1.6rem; + font-size: var(--text-lg); + color: var(--ink-soft); + line-height: 1.7; +} + +.sponsorship-body p { + text-wrap: pretty; +} + +.sponsorship-body code { + font-family: ui-monospace, "SFMono-Regular", "Menlo", monospace; + font-size: 0.92em; + padding: 0.08rem 0.4rem; + border-radius: 0.4rem; + background: var(--bg-paper-strong); + color: var(--ink); +} + +.sponsorship-body a:not(.hero-action):not(.tier-cta) { + color: var(--accent-deep); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.2em; + transition: color 180ms ease; +} + +.sponsorship-body a:not(.hero-action):not(.tier-cta):hover { + color: var(--accent); +} + +.sponsorship-lede { + font-family: var(--font-display); + font-size: clamp(1.55rem, 2.6vw, 2rem); + line-height: 1.25; + color: var(--ink); + letter-spacing: -0.01em; + text-wrap: pretty; +} + +.sponsorship-facts { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1.4rem; + border-top: 1px solid var(--line); + padding-top: 1.6rem; +} + +.sponsorship-facts > div { + display: grid; + grid-template-columns: minmax(0, 12rem) minmax(0, 1fr); + gap: clamp(1rem, 3vw, 2rem); + align-items: baseline; +} + +.sponsorship-facts dt { + font-size: var(--text-xs); + font-weight: 800; + letter-spacing: 0.05em; + color: var(--ink); +} + +.sponsorship-facts dd { + color: var(--ink-soft); + font-size: var(--text-base); + line-height: 1.65; +} + +.tier-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: clamp(1.5rem, 3vw, 2.75rem); +} + +.tier { + display: flex; + flex-direction: column; + gap: 1rem; + padding-block: 1.65rem; + border-top: 1px solid var(--line-strong); +} + +.tier-eyebrow { + font-size: var(--text-xs); + font-weight: 800; + letter-spacing: 0.05em; + color: var(--ink); +} + +.tier-price { + display: flex; + align-items: baseline; + gap: 0.55rem; + margin-bottom: 0.25rem; +} + +.tier-amount { + font-family: var(--font-display); + font-size: clamp(3rem, 5.5vw, 4.5rem); + font-weight: 600; + line-height: 0.9; + letter-spacing: -0.025em; + color: var(--ink); +} + +.tier-cadence { + color: var(--ink-muted); + font-size: var(--text-base); + font-weight: 600; + letter-spacing: 0.01em; +} + +.tier-summary { + font-size: var(--text-lg); + color: var(--ink); + line-height: 1.5; + text-wrap: pretty; +} + +.tier-includes { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.6rem; + border-top: 1px solid var(--line); + padding-top: 1.1rem; +} + +.tier-includes li { + position: relative; + padding-left: 1.4rem; + color: var(--ink-soft); + font-size: var(--text-base); + line-height: 1.6; +} + +.tier-includes li::before { + content: ""; + position: absolute; + left: 0; + top: 0.65rem; + width: 0.55rem; + height: 1px; + background: var(--line-strong); +} + +.tier-cta { + align-self: start; + margin-top: 0.75rem; + color: var(--accent-deep); + font-size: var(--text-sm); + font-weight: 700; + letter-spacing: 0.01em; + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.22em; + transition: color 180ms ease, text-decoration-color 180ms ease; +} + +.tier-cta:hover { + color: var(--accent); + text-decoration-color: var(--accent); +} + +.past-sponsors { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.past-sponsors li { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.65rem; + padding-block: 0.4rem; +} + +.past-sponsors a { + font-family: var(--font-display); + font-size: clamp(1.6rem, 2.8vw, 2.1rem); + font-weight: 600; + line-height: 1; + letter-spacing: -0.02em; + color: var(--ink); + transition: color 180ms ease; +} + +.past-sponsors a:hover { + color: var(--accent-deep); +} + +.past-sponsor-desc { + color: var(--ink-muted); + font-size: var(--text-base); +} + +.sponsorship-cta-row { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; + margin-top: 0.5rem; +} + +.sponsorship-cta-row .hero-action-primary { + color: var(--hero-text); + background: linear-gradient(135deg, var(--accent), var(--accent-deep)); +} + +.sponsorship-fineprint { + font-size: var(--text-base); + color: var(--ink-muted); +} + +.final-cta { + padding-block: clamp(3rem, 7vw, 5.5rem); + background: var(--cta-bg); +} + +.final-cta > .section-shell { + display: grid; + gap: 1rem; +} + +.final-cta p { + color: var(--ink-soft); + font-size: clamp(1rem, 1.6vw, 1.08rem); +} + +.final-cta .hero-action-primary { + color: var(--hero-text); + background: linear-gradient(135deg, var(--accent), var(--accent-deep)); +} + +.final-cta .hero-action-secondary { + color: var(--ink); + background: transparent; + border-color: var(--line-strong); +} + +.final-cta .hero-action-secondary:hover { + background: var(--accent-soft); + border-color: var(--accent); +} + +.footer { + margin-top: auto; + background: var(--footer-bg); + padding: 3rem var(--shell-pad); + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + font-size: var(--text-sm); + color: var(--footer-text); +} + +.footer a { + color: var(--footer-link); +} + +.footer a:hover { + color: var(--footer-link-hover); + text-decoration: underline; + text-decoration-color: oklch(95% 0.01 80 / 0.4); + text-underline-offset: 0.2em; +} + +.footer-left { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.footer-brand { + font-weight: 700; + letter-spacing: 0.03em; + color: var(--footer-link); +} + +.footer-links { + display: block; + text-align: right; +} + +.footer-sep { + color: var(--footer-sep); +} + +.noscript-msg { + padding: 1rem var(--shell-pad) 0; + text-align: center; + color: var(--ink-muted); +} + +[data-reveal].will-reveal { + opacity: 0; + transform: translateY(1.8rem); +} + +[data-reveal].will-reveal.is-visible { + opacity: 1; + transform: translateY(0); + transition: + opacity 600ms ease, + transform 600ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +@keyframes hero-rise { + from { + opacity: 0; + transform: translateY(1.3rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes expand-in { + from { + opacity: 0; + transform: translateY(-0.4rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes sheen-drift { + from { + transform: translateX(-30%); + } + to { + transform: translateX(35%); + } +} + +@media (max-width: 960px) { + .hero-shell { + min-height: auto; + padding-bottom: 2rem; + } + + .hero-grid, + .results-intro, + .sponsor-shell, + .sponsorship-shell { + grid-template-columns: 1fr; + } + + .sponsorship-meta { + position: static; + } + + .tier-list { + grid-template-columns: 1fr; + gap: 0; + } + + .sponsorship-facts > div { + grid-template-columns: 1fr; + gap: 0.35rem; + } + + .hero-category-nav { + grid-template-columns: 1fr; + gap: 0.95rem; + } + + .hero-category-links { + grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); + } + + .results-note { + justify-self: start; + } + + .col-commit, + .col-downloads { + display: none; + } + + .expand-commit, + .expand-downloads { + display: inline; + } + + .tag-group { + display: none; + } + + .tag { + padding: 0.38rem 0.65rem; + font-size: var(--text-xs); + } + + .table-wrap { + overflow-x: clip; + } +} + +@media (max-width: 680px) { + .hero { + min-height: auto; + } + + .hero-topbar { + gap: 0.75rem; + } + + .footer { + flex-direction: column; + align-items: center; + text-align: center; + } + + .hero-actions, + .final-cta-actions { + width: 100%; + } + + .hero-topbar-actions { + width: auto; + flex: 0 0 auto; + } + + .hero-topbar-link { + width: auto; + white-space: nowrap; + } + + .hero-action { + width: 100%; + } + + .hero h1 { + font-size: clamp(3.6rem, 18vw, 5.2rem); + } + + .hero-category-links { + grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); + column-gap: 0.75rem; + } + + .hero-category-link { + font-size: 0.68rem; + } + + .search { + min-height: 3.5rem; + border-radius: 1.25rem; + } + + .table thead th, + .table tbody td { + padding-inline: 0.55rem; + } + + .table thead th:first-child, + .table tbody td:first-child { + padding-left: 0.8rem; + } + + .table thead th:last-child, + .table tbody td:last-child { + padding-right: 0.8rem; + } + + .col-num, + .col-cat { + display: none; + } + + .expand-row td:first-child, + .expand-row td:last-child { + display: none; + } + + .col-name { + white-space: normal; + } + + .mobile-cat { + display: block; + margin-top: 0.25rem; + font-size: var(--text-xs); + font-weight: 600; + color: var(--ink-muted); + } + + .expand-row td[colspan] { + padding-left: 0.8rem; + padding-right: 0.8rem; + } + + .col-stars { + width: 5.4rem; + } + + .col-arrow { + width: 1.8rem; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/website/templates/base.html b/website/templates/base.html new file mode 100644 index 0000000000..dc164347d5 --- /dev/null +++ b/website/templates/base.html @@ -0,0 +1,97 @@ + + + + {% set default_meta_title = "Awesome Python" %} + {% set default_meta_description = "An opinionated guide to the best Python frameworks, libraries, and tools. Explore " ~ (entries | length) ~ " curated projects across " ~ total_categories ~ " categories, from AI and agents to data science and web development." %} + {% set default_canonical_url = "https://awesome-python.com/" %} + {% set social_image_url = "https://awesome-python.com/static/og-image.png" %} + {% set meta_title %}{% block title %}{{ default_meta_title }}{% endblock %}{% endset %} + {% set meta_description %}{% block description %}{{ default_meta_description }}{% endblock %}{% endset %} + {% set canonical_url %}{% block canonical_url %}{{ default_canonical_url }}{% endblock %}{% endset %} + + + {{ meta_title | trim }} + + + {% block alternate_links %} + + {% endblock %} + + + + + + + + + + + + + + + + + + + {% block extra_head %}{% endblock %} + + + + + {% block header %}{% endblock %} +
+ + {% block content %}{% endblock %} +
+ + + + + + diff --git a/website/templates/category.html b/website/templates/category.html new file mode 100644 index 0000000000..e29e2f1780 --- /dev/null +++ b/website/templates/category.html @@ -0,0 +1,309 @@ +{% extends "base.html" %} +{% block title %}{{ category_title }}{% endblock %} +{% block description %}{{ category_description }}{% endblock %} +{% block canonical_url %}{{ category_url }}{% endblock %} +{% block alternate_links %}{% endblock %} +{% block extra_head %} + +{% endblock %} +{% block header %} +
+ + + +
+ + +
+ {% if parent_category %} +

+ {{ parent_category.name }} +

+ {% endif %} +

{{ category.name }}

+ {% if category.description_html %} +

{{ category.description_html | safe }}

+ {% endif %} +
+ + {% if group_categories %} + + {% endif %} +
+
+{% endblock %} +{% block content %} + +
+
+
+

Search every project in one place

+
+

+ Press / to search. Tap a tag to filter. Click any row for + details. +

+
+ +
+

Search and filter

+
+ + + + + +
+
+ Filtering for + +
+
+ +

Results

+
+ + + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + + {% if entry.description %} + + + + + {% endif %} + + + + + + {% endfor %} + +
Row number + + + + + + + + Tags + +
+
+ {% if entry.also_see %} +
+ Also see: {% for see in entry.also_see %}{{ see.name }}{% if not loop.last %}, {% endif %}{% endfor %} +
+ {% endif %} +
+ {% if entry.owner %}{{ entry.owner }}/{% endif %}{{ entry.url | replace("https://", "") }} + {% if entry.last_commit_at %}/{% endif %} {% if entry.downloads is not none %}/{{ + "{:,}".format(entry.downloads) }} downloads/month{% endif %} +
+
+
+
+ + +
+ +
+
+ +

Know a project that belongs here?

+

Tell us what it does and why it stands out.

+ +
+
+{% endblock %} diff --git a/website/templates/index.html b/website/templates/index.html new file mode 100644 index 0000000000..35329f4324 --- /dev/null +++ b/website/templates/index.html @@ -0,0 +1,344 @@ +{% extends "base.html" %} +{% block extra_head %} + +{% endblock %} +{% block header %} +
+ + + +
+ + +
+
+

The definitive list that answers "I want to do X in Python, which tool should I use?"

+

Awesome Python

+

{{ subtitle }}

+

+ {% if repo_stars %}{{ repo_stars }}+ stars on GitHub{% endif %} {% if repo_stars and build_date %}/{% endif %} {% if build_date %}Updated {{ build_date }}{% endif %} {% if repo_stars or + build_date %}/{% endif %} Maintained by + @vinta + and + @JinyangWang27 +

+ +
+
+ + +
+
+{% endblock %} +{% block content %} +{% if sponsors %} + +{% endif %} + + +
+
+
+

Search every project in one place

+
+

+ Press / to search. Tap a tag to filter. Click any row for + details. +

+
+ +
+

Search and filter

+
+ + + + + +
+
+ Filtering for + +
+
+ +

Results

+
+ + + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + + {% if entry.description %} + + + + + {% endif %} + + + + + + {% endfor %} + +
Row number + + + + + + + + Tags + +
+
+ {% if entry.description %} +
{{ entry.description | safe }}
+ {% endif %} {% if entry.also_see %} +
+ Also see: {% for see in entry.also_see %}{{ see.name }}{% if not loop.last %}, {% endif %}{% endfor %} +
+ {% endif %} +
+ {% if entry.owner %}{{ entry.owner }}/{% endif %}{{ entry.url | replace("https://", "") }} + {% if entry.last_commit_at %}/{% endif %} {% if entry.downloads is not none %}/{{ + "{:,}".format(entry.downloads) }} downloads/month{% endif %} +
+
+
+
+ + +
+ +
+
+ +

Know a project that belongs here?

+

Tell us what it does and why it stands out.

+ +
+
+{% endblock %} diff --git a/website/templates/llms.txt b/website/templates/llms.txt new file mode 100644 index 0000000000..c29782d8c3 --- /dev/null +++ b/website/templates/llms.txt @@ -0,0 +1,19 @@ +# Awesome Python + +{{ subtitle }} + +{{ total_entries }} projects across {{ total_categories }} {% if total_categories == 1 %}category{% else %}categories{% endif %}, updated on {{ build_date }}. + +Scan the category index, then jump to the matching section for direct project links and short descriptions. Entries end with `PyPI downloads/month: N` (last 30 days, mirrors and CI included) and `GitHub stars: N` notes in parentheses where known; treat both as popularity context, not a quality guarantee. Use the homepage for project context outside the shortlist. + +## Primary Links + +- Homepage: {{ site_url }} +- GitHub repository: {{ github_repo_url }} +- Contributing guide: {{ contributing_url }} +- Sponsorship: {{ sponsorship_url }} +- Sitemap: {{ sitemap_url }} + +## Categories + +{{ categories_md }} diff --git a/website/templates/sponsorship.html b/website/templates/sponsorship.html new file mode 100644 index 0000000000..224d87e765 --- /dev/null +++ b/website/templates/sponsorship.html @@ -0,0 +1,293 @@ +{% extends "base.html" %} +{% block title %}Sponsor Awesome Python{% endblock %} +{% block description %}{{ sponsorship_description }}{% endblock %} +{% block canonical_url %}https://awesome-python.com/sponsorship/{% endblock %} +{% block alternate_links %}{% endblock %} +{% block extra_head %} + +{% endblock %} +{% block header %} +
+ + + +
+ + +
+

Sponsorship

+

Sponsor Awesome Python

+

+ The #10 most-starred repository on GitHub, and the list Python + developers check when choosing what to use. Your sponsorship puts your + product in front of them at the moment of decision. +

+ + {% if hero_stats %} +

+ {% for stat in hero_stats %}{{ stat }}{% if not loop.last %} + / {% endif %}{% endfor %} +

+ {% endif %} + + +
+
+
+{% endblock %} {% block content %} +
+
+
+ +
+
+

+ Professional Python developers evaluating libraries and tools for + production use. Not beginners browsing tutorials. People making adoption + decisions. +

+
+
+
Who visits
+
+ Mid to senior Python developers arriving with a specific question: a + maintained ORM, a fast HTTP client, a task queue worth running in + production. +
+
+
+
Where they come from
+
+ Google Search, GitHub, Reddit, YouTube, ChatGPT and other LLMs, + Hacker News. +
+
+
+
Why it works
+
+ Ranks on the first page of Google for "best Python libraries". + ChatGPT and other LLMs cite it when recommending Python tools. + Developers send it to each other. +
+
+
+
+
+
+ +
+
+
+ +

+ One upfront payment per term. Setup takes less than 24 hours. +

+
+
+
    +
  1. +

    Headline Sponsor

    +

    + $500 + / month +

    +

    + Logo pinned at the top of the README. Logo on the website. +

    +
      +
    • + Large logo and one-line description (max 120 characters) pinned at + the very top of the README, above all project entries. +
    • +
    • Logo link in the sponsor section of awesome-python.com.
    • +
    + Email about Headline tier +
  2. +
  3. +

    Featured Sponsor

    +

    + $200 + / month +

    +

    + Text link pinned at the top of the README. Text link on the website. +

    +
      +
    • + Text entry ([Name](URL) - Description., max 120 + characters) pinned at the top of the README, directly below + Headline sponsors. +
    • +
    • Text link in the sponsor section of awesome-python.com.
    • +
    + Email about Featured tier +
  4. +
  5. +

    Indie Dev Sponsor

    +

    + $99 + / month or $249 / quarter +

    +

    + Lower-cost text link for eligible indie developers and solo + founders. +

    +
      +
    • + Text entry ([Name](URL) - Description., max 120 + characters) in the README sponsor section, directly below Featured + sponsors. +
    • +
    • Text link in the sponsor section of awesome-python.com.
    • +
    • + Available only to founder-led, one-person-company products. Funded + startups, agencies, recruiters, larger companies, and products + with enterprise sales teams should use Featured or Headline. +
    • +
    + Email about Indie Dev tier +
  6. +
+
+
+
+ +
+
+
+ +
+
+
    +
  • + Warp + The terminal for modern developers. +
  • +
  • + pyr + Zero-config Python project manager. +
  • +
+
+
+
+ +
+
+
+ +
+
+

+ Email + sponsorship@awesome-python.com + with the four items below. +

+
+
+
Tier
+
+ Headline Sponsor ($500/mo), Featured Sponsor ($200/mo), or Indie Dev + Sponsor ($99/mo or $249/quarter). +
+
+
+
Content
+
+ Product name, URL, logo, and description (Headline tier), or + [Name](URL) - Description. entry (Featured or Indie Dev + tier). +
+
+
+
Duration
+
1, 3, 6 months, or longer.
+
+
+
Payment method
+
US bank transfer (ACH/wire) or PayPal.
+
+
+ +
+
+
+ +
+
+
+ +
+
+

+ Sponsorship is logo and link placement in the README header. It does not + influence which projects are listed. We curate listings on merit through + the normal + contribution process. +

+

+ We reserve the right to request changes to sponsor text, logos, or links + that are misleading, off-topic, or incompatible with the README + formatting. +

+
+
+
+{% endblock %} diff --git a/website/tests/test_build.py b/website/tests/test_build.py new file mode 100644 index 0000000000..e93ca3ce9d --- /dev/null +++ b/website/tests/test_build.py @@ -0,0 +1,1266 @@ +"""Tests for the build module.""" + +import json +import os +import shutil +import textwrap +import xml.etree.ElementTree as ET +from datetime import UTC, date, datetime +from html.parser import HTMLParser +from pathlib import Path + +import pytest +from build import ( + TemplateEntry, + annotate_entries_with_stats, + build, + detect_source_type, + extract_entries, + extract_github_repo, + load_downloads, + load_stars, + sort_entries, + subcategory_path, +) +from readme_parser import parse_readme, slugify + + +class HeadMetadataParser(HTMLParser): + def __init__(self): + super().__init__() + self.title_count = 0 + self.title = "" + self.meta_by_name = {} + self.meta_by_property = {} + self.links_by_rel = {} + self._in_title = False + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if tag == "title": + self.title_count += 1 + self._in_title = True + elif tag == "meta": + if "name" in attrs: + self.meta_by_name[attrs["name"]] = attrs.get("content", "") + if "property" in attrs: + self.meta_by_property[attrs["property"]] = attrs.get("content", "") + elif tag == "link" and attrs.get("rel"): + for rel in attrs["rel"].split(): + self.links_by_rel[rel] = attrs.get("href", "") + + def handle_endtag(self, tag): + if tag == "title": + self._in_title = False + + def handle_data(self, data): + if self._in_title: + self.title += data + + +# --------------------------------------------------------------------------- +# slugify +# --------------------------------------------------------------------------- + + +class TestSlugify: + def test_simple(self): + assert slugify("Admin Panels") == "admin-panels" + + def test_uppercase_acronym(self): + assert slugify("RESTful API") == "restful-api" + + def test_hyphenated_input(self): + assert slugify("Command-line Tools") == "command-line-tools" + + def test_extra_spaces(self): + assert slugify(" Date and Time ") == "date-and-time" + + +class TestSubcategoryPath: + def test_builds_path(self): + assert subcategory_path("web-frameworks", "synchronous") == "/categories/web-frameworks/synchronous/" + + +# --------------------------------------------------------------------------- +# build (integration) +# --------------------------------------------------------------------------- + + +class TestBuild: + @pytest.fixture(autouse=True) + def _make_sponsorship_md(self, tmp_path): + (tmp_path / "SPONSORSHIP.md").write_text("# Sponsorship\n", encoding="utf-8") + + def _make_repo(self, tmp_path, readme): + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + tpl_dir = tmp_path / "website" / "templates" + tpl_dir.mkdir(parents=True) + (tpl_dir / "base.html").write_text( + "{% block title %}{% endblock %}" + "" + "{% block content %}{% endblock %}", + encoding="utf-8", + ) + (tpl_dir / "index.html").write_text( + '{% extends "base.html" %}{% block content %}' + "{% for entry in entries %}" + '
' + "{{ entry.name }}" + "{{ entry.categories | join(', ') }}" + "{{ entry.groups | join(', ') }}" + "
" + "{% endfor %}" + "{% endblock %}", + encoding="utf-8", + ) + (tpl_dir / "category.html").write_text( + '{% extends "base.html" %}{% block content %}

{{ category.name }}

{% for entry in entries %}{{ entry.name }}{% endfor %}{% endblock %}', + encoding="utf-8", + ) + (tpl_dir / "sponsorship.html").write_text( + '{% extends "base.html" %}{% block content %}

Sponsor

{% endblock %}', + encoding="utf-8", + ) + (tpl_dir / "llms.txt").write_text( + "# Awesome Python\n\nHomepage: {{ site_url }}\n\n## Categories\n\n{{ categories_md }}\n", + encoding="utf-8", + ) + + def _copy_real_templates(self, tmp_path): + real_tpl = Path(__file__).parent / ".." / "templates" + tpl_dir = tmp_path / "website" / "templates" + shutil.copytree(real_tpl, tpl_dir) + + def test_build_creates_homepage_and_category_pages(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + ## Projects + + **Tools** + + ### Widgets + + _Widget libraries. Also see [awesome-widgets](https://example.com/widgets)._ + + - [w1](https://example.com) - A widget. + + ### Gadgets + + _Gadget tools._ + + - [g1](https://example.com) - A gadget. + + ## Resources + + Info. + + ### Newsletters + + - [NL](https://example.com) + + ## Contributing + + Help! + """) + self._make_repo(tmp_path, readme) + build(tmp_path) + + site = tmp_path / "website" / "output" + assert (site / "index.html").exists() + assert (site / "categories" / "widgets" / "index.html").exists() + assert (site / "categories" / "gadgets" / "index.html").exists() + + def test_build_creates_root_discovery_files(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + ## Projects + + **Tools** + + ### Widgets + + - Sync + + - [w1](https://example.com) - A widget. + + ## Contributing + + Help! + """) + self._make_repo(tmp_path, readme) + sponsorship_mtime = datetime(2024, 1, 2, tzinfo=UTC).timestamp() + os.utime(tmp_path / "SPONSORSHIP.md", (sponsorship_mtime, sponsorship_mtime)) + expected_sponsorship_lastmod = "2024-01-02" + start_date = datetime.now(UTC).date() + build(tmp_path) + end_date = datetime.now(UTC).date() + + site = tmp_path / "website" / "output" + robots = (site / "robots.txt").read_text(encoding="utf-8") + assert robots == ("User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: /\n\nSitemap: https://awesome-python.com/sitemap.xml\n") + + sitemap = ET.parse(site / "sitemap.xml") + root = sitemap.getroot() + ns = {"sitemap": "http://www.sitemaps.org/schemas/sitemap/0.9"} + locs = [loc.text or "" for loc in root.findall("sitemap:url/sitemap:loc", ns)] + lastmods = [lastmod.text or "" for lastmod in root.findall("sitemap:url/sitemap:lastmod", ns)] + lastmod_by_loc = dict(zip(locs, lastmods, strict=True)) + + assert root.tag == "{http://www.sitemaps.org/schemas/sitemap/0.9}urlset" + assert locs == [ + "https://awesome-python.com/", + "https://awesome-python.com/categories/widgets/", + "https://awesome-python.com/categories/tools/", + "https://awesome-python.com/categories/widgets/sync/", + "https://awesome-python.com/sponsorship/", + ] + assert len(lastmods) == len(locs) + assert lastmod_by_loc["https://awesome-python.com/sponsorship/"] == expected_sponsorship_lastmod + assert all(start_date <= date.fromisoformat(lastmod) <= end_date for loc, lastmod in lastmod_by_loc.items() if loc != "https://awesome-python.com/sponsorship/") + assert all(loc.startswith("https://awesome-python.com/") for loc in locs) + assert all("?" not in loc for loc in locs) + + def test_build_creates_category_pages_with_metadata_and_links(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + ## Projects + + **Tools** + + ## Widgets + + _Widget libraries. Also see [awesome-widgets](https://example.com/widgets)._ + + - [w1](https://example.com/w1) - A widget. + - [w2](https://github.com/owner/w2) - A starred widget. + + ## Gadgets + + _Gadget tools._ + + - [g1](https://example.com/g1) - A gadget. + + # Contributing + + Help! + """) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + self._copy_real_templates(tmp_path) + + data_dir = tmp_path / "website" / "data" + data_dir.mkdir(parents=True) + stars = { + "owner/w2": { + "stars": 42, + "owner": "owner", + "last_commit_at": "2026-01-01T00:00:00+00:00", + "fetched_at": "2026-01-01T00:00:00+00:00", + }, + } + (data_dir / "github_stars.json").write_text(json.dumps(stars), encoding="utf-8") + + build(tmp_path) + + site = tmp_path / "website" / "output" + index_html = (site / "index.html").read_text(encoding="utf-8") + category_html = (site / "categories" / "widgets" / "index.html").read_text(encoding="utf-8") + parser = HeadMetadataParser() + parser.feed(category_html) + + assert 'href="/categories/widgets/"' in index_html + assert 'data-value="Widgets"' in index_html + assert parser.title.strip() == "Widgets Python Libraries - Awesome Python" + assert parser.meta_by_name["description"] == "Widget libraries. Also see awesome-widgets. Explore 2 curated Python projects in Widgets." + assert parser.links_by_rel["canonical"] == "https://awesome-python.com/categories/widgets/" + assert parser.meta_by_property["og:url"] == "https://awesome-python.com/categories/widgets/" + assert '' not in category_html + assert 'Sponsorship' in category_html + assert "

Widgets

" in category_html + assert 'Widget libraries. Also see awesome-widgets.' in category_html + assert 'href="https://example.com/w1"' in category_html + assert "A widget." in category_html + assert 'href="https://github.com/owner/w2"' in category_html + assert '' in category_html + assert "42" in category_html + assert "2026-01-01T00:00:00+00:00" in category_html + + def test_build_creates_llms_text_alternate_without_sponsors(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + ## **Sponsors** + + - **[Sponsor](https://sponsor.example.com)**: Sponsored tool. + + > Become a sponsor: [Sponsor us](SPONSORSHIP.md). + + ## Categories + + **Tools** + + - [Widgets](#widgets) + + ## Projects + + **Tools** + + ### Widgets + + - [w1](https://example.com) - A widget. + - [w2](https://github.com/owner/w2) - A starred widget. + + ## Contributing + + Help! + """) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + self._copy_real_templates(tmp_path) + + data_dir = tmp_path / "website" / "data" + data_dir.mkdir(parents=True) + stars = { + "owner/w2": {"stars": 42, "owner": "owner", "fetched_at": "2026-01-01T00:00:00+00:00"}, + } + (data_dir / "github_stars.json").write_text(json.dumps(stars), encoding="utf-8") + (data_dir / "pypi_downloads.tsv").write_text( + "name\tpackage\tdownloads\tfetched_at\nw1\tw1\t777\t2026-08-16\n", + encoding="utf-8", + ) + + build(tmp_path) + + site = tmp_path / "website" / "output" + index_html = (site / "index.html").read_text(encoding="utf-8") + llms_txt = (site / "llms.txt").read_text(encoding="utf-8") + + assert '' in index_html + + assert llms_txt.startswith("# Awesome Python\n\nIntro.\n") + assert "2 projects across 1 category, updated on " in llms_txt + assert "Scan the category index" in llms_txt + assert "Homepage: https://awesome-python.com/" in llms_txt + assert "Markdown homepage" not in llms_txt + assert "https://awesome-python.com/index.md" not in llms_txt + assert "GitHub repository: https://github.com/vinta/awesome-python" in llms_txt + assert "Contributing guide: https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md" in llms_txt + assert "Sponsorship: https://awesome-python.com/sponsorship/" in llms_txt + assert "Sitemap: https://awesome-python.com/sitemap.xml" in llms_txt + assert "## Categories" in llms_txt + assert "**Tools**" in llms_txt + assert "- [Widgets](https://awesome-python.com/categories/widgets/)" in llms_txt + assert "- [Widgets](#widgets)" not in llms_txt + assert "### Widgets" in llms_txt + assert "- [w1](https://example.com) - A widget. (PyPI downloads/month: 777)" in llms_txt + assert "- [w2](https://github.com/owner/w2) - A starred widget. (GitHub stars: 42)" in llms_txt + assert llms_txt != readme + assert "# Contributing" not in llms_txt + + def test_build_cleans_stale_output(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ## Only + + - [x](https://x.com) - X. + + # Contributing + + Done. + """) + self._make_repo(tmp_path, readme) + + stale = tmp_path / "website" / "output" / "categories" / "stale" + stale.mkdir(parents=True) + (stale / "index.html").write_text("old", encoding="utf-8") + + build(tmp_path) + + assert not (tmp_path / "website" / "output" / "categories" / "stale").exists() + + def test_build_with_stars_sorts_by_stars(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ## Stuff + + - [low-stars](https://github.com/org/low) - Low. + - [high-stars](https://github.com/org/high) - High. + - [no-stars](https://example.com/none) - None. + + # Contributing + + Done. + """) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + + # Copy real templates + real_tpl = Path(__file__).parent / ".." / "templates" + tpl_dir = tmp_path / "website" / "templates" + shutil.copytree(real_tpl, tpl_dir) + + # Create mock star data + data_dir = tmp_path / "website" / "data" + data_dir.mkdir(parents=True) + stars = { + "org/high": {"stars": 5000, "owner": "org", "fetched_at": "2026-01-01T00:00:00+00:00"}, + "org/low": {"stars": 100, "owner": "org", "fetched_at": "2026-01-01T00:00:00+00:00"}, + } + (data_dir / "github_stars.json").write_text(json.dumps(stars), encoding="utf-8") + + build(tmp_path) + + html = (tmp_path / "website" / "output" / "index.html").read_text(encoding="utf-8") + # Star-sorted: high-stars (5000) before low-stars (100) before no-stars (None) + assert html.index("high-stars") < html.index("low-stars") + assert html.index("low-stars") < html.index("no-stars") + # Formatted star counts + assert "5,000" in html + assert "100" in html + # Expand content present + assert "expand-content" in html + + def test_build_with_downloads_renders_column(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ## Stuff + + - [My-Lib](https://github.com/org/mylib) - On PyPI. + - [no-pypi](https://example.com/none) - Not on PyPI. + - [asyncio](https://docs.python.org/3/library/asyncio.html) - Built-in. + + # Contributing + + Done. + """) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + self._copy_real_templates(tmp_path) + + data_dir = tmp_path / "website" / "data" + data_dir.mkdir(parents=True) + # Keyed by normalized README display name, like fetch_pypi_downloads_via_clickpy.py writes it + (data_dir / "pypi_downloads.tsv").write_text( + "name\tpackage\tdownloads\tfetched_at\nasyncio\tasyncio\t26305454\t2026-08-16\nmy-lib\tmy-lib\t1234567\t2026-08-16\n", + encoding="utf-8", + ) + + build(tmp_path) + + html = (tmp_path / "website" / "output" / "index.html").read_text(encoding="utf-8") + assert "1,234,567" in html + # Built-in entries never show PyPI counts: the asyncio row is the backport package + assert "26,305,454" not in html + # Default sort: entries with download counts come first + assert html.index("My-Lib") < html.index("no-pypi") + + def test_build_fails_when_group_and_category_slug_collide(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Widgets** + + ## Widgets + + - [w1](https://example.com) - W. + + # Contributing + + Done. + """) + self._make_repo(tmp_path, readme) + with pytest.raises(ValueError, match="slug collision"): + build(tmp_path) + + def test_index_contains_aligned_homepage_metadata(self, tmp_path): + readme = (Path(__file__).parents[2] / "README.md").read_text(encoding="utf-8") + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + self._copy_real_templates(tmp_path) + + build(tmp_path) + + parsed_groups = parse_readme(readme) + categories = [cat for group in parsed_groups for cat in group["categories"]] + entries = extract_entries(categories, parsed_groups) + html = (tmp_path / "website" / "output" / "index.html").read_text(encoding="utf-8") + parser = HeadMetadataParser() + parser.feed(html) + + expected_title = "Awesome Python" + expected_description = f"An opinionated guide to the best Python frameworks, libraries, and tools. Explore {len(entries)} curated projects across {len(categories)} categories, from AI and agents to data science and web development." + expected_url = "https://awesome-python.com/" + expected_image = "https://awesome-python.com/static/og-image.png" + + assert parser.title_count == 1 + assert parser.title.strip() == expected_title + assert parser.meta_by_name["description"] == expected_description + assert parser.links_by_rel["canonical"] == expected_url + assert parser.meta_by_property["og:type"] == "website" + assert parser.meta_by_property["og:title"] == expected_title + assert parser.meta_by_property["og:description"] == expected_description + assert parser.meta_by_property["og:image"] == expected_image + assert parser.meta_by_property["og:url"] == expected_url + assert parser.meta_by_name["twitter:card"] == "summary_large_image" + assert parser.meta_by_name["twitter:title"] == expected_title + assert parser.meta_by_name["twitter:description"] == expected_description + assert parser.meta_by_name["twitter:image"] == expected_image + assert "\n Sponsorship' in html + assert 'id="hero-category-heading">Browse by category' in html + assert 'class="hero-category-link" href="/categories/ai-and-agents/"' in html + + def test_index_contains_homepage_json_ld(self, tmp_path): + readme = (Path(__file__).parents[2] / "README.md").read_text(encoding="utf-8") + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + self._copy_real_templates(tmp_path) + + build(tmp_path) + + parsed_groups = parse_readme(readme) + categories = [cat for group in parsed_groups for cat in group["categories"]] + entries = extract_entries(categories, parsed_groups) + html = (tmp_path / "website" / "output" / "index.html").read_text(encoding="utf-8") + + marker = '", start) + block = html[start:end] + assert "" not in block + data = json.loads(block) + + assert data["@context"] == "https://schema.org" + graph = {node["@type"]: node for node in data["@graph"]} + assert set(graph) == {"WebSite", "CollectionPage"} + assert graph["WebSite"]["url"] == "https://awesome-python.com/" + assert graph["WebSite"]["name"] == "Awesome Python" + assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website" + + collection = graph["CollectionPage"] + assert collection["@id"] == "https://awesome-python.com/" + assert collection["url"] == "https://awesome-python.com/" + assert collection["isPartOf"] == {"@type": "WebSite", "@id": graph["WebSite"]["@id"]} + expected_description = f"An opinionated guide to the best Python frameworks, libraries, and tools. Explore {len(entries)} curated projects across {len(categories)} categories, from AI and agents to data science and web development." + assert collection["description"] == expected_description + + item_list = collection["mainEntity"] + assert item_list["@type"] == "ItemList" + assert item_list["numberOfItems"] == len(entries) + assert len(item_list["itemListElement"]) == len(entries) + + positions = [item["position"] for item in item_list["itemListElement"]] + assert positions == list(range(1, len(entries) + 1)) + assert all(item["@type"] == "ListItem" for item in item_list["itemListElement"]) + assert all(item["url"].startswith(("http://", "https://")) for item in item_list["itemListElement"]) + + rendered_names = {item["name"] for item in item_list["itemListElement"]} + rendered_urls = {item["url"] for item in item_list["itemListElement"]} + assert rendered_names == {e["name"] for e in entries} + assert rendered_urls == {e["url"] for e in entries} + + def test_category_page_contains_json_ld(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + ## Projects + + **Tools** + + ## Widgets + + _Widget libraries._ + + - [w1](https://example.com/w1) - A widget. + - [w2](https://github.com/owner/w2) - A starred widget. + + # Contributing + + Help! + """) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + self._copy_real_templates(tmp_path) + build(tmp_path) + + category_html = (tmp_path / "website" / "output" / "categories" / "widgets" / "index.html").read_text(encoding="utf-8") + marker = '", start) + block = category_html[start:end] + assert "" not in block + data = json.loads(block) + + assert data["@context"] == "https://schema.org" + graph = {node["@type"]: node for node in data["@graph"]} + assert set(graph) == {"WebSite", "CollectionPage", "BreadcrumbList"} + assert graph["WebSite"]["@id"] == "https://awesome-python.com/#website" + collection = graph["CollectionPage"] + assert collection["name"] == "Widgets Python Libraries" + assert collection["@id"] == "https://awesome-python.com/categories/widgets/" + assert collection["url"] == "https://awesome-python.com/categories/widgets/" + assert collection["description"] == "Widget libraries. Explore 2 curated Python projects in Widgets." + assert collection["isPartOf"] == {"@type": "WebSite", "@id": "https://awesome-python.com/#website"} + + item_list = collection["mainEntity"] + assert item_list["@type"] == "ItemList" + assert item_list["numberOfItems"] == 2 + names = {item["name"] for item in item_list["itemListElement"]} + urls = {item["url"] for item in item_list["itemListElement"]} + assert names == {"w1", "w2"} + assert urls == {"https://example.com/w1", "https://github.com/owner/w2"} + positions = sorted(item["position"] for item in item_list["itemListElement"]) + assert positions == [1, 2] + + breadcrumbs = graph["BreadcrumbList"]["itemListElement"] + assert breadcrumbs == [ + {"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"}, + {"@type": "ListItem", "position": 2, "name": "Widgets", "item": "https://awesome-python.com/categories/widgets/"}, + ] + + def test_group_page_falls_back_to_default_description_in_json_ld(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **AI & ML** + + ## Deep Learning + + - [dl1](https://example.com/dl1) - DL. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + group_html = (tmp_path / "website" / "output" / "categories" / "ai-ml" / "index.html").read_text(encoding="utf-8") + marker = '", start) + data = json.loads(group_html[start:end]) + + graph = {node["@type"]: node for node in data["@graph"]} + collection = graph["CollectionPage"] + assert collection["name"] == "AI & ML Python Libraries" + assert collection["@id"] == "https://awesome-python.com/categories/ai-ml/" + assert collection["url"] == "https://awesome-python.com/categories/ai-ml/" + assert collection["description"] == "Explore 1 curated Python projects in AI & ML. Part of the Awesome Python catalog." + + def test_build_creates_subcategory_pages(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Web** + + ## Web Frameworks + + - Synchronous + + - [django](https://example.com/django) - Sync framework. + + - Asynchronous + + - [fastapi](https://example.com/fastapi) - Async framework. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + sync = (site / "categories" / "web-frameworks" / "synchronous" / "index.html").read_text(encoding="utf-8") + async_ = (site / "categories" / "web-frameworks" / "asynchronous" / "index.html").read_text(encoding="utf-8") + + assert "django" in sync + assert "fastapi" not in sync + assert "fastapi" in async_ + assert "django" not in async_ + + def test_subcategory_page_shows_breadcrumb(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Web** + + ## Web Frameworks + + - Synchronous + + - [django](https://example.com/django) - Sync. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + sync = (site / "categories" / "web-frameworks" / "synchronous" / "index.html").read_text(encoding="utf-8") + assert 'href="/categories/web-frameworks/"' in sync + assert "Web Frameworks" in sync + assert "

Synchronous

" in sync + assert "category-breadcrumb" in sync + + parser = HeadMetadataParser() + parser.feed(sync) + assert parser.title.strip() == "Synchronous for Web Frameworks - Awesome Python" + assert parser.meta_by_name["description"] == "Explore 1 curated Python projects in Synchronous for Web Frameworks. Part of the Awesome Python catalog." + + marker = '", start) + graph = {node["@type"]: node for node in json.loads(sync[start:end])["@graph"]} + assert graph["CollectionPage"]["name"] == "Synchronous for Web Frameworks" + assert graph["BreadcrumbList"]["itemListElement"] == [ + {"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"}, + { + "@type": "ListItem", + "position": 2, + "name": "Web Frameworks", + "item": "https://awesome-python.com/categories/web-frameworks/", + }, + { + "@type": "ListItem", + "position": 3, + "name": "Synchronous", + "item": "https://awesome-python.com/categories/web-frameworks/synchronous/", + }, + ] + + parent = (site / "categories" / "web-frameworks" / "index.html").read_text(encoding="utf-8") + assert "category-breadcrumb" not in parent + + def test_sponsorship_page_contains_json_ld(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Tools** + + ## Widgets + + - [w1](https://example.com/w1) - A widget. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + html = (site / "sponsorship" / "index.html").read_text(encoding="utf-8") + parser = HeadMetadataParser() + parser.feed(html) + + assert parser.title.strip() == "Sponsor Awesome Python" + assert parser.meta_by_name["description"] == ( + "Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use." + ) + assert parser.links_by_rel["canonical"] == "https://awesome-python.com/sponsorship/" + assert 'Sponsorship' in html + + marker = '", start) + graph = {node["@type"]: node for node in json.loads(html[start:end])["@graph"]} + + assert set(graph) == {"WebSite", "WebPage", "BreadcrumbList"} + assert graph["WebPage"]["@id"] == "https://awesome-python.com/sponsorship/" + assert graph["WebPage"]["url"] == "https://awesome-python.com/sponsorship/" + assert graph["BreadcrumbList"]["itemListElement"] == [ + {"@type": "ListItem", "position": 1, "name": "Awesome Python", "item": "https://awesome-python.com/"}, + {"@type": "ListItem", "position": 2, "name": "Sponsorship", "item": "https://awesome-python.com/sponsorship/"}, + ] + + def test_index_embeds_filter_urls_json(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **AI & ML** + + ## Deep Learning + + - [dl1](https://example.com/dl1) - DL. + + ## Machine Learning + + - Classical + + - [ml1](https://example.com/ml1) - ML. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + index_html = (site / "index.html").read_text(encoding="utf-8") + + marker = '", start) + data = json.loads(index_html[start:end]) + + assert data["Deep Learning"] == "/categories/deep-learning/" + assert data["Machine Learning"] == "/categories/machine-learning/" + assert data["AI & ML"] == "/categories/ai-ml/" + assert data["Machine Learning > Classical"] == "/categories/machine-learning/classical/" + + def test_filter_urls_json_escapes_closing_script_tag(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ## Sneaky + + - [a](https://example.com) - A. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + index_html = (site / "index.html").read_text(encoding="utf-8") + + marker = '", start) + block = index_html[start:end] + assert "" not in block + data = json.loads(block) + assert any("Sneaky" in key for key in data) + + def test_build_creates_group_pages(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **AI & ML** + + ## Deep Learning + + - [dl1](https://example.com/dl1) - DL. + + ## Machine Learning + + - [ml1](https://example.com/ml1) - ML. + + **Web Development** + + ## Web Frameworks + + - [wf1](https://example.com/wf1) - WF. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + ai_ml = (site / "categories" / "ai-ml" / "index.html").read_text(encoding="utf-8") + web_dev = (site / "categories" / "web-development" / "index.html").read_text(encoding="utf-8") + + assert "dl1" in ai_ml + assert "ml1" in ai_ml + assert "wf1" not in ai_ml + assert "wf1" in web_dev + assert "dl1" not in web_dev + + def test_tag_buttons_have_data_url(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **AI & ML** + + ## Deep Learning + + - Vision + + - [v1](https://example.com/v1) - Vision lib. + + # Contributing + + Done. + """) + self._copy_real_templates(tmp_path) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + build(tmp_path) + + site = tmp_path / "website" / "output" + index_html = (site / "index.html").read_text(encoding="utf-8") + + assert 'data-value="Deep Learning"' in index_html + assert 'data-url="/categories/deep-learning/"' in index_html + assert 'data-value="AI & ML"' in index_html or 'data-value="AI & ML"' in index_html + assert 'data-url="/categories/ai-ml/"' in index_html + assert 'data-url="/categories/deep-learning/vision/"' in index_html + + +# --------------------------------------------------------------------------- +# extract_github_repo +# --------------------------------------------------------------------------- + + +class TestExtractGithubRepo: + def test_github_url(self): + assert extract_github_repo("https://github.com/psf/requests") == "psf/requests" + + def test_non_github_url(self): + assert extract_github_repo("https://foss.heptapod.net/pypy/pypy") is None + + def test_github_io_url(self): + assert extract_github_repo("https://user.github.io/proj") is None + + def test_trailing_slash(self): + assert extract_github_repo("https://github.com/org/repo/") == "org/repo" + + def test_deep_path(self): + assert extract_github_repo("https://github.com/org/repo/tree/main") is None + + def test_dot_git_suffix(self): + assert extract_github_repo("https://github.com/org/repo.git") == "org/repo" + + def test_org_only(self): + assert extract_github_repo("https://github.com/org") is None + + +# --------------------------------------------------------------------------- +# load_stars +# --------------------------------------------------------------------------- + + +class TestLoadStars: + def test_returns_empty_when_missing(self, tmp_path): + result = load_stars(tmp_path / "nonexistent.json") + assert result == {} + + def test_loads_valid_json(self, tmp_path): + data = {"psf/requests": {"stars": 52467, "owner": "psf", "fetched_at": "2026-01-01T00:00:00+00:00"}} + f = tmp_path / "stars.json" + f.write_text(json.dumps(data), encoding="utf-8") + result = load_stars(f) + assert result["psf/requests"]["stars"] == 52467 + + def test_returns_empty_on_corrupt_json(self, tmp_path): + f = tmp_path / "stars.json" + f.write_text("not json", encoding="utf-8") + result = load_stars(f) + assert result == {} + + +# --------------------------------------------------------------------------- +# sort_entries +# --------------------------------------------------------------------------- + + +def _template_entry(name: str, stars: int | None, source_type: str | None = None) -> TemplateEntry: + return TemplateEntry( + name=name, + url="", + description="", + categories=[], + groups=[], + subcategories=[], + stars=stars, + downloads=None, + owner=None, + last_commit_at=None, + source_type=source_type, + also_see=[], + ) + + +class TestSortEntries: + def test_sorts_by_stars_descending(self): + entries = [ + _template_entry("a", 100), + _template_entry("b", 500), + _template_entry("c", 200), + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["b", "c", "a"] + + def test_equal_stars_sorted_alphabetically(self): + entries = [ + _template_entry("beta", 100), + _template_entry("alpha", 100), + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["alpha", "beta"] + + def test_no_stars_go_to_bottom(self): + entries = [ + _template_entry("no-stars", None), + _template_entry("has-stars", 50), + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["has-stars", "no-stars"] + + def test_no_stars_sorted_alphabetically(self): + entries = [ + _template_entry("zebra", None), + _template_entry("apple", None), + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["apple", "zebra"] + + def test_builtin_between_starred_and_unstarred(self): + entries = [ + _template_entry("builtin", None, "Built-in"), + _template_entry("starred", 100), + _template_entry("unstarred", None), + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["starred", "builtin", "unstarred"] + + +# --------------------------------------------------------------------------- +# detect_source_type +# --------------------------------------------------------------------------- + + +class TestDetectSourceType: + def test_github_repo_returns_none(self): + assert detect_source_type("https://github.com/psf/requests") is None + + def test_stdlib_url(self): + assert detect_source_type("https://docs.python.org/3/library/asyncio.html") == "Built-in" + + def test_gitlab_url(self): + assert detect_source_type("https://gitlab.com/org/repo") == "GitLab" + + def test_bitbucket_url(self): + assert detect_source_type("https://bitbucket.org/org/repo") == "Bitbucket" + + def test_non_github_external(self): + assert detect_source_type("https://example.com/tool") == "External" + + def test_github_non_repo_returns_none(self): + assert detect_source_type("https://github.com/org/repo/wiki") is None + + +# --------------------------------------------------------------------------- +# extract_entries +# --------------------------------------------------------------------------- + + +class TestExtractEntries: + def test_basic_extraction(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Tools** + + ## Widgets + + - [widget](https://example.com) - A widget. + + # Contributing + + Done. + """) + groups = parse_readme(readme) + categories = [c for g in groups for c in g["categories"]] + entries = extract_entries(categories, groups) + assert len(entries) == 1 + assert entries[0]["name"] == "widget" + assert entries[0]["categories"] == ["Widgets"] + assert entries[0]["groups"] == ["Tools"] + + def test_duplicate_entry_merged(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Tools** + + ## Alpha + + - [shared](https://example.com/shared) - Shared lib. + + ## Beta + + - [shared](https://example.com/shared) - Shared lib. + + # Contributing + + Done. + """) + groups = parse_readme(readme) + categories = [c for g in groups for c in g["categories"]] + entries = extract_entries(categories, groups) + shared = [e for e in entries if e["name"] == "shared"] + assert len(shared) == 1 + assert sorted(shared[0]["categories"]) == ["Alpha", "Beta"] + + def test_source_type_detected(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ## Stdlib + + - [asyncio](https://docs.python.org/3/library/asyncio.html) - Async I/O. + + # Contributing + + Done. + """) + groups = parse_readme(readme) + categories = [c for g in groups for c in g["categories"]] + entries = extract_entries(categories, groups) + assert entries[0]["source_type"] == "Built-in" + + def test_subcategory_includes_slug_and_url(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Tools** + + ## Web Frameworks + + - Synchronous + + - [django](https://example.com/django) - A framework. + + # Contributing + + Done. + """) + groups = parse_readme(readme) + categories = [c for g in groups for c in g["categories"]] + entries = extract_entries(categories, groups) + assert entries[0]["subcategories"] == [ + { + "name": "Synchronous", + "value": "Web Frameworks > Synchronous", + "slug": "synchronous", + "url": "/categories/web-frameworks/synchronous/", + }, + ] + + +# --------------------------------------------------------------------------- +# annotate_entries_with_stats +# --------------------------------------------------------------------------- + + +class TestAnnotateEntriesWithStats: + def test_appends_star_count_to_bullet(self): + markdown = "- [foo](https://github.com/owner/foo) - A foo.\n" + stars = {"owner/foo": {"stars": 123, "owner": "owner"}} + assert annotate_entries_with_stats(markdown, stars, {}) == ("- [foo](https://github.com/owner/foo) - A foo. (GitHub stars: 123)\n") + + def test_appends_downloads_by_display_name(self): + markdown = "- [Foo.py](https://example.com) - A foo.\n" + assert annotate_entries_with_stats(markdown, {}, {"foo-py": 777}) == ("- [Foo.py](https://example.com) - A foo. (PyPI downloads/month: 777)\n") + + def test_appends_downloads_and_stars_together(self): + markdown = "- [foo](https://github.com/owner/foo) - A foo.\n" + stars = {"owner/foo": {"stars": 123, "owner": "owner"}} + assert annotate_entries_with_stats(markdown, stars, {"foo": 777}) == ("- [foo](https://github.com/owner/foo) - A foo. (PyPI downloads/month: 777, GitHub stars: 123)\n") + + def test_uses_first_github_link(self): + markdown = "- [foo](https://github.com/owner/foo) - A foo. Also [bar](https://github.com/owner/bar).\n" + stars = { + "owner/foo": {"stars": 10, "owner": "owner"}, + "owner/bar": {"stars": 99, "owner": "owner"}, + } + assert annotate_entries_with_stats(markdown, stars, {}) == ("- [foo](https://github.com/owner/foo) - A foo. Also [bar](https://github.com/owner/bar). (GitHub stars: 10)\n") + + def test_skips_entries_without_data(self): + markdown = "- [foo](https://github.com/owner/foo) - A foo.\n" + assert annotate_entries_with_stats(markdown, {}, {}) == markdown + + def test_skips_non_github_links_for_stars(self): + markdown = "- [foo](https://example.com) - A foo.\n" + stars = {"owner/foo": {"stars": 1, "owner": "owner"}} + assert annotate_entries_with_stats(markdown, stars, {}) == markdown + + def test_skips_non_bullet_lines(self): + markdown = "See [foo](https://github.com/owner/foo) for details.\n" + stars = {"owner/foo": {"stars": 1, "owner": "owner"}} + assert annotate_entries_with_stats(markdown, stars, {"foo": 5}) == markdown + + def test_handles_indented_bullets(self): + markdown = " - [foo](https://github.com/owner/foo)\n" + stars = {"owner/foo": {"stars": 7, "owner": "owner"}} + assert annotate_entries_with_stats(markdown, stars, {}) == (" - [foo](https://github.com/owner/foo) (GitHub stars: 7)\n") + + def test_preserves_lines_without_trailing_newline(self): + markdown = "- [foo](https://github.com/owner/foo) - A foo." + stars = {"owner/foo": {"stars": 5, "owner": "owner"}} + assert annotate_entries_with_stats(markdown, stars, {}) == ("- [foo](https://github.com/owner/foo) - A foo. (GitHub stars: 5)") + + +class TestLoadDownloads: + def test_parses_tsv_and_skips_not_found(self, tmp_path): + tsv = tmp_path / "pypi_downloads.tsv" + tsv.write_text( + "name\tpackage\tdownloads\tfetched_at\naiohttp\taiohttp\t649105404\t2026-08-16\npytorch\ttorch\t50000000\t2026-08-16\ndead-pkg\t-\tNOT_FOUND\t2026-08-16\n", + encoding="utf-8", + ) + assert load_downloads(tsv) == {"aiohttp": 649105404, "pytorch": 50000000} + + def test_missing_file_returns_empty(self, tmp_path): + assert load_downloads(tmp_path / "nope.tsv") == {} diff --git a/website/tests/test_fetch_github_stars.py b/website/tests/test_fetch_github_stars.py new file mode 100644 index 0000000000..6f0297b658 --- /dev/null +++ b/website/tests/test_fetch_github_stars.py @@ -0,0 +1,266 @@ +"""Tests for fetch_github_stars module.""" + +import json + +from fetch_github_stars import ( + build_graphql_query, + extract_github_repos, + parse_graphql_response, + save_cache, +) + + +class TestExtractGithubRepos: + def test_extracts_owner_repo_from_github_url(self): + readme = "* [requests](https://github.com/psf/requests) - HTTP lib." + result = extract_github_repos(readme) + assert result == {"psf/requests"} + + def test_multiple_repos(self): + readme = "* [requests](https://github.com/psf/requests) - HTTP.\n* [flask](https://github.com/pallets/flask) - Micro." + result = extract_github_repos(readme) + assert result == {"psf/requests", "pallets/flask"} + + def test_deduplicates(self): + readme = "* [a](https://github.com/org/repo) - A.\n* [b](https://github.com/org/repo) - B." + result = extract_github_repos(readme) + assert result == {"org/repo"} + + def test_strips_fragment(self): + readme = "* [lib](https://github.com/org/repo#section) - Lib." + result = extract_github_repos(readme) + assert result == {"org/repo"} + + +class TestSaveCache: + def test_creates_directory_and_writes_json(self, tmp_path, monkeypatch): + data_dir = tmp_path / "data" + cache_file = data_dir / "stars.json" + monkeypatch.setattr("fetch_github_stars.DATA_DIR", data_dir) + monkeypatch.setattr("fetch_github_stars.CACHE_FILE", cache_file) + save_cache({"a/b": {"stars": 1}}) + assert cache_file.exists() + assert json.loads(cache_file.read_text(encoding="utf-8")) == {"a/b": {"stars": 1}} + + +class TestBuildGraphqlQuery: + def test_single_repo(self): + query = build_graphql_query(["psf/requests"]) + assert "repository" in query + assert 'owner: "psf"' in query + assert 'name: "requests"' in query + assert "stargazerCount" in query + + def test_multiple_repos_use_aliases(self): + query = build_graphql_query(["psf/requests", "pallets/flask"]) + assert "repo_0:" in query + assert "repo_1:" in query + + def test_empty_list(self): + query = build_graphql_query([]) + assert query == "" + + def test_skips_repos_with_quotes_in_name(self): + query = build_graphql_query(['org/"bad"']) + assert query == "" + + def test_skips_only_bad_repos(self): + query = build_graphql_query(["good/repo", 'bad/"repo"']) + assert "good" in query + assert "bad" not in query + + def test_skips_graphql_injection_in_owner(self): + query = build_graphql_query(['org"){evil}/repo']) + assert query == "" + + def test_skips_graphql_injection_in_name(self): + query = build_graphql_query(['org/repo"){evil}']) + assert query == "" + + def test_skips_owner_starting_with_hyphen(self): + query = build_graphql_query(["-bad/repo"]) + assert query == "" + + def test_skips_owner_starting_with_dot(self): + query = build_graphql_query([".bad/repo"]) + assert query == "" + + def test_skips_repo_starting_with_dot(self): + query = build_graphql_query(["org/.hidden"]) + assert query == "" + + def test_allows_repo_with_dots_and_underscores(self): + query = build_graphql_query(["org/my_repo.py"]) + assert 'name: "my_repo.py"' in query + + def test_allows_hyphenated_owner(self): + query = build_graphql_query(["my-org/repo"]) + assert 'owner: "my-org"' in query + + def test_skips_owner_with_underscore(self): + query = build_graphql_query(["bad_owner/repo"]) + assert query == "" + + +class TestParseGraphqlResponse: + def test_parses_star_count_and_owner(self): + data = { + "repo_0": { + "stargazerCount": 52467, + "owner": {"login": "psf"}, + } + } + repos = ["psf/requests"] + result = parse_graphql_response(data, repos) + assert result["psf/requests"]["stars"] == 52467 + assert result["psf/requests"]["owner"] == "psf" + + def test_skips_null_repos(self): + data = {"repo_0": None} + repos = ["deleted/repo"] + result = parse_graphql_response(data, repos) + assert result == {} + + def test_handles_missing_owner(self): + data = {"repo_0": {"stargazerCount": 100}} + repos = ["org/repo"] + result = parse_graphql_response(data, repos) + assert result["org/repo"]["owner"] == "" + + def test_multiple_repos(self): + data = { + "repo_0": {"stargazerCount": 100, "owner": {"login": "a"}}, + "repo_1": {"stargazerCount": 200, "owner": {"login": "b"}}, + } + repos = ["a/x", "b/y"] + result = parse_graphql_response(data, repos) + assert len(result) == 2 + assert result["a/x"]["stars"] == 100 + assert result["b/y"]["stars"] == 200 + + def test_extracts_last_commit_at(self): + data = { + "repo_0": { + "stargazerCount": 100, + "owner": {"login": "org"}, + "defaultBranchRef": {"target": {"committedDate": "2025-06-01T00:00:00Z"}}, + } + } + repos = ["org/repo"] + result = parse_graphql_response(data, repos) + assert result["org/repo"]["last_commit_at"] == "2025-06-01T00:00:00Z" + + def test_missing_default_branch_ref(self): + data = {"repo_0": {"stargazerCount": 50, "owner": {"login": "org"}}} + repos = ["org/repo"] + result = parse_graphql_response(data, repos) + assert result["org/repo"]["last_commit_at"] == "" + + +class TestMainSkipsFreshCache: + """Verify that main() skips fetching when all cache entries are fresh.""" + + def test_skips_fetch_when_cache_is_fresh(self, tmp_path, monkeypatch, capsys): + from datetime import datetime, timedelta, timezone + + from fetch_github_stars import main + + # Set up a minimal README with one repo + readme = tmp_path / "README.md" + readme.write_text("* [req](https://github.com/psf/requests) - HTTP.\n") + monkeypatch.setattr("fetch_github_stars.README_PATH", readme) + + # Pre-populate cache with a fresh entry (1 hour ago) + data_dir = tmp_path / "data" + data_dir.mkdir() + cache_file = data_dir / "github_stars.json" + now = datetime.now(timezone.utc) + fresh_cache = { + "psf/requests": { + "stars": 52000, + "owner": "psf", + "last_commit_at": "2025-01-01T00:00:00+00:00", + "fetched_at": (now - timedelta(hours=1)).isoformat(), + }, + "vinta/awesome-python": { + "stars": 230000, + "owner": "vinta", + "last_commit_at": "2025-01-01T00:00:00+00:00", + "fetched_at": (now - timedelta(hours=1)).isoformat(), + }, + } + cache_file.write_text(json.dumps(fresh_cache), encoding="utf-8") + monkeypatch.setattr("fetch_github_stars.CACHE_FILE", cache_file) + monkeypatch.setattr("fetch_github_stars.DATA_DIR", data_dir) + monkeypatch.setenv("GITHUB_TOKEN", "fake-token") + + main() + + output = capsys.readouterr().out + assert "0 repos to fetch" in output + assert "Cache is up to date" in output + + def test_fetches_when_cache_is_stale(self, tmp_path, monkeypatch, capsys): + from datetime import datetime, timedelta, timezone + from unittest.mock import MagicMock + + from fetch_github_stars import main + + # Set up a minimal README with one repo + readme = tmp_path / "README.md" + readme.write_text("* [req](https://github.com/psf/requests) - HTTP.\n") + monkeypatch.setattr("fetch_github_stars.README_PATH", readme) + + # Pre-populate cache with a stale entry (24 hours ago) + data_dir = tmp_path / "data" + data_dir.mkdir() + cache_file = data_dir / "github_stars.json" + now = datetime.now(timezone.utc) + stale_cache = { + "psf/requests": { + "stars": 52000, + "owner": "psf", + "last_commit_at": "2025-01-01T00:00:00+00:00", + "fetched_at": (now - timedelta(hours=24)).isoformat(), + }, + "vinta/awesome-python": { + "stars": 230000, + "owner": "vinta", + "last_commit_at": "2025-01-01T00:00:00+00:00", + "fetched_at": (now - timedelta(hours=24)).isoformat(), + }, + } + cache_file.write_text(json.dumps(stale_cache), encoding="utf-8") + monkeypatch.setattr("fetch_github_stars.CACHE_FILE", cache_file) + monkeypatch.setattr("fetch_github_stars.DATA_DIR", data_dir) + monkeypatch.setenv("GITHUB_TOKEN", "fake-token") + + # Mock httpx.Client to avoid real API calls + mock_response = MagicMock() + mock_response.json.return_value = { + "data": { + "repo_0": { + "stargazerCount": 53000, + "owner": {"login": "psf"}, + "defaultBranchRef": {"target": {"committedDate": "2025-06-01T00:00:00Z"}}, + }, + "repo_1": { + "stargazerCount": 231000, + "owner": {"login": "vinta"}, + "defaultBranchRef": {"target": {"committedDate": "2025-06-01T00:00:00Z"}}, + }, + } + } + mock_response.raise_for_status = MagicMock() + mock_client = MagicMock() + mock_client.__enter__ = MagicMock(return_value=mock_client) + mock_client.__exit__ = MagicMock(return_value=False) + mock_client.post.return_value = mock_response + monkeypatch.setattr("fetch_github_stars.httpx.Client", lambda **kwargs: mock_client) + + main() + + output = capsys.readouterr().out + assert "2 repos to fetch" in output + assert "Done. Fetched 2 repos" in output + mock_client.post.assert_called_once() diff --git a/website/tests/test_readme_parser.py b/website/tests/test_readme_parser.py new file mode 100644 index 0000000000..4d0b7ae73f --- /dev/null +++ b/website/tests/test_readme_parser.py @@ -0,0 +1,521 @@ +"""Tests for the readme_parser module.""" + +import textwrap +from pathlib import Path + +import pytest + +from readme_parser import ( + _find_inline, + _parse_section_entries, + parse_readme, + render_inline_html, + render_inline_text, +) + +from markdown_it import MarkdownIt +from markdown_it.tree import SyntaxTreeNode + + +def _parse_inline(md_text: str) -> list[SyntaxTreeNode]: + """Helper: parse a single paragraph and return its inline children.""" + md = MarkdownIt("commonmark") + root = SyntaxTreeNode(md.parse(md_text)) + # root > paragraph > inline > children + return root.children[0].children[0].children + + +class TestRenderInlineHtml: + def test_plain_text_escapes_html(self): + children = _parse_inline("Hello & friends") + assert render_inline_html(children) == "Hello <world> & friends" + + def test_link_with_target(self): + children = _parse_inline("[name](https://example.com)") + html = render_inline_html(children) + assert 'href="https://example.com"' in html + assert 'target="_blank"' in html + assert 'rel="noopener"' in html + assert ">name" in html + + def test_emphasis(self): + children = _parse_inline("*italic* text") + assert "italic" in render_inline_html(children) + + def test_strong(self): + children = _parse_inline("**bold** text") + assert "bold" in render_inline_html(children) + + def test_code_inline(self): + children = _parse_inline("`some code`") + assert "some code" in render_inline_html(children) + + def test_mixed_link_and_text(self): + children = _parse_inline("See [foo](https://x.com) for details.") + html = render_inline_html(children) + assert "See " in html + assert ">foo" in html + assert " for details." in html + + +class TestRenderInlineText: + def test_plain_text(self): + children = _parse_inline("Hello world") + assert render_inline_text(children) == "Hello world" + + def test_link_becomes_text(self): + children = _parse_inline("See [awesome-algos](https://github.com/x/y).") + assert render_inline_text(children) == "See awesome-algos." + + def test_emphasis_stripped(self): + children = _parse_inline("*italic* text") + assert render_inline_text(children) == "italic text" + + def test_code_inline_kept(self): + children = _parse_inline("`code` here") + assert render_inline_text(children) == "code here" + + +MINIMAL_README = textwrap.dedent("""\ + # Awesome Python + + Some intro text. + + ## Projects + + ### Alpha + + _Libraries for alpha stuff._ + + - [lib-a](https://example.com/a) - Does A. + - [lib-b](https://example.com/b) - Does B. + + ### Beta + + _Tools for beta._ + + - [lib-c](https://example.com/c) - Does C. + + ## Resources + + Where to discover resources. + + ### Newsletters + + - [News One](https://example.com/n1) + - [News Two](https://example.com/n2) + + ### Podcasts + + - [Pod One](https://example.com/p1) + + ## Contributing + + Please contribute! +""") + + +GROUPED_README = textwrap.dedent("""\ + # Awesome Python + + Some intro text. + + ## Projects + + **Group One** + + ### Alpha + + _Libraries for alpha stuff._ + + - [lib-a](https://example.com/a) - Does A. + - [lib-b](https://example.com/b) - Does B. + + **Group Two** + + ### Beta + + _Tools for beta._ + + - [lib-c](https://example.com/c) - Does C. + + ### Gamma + + - [lib-d](https://example.com/d) - Does D. + + ## Resources + + Where to discover resources. + + ### Newsletters + + - [News One](https://example.com/n1) + + ## Contributing + + Please contribute! +""") + + +class TestParseReadmeSections: + def test_ungrouped_categories_go_to_other(self): + groups = parse_readme(MINIMAL_README) + assert len(groups) == 1 + assert groups[0]["name"] == "Other" + assert len(groups[0]["categories"]) == 2 + + def test_ungrouped_category_names(self): + groups = parse_readme(MINIMAL_README) + cats = groups[0]["categories"] + assert cats[0]["name"] == "Alpha" + assert cats[1]["name"] == "Beta" + + def test_category_slugs(self): + groups = parse_readme(MINIMAL_README) + cats = groups[0]["categories"] + assert cats[0]["slug"] == "alpha" + assert cats[1]["slug"] == "beta" + + def test_category_description(self): + groups = parse_readme(MINIMAL_README) + cats = groups[0]["categories"] + assert cats[0]["description"] == "Libraries for alpha stuff." + assert cats[0]["description_html"] == "Libraries for alpha stuff." + assert cats[1]["description"] == "Tools for beta." + assert cats[1]["description_html"] == "Tools for beta." + + def test_contributing_skipped(self): + groups = parse_readme(MINIMAL_README) + all_names = [] + for g in groups: + all_names.extend(c["name"] for c in g["categories"]) + assert "Contributing" not in all_names + + def test_no_projects_heading(self): + groups = parse_readme("# Just a heading\n\nSome text.\n") + assert groups == [] + + def test_no_description(self): + readme = textwrap.dedent("""\ + # Title + + ## Projects + + ### NullDesc + + - [item](https://x.com) - Thing. + + ## Resources + + ### Tips + + - [tip](https://x.com) + + ## Contributing + + Done. + """) + groups = parse_readme(readme) + cats = groups[0]["categories"] + assert cats[0]["description"] == "" + assert cats[0]["description_html"] == "" + assert cats[0]["entries"][0]["name"] == "item" + + def test_description_with_link_stripped(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ### Algos + + _Algorithms. Also see [awesome-algos](https://example.com)._ + + - [lib](https://x.com) - Lib. + + ## Contributing + + Done. + """) + groups = parse_readme(readme) + cats = groups[0]["categories"] + assert cats[0]["description"] == "Algorithms. Also see awesome-algos." + assert cats[0]["description_html"] == 'Algorithms. Also see awesome-algos.' + + +class TestParseGroupedReadme: + def test_group_count(self): + groups = parse_readme(GROUPED_README) + assert len(groups) == 2 + + def test_group_names(self): + groups = parse_readme(GROUPED_README) + assert groups[0]["name"] == "Group One" + assert groups[1]["name"] == "Group Two" + + def test_group_slugs(self): + groups = parse_readme(GROUPED_README) + assert groups[0]["slug"] == "group-one" + assert groups[1]["slug"] == "group-two" + + def test_group_one_has_one_category(self): + groups = parse_readme(GROUPED_README) + assert len(groups[0]["categories"]) == 1 + assert groups[0]["categories"][0]["name"] == "Alpha" + + def test_group_two_has_two_categories(self): + groups = parse_readme(GROUPED_README) + assert len(groups[1]["categories"]) == 2 + assert groups[1]["categories"][0]["name"] == "Beta" + assert groups[1]["categories"][1]["name"] == "Gamma" + + def test_empty_group_skipped(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Empty** + + **HasCats** + + ### Cat + + - [x](https://x.com) - X. + + ## Contributing + + Done. + """) + groups = parse_readme(readme) + assert len(groups) == 1 + assert groups[0]["name"] == "HasCats" + + def test_bold_with_extra_text_not_group_marker(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + **Note:** This is not a group marker. + + ### Cat + + - [x](https://x.com) - X. + + ## Contributing + + Done. + """) + groups = parse_readme(readme) + # "Note:" has text after the strong node, so it's not a group marker + # Category goes into "Other" + assert len(groups) == 1 + assert groups[0]["name"] == "Other" + + def test_categories_before_any_group_marker(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ### Orphan + + - [x](https://x.com) - X. + + **A Group** + + ### Grouped + + - [y](https://x.com) - Y. + + ## Contributing + + Done. + """) + groups = parse_readme(readme) + assert len(groups) == 2 + assert groups[0]["name"] == "Other" + assert groups[0]["categories"][0]["name"] == "Orphan" + assert groups[1]["name"] == "A Group" + assert groups[1]["categories"][0]["name"] == "Grouped" + + +def _content_nodes(md_text: str) -> list[SyntaxTreeNode]: + """Helper: parse markdown and return all block nodes.""" + md = MarkdownIt("commonmark") + root = SyntaxTreeNode(md.parse(md_text)) + return root.children + + +class TestParseSectionEntries: + def test_flat_entries(self): + nodes = _content_nodes("- [django](https://example.com/d) - A web framework.\n- [flask](https://example.com/f) - A micro framework.\n") + entries = _parse_section_entries(nodes) + assert len(entries) == 2 + assert entries[0]["name"] == "django" + assert entries[0]["url"] == "https://example.com/d" + assert "web framework" in entries[0]["description"] + assert entries[0]["also_see"] == [] + assert entries[1]["name"] == "flask" + + def test_link_only_entry(self): + nodes = _content_nodes("- [tool](https://x.com)\n") + entries = _parse_section_entries(nodes) + assert len(entries) == 1 + assert entries[0]["name"] == "tool" + assert entries[0]["description"] == "" + + def test_subcategorized_entries(self): + nodes = _content_nodes("- Algorithms\n - [algos](https://x.com/a) - Algo lib.\n - [sorts](https://x.com/s) - Sort lib.\n- Design Patterns\n - [patterns](https://x.com/p) - Pattern lib.\n") + entries = _parse_section_entries(nodes) + assert len(entries) == 3 + assert entries[0]["name"] == "algos" + assert entries[2]["name"] == "patterns" + + def test_text_before_link_is_subcategory(self): + nodes = _content_nodes( + "- MySQL - [awesome-mysql](http://example.com/awesome-mysql/)\n" + " - [mysqlclient](https://example.com/mysqlclient) - MySQL connector.\n" + " - [pymysql](https://example.com/pymysql) - Pure Python MySQL driver.\n" + ) + entries = _parse_section_entries(nodes) + # awesome-mysql is a subcategory label, not an entry + assert len(entries) == 2 + names = [e["name"] for e in entries] + assert "awesome-mysql" not in names + assert "mysqlclient" in names + assert "pymysql" in names + + def test_also_see_sub_entries(self): + nodes = _content_nodes( + "- [asyncio](https://docs.python.org/3/library/asyncio.html) - Async I/O.\n" + " - [awesome-asyncio](https://github.com/timofurrer/awesome-asyncio)\n" + "- [trio](https://github.com/python-trio/trio) - Friendly async.\n" + ) + entries = _parse_section_entries(nodes) + assert len(entries) == 2 + assert entries[0]["name"] == "asyncio" + assert len(entries[0]["also_see"]) == 1 + assert entries[0]["also_see"][0]["name"] == "awesome-asyncio" + assert entries[1]["name"] == "trio" + assert entries[1]["also_see"] == [] + + def test_entry_count_includes_also_see(self): + readme = textwrap.dedent("""\ + # T + + ## Projects + + ### Async + + - [asyncio](https://x.com) - Async I/O. + - [awesome-asyncio](https://y.com) + - [trio](https://z.com) - Friendly async. + + ## Contributing + + Done. + """) + groups = parse_readme(readme) + cats = groups[0]["categories"] + # 2 main entries + 1 also_see = 3 + assert cats[0]["entry_count"] == 3 + + def test_description_html_escapes_xss(self): + nodes = _content_nodes("- [lib](https://x.com) - A lib.\n") + entries = _parse_section_entries(nodes) + assert "