Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Any, List, Optional, Sequence, Tuple
from .dates_add_days import days_in_month ← from dates.add-days ^1.0.0 · built alongside by fune
from .todo_item_types import TodoDraft, TodoValidation
MAX_TITLE = 200
MAX_NOTES = 2000
MAX_TAGS = 10
MAX_TAG_LENGTH = 30
MAX_INTERVAL = 999
_PRIORITIES = ("none", "low", "medium", "high")
_FREQUENCIES = ("daily", "weekdays", "weekly", "monthly", "yearly")
_SPACE = " \t\n\r\x0b\x0c"
def trim_space(text: str) -> str:
"""Trim ASCII whitespace only, so "trimmed" means the same in every language."""
return text.strip(_SPACE)
def code_point_length(text: str) -> int:
return len(text)
def _digits(text: str, start: int, stop: int) -> bool:
return all("0" <= ch <= "9" for ch in text[start:stop])
def is_iso_date(text: Any) -> bool:
"""A real calendar date written YYYY-MM-DD, years 0001 to 9999. Never raises."""
if not isinstance(text, str) or len(text) != 10 or text[4] != "-" or text[7] != "-":
return False
if not (_digits(text, 0, 4) and _digits(text, 5, 7) and _digits(text, 8, 10)):
return False
year, month, day = int(text[0:4]), int(text[5:7]), int(text[8:10])
return year >= 1 and 1 <= month <= 12 and 1 <= day <= days_in_month(year, month)
def _is_tag_form(tag: str) -> bool:
if tag == "" or tag[0] == "-" or tag[-1] == "-":
return False
for i, ch in enumerate(tag):
ok = ("a" <= ch <= "z") or ("0" <= ch <= "9") or (ch == "-" and tag[i - 1] != "-")
if not ok:
return False
return True
def _title_error(title: str) -> Optional[str]:
trimmed = trim_space(title)
if trimmed == "":
return "Enter a title."
if any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in trimmed):
return "Keep the title to one line, without control characters."
if len(trimmed) > MAX_TITLE:
return f"Use no more than {MAX_TITLE} characters for the title."
return None
def _tags_error(tags: Sequence[str]) -> Optional[str]:
if len(tags) > MAX_TAGS:
return f"Use no more than {MAX_TAGS} tags."
seen: List[str] = []
for tag in tags:
if not _is_tag_form(tag):
return f'Tag "{tag}" can only use lower-case letters, digits and single hyphens.'
if len(tag) > MAX_TAG_LENGTH:
return f'Tag "{tag}" is longer than {MAX_TAG_LENGTH} characters.'
if tag in seen:
return f'Tag "{tag}" is listed twice.'
seen.append(tag)
return None
def _recurrence_error(draft: Any) -> Optional[str]:
rule = draft.recurrence
if rule is None:
return None
if rule.frequency not in _FREQUENCIES:
return "Choose how it repeats: daily, weekdays, weekly, monthly or yearly."
interval = rule.interval
if isinstance(interval, bool) or not isinstance(interval, int) or interval < 1 or interval > MAX_INTERVAL:
return f"Repeat every 1 to {MAX_INTERVAL} days, weeks, months or years."
if rule.frequency == "weekdays" and interval != 1:
return "Weekday repeats cannot skip weeks; use an interval of 1."
if not is_iso_date(rule.anchor):
return "Enter the repeat start as a real date, YYYY-MM-DD."
if draft.due is None:
return "A repeating todo needs a due date."
if is_iso_date(draft.due) and rule.anchor > draft.due:
return "The repeat cannot start after the due date."
return None
def draft_errors(draft: Any) -> List[Tuple[str, str]]:
"""The edit rules as ordered (field, message) pairs; works on a Todo too."""
errors: List[Tuple[str, str]] = []
title = _title_error(draft.title)
if title is not None:
errors.append(("title", title))
if draft.notes is not None and len(draft.notes) > MAX_NOTES:
errors.append(("notes", f"Use no more than {MAX_NOTES} characters for the notes."))
if draft.priority not in _PRIORITIES:
errors.append(("priority", "Choose a priority: none, low, medium or high."))
if draft.due is not None and not is_iso_date(draft.due):
errors.append(("due", "Enter the due date as a real date, YYYY-MM-DD."))
tags = _tags_error(draft.tags)
if tags is not None:
errors.append(("tags", tags))
recurrence = _recurrence_error(draft)
if recurrence is not None:
errors.append(("recurrence", recurrence))
return errors
def validate_draft(draft: TodoDraft) -> TodoValidation:
"""Check what a person typed into the add or edit form; the title is judged trimmed."""
errors = dict(draft_errors(draft))
return TodoValidation(valid=len(errors) == 0, errors=errors)