Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bengal/assets/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _isoformat(timestamp: float | None) -> str | None:
return datetime.fromtimestamp(timestamp, tz=UTC).isoformat().replace("+00:00", "Z")


@dataclass(slots=True)
@dataclass(frozen=True, slots=True)
class AssetManifestEntry:
"""
Manifest entry for a single logical asset.
Expand Down
2 changes: 1 addition & 1 deletion bengal/build/provenance/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from bengal.protocols.core import PageLike


@dataclass
@dataclass(frozen=True, slots=True)
class ProvenanceFilterResult:
"""Result of provenance-based filtering."""

Expand Down
2 changes: 1 addition & 1 deletion bengal/build/provenance/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def summary(self) -> str:
return f"Provenance({', '.join(parts)}) → {self.combined_hash}"


@dataclass
@dataclass(frozen=True, slots=True)
class ProvenanceRecord:
"""
Stored provenance record with metadata.
Expand Down
2 changes: 1 addition & 1 deletion bengal/cache/build_cache/autodoc_content_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
logger = get_logger(__name__)


@dataclass
@dataclass(frozen=True, slots=True)
class CachedModuleInfo:
"""Cached parsed module data.

Expand Down
2 changes: 1 addition & 1 deletion bengal/cache/build_cache/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
logger = get_logger(__name__)


@dataclass
@dataclass(frozen=True, slots=True)
class FileFingerprint:
"""
Fast file change detection using mtime + size, with optional hash verification.
Expand Down
16 changes: 14 additions & 2 deletions bengal/cache/utils/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@
from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, TypedDict

if TYPE_CHECKING:
from collections.abc import Callable


class TaxonomyStatsDict(TypedDict, total=False):
"""Statistics for taxonomy-type caches."""

total_tags: int
valid_tags: int
invalid_tags: int
total_unique_pages: int
total_page_tag_pairs: int
avg_tags_per_page: float
cache_size_bytes: int

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TaxonomyStatsDict is defined with total=False, but compute_taxonomy_stats() always populates the core fields (total_tags, valid_tags, etc.) and only conditionally includes cache_size_bytes. Making all keys optional reduces type precision; consider using a regular TypedDict with NotRequired[int] for the optional cache_size_bytes key (pattern already used elsewhere in the repo).

Copilot uses AI. Check for mistakes.

def compute_validity_stats[V](
entries: dict[str, V],
is_valid: Callable[[V], bool],
Expand Down Expand Up @@ -106,7 +118,7 @@ def compute_taxonomy_stats(
is_valid: Callable[[Any], bool],
get_page_paths: Callable[[Any], list[str]],
serialize: Callable[[Any], dict[str, Any]] | None = None,
) -> dict[str, Any]:
) -> TaxonomyStatsDict:
"""
Compute statistics for taxonomy-type caches.

Expand Down
2 changes: 1 addition & 1 deletion bengal/cli/dashboard/widgets/phase_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from textual.app import ComposeResult


@dataclass(frozen=True)
@dataclass(frozen=True, slots=True)
class BuildPhase:
"""
Represents a build phase with status.
Expand Down
2 changes: 1 addition & 1 deletion bengal/config/build_options_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from bengal.orchestration.build.options import BuildOptions


@dataclass
@dataclass(frozen=True, slots=True)
class CLIFlags:
"""
Flags explicitly passed via CLI (None = not passed).
Expand Down
218 changes: 110 additions & 108 deletions bengal/content/sources/notion.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,77 +390,78 @@ def _blocks_to_markdown(self, blocks: list[dict[str, Any]]) -> str:
block_type = block.get("type")
block_data = block.get(str(block_type) if block_type else "", {})

if block_type == "paragraph":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(text)

elif block_type == "heading_1":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"# {text}")

elif block_type == "heading_2":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"## {text}")

elif block_type == "heading_3":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"### {text}")

elif block_type == "bulleted_list_item":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"- {text}")

elif block_type == "numbered_list_item":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"1. {text}")

elif block_type == "to_do":
text = self._rich_text_to_md(block_data.get("rich_text", []))
checked = "x" if block_data.get("checked") else " "
lines.append(f"- [{checked}] {text}")

elif block_type == "toggle":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"<details><summary>{text}</summary>")
# Note: nested blocks not handled in this simple implementation
lines.append("</details>")

elif block_type == "code":
code = self._rich_text_to_md(block_data.get("rich_text", []))
lang = block_data.get("language", "")
lines.append(f"```{lang}")
lines.append(code)
lines.append("```")

elif block_type == "quote":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"> {text}")

elif block_type == "callout":
icon = block_data.get("icon", {}).get("emoji", "💡")
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"> {icon} {text}")

elif block_type == "divider":
lines.append("---")

elif block_type == "image":
image_data = block_data
url = image_data.get("external", {}).get("url") or image_data.get("file", {}).get(
"url"
)
caption = self._rich_text_to_md(image_data.get("caption", []))
if url:
lines.append(f"![{caption}]({url})")
match block_type:
case "paragraph":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(text)

case "heading_1":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"# {text}")

case "heading_2":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"## {text}")

case "heading_3":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"### {text}")

case "bulleted_list_item":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"- {text}")

case "numbered_list_item":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"1. {text}")

case "to_do":
text = self._rich_text_to_md(block_data.get("rich_text", []))
checked = "x" if block_data.get("checked") else " "
lines.append(f"- [{checked}] {text}")

case "toggle":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"<details><summary>{text}</summary>")
# Note: nested blocks not handled in this simple implementation
lines.append("</details>")

case "code":
code = self._rich_text_to_md(block_data.get("rich_text", []))
lang = block_data.get("language", "")
lines.append(f"```{lang}")
lines.append(code)
lines.append("```")

case "quote":
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"> {text}")

case "callout":
icon = block_data.get("icon", {}).get("emoji", "💡")
text = self._rich_text_to_md(block_data.get("rich_text", []))
lines.append(f"> {icon} {text}")

case "divider":
lines.append("---")

case "image":
image_data = block_data
url = image_data.get("external", {}).get("url") or image_data.get(
"file", {}
).get("url")
caption = self._rich_text_to_md(image_data.get("caption", []))
if url:
lines.append(f"![{caption}]({url})")

elif block_type == "bookmark":
url = block_data.get("url", "")
caption = self._rich_text_to_md(block_data.get("caption", []))
lines.append(f"[{caption or url}]({url})")
case "bookmark":
url = block_data.get("url", "")
caption = self._rich_text_to_md(block_data.get("caption", []))
lines.append(f"[{caption or url}]({url})")

elif block_type == "equation":
expression = block_data.get("expression", "")
lines.append(f"$$\n{expression}\n$$")
case "equation":
expression = block_data.get("expression", "")
lines.append(f"$$\n{expression}\n$$")

# Add blank line between blocks
lines.append("")
Expand Down Expand Up @@ -524,56 +525,57 @@ def _extract_properties(self, page: dict[str, Any]) -> dict[str, Any]:
prop = properties[notion_prop]
prop_type = prop.get("type")

if prop_type == "title":
frontmatter[fm_key] = self._rich_text_to_md(prop.get("title", []))
match prop_type:
case "title":
frontmatter[fm_key] = self._rich_text_to_md(prop.get("title", []))

elif prop_type == "rich_text":
frontmatter[fm_key] = self._rich_text_to_md(prop.get("rich_text", []))
case "rich_text":
frontmatter[fm_key] = self._rich_text_to_md(prop.get("rich_text", []))

elif prop_type == "date":
date_obj = prop.get("date")
if date_obj:
frontmatter[fm_key] = date_obj.get("start")
case "date":
date_obj = prop.get("date")
if date_obj:
frontmatter[fm_key] = date_obj.get("start")

elif prop_type == "multi_select":
frontmatter[fm_key] = [opt["name"] for opt in prop.get("multi_select", [])]
case "multi_select":
frontmatter[fm_key] = [opt["name"] for opt in prop.get("multi_select", [])]

elif prop_type == "select":
select_obj = prop.get("select")
if select_obj:
frontmatter[fm_key] = select_obj.get("name")
case "select":
select_obj = prop.get("select")
if select_obj:
frontmatter[fm_key] = select_obj.get("name")

elif prop_type == "checkbox":
frontmatter[fm_key] = prop.get("checkbox", False)
case "checkbox":
frontmatter[fm_key] = prop.get("checkbox", False)

elif prop_type == "number":
frontmatter[fm_key] = prop.get("number")
case "number":
frontmatter[fm_key] = prop.get("number")

elif prop_type == "url":
frontmatter[fm_key] = prop.get("url")
case "url":
frontmatter[fm_key] = prop.get("url")

elif prop_type == "email":
frontmatter[fm_key] = prop.get("email")
case "email":
frontmatter[fm_key] = prop.get("email")

elif prop_type == "phone_number":
frontmatter[fm_key] = prop.get("phone_number")
case "phone_number":
frontmatter[fm_key] = prop.get("phone_number")

elif prop_type == "people":
people = prop.get("people", [])
frontmatter[fm_key] = [p.get("name", p.get("id")) for p in people]
case "people":
people = prop.get("people", [])
frontmatter[fm_key] = [p.get("name", p.get("id")) for p in people]

elif prop_type == "files":
files = prop.get("files", [])
urls = []
for f in files:
url = f.get("external", {}).get("url") or f.get("file", {}).get("url")
if url:
urls.append(url)
frontmatter[fm_key] = urls
case "files":
files = prop.get("files", [])
urls = []
for f in files:
url = f.get("external", {}).get("url") or f.get("file", {}).get("url")
if url:
urls.append(url)
frontmatter[fm_key] = urls

elif prop_type == "status":
status_obj = prop.get("status")
if status_obj:
frontmatter[fm_key] = status_obj.get("name")
case "status":
status_obj = prop.get("status")
if status_obj:
frontmatter[fm_key] = status_obj.get("name")

return frontmatter
2 changes: 1 addition & 1 deletion bengal/content_types/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
logger = get_logger(__name__)

#: Page type literals for template resolution
PageType = Literal["home", "list", "single"]
type PageType = Literal["home", "list", "single"]


def classify_page(page: PageLike) -> PageType:
Expand Down
15 changes: 13 additions & 2 deletions bengal/core/author.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,18 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any
from typing import Any, TypedDict


class AuthorDict(TypedDict):
"""Serialized form of Author for template context."""

name: str
email: str
bio: str
avatar: str
url: str
social: dict[str, str]


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -129,7 +140,7 @@ def mastodon(self) -> str:
"""Shortcut for Mastodon handle."""
return self.social.get("mastodon", "")

def to_dict(self) -> dict[str, Any]:
def to_dict(self) -> AuthorDict:
"""
Convert to dictionary for template context or serialization.

Expand Down
Loading
Loading