mirror of
https://github.com/vinta/awesome-python.git
synced 2026-06-09 18:01:42 +00:00
Merge branch 'master' into add-promptise-foundry
This commit is contained in:
commit
2547c1dcfb
21 changed files with 2984 additions and 563 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
124
.impeccable.md
124
.impeccable.md
|
|
@ -1,124 +0,0 @@
|
|||
# Design Context
|
||||
|
||||
awesome-python.com is a searchable, filterable index of ~650 curated Python projects. It is a reference tool, not a landing page and not a GitHub README mirror.
|
||||
|
||||
## Users
|
||||
|
||||
Working Python developers (mid to senior). They already write Python daily and arrive with a specific question in mind: "what's a good HTTP client these days", "is there still a maintained ORM for X", "what are people using for task queues now". Secondary readers: polyglot developers evaluating Python's ecosystem, and curious browsers.
|
||||
|
||||
Jobs to be done:
|
||||
|
||||
1. Find a library for a specific need fast (search + tag filter).
|
||||
2. Compare candidates at a glance (stars, last commit, tags, one-line description).
|
||||
3. Confirm a project is alive before clicking through.
|
||||
|
||||
These users skim. They reward density and terse copy. They penalize marketing fluff.
|
||||
|
||||
## Brand Personality
|
||||
|
||||
Three words: **opinionated, confident, dense**.
|
||||
|
||||
Voice:
|
||||
|
||||
- Editorial. Every word earns its place.
|
||||
- Confident, not combative. "This is the list" energy, not "check out these cool projects".
|
||||
- No hype. The content is what's interesting.
|
||||
- Calm authority. Closer to a well-edited technical reference (O'Reilly index, The Economist briefing, a good man page) than a blog or product site.
|
||||
|
||||
Emotional goals: trust, efficiency, craft. The reader should feel the list was edited by someone with taste, find what they need in seconds, and notice the typographic care as a signal that the curation is careful too.
|
||||
|
||||
## Aesthetic Direction
|
||||
|
||||
Stay close to the current direction. It works.
|
||||
|
||||
- Warm editorial palette in OKLCH. Cream/ivory page, dark earthy hero, warm brown-red accent near `oklch(58% 0.16 45)`.
|
||||
- Type pairing: `Cormorant Garamond` (serif display, 600) with `Manrope` (sans body, 400/600/700/800). Do not swap.
|
||||
- Magazine-cover scale for the main headline (`clamp(4.5rem, 11vw, 8.5rem)`), then a tight modular scale for the rest.
|
||||
- Textured hero: subtle grid, slow sheen, warm radial gradients. Respect `prefers-reduced-motion`.
|
||||
- Light theme only (`color-scheme: light`). No dark mode toggle, no alternate palettes.
|
||||
- Table-driven index (sticky header, sortable columns, expandable rows). Not a card grid.
|
||||
- Dark warm charcoal footer, part of the same system.
|
||||
|
||||
References (what to stay close to):
|
||||
|
||||
- **https://www.placestoread.xyz** is the primary visual model for the table, expand row, sorting, and footer. "Like placestoread" means dense single-page list, inline click-to-expand rows that indent under the Name column, sortable headers, minimum decoration. When in doubt about a table or row treatment, check placestoread first.
|
||||
- Magazine reference pages (The Economist, FT Weekend, Monocle).
|
||||
- Field-guide books. Curated, functional, hand-made.
|
||||
- Library card catalogs. Dense tabular information, excellent typography, no decoration for decoration's sake.
|
||||
|
||||
Color aversions:
|
||||
|
||||
- No green. The user rejected it when picking the palette. Warm brown-red, ivory, and dark earthy tones are the established system. Do not introduce green even for success states or ancillary accents.
|
||||
|
||||
Anti-references (avoid strictly):
|
||||
|
||||
- Generic dark developer-tool look. No cyan on near-black, neon gradients, VSCode-palette dashboards, terminal-green monospace branding.
|
||||
- Other awesome-* sites. No plain README dumps, bare lists of links, no voice.
|
||||
- SaaS marketing pages. No big metric counters, testimonial cards, feature grids, pricing tiers, or "join 10,000+ developers" social proof bands.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **The list is the hero.** Hero, sponsor band, and CTA exist, but they must not compete with the table for attention.
|
||||
2. **Density is a feature.** Prefer tables and tight rhythm over giant cards with one fact each. Mid-senior developers want to see more at once.
|
||||
3. **Editorial typography over decoration.** Visual interest comes from the serif/sans pairing, type hierarchy, and whitespace. Not from gradients, shadows, badges, or icon boxes above headings.
|
||||
4. **Warm, not cool.** Neutrals tint toward warm hues (roughly 55 to 80 in OKLCH). Pure grays and cool blues do not belong.
|
||||
5. **One point of view.** No dark mode, no theme picker, no alternate palettes. Consistency signals curation.
|
||||
|
||||
## Implementation Rules
|
||||
|
||||
The project already follows these. Future work must keep them.
|
||||
|
||||
Layout and sizing:
|
||||
|
||||
- Keep existing `--shell-max: 84rem` (~1344px) applied via `.section-shell`. This is the ONLY width cap in the project. Widescreen monitors are the default viewing context.
|
||||
- Do NOT add `max-width` to sections, cards, table cells, table rows, expanded rows, CTA backgrounds, sponsor descriptions, hero subcopy, paragraphs, or list items. The user has removed narrow inner caps repeatedly (`56ch`, `65-75ch`, etc.). Default is no inner cap.
|
||||
- The impeccable skill reference rule "cap line length at ~65-75ch" does NOT apply here. Ignore it. Readability at wide widths is carried by vertical rhythm, leading, and the modular type scale instead.
|
||||
- If you believe a width cap is actually necessary for some specific element, ask first with a concrete reason before adding it.
|
||||
- Body type floor is 16px (`--text-base: 1rem`). Content-heavy passages may go to 1.125rem.
|
||||
- When in doubt about any type size, pick one step larger than what the impeccable skill's scale references suggest. The user has repeatedly corrected sizes upward (11+ separate requests across 8 sessions). Never reduce an existing size unprompted. Footer, meta rows, expand content, labels, headings all trend too small by default.
|
||||
- Row numbers in the table: left-align, no leading zeros. The user tried zero-padding and rejected it.
|
||||
- Adjacent heading levels differ by at least 0.25rem of rendered size.
|
||||
|
||||
Color:
|
||||
|
||||
- Use OKLCH for any new color. Not HSL, not hex.
|
||||
- Accent colors (`--accent`, `--accent-deep`, `--accent-soft`) are reserved for interactive elements. Clickable filter tags (`.tag`) correctly use `--accent-soft` background with `--accent-deep` text. Interactive link states (`.col-name > a:hover`, `.sponsor-link:hover`, `.hero-action-primary`, `.back-to-top`, CTAs) use accent tokens.
|
||||
- Non-interactive elements (inline code, `.source-badge`, static labels, decorative pills) must use `--ink-muted`, `--ink-soft`, or `--bg-paper-strong`. Never the accent. Users should not mistake static decoration for something clickable.
|
||||
|
||||
CSS hygiene:
|
||||
|
||||
- CSS custom properties for all colors and repeated values.
|
||||
- `rem` for spacing and type. `px` only for borders and shadows.
|
||||
- `gap` over child margins in flex and grid.
|
||||
- Logical properties (`margin-inline`, `padding-block`) over physical (`margin-left`, `padding-top`).
|
||||
- Never `!important`. Fix specificity instead.
|
||||
- Never `text-transform`. Write the casing in the markup.
|
||||
- Sibling components (card lists, grid items) share identical spacing.
|
||||
|
||||
Visual consistency check:
|
||||
|
||||
Before shipping any visual change, check peer elements. The user catches inconsistencies repeatedly.
|
||||
|
||||
- Hover and focus states: if one link type gets a hover treatment, peer links (hero topbar, footer, project names, sponsor names, expand-meta) share it.
|
||||
- Tag variants (group, subcat, source, built-in) inherit the base `.tag` style and differ only where a real difference is needed.
|
||||
- Typography tiers: labels that play the same role share size, weight, and letter-spacing.
|
||||
- Symmetric gutters: logo left-gap equals logo right-gap, column paddings match across header and body.
|
||||
- Role-based color tokens: same role uses the same token everywhere. No one-off inline `color: oklch(...)` buried in a rule.
|
||||
|
||||
Narrow-screen behavior:
|
||||
|
||||
The user actively tests `< 960px` and `< 680px`. Narrow screens must stay functional.
|
||||
|
||||
- Do not drop features that the user might want (sort affordance, filter chips, sticky header where reasonable). Hiding is a last resort and requires justification.
|
||||
- Always run the `playwright-cli` skill at a narrow viewport after any layout change.
|
||||
|
||||
Absolute bans (from the impeccable skill):
|
||||
|
||||
- No `border-left` or `border-right` greater than 1px as a colored accent stripe on cards, list items, callouts, or alerts. Use a different structure.
|
||||
- No gradient text (`background-clip: text` on gradients). Solid color only.
|
||||
- No glassmorphism as default decoration.
|
||||
- No bounce or elastic easing. Real objects decelerate smoothly.
|
||||
|
||||
## Verification
|
||||
|
||||
After any frontend change, use the `playwright-cli` skill to visually verify in a real browser. Check layout, responsiveness, and interactive behavior. Do not claim a UI change works based on code alone.
|
||||
223
DESIGN.md
Normal file
223
DESIGN.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
---
|
||||
version: alpha
|
||||
name: awesome-python.com
|
||||
description: Warm editorial Python index. Light cream canvas, brown-red interactive accent, Cormorant Garamond plus Manrope, table-driven single-page reference.
|
||||
---
|
||||
|
||||
# awesome-python.com DESIGN.md
|
||||
|
||||
awesome-python.com is a searchable, filterable index of ~650 curated Python projects. It is a reference tool, not a landing page and not a GitHub README mirror.
|
||||
|
||||
This file follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/overview/). The source of truth for token values lives in `website/static/style.css`. Color tokens here are written in OKLCH because the project mandates OKLCH over hex, which is a deliberate divergence from the spec's hex-only token requirement.
|
||||
|
||||
## Overview
|
||||
|
||||
Three words: **opinionated, confident, dense**.
|
||||
|
||||
Working Python developers (mid to senior) are the target reader. They write Python daily and arrive with a specific question in mind: "what's a good HTTP client these days", "is there still a maintained ORM for X", "what are people using for task queues now". Secondary readers: polyglot developers evaluating Python's ecosystem, and curious browsers.
|
||||
|
||||
Jobs to be done:
|
||||
|
||||
1. Find a library for a specific need fast (search + tag filter).
|
||||
2. Compare candidates at a glance (stars, last commit, tags, one-line description).
|
||||
3. Confirm a project is alive before clicking through.
|
||||
|
||||
These users skim. They reward density and terse copy. They penalize marketing fluff.
|
||||
|
||||
Voice:
|
||||
|
||||
- Editorial. Every word earns its place.
|
||||
- Confident, not combative. "This is the list" energy, not "check out these cool projects".
|
||||
- No hype. The content is what's interesting.
|
||||
- Calm authority. Closer to a well-edited technical reference (O'Reilly index, The Economist briefing, a good man page) than a blog or product site.
|
||||
|
||||
Emotional goals: trust, efficiency, craft. The reader should feel the list was edited by someone with taste, find what they need in seconds, and notice the typographic care as a signal that the curation is careful too.
|
||||
|
||||
Reference points (stay close to these):
|
||||
|
||||
- **https://www.placestoread.xyz** is the primary visual model for the table, expand row, sorting, and footer. "Like placestoread" means dense single-page list, inline click-to-expand rows that indent under the Name column, sortable headers, minimum decoration. When in doubt about a table or row treatment, check placestoread first.
|
||||
- Magazine reference pages (The Economist, FT Weekend, Monocle).
|
||||
- Field-guide books. Curated, functional, hand-made.
|
||||
- Library card catalogs. Dense tabular information, excellent typography, no decoration for decoration's sake.
|
||||
|
||||
Anti-references (avoid strictly):
|
||||
|
||||
- Generic dark developer-tool look. No cyan on near-black, neon gradients, VSCode-palette dashboards, terminal-green monospace branding.
|
||||
- Other awesome-\* sites. No plain README dumps, bare lists of links, no voice.
|
||||
- SaaS marketing pages. No big metric counters, testimonial cards, feature grids, pricing tiers, or "join 10,000+ developers" social proof bands.
|
||||
|
||||
Design principles:
|
||||
|
||||
1. **The list is the hero.** Hero, sponsor band, and CTA exist, but they must not compete with the table for attention.
|
||||
2. **Density is a feature.** Prefer tables and tight rhythm over giant cards with one fact each. Mid-senior developers want to see more at once.
|
||||
3. **Editorial typography over decoration.** Visual interest comes from the serif/sans pairing, type hierarchy, and whitespace. Not from gradients, shadows, badges, or icon boxes above headings.
|
||||
4. **Warm, not cool.** Neutrals tint toward warm hues (roughly 55 to 80 in OKLCH). Pure grays and cool blues do not belong.
|
||||
5. **One point of view.** No dark mode, no theme picker, no alternate palettes. Consistency signals curation.
|
||||
|
||||
## Colors
|
||||
|
||||
Warm editorial palette. Light theme only (`color-scheme: light`). OKLCH only.
|
||||
|
||||
Each token below shows the OKLCH value (canonical, lives in `style.css`) followed by an approximate hex sRGB equivalent for spec linters and any tool that expects hex.
|
||||
|
||||
Surfaces:
|
||||
|
||||
- `--bg-page` `oklch(96.8% 0.018 80)` ≈ `#FBF3E7`. Cream/ivory canvas, the body floor. The body uses a vertical gradient between `--bg-page-top` `oklch(95.2% 0.018 78)` ≈ `#F7EFE3`, `--bg-page` at 24rem, and `--bg-page-end` `oklch(98.4% 0.01 80)` ≈ `#FCF8F0`, with a soft radial highlight in the top-left corner.
|
||||
- `--bg-paper` `oklch(98.6% 0.01 80)` ≈ `#FEFAF3`. Warm white for the content shell.
|
||||
- `--bg-paper-strong` `oklch(95.7% 0.016 76)` ≈ `#F7F0E5`. Tinted paper for sponsor band, CTA backgrounds, static decoration.
|
||||
- `--hero-bg-start` `oklch(14% 0.03 32)` ≈ `#130503` through `--hero-bg-mid` `oklch(19% 0.035 35)` ≈ `#22120B` to `--hero-bg-end` `oklch(28% 0.05 42)` ≈ `#3D2014`. Dark earthy hero gradient.
|
||||
- `--footer-bg` `oklch(16% 0.025 35)` ≈ `#170906`. Dark warm charcoal footer, part of the same system.
|
||||
|
||||
Ink:
|
||||
|
||||
- `--ink` `oklch(22% 0.02 55)` ≈ `#221812`. Body text.
|
||||
- `--ink-soft` `oklch(38% 0.018 55)` ≈ `#4A4039`. Secondary copy.
|
||||
- `--ink-muted` `oklch(52% 0.02 55)` ≈ `#72665E`. Meta rows, captions, static labels.
|
||||
- `--line` / `--line-strong`. Hairlines and dividers.
|
||||
|
||||
Accent (warm brown-red, reserved for interactive):
|
||||
|
||||
- `--accent` `oklch(58% 0.16 45)` ≈ `#C4530F`. Primary accent.
|
||||
- `--accent-deep` `oklch(44% 0.15 42)` ≈ `#922900`. Link text, hover.
|
||||
- `--accent-soft` `oklch(92% 0.045 55)` ≈ `#FDDDC9`. Tinted background for filter tags.
|
||||
- `--accent-underline` `oklch(58% 0.16 45 / 0.4)` ≈ `#C4530F66`. Subtle text-decoration-color.
|
||||
|
||||
Rules:
|
||||
|
||||
- Use OKLCH for any new color. Not HSL, not hex.
|
||||
- Accent tokens (`--accent`, `--accent-deep`, `--accent-soft`) are reserved for interactive elements. Clickable filter tags (`.tag`) correctly use `--accent-soft` background with `--accent-deep` text. Interactive link states (`.col-name > a:hover`, `.sponsor-link:hover`, `.hero-action-primary`, `.back-to-top`, CTAs) use accent tokens.
|
||||
- Non-interactive elements (inline code, `.source-badge`, static labels, decorative pills) must use ink tokens (`--ink`, `--ink-soft`, `--ink-muted`) on `--bg-paper-strong` or `--bg-paper`, never the accent. `.source-badge` uses `--ink-soft`; `.sponsorship-body code` uses `--ink`. Users should not mistake static decoration for something clickable.
|
||||
- Same role uses the same token everywhere. No one-off inline `color: oklch(...)` buried in a rule.
|
||||
|
||||
Aversions:
|
||||
|
||||
- **No green.** The user rejected it when picking the palette. Warm brown-red, ivory, and dark earthy tones are the established system. Do not introduce green even for success states or ancillary accents.
|
||||
- No cyan, no neon gradients, no pure grays, no cool blues.
|
||||
|
||||
## Typography
|
||||
|
||||
Pairing (do not swap):
|
||||
|
||||
- **Display**: `Cormorant Garamond` (serif, 600 only).
|
||||
- **Body**: `Manrope` (sans, 400 / 600 / 700 / 800).
|
||||
|
||||
Scale:
|
||||
|
||||
| Role | Token | Size | Family | Weight | Notes |
|
||||
| ---------------- | ----------------- | ----------------------------- | ------------------ | --------- | ---------------------------------------------- |
|
||||
| Hero headline | (literal `clamp`) | `clamp(4.5rem, 11vw, 8.5rem)` | Cormorant Garamond | 600 | Magazine-cover scale, single use on the hero |
|
||||
| Body large | `--text-lg` | `1.125rem` | Manrope | 400 | Content-heavy passages |
|
||||
| Body | `--text-base` | `1rem` (16px) | Manrope | 400 | Body floor, do not go smaller |
|
||||
| Meta / secondary | `--text-sm` | `0.95rem` | Manrope | 400 / 600 | Meta rows, secondary copy |
|
||||
| Caption / pill | `--text-xs` | `0.8rem` (12.8px) | Manrope | 600 / 700 | Smallest token, pills, badges, tags, footnotes |
|
||||
|
||||
Hard-won sizing rules (do not relax):
|
||||
|
||||
- **Body type floor is 16px.** Do not go smaller.
|
||||
- **Absolute minimum font size is 12px (`0.75rem`) for ANY text**, including pills, badges, tags, captions, footnotes. Anything smaller hits Chrome's default minimum-font-size floor and renders inconsistently across browsers and user accessibility settings. Use `var(--text-xs)` (`0.8rem`) as the smallest token in code.
|
||||
- **When in doubt, pick one step larger** than what generic scale references suggest. The user has corrected sizes upward 11+ times across 8 sessions. Footer, meta rows, expand content, labels, and headings all trend too small by default. **Never reduce an existing size unprompted.**
|
||||
- Adjacent heading levels differ by at least 0.25rem of rendered size.
|
||||
- Row numbers in the table: left-align, no leading zeros. Zero-padding was tried and rejected.
|
||||
- **Never `text-transform`.** Write the casing in the markup.
|
||||
|
||||
## Layout
|
||||
|
||||
- **Single width cap: `--shell-max: 84rem` (~1344px) applied via `.section-shell`.** This is the ONLY width cap in the project. Widescreen monitors are the default viewing context.
|
||||
- **Do NOT add `max-width`** to sections, cards, table cells, table rows, expanded rows, CTA backgrounds, sponsor descriptions, hero subcopy, paragraphs, or list items. The user has removed narrow inner caps repeatedly (`56ch`, `65-75ch`, etc.). Default is no inner cap.
|
||||
- The "cap line length at ~65-75ch" rule does NOT apply here. Ignore it. Readability at wide widths is carried by vertical rhythm, leading, and the modular type scale instead.
|
||||
- If a width cap is genuinely necessary for a specific element, ask first with a concrete reason before adding it.
|
||||
- Shell padding: `--shell-pad: clamp(1.25rem, 3vw, 2.5rem)`. Symmetric gutters: logo left-gap equals logo right-gap, column paddings match across header and body.
|
||||
- `gap` over child margins in flex and grid.
|
||||
- Logical properties (`margin-inline`, `padding-block`) over physical (`margin-left`, `padding-top`).
|
||||
- `rem` for spacing and type. `px` only for borders and shadows.
|
||||
- CSS custom properties for all colors and repeated values.
|
||||
- Sibling components (card lists, grid items) share identical spacing.
|
||||
- Use flexbox or grid for layout. Avoid floats and absolute positioning except for genuine overlay cases (focus rings, sticky headers).
|
||||
- Never `!important`. Fix specificity instead.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
Depth comes from **tonal layers**, not heavy shadows.
|
||||
|
||||
- The page is a quiet warm canvas (`--bg-page`). The content shell is slightly brighter paper (`--bg-paper`). The sponsor band, CTA backgrounds, and inline decorative blocks step up to `--bg-paper-strong`.
|
||||
- The hero is the one place that uses real atmosphere: subtle grid, slow sheen, warm radial gradients on a dark earthy ground (`--hero-bg-start` → `--hero-bg-mid` → `--hero-bg-end`). The sheen and any other motion respect `prefers-reduced-motion`.
|
||||
- The footer is a single tonal block in `--footer-bg`, no internal gradients.
|
||||
- Two depth treatments are allowed and only these two. The search input combines a 1px inset highlight (`--search-inset`) with a soft warm drop shadow (`--search-shadow`, intensified by `--search-focus-shadow` on focus). The primary CTA button (`.hero-action-primary`) carries a warm drop shadow for press affordance. Both shadows are soft, warm-tinted, and tied to interactive elements. No new drop shadows on cards, panels, rows, or static decoration.
|
||||
- No glassmorphism as default decoration.
|
||||
- No bounce or elastic easing. Real objects decelerate smoothly.
|
||||
|
||||
## Shapes
|
||||
|
||||
The shape language is overwhelmingly **pill on small, zero radius on large**.
|
||||
|
||||
- **Pills** (`border-radius: 999px`) for tags, search, sponsor logo chip, source badges, back-to-top, and primary CTA buttons.
|
||||
- **`0.4rem`** is used in exactly one place: inline `<code>` inside `.sponsorship-body`. Do not introduce a tokenized radius scale. The project does not need one.
|
||||
- Containers use the page surface itself, not rounded panels. When a panel is needed, prefer pill on small chips and zero radius on large surfaces.
|
||||
- **No `border-left` or `border-right` greater than 1px as a colored accent stripe** on cards, list items, callouts, or alerts. Use a different structure.
|
||||
|
||||
## Components
|
||||
|
||||
The component vocabulary is small and table-led. Source of truth: `website/static/style.css`.
|
||||
|
||||
- **Table-driven index** (the hero of the page). Sticky header, sortable columns, click-to-expand rows that indent under the Name column. Modeled on placestoread.xyz. Not a card grid.
|
||||
- **Filter tags** (`.tag`). `--accent-soft` background with `--accent-deep` text. Pill shape. Hover swaps to `--highlight` background with `--tag-hover-border` border and ink text. Active state uses the warm `--tag-active-start` → `--tag-active-end` gradient with hero-ink text. Tag variants (`tag-group`, `tag-source`) inherit the base `.tag` style today and differ only at narrow widths (`tag-group` hides under 960px). Add a new variant only when a real visual difference is needed.
|
||||
- **Hero**. Magazine-cover headline, dark earthy ground, kicker and proof microcopy, primary CTA button using `--hero-btn-start` / `--hero-btn-end`. Subtle grid plus slow sheen. Respects `prefers-reduced-motion`.
|
||||
- **Sponsor band**. Sits in the README header on `--bg-paper-strong`. Editorial layout, not a logo wall. Sponsor links share the global accent treatment.
|
||||
- **CTA**. Warm `--cta-bg`, full-bleed within shell. The button itself uses accent tokens.
|
||||
- **Footer**. Dark warm charcoal, part of the same system. Footer links share the global hover and focus treatment.
|
||||
- **Search**. Pill input with `--search-inset` interior and `--search-focus-ring` focus ring. Focus shadow uses `--search-focus-shadow`.
|
||||
- **Source badge / inline code**. Static decoration on `--bg-paper-strong`. `.source-badge` uses `--ink-soft` text in pill shape; `.sponsorship-body code` uses `--ink` text with the lone `0.4rem` radius. Never the accent.
|
||||
|
||||
Peer-consistency check (run before shipping any visual change):
|
||||
|
||||
- Hover and focus states: if one link type gets a treatment, peer links (hero topbar, footer, project names, sponsor names, expand-meta) share it.
|
||||
- Tag variants inherit the base `.tag` style. Differ only where a real difference is needed.
|
||||
- Typography tiers: labels that play the same role share size, weight, and letter-spacing.
|
||||
- Symmetric gutters: logo left-gap equals logo right-gap, column paddings match across header and body.
|
||||
- Role-based color tokens: same role uses the same token everywhere.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
- **Do** keep the table the focal point. Hero, sponsor band, and CTA must not compete.
|
||||
- **Do** use accent tokens only on interactive elements.
|
||||
- **Do** prefer density over whitespace expansion.
|
||||
- **Do** check peer elements before shipping a visual change.
|
||||
- **Do** use OKLCH for every new color.
|
||||
- **Don't** add inner `max-width` to anything. The shell handles width.
|
||||
- **Don't** introduce green, cyan, neon, pure gray, or cool blue.
|
||||
- **Don't** add a dark mode, theme picker, or alternate palette.
|
||||
- **Don't** use gradient text (`background-clip: text` on gradients). Solid color only.
|
||||
- **Don't** use `!important`. Fix specificity instead.
|
||||
- **Don't** use `text-transform`. Write the casing in markup.
|
||||
- **Don't** use a `border-left` or `border-right` greater than 1px as an accent stripe.
|
||||
- **Don't** use bounce or elastic easing.
|
||||
- **Don't** use glassmorphism as default decoration.
|
||||
- **Don't** mimic generic dark developer-tool sites, other awesome-\* sites, or SaaS marketing pages.
|
||||
|
||||
## Narrow-Screen Behavior
|
||||
|
||||
The user actively tests `< 960px` and `< 680px`. Narrow screens must stay functional.
|
||||
|
||||
- Do not drop features the user might want (sort affordance, filter chips, sticky header where reasonable). Hiding is a last resort and requires justification.
|
||||
- Always run the `playwright-cli` skill at a narrow viewport after any layout change.
|
||||
|
||||
## Iteration Guide
|
||||
|
||||
Run this audit after generating or modifying a screen. Failure on any item means revise before moving on.
|
||||
|
||||
1. **Width caps.** Inspect every section, card, paragraph, table cell, expanded row, CTA, sponsor description, hero subcopy. Only `.section-shell` (`--shell-max: 84rem`) may cap width. Anything else with a `max-width` is wrong.
|
||||
2. **Accent reservation.** Grep the changed CSS for `--accent`, `--accent-deep`, `--accent-soft`. Each match must back an interactive element (link, button, focus ring, filter tag). Static decoration must use ink tokens (`--ink`, `--ink-soft`, `--ink-muted`) on `--bg-paper-strong` or `--bg-paper`.
|
||||
3. **Shape language.** Containers are square or pill. Anything in the 4px-to-16px radius range is suspect. The lone `0.4rem` on `.sponsorship-body code` is the only allowed exception.
|
||||
4. **Type sizes.** Confirm no rendered text falls below 12px. If a size feels small to a mid-senior reader on a 27-inch display, bump one step up. Never reduce an existing size.
|
||||
5. **Peer consistency.** Compare against the closest peer element (sibling link type, sibling tag variant, sibling label). Hover, focus, color token, and gutter must match unless there is a stated reason to differ.
|
||||
6. **Narrow viewport.** Run the `playwright-cli` skill at `< 960px` and `< 680px`. Sort affordance, filter chips, and sticky header must remain functional.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Color format diverges from the Stitch spec.** The official linter requires hex sRGB (`/^#([0-9a-fA-F]{3,8})$/`). The project mandates OKLCH in `style.css`. The Colors section above resolves this by showing both: OKLCH is canonical, hex is the linter-friendly approximation.
|
||||
- **YAML frontmatter is minimal.** Only `version`, `name`, and `description` are encoded. The project has no JSON / Figma export pipeline that would consume token-level frontmatter, so the prose-led format is preferred for everything else.
|
||||
- **No formal spacing or radius scale.** The codebase uses `clamp()` and ad-hoc rem values rather than a tokenized scale. Adding one would be invention, not documentation.
|
||||
|
||||
## Verification
|
||||
|
||||
After any frontend change, use the `playwright-cli` skill to visually verify in a real browser. Check layout, responsiveness, and interactive behavior. Do not claim a UI change works based on code alone.
|
||||
9
Makefile
9
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
|
||||
|
||||
|
|
|
|||
187
README.md
187
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Repository = "https://github.com/vinta/awesome-python"
|
|||
|
||||
[dependency-groups]
|
||||
build = ["httpx==0.28.1", "jinja2==3.1.6", "markdown-it-py==4.0.0"]
|
||||
lint = ["ruff==0.15.6"]
|
||||
lint = ["ruff==0.15.6", "ty==0.0.33"]
|
||||
test = ["pytest==9.0.3"]
|
||||
dev = [
|
||||
{ include-group = "build" },
|
||||
|
|
@ -23,16 +23,30 @@ dev = [
|
|||
"watchdog==6.0.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["website/tests"]
|
||||
pythonpath = ["website"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 200
|
||||
|
||||
[tool.uv]
|
||||
exclude-newer = "3 days"
|
||||
no-build = true
|
||||
|
||||
[tool.uv.pip]
|
||||
only-binary = [":all:"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 200
|
||||
|
||||
[tool.ty.environment]
|
||||
python-version = "3.13"
|
||||
root = ["website"]
|
||||
|
||||
[tool.ty.terminal]
|
||||
error-on-warning = true
|
||||
|
||||
[tool.ty.rules]
|
||||
division-by-zero = "error"
|
||||
possibly-missing-attribute = "error"
|
||||
possibly-missing-import = "error"
|
||||
possibly-unresolved-reference = "error"
|
||||
unused-ignore-comment = "error"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["website/tests"]
|
||||
pythonpath = ["website"]
|
||||
|
|
|
|||
34
uv.lock
generated
34
uv.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
498
website/build.py
498
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("</", "<\\/")
|
||||
homepage_json_ld = json.dumps(
|
||||
build_homepage_json_ld(entries, len(categories)),
|
||||
ensure_ascii=False,
|
||||
).replace("</", "<\\/")
|
||||
|
||||
tpl_index = env.get_template("index.html")
|
||||
(site_dir / "index.html").write_text(
|
||||
tpl_index.render(
|
||||
|
|
@ -171,20 +491,146 @@ def build(repo_root: Path) -> None:
|
|||
total_entries=total_entries,
|
||||
total_categories=len(categories),
|
||||
repo_stars=repo_stars,
|
||||
build_date=datetime.now(UTC).strftime("%B %d, %Y"),
|
||||
build_date=build_date.strftime("%B %d, %Y"),
|
||||
sponsors=sponsors,
|
||||
category_urls=category_urls,
|
||||
filter_urls=filter_urls,
|
||||
filter_urls_json=filter_urls_json,
|
||||
homepage_json_ld=homepage_json_ld,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
tpl_category = env.get_template("category.html")
|
||||
categories_dir = site_dir / "categories"
|
||||
|
||||
def render_category(
|
||||
category: TemplateCategory,
|
||||
*,
|
||||
category_url: str,
|
||||
entries: Sequence[TemplateEntry],
|
||||
current_path: str,
|
||||
page_dir: Path,
|
||||
parent_category: ParsedSection | None = None,
|
||||
group_categories: Sequence[ParsedSection] | None = None,
|
||||
) -> None:
|
||||
page_dir.mkdir(parents=True, exist_ok=True)
|
||||
category_description = category_meta_description(category["name"], len(entries), category["description"])
|
||||
category_json_ld = json.dumps(
|
||||
build_category_json_ld(category["name"], category_url, category_description, entries),
|
||||
ensure_ascii=False,
|
||||
).replace("</", "<\\/")
|
||||
(page_dir / "index.html").write_text(
|
||||
tpl_category.render(
|
||||
category=category,
|
||||
category_url=category_url,
|
||||
category_description=category_description,
|
||||
entries=entries,
|
||||
total_categories=len(categories),
|
||||
category_urls=category_urls,
|
||||
current_path=current_path,
|
||||
filter_urls=filter_urls,
|
||||
filter_urls_json=filter_urls_json,
|
||||
parent_category=parent_category,
|
||||
group_categories=group_categories,
|
||||
category_json_ld=category_json_ld,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
for category in categories:
|
||||
render_category(
|
||||
category,
|
||||
category_url=category_public_url(category),
|
||||
entries=[e for e in entries if category["name"] in e["categories"]],
|
||||
current_path=category_path(category),
|
||||
page_dir=categories_dir / category["slug"],
|
||||
)
|
||||
|
||||
for group in parsed_groups:
|
||||
render_category(
|
||||
synthetic_category(group["name"], group["slug"]),
|
||||
category_url=group_public_url(group["slug"]),
|
||||
entries=[e for e in entries if group["name"] in e["groups"]],
|
||||
current_path=group_path(group["slug"]),
|
||||
page_dir=categories_dir / group["slug"],
|
||||
group_categories=group["categories"],
|
||||
)
|
||||
|
||||
if builtin_entries:
|
||||
render_category(
|
||||
synthetic_category(BUILTIN_FILTER, BUILTIN_SLUG),
|
||||
category_url=BUILTIN_PUBLIC_URL,
|
||||
entries=builtin_entries,
|
||||
current_path=BUILTIN_PATH,
|
||||
page_dir=categories_dir / BUILTIN_SLUG,
|
||||
)
|
||||
|
||||
sponsorship_dir = site_dir / "sponsorship"
|
||||
sponsorship_dir.mkdir(parents=True, exist_ok=True)
|
||||
tpl_sponsorship = env.get_template("sponsorship.html")
|
||||
hero_stats: list[str] = []
|
||||
if repo_stars:
|
||||
hero_stats.append(f"{repo_stars}+ stars on GitHub")
|
||||
hero_stats.append(f"Updated {build_date.strftime('%B %d, %Y')}")
|
||||
(sponsorship_dir / "index.html").write_text(
|
||||
tpl_sponsorship.render(hero_stats=hero_stats),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
subcat_to_entries: dict[str, list[TemplateEntry]] = {}
|
||||
subcat_meta: dict[str, tuple[str, str, str]] = {} # value -> (cat_slug, sub_slug, sub_name)
|
||||
cat_slug_by_url_prefix = {f"/categories/{c['slug']}/": c["slug"] for c in categories}
|
||||
cat_by_slug = {c["slug"]: c for c in categories}
|
||||
for entry in entries:
|
||||
for sub in entry.get("subcategories", []):
|
||||
value = sub["value"]
|
||||
subcat_to_entries.setdefault(value, []).append(entry)
|
||||
if value not in subcat_meta:
|
||||
for prefix, cat_slug in cat_slug_by_url_prefix.items():
|
||||
if sub["url"].startswith(prefix):
|
||||
subcat_meta[value] = (cat_slug, sub["slug"], sub["name"])
|
||||
break
|
||||
|
||||
for value, (cat_slug, sub_slug, sub_name) in subcat_meta.items():
|
||||
render_category(
|
||||
synthetic_category(sub_name, sub_slug),
|
||||
category_url=subcategory_public_url(cat_slug, sub_slug),
|
||||
entries=subcat_to_entries[value],
|
||||
current_path=subcategory_path(cat_slug, sub_slug),
|
||||
page_dir=categories_dir / cat_slug / sub_slug,
|
||||
parent_category=cat_by_slug[cat_slug],
|
||||
)
|
||||
|
||||
static_src = website / "static"
|
||||
static_dst = site_dir / "static"
|
||||
if static_src.exists():
|
||||
shutil.copytree(static_src, static_dst, dirs_exist_ok=True)
|
||||
|
||||
(site_dir / "llms.txt").write_text(readme_text, encoding="utf-8")
|
||||
sponsorship_md = repo_root / "SPONSORSHIP.md"
|
||||
sponsorship_md_mtime = datetime.fromtimestamp(sponsorship_md.stat().st_mtime, tz=UTC).date().isoformat()
|
||||
llms_template = (website / "templates" / "llms.txt").read_text(encoding="utf-8")
|
||||
llms_txt = build_llms_txt(
|
||||
llms_template,
|
||||
readme_text=readme_text,
|
||||
stars_data=stars_data,
|
||||
categories=categories,
|
||||
total_entries=total_entries,
|
||||
)
|
||||
(site_dir / "robots.txt").write_text(build_robots_txt(), encoding="utf-8")
|
||||
sitemap_date = build_date.date().isoformat()
|
||||
sitemap_urls = [(SITE_URL, sitemap_date)]
|
||||
sitemap_urls.extend((category_public_url(c), sitemap_date) for c in categories)
|
||||
sitemap_urls.extend((group_public_url(g["slug"]), sitemap_date) for g in parsed_groups)
|
||||
if builtin_entries:
|
||||
sitemap_urls.append((BUILTIN_PUBLIC_URL, sitemap_date))
|
||||
for cat_slug, sub_slug, _ in sorted(subcat_meta.values()):
|
||||
sitemap_urls.append((subcategory_public_url(cat_slug, sub_slug), sitemap_date))
|
||||
sitemap_urls.append((SPONSORSHIP_PUBLIC_URL, sponsorship_md_mtime))
|
||||
write_sitemap_xml(site_dir / "sitemap.xml", sitemap_urls)
|
||||
(site_dir / "llms.txt").write_text(llms_txt, encoding="utf-8")
|
||||
|
||||
print(f"Built single page with {len(parsed_groups)} groups, {len(categories)} categories")
|
||||
print(f"Built site with {len(parsed_groups)} groups, {len(categories)} categories")
|
||||
print(f"Total entries: {total_entries}")
|
||||
print(f"Output: {site_dir}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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) + " }"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class ParsedSection(TypedDict):
|
|||
name: str
|
||||
slug: str
|
||||
description: str # plain text, links resolved to text
|
||||
description_html: str # inline HTML, properly escaped
|
||||
entries: list[ParsedEntry]
|
||||
entry_count: int
|
||||
|
||||
|
|
@ -113,22 +114,29 @@ def _heading_text(node: SyntaxTreeNode) -> str:
|
|||
return ""
|
||||
|
||||
|
||||
def _extract_description(nodes: list[SyntaxTreeNode]) -> str:
|
||||
"""Extract description from the first paragraph if it's a single <em> block.
|
||||
def _heading_level(node: SyntaxTreeNode) -> int | None:
|
||||
"""Return the numeric level for a heading node."""
|
||||
if node.type != "heading" or not node.tag.startswith("h"):
|
||||
return None
|
||||
return int(node.tag[1:])
|
||||
|
||||
|
||||
def _extract_description_children(nodes: list[SyntaxTreeNode]) -> list[SyntaxTreeNode]:
|
||||
"""Extract description children from the first paragraph if it's a single <em> block.
|
||||
|
||||
Pattern: _Libraries for foo._ -> "Libraries for foo."
|
||||
"""
|
||||
if not nodes:
|
||||
return ""
|
||||
return []
|
||||
first = nodes[0]
|
||||
if first.type != "paragraph":
|
||||
return ""
|
||||
return []
|
||||
for child in first.children:
|
||||
if child.type == "inline" and len(child.children) == 1:
|
||||
em = child.children[0]
|
||||
if em.type == "em":
|
||||
return render_inline_text(em.children)
|
||||
return ""
|
||||
return em.children
|
||||
return []
|
||||
|
||||
|
||||
# --- Entry extraction --------------------------------------------------------
|
||||
|
|
@ -228,18 +236,22 @@ def _parse_list_entries(
|
|||
if sub_inline:
|
||||
sub_link = _find_child(sub_inline, "link")
|
||||
if sub_link:
|
||||
also_see.append(AlsoSee(
|
||||
name=render_inline_text(sub_link.children),
|
||||
url=_href(sub_link),
|
||||
))
|
||||
also_see.append(
|
||||
AlsoSee(
|
||||
name=render_inline_text(sub_link.children),
|
||||
url=_href(sub_link),
|
||||
)
|
||||
)
|
||||
|
||||
entries.append(ParsedEntry(
|
||||
name=name,
|
||||
url=url,
|
||||
description=desc_html,
|
||||
also_see=also_see,
|
||||
subcategory=subcategory,
|
||||
))
|
||||
entries.append(
|
||||
ParsedEntry(
|
||||
name=name,
|
||||
url=url,
|
||||
description=desc_html,
|
||||
also_see=also_see,
|
||||
subcategory=subcategory,
|
||||
)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
|
|
@ -258,20 +270,22 @@ def _parse_section_entries(content_nodes: list[SyntaxTreeNode]) -> list[ParsedEn
|
|||
|
||||
def _build_section(name: str, body: list[SyntaxTreeNode]) -> ParsedSection:
|
||||
"""Build a ParsedSection from a heading name and its body nodes."""
|
||||
desc = _extract_description(body)
|
||||
content_nodes = body[1:] if desc else body
|
||||
desc_children = _extract_description_children(body)
|
||||
desc = render_inline_text(desc_children) if desc_children else ""
|
||||
desc_html = render_inline_html(desc_children) if desc_children else ""
|
||||
content_nodes = body[1:] if desc_children else body
|
||||
entries = _parse_section_entries(content_nodes)
|
||||
entry_count = len(entries) + sum(len(e["also_see"]) for e in entries)
|
||||
return ParsedSection(
|
||||
name=name,
|
||||
slug=slugify(name),
|
||||
description=desc,
|
||||
description_html=desc_html,
|
||||
entries=entries,
|
||||
entry_count=entry_count,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _is_bold_marker(node: SyntaxTreeNode) -> str | None:
|
||||
"""Detect a bold-only paragraph used as a group marker.
|
||||
|
||||
|
|
@ -296,7 +310,7 @@ def _parse_grouped_sections(
|
|||
) -> list[ParsedGroup]:
|
||||
"""Parse nodes into groups of categories using bold markers as group boundaries.
|
||||
|
||||
Bold-only paragraphs (**Group Name**) delimit groups. H2 headings under each
|
||||
Bold-only paragraphs (**Group Name**) delimit groups. H3 headings under each
|
||||
bold marker become categories within that group. Categories appearing before
|
||||
any bold marker go into an "Other" group.
|
||||
"""
|
||||
|
|
@ -317,11 +331,13 @@ def _parse_grouped_sections(
|
|||
nonlocal current_group_name, current_group_cats
|
||||
if current_group_cats:
|
||||
name = current_group_name or "Other"
|
||||
groups.append(ParsedGroup(
|
||||
name=name,
|
||||
slug=slugify(name),
|
||||
categories=list(current_group_cats),
|
||||
))
|
||||
groups.append(
|
||||
ParsedGroup(
|
||||
name=name,
|
||||
slug=slugify(name),
|
||||
categories=list(current_group_cats),
|
||||
)
|
||||
)
|
||||
current_group_name = None
|
||||
current_group_cats = []
|
||||
|
||||
|
|
@ -332,7 +348,7 @@ def _parse_grouped_sections(
|
|||
flush_group()
|
||||
current_group_name = bold_name
|
||||
current_cat_body = []
|
||||
elif node.type == "heading" and node.tag == "h2":
|
||||
elif node.type == "heading" and node.tag in ("h2", "h3"):
|
||||
flush_cat()
|
||||
current_cat_name = _heading_text(node)
|
||||
current_cat_body = []
|
||||
|
|
@ -374,7 +390,7 @@ def _parse_sponsor_item(inline: SyntaxTreeNode) -> ParsedSponsor | None:
|
|||
|
||||
|
||||
def parse_sponsors(text: str) -> list[ParsedSponsor]:
|
||||
"""Parse the `# Sponsors` section of README.md into a list of sponsors.
|
||||
"""Parse the `Sponsors` section of README.md into a list of sponsors.
|
||||
|
||||
Expects bullets in the form `**[name](url)**: description`.
|
||||
Returns [] if no Sponsors section exists.
|
||||
|
|
@ -386,14 +402,18 @@ def parse_sponsors(text: str) -> list[ParsedSponsor]:
|
|||
|
||||
start_idx = None
|
||||
end_idx = len(children)
|
||||
start_level = None
|
||||
for i, node in enumerate(children):
|
||||
if node.type == "heading" and node.tag == "h1":
|
||||
title = _heading_text(node).strip().lower()
|
||||
if start_idx is None and title == "sponsors":
|
||||
start_idx = i + 1
|
||||
elif start_idx is not None:
|
||||
end_idx = i
|
||||
break
|
||||
level = _heading_level(node)
|
||||
if level is None:
|
||||
continue
|
||||
title = _heading_text(node).strip().lower()
|
||||
if start_idx is None and title == "sponsors":
|
||||
start_idx = i + 1
|
||||
start_level = level
|
||||
elif start_idx is not None and start_level is not None and level <= start_level:
|
||||
end_idx = i
|
||||
break
|
||||
if start_idx is None:
|
||||
return []
|
||||
|
||||
|
|
@ -417,26 +437,26 @@ def parse_readme(text: str) -> list[ParsedGroup]:
|
|||
"""Parse README.md text into grouped categories.
|
||||
|
||||
Returns a list of ParsedGroup dicts containing nested categories.
|
||||
Content between the thematic break (---) and # Resources or # Contributing
|
||||
is parsed as categories grouped by bold markers (**Group Name**).
|
||||
Content between the Projects heading and Resources or Contributing is parsed
|
||||
as categories grouped by bold markers (**Group Name**).
|
||||
"""
|
||||
md = MarkdownIt("commonmark")
|
||||
tokens = md.parse(text)
|
||||
root = SyntaxTreeNode(tokens)
|
||||
children = root.children
|
||||
|
||||
# Find thematic break (---) and section boundaries in one pass
|
||||
hr_idx = None
|
||||
# Find Projects and section boundaries in one pass.
|
||||
projects_idx = None
|
||||
cat_end_idx = None
|
||||
for i, node in enumerate(children):
|
||||
if hr_idx is None and node.type == "hr":
|
||||
hr_idx = i
|
||||
elif node.type == "heading" and node.tag == "h1":
|
||||
if _heading_level(node) in (1, 2):
|
||||
text_content = _heading_text(node)
|
||||
if cat_end_idx is None and text_content in ("Resources", "Contributing"):
|
||||
if projects_idx is None and text_content == "Projects":
|
||||
projects_idx = i
|
||||
elif cat_end_idx is None and text_content in ("Resources", "Contributing"):
|
||||
cat_end_idx = i
|
||||
if hr_idx is None:
|
||||
if projects_idx is None:
|
||||
return []
|
||||
|
||||
cat_nodes = children[hr_idx + 1 : cat_end_idx or len(children)]
|
||||
cat_nodes = children[projects_idx + 1 : cat_end_idx or len(children)]
|
||||
return _parse_grouped_sections(cat_nodes)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,30 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% set default_meta_title = "Awesome Python" %}
|
||||
{% set default_meta_description = "An opinionated guide to the best Python frameworks, libraries, and tools. Explore " ~ (entries | length) ~ " curated projects across " ~ total_categories ~ " categories, from AI and agents to data science and web development." %}
|
||||
{% set default_canonical_url = "https://awesome-python.com/" %}
|
||||
{% set social_image_url = "https://awesome-python.com/static/og-image.png" %}
|
||||
{% set meta_title %}{% block title %}{{ default_meta_title }}{% endblock %}{% endset %}
|
||||
{% set meta_description %}{% block description %}{{ default_meta_description }}{% endblock %}{% endset %}
|
||||
{% set canonical_url %}{% block canonical_url %}{{ default_canonical_url }}{% endblock %}{% endset %}
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{% block title %}Awesome Python{% endblock %}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="{% block description %}An opinionated list of Python frameworks, libraries, tools, and resources. {{ total_entries }} projects across {{ categories | length }} categories.{% endblock %}"
|
||||
/>
|
||||
<link rel="canonical" href="https://awesome-python.com/" />
|
||||
<title>{{ meta_title | trim }}</title>
|
||||
<meta name="description" content="{{ meta_description | trim }}" />
|
||||
<link rel="canonical" href="{{ canonical_url | trim }}" />
|
||||
{% block alternate_links %}
|
||||
<link rel="alternate" type="text/plain" href="/llms.txt" title="LLMs text entry point" />
|
||||
{% endblock %}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Awesome Python" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="An opinionated list of Python frameworks, libraries, tools, and resources."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://awesome-python.com/static/og-image.png"
|
||||
/>
|
||||
<meta property="og:url" content="https://awesome-python.com/" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta property="og:title" content="{{ meta_title | trim }}" />
|
||||
<meta property="og:description" content="{{ meta_description | trim }}" />
|
||||
<meta property="og:image" content="{{ social_image_url }}" />
|
||||
<meta property="og:url" content="{{ canonical_url | trim }}" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="{{ meta_title | trim }}" />
|
||||
<meta name="twitter:description" content="{{ meta_description | trim }}" />
|
||||
<meta name="twitter:image" content="{{ social_image_url }}" />
|
||||
<meta name="theme-color" content="#1c1410" />
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
|
|
@ -49,6 +53,7 @@
|
|||
gtag("js", new Date());
|
||||
gtag("config", "G-0LMLYE0HER");
|
||||
</script>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<a href="#content" class="skip-link">Skip to content</a>
|
||||
|
|
@ -65,7 +70,9 @@
|
|||
|
||||
<footer class="footer">
|
||||
<div class="footer-left">
|
||||
<span class="footer-brand">Awesome Python</span>
|
||||
<a href="/" class="footer-brand">Awesome Python</a>
|
||||
<span class="footer-sep">/</span>
|
||||
<a href="/sponsorship/">Sponsorship</a>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<span
|
||||
|
|
|
|||
294
website/templates/category.html
Normal file
294
website/templates/category.html
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{ category.name }} Python Libraries - Awesome Python{% endblock %}
|
||||
{% block description %}{{ category_description }}{% endblock %}
|
||||
{% block canonical_url %}{{ category_url }}{% endblock %}
|
||||
{% block alternate_links %}{% endblock %}
|
||||
{% block extra_head %}
|
||||
<script type="application/ld+json">{{ category_json_ld | safe }}</script>
|
||||
{% endblock %}
|
||||
{% block header %}
|
||||
<header class="category-hero">
|
||||
<div class="hero-sheen" aria-hidden="true"></div>
|
||||
<div class="hero-noise" aria-hidden="true"></div>
|
||||
|
||||
<div class="category-hero-shell">
|
||||
<nav class="hero-topbar category-topbar" aria-label="Site">
|
||||
<a href="/" class="hero-brand-mini">Awesome Python</a>
|
||||
<div class="hero-topbar-actions">
|
||||
<a href="/#library-index" class="hero-topbar-link">All projects</a>
|
||||
<a
|
||||
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
|
||||
class="hero-topbar-link hero-topbar-link-strong"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Submit a project</a
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="category-hero-copy">
|
||||
{% if parent_category %}
|
||||
<p class="category-breadcrumb">
|
||||
<a href="/categories/{{ parent_category.slug }}/">{{ parent_category.name }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<h1>{{ category.name }}</h1>
|
||||
{% if category.description_html %}
|
||||
<p class="category-subtitle">{{ category.description_html | safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if group_categories %}
|
||||
<nav class="hero-category-nav" aria-labelledby="hero-category-heading">
|
||||
<div class="hero-category-meta">
|
||||
<h2 id="hero-category-heading">Browse by category</h2>
|
||||
</div>
|
||||
<ul class="hero-category-links">
|
||||
{% for sub in group_categories %}
|
||||
<li>
|
||||
<a class="hero-category-link" href="{{ category_urls[sub.name] }}"
|
||||
>{{ sub.name }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
|
||||
<section class="results-section" id="library-index">
|
||||
<div class="results-intro section-shell" data-reveal>
|
||||
<div>
|
||||
<h2>Search every project in one place</h2>
|
||||
</div>
|
||||
<p class="results-note">
|
||||
Press <kbd>/</kbd> to search. Tap a tag to filter. Click any row for
|
||||
details.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="controls section-shell" data-reveal>
|
||||
<h2 class="sr-only">Search and filter</h2>
|
||||
<div class="search-wrap">
|
||||
<svg
|
||||
class="search-icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
class="search"
|
||||
placeholder="Search {{ entries | length }} projects in {{ category.name }}..."
|
||||
aria-label="Search projects"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-bar" aria-live="polite">
|
||||
<span>Filtering for <strong class="filter-value"></strong></span>
|
||||
<button class="filter-clear" aria-label="Clear filter">
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sr-only">Results</h2>
|
||||
<div
|
||||
class="table-wrap"
|
||||
tabindex="0"
|
||||
role="region"
|
||||
aria-label="Libraries table"
|
||||
>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-num"><span class="sr-only">Row number</span></th>
|
||||
<th class="col-name" data-sort="name">
|
||||
<button type="button" class="sort-btn">Project Name</button>
|
||||
</th>
|
||||
<th class="col-stars" data-sort="stars">
|
||||
<button type="button" class="sort-btn">GitHub Stars</button>
|
||||
</th>
|
||||
<th class="col-commit" data-sort="commit-time">
|
||||
<button type="button" class="sort-btn">Last Commit</button>
|
||||
</th>
|
||||
<th class="col-cat">Tags</th>
|
||||
<th class="col-arrow">
|
||||
<button class="back-to-top" aria-label="Back to top">
|
||||
Top ↑
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr
|
||||
class="row"
|
||||
data-tags="{{ entry.categories | join('||') }}{% if entry.subcategories %}||{{ entry.subcategories | map(attribute='value') | join('||') }}{% endif %}||{{ entry.groups | join('||') }}{% if entry.source_type == 'Built-in' %}||Built-in{% endif %}"
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
aria-controls="expand-{{ loop.index }}"
|
||||
>
|
||||
<td class="col-num">{{ loop.index }}</td>
|
||||
<td class="col-name">
|
||||
<a href="{{ entry.url }}" target="_blank" rel="noopener"
|
||||
>{{ entry.name }}</a
|
||||
>
|
||||
<span class="mobile-cat"
|
||||
>{% if entry.subcategories %}{{ entry.subcategories[0].name }}{%
|
||||
else %}{{ category.name }}{% endif %}</span
|
||||
>
|
||||
</td>
|
||||
<td class="col-stars">
|
||||
{% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{%
|
||||
elif entry.source_type %}<span class="source-badge"
|
||||
>{{ entry.source_type }}</span
|
||||
>{% else %}—{% endif %}
|
||||
</td>
|
||||
<td
|
||||
class="col-commit"
|
||||
{%
|
||||
if
|
||||
entry.last_commit_at
|
||||
%}data-commit="{{ entry.last_commit_at }}"
|
||||
{%
|
||||
endif
|
||||
%}
|
||||
>
|
||||
{% if entry.last_commit_at %}<time
|
||||
datetime="{{ entry.last_commit_at }}"
|
||||
>{{ entry.last_commit_at[:10] }}</time
|
||||
>{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="col-cat">
|
||||
{% for subcat in entry.subcategories %}
|
||||
<a class="tag{% if subcat.url == current_path %} active{% endif %}" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
|
||||
{{ subcat.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% for cat in entry.categories %}
|
||||
<a
|
||||
class="tag{% if category_urls[cat] == current_path %} active{% endif %}"
|
||||
href="{{ category_urls[cat] }}"
|
||||
data-value="{{ cat }}"
|
||||
data-url="{{ category_urls[cat] }}"
|
||||
>{{ cat }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
{% if entry.groups %}
|
||||
{% set group_url = filter_urls[entry.groups[0]] %}
|
||||
<a
|
||||
class="tag tag-group{% if group_url == current_path %} active{% endif %}"
|
||||
href="{{ group_url }}"
|
||||
data-value="{{ entry.groups[0] }}"
|
||||
data-url="{{ group_url }}"
|
||||
>
|
||||
{{ entry.groups[0] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if entry.source_type == 'Built-in' %}
|
||||
<a
|
||||
class="tag tag-source{% if '/categories/built-in/' == current_path %} active{% endif %}"
|
||||
href="/categories/built-in/"
|
||||
data-value="Built-in"
|
||||
data-url="/categories/built-in/"
|
||||
>
|
||||
Built-in
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="col-arrow"><span class="arrow">→</span></td>
|
||||
</tr>
|
||||
{% if entry.description %}
|
||||
<tr class="desc-row" aria-hidden="true">
|
||||
<td class="col-num"></td>
|
||||
<td colspan="5">
|
||||
<div class="desc-text">{{ entry.description | safe }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="expand-row" id="expand-{{ loop.index }}">
|
||||
<td></td>
|
||||
<td colspan="4">
|
||||
<div class="expand-content">
|
||||
{% if entry.also_see %}
|
||||
<div class="expand-also-see">
|
||||
Also see: {% for see in entry.also_see %}<a
|
||||
href="{{ see.url }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ see.name }}</a
|
||||
>{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="expand-meta">
|
||||
{% if entry.owner %}<a
|
||||
href="https://github.com/{{ entry.owner }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ entry.owner }}</a
|
||||
><span class="expand-sep">/</span>{% endif %}<a
|
||||
href="{{ entry.url }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ entry.url | replace("https://", "") }}</a
|
||||
>
|
||||
{% if entry.last_commit_at %}<span class="expand-commit"
|
||||
><span class="expand-sep">/</span
|
||||
><time datetime="{{ entry.last_commit_at }}"
|
||||
>{{ entry.last_commit_at[:10] }}</time
|
||||
></span
|
||||
>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="no-results" hidden>
|
||||
<p>No projects match your search or filter.</p>
|
||||
<p class="no-results-hint">
|
||||
Try a broader term, or
|
||||
<button class="no-results-clear">browse all projects</button>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="final-cta" data-reveal>
|
||||
<div class="section-shell">
|
||||
<p class="section-label">Contribute</p>
|
||||
<h2>Know a project that belongs here?</h2>
|
||||
<p>Tell us what it does and why it stands out.</p>
|
||||
<div class="final-cta-actions">
|
||||
<a
|
||||
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
|
||||
class="hero-action hero-action-primary"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Submit a project</a
|
||||
>
|
||||
<a
|
||||
href="https://github.com/vinta/awesome-python"
|
||||
class="hero-action hero-action-secondary"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Star the repository</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block extra_head %}
|
||||
<script type="application/ld+json">{{ homepage_json_ld | safe }}</script>
|
||||
{% endblock %}
|
||||
{% block header %}
|
||||
<header class="hero">
|
||||
<div class="hero-sheen" aria-hidden="true"></div>
|
||||
|
|
@ -61,6 +64,21 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="hero-category-nav" aria-labelledby="hero-category-heading">
|
||||
<div class="hero-category-meta">
|
||||
<h2 id="hero-category-heading">Browse by category</h2>
|
||||
</div>
|
||||
<ul class="hero-category-links">
|
||||
{% for category in categories %}
|
||||
<li>
|
||||
<a class="hero-category-link" href="{{ category_urls[category.name] }}"
|
||||
>{{ category.name }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
|
@ -70,14 +88,7 @@
|
|||
<div class="section-shell sponsor-shell">
|
||||
<header class="sponsor-meta">
|
||||
<p class="section-label" id="sponsor-heading">Sponsors</p>
|
||||
<a
|
||||
class="sponsor-become"
|
||||
href="https://github.com/vinta/awesome-python/blob/master/SPONSORSHIP.md"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Become a sponsor
|
||||
</a>
|
||||
<a class="sponsor-become" href="/sponsorship/"> Become a sponsor </a>
|
||||
</header>
|
||||
<ul class="sponsor-list">
|
||||
{% for sponsor in sponsors %}
|
||||
|
|
@ -98,6 +109,7 @@
|
|||
</section>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/json" id="filter-urls">{{ filter_urls_json | safe }}</script>
|
||||
<section class="results-section" id="library-index">
|
||||
<div class="results-intro section-shell" data-reveal>
|
||||
<div>
|
||||
|
|
@ -211,23 +223,47 @@
|
|||
</td>
|
||||
<td class="col-cat">
|
||||
{% for subcat in entry.subcategories %}
|
||||
<button class="tag" data-value="{{ subcat.value }}">
|
||||
<a class="tag" href="{{ subcat.url }}" data-value="{{ subcat.value }}" data-url="{{ subcat.url }}">
|
||||
{{ subcat.name }}
|
||||
</button>
|
||||
</a>
|
||||
{% endfor %} {% for cat in entry.categories %}
|
||||
<button class="tag" data-value="{{ cat }}">{{ cat }}</button>
|
||||
<a
|
||||
class="tag"
|
||||
href="{{ category_urls[cat] }}"
|
||||
data-value="{{ cat }}"
|
||||
data-url="{{ category_urls[cat] }}"
|
||||
>{{ cat }}</a
|
||||
>
|
||||
{% endfor %}
|
||||
<button class="tag tag-group" data-value="{{ entry.groups[0] }}">
|
||||
<a
|
||||
class="tag tag-group"
|
||||
href="{{ filter_urls[entry.groups[0]] }}"
|
||||
data-value="{{ entry.groups[0] }}"
|
||||
data-url="{{ filter_urls[entry.groups[0]] }}"
|
||||
>
|
||||
{{ entry.groups[0] }}
|
||||
</button>
|
||||
</a>
|
||||
{% if entry.source_type == 'Built-in' %}
|
||||
<button class="tag tag-source" data-value="Built-in">
|
||||
<a
|
||||
class="tag tag-source"
|
||||
href="/categories/built-in/"
|
||||
data-value="Built-in"
|
||||
data-url="/categories/built-in/"
|
||||
>
|
||||
Built-in
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="col-arrow"><span class="arrow">→</span></td>
|
||||
</tr>
|
||||
{% if entry.description %}
|
||||
<tr class="desc-row" aria-hidden="true" hidden>
|
||||
<td class="col-num"></td>
|
||||
<td colspan="5">
|
||||
<div class="desc-text">{{ entry.description | safe }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr class="expand-row" id="expand-{{ loop.index }}">
|
||||
<td></td>
|
||||
<td colspan="4">
|
||||
|
|
|
|||
17
website/templates/llms.txt
Normal file
17
website/templates/llms.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Awesome Python
|
||||
|
||||
Awesome Python is an opinionated catalog of {{ total_entries }} Python frameworks, libraries, tools, and resources across {{ total_categories }} {% if total_categories == 1 %}category{% else %}categories{% endif %}.
|
||||
|
||||
Scan the category index, then jump to the matching section for direct project links and short descriptions. GitHub entries with known star data end with a `GitHub stars: N` note in parentheses; treat it as popularity context, not a quality guarantee. Use the homepage for project context outside the catalog.
|
||||
|
||||
## Primary Links
|
||||
|
||||
- Homepage: {{ site_url }}
|
||||
- GitHub repository: {{ github_repo_url }}
|
||||
- Contributing guide: {{ contributing_url }}
|
||||
- Sponsorship: {{ sponsorship_url }}
|
||||
- Sitemap: {{ sitemap_url }}
|
||||
|
||||
## Categories
|
||||
|
||||
{{ categories_md }}
|
||||
250
website/templates/sponsorship.html
Normal file
250
website/templates/sponsorship.html
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Sponsor Awesome Python{% endblock %}
|
||||
{% block description %}Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use.{% endblock %}
|
||||
{% block canonical_url %}https://awesome-python.com/sponsorship/{% endblock %}
|
||||
{% block alternate_links %}{% endblock %}
|
||||
{% block header %}
|
||||
<header class="category-hero sponsorship-hero">
|
||||
<div class="hero-sheen" aria-hidden="true"></div>
|
||||
<div class="hero-noise" aria-hidden="true"></div>
|
||||
|
||||
<div class="category-hero-shell">
|
||||
<nav class="hero-topbar category-topbar" aria-label="Site">
|
||||
<a href="/" class="hero-brand-mini">Awesome Python</a>
|
||||
<div class="hero-topbar-actions">
|
||||
<a
|
||||
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
|
||||
class="hero-topbar-link hero-topbar-link-strong"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Submit a project</a
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="category-hero-copy sponsorship-hero-copy">
|
||||
<p class="hero-kicker">Sponsorship</p>
|
||||
<h1>Sponsor Awesome Python</h1>
|
||||
<p class="category-subtitle">
|
||||
The #10 most-starred repository on GitHub, and the list Python
|
||||
developers check when choosing what to use. Your sponsorship puts your
|
||||
product in front of them at the moment of decision.
|
||||
</p>
|
||||
|
||||
{% if hero_stats %}
|
||||
<p class="hero-proof sponsorship-proof">
|
||||
{% for stat in hero_stats %}{{ stat }}{% if not loop.last %}
|
||||
<span class="proof-sep">/</span> {% endif %}{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="hero-actions">
|
||||
<a
|
||||
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship"
|
||||
class="hero-action hero-action-primary"
|
||||
>Email vinta.chen@gmail.com</a
|
||||
>
|
||||
<a
|
||||
href="https://github.com/vinta/awesome-python"
|
||||
class="hero-action hero-action-secondary"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>View on GitHub</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %} {% block content %}
|
||||
<section class="sponsorship-section sponsorship-audience" data-reveal>
|
||||
<div class="section-shell sponsorship-shell">
|
||||
<header class="sponsorship-meta">
|
||||
<p class="section-label">Audience</p>
|
||||
</header>
|
||||
<div class="sponsorship-body">
|
||||
<p class="sponsorship-lede">
|
||||
Professional Python developers evaluating libraries and tools for
|
||||
production use. Not beginners browsing tutorials. People making adoption
|
||||
decisions.
|
||||
</p>
|
||||
<dl class="sponsorship-facts">
|
||||
<div>
|
||||
<dt>Who visits</dt>
|
||||
<dd>
|
||||
Mid to senior Python developers arriving with a specific question:
|
||||
a maintained ORM, a fast HTTP client, a task queue worth running in
|
||||
production.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Where they come from</dt>
|
||||
<dd>
|
||||
Google Search, GitHub, Reddit, YouTube, ChatGPT and other LLMs,
|
||||
Hacker News.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Why it works</dt>
|
||||
<dd>
|
||||
Ranks on the first page of Google for "best Python libraries".
|
||||
ChatGPT and other LLMs cite it when recommending Python tools.
|
||||
Developers send it to each other.
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sponsorship-section sponsorship-tiers" data-reveal>
|
||||
<div class="section-shell sponsorship-shell">
|
||||
<header class="sponsorship-meta">
|
||||
<p class="section-label">Tiers</p>
|
||||
<p class="sponsorship-meta-note">
|
||||
One upfront payment per term. Setup takes less than 24 hours.
|
||||
</p>
|
||||
</header>
|
||||
<div class="sponsorship-body">
|
||||
<ol class="tier-list">
|
||||
<li class="tier">
|
||||
<p class="tier-eyebrow">Headline Sponsor</p>
|
||||
<p class="tier-price">
|
||||
<span class="tier-amount">$500</span>
|
||||
<span class="tier-cadence">/ month</span>
|
||||
</p>
|
||||
<p class="tier-summary">
|
||||
Logo pinned at the top of the README. Logo on the website.
|
||||
</p>
|
||||
<ul class="tier-includes">
|
||||
<li>
|
||||
Large logo and one-line description (max 120 characters) pinned at
|
||||
the very top of the README, above all project entries.
|
||||
</li>
|
||||
<li>Logo link in the sponsor section of awesome-python.com.</li>
|
||||
</ul>
|
||||
<a
|
||||
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship%20-%20Headline"
|
||||
class="tier-cta"
|
||||
>Email about Headline tier</a
|
||||
>
|
||||
</li>
|
||||
<li class="tier">
|
||||
<p class="tier-eyebrow">Featured Sponsor</p>
|
||||
<p class="tier-price">
|
||||
<span class="tier-amount">$150</span>
|
||||
<span class="tier-cadence">/ month</span>
|
||||
</p>
|
||||
<p class="tier-summary">
|
||||
Text link pinned at the top of the README. Text link on the website.
|
||||
</p>
|
||||
<ul class="tier-includes">
|
||||
<li>
|
||||
Text entry (<code>[Name](URL) - Description.</code>, max 120
|
||||
characters) pinned at the top of the README, directly below
|
||||
Headline sponsors.
|
||||
</li>
|
||||
<li>Text link in the sponsor section of awesome-python.com.</li>
|
||||
</ul>
|
||||
<a
|
||||
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship%20-%20Featured"
|
||||
class="tier-cta"
|
||||
>Email about Featured tier</a
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sponsorship-section sponsorship-past" data-reveal>
|
||||
<div class="section-shell sponsorship-shell">
|
||||
<header class="sponsorship-meta">
|
||||
<p class="section-label">Previously sponsored by</p>
|
||||
</header>
|
||||
<div class="sponsorship-body">
|
||||
<ul class="past-sponsors">
|
||||
<li>
|
||||
<a href="https://www.warp.dev/" target="_blank" rel="noopener"
|
||||
>Warp</a
|
||||
>
|
||||
<span class="past-sponsor-desc"
|
||||
>The terminal for modern developers.</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sponsorship-section sponsorship-getstarted" data-reveal>
|
||||
<div class="section-shell sponsorship-shell">
|
||||
<header class="sponsorship-meta">
|
||||
<p class="section-label">Get started</p>
|
||||
</header>
|
||||
<div class="sponsorship-body">
|
||||
<p class="sponsorship-lede">
|
||||
Email
|
||||
<a
|
||||
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship"
|
||||
>vinta.chen@gmail.com</a
|
||||
>
|
||||
with the four items below.
|
||||
</p>
|
||||
<dl class="sponsorship-facts">
|
||||
<div>
|
||||
<dt>Tier</dt>
|
||||
<dd>Headline Sponsor ($500/mo) or Featured Sponsor ($150/mo).</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Content</dt>
|
||||
<dd>
|
||||
Product name, URL, logo, and description (Headline tier), or
|
||||
<code>[Name](URL) - Description.</code> entry (Featured tier).
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Duration</dt>
|
||||
<dd>1, 3, 6 months, or longer.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Payment method</dt>
|
||||
<dd>US bank transfer (ACH/wire) or PayPal.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="sponsorship-cta-row">
|
||||
<a
|
||||
href="mailto:vinta.chen@gmail.com?subject=Awesome%20Python%20Sponsorship"
|
||||
class="hero-action hero-action-primary"
|
||||
>Email vinta.chen@gmail.com</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sponsorship-section sponsorship-independence" data-reveal>
|
||||
<div class="section-shell sponsorship-shell">
|
||||
<header class="sponsorship-meta">
|
||||
<p class="section-label">Editorial independence</p>
|
||||
</header>
|
||||
<div class="sponsorship-body">
|
||||
<p>
|
||||
Sponsorship is logo and link placement in the README header. It does not
|
||||
influence which projects are listed. We curate listings on merit through
|
||||
the normal
|
||||
<a
|
||||
href="https://github.com/vinta/awesome-python/blob/master/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>contribution process</a
|
||||
>.
|
||||
</p>
|
||||
<p class="sponsorship-fineprint">
|
||||
We reserve the right to request changes to sponsor text, logos, or links
|
||||
that are misleading, off-topic, or incompatible with the README
|
||||
formatting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -17,41 +17,12 @@ class TestExtractGithubRepos:
|
|||
assert result == {"psf/requests"}
|
||||
|
||||
def test_multiple_repos(self):
|
||||
readme = (
|
||||
"* [requests](https://github.com/psf/requests) - HTTP.\n"
|
||||
"* [flask](https://github.com/pallets/flask) - Micro."
|
||||
)
|
||||
readme = "* [requests](https://github.com/psf/requests) - HTTP.\n* [flask](https://github.com/pallets/flask) - Micro."
|
||||
result = extract_github_repos(readme)
|
||||
assert result == {"psf/requests", "pallets/flask"}
|
||||
|
||||
def test_ignores_non_github_urls(self):
|
||||
readme = "* [pypy](https://foss.heptapod.net/pypy/pypy) - Fast Python."
|
||||
result = extract_github_repos(readme)
|
||||
assert result == set()
|
||||
|
||||
def test_ignores_github_io_urls(self):
|
||||
readme = "* [docs](https://user.github.io/project) - Docs site."
|
||||
result = extract_github_repos(readme)
|
||||
assert result == set()
|
||||
|
||||
def test_ignores_github_wiki_and_blob_urls(self):
|
||||
readme = (
|
||||
"* [wiki](https://github.com/org/repo/wiki) - Wiki.\n"
|
||||
"* [file](https://github.com/org/repo/blob/main/f.py) - File."
|
||||
)
|
||||
result = extract_github_repos(readme)
|
||||
assert result == set()
|
||||
|
||||
def test_handles_trailing_slash(self):
|
||||
readme = "* [lib](https://github.com/org/repo/) - Lib."
|
||||
result = extract_github_repos(readme)
|
||||
assert result == {"org/repo"}
|
||||
|
||||
def test_deduplicates(self):
|
||||
readme = (
|
||||
"* [a](https://github.com/org/repo) - A.\n"
|
||||
"* [b](https://github.com/org/repo) - B."
|
||||
)
|
||||
readme = "* [a](https://github.com/org/repo) - A.\n* [b](https://github.com/org/repo) - B."
|
||||
result = extract_github_repos(readme)
|
||||
assert result == {"org/repo"}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,35 +81,35 @@ MINIMAL_README = textwrap.dedent("""\
|
|||
|
||||
Some intro text.
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
## Alpha
|
||||
### Alpha
|
||||
|
||||
_Libraries for alpha stuff._
|
||||
|
||||
- [lib-a](https://example.com/a) - Does A.
|
||||
- [lib-b](https://example.com/b) - Does B.
|
||||
|
||||
## Beta
|
||||
### Beta
|
||||
|
||||
_Tools for beta._
|
||||
|
||||
- [lib-c](https://example.com/c) - Does C.
|
||||
|
||||
# Resources
|
||||
## Resources
|
||||
|
||||
Where to discover resources.
|
||||
|
||||
## Newsletters
|
||||
### Newsletters
|
||||
|
||||
- [News One](https://example.com/n1)
|
||||
- [News Two](https://example.com/n2)
|
||||
|
||||
## Podcasts
|
||||
### Podcasts
|
||||
|
||||
- [Pod One](https://example.com/p1)
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Please contribute!
|
||||
""")
|
||||
|
|
@ -120,11 +120,11 @@ GROUPED_README = textwrap.dedent("""\
|
|||
|
||||
Some intro text.
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
**Group One**
|
||||
|
||||
## Alpha
|
||||
### Alpha
|
||||
|
||||
_Libraries for alpha stuff._
|
||||
|
||||
|
|
@ -133,25 +133,25 @@ GROUPED_README = textwrap.dedent("""\
|
|||
|
||||
**Group Two**
|
||||
|
||||
## Beta
|
||||
### Beta
|
||||
|
||||
_Tools for beta._
|
||||
|
||||
- [lib-c](https://example.com/c) - Does C.
|
||||
|
||||
## Gamma
|
||||
### Gamma
|
||||
|
||||
- [lib-d](https://example.com/d) - Does D.
|
||||
|
||||
# Resources
|
||||
## Resources
|
||||
|
||||
Where to discover resources.
|
||||
|
||||
## Newsletters
|
||||
### Newsletters
|
||||
|
||||
- [News One](https://example.com/n1)
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Please contribute!
|
||||
""")
|
||||
|
|
@ -180,7 +180,9 @@ class TestParseReadmeSections:
|
|||
groups = parse_readme(MINIMAL_README)
|
||||
cats = groups[0]["categories"]
|
||||
assert cats[0]["description"] == "Libraries for alpha stuff."
|
||||
assert cats[0]["description_html"] == "Libraries for alpha stuff."
|
||||
assert cats[1]["description"] == "Tools for beta."
|
||||
assert cats[1]["description_html"] == "Tools for beta."
|
||||
|
||||
def test_contributing_skipped(self):
|
||||
groups = parse_readme(MINIMAL_README)
|
||||
|
|
@ -189,7 +191,7 @@ class TestParseReadmeSections:
|
|||
all_names.extend(c["name"] for c in g["categories"])
|
||||
assert "Contributing" not in all_names
|
||||
|
||||
def test_no_separator(self):
|
||||
def test_no_projects_heading(self):
|
||||
groups = parse_readme("# Just a heading\n\nSome text.\n")
|
||||
assert groups == []
|
||||
|
||||
|
|
@ -197,46 +199,48 @@ class TestParseReadmeSections:
|
|||
readme = textwrap.dedent("""\
|
||||
# Title
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
## NullDesc
|
||||
### NullDesc
|
||||
|
||||
- [item](https://x.com) - Thing.
|
||||
|
||||
# Resources
|
||||
## Resources
|
||||
|
||||
## Tips
|
||||
### Tips
|
||||
|
||||
- [tip](https://x.com)
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
groups = parse_readme(readme)
|
||||
cats = groups[0]["categories"]
|
||||
assert cats[0]["description"] == ""
|
||||
assert cats[0]["description_html"] == ""
|
||||
assert cats[0]["entries"][0]["name"] == "item"
|
||||
|
||||
def test_description_with_link_stripped(self):
|
||||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
## Algos
|
||||
### Algos
|
||||
|
||||
_Algorithms. Also see [awesome-algos](https://example.com)._
|
||||
|
||||
- [lib](https://x.com) - Lib.
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
groups = parse_readme(readme)
|
||||
cats = groups[0]["categories"]
|
||||
assert cats[0]["description"] == "Algorithms. Also see awesome-algos."
|
||||
assert cats[0]["description_html"] == 'Algorithms. Also see <a href="https://example.com" target="_blank" rel="noopener">awesome-algos</a>.'
|
||||
|
||||
|
||||
class TestParseGroupedReadme:
|
||||
|
|
@ -269,17 +273,17 @@ class TestParseGroupedReadme:
|
|||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
**Empty**
|
||||
|
||||
**HasCats**
|
||||
|
||||
## Cat
|
||||
### Cat
|
||||
|
||||
- [x](https://x.com) - X.
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
|
|
@ -291,15 +295,15 @@ class TestParseGroupedReadme:
|
|||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
**Note:** This is not a group marker.
|
||||
|
||||
## Cat
|
||||
### Cat
|
||||
|
||||
- [x](https://x.com) - X.
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
|
|
@ -313,19 +317,19 @@ class TestParseGroupedReadme:
|
|||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
## Orphan
|
||||
### Orphan
|
||||
|
||||
- [x](https://x.com) - X.
|
||||
|
||||
**A Group**
|
||||
|
||||
## Grouped
|
||||
### Grouped
|
||||
|
||||
- [y](https://x.com) - Y.
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
|
|
@ -346,10 +350,7 @@ def _content_nodes(md_text: str) -> list[SyntaxTreeNode]:
|
|||
|
||||
class TestParseSectionEntries:
|
||||
def test_flat_entries(self):
|
||||
nodes = _content_nodes(
|
||||
"- [django](https://example.com/d) - A web framework.\n"
|
||||
"- [flask](https://example.com/f) - A micro framework.\n"
|
||||
)
|
||||
nodes = _content_nodes("- [django](https://example.com/d) - A web framework.\n- [flask](https://example.com/f) - A micro framework.\n")
|
||||
entries = _parse_section_entries(nodes)
|
||||
assert len(entries) == 2
|
||||
assert entries[0]["name"] == "django"
|
||||
|
|
@ -366,13 +367,7 @@ class TestParseSectionEntries:
|
|||
assert entries[0]["description"] == ""
|
||||
|
||||
def test_subcategorized_entries(self):
|
||||
nodes = _content_nodes(
|
||||
"- Algorithms\n"
|
||||
" - [algos](https://x.com/a) - Algo lib.\n"
|
||||
" - [sorts](https://x.com/s) - Sort lib.\n"
|
||||
"- Design Patterns\n"
|
||||
" - [patterns](https://x.com/p) - Pattern lib.\n"
|
||||
)
|
||||
nodes = _content_nodes("- Algorithms\n - [algos](https://x.com/a) - Algo lib.\n - [sorts](https://x.com/s) - Sort lib.\n- Design Patterns\n - [patterns](https://x.com/p) - Pattern lib.\n")
|
||||
entries = _parse_section_entries(nodes)
|
||||
assert len(entries) == 3
|
||||
assert entries[0]["name"] == "algos"
|
||||
|
|
@ -410,15 +405,15 @@ class TestParseSectionEntries:
|
|||
readme = textwrap.dedent("""\
|
||||
# T
|
||||
|
||||
---
|
||||
## Projects
|
||||
|
||||
## Async
|
||||
### Async
|
||||
|
||||
- [asyncio](https://x.com) - Async I/O.
|
||||
- [awesome-asyncio](https://y.com)
|
||||
- [trio](https://z.com) - Friendly async.
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
Done.
|
||||
""")
|
||||
|
|
@ -428,7 +423,7 @@ class TestParseSectionEntries:
|
|||
assert cats[0]["entry_count"] == 3
|
||||
|
||||
def test_description_html_escapes_xss(self):
|
||||
nodes = _content_nodes('- [lib](https://x.com) - A <script>alert(1)</script> lib.\n')
|
||||
nodes = _content_nodes("- [lib](https://x.com) - A <script>alert(1)</script> lib.\n")
|
||||
entries = _parse_section_entries(nodes)
|
||||
assert "<script>" not in entries[0]["description"]
|
||||
assert "<script>" in entries[0]["description"]
|
||||
|
|
@ -445,9 +440,6 @@ class TestParseRealReadme:
|
|||
def test_at_least_11_groups(self):
|
||||
assert len(self.groups) >= 11
|
||||
|
||||
def test_first_group_is_ai_ml(self):
|
||||
assert self.groups[0]["name"] == "AI & ML"
|
||||
|
||||
def test_at_least_69_categories(self):
|
||||
assert len(self.cats) >= 69
|
||||
|
||||
|
|
@ -455,38 +447,10 @@ class TestParseRealReadme:
|
|||
all_names = [c["name"] for c in self.cats]
|
||||
assert "Contributing" not in all_names
|
||||
|
||||
def test_first_category_is_ai_and_agents(self):
|
||||
assert self.cats[0]["name"] == "AI and Agents"
|
||||
assert self.cats[0]["slug"] == "ai-and-agents"
|
||||
|
||||
def test_web_apis_slug(self):
|
||||
slugs = [c["slug"] for c in self.cats]
|
||||
assert "web-apis" in slugs
|
||||
|
||||
def test_descriptions_extracted(self):
|
||||
ai = next(c for c in self.cats if c["name"] == "AI and Agents")
|
||||
assert "AI applications" in ai["description"]
|
||||
|
||||
def test_entry_counts_nonzero(self):
|
||||
for cat in self.cats:
|
||||
assert cat["entry_count"] > 0, f"{cat['name']} has 0 entries"
|
||||
|
||||
def test_async_has_also_see(self):
|
||||
async_cat = next(c for c in self.cats if c["name"] == "Asynchronous Programming")
|
||||
asyncio_entry = next(e for e in async_cat["entries"] if e["name"] == "asyncio")
|
||||
assert len(asyncio_entry["also_see"]) >= 1
|
||||
assert asyncio_entry["also_see"][0]["name"] == "awesome-asyncio"
|
||||
|
||||
def test_description_links_stripped_to_text(self):
|
||||
algos = next(c for c in self.cats if c["name"] == "Algorithms and Design Patterns")
|
||||
assert "awesome-algorithms" in algos["description"]
|
||||
assert "https://" not in algos["description"]
|
||||
|
||||
def test_miscellaneous_in_own_group(self):
|
||||
misc_group = next((g for g in self.groups if g["name"] == "Miscellaneous"), None)
|
||||
assert misc_group is not None
|
||||
assert any(c["name"] == "Miscellaneous" for c in misc_group["categories"])
|
||||
|
||||
def test_all_entries_have_nonempty_names(self):
|
||||
bad = []
|
||||
for cat in self.cats:
|
||||
|
|
@ -516,21 +480,21 @@ class TestParseRealReadme:
|
|||
md = MarkdownIt("commonmark")
|
||||
root = SyntaxTreeNode(md.parse(self.readme_text))
|
||||
|
||||
# Find category section boundaries (between --- and # Resources/Contributing)
|
||||
hr_idx = None
|
||||
# Find category section boundaries (between Projects and Resources/Contributing)
|
||||
projects_idx = None
|
||||
end_idx = None
|
||||
for i, node in enumerate(root.children):
|
||||
if hr_idx is None and node.type == "hr":
|
||||
hr_idx = i
|
||||
elif node.type == "heading" and node.tag == "h1":
|
||||
if node.type == "heading" and node.tag in ("h1", "h2"):
|
||||
text = render_inline_text(node.children[0].children) if node.children else ""
|
||||
if end_idx is None and text in ("Resources", "Contributing"):
|
||||
if projects_idx is None and text == "Projects":
|
||||
projects_idx = i
|
||||
elif end_idx is None and text in ("Resources", "Contributing"):
|
||||
end_idx = i
|
||||
if hr_idx is None:
|
||||
if projects_idx is None:
|
||||
return
|
||||
|
||||
bad = []
|
||||
cat_nodes = root.children[hr_idx + 1 : end_idx or len(root.children)]
|
||||
cat_nodes = root.children[projects_idx + 1 : end_idx or len(root.children)]
|
||||
for node in cat_nodes:
|
||||
if node.type != "bullet_list":
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue