#!/usr/bin/env python3
"""
# Script ob-rand
## Metadata
#obsidian/ob-script #python
**Parent**:: [[Obsidian Chore Scripts]], [[Executable Markdown File]]
## Synopsis
Generate [[,rand|Random Board]] for review. Recognize zettel tags in frontmatter `tags`.
Requires PyYAML (`python -m pip install PyYAML`).
## Script
```python
#"""
from collections.abc import Hashable
import datetime
import os
from pathlib import Path
import random
import re
import sys
import yaml
class FrontmatterLoader(yaml.SafeLoader):
def construct_mapping(self, node, deep=False):
mapping = {}
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
if not isinstance(key, Hashable) or key in mapping:
raise yaml.constructor.ConstructorError(
None, None, "Invalid or duplicate property key", key_node.start_mark
)
mapping[key] = self.construct_object(value_node, deep=deep)
return mapping
# Obsidian does not expand YAML merge keys into frontmatter properties.
FrontmatterLoader.yaml_implicit_resolvers = {
key: [(tag, regex) for tag, regex in resolvers if tag != "tag:yaml.org,2002:merge"]
for key, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items()
}
def matches_tag(text, pattern):
match = re.match(r"\A---\n(.*?)\n---(?:\n|$)", text, re.DOTALL)
if not match:
return False
try:
properties = yaml.load(match[1], Loader=FrontmatterLoader)
except yaml.YAMLError:
return False
values = properties.get("tags") if isinstance(properties, dict) else None
if isinstance(values, str):
values = [values]
if not isinstance(values, list):
return False
return any(
isinstance(value, str) and re.search(pattern, "#" + value.strip(), re.IGNORECASE)
for value in values
)
def tagged_files(roots, pattern):
matches = []
for folder in roots:
for root, dirs, files in os.walk(folder):
for file in files:
if file.endswith(".md"):
path = os.path.join(root, file)
if matches_tag(Path(path).read_text(encoding="utf-8"), pattern):
matches.append(path)
return matches
def candidates():
permanent = tagged_files(["dock", "para"], r"#zettel/(literature|permanent|index)")
for folder in ["output/§ Blog/Posts", "output/§ Blog/Games"]:
for root, dirs, files in os.walk(folder):
permanent.extend(
os.path.join(root, file) for file in files if file.startswith("♯ ")
)
fleeting = tagged_files(["dock", "para", "robot"], r"#zettel/fleeting")
return permanent, fleeting
def render_board(permanent, fleeting, now, rng=random):
lines = [f"## {now}", "", f"**Generated**:: [[{now:%Y-%m-%d}]]", ""]
for title, files in [("Permanent", permanent), ("Fleeting", fleeting)]:
lines.extend([f"### {title}", ""])
files = list(files)
rng.shuffle(files)
lines.extend(
f"- [ ] [[{os.path.splitext(os.path.basename(file))[0]}]]"
for file in files[:11]
)
lines.append("")
return "\n".join(lines) + "\n"
def main():
now = datetime.datetime.now()
output = render_board(*candidates(), now)
if os.environ.get("DEBUG"):
sys.stdout.write(output)
else:
with open("shore/boards/,rand.md", "a", encoding="utf-8", newline="\n") as stream:
stream.write(output)
if __name__ == "__main__":
main()
"""
# vim: ft=python
```
"""