Merge branch 'master' into add-promptise-foundry

This commit is contained in:
cryxnet 2026-05-04 21:38:37 +02:00 committed by GitHub
commit 2547c1dcfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 2984 additions and 563 deletions

14
.gitignore vendored
View file

@ -10,12 +10,12 @@ __pycache__/
website/output/
website/data/
# claude code
.claude/skills/
.gstack/
.playwright-cli/
.superpowers/
skills-lock.json
# planning docs
docs/
# codex
# agents
.agents/
.claude/skills/
.superpowers/
.playwright-cli/
skills-lock.json

View file

@ -1,124 +0,0 @@
# Design Context
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.
## Users
Working Python developers (mid to senior). They already 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.
## Brand Personality
Three words: **opinionated, confident, dense**.
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.
## Aesthetic Direction
Stay close to the current direction. It works.
- Warm editorial palette in OKLCH. Cream/ivory page, dark earthy hero, warm brown-red accent near `oklch(58% 0.16 45)`.
- Type pairing: `Cormorant Garamond` (serif display, 600) with `Manrope` (sans body, 400/600/700/800). Do not swap.
- Magazine-cover scale for the main headline (`clamp(4.5rem, 11vw, 8.5rem)`), then a tight modular scale for the rest.
- Textured hero: subtle grid, slow sheen, warm radial gradients. Respect `prefers-reduced-motion`.
- Light theme only (`color-scheme: light`). No dark mode toggle, no alternate palettes.
- Table-driven index (sticky header, sortable columns, expandable rows). Not a card grid.
- Dark warm charcoal footer, part of the same system.
References (what to stay close to):
- **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.
Color 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.
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.
## Implementation Rules
The project already follows these. Future work must keep them.
Layout and sizing:
- Keep existing `--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 impeccable skill reference rule "cap line length at ~65-75ch" does NOT apply here. Ignore it. Readability at wide widths is carried by vertical rhythm, leading, and the modular type scale instead.
- If you believe a width cap is actually necessary for some specific element, ask first with a concrete reason before adding it.
- Body type floor is 16px (`--text-base: 1rem`). Content-heavy passages may go to 1.125rem.
- When in doubt about any type size, pick one step larger than what the impeccable skill's scale references suggest. The user has repeatedly corrected sizes upward (11+ separate requests across 8 sessions). Never reduce an existing size unprompted. Footer, meta rows, expand content, labels, headings all trend too small by default.
- Row numbers in the table: left-align, no leading zeros. The user tried zero-padding and rejected it.
- Adjacent heading levels differ by at least 0.25rem of rendered size.
Color:
- Use OKLCH for any new color. Not HSL, not hex.
- Accent colors (`--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-muted`, `--ink-soft`, or `--bg-paper-strong`. Never the accent. Users should not mistake static decoration for something clickable.
CSS hygiene:
- CSS custom properties for all colors and repeated values.
- `rem` for spacing and type. `px` only for borders and shadows.
- `gap` over child margins in flex and grid.
- Logical properties (`margin-inline`, `padding-block`) over physical (`margin-left`, `padding-top`).
- Never `!important`. Fix specificity instead.
- Never `text-transform`. Write the casing in the markup.
- Sibling components (card lists, grid items) share identical spacing.
Visual consistency check:
Before shipping any visual change, check peer elements. The user catches inconsistencies repeatedly.
- Hover and focus states: if one link type gets a hover treatment, peer links (hero topbar, footer, project names, sponsor names, expand-meta) share it.
- Tag variants (group, subcat, source, built-in) inherit the base `.tag` style and 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. No one-off inline `color: oklch(...)` buried in a rule.
Narrow-screen behavior:
The user actively tests `< 960px` and `< 680px`. Narrow screens must stay functional.
- Do not drop features that 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.
Absolute bans (from the impeccable skill):
- 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.
- No gradient text (`background-clip: text` on gradients). Solid color only.
- No glassmorphism as default decoration.
- No bounce or elastic easing. Real objects decelerate smoothly.
## 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.

223
DESIGN.md Normal file
View file

@ -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 `<code>` 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.

View file

@ -10,6 +10,15 @@ fetch_github_stars:
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

187
README.md
View file

@ -1,14 +1,14 @@
# Awesome Python
An opinionated list of Python frameworks, libraries, tools, and resources.
An opinionated guide to the best Python frameworks, libraries, tools, and resources.
# **Sponsors**
## **Sponsors**
- **[pyr](https://pyrun.dev)**: Zero-config Python project manager. Bootstraps its own runtime, app-convention, and working imports - out the box.
> The **#10 most-starred repo on GitHub**. Put your product in front of Python developers. [Become a sponsor](SPONSORSHIP.md).
# Categories
## Categories
**AI & ML**
@ -71,6 +71,7 @@ An opinionated list of Python frameworks, libraries, tools, and resources.
- [DevOps Tools](#devops-tools)
- [Distributed Computing](#distributed-computing)
- [Task Queues](#task-queues)
- [Messaging](#messaging)
- [Job Schedulers](#job-schedulers)
- [Logging](#logging)
- [Network Virtualization](#network-virtualization)
@ -114,18 +115,19 @@ An opinionated list of Python frameworks, libraries, tools, and resources.
- [Cryptography](#cryptography)
- [Penetration Testing](#penetration-testing)
- [Web Security](#web-security)
**Miscellaneous**
**Other**
- [Hardware](#hardware)
- [Microsoft Windows](#microsoft-windows)
- [Miscellaneous](#miscellaneous)
---
## Projects
**AI & ML**
## AI and Agents
### AI and Agents
_Libraries for building AI applications, LLM integrations, and autonomous agents._
@ -141,7 +143,9 @@ _Libraries for building AI applications, LLM integrations, and autonomous agents
- [dspy](https://github.com/stanfordnlp/dspy) - A framework for programming, not prompting, language models.
- [hermes-agent](https://github.com/nousresearch/hermes-agent) - An adaptive AI agent framework that grows with you.
- [langchain](https://github.com/langchain-ai/langchain) - Building applications with LLMs through composability.
- [promptise](https://github.com/promptise-com/foundry) - A framework for building MCP-native agents with an autonomous runtime, governance, memory, and guardrails.
- [promptise](https://github.com/promptise-com/foundry) - A framework for building fullstack agentic systems.
- [openai-agents](https://github.com/openai/openai-agents-python) - OpenAI's framework for building and managing AI agents.
- [OpenChronicle](https://github.com/Einsia/OpenChronicle) - Open-source, local-first memory for any tool-capable LLM agent.
- [pydantic-ai](https://github.com/pydantic/pydantic-ai) - A Python agent framework for building generative AI applications with structured schemas.
- [TradingAgents](https://github.com/TauricResearch/TradingAgents) - A multi-agents LLM financial trading framework.
- Data Layer
@ -160,7 +164,7 @@ _Libraries for building AI applications, LLM integrations, and autonomous agents
- [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.
- [voxcpm](https://github.com/OpenBMB/VoxCPM) - A tokenizer-free text-to-speech foundation model for multilingual speech generation and voice cloning.
## Deep Learning
### Deep Learning
_Frameworks for Neural Networks and Deep Learning. Also see [awesome-deep-learning](https://github.com/ChristosChristofidis/awesome-deep-learning)._
@ -171,7 +175,7 @@ _Frameworks for Neural Networks and Deep Learning. Also see [awesome-deep-learni
- [stable-baselines3](https://github.com/DLR-RM/stable-baselines3) - PyTorch implementations of Stable Baselines (deep) reinforcement learning algorithms.
- [tensorflow](https://github.com/tensorflow/tensorflow) - The most popular Deep Learning framework created by Google.
## Machine Learning
### Machine Learning
_Libraries for Machine Learning. Also see [awesome-machine-learning](https://github.com/josephmisiti/awesome-machine-learning#python)._
@ -187,7 +191,7 @@ _Libraries for Machine Learning. Also see [awesome-machine-learning](https://git
- [timesfm](https://github.com/google-research/timesfm) - A pretrained foundation model from Google Research for time-series forecasting.
- [xgboost](https://github.com/dmlc/xgboost) - A scalable, portable, and distributed gradient boosting library.
## Natural Language Processing
### Natural Language Processing
_Libraries for working with human languages._
@ -200,7 +204,7 @@ _Libraries for working with human languages._
- [funnlp](https://github.com/fighting41love/funNLP) - A collection of tools and datasets for Chinese NLP.
- [jieba](https://github.com/fxsjy/jieba) - The most popular Chinese text segmentation library.
## Computer Vision
### Computer Vision
_Libraries for Computer Vision._
@ -209,7 +213,7 @@ _Libraries for Computer Vision._
- [opencv](https://github.com/opencv/opencv-python) - Open Source Computer Vision Library.
- [pytesseract](https://github.com/madmaze/pytesseract) - A wrapper for [Google Tesseract OCR](https://github.com/tesseract-ocr).
## Recommender Systems
### Recommender Systems
_Libraries for building recommender systems._
@ -219,14 +223,14 @@ _Libraries for building recommender systems._
**Web Development**
## Web Frameworks
### Web Frameworks
_Traditional full stack web frameworks. Also see [Web APIs](#web-apis)._
- Synchronous
- [bottle](https://github.com/bottlepy/bottle) - A fast and simple micro-framework distributed as a single file with no dependencies.
- [django](https://github.com/django/django) - The most popular web framework in Python.
- [awesome-django](https://github.com/shahraizali/awesome-django)
- [awesome-django](https://github.com/wsvincent/awesome-django)
- [flask](https://github.com/pallets/flask) - A microframework for Python.
- [awesome-flask](https://github.com/humiaozuzu/awesome-flask)
- [pyramid](https://github.com/Pylons/pyramid) - A small, fast, down-to-earth, open source Python web framework.
@ -242,7 +246,7 @@ _Traditional full stack web frameworks. Also see [Web APIs](#web-apis)._
- [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.
## Web APIs
### Web APIs
_Libraries for building RESTful and GraphQL APIs._
@ -261,7 +265,7 @@ _Libraries for building RESTful and GraphQL APIs._
- [strawberry](https://github.com/strawberry-graphql/strawberry) - A GraphQL library that leverages Python type annotations for schema definition.
- [webargs](https://github.com/marshmallow-code/webargs) - A friendly library for parsing HTTP request arguments with built-in support for popular web frameworks.
## Web Servers
### Web Servers
_ASGI and WSGI compatible web servers._
@ -278,7 +282,7 @@ _ASGI and WSGI compatible web servers._
- [grpcio](https://github.com/grpc/grpc) - HTTP/2-based RPC framework with Python bindings, built by Google.
- [rpyc](https://github.com/tomerfiliba-org/rpyc) (Remote Python Call) - A transparent and symmetric RPC library for Python.
## WebSocket
### WebSocket
_Libraries for working with WebSocket._
@ -288,21 +292,21 @@ _Libraries for working with WebSocket._
- [picows](https://github.com/tarasko/picows) - Fastest WebSocket clients and servers with a frame level interface for the most demanding use-cases.
- [websockets](https://github.com/python-websockets/websockets) - A library for building WebSocket servers and clients with a focus on correctness and simplicity.
## Template Engines
### Template Engines
_Libraries and tools for templating and lexing._
- [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.
## Web Asset Management
### Web 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](https://github.com/jschneier/django-storages) - A collection of custom storage back ends for Django.
## Authentication
### Authentication
_Libraries for implementing authentication schemes._
@ -317,7 +321,7 @@ _Libraries for implementing authentication schemes._
- [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.
## Admin Panels
### Admin Panels
_Libraries for administrative interfaces._
@ -329,7 +333,7 @@ _Libraries for administrative interfaces._
- [func-to-web](https://github.com/offerrall/FuncToWeb) - Instantly create web UIs from Python functions using type hints. Zero frontend code required.
- [jet-bridge](https://github.com/jet-admin/jet-bridge) - Admin panel framework for any application with nice UI (ex Jet Django).
## CMS
### CMS
_Content Management Systems._
@ -337,7 +341,7 @@ _Content Management Systems._
- [indico](https://github.com/indico/indico) - A feature-rich event management system, made @ [CERN](https://en.wikipedia.org/wiki/CERN).
- [wagtail](https://github.com/wagtail/wagtail) - A Django content management system.
## Static Site Generators
### Static Site Generators
_Static site generator is a software that takes some text + templates as input and produces HTML files on the output._
@ -347,7 +351,7 @@ _Static site generator is a software that takes some text + templates as input a
**HTTP & Scraping**
## HTTP Clients
### HTTP Clients
_Libraries for working with HTTP._
@ -358,7 +362,7 @@ _Libraries for working with HTTP._
- [requests](https://github.com/psf/requests) - HTTP Requests for Humans.
- [urllib3](https://github.com/urllib3/urllib3) - A HTTP library with thread-safe connection pooling, file post support, sanity friendly.
## Web Scraping
### Web Scraping
_Libraries to automate web scraping and extract web content._
@ -374,7 +378,7 @@ _Libraries to automate web scraping and extract web content._
- [sumy](https://github.com/miso-belica/sumy) - A module for automatic summarization of text documents and HTML pages.
- [trafilatura](https://github.com/adbar/trafilatura) - A tool for gathering text and metadata from the web, with built-in content filtering.
## Email
### Email
_Libraries for sending and parsing email, and mail server management._
@ -383,7 +387,7 @@ _Libraries for sending and parsing email, and mail server management._
**Database & Storage**
## ORM
### ORM
_Libraries that implement Object-Relational Mapping or data mapping techniques._
@ -401,7 +405,7 @@ _Libraries that implement Object-Relational Mapping or data mapping techniques._
- [mongoengine](https://github.com/MongoEngine/mongoengine) - A Python Object-Document-Mapper for working with MongoDB.
- [pynamodb](https://github.com/pynamodb/PynamoDB) - A Pythonic interface for [Amazon DynamoDB](https://aws.amazon.com/dynamodb/).
## Database Drivers
### Database Drivers
_Libraries for connecting and operating databases._
@ -422,7 +426,7 @@ _Libraries for connecting and operating databases._
- [pymongo](https://github.com/mongodb/mongo-python-driver) - The official Python client for MongoDB.
- [redis-py](https://github.com/redis/redis-py) - The Python client for Redis.
## Database
### Database
_Databases implemented in Python._
@ -432,7 +436,7 @@ _Databases implemented in Python._
- [tinydb](https://github.com/msiemens/tinydb) - A tiny, document-oriented database.
- [ZODB](https://github.com/zopefoundation/ZODB) - A native object database for Python. A key-value and object graph database.
## Caching
### Caching
_Libraries for caching data._
@ -441,7 +445,7 @@ _Libraries for caching data._
- [dogpile.cache](https://github.com/sqlalchemy/dogpile.cache) - dogpile.cache is a next generation replacement for Beaker made by the same authors.
- [python-diskcache](https://github.com/grantjenks/python-diskcache) - SQLite and file backed cache backend with faster lookups than memcached and redis.
## Search
### Search
_Libraries and software for indexing and performing search queries on data._
@ -449,7 +453,7 @@ _Libraries and software for indexing and performing search queries on data._
- [elasticsearch-py](https://github.com/elastic/elasticsearch-py) - The official low-level Python client for [Elasticsearch](https://www.elastic.co/products/elasticsearch).
- [pysolr](https://github.com/django-haystack/pysolr) - A lightweight Python wrapper for [Apache Solr](https://lucene.apache.org/solr/).
## Serialization
### Serialization
_Libraries for serializing complex data types._
@ -459,7 +463,7 @@ _Libraries for serializing complex data types._
**Data & Science**
## Data Analysis
### Data Analysis
_Libraries for data analysis._
@ -480,7 +484,7 @@ _Libraries for data analysis._
- [openbb](https://github.com/OpenBB-finance/OpenBB) - A financial data platform for analysts, quants and AI agents.
- [yfinance](https://github.com/ranaroussi/yfinance) - Easy Pythonic way to download market and financial data from Yahoo Finance.
## Data Validation
### Data Validation
_Libraries for validating data. Used for forms in many cases._
@ -490,7 +494,7 @@ _Libraries for validating data. Used for forms in many cases._
- [pydantic](https://github.com/pydantic/pydantic) - Data validation using Python type hints.
- [voluptuous](https://github.com/alecthomas/voluptuous) - A Python data validation library primarily intended for validating data from untrusted sources.
## Data Visualization
### Data Visualization
_Libraries for visualizing data. Also see [awesome-javascript](https://github.com/sorrycc/awesome-javascript#data-visualization)._
@ -513,7 +517,7 @@ _Libraries for visualizing data. Also see [awesome-javascript](https://github.co
- [gradio](https://github.com/gradio-app/gradio) - Build and share machine learning apps, all in Python.
- [streamlit](https://github.com/streamlit/streamlit) - A framework which lets you build dashboards, generate reports, or create chat apps in minutes.
## Geolocation
### Geolocation
_Libraries for geocoding addresses and working with latitudes and longitudes._
@ -523,7 +527,7 @@ _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.
## Science
### Science
_Libraries for scientific computing. Also see [Python-for-Scientists](https://github.com/TomNicholas/Python-for-Scientists)._
@ -544,6 +548,7 @@ _Libraries for scientific computing. Also see [Python-for-Scientists](https://gi
- [pydy](https://github.com/pydy/pydy) - Short for Python Dynamics, used to assist with workflow in the modeling of dynamic motion.
- [PythonRobotics](https://github.com/AtsushiSakai/PythonRobotics) - This is a compilation of various robotics algorithms with visualizations.
- Simulation and Modeling
- [mesa](https://github.com/projectmesa/mesa) - An agent-based modeling framework for building, analyzing, and visualizing complex system simulations.
- [pathsim](https://github.com/pathsim/pathsim) - A block-based system modeling and simulation framework with a browser-based visual editor.
- [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.
@ -553,7 +558,7 @@ _Libraries for scientific computing. Also see [Python-for-Scientists](https://gi
- [networkx](https://github.com/networkx/networkx) - A high-productivity software for complex networks.
- [shapely](https://github.com/shapely/shapely) - Manipulation and analysis of geometric objects in the Cartesian plane.
## Quantum Computing
### Quantum Computing
_Libraries for quantum computing._
@ -564,7 +569,7 @@ _Libraries for quantum computing._
**Developer Tools**
## Algorithms and Design Patterns
### Algorithms and Design Patterns
_Python implementation of data structures, algorithms and design patterns. Also see [awesome-algorithms](https://github.com/tayllan/awesome-algorithms)._
@ -576,7 +581,7 @@ _Python implementation of data structures, algorithms and design patterns. Also
- [python-patterns](https://github.com/faif/python-patterns) - A collection of design patterns in Python.
- [transitions](https://github.com/pytransitions/transitions) - A lightweight, object-oriented finite state machine implementation.
## Interactive Interpreter
### Interactive Interpreter
_Interactive Python interpreters (REPL)._
@ -585,7 +590,7 @@ _Interactive Python interpreters (REPL)._
- [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
### Code Analysis
_Tools of static analysis, linters and code quality checkers. Also see [awesome-static-analysis](https://github.com/analysis-tools-dev/static-analysis)._
@ -615,13 +620,14 @@ _Tools of static analysis, linters and code quality checkers. Also see [awesome-
- [monkeytype](https://github.com/Instagram/MonkeyType) - A system for Python that generates static type annotations by collecting runtime types.
- [pytype](https://github.com/google/pytype) - Pytype checks and infers types for Python code - without requiring type annotations.
## Testing
### Testing
_Libraries for testing codebases and generating test data._
_Libraries for testing codebases and generating test data. Also see [awesome-python-testing](https://github.com/cleder/awesome-python-testing)._
- Frameworks
- [hypothesis](https://github.com/HypothesisWorks/hypothesis) - Hypothesis is an advanced Quickcheck style property based testing library.
- [pytest](https://github.com/pytest-dev/pytest) - A mature full-featured Python testing tool.
- [awesome-pytest](https://github.com/augustogoulart/awesome-pytest)
- [robotframework](https://github.com/robotframework/robotframework) - A generic test automation framework.
- [scanapi](https://github.com/scanapi/scanapi) - Automated Testing and Documentation for your REST API.
- [unittest](https://docs.python.org/3/library/unittest.html) - (Python standard library) Unit testing framework.
@ -650,7 +656,7 @@ _Libraries for testing codebases and generating test 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
### Debugging Tools
_Libraries for debugging code._
@ -669,7 +675,7 @@ _Libraries for debugging code._
- [icecream](https://github.com/gruns/icecream) - Inspect variables, expressions, and program execution with a single, simple function call.
- [memory_graph](https://github.com/bterwijn/memory_graph) - Visualize Python data at runtime to debug references, mutability, and aliasing.
## Build Tools
### Build Tools
_Compile software from source code._
@ -680,7 +686,7 @@ _Compile software from source code._
- [doit](https://github.com/pydoit/doit) - A task runner and build tool.
- [scons](https://github.com/SCons/scons) - A software construction tool.
## Documentation
### Documentation
_Libraries for generating project documentation._
@ -692,7 +698,7 @@ _Libraries for generating project documentation._
**DevOps**
## DevOps Tools
### DevOps Tools
_Software and libraries for DevOps._
@ -718,7 +724,7 @@ _Software and libraries for DevOps._
- [chaostoolkit](https://github.com/chaostoolkit/chaostoolkit) - A Chaos Engineering toolkit & Orchestration for Developers.
- [pre-commit](https://github.com/pre-commit/pre-commit) - A framework for managing and maintaining multi-language pre-commit hooks.
## Distributed Computing
### Distributed Computing
_Frameworks and libraries for Distributed Computing._
@ -730,7 +736,7 @@ _Frameworks and libraries for Distributed Computing._
- [joblib](https://github.com/joblib/joblib) - A set of tools to provide lightweight pipelining in Python.
- [ray](https://github.com/ray-project/ray/) - A system for parallel and distributed Python that unifies the machine learning ecosystem.
## Task Queues
### Task Queues
_Libraries for working with task queues._
@ -739,7 +745,13 @@ _Libraries for working with task queues._
- [huey](https://github.com/coleifer/huey) - Little multi-threaded task queue.
- [rq](https://github.com/rq/rq) - Simple job queues for Python.
## Job Schedulers
### Messaging
_Libraries for working with message brokers and event streaming._
- [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._
@ -750,7 +762,7 @@ _Libraries for scheduling jobs._
- [schedule](https://github.com/dbader/schedule) - Python job scheduling for humans.
- [SpiffWorkflow](https://github.com/sartography/SpiffWorkflow) - A powerful workflow engine implemented in pure Python.
## Logging
### Logging
_Libraries for generating and working with logs._
@ -759,7 +771,7 @@ _Libraries for generating and working with logs._
- [loguru](https://github.com/Delgan/loguru) - Library which aims to bring enjoyable logging in Python.
- [structlog](https://github.com/hynek/structlog) - Structured logging made easy.
## Network Virtualization
### Network Virtualization
_Tools and libraries for Virtual Networking and SDN (Software Defined Networking)._
@ -769,7 +781,7 @@ _Tools and libraries for Virtual Networking and SDN (Software Defined Networking
**CLI & GUI**
## CLI Development
### CLI Development
_Libraries for building command-line applications._
@ -788,7 +800,7 @@ _Libraries for building command-line applications._
- [textual](https://github.com/Textualize/textual) - A framework for building interactive user interfaces that run in the terminal and the browser.
- [tqdm](https://github.com/tqdm/tqdm) - Fast, extensible progress bar for loops and CLI.
## CLI Tools
### CLI Tools
_Useful CLI-based tools for productivity._
@ -807,7 +819,7 @@ _Useful CLI-based tools for productivity._
- [mycli](https://github.com/dbcli/mycli) - MySQL CLI with autocompletion and syntax highlighting.
- [pgcli](https://github.com/dbcli/pgcli) - PostgreSQL CLI with autocompletion and syntax highlighting.
## GUI Development
### GUI Development
_Libraries for working with graphical user interface applications._
@ -828,14 +840,14 @@ _Libraries for working with graphical user interface applications._
- [nicegui](https://github.com/zauberzeug/nicegui) - An easy-to-use, Python-based UI framework, which shows up in your web browser.
- [pywebview](https://github.com/r0x0r/pywebview/) - A lightweight cross-platform native wrapper around a webview component.
- Terminal
- [curses](https://docs.python.org/3/library/curses.html) - Built-in wrapper for [ncurses](http://www.gnu.org/software/ncurses/) used to create terminal GUI applications.
- [curses](https://docs.python.org/3/library/curses.html) - (Python standard library) The built-in wrapper for [ncurses](http://www.gnu.org/software/ncurses/) used to create terminal GUI applications.
- [urwid](https://github.com/urwid/urwid) - A library for creating terminal GUI applications with strong support for widgets, events, rich colors, etc.
- Wrappers
- [gooey](https://github.com/chriskiehl/Gooey) - Turn command line programs into a full GUI application with one line.
**Text & Documents**
## Text Processing
### Text Processing
_Libraries for parsing and manipulating plain texts._
@ -861,7 +873,7 @@ _Libraries for parsing and manipulating plain texts._
- [python-user-agents](https://github.com/selwin/python-user-agents) - Browser user agent parser.
- [sqlparse](https://github.com/andialbrecht/sqlparse) - A non-validating SQL parser.
## HTML Manipulation
### HTML Manipulation
_Libraries for working with HTML and XML._
@ -873,7 +885,7 @@ _Libraries for working with HTML and XML._
- [tinycss2](https://github.com/Kozea/tinycss2) - A low-level CSS parser and generator written in Python.
- [xmltodict](https://github.com/martinblech/xmltodict) - Working with XML feel like you are working with JSON.
## File Format Processing
### File Format Processing
_Libraries for parsing and manipulating specific text formats._
@ -907,7 +919,7 @@ _Libraries for parsing and manipulating specific text formats._
- [pyyaml](https://github.com/yaml/pyyaml) - YAML implementations for Python.
- [tomllib](https://docs.python.org/3/library/tomllib.html) - (Python standard library) Parse TOML files.
## File Manipulation
### File Manipulation
_Libraries for file manipulation._
@ -919,7 +931,7 @@ _Libraries for file manipulation._
**Media**
## Image Processing
### Image Processing
_Libraries for manipulating images._
@ -932,7 +944,7 @@ _Libraries for manipulating images._
- [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/emcconville/wand) - Python bindings for [MagickWand](https://www.imagemagick.org/script/magick-wand.php), C API for ImageMagick.
## Audio & Video Processing
### Audio & Video Processing
_Libraries for manipulating audio, video, and their metadata._
@ -949,7 +961,7 @@ _Libraries for manipulating audio, video, and their metadata._
- [mutagen](https://github.com/quodlibet/mutagen) - A Python module to handle audio metadata.
- [tinytag](https://github.com/devsnd/tinytag) - A library for reading music meta data of MP3, OGG, FLAC and Wave files.
## Game Development
### Game Development
_Awesome game development libraries._
@ -962,7 +974,7 @@ _Awesome game development libraries._
**Python Language**
## Implementations
### Implementations
_Implementations of Python._
@ -973,7 +985,7 @@ _Implementations of Python._
- [pyodide](https://github.com/pyodide/pyodide) - Python distribution for the browser and Node.js based on WebAssembly.
- [pypy](https://github.com/pypy/pypy) - A very fast and compliant implementation of the Python language.
## Built-in Classes Enhancement
### Built-in Classes Enhancement
_Libraries for enhancing Python built-in classes._
@ -981,7 +993,7 @@ _Libraries for enhancing Python built-in classes._
- [bidict](https://github.com/jab/bidict) - Efficient, Pythonic bidirectional map data structures and related functionality.
- [box](https://github.com/cdgriffith/Box) - Python dictionaries with advanced dot notation access.
## Functional Programming
### Functional Programming
_Functional Programming with Python._
@ -992,7 +1004,7 @@ _Functional Programming with Python._
- [returns](https://github.com/dry-python/returns) - A set of type-safe monads, transformers, and composition utilities.
- [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.
## Asynchronous Programming
### Asynchronous Programming
_Libraries for asynchronous, concurrent and parallel execution. Also see [awesome-asyncio](https://github.com/timofurrer/awesome-asyncio)._
@ -1006,7 +1018,7 @@ _Libraries for asynchronous, concurrent and parallel execution. Also see [awesom
- [twisted](https://github.com/twisted/twisted) - An event-driven networking engine.
- [uvloop](https://github.com/MagicStack/uvloop) - Ultra fast asyncio event loop.
## Date and Time
### Date and Time
_Libraries for working with dates and times._
@ -1017,16 +1029,17 @@ _Libraries for working with dates and times._
**Python Toolchain**
## Environment Management
### Environment Management
_Libraries for Python version and virtual environment management._
- [KillPy](https://github.com/Tlaloc-Es/killpy) - Analyze, detect, and clean unused Python environments and pipx packages.
- [pyenv](https://github.com/pyenv/pyenv) - Simple Python version management.
- [pyenv-win](https://github.com/pyenv-win/pyenv-win) - Pyenv for Windows.
- [uv](https://github.com/astral-sh/uv) - An extremely fast Python version, package and project manager, written in Rust.
- [virtualenv](https://github.com/pypa/virtualenv) - A tool to create isolated Python environments.
## Package Management
### Package Management
_Libraries for package and dependency management._
@ -1036,7 +1049,7 @@ _Libraries for package and dependency management._
- [poetry](https://github.com/python-poetry/poetry) - Python dependency management and packaging made easy.
- [uv](https://github.com/astral-sh/uv) - An extremely fast Python version, package and project manager, written in Rust.
## Package Repositories
### Package Repositories
_Local PyPI repository server and proxies._
@ -1044,7 +1057,7 @@ _Local PyPI repository server and proxies._
- [devpi](https://github.com/devpi/devpi) - PyPI server and packaging/testing/release tool.
- [warehouse](https://github.com/pypa/warehouse) - Next generation Python Package Repository (PyPI).
## Distribution
### Distribution
_Libraries to create packaged executables for release distribution._
@ -1054,7 +1067,7 @@ _Libraries to create packaged executables for release distribution._
- [pyinstaller](https://github.com/pyinstaller/pyinstaller) - Converts Python programs into stand-alone executables (cross-platform).
- [shiv](https://github.com/linkedin/shiv) - A command line utility for building fully self-contained zipapps (PEP 441), but with all their dependencies included.
## Configuration Files
### Configuration Files
_Libraries for storing and parsing configuration options._
@ -1066,13 +1079,13 @@ _Libraries for storing and parsing configuration options._
**Security**
## Cryptography
### Cryptography
- [cryptography](https://github.com/pyca/cryptography) - A package designed to expose cryptographic primitives and recipes to Python developers.
- [paramiko](https://github.com/paramiko/paramiko) - The leading native Python SSHv2 protocol library.
- [pynacl](https://github.com/pyca/pynacl) - Python binding to the Networking and Cryptography (NaCl) library.
## Penetration Testing
### Penetration Testing
_Frameworks and tools for penetration testing._
@ -1081,16 +1094,22 @@ _Frameworks and tools for penetration testing._
- [sherlock](https://github.com/sherlock-project/sherlock) - Hunt down social media accounts by username across social networks.
- [sqlmap](https://github.com/sqlmapproject/sqlmap) - Automatic SQL injection and database takeover tool.
**Miscellaneous**
### Web Security
## Hardware
_Libraries for application-layer web security._
- [secure](https://github.com/TypeError/secure) - HTTP security headers for Python web applications with ASGI and WSGI middleware.
**Other**
### Hardware
_Libraries for programming with hardware._
- [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.
## Microsoft Windows
### Microsoft Windows
_Python programming on Microsoft Windows._
@ -1098,7 +1117,7 @@ _Python programming on Microsoft Windows._
- [pywin32](https://github.com/mhammond/pywin32) - Python Extensions for Windows.
- [winpython](https://github.com/winpython/winpython) - Portable development environment for Windows 10/11.
## Miscellaneous
### Miscellaneous
_Useful libraries or tools that don't fit in the categories above._
@ -1107,18 +1126,18 @@ _Useful libraries or tools that don't fit in the categories above._
- [itsdangerous](https://github.com/pallets/itsdangerous) - Various helpers to pass trusted data to untrusted environments.
- [tryton](https://github.com/tryton/tryton) - A general-purpose business framework.
# Resources
## Resources
Where to discover learning resources or new Python libraries.
## Newsletters
### Newsletters
- [Awesome Python Newsletter](http://python.libhunt.com/newsletter)
- [Pycoder's Weekly](https://pycoders.com/)
- [Python Tricks](https://realpython.com/python-tricks/)
- [Python Weekly](https://www.pythonweekly.com/)
## Podcasts
### Podcasts
- [Django Chat](https://djangochat.com/)
- [PyPodcats](https://pypodcats.live)
@ -1126,11 +1145,11 @@ Where to discover learning resources or new Python libraries.
- [Talk Python To Me](https://talkpython.fm/)
- [The Real Python Podcast](https://realpython.com/podcasts/rpp/)
## Websites
### 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
## 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.

View file

@ -37,7 +37,7 @@ Your sponsorship puts your product in front of developers at the exact moment th
## Get Started
Email [vinta.chen@gmail.com](mailto:vinta.chen@gmail.com?subject=awesome-python%20Sponsorship) with:
Email [vinta.chen@gmail.com](mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship) with:
- **Tier:** Headline Sponsor ($500/mo) or Featured Sponsor ($150/mo)
- **Content:** Product name, URL, logo, and description (Headline tier) or `[Name](URL) - Description.` entry (Featured tier)

View file

@ -14,7 +14,7 @@ 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"]
lint = ["ruff==0.15.6", "ty==0.0.33"]
test = ["pytest==9.0.3"]
dev = [
{ include-group = "build" },
@ -23,16 +23,30 @@ dev = [
"watchdog==6.0.0",
]
[tool.pytest.ini_options]
testpaths = ["website/tests"]
pythonpath = ["website"]
[tool.ruff]
line-length = 200
[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.13"
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"]

34
uv.lock generated
View file

@ -3,7 +3,7 @@ revision = 3
requires-python = ">=3.13"
[options]
exclude-newer = "2026-04-18T18:21:23.412234Z"
exclude-newer = "2026-04-30T04:38:39.45925Z"
exclude-newer-span = "P3D"
[[package]]
@ -35,10 +35,12 @@ dev = [
{ name = "markdown-it-py" },
{ name = "pytest" },
{ name = "ruff" },
{ name = "ty" },
{ name = "watchdog" },
]
lint = [
{ name = "ruff" },
{ name = "ty" },
]
test = [
{ name = "pytest" },
@ -58,9 +60,13 @@ dev = [
{ 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 = "watchdog", specifier = "==6.0.0" },
]
lint = [{ name = "ruff", specifier = "==0.15.6" }]
lint = [
{ name = "ruff", specifier = "==0.15.6" },
{ name = "ty", specifier = "==0.0.33" },
]
test = [{ name = "pytest", specifier = "==9.0.3" }]
[[package]]
@ -289,6 +295,30 @@ wheels = [
{ 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 = "watchdog"
version = "6.0.0"

View file

@ -4,14 +4,30 @@
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 Any
from typing import TypedDict
from jinja2 import Environment, FileSystemLoader
from readme_parser import ParsedGroup, ParsedSection, parse_readme, parse_sponsors
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/"
SOURCE_TYPE_DOMAINS = {
"docs.python.org": "Built-in",
@ -20,6 +36,37 @@ SOURCE_TYPE_DOMAINS = {
}
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
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):
@ -48,13 +95,13 @@ def load_stars(path: Path) -> dict[str, dict]:
return {}
def sort_entries(entries: list[dict]) -> list[dict]:
def sort_entries(entries: Sequence[TemplateEntry]) -> list[TemplateEntry]:
"""Sort entries by stars descending, then name ascending.
Three tiers: starred entries first, stdlib second, other non-starred last.
"""
def sort_key(entry: dict) -> tuple[int, int, int, str]:
def sort_key(entry: TemplateEntry) -> tuple[int, int, int, str]:
stars = entry["stars"]
name = entry["name"].lower()
if stars is not None:
@ -67,10 +114,250 @@ def sort_entries(entries: list[dict]) -> list[dict]:
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,
}
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,
"mainEntity": _item_list_payload(entries),
},
],
}
def category_meta_description(name: str, entry_count: int, description: str) -> str:
count_sentence = f"Explore {entry_count} curated Python projects in {name}."
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_category_json_ld(name: str, url: str, description: str, entries: Sequence[TemplateEntry]) -> dict:
return {
"@context": "https://schema.org",
"@graph": [
_website_node(),
{
"@type": "CollectionPage",
"@id": url,
"name": f"{name} Python Libraries",
"url": url,
"description": description,
"isPartOf": ISPARTOF_WEBSITE,
"mainEntity": _item_list_payload(entries),
},
],
}
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 build_llms_txt(
template_text: str,
*,
readme_text: str,
stars_data: dict[str, dict],
categories: Sequence[ParsedSection],
total_entries: int,
) -> str:
"""Render the llms.txt entry point with the curated category catalog."""
categories_md = annotate_entries_with_stars(
extract_categories_body(readme_text).rstrip(),
stars_data,
format_stars=lambda n: f"GitHub stars: {n}",
)
text_env = Environment(autoescape=False, trim_blocks=True, lstrip_blocks=True)
rendered = text_env.from_string(template_text).render(
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_stars(
markdown: str,
stars_data: dict[str, dict],
*,
format_stars=None,
) -> str:
"""Append the star count to bullet entry lines whose first GitHub link has known star data.
`format_stars` controls the parenthesized text. Defaults to "{N} GitHub stars".
Pass `str` for a bare number.
"""
if format_stars is None:
format_stars = lambda n: f"{n} GitHub stars" # noqa: E731 lambda-assignment
lines = markdown.splitlines(keepends=True)
out: list[str] = []
for line in lines:
if not BULLET_LINE_RE.match(line):
out.append(line)
continue
annotated = line
for match in MARKDOWN_LINK_RE.finditer(line):
repo_key = extract_github_repo(match.group(1))
if not repo_key:
continue
entry = stars_data.get(repo_key)
if not entry or "stars" not in entry:
continue
stripped = line.rstrip("\n")
ending = line[len(stripped) :]
annotated = f"{stripped} ({format_stars(entry['stars'])}){ending}"
break
out.append(annotated)
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[dict]:
) -> list[TemplateEntry]:
"""Flatten categories into individual library entries for table display.
Entries appearing in multiple categories are merged into a single entry
@ -78,27 +365,27 @@ def extract_entries(
"""
cat_to_group = {cat["name"]: group["name"] for group in groups for cat in group["categories"]}
seen: dict[tuple[str, str], dict[str, Any]] = {} # (url, name) -> entry
entries: list[dict[str, Any]] = []
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: dict[str, Any] | None = seen.get(key)
existing = seen.get(key)
if existing is None:
existing = {
"name": entry["name"],
"url": entry["url"],
"description": entry["description"],
"categories": [],
"groups": [],
"subcategories": [],
"stars": None,
"owner": None,
"last_commit_at": None,
"source_type": detect_source_type(entry["url"]),
"also_see": entry["also_see"],
}
existing = TemplateEntry(
name=entry["name"],
url=entry["url"],
description=entry["description"],
categories=[],
groups=[],
subcategories=[],
stars=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"]:
@ -109,7 +396,15 @@ def extract_entries(
if subcat:
scoped = f"{cat['name']} > {subcat}"
if not any(s["value"] == scoped for s in existing["subcategories"]):
existing["subcategories"].append({"name": subcat, "value": scoped})
sub_slug = slugify(subcat)
existing["subcategories"].append(
TemplateSubcategory(
name=subcat,
value=scoped,
slug=sub_slug,
url=f"/categories/{cat['slug']}/{sub_slug}/",
)
)
return entries
@ -129,8 +424,15 @@ def build(repo_root: Path) -> None:
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")
@ -151,17 +453,35 @@ def build(repo_root: Path) -> None:
entry["last_commit_at"] = sd.get("last_commit_at", "")
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("</", "<\\/")
homepage_json_ld = json.dumps(
build_homepage_json_ld(entries, len(categories)),
ensure_ascii=False,
).replace("</", "<\\/")
tpl_index = env.get_template("index.html")
(site_dir / "index.html").write_text(
tpl_index.render(
@ -171,20 +491,146 @@ def build(repo_root: Path) -> None:
total_entries=total_entries,
total_categories=len(categories),
repo_stars=repo_stars,
build_date=datetime.now(UTC).strftime("%B %d, %Y"),
build_date=build_date.strftime("%B %d, %Y"),
sponsors=sponsors,
category_urls=category_urls,
filter_urls=filter_urls,
filter_urls_json=filter_urls_json,
homepage_json_ld=homepage_json_ld,
),
encoding="utf-8",
)
tpl_category = env.get_template("category.html")
categories_dir = site_dir / "categories"
def render_category(
category: TemplateCategory,
*,
category_url: str,
entries: Sequence[TemplateEntry],
current_path: str,
page_dir: Path,
parent_category: ParsedSection | None = None,
group_categories: Sequence[ParsedSection] | None = None,
) -> None:
page_dir.mkdir(parents=True, exist_ok=True)
category_description = category_meta_description(category["name"], len(entries), category["description"])
category_json_ld = json.dumps(
build_category_json_ld(category["name"], category_url, category_description, entries),
ensure_ascii=False,
).replace("</", "<\\/")
(page_dir / "index.html").write_text(
tpl_category.render(
category=category,
category_url=category_url,
category_description=category_description,
entries=entries,
total_categories=len(categories),
category_urls=category_urls,
current_path=current_path,
filter_urls=filter_urls,
filter_urls_json=filter_urls_json,
parent_category=parent_category,
group_categories=group_categories,
category_json_ld=category_json_ld,
),
encoding="utf-8",
)
for category in categories:
render_category(
category,
category_url=category_public_url(category),
entries=[e for e in entries if category["name"] in e["categories"]],
current_path=category_path(category),
page_dir=categories_dir / category["slug"],
)
for group in parsed_groups:
render_category(
synthetic_category(group["name"], group["slug"]),
category_url=group_public_url(group["slug"]),
entries=[e for e in entries if group["name"] in e["groups"]],
current_path=group_path(group["slug"]),
page_dir=categories_dir / group["slug"],
group_categories=group["categories"],
)
if builtin_entries:
render_category(
synthetic_category(BUILTIN_FILTER, BUILTIN_SLUG),
category_url=BUILTIN_PUBLIC_URL,
entries=builtin_entries,
current_path=BUILTIN_PATH,
page_dir=categories_dir / BUILTIN_SLUG,
)
sponsorship_dir = site_dir / "sponsorship"
sponsorship_dir.mkdir(parents=True, exist_ok=True)
tpl_sponsorship = env.get_template("sponsorship.html")
hero_stats: list[str] = []
if repo_stars:
hero_stats.append(f"{repo_stars}+ stars on GitHub")
hero_stats.append(f"Updated {build_date.strftime('%B %d, %Y')}")
(sponsorship_dir / "index.html").write_text(
tpl_sponsorship.render(hero_stats=hero_stats),
encoding="utf-8",
)
subcat_to_entries: dict[str, list[TemplateEntry]] = {}
subcat_meta: dict[str, tuple[str, str, str]] = {} # value -> (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)
(site_dir / "llms.txt").write_text(readme_text, encoding="utf-8")
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,
stars_data=stars_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 single page with {len(parsed_groups)} groups, {len(categories)} categories")
print(f"Built site with {len(parsed_groups)} groups, {len(categories)} categories")
print(f"Total entries: {total_entries}")
print(f"Output: {site_dir}")

View file

@ -11,7 +11,6 @@ from itertools import batched
from pathlib import Path
import httpx
from build import extract_github_repo, load_stars
CACHE_MAX_AGE_HOURS = 12
@ -53,10 +52,7 @@ def build_graphql_query(repos: Sequence[str]) -> str:
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}") '
f"{{ stargazerCount owner {{ login }} defaultBranchRef {{ target {{ ... on Commit {{ committedDate }} }} }} }}"
)
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) + " }"

View file

@ -27,6 +27,7 @@ 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
@ -113,22 +114,29 @@ def _heading_text(node: SyntaxTreeNode) -> str:
return ""
def _extract_description(nodes: list[SyntaxTreeNode]) -> str:
"""Extract description from the first paragraph if it's a single <em> block.
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 <em> block.
Pattern: _Libraries for foo._ -> "Libraries for foo."
"""
if not nodes:
return ""
return []
first = nodes[0]
if first.type != "paragraph":
return ""
return []
for child in first.children:
if child.type == "inline" and len(child.children) == 1:
em = child.children[0]
if em.type == "em":
return render_inline_text(em.children)
return ""
return em.children
return []
# --- Entry extraction --------------------------------------------------------
@ -228,18 +236,22 @@ def _parse_list_entries(
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),
))
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,
))
entries.append(
ParsedEntry(
name=name,
url=url,
description=desc_html,
also_see=also_see,
subcategory=subcategory,
)
)
return entries
@ -258,20 +270,22 @@ def _parse_section_entries(content_nodes: list[SyntaxTreeNode]) -> list[ParsedEn
def _build_section(name: str, body: list[SyntaxTreeNode]) -> ParsedSection:
"""Build a ParsedSection from a heading name and its body nodes."""
desc = _extract_description(body)
content_nodes = body[1:] if desc else body
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.
@ -296,7 +310,7 @@ def _parse_grouped_sections(
) -> list[ParsedGroup]:
"""Parse nodes into groups of categories using bold markers as group boundaries.
Bold-only paragraphs (**Group Name**) delimit groups. H2 headings under each
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.
"""
@ -317,11 +331,13 @@ def _parse_grouped_sections(
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),
))
groups.append(
ParsedGroup(
name=name,
slug=slugify(name),
categories=list(current_group_cats),
)
)
current_group_name = None
current_group_cats = []
@ -332,7 +348,7 @@ def _parse_grouped_sections(
flush_group()
current_group_name = bold_name
current_cat_body = []
elif node.type == "heading" and node.tag == "h2":
elif node.type == "heading" and node.tag in ("h2", "h3"):
flush_cat()
current_cat_name = _heading_text(node)
current_cat_body = []
@ -374,7 +390,7 @@ def _parse_sponsor_item(inline: SyntaxTreeNode) -> ParsedSponsor | None:
def parse_sponsors(text: str) -> list[ParsedSponsor]:
"""Parse the `# Sponsors` section of README.md into a list of sponsors.
"""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.
@ -386,14 +402,18 @@ def parse_sponsors(text: str) -> list[ParsedSponsor]:
start_idx = None
end_idx = len(children)
start_level = None
for i, node in enumerate(children):
if node.type == "heading" and node.tag == "h1":
title = _heading_text(node).strip().lower()
if start_idx is None and title == "sponsors":
start_idx = i + 1
elif start_idx is not None:
end_idx = i
break
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 []
@ -417,26 +437,26 @@ 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 thematic break (---) and # Resources or # Contributing
is parsed as categories grouped by bold markers (**Group Name**).
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 thematic break (---) and section boundaries in one pass
hr_idx = None
# Find Projects and section boundaries in one pass.
projects_idx = None
cat_end_idx = None
for i, node in enumerate(children):
if hr_idx is None and node.type == "hr":
hr_idx = i
elif node.type == "heading" and node.tag == "h1":
if _heading_level(node) in (1, 2):
text_content = _heading_text(node)
if cat_end_idx is None and text_content in ("Resources", "Contributing"):
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 hr_idx is None:
if projects_idx is None:
return []
cat_nodes = children[hr_idx + 1 : cat_end_idx or len(children)]
cat_nodes = children[projects_idx + 1 : cat_end_idx or len(children)]
return _parse_grouped_sections(cat_nodes)

View file

@ -59,6 +59,19 @@ document.querySelectorAll("[data-scroll-to]").forEach(function (link) {
});
});
// 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");
@ -100,7 +113,12 @@ document
rows.forEach(function (row, i) {
row._origIndex = i;
row._expandRow = row.nextElementSibling;
let next = row.nextElementSibling;
if (next && next.classList.contains("desc-row")) {
row._descRow = next;
next = next.nextElementSibling;
}
row._expandRow = next;
});
function collapseAll() {
@ -114,6 +132,7 @@ function collapseAll() {
function applyFilters() {
const query = searchInput ? searchInput.value.toLowerCase().trim() : "";
const descRowsVisible = !isIndexDocument || activeFilter !== null;
let visibleCount = 0;
collapseAll();
@ -129,9 +148,11 @@ function applyFilters() {
if (show && query) {
if (!row._searchText) {
let text = row.textContent.toLowerCase();
const next = row.nextElementSibling;
if (next && next.classList.contains("expand-row")) {
text += " " + next.textContent.toLowerCase();
if (row._descRow) {
text += " " + row._descRow.textContent.toLowerCase();
}
if (row._expandRow) {
text += " " + row._expandRow.textContent.toLowerCase();
}
row._searchText = text;
}
@ -139,6 +160,12 @@ function applyFilters() {
}
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++;
@ -167,19 +194,36 @@ function applyFilters() {
updateURL();
}
function 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 (activeFilter) {
params.set("filter", activeFilter);
}
if (activeSort.col !== "stars" || activeSort.order !== "desc") {
params.set("sort", activeSort.col);
params.set("order", activeSort.order);
}
const qs = params.toString();
history.replaceState(null, "", qs ? "?" + qs : location.pathname);
return qs ? "?" + qs : "";
}
function updateURL() {
if (!isIndexDocument) return;
const path =
activeFilter && filterToUrl[activeFilter] ? filterToUrl[activeFilter] : "/";
history.replaceState(null, "", path + buildQueryString());
}
function getSortValue(row, col) {
@ -202,6 +246,8 @@ function getSortValue(row, col) {
}
function sortRows() {
if (!tbody) return;
const arr = Array.prototype.slice.call(rows);
const col = activeSort.col;
const order = activeSort.order;
@ -230,7 +276,8 @@ function sortRows() {
const frag = document.createDocumentFragment();
arr.forEach(function (row) {
frag.appendChild(row);
frag.appendChild(row._expandRow);
if (row._descRow) frag.appendChild(row._descRow);
if (row._expandRow) frag.appendChild(row._expandRow);
});
tbody.appendChild(frag);
applyFilters();
@ -259,7 +306,11 @@ if (tbody) {
// Don't toggle if clicking a link or tag button
if (e.target.closest("a") || e.target.closest(".tag")) return;
const row = e.target.closest("tr.row");
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");
@ -286,13 +337,27 @@ tags.forEach(function (tag) {
tag.addEventListener("click", function (e) {
e.preventDefault();
const value = tag.dataset.value;
activeFilter = activeFilter === value ? null : value;
applyFilters();
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();
});
@ -301,6 +366,10 @@ if (filterClear) {
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();
@ -394,19 +463,29 @@ if (backToTop) {
(function () {
const params = new URLSearchParams(location.search);
const q = params.get("q");
const filter = params.get("filter");
const sort = params.get("sort");
const order = params.get("order");
if (q && searchInput) searchInput.value = q;
if (filter) activeFilter = filter;
if (
(sort === "name" || sort === "stars" || sort === "commit-time") &&
(order === "desc" || order === "asc")
) {
activeSort = { col: sort, order: order };
}
if (q || filter || sort) {
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();
});

View file

@ -335,6 +335,49 @@ kbd {
letter-spacing: 0.02em;
}
.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;
@ -376,18 +419,81 @@ kbd {
}
.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:
@ -682,6 +788,11 @@ kbd {
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;
@ -813,14 +924,66 @@ th[data-sort].sort-asc::after {
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;
@ -887,7 +1050,7 @@ th[data-sort].sort-asc::after {
background: var(--accent-soft);
color: var(--accent-deep);
padding: 0.14rem 0.48rem;
font-size: 0.6rem;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
@ -972,6 +1135,298 @@ th[data-sort].sort-asc::after {
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(2, 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);
@ -1028,8 +1483,8 @@ th[data-sort].sort-asc::after {
.footer-left {
display: flex;
flex-direction: column;
gap: 0.3rem;
align-items: center;
gap: 0.6rem;
}
.footer-brand {
@ -1117,10 +1572,34 @@ th[data-sort].sort-asc::after {
.hero-grid,
.results-intro,
.sponsor-shell {
.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;
}
@ -1139,7 +1618,7 @@ th[data-sort].sort-asc::after {
.tag {
padding: 0.38rem 0.65rem;
font-size: 0.65rem;
font-size: var(--text-xs);
}
.table-wrap {
@ -1185,6 +1664,15 @@ th[data-sort].sort-asc::after {
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;

View file

@ -1,26 +1,30 @@
<!doctype html>
<html lang="en">
<head>
{% 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 charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Awesome Python{% endblock %}</title>
<meta
name="description"
content="{% block description %}An opinionated list of Python frameworks, libraries, tools, and resources. {{ total_entries }} projects across {{ categories | length }} categories.{% endblock %}"
/>
<link rel="canonical" href="https://awesome-python.com/" />
<title>{{ meta_title | trim }}</title>
<meta name="description" content="{{ meta_description | trim }}" />
<link rel="canonical" href="{{ canonical_url | trim }}" />
{% block alternate_links %}
<link rel="alternate" type="text/plain" href="/llms.txt" title="LLMs text entry point" />
{% endblock %}
<meta property="og:type" content="website" />
<meta property="og:title" content="Awesome Python" />
<meta
property="og:description"
content="An opinionated list of Python frameworks, libraries, tools, and resources."
/>
<meta
property="og:image"
content="https://awesome-python.com/static/og-image.png"
/>
<meta property="og:url" content="https://awesome-python.com/" />
<meta name="twitter:card" content="summary" />
<meta property="og:title" content="{{ meta_title | trim }}" />
<meta property="og:description" content="{{ meta_description | trim }}" />
<meta property="og:image" content="{{ social_image_url }}" />
<meta property="og:url" content="{{ canonical_url | trim }}" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ meta_title | trim }}" />
<meta name="twitter:description" content="{{ meta_description | trim }}" />
<meta name="twitter:image" content="{{ social_image_url }}" />
<meta name="theme-color" content="#1c1410" />
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
@ -49,6 +53,7 @@
gtag("js", new Date());
gtag("config", "G-0LMLYE0HER");
</script>
{% block extra_head %}{% endblock %}
</head>
<body>
<a href="#content" class="skip-link">Skip to content</a>
@ -65,7 +70,9 @@
<footer class="footer">
<div class="footer-left">
<span class="footer-brand">Awesome Python</span>
<a href="/" class="footer-brand">Awesome Python</a>
<span class="footer-sep">/</span>
<a href="/sponsorship/">Sponsorship</a>
</div>
<div class="footer-links">
<span

View file

@ -0,0 +1,294 @@
{% extends "base.html" %}
{% block title %}{{ category.name }} Python Libraries - Awesome Python{% endblock %}
{% block description %}{{ category_description }}{% endblock %}
{% block canonical_url %}{{ category_url }}{% endblock %}
{% block alternate_links %}{% endblock %}
{% block extra_head %}
<script type="application/ld+json">{{ category_json_ld | safe }}</script>
{% endblock %}
{% block header %}
<header class="category-hero">
<div class="hero-sheen" aria-hidden="true"></div>
<div class="hero-noise" aria-hidden="true"></div>
<div class="category-hero-shell">
<nav class="hero-topbar category-topbar" aria-label="Site">
<a href="/" class="hero-brand-mini">Awesome Python</a>
<div class="hero-topbar-actions">
<a href="/#library-index" class="hero-topbar-link">All projects</a>
<a
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
class="hero-topbar-link hero-topbar-link-strong"
target="_blank"
rel="noopener"
>Submit a project</a
>
</div>
</nav>
<div class="category-hero-copy">
{% if parent_category %}
<p class="category-breadcrumb">
<a href="/categories/{{ parent_category.slug }}/">{{ parent_category.name }}</a>
</p>
{% endif %}
<h1>{{ category.name }}</h1>
{% if category.description_html %}
<p class="category-subtitle">{{ category.description_html | safe }}</p>
{% endif %}
</div>
{% if group_categories %}
<nav class="hero-category-nav" aria-labelledby="hero-category-heading">
<div class="hero-category-meta">
<h2 id="hero-category-heading">Browse by category</h2>
</div>
<ul class="hero-category-links">
{% for sub in group_categories %}
<li>
<a class="hero-category-link" href="{{ category_urls[sub.name] }}"
>{{ sub.name }}</a
>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
</div>
</header>
{% endblock %}
{% block content %}
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
<section class="results-section" id="library-index">
<div class="results-intro section-shell" data-reveal>
<div>
<h2>Search every project in one place</h2>
</div>
<p class="results-note">
Press <kbd>/</kbd> to search. Tap a tag to filter. Click any row for
details.
</p>
</div>
<div class="controls section-shell" data-reveal>
<h2 class="sr-only">Search and filter</h2>
<div class="search-wrap">
<svg
class="search-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input
type="search"
class="search"
placeholder="Search {{ entries | length }} projects in {{ category.name }}..."
aria-label="Search projects"
/>
</div>
<div class="filter-bar" aria-live="polite">
<span>Filtering for <strong class="filter-value"></strong></span>
<button class="filter-clear" aria-label="Clear filter">
Clear filter
</button>
</div>
</div>
<h2 class="sr-only">Results</h2>
<div
class="table-wrap"
tabindex="0"
role="region"
aria-label="Libraries table"
>
<table class="table">
<thead>
<tr>
<th class="col-num"><span class="sr-only">Row number</span></th>
<th class="col-name" data-sort="name">
<button type="button" class="sort-btn">Project Name</button>
</th>
<th class="col-stars" data-sort="stars">
<button type="button" class="sort-btn">GitHub Stars</button>
</th>
<th class="col-commit" data-sort="commit-time">
<button type="button" class="sort-btn">Last Commit</button>
</th>
<th class="col-cat">Tags</th>
<th class="col-arrow">
<button class="back-to-top" aria-label="Back to top">
Top &uarr;
</button>
</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr
class="row"
data-tags="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | map(attribute='value') | join('||') }}{% endif %}||{{ entry.groups | join('||') }}{% if entry.source_type == 'Built-in' %}||Built-in{% endif %}"
tabindex="0"
aria-expanded="false"
aria-controls="expand-{{ loop.index }}"
>
<td class="col-num">{{ loop.index }}</td>
<td class="col-name">
<a href="{{ entry.url }}" target="_blank" rel="noopener"
>{{ entry.name }}</a
>
<span class="mobile-cat"
>{% if entry.subcategories %}{{ entry.subcategories[0].name }}{%
else %}{{ category.name }}{% endif %}</span
>
</td>
<td class="col-stars">
{% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{%
elif entry.source_type %}<span class="source-badge"
>{{ entry.source_type }}</span
>{% else %}&mdash;{% endif %}
</td>
<td
class="col-commit"
{%
if
entry.last_commit_at
%}data-commit="{{ entry.last_commit_at }}"
{%
endif
%}
>
{% if entry.last_commit_at %}<time
datetime="{{ entry.last_commit_at }}"
>{{ entry.last_commit_at[:10] }}</time
>{% else %}&mdash;{% endif %}
</td>
<td class="col-cat">
{% for subcat in entry.subcategories %}
<a class="tag{% if subcat.url == current_path %} active{% endif %}" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
{{ subcat.name }}
</a>
{% endfor %}
{% for cat in entry.categories %}
<a
class="tag{% if category_urls[cat] == current_path %} active{% endif %}"
href="{{ category_urls[cat] }}"
data-value="{{ cat }}"
data-url="{{ category_urls[cat] }}"
>{{ cat }}</a
>
{% endfor %}
{% if entry.groups %}
{% set group_url = filter_urls[entry.groups[0]] %}
<a
class="tag tag-group{% if group_url == current_path %} active{% endif %}"
href="{{ group_url }}"
data-value="{{ entry.groups[0] }}"
data-url="{{ group_url }}"
>
{{ entry.groups[0] }}
</a>
{% endif %}
{% if entry.source_type == 'Built-in' %}
<a
class="tag tag-source{% if '/categories/built-in/' == current_path %} active{% endif %}"
href="/categories/built-in/"
data-value="Built-in"
data-url="/categories/built-in/"
>
Built-in
</a>
{% endif %}
</td>
<td class="col-arrow"><span class="arrow">&rarr;</span></td>
</tr>
{% if entry.description %}
<tr class="desc-row" aria-hidden="true">
<td class="col-num"></td>
<td colspan="5">
<div class="desc-text">{{ entry.description | safe }}</div>
</td>
</tr>
{% endif %}
<tr class="expand-row" id="expand-{{ loop.index }}">
<td></td>
<td colspan="4">
<div class="expand-content">
{% if entry.also_see %}
<div class="expand-also-see">
Also see: {% for see in entry.also_see %}<a
href="{{ see.url }}"
target="_blank"
rel="noopener"
>{{ see.name }}</a
>{% if not loop.last %}, {% endif %}{% endfor %}
</div>
{% endif %}
<div class="expand-meta">
{% if entry.owner %}<a
href="https://github.com/{{ entry.owner }}"
target="_blank"
rel="noopener"
>{{ entry.owner }}</a
><span class="expand-sep">/</span>{% endif %}<a
href="{{ entry.url }}"
target="_blank"
rel="noopener"
>{{ entry.url | replace("https://", "") }}</a
>
{% if entry.last_commit_at %}<span class="expand-commit"
><span class="expand-sep">/</span
><time datetime="{{ entry.last_commit_at }}"
>{{ entry.last_commit_at[:10] }}</time
></span
>{% endif %}
</div>
</div>
</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="no-results" hidden>
<p>No projects match your search or filter.</p>
<p class="no-results-hint">
Try a broader term, or
<button class="no-results-clear">browse all projects</button>.
</p>
</div>
</section>
<section class="final-cta" data-reveal>
<div class="section-shell">
<p class="section-label">Contribute</p>
<h2>Know a project that belongs here?</h2>
<p>Tell us what it does and why it stands out.</p>
<div class="final-cta-actions">
<a
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
class="hero-action hero-action-primary"
target="_blank"
rel="noopener"
>Submit a project</a
>
<a
href="https://github.com/vinta/awesome-python"
class="hero-action hero-action-secondary"
target="_blank"
rel="noopener"
>Star the repository</a
>
</div>
</div>
</section>
{% endblock %}

View file

@ -1,4 +1,7 @@
{% extends "base.html" %}
{% block extra_head %}
<script type="application/ld+json">{{ homepage_json_ld | safe }}</script>
{% endblock %}
{% block header %}
<header class="hero">
<div class="hero-sheen" aria-hidden="true"></div>
@ -61,6 +64,21 @@
{% endif %}
</div>
</div>
<nav class="hero-category-nav" aria-labelledby="hero-category-heading">
<div class="hero-category-meta">
<h2 id="hero-category-heading">Browse by category</h2>
</div>
<ul class="hero-category-links">
{% for category in categories %}
<li>
<a class="hero-category-link" href="{{ category_urls[category.name] }}"
>{{ category.name }}</a
>
</li>
{% endfor %}
</ul>
</nav>
</div>
</header>
{% endblock %}
@ -70,14 +88,7 @@
<div class="section-shell sponsor-shell">
<header class="sponsor-meta">
<p class="section-label" id="sponsor-heading">Sponsors</p>
<a
class="sponsor-become"
href="https://github.com/vinta/awesome-python/blob/master/SPONSORSHIP.md"
target="_blank"
rel="noopener"
>
Become a sponsor
</a>
<a class="sponsor-become" href="/sponsorship/"> Become a sponsor </a>
</header>
<ul class="sponsor-list">
{% for sponsor in sponsors %}
@ -98,6 +109,7 @@
</section>
{% endif %}
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
<section class="results-section" id="library-index">
<div class="results-intro section-shell" data-reveal>
<div>
@ -211,23 +223,47 @@
</td>
<td class="col-cat">
{% for subcat in entry.subcategories %}
<button class="tag" data-value="{{ subcat.value }}">
<a class="tag" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
{{ subcat.name }}
</button>
</a>
{% endfor %} {% for cat in entry.categories %}
<button class="tag" data-value="{{ cat }}">{{ cat }}</button>
<a
class="tag"
href="{{ category_urls[cat] }}"
data-value="{{ cat }}"
data-url="{{ category_urls[cat] }}"
>{{ cat }}</a
>
{% endfor %}
<button class="tag tag-group" data-value="{{ entry.groups[0] }}">
<a
class="tag tag-group"
href="{{ filter_urls[entry.groups[0]] }}"
data-value="{{ entry.groups[0] }}"
data-url="{{ filter_urls[entry.groups[0]] }}"
>
{{ entry.groups[0] }}
</button>
</a>
{% if entry.source_type == 'Built-in' %}
<button class="tag tag-source" data-value="Built-in">
<a
class="tag tag-source"
href="/categories/built-in/"
data-value="Built-in"
data-url="/categories/built-in/"
>
Built-in
</button>
</a>
{% endif %}
</td>
<td class="col-arrow"><span class="arrow">&rarr;</span></td>
</tr>
{% if entry.description %}
<tr class="desc-row" aria-hidden="true" hidden>
<td class="col-num"></td>
<td colspan="5">
<div class="desc-text">{{ entry.description | safe }}</div>
</td>
</tr>
{% endif %}
<tr class="expand-row" id="expand-{{ loop.index }}">
<td></td>
<td colspan="4">

View file

@ -0,0 +1,17 @@
# Awesome Python
Awesome Python is an opinionated catalog of {{ total_entries }} Python frameworks, libraries, tools, and resources across {{ total_categories }} {% if total_categories == 1 %}category{% else %}categories{% endif %}.
Scan the category index, then jump to the matching section for direct project links and short descriptions. GitHub entries with known star data end with a `GitHub stars: N` note in parentheses; treat it as popularity context, not a quality guarantee. Use the homepage for project context outside the catalog.
## Primary Links
- Homepage: {{ site_url }}
- GitHub repository: {{ github_repo_url }}
- Contributing guide: {{ contributing_url }}
- Sponsorship: {{ sponsorship_url }}
- Sitemap: {{ sitemap_url }}
## Categories
{{ categories_md }}

View file

@ -0,0 +1,250 @@
{% extends "base.html" %}
{% block title %}Sponsor Awesome Python{% endblock %}
{% block description %}Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use.{% endblock %}
{% block canonical_url %}https://awesome-python.com/sponsorship/{% endblock %}
{% block alternate_links %}{% endblock %}
{% block header %}
<header class="category-hero sponsorship-hero">
<div class="hero-sheen" aria-hidden="true"></div>
<div class="hero-noise" aria-hidden="true"></div>
<div class="category-hero-shell">
<nav class="hero-topbar category-topbar" aria-label="Site">
<a href="/" class="hero-brand-mini">Awesome Python</a>
<div class="hero-topbar-actions">
<a
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
class="hero-topbar-link hero-topbar-link-strong"
target="_blank"
rel="noopener"
>Submit a project</a
>
</div>
</nav>
<div class="category-hero-copy sponsorship-hero-copy">
<p class="hero-kicker">Sponsorship</p>
<h1>Sponsor Awesome Python</h1>
<p class="category-subtitle">
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.
</p>
{% if hero_stats %}
<p class="hero-proof sponsorship-proof">
{% for stat in hero_stats %}{{ stat }}{% if not loop.last %}
<span class="proof-sep">/</span> {% endif %}{% endfor %}
</p>
{% endif %}
<div class="hero-actions">
<a
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship"
class="hero-action hero-action-primary"
>Email vinta.chen@gmail.com</a
>
<a
href="https://github.com/vinta/awesome-python"
class="hero-action hero-action-secondary"
target="_blank"
rel="noopener"
>View on GitHub</a
>
</div>
</div>
</div>
</header>
{% endblock %} {% block content %}
<section class="sponsorship-section sponsorship-audience" data-reveal>
<div class="section-shell sponsorship-shell">
<header class="sponsorship-meta">
<p class="section-label">Audience</p>
</header>
<div class="sponsorship-body">
<p class="sponsorship-lede">
Professional Python developers evaluating libraries and tools for
production use. Not beginners browsing tutorials. People making adoption
decisions.
</p>
<dl class="sponsorship-facts">
<div>
<dt>Who visits</dt>
<dd>
Mid to senior Python developers arriving with a specific question:
a maintained ORM, a fast HTTP client, a task queue worth running in
production.
</dd>
</div>
<div>
<dt>Where they come from</dt>
<dd>
Google Search, GitHub, Reddit, YouTube, ChatGPT and other LLMs,
Hacker News.
</dd>
</div>
<div>
<dt>Why it works</dt>
<dd>
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.
</dd>
</div>
</dl>
</div>
</div>
</section>
<section class="sponsorship-section sponsorship-tiers" data-reveal>
<div class="section-shell sponsorship-shell">
<header class="sponsorship-meta">
<p class="section-label">Tiers</p>
<p class="sponsorship-meta-note">
One upfront payment per term. Setup takes less than 24 hours.
</p>
</header>
<div class="sponsorship-body">
<ol class="tier-list">
<li class="tier">
<p class="tier-eyebrow">Headline Sponsor</p>
<p class="tier-price">
<span class="tier-amount">$500</span>
<span class="tier-cadence">/ month</span>
</p>
<p class="tier-summary">
Logo pinned at the top of the README. Logo on the website.
</p>
<ul class="tier-includes">
<li>
Large logo and one-line description (max 120 characters) pinned at
the very top of the README, above all project entries.
</li>
<li>Logo link in the sponsor section of awesome-python.com.</li>
</ul>
<a
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship%20-%20Headline"
class="tier-cta"
>Email about Headline tier</a
>
</li>
<li class="tier">
<p class="tier-eyebrow">Featured Sponsor</p>
<p class="tier-price">
<span class="tier-amount">$150</span>
<span class="tier-cadence">/ month</span>
</p>
<p class="tier-summary">
Text link pinned at the top of the README. Text link on the website.
</p>
<ul class="tier-includes">
<li>
Text entry (<code>[Name](URL) - Description.</code>, max 120
characters) pinned at the top of the README, directly below
Headline sponsors.
</li>
<li>Text link in the sponsor section of awesome-python.com.</li>
</ul>
<a
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship%20-%20Featured"
class="tier-cta"
>Email about Featured tier</a
>
</li>
</ol>
</div>
</div>
</section>
<section class="sponsorship-section sponsorship-past" data-reveal>
<div class="section-shell sponsorship-shell">
<header class="sponsorship-meta">
<p class="section-label">Previously sponsored by</p>
</header>
<div class="sponsorship-body">
<ul class="past-sponsors">
<li>
<a href="https://www.warp.dev/" target="_blank" rel="noopener"
>Warp</a
>
<span class="past-sponsor-desc"
>The terminal for modern developers.</span
>
</li>
</ul>
</div>
</div>
</section>
<section class="sponsorship-section sponsorship-getstarted" data-reveal>
<div class="section-shell sponsorship-shell">
<header class="sponsorship-meta">
<p class="section-label">Get started</p>
</header>
<div class="sponsorship-body">
<p class="sponsorship-lede">
Email
<a
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship"
>vinta.chen@gmail.com</a
>
with the four items below.
</p>
<dl class="sponsorship-facts">
<div>
<dt>Tier</dt>
<dd>Headline Sponsor ($500/mo) or Featured Sponsor ($150/mo).</dd>
</div>
<div>
<dt>Content</dt>
<dd>
Product name, URL, logo, and description (Headline tier), or
<code>[Name](URL) - Description.</code> entry (Featured tier).
</dd>
</div>
<div>
<dt>Duration</dt>
<dd>1, 3, 6 months, or longer.</dd>
</div>
<div>
<dt>Payment method</dt>
<dd>US bank transfer (ACH/wire) or PayPal.</dd>
</div>
</dl>
<div class="sponsorship-cta-row">
<a
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship"
class="hero-action hero-action-primary"
>Email vinta.chen@gmail.com</a
>
</div>
</div>
</div>
</section>
<section class="sponsorship-section sponsorship-independence" data-reveal>
<div class="section-shell sponsorship-shell">
<header class="sponsorship-meta">
<p class="section-label">Editorial independence</p>
</header>
<div class="sponsorship-body">
<p>
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
<a
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
target="_blank"
rel="noopener"
>contribution process</a
>.
</p>
<p class="sponsorship-fineprint">
We reserve the right to request changes to sponsor text, logos, or links
that are misleading, off-topic, or incompatible with the README
formatting.
</p>
</div>
</div>
</section>
{% endblock %}

File diff suppressed because it is too large Load diff

View file

@ -17,41 +17,12 @@ class TestExtractGithubRepos:
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."
)
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_ignores_non_github_urls(self):
readme = "* [pypy](https://foss.heptapod.net/pypy/pypy) - Fast Python."
result = extract_github_repos(readme)
assert result == set()
def test_ignores_github_io_urls(self):
readme = "* [docs](https://user.github.io/project) - Docs site."
result = extract_github_repos(readme)
assert result == set()
def test_ignores_github_wiki_and_blob_urls(self):
readme = (
"* [wiki](https://github.com/org/repo/wiki) - Wiki.\n"
"* [file](https://github.com/org/repo/blob/main/f.py) - File."
)
result = extract_github_repos(readme)
assert result == set()
def test_handles_trailing_slash(self):
readme = "* [lib](https://github.com/org/repo/) - Lib."
result = extract_github_repos(readme)
assert result == {"org/repo"}
def test_deduplicates(self):
readme = (
"* [a](https://github.com/org/repo) - A.\n"
"* [b](https://github.com/org/repo) - B."
)
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"}

View file

@ -81,35 +81,35 @@ MINIMAL_README = textwrap.dedent("""\
Some intro text.
---
## Projects
## Alpha
### Alpha
_Libraries for alpha stuff._
- [lib-a](https://example.com/a) - Does A.
- [lib-b](https://example.com/b) - Does B.
## Beta
### Beta
_Tools for beta._
- [lib-c](https://example.com/c) - Does C.
# Resources
## Resources
Where to discover resources.
## Newsletters
### Newsletters
- [News One](https://example.com/n1)
- [News Two](https://example.com/n2)
## Podcasts
### Podcasts
- [Pod One](https://example.com/p1)
# Contributing
## Contributing
Please contribute!
""")
@ -120,11 +120,11 @@ GROUPED_README = textwrap.dedent("""\
Some intro text.
---
## Projects
**Group One**
## Alpha
### Alpha
_Libraries for alpha stuff._
@ -133,25 +133,25 @@ GROUPED_README = textwrap.dedent("""\
**Group Two**
## Beta
### Beta
_Tools for beta._
- [lib-c](https://example.com/c) - Does C.
## Gamma
### Gamma
- [lib-d](https://example.com/d) - Does D.
# Resources
## Resources
Where to discover resources.
## Newsletters
### Newsletters
- [News One](https://example.com/n1)
# Contributing
## Contributing
Please contribute!
""")
@ -180,7 +180,9 @@ class TestParseReadmeSections:
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)
@ -189,7 +191,7 @@ class TestParseReadmeSections:
all_names.extend(c["name"] for c in g["categories"])
assert "Contributing" not in all_names
def test_no_separator(self):
def test_no_projects_heading(self):
groups = parse_readme("# Just a heading\n\nSome text.\n")
assert groups == []
@ -197,46 +199,48 @@ class TestParseReadmeSections:
readme = textwrap.dedent("""\
# Title
---
## Projects
## NullDesc
### NullDesc
- [item](https://x.com) - Thing.
# Resources
## Resources
## Tips
### Tips
- [tip](https://x.com)
# Contributing
## 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
### Algos
_Algorithms. Also see [awesome-algos](https://example.com)._
- [lib](https://x.com) - Lib.
# Contributing
## 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 <a href="https://example.com" target="_blank" rel="noopener">awesome-algos</a>.'
class TestParseGroupedReadme:
@ -269,17 +273,17 @@ class TestParseGroupedReadme:
readme = textwrap.dedent("""\
# T
---
## Projects
**Empty**
**HasCats**
## Cat
### Cat
- [x](https://x.com) - X.
# Contributing
## Contributing
Done.
""")
@ -291,15 +295,15 @@ class TestParseGroupedReadme:
readme = textwrap.dedent("""\
# T
---
## Projects
**Note:** This is not a group marker.
## Cat
### Cat
- [x](https://x.com) - X.
# Contributing
## Contributing
Done.
""")
@ -313,19 +317,19 @@ class TestParseGroupedReadme:
readme = textwrap.dedent("""\
# T
---
## Projects
## Orphan
### Orphan
- [x](https://x.com) - X.
**A Group**
## Grouped
### Grouped
- [y](https://x.com) - Y.
# Contributing
## Contributing
Done.
""")
@ -346,10 +350,7 @@ def _content_nodes(md_text: str) -> list[SyntaxTreeNode]:
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"
)
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"
@ -366,13 +367,7 @@ class TestParseSectionEntries:
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"
)
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"
@ -410,15 +405,15 @@ class TestParseSectionEntries:
readme = textwrap.dedent("""\
# T
---
## Projects
## Async
### Async
- [asyncio](https://x.com) - Async I/O.
- [awesome-asyncio](https://y.com)
- [trio](https://z.com) - Friendly async.
# Contributing
## Contributing
Done.
""")
@ -428,7 +423,7 @@ class TestParseSectionEntries:
assert cats[0]["entry_count"] == 3
def test_description_html_escapes_xss(self):
nodes = _content_nodes('- [lib](https://x.com) - A <script>alert(1)</script> lib.\n')
nodes = _content_nodes("- [lib](https://x.com) - A <script>alert(1)</script> lib.\n")
entries = _parse_section_entries(nodes)
assert "<script>" not in entries[0]["description"]
assert "&lt;script&gt;" in entries[0]["description"]
@ -445,9 +440,6 @@ class TestParseRealReadme:
def test_at_least_11_groups(self):
assert len(self.groups) >= 11
def test_first_group_is_ai_ml(self):
assert self.groups[0]["name"] == "AI & ML"
def test_at_least_69_categories(self):
assert len(self.cats) >= 69
@ -455,38 +447,10 @@ class TestParseRealReadme:
all_names = [c["name"] for c in self.cats]
assert "Contributing" not in all_names
def test_first_category_is_ai_and_agents(self):
assert self.cats[0]["name"] == "AI and Agents"
assert self.cats[0]["slug"] == "ai-and-agents"
def test_web_apis_slug(self):
slugs = [c["slug"] for c in self.cats]
assert "web-apis" in slugs
def test_descriptions_extracted(self):
ai = next(c for c in self.cats if c["name"] == "AI and Agents")
assert "AI applications" in ai["description"]
def test_entry_counts_nonzero(self):
for cat in self.cats:
assert cat["entry_count"] > 0, f"{cat['name']} has 0 entries"
def test_async_has_also_see(self):
async_cat = next(c for c in self.cats if c["name"] == "Asynchronous Programming")
asyncio_entry = next(e for e in async_cat["entries"] if e["name"] == "asyncio")
assert len(asyncio_entry["also_see"]) >= 1
assert asyncio_entry["also_see"][0]["name"] == "awesome-asyncio"
def test_description_links_stripped_to_text(self):
algos = next(c for c in self.cats if c["name"] == "Algorithms and Design Patterns")
assert "awesome-algorithms" in algos["description"]
assert "https://" not in algos["description"]
def test_miscellaneous_in_own_group(self):
misc_group = next((g for g in self.groups if g["name"] == "Miscellaneous"), None)
assert misc_group is not None
assert any(c["name"] == "Miscellaneous" for c in misc_group["categories"])
def test_all_entries_have_nonempty_names(self):
bad = []
for cat in self.cats:
@ -516,21 +480,21 @@ class TestParseRealReadme:
md = MarkdownIt("commonmark")
root = SyntaxTreeNode(md.parse(self.readme_text))
# Find category section boundaries (between --- and # Resources/Contributing)
hr_idx = None
# Find category section boundaries (between Projects and Resources/Contributing)
projects_idx = None
end_idx = None
for i, node in enumerate(root.children):
if hr_idx is None and node.type == "hr":
hr_idx = i
elif node.type == "heading" and node.tag == "h1":
if node.type == "heading" and node.tag in ("h1", "h2"):
text = render_inline_text(node.children[0].children) if node.children else ""
if end_idx is None and text in ("Resources", "Contributing"):
if projects_idx is None and text == "Projects":
projects_idx = i
elif end_idx is None and text in ("Resources", "Contributing"):
end_idx = i
if hr_idx is None:
if projects_idx is None:
return
bad = []
cat_nodes = root.children[hr_idx + 1 : end_idx or len(root.children)]
cat_nodes = root.children[projects_idx + 1 : end_idx or len(root.children)]
for node in cat_nodes:
if node.type != "bullet_list":
continue