diff --git a/.gitignore b/.gitignore index ca26a6e..0d9f410 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.impeccable.md b/.impeccable.md deleted file mode 100644 index dd35e50..0000000 --- a/.impeccable.md +++ /dev/null @@ -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. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..1e678f2 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,223 @@ +--- +version: alpha +name: awesome-python.com +description: Warm editorial Python index. Light cream canvas, brown-red interactive accent, Cormorant Garamond plus Manrope, table-driven single-page reference. +--- + +# awesome-python.com DESIGN.md + +awesome-python.com is a searchable, filterable index of ~650 curated Python projects. It is a reference tool, not a landing page and not a GitHub README mirror. + +This file follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/overview/). The source of truth for token values lives in `website/static/style.css`. Color tokens here are written in OKLCH because the project mandates OKLCH over hex, which is a deliberate divergence from the spec's hex-only token requirement. + +## Overview + +Three words: **opinionated, confident, dense**. + +Working Python developers (mid to senior) are the target reader. They write Python daily and arrive with a specific question in mind: "what's a good HTTP client these days", "is there still a maintained ORM for X", "what are people using for task queues now". Secondary readers: polyglot developers evaluating Python's ecosystem, and curious browsers. + +Jobs to be done: + +1. Find a library for a specific need fast (search + tag filter). +2. Compare candidates at a glance (stars, last commit, tags, one-line description). +3. Confirm a project is alive before clicking through. + +These users skim. They reward density and terse copy. They penalize marketing fluff. + +Voice: + +- Editorial. Every word earns its place. +- Confident, not combative. "This is the list" energy, not "check out these cool projects". +- No hype. The content is what's interesting. +- Calm authority. Closer to a well-edited technical reference (O'Reilly index, The Economist briefing, a good man page) than a blog or product site. + +Emotional goals: trust, efficiency, craft. The reader should feel the list was edited by someone with taste, find what they need in seconds, and notice the typographic care as a signal that the curation is careful too. + +Reference points (stay close to these): + +- **https://www.placestoread.xyz** is the primary visual model for the table, expand row, sorting, and footer. "Like placestoread" means dense single-page list, inline click-to-expand rows that indent under the Name column, sortable headers, minimum decoration. When in doubt about a table or row treatment, check placestoread first. +- Magazine reference pages (The Economist, FT Weekend, Monocle). +- Field-guide books. Curated, functional, hand-made. +- Library card catalogs. Dense tabular information, excellent typography, no decoration for decoration's sake. + +Anti-references (avoid strictly): + +- Generic dark developer-tool look. No cyan on near-black, neon gradients, VSCode-palette dashboards, terminal-green monospace branding. +- Other awesome-\* sites. No plain README dumps, bare lists of links, no voice. +- SaaS marketing pages. No big metric counters, testimonial cards, feature grids, pricing tiers, or "join 10,000+ developers" social proof bands. + +Design principles: + +1. **The list is the hero.** Hero, sponsor band, and CTA exist, but they must not compete with the table for attention. +2. **Density is a feature.** Prefer tables and tight rhythm over giant cards with one fact each. Mid-senior developers want to see more at once. +3. **Editorial typography over decoration.** Visual interest comes from the serif/sans pairing, type hierarchy, and whitespace. Not from gradients, shadows, badges, or icon boxes above headings. +4. **Warm, not cool.** Neutrals tint toward warm hues (roughly 55 to 80 in OKLCH). Pure grays and cool blues do not belong. +5. **One point of view.** No dark mode, no theme picker, no alternate palettes. Consistency signals curation. + +## Colors + +Warm editorial palette. Light theme only (`color-scheme: light`). OKLCH only. + +Each token below shows the OKLCH value (canonical, lives in `style.css`) followed by an approximate hex sRGB equivalent for spec linters and any tool that expects hex. + +Surfaces: + +- `--bg-page` `oklch(96.8% 0.018 80)` ≈ `#FBF3E7`. Cream/ivory canvas, the body floor. The body uses a vertical gradient between `--bg-page-top` `oklch(95.2% 0.018 78)` ≈ `#F7EFE3`, `--bg-page` at 24rem, and `--bg-page-end` `oklch(98.4% 0.01 80)` ≈ `#FCF8F0`, with a soft radial highlight in the top-left corner. +- `--bg-paper` `oklch(98.6% 0.01 80)` ≈ `#FEFAF3`. Warm white for the content shell. +- `--bg-paper-strong` `oklch(95.7% 0.016 76)` ≈ `#F7F0E5`. Tinted paper for sponsor band, CTA backgrounds, static decoration. +- `--hero-bg-start` `oklch(14% 0.03 32)` ≈ `#130503` through `--hero-bg-mid` `oklch(19% 0.035 35)` ≈ `#22120B` to `--hero-bg-end` `oklch(28% 0.05 42)` ≈ `#3D2014`. Dark earthy hero gradient. +- `--footer-bg` `oklch(16% 0.025 35)` ≈ `#170906`. Dark warm charcoal footer, part of the same system. + +Ink: + +- `--ink` `oklch(22% 0.02 55)` ≈ `#221812`. Body text. +- `--ink-soft` `oklch(38% 0.018 55)` ≈ `#4A4039`. Secondary copy. +- `--ink-muted` `oklch(52% 0.02 55)` ≈ `#72665E`. Meta rows, captions, static labels. +- `--line` / `--line-strong`. Hairlines and dividers. + +Accent (warm brown-red, reserved for interactive): + +- `--accent` `oklch(58% 0.16 45)` ≈ `#C4530F`. Primary accent. +- `--accent-deep` `oklch(44% 0.15 42)` ≈ `#922900`. Link text, hover. +- `--accent-soft` `oklch(92% 0.045 55)` ≈ `#FDDDC9`. Tinted background for filter tags. +- `--accent-underline` `oklch(58% 0.16 45 / 0.4)` ≈ `#C4530F66`. Subtle text-decoration-color. + +Rules: + +- Use OKLCH for any new color. Not HSL, not hex. +- Accent tokens (`--accent`, `--accent-deep`, `--accent-soft`) are reserved for interactive elements. Clickable filter tags (`.tag`) correctly use `--accent-soft` background with `--accent-deep` text. Interactive link states (`.col-name > a:hover`, `.sponsor-link:hover`, `.hero-action-primary`, `.back-to-top`, CTAs) use accent tokens. +- Non-interactive elements (inline code, `.source-badge`, static labels, decorative pills) must use ink tokens (`--ink`, `--ink-soft`, `--ink-muted`) on `--bg-paper-strong` or `--bg-paper`, never the accent. `.source-badge` uses `--ink-soft`; `.sponsorship-body code` uses `--ink`. Users should not mistake static decoration for something clickable. +- Same role uses the same token everywhere. No one-off inline `color: oklch(...)` buried in a rule. + +Aversions: + +- **No green.** The user rejected it when picking the palette. Warm brown-red, ivory, and dark earthy tones are the established system. Do not introduce green even for success states or ancillary accents. +- No cyan, no neon gradients, no pure grays, no cool blues. + +## Typography + +Pairing (do not swap): + +- **Display**: `Cormorant Garamond` (serif, 600 only). +- **Body**: `Manrope` (sans, 400 / 600 / 700 / 800). + +Scale: + +| Role | Token | Size | Family | Weight | Notes | +| ---------------- | ----------------- | ----------------------------- | ------------------ | --------- | ---------------------------------------------- | +| Hero headline | (literal `clamp`) | `clamp(4.5rem, 11vw, 8.5rem)` | Cormorant Garamond | 600 | Magazine-cover scale, single use on the hero | +| Body large | `--text-lg` | `1.125rem` | Manrope | 400 | Content-heavy passages | +| Body | `--text-base` | `1rem` (16px) | Manrope | 400 | Body floor, do not go smaller | +| Meta / secondary | `--text-sm` | `0.95rem` | Manrope | 400 / 600 | Meta rows, secondary copy | +| Caption / pill | `--text-xs` | `0.8rem` (12.8px) | Manrope | 600 / 700 | Smallest token, pills, badges, tags, footnotes | + +Hard-won sizing rules (do not relax): + +- **Body type floor is 16px.** Do not go smaller. +- **Absolute minimum font size is 12px (`0.75rem`) for ANY text**, including pills, badges, tags, captions, footnotes. Anything smaller hits Chrome's default minimum-font-size floor and renders inconsistently across browsers and user accessibility settings. Use `var(--text-xs)` (`0.8rem`) as the smallest token in code. +- **When in doubt, pick one step larger** than what generic scale references suggest. The user has corrected sizes upward 11+ times across 8 sessions. Footer, meta rows, expand content, labels, and headings all trend too small by default. **Never reduce an existing size unprompted.** +- Adjacent heading levels differ by at least 0.25rem of rendered size. +- Row numbers in the table: left-align, no leading zeros. Zero-padding was tried and rejected. +- **Never `text-transform`.** Write the casing in the markup. + +## Layout + +- **Single width cap: `--shell-max: 84rem` (~1344px) applied via `.section-shell`.** This is the ONLY width cap in the project. Widescreen monitors are the default viewing context. +- **Do NOT add `max-width`** to sections, cards, table cells, table rows, expanded rows, CTA backgrounds, sponsor descriptions, hero subcopy, paragraphs, or list items. The user has removed narrow inner caps repeatedly (`56ch`, `65-75ch`, etc.). Default is no inner cap. +- The "cap line length at ~65-75ch" rule does NOT apply here. Ignore it. Readability at wide widths is carried by vertical rhythm, leading, and the modular type scale instead. +- If a width cap is genuinely necessary for a specific element, ask first with a concrete reason before adding it. +- Shell padding: `--shell-pad: clamp(1.25rem, 3vw, 2.5rem)`. Symmetric gutters: logo left-gap equals logo right-gap, column paddings match across header and body. +- `gap` over child margins in flex and grid. +- Logical properties (`margin-inline`, `padding-block`) over physical (`margin-left`, `padding-top`). +- `rem` for spacing and type. `px` only for borders and shadows. +- CSS custom properties for all colors and repeated values. +- Sibling components (card lists, grid items) share identical spacing. +- Use flexbox or grid for layout. Avoid floats and absolute positioning except for genuine overlay cases (focus rings, sticky headers). +- Never `!important`. Fix specificity instead. + +## Elevation & Depth + +Depth comes from **tonal layers**, not heavy shadows. + +- The page is a quiet warm canvas (`--bg-page`). The content shell is slightly brighter paper (`--bg-paper`). The sponsor band, CTA backgrounds, and inline decorative blocks step up to `--bg-paper-strong`. +- The hero is the one place that uses real atmosphere: subtle grid, slow sheen, warm radial gradients on a dark earthy ground (`--hero-bg-start` → `--hero-bg-mid` → `--hero-bg-end`). The sheen and any other motion respect `prefers-reduced-motion`. +- The footer is a single tonal block in `--footer-bg`, no internal gradients. +- Two depth treatments are allowed and only these two. The search input combines a 1px inset highlight (`--search-inset`) with a soft warm drop shadow (`--search-shadow`, intensified by `--search-focus-shadow` on focus). The primary CTA button (`.hero-action-primary`) carries a warm drop shadow for press affordance. Both shadows are soft, warm-tinted, and tied to interactive elements. No new drop shadows on cards, panels, rows, or static decoration. +- No glassmorphism as default decoration. +- No bounce or elastic easing. Real objects decelerate smoothly. + +## Shapes + +The shape language is overwhelmingly **pill on small, zero radius on large**. + +- **Pills** (`border-radius: 999px`) for tags, search, sponsor logo chip, source badges, back-to-top, and primary CTA buttons. +- **`0.4rem`** is used in exactly one place: inline `` inside `.sponsorship-body`. Do not introduce a tokenized radius scale. The project does not need one. +- Containers use the page surface itself, not rounded panels. When a panel is needed, prefer pill on small chips and zero radius on large surfaces. +- **No `border-left` or `border-right` greater than 1px as a colored accent stripe** on cards, list items, callouts, or alerts. Use a different structure. + +## Components + +The component vocabulary is small and table-led. Source of truth: `website/static/style.css`. + +- **Table-driven index** (the hero of the page). Sticky header, sortable columns, click-to-expand rows that indent under the Name column. Modeled on placestoread.xyz. Not a card grid. +- **Filter tags** (`.tag`). `--accent-soft` background with `--accent-deep` text. Pill shape. Hover swaps to `--highlight` background with `--tag-hover-border` border and ink text. Active state uses the warm `--tag-active-start` → `--tag-active-end` gradient with hero-ink text. Tag variants (`tag-group`, `tag-source`) inherit the base `.tag` style today and differ only at narrow widths (`tag-group` hides under 960px). Add a new variant only when a real visual difference is needed. +- **Hero**. Magazine-cover headline, dark earthy ground, kicker and proof microcopy, primary CTA button using `--hero-btn-start` / `--hero-btn-end`. Subtle grid plus slow sheen. Respects `prefers-reduced-motion`. +- **Sponsor band**. Sits in the README header on `--bg-paper-strong`. Editorial layout, not a logo wall. Sponsor links share the global accent treatment. +- **CTA**. Warm `--cta-bg`, full-bleed within shell. The button itself uses accent tokens. +- **Footer**. Dark warm charcoal, part of the same system. Footer links share the global hover and focus treatment. +- **Search**. Pill input with `--search-inset` interior and `--search-focus-ring` focus ring. Focus shadow uses `--search-focus-shadow`. +- **Source badge / inline code**. Static decoration on `--bg-paper-strong`. `.source-badge` uses `--ink-soft` text in pill shape; `.sponsorship-body code` uses `--ink` text with the lone `0.4rem` radius. Never the accent. + +Peer-consistency check (run before shipping any visual change): + +- Hover and focus states: if one link type gets a treatment, peer links (hero topbar, footer, project names, sponsor names, expand-meta) share it. +- Tag variants inherit the base `.tag` style. Differ only where a real difference is needed. +- Typography tiers: labels that play the same role share size, weight, and letter-spacing. +- Symmetric gutters: logo left-gap equals logo right-gap, column paddings match across header and body. +- Role-based color tokens: same role uses the same token everywhere. + +## Do's and Don'ts + +- **Do** keep the table the focal point. Hero, sponsor band, and CTA must not compete. +- **Do** use accent tokens only on interactive elements. +- **Do** prefer density over whitespace expansion. +- **Do** check peer elements before shipping a visual change. +- **Do** use OKLCH for every new color. +- **Don't** add inner `max-width` to anything. The shell handles width. +- **Don't** introduce green, cyan, neon, pure gray, or cool blue. +- **Don't** add a dark mode, theme picker, or alternate palette. +- **Don't** use gradient text (`background-clip: text` on gradients). Solid color only. +- **Don't** use `!important`. Fix specificity instead. +- **Don't** use `text-transform`. Write the casing in markup. +- **Don't** use a `border-left` or `border-right` greater than 1px as an accent stripe. +- **Don't** use bounce or elastic easing. +- **Don't** use glassmorphism as default decoration. +- **Don't** mimic generic dark developer-tool sites, other awesome-\* sites, or SaaS marketing pages. + +## Narrow-Screen Behavior + +The user actively tests `< 960px` and `< 680px`. Narrow screens must stay functional. + +- Do not drop features the user might want (sort affordance, filter chips, sticky header where reasonable). Hiding is a last resort and requires justification. +- Always run the `playwright-cli` skill at a narrow viewport after any layout change. + +## Iteration Guide + +Run this audit after generating or modifying a screen. Failure on any item means revise before moving on. + +1. **Width caps.** Inspect every section, card, paragraph, table cell, expanded row, CTA, sponsor description, hero subcopy. Only `.section-shell` (`--shell-max: 84rem`) may cap width. Anything else with a `max-width` is wrong. +2. **Accent reservation.** Grep the changed CSS for `--accent`, `--accent-deep`, `--accent-soft`. Each match must back an interactive element (link, button, focus ring, filter tag). Static decoration must use ink tokens (`--ink`, `--ink-soft`, `--ink-muted`) on `--bg-paper-strong` or `--bg-paper`. +3. **Shape language.** Containers are square or pill. Anything in the 4px-to-16px radius range is suspect. The lone `0.4rem` on `.sponsorship-body code` is the only allowed exception. +4. **Type sizes.** Confirm no rendered text falls below 12px. If a size feels small to a mid-senior reader on a 27-inch display, bump one step up. Never reduce an existing size. +5. **Peer consistency.** Compare against the closest peer element (sibling link type, sibling tag variant, sibling label). Hover, focus, color token, and gutter must match unless there is a stated reason to differ. +6. **Narrow viewport.** Run the `playwright-cli` skill at `< 960px` and `< 680px`. Sort affordance, filter chips, and sticky header must remain functional. + +## Known Gaps + +- **Color format diverges from the Stitch spec.** The official linter requires hex sRGB (`/^#([0-9a-fA-F]{3,8})$/`). The project mandates OKLCH in `style.css`. The Colors section above resolves this by showing both: OKLCH is canonical, hex is the linter-friendly approximation. +- **YAML frontmatter is minimal.** Only `version`, `name`, and `description` are encoded. The project has no JSON / Figma export pipeline that would consume token-level frontmatter, so the prose-led format is preferred for everything else. +- **No formal spacing or radius scale.** The codebase uses `clamp()` and ad-hoc rem values rather than a tokenized scale. Adding one would be invention, not documentation. + +## Verification + +After any frontend change, use the `playwright-cli` skill to visually verify in a real browser. Check layout, responsiveness, and interactive behavior. Do not claim a UI change works based on code alone. diff --git a/Makefile b/Makefile index 5b78254..6430945 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index b0b88d7..81493ae 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/SPONSORSHIP.md b/SPONSORSHIP.md index b833748..debd6f9 100644 --- a/SPONSORSHIP.md +++ b/SPONSORSHIP.md @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 06e008b..a1f0917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/uv.lock b/uv.lock index 88d2b27..635a3db 100644 --- a/uv.lock +++ b/uv.lock @@ -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" diff --git a/website/build.py b/website/build.py index c223ef1..96cf050 100644 --- a/website/build.py +++ b/website/build.py @@ -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(" 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(" (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}") diff --git a/website/fetch_github_stars.py b/website/fetch_github_stars.py index c93ef4e..48aaacf 100644 --- a/website/fetch_github_stars.py +++ b/website/fetch_github_stars.py @@ -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) + " }" diff --git a/website/readme_parser.py b/website/readme_parser.py index 10d2688..61bf1e9 100644 --- a/website/readme_parser.py +++ b/website/readme_parser.py @@ -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 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 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) diff --git a/website/static/main.js b/website/static/main.js index 7353ff2..d5b337b 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -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(); +}); diff --git a/website/static/style.css b/website/static/style.css index ec395e9..bbdac35 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -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; diff --git a/website/templates/base.html b/website/templates/base.html index 34546e7..dc16434 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -1,26 +1,30 @@ + {% 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 %} - {% block title %}Awesome Python{% endblock %} - - + {{ meta_title | trim }} + + + {% block alternate_links %} + + {% endblock %} - - - - - + + + + + + + + @@ -49,6 +53,7 @@ gtag("js", new Date()); gtag("config", "G-0LMLYE0HER"); + {% block extra_head %}{% endblock %} @@ -65,7 +70,9 @@