feat: group private apps into andreknie-privat

This commit is contained in:
2026-07-25 15:47:01 +02:00
parent 4c7d48ae6d
commit 2e83750cb7
109 changed files with 0 additions and 0 deletions
@@ -0,0 +1 @@
"""Output rendering, folder routing, and filename generation."""
@@ -0,0 +1,65 @@
"""Filename generation with collision avoidance.
Pattern: YYYY-MM-DD-slugified-title.md
Slug: lowercase alphanumeric + hyphens only.
Appends numeric suffix (-2, -3, ...) if file already exists.
"""
import logging
from datetime import datetime, timezone
from pathlib import Path
from slugify import slugify
logger = logging.getLogger(__name__)
def generate_filename(title: str, date: str | None = None) -> str:
"""Generate a filename from title and optional date.
Args:
title: The note title to slugify.
date: Optional date string (YYYY-MM-DD). Uses today if not provided.
Returns:
Filename in format YYYY-MM-DD-slugified-title.md
"""
if date is None:
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
# Ensure date is just the date portion (handle ISO datetime strings)
date_part = date[:10] if len(date) >= 10 else date
slug = slugify(title, max_length=80)
if not slug:
slug = "untitled"
return f"{date_part}-{slug}.md"
def resolve_collision(target_dir: Path, filename: str) -> Path:
"""Resolve filename collisions by appending numeric suffix.
Args:
target_dir: Directory where the file will be written.
filename: Desired filename (e.g., 2024-03-15-meeting-notes.md).
Returns:
Full path with unique filename (appends -2, -3, etc. if needed).
"""
target = Path(target_dir) / filename
if not target.exists():
return target
# Split filename to insert suffix before .md
stem = filename.rsplit(".md", 1)[0]
suffix_num = 2
while True:
new_filename = f"{stem}-{suffix_num}.md"
new_target = Path(target_dir) / new_filename
if not new_target.exists():
logger.debug("Collision resolved: %s%s", filename, new_filename)
return new_target
suffix_num += 1
@@ -0,0 +1,97 @@
"""Markdown + YAML frontmatter rendering for processed notes.
Renders:
- Frontmatter with title, date, tags, people, projects, source
- Body with wiki-links inserted
- Action Items section at the end
"""
import logging
from datetime import datetime, timezone
from pathlib import Path
import yaml
from ingestion.enrichment.models import EnrichmentResult
logger = logging.getLogger(__name__)
def render_note(
enrichment: EnrichmentResult, body_text: str, source_file: Path
) -> str:
"""Render a complete markdown note with YAML frontmatter.
Args:
enrichment: The enrichment result with metadata and entities.
body_text: The document body (with wiki-links already inserted).
source_file: Original source file path for provenance.
Returns:
Complete markdown string with frontmatter, body, and action items.
"""
# Build frontmatter data
frontmatter: dict = {
"title": enrichment.title,
}
# Extract date from entities or use today
date_entities = [e for e in enrichment.entities if e.type == "date"]
if date_entities:
frontmatter["date"] = date_entities[0].value
else:
frontmatter["date"] = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if enrichment.tags:
frontmatter["tags"] = enrichment.tags
# People and projects from entities
people = list(
{e.value for e in enrichment.entities if e.type == "person"}
)
projects = list(
{e.value for e in enrichment.entities if e.type == "project"}
)
if people:
frontmatter["people"] = sorted(people)
if projects:
frontmatter["projects"] = sorted(projects)
frontmatter["source"] = {
"file": source_file.name,
"imported": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S"),
}
# Render YAML frontmatter
yaml_str = yaml.dump(
frontmatter,
default_flow_style=False,
allow_unicode=True,
sort_keys=False,
)
# Build the full document
parts = [
"---",
yaml_str.rstrip(),
"---",
"",
body_text.rstrip(),
]
# Add action items section if any
if enrichment.action_items:
parts.append("")
parts.append("## Action Items")
parts.append("")
for item in enrichment.action_items:
line = f"- [ ] {item.description}"
if item.assignee:
line += f" ({item.assignee})"
if item.deadline:
line += f" (deadline: {item.deadline})"
parts.append(line)
parts.append("") # trailing newline
return "\n".join(parts)
@@ -0,0 +1,44 @@
"""Category-based folder routing for output files.
Routes notes to the appropriate NoteGraph folder based on detected category:
- meeting → notes/meetings/
- project → notes/projects/
- decision → notes/decisions/
- default → notes/inbox/
"""
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
# Category to folder mapping
CATEGORY_FOLDERS: dict[str, str] = {
"meeting": "meetings",
"project": "projects",
"decision": "decisions",
}
def route_to_folder(
category: str, notes_dir: Path, override: Path | None = None
) -> Path:
"""Determine the output folder for a note based on its category.
Args:
category: The detected category (meeting, project, decision, inbox).
notes_dir: Root notes directory (e.g., ./notes).
override: Optional explicit output directory override (--output-dir).
Returns:
Path to the target folder (created if it doesn't exist).
"""
if override is not None:
target = Path(override)
else:
folder_name = CATEGORY_FOLDERS.get(category, "inbox")
target = Path(notes_dir) / folder_name
target.mkdir(parents=True, exist_ok=True)
logger.debug("Routing category '%s' to folder: %s", category, target)
return target