src/applytrack/store.py
Rama main · réplica de lectura del repositorio original.
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Aaron K. Clark
"""Pure helpers for Markdown application notes.
The .NET API owns CRUD now; this module only keeps the small codec shared by the
importer, poller, and DB writer.
"""
from __future__ import annotations
import re
from dataclasses import asdict, dataclass
from datetime import date
from typing import Any
import yaml
# Pipeline stages, in order. The UI colour-codes and orders by this list.
# lead discovered/triage ready materials drafted, awaiting your submit
# applied you submitted screen recruiter/phone screen
# onsite interview loop offer / rejected / passed
STATUSES = ["lead", "ready", "applied", "screen", "onsite", "offer", "rejected", "passed"]
# Which strength the role leads with — mirrors the cover-letter \lane switch.
LANES = ["dotnet", "devrel", "ai"]
_ILLEGAL_FILENAME_CHARS = re.compile(r'[\\/:*?"<>|\x00-\x1f]')
_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.DOTALL)
@dataclass
class AppFields:
"""The structured contents of a single job-application note."""
company: str
role: str = ""
lane: str = "ai"
status: str = "lead"
link: str = ""
location: str = ""
salary: str = ""
source: str = ""
contact: str = ""
contact_email: str = ""
applied: str = ""
followup: str = ""
created: str = ""
score: str = ""
notes: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> AppFields:
def s(key: str) -> str:
val = data.get(key, "")
return "" if val is None else str(val).strip()
lane = s("lane").lower() or "ai"
status = s("status").lower() or "lead"
return cls(
company=s("company"),
role=s("role"),
lane=lane if lane in LANES else "ai",
status=status if status in STATUSES else "lead",
link=s("link"),
location=s("location"),
salary=s("salary"),
source=s("source"),
contact=s("contact"),
contact_email=s("contact_email"),
applied=s("applied"),
followup=s("followup"),
created=s("created"),
score=s("score"),
notes=str(data.get("notes", "")).rstrip(),
)
def today() -> str:
return date.today().isoformat()
def filename_for(fields: AppFields) -> str:
"""Build the slug filename from company + role (disk-free).
Shared with the poller's DB writer so a lead staged by the Python runtime gets
the exact same ``name`` the .NET API's ``Slug.FilenameFor`` would mint.
"""
stem = f"{fields.company} {fields.role}".strip() or fields.company
cleaned = _ILLEGAL_FILENAME_CHARS.sub(" ", stem).strip()
cleaned = re.sub(r"\s+", "-", cleaned).lower()
if not cleaned:
raise ValueError("company/role produces an empty filename")
return f"{cleaned}.md"
def parse_app(md: str) -> AppFields:
"""Parse a file's YAML frontmatter + Markdown body into fields."""
match = _FRONTMATTER_RE.match(md.lstrip(""))
if match:
front_raw, body = match.group(1), match.group(2)
try:
data = yaml.safe_load(front_raw) or {}
except yaml.YAMLError:
data = {}
if not isinstance(data, dict):
data = {}
else:
data, body = {}, md
data["notes"] = body.strip()
return AppFields.from_dict(data)
def render_fields(f: AppFields) -> str:
"""Render structured fields back into frontmatter + body Markdown."""
front: dict[str, Any] = {
"company": f.company,
"role": f.role,
"lane": f.lane,
"status": f.status,
"link": f.link,
"location": f.location,
"salary": f.salary,
"source": f.source,
"contact": f.contact,
"contact_email": f.contact_email,
"applied": f.applied,
"followup": f.followup,
"created": f.created or today(),
"score": f.score,
}
front_yaml = yaml.safe_dump(front, sort_keys=False, allow_unicode=True).rstrip()
body = f.notes.strip()
return f"---\n{front_yaml}\n---\n\n{body}\n" if body else f"---\n{front_yaml}\n---\n"