#!/usr/bin/env python3# -*- coding: utf-8 -*-"""Stage the euler-content wheel source: `packages/content/` plus the public tree, in a scratch directory. python scripts/content/stage.py OUT_DIR # then: pip install OUT_DIRCopies `packages/content/` to *OUT_DIR*, lays the public tree into `OUT_DIR/euler_content/tree/` andstamps `_version.py` from `packages/solver/solver/version.py` (docs/package-guide.md § 4.1, § 5.2). Only **tracked**files under the public roots are copied — an untracked scratch file never ships — and theworking-tree copy is what ships, as with the code the same deploy installs. Anything under`solutions/private` is refused outright: that tree is not public, and must not be in a packagewhatever else goes wrong. Stdlib-only; run as the checkout's owner (git refuses a repositoryanother uid owns), then install the result as whoever owns the target venv."""from__future__importannotationsimportreimportshutilimportsubprocessimportsysfrompathlibimportPathROOT=Path(__file__).resolve().parents[2]#: The public paths, repo-relative: a file, or a directory taken whole.PUBLIC:tuple[str,...]=('README.md','LICENSE','docs/','blogs/','solutions/public/','solutions/problems.json',# The code viewer's scope (/code/, euler_site.site.content.CODE_ROOTS — the two lists are# held together by tests/test_public_site.py). `packages/` takes in what the pages read# from it too: the solution templates (/docs/file/), the skills (/docs/ai), the home summary.'packages/','scripts/','.gitattributes','.gitignore','.shellcheckrc','install.sh','Makefile','mypy.ini','pyproject.toml','tox.ini',)PRIVATE:str='solutions/private/'defpublic_files(root:Path=ROOT)->list[str]:"""The tracked files under :data:`PUBLIC`, repo-relative; raises if one is private."""listed=subprocess.run(['git','-C',str(root),'ls-files','-z','--',*PUBLIC],check=True,capture_output=True).stdout.decode('utf-8')files=sorted(namefornameinlisted.split('\0')ifname)leaked=[namefornameinfilesifname.startswith(PRIVATE)]ifleaked:raiseSystemExit(f'stage: refusing — {len(leaked)} private file(s) under the public roots')returnfilesdefversion(root:Path=ROOT)->str:"""The code's version — the content's too (lockstep)."""version_file=root/'packages'/'solver'/'solver'/'version.py'match=re.search(r"__version__\s*=\s*'([^']+)'",version_file.read_text(encoding='utf-8'))ifmatchisNone:raiseSystemExit('stage: no __version__ in packages/solver/solver/version.py')returnmatch.group(1)defstage(out:Path,root:Path=ROOT)->int:"""Build the wheel source in *out* (replaced if present); the number of files laid down."""ifout.exists():shutil.rmtree(out)shutil.copytree(root/'packages'/'content',out,ignore=shutil.ignore_patterns('tree','_version.py','__pycache__','*.egg-info','build'))tree=out/'euler_content'/'tree'files=public_files(root)fornameinfiles:source=root/nameifnotsource.is_file():# tracked but deleted in the working treecontinuetarget=tree/nametarget.parent.mkdir(parents=True,exist_ok=True)shutil.copy2(source,target)(out/'euler_content'/'_version.py').write_text(f"__version__ = '{version(root)}'\n",encoding='utf-8')returnlen(files)defmain(argv:list[str])->int:iflen(argv)!=1:print(__doc__,file=sys.stderr)return2count=stage(Path(argv[0]).absolute())print(f'staged euler-content {version()} ({count} files) in {argv[0]}')return0if__name__=='__main__':raiseSystemExit(main(sys.argv[1:]))