2929import re
3030import sys
3131import time
32- from datetime import datetime , timezone
32+ from collections .abc import Callable
33+ from datetime import UTC , datetime
3334from pathlib import Path
35+ from typing import Any
3436from zoneinfo import ZoneInfo
35- from typing import Any , Callable
3637
3738import requests
3839import yaml
5354
5455class PublishError (Exception ):
5556 """Fatal pipeline error; main() converts it to a message and exit 1."""
57+
58+
5659_CONFIG_FILE = Path (__file__ ).parent .parent .parent / "_config.yml"
5760
5861POST_FILENAME_RE = re .compile (r"^(\d{4}-\d{2}-\d{2})-(.+)\.md$" )
5962
60- MANAGED_FIELDS = frozenset (
61- {"$type" , "site" , "path" , "title" , "description" , "tags" , "publishedAt" }
62- )
63+ MANAGED_FIELDS = frozenset ({"$type" , "site" , "path" , "title" , "description" , "tags" , "publishedAt" })
6364
6465# Publication fields the pipeline manages; values come from _config.yml so
6566# site metadata has a single home (title → name, description, url).
@@ -78,13 +79,14 @@ def desired_publication_record(config: dict) -> dict[str, Any]:
7879 "preferences" : {"showInDiscover" : True },
7980 }
8081
82+
8183# ---------------------------------------------------------------------------
8284# Config helpers
8385# ---------------------------------------------------------------------------
8486
8587
8688def load_config (config_path : Path = _CONFIG_FILE ) -> dict :
87- with open (config_path , encoding = "utf-8" ) as fh :
89+ with config_path . open (encoding = "utf-8" ) as fh :
8890 return yaml .safe_load (fh ) or {}
8991
9092
@@ -199,9 +201,7 @@ def _extract_frontmatter_strict(text: str) -> dict:
199201 return {}
200202 fm = yaml .safe_load (m .group (1 )) or {}
201203 if not isinstance (fm , dict ):
202- raise ValueError (
203- f"front matter is not a YAML mapping (got { type (fm ).__name__ } )"
204- )
204+ raise ValueError (f"front matter is not a YAML mapping (got { type (fm ).__name__ } )" )
205205 return fm
206206
207207
@@ -336,7 +336,8 @@ def list_records(self, collection: str) -> list[dict]:
336336 if cursor :
337337 params ["cursor" ] = cursor
338338 resp = self ._retry_request (
339- lambda : self ._http .get (
339+ # Bind params per iteration; the loop rebinds it each page.
340+ lambda params = params : self ._http .get (
340341 f"{ self ._pds } /xrpc/com.atproto.repo.listRecords" ,
341342 params = params ,
342343 headers = self ._auth (),
@@ -438,14 +439,11 @@ def _verify_publication(client: AtprotoClient, publication_uri: str) -> dict:
438439 uri_did , rkey = m .group (1 ), m .group (2 )
439440 if uri_did != client .did :
440441 raise PublishError (
441- f"publication_uri belongs to { uri_did } but the session is "
442- f"authenticated as { client .did } "
442+ f"publication_uri belongs to { uri_did } but the session is authenticated as { client .did } "
443443 )
444444 remote = client .get_record (client .did , "site.standard.publication" , rkey )
445445 if remote is None :
446- raise PublishError (
447- f"publication record does not exist on the PDS: { publication_uri } "
448- )
446+ raise PublishError (f"publication record does not exist on the PDS: { publication_uri } " )
449447 return remote
450448
451449
@@ -460,9 +458,7 @@ def _records_differ(local: dict, remote: dict) -> bool:
460458def _document_sources (
461459 posts_dir : Path , books_dir : Path | None
462460) -> list [tuple [Path , Callable [[Path ], dict | None ]]]:
463- sources : list [tuple [Path , Callable [[Path ], dict | None ]]] = [
464- (posts_dir , parse_post )
465- ]
461+ sources : list [tuple [Path , Callable [[Path ], dict | None ]]] = [(posts_dir , parse_post )]
466462 if books_dir is not None :
467463 bd = books_dir
468464 sources .append ((bd , lambda f : parse_book (f , bd )))
@@ -474,6 +470,7 @@ def _source_files(src_dir: Path) -> list[Path]:
474470 All candidate markdown files under src_dir, recursively, mirroring
475471 Jekyll: directories starting with '_' (templates) are not read.
476472 """
473+
477474 def included (f : Path ) -> bool :
478475 rel = f .relative_to (src_dir )
479476 if any (part .startswith ("_" ) for part in rel .parts [:- 1 ]):
@@ -519,10 +516,7 @@ def _collect_documents(posts_dir: Path, books_dir: Path | None):
519516 if not rec ["title" ].strip ():
520517 errors .append ("missing or empty 'title'" )
521518 if not rec .get ("publishedAt" ):
522- errors .append (
523- "cannot derive publishedAt from the 'date' front matter "
524- "or filename"
525- )
519+ errors .append ("cannot derive publishedAt from the 'date' front matter or filename" )
526520 if rec ["path" ] in seen_paths :
527521 errors .append (
528522 f"duplicate path { rec ['path' ]!r} "
@@ -557,9 +551,7 @@ def sync_documents(
557551 # --- Keep the publication record itself in sync with _config.yml ---
558552 desired_pub = desired_publication_record (config )
559553 remote_pub_value = remote_publication ["value" ]
560- pub_subset = {
561- k : v for k , v in remote_pub_value .items () if k in PUBLICATION_MANAGED_FIELDS
562- }
554+ pub_subset = {k : v for k , v in remote_pub_value .items () if k in PUBLICATION_MANAGED_FIELDS }
563555 if pub_subset != desired_pub :
564556 merged_pub = dict (remote_pub_value )
565557 merged_pub .update (desired_pub )
@@ -580,9 +572,7 @@ def sync_documents(
580572 local : dict [str , tuple [Path , dict ]] = {}
581573 for doc_file , rec , errors in _collect_documents (posts_dir , books_dir ):
582574 if errors :
583- raise PublishError (
584- "; " .join (f"{ doc_file .name } : { msg } " for msg in errors )
585- )
575+ raise PublishError ("; " .join (f"{ doc_file .name } : { msg } " for msg in errors ))
586576 if rec is None :
587577 raise AssertionError ("collector yielded no record and no errors" )
588578 rec ["site" ] = publication_uri
@@ -610,9 +600,7 @@ def sync_documents(
610600 data_map : dict [str , str ] = {}
611601 for path_key , (rkey , _ , _ ) in remote .items ():
612602 if path_key in local :
613- data_map [path_key ] = (
614- f"at://{ client .did } /site.standard.document/{ rkey } "
615- )
603+ data_map [path_key ] = f"at://{ client .did } /site.standard.document/{ rkey } "
616604
617605 # --- Diff and sync ---
618606 created = updated = unchanged = orphaned = 0
@@ -640,15 +628,11 @@ def sync_documents(
640628 merged [key ] = local_rec [key ]
641629 elif key in merged :
642630 del merged [key ]
643- merged ["updatedAt" ] = datetime .now (timezone .utc ).strftime (
644- "%Y-%m-%dT%H:%M:%SZ"
645- )
631+ merged ["updatedAt" ] = datetime .now (UTC ).strftime ("%Y-%m-%dT%H:%M:%SZ" )
646632 if dry_run :
647633 print (f"[dry-run] Would update: { path_key } " )
648634 else :
649- client .put_record (
650- "site.standard.document" , rkey , merged , swap_cid = cid
651- )
635+ client .put_record ("site.standard.document" , rkey , merged , swap_cid = cid )
652636 print (f"Updated: { path_key } " )
653637 updated += 1
654638 else :
@@ -670,13 +654,13 @@ def sync_documents(
670654 if dry_run :
671655 print (f"[dry-run] Would write { len (data_map )} entries to { data_out } " )
672656 else :
673- output = {path_key : data_map [path_key ] for path_key in sorted (local ) if path_key in data_map }
657+ output = {
658+ path_key : data_map [path_key ] for path_key in sorted (local ) if path_key in data_map
659+ }
674660 data_out .parent .mkdir (parents = True , exist_ok = True )
675661 data_out .write_text (json .dumps (output , indent = 2 ) + "\n " , encoding = "utf-8" )
676662
677- print (
678- f"created={ created } updated={ updated } unchanged={ unchanged } orphaned={ orphaned } "
679- )
663+ print (f"created={ created } updated={ updated } unchanged={ unchanged } orphaned={ orphaned } " )
680664
681665
682666# ---------------------------------------------------------------------------
@@ -790,8 +774,7 @@ def validate_documents(
790774 error_count += 1
791775 elif wk .read_text (encoding = "utf-8" ) != expected_publication_uri :
792776 print (
793- f"ERROR: { wk } content does not match "
794- "standard_site.publication_uri" ,
777+ f"ERROR: { wk } content does not match standard_site.publication_uri" ,
795778 file = sys .stderr ,
796779 )
797780 error_count += 1
@@ -821,9 +804,7 @@ def delete_orphans(
821804 local_paths : set [str ] = set ()
822805 for doc_file , rec , errors in _collect_documents (posts_dir , books_dir ):
823806 if errors :
824- raise PublishError (
825- "; " .join (f"{ doc_file .name } : { msg } " for msg in errors )
826- )
807+ raise PublishError ("; " .join (f"{ doc_file .name } : { msg } " for msg in errors ))
827808 if rec is None :
828809 raise AssertionError ("collector yielded no record and no errors" )
829810 local_paths .add (rec ["path" ])
@@ -868,9 +849,7 @@ def delete_orphans(
868849def init_publication (client : AtprotoClient , config : dict ) -> None :
869850 existing = get_publication_uri (config )
870851 if existing :
871- raise PublishError (
872- f"publication_uri is already set in _config.yml: { existing !r} "
873- )
852+ raise PublishError (f"publication_uri is already set in _config.yml: { existing !r} " )
874853 # The config guard is local-only; also check the PDS so running twice
875854 # before committing cannot create two publication records.
876855 remote_pubs = client .list_records ("site.standard.publication" )
@@ -880,9 +859,7 @@ def init_publication(client: AtprotoClient, config: dict) -> None:
880859 f"publication record(s) already exist on the PDS: { uris } — "
881860 "put the URI in _config.yml instead of creating another"
882861 )
883- at_uri = client .create_record (
884- "site.standard.publication" , desired_publication_record (config )
885- )
862+ at_uri = client .create_record ("site.standard.publication" , desired_publication_record (config ))
886863 print (f"Publication record created: { at_uri } " )
887864 print ("Add this URI to _config.yml → standard_site.publication_uri and commit." )
888865
@@ -911,9 +888,7 @@ def _dispatch(argv: list[str] | None = None) -> None:
911888 parser = argparse .ArgumentParser (description = __doc__ )
912889 sub = parser .add_subparsers (dest = "cmd" , required = True )
913890
914- pub_p = sub .add_parser (
915- "publish" , help = "Sync posts and books to PDS document records"
916- )
891+ pub_p = sub .add_parser ("publish" , help = "Sync posts and books to PDS document records" )
917892 pub_p .add_argument ("--posts-dir" , required = True , type = Path )
918893 pub_p .add_argument ("--books-dir" , required = True , type = Path )
919894 pub_p .add_argument ("--data-out" , required = True , type = Path )
@@ -923,8 +898,7 @@ def _dispatch(argv: list[str] | None = None) -> None:
923898
924899 val_p = sub .add_parser (
925900 "validate" ,
926- help = "Validate posts and books for AT Protocol compatibility "
927- "(no network, no credentials)" ,
901+ help = "Validate posts and books for AT Protocol compatibility (no network, no credentials)" ,
928902 )
929903 val_p .add_argument ("--posts-dir" , required = True , type = Path )
930904 val_p .add_argument ("--books-dir" , required = True , type = Path )
0 commit comments