pydantic model generator for atproto lexicons

docs: improve adoption guide with ci examples, add benchmark

- fix import path example (your_project.models not src.models.models)
- add pre-commit, justfile, and github actions examples
- add bench.py for measuring performance on real lexicons
- add `just bench` target

benchmark results (297 atproto lexicons → 2669 lines):
cold generation: ~0.17s
cache hit: ~0.07s

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+89 -5
+30 -5
README.md
··· 86 86 ### 3. use in your code 87 87 88 88 ```python 89 - from src.models.models import FmPlyrTrack, FmPlyrLike 89 + from your_project.models import FmPlyrTrack, FmPlyrLike 90 90 91 91 track = FmPlyrTrack( 92 92 uri="at://did:plc:xyz/fm.plyr.track/123", ··· 97 97 98 98 ### 4. regenerate when lexicons change 99 99 100 - add to your build/ci: 100 + **option a: pre-commit hook** 101 + 102 + ```yaml 103 + # .pre-commit-config.yaml 104 + repos: 105 + - repo: local 106 + hooks: 107 + - id: pmgfal 108 + name: generate atproto models 109 + entry: uvx pmgfal ./lexicons -o ./src/models -p fm.plyr 110 + language: system 111 + files: ^lexicons/.*\.json$ 112 + pass_filenames: false 113 + ``` 114 + 115 + **option b: justfile** 116 + 117 + ```just 118 + # justfile 119 + generate: 120 + uvx pmgfal ./lexicons -o ./src/models -p fm.plyr 121 + ``` 122 + 123 + **option c: github actions** 101 124 102 - ```bash 103 - uvx pmgfal ./lexicons -o ./src/models -p fm.plyr 125 + ```yaml 126 + # .github/workflows/ci.yml 127 + - name: generate models 128 + run: uvx pmgfal ./lexicons -o ./src/models -p fm.plyr 104 129 ``` 105 130 106 - caching ensures this is fast when lexicons haven't changed. 131 + caching ensures regeneration is fast (~0.3s for 300 lexicons) when files haven't changed. 107 132 108 133 ## external refs 109 134
+55
bench.py
··· 1 + #!/usr/bin/env python3 2 + """benchmark pmgfal on real lexicons.""" 3 + 4 + import subprocess 5 + import tempfile 6 + import time 7 + from pathlib import Path 8 + 9 + 10 + def bench_atproto(): 11 + """benchmark against full atproto lexicons.""" 12 + with tempfile.TemporaryDirectory() as tmp: 13 + # clone atproto 14 + print("cloning atproto lexicons...") 15 + subprocess.run( 16 + ["git", "clone", "--depth=1", "https://github.com/bluesky-social/atproto.git", tmp], 17 + capture_output=True, 18 + check=True, 19 + ) 20 + 21 + lexicon_dir = Path(tmp) / "lexicons" 22 + output_dir = Path(tmp) / "output" 23 + json_files = list(lexicon_dir.rglob("*.json")) 24 + 25 + print(f"found {len(json_files)} lexicon files") 26 + 27 + # benchmark generation (cold) 28 + start = time.perf_counter() 29 + subprocess.run( 30 + ["uv", "run", "pmgfal", str(lexicon_dir), "-o", str(output_dir), "--no-cache"], 31 + check=True, 32 + ) 33 + cold_time = time.perf_counter() - start 34 + 35 + # count output 36 + models_file = output_dir / "models.py" 37 + lines = len(models_file.read_text().splitlines()) if models_file.exists() else 0 38 + 39 + # benchmark cache hit 40 + start = time.perf_counter() 41 + subprocess.run( 42 + ["uv", "run", "pmgfal", str(lexicon_dir), "-o", str(output_dir)], 43 + check=True, 44 + ) 45 + cache_time = time.perf_counter() - start 46 + 47 + print(f"\nresults:") 48 + print(f" lexicons: {len(json_files)}") 49 + print(f" output: {lines} lines") 50 + print(f" cold generation: {cold_time:.3f}s") 51 + print(f" cache hit: {cache_time:.3f}s") 52 + 53 + 54 + if __name__ == "__main__": 55 + bench_atproto()
+4
justfile
··· 20 20 uv run ruff check --fix . 21 21 uv run ruff format . 22 22 23 + # benchmark on atproto lexicons 24 + bench: dev 25 + uv run python bench.py 26 + 23 27 # clean build artifacts 24 28 clean: 25 29 rm -rf target dist *.egg-info