diff --git a/.gitignore b/.gitignore index 9f878b42..de692147 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ website/output/ website/data/* !website/data/pypi_name_overrides.json !website/data/redirects.json +!website/data/category_intros/ # agents .playwright-cli/ diff --git a/website/build.py b/website/build.py index 89dbc4a3..46985670 100644 --- a/website/build.py +++ b/website/build.py @@ -13,7 +13,9 @@ from typing import TypedDict from fetch_pypi_downloads_via_clickpy import OVERRIDES_FILE, normalize from jinja2 import Environment, FileSystemLoader -from readme_parser import AlsoSee, ParsedGroup, ParsedSection, parse_readme, parse_sponsors, slugify +from markdown_it import MarkdownIt +from markdown_it.tree import SyntaxTreeNode +from readme_parser import AlsoSee, ParsedGroup, ParsedSection, parse_readme, parse_sponsors, render_inline_text, slugify GITHUB_REPO_URL_RE = re.compile(r"^https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$") MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)\s]+)\)") @@ -235,6 +237,24 @@ def category_meta_description(name: str, entry_count: int, description: str, par return f"{count_sentence} Part of the Awesome Python catalog." +def load_category_intro(path: Path) -> tuple[str, str]: + """Render a category intro file to HTML, plus its first paragraph as plain text for the meta description. + + Returns empty strings if the category has no intro file. + """ + if not path.exists(): + return "", "" + md = MarkdownIt("commonmark") + tokens = md.parse(path.read_text(encoding="utf-8")) + for token in tokens: + for child in token.children or []: + if child.type == "link_open": + child.attrSet("target", "_blank") + child.attrSet("rel", "noopener") + lead = next(node for node in SyntaxTreeNode(tokens).children if node.type == "paragraph") + return md.renderer.render(tokens, md.options, {}), render_inline_text(lead.children[0].children) + + def build_breadcrumb_json_ld(items: Sequence[tuple[str, str]]) -> dict: return { "@type": "BreadcrumbList", @@ -676,7 +696,8 @@ def build(repo_root: Path) -> None: page_dir.mkdir(parents=True, exist_ok=True) parent_name = parent_category["name"] if parent_category else None category_title = category_meta_title(category["name"], parent_name) - category_description = category_meta_description(category["name"], len(entries), category["description"], parent_name) + intro_html, intro_lead = load_category_intro(website / "data" / "category_intros" / f"{current_path.removeprefix('/categories/').strip('/')}.md") + category_description = intro_lead or category_meta_description(category["name"], len(entries), category["description"], parent_name) breadcrumbs = [("Awesome Python", SITE_URL)] if parent_category: breadcrumbs.append((parent_category["name"], category_public_url(parent_category))) @@ -691,6 +712,7 @@ def build(repo_root: Path) -> None: category_title=category_title, category_url=category_url, category_description=category_description, + intro_html=intro_html, entries=entries, total_categories=len(categories), category_urls=category_urls, diff --git a/website/data/category_intros/orm.md b/website/data/category_intros/orm.md new file mode 100644 index 00000000..8b669ef1 --- /dev/null +++ b/website/data/category_intros/orm.md @@ -0,0 +1,20 @@ +Use SQLAlchemy for most projects, and the Django ORM inside Django. With FastAPI, use SQLModel, so your tables and API schemas share the same fields. + +How to choose: + +- Any framework, or full control over the SQL: SQLAlchemy +- A Django project: the Django ORM, which needs Django's settings even outside a web app +- A FastAPI app: SQLModel +- A simple ORM with few concepts to learn: peewee +- MongoDB: Beanie for async code, MongoEngine for sync code +- DynamoDB: PynamoDB + +With SQLAlchemy, write [typed models](https://docs.sqlalchemy.org/en/latest/orm/declarative_styles.html) with `DeclarativeBase`, `Mapped[]`, and `mapped_column()`. Create one `sessionmaker` at startup and open one session per request. Load relationships with `selectinload()`, and run migrations with Alembic, which comes from the SQLAlchemy project itself. + +Sync code is the safe default, even in an async framework. FastAPI's own docs put it plainly: ["If you just don't know, use normal `def`."](https://fastapi.tiangolo.com/async/) If you do go async, give each task its own `AsyncSession`. + +In Django, let `makemigrations` and `migrate` own the schema, and use `select_related()` or `prefetch_related()` whenever you touch related rows. Reach for raw SQL last: ["Explore the ORM before using raw SQL!"](https://docs.djangoproject.com/en/stable/topics/db/sql/) + +With SQLModel, follow its FastAPI tutorial: a base model for the shared fields, a `table=True` model for the database, and [separate models for create, read, and update](https://sqlmodel.tiangolo.com/tutorial/fastapi/multiple-models/). When you outgrow it, plug SQLAlchemy in directly. + +For MongoDB and DynamoDB, model around your queries, not your tables. MongoDB's rule is that ["data that's accessed together should be stored together."](https://www.mongodb.com/docs/manual/core/data-modeling-introduction/) AWS goes further: [don't design a DynamoDB schema](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-general-nosql-design.html) until you know the questions it needs to answer. diff --git a/website/static/style.css b/website/static/style.css index 2313c010..bdad3835 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -496,17 +496,44 @@ kbd { text-wrap: pretty; } -.category-subtitle a { +.category-subtitle a, +.category-intro 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 { +.category-subtitle a:hover, +.category-intro a:hover { text-decoration-color: oklch(100% 0 0 / 0.7); } +.category-intro { + margin-top: 1.75rem; + display: grid; + gap: 0.9rem; + color: var(--hero-muted); + font-size: clamp(1.05rem, 1.8vw, 1.2rem); + text-wrap: pretty; +} + +.category-intro > p:first-child { + color: var(--hero-text); + font-size: clamp(1.2rem, 2.2vw, 1.45rem); + line-height: 1.45; +} + +.category-intro ul { + display: grid; + gap: 0.35rem; + padding-left: 1.25rem; +} + +.category-intro code { + font-size: 0.9em; +} + .sponsor-band { padding-block: clamp(2.5rem, 5.5vw, 4rem); background: diff --git a/website/templates/category.html b/website/templates/category.html index 0a868bc2..7325aa2b 100644 --- a/website/templates/category.html +++ b/website/templates/category.html @@ -37,6 +37,9 @@ {% if category.description_html %}
{{ category.description_html | safe }}
{% endif %} + {% if intro_html %} +Use w1 for most apps.