# Packaging Scholion for PyPI. # # Two decisions worth explaining, because neither is obvious: # # 1. One dependency, and it is deliberate. Reading laboratory PDFs is not a # side feature — for most people it is the first thing the tool does with # their data, and `pdfplumber` is what does it. Declaring it means `pip # install scholion` gives a working application. # # It used to be an extra, and the ingest code compensated by running # `pip install pdfplumber` on its own when a PDF turned up — including with # `--break-system-packages`. A project that promises nothing happens without # the user's command must not install packages into their interpreter behind # their back. Declared beats silent. # # `pysam` stays an extra: it needs compilation, it is platform-specific, and # the genome path needs external tools (bcftools, samtools) anyway. Without # it the built-in tabixlite works — slower, on the standard library. # # 2. The version is read from the VERSION file. That file is already the single # source of truth for the changelog and for publication — there is no reason # to keep a second one. `__version__` in `src/scholion/__init__.py` is not a # constant: it comes from the metadata of the installed distribution, or, when # running from the sources, from that same VERSION file. [build-system] requires = ["hatchling>=1.27"] build-backend = "hatchling.build" [project] name = "scholion" dynamic = ["version"] description = "Local health insights for patients and clinicians: connect lab trends, genetics, prescriptions and wearable data" readme = "README.md" requires-python = ">=3.10" # PEP 639: a licence expression plus the list of files. The code is Apache-2.0, # but the curated knowledge base inside the package is CC BY 4.0, and both files, # together with the required source notices, are obliged to travel in the # distribution. license = "Apache-2.0" license-files = ["LICENSE", "LICENSE-DATA", "NOTICE", "ATTRIBUTION.md"] # The e-mail belongs HERE, in the field the metadata standard has for it, and not # in `[project.urls]`. PyPI rejects a `mailto:` there — «is not a valid url» — and # it rejects it at UPLOAD, after the build, after `twine check` has passed, which # is why two releases reached GitHub and neither reached PyPI. authors = [{ name = "CrossRead", email = "scholion.dev@proton.me" }] keywords = [ "genomics", "pharmacogenomics", "clinvar", "acmg", "cpic", "polygenic-score", "laboratory", "wearables", "personal-health", "second-opinion", "n-of-1", ] # A `License ::` classifier is deliberately absent — PEP 639 prescribes the # `license` field instead. classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: Web Environment", "Intended Audience :: End Users/Desktop", "Intended Audience :: Healthcare Industry", "Intended Audience :: Science/Research", "Natural Language :: English", "Natural Language :: Russian", "Operating System :: MacOS", "Operating System :: POSIX :: Linux", # Added with the matrix cell that checks it, not before. Everything # this package does by itself runs there; what needs a Unix machine # is the OPTIONAL genome preparation in src/ingest — shell scripts # driving bwa, samtools and mosdepth — and the tools it looks for # are each behind a `which`, so their absence is reported rather # than fatal. If the Windows cell goes red, this line comes out. "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Bio-Informatics", "Topic :: Scientific/Engineering :: Medical Science Apps.", ] # PDF extraction and, on Windows, the IANA database absent from the OS. dependencies = [ # Lab PDFs are the first data most people load. See the note at the top. "pdfplumber>=0.11", "tzdata; sys_platform == 'win32'", ] [project.optional-dependencies] # Kept so that `pip install "scholion[pdf]"` from older instructions still works; # pdfplumber is a plain dependency now, so this extra adds nothing. pdf = [] # Fast VCF access. Without it the built-in tabixlite is used — slower, but on # the standard library alone. genome = ["pysam>=0.22"] all = ["scholion[pdf,genome]"] [project.scripts] scholion = "scholion.cli:main" # A second entry point over the same core: `crossread` — "read your sources # against each other". The CLI takes the program name from whatever it was called # as, so the help text reads correctly either way. crossread = "scholion.cli:main" [project.urls] Homepage = "https://github.com/CrossRead/scholion" Repository = "https://github.com/CrossRead/scholion" Changelog = "https://github.com/CrossRead/scholion/blob/main/CHANGELOG.md" Issues = "https://github.com/CrossRead/scholion/issues" # Every value under [project.urls] must be a URL, and PyPI means it. The address # itself now travels in `authors`, where it is metadata rather than a link; this # points at the section of the README that explains what to write about and, more # importantly, what NOT to send. Contact = "https://github.com/CrossRead/scholion#contact" [tool.hatch.version] path = "VERSION" pattern = "(?P[^\\s]+)" [tool.hatch.build.targets.wheel] packages = ["src/scholion"] # The same canonical readers serve source and installed commands. A wheel cannot # rely on src/ingest beside the user's personal data directory. [tool.hatch.build.targets.wheel.force-include] "src/ingest/ingest_garmin.py" = "scholion/readers/ingest_garmin.py" "src/ingest/ingest_whoop.py" = "scholion/readers/ingest_whoop.py" # The sdist carries exactly what is needed for a reproducible build and for # understanding the legal position. The personal directories (profile/, genome/) # are not here and cannot be: they never reach git either. # The tests and the checking tools travel in the sdist deliberately. The release # gate requires running them INSIDE the unpacked artefact rather than in the # repository: a green run at the author's end with a red one at the recipient's # does not mean "verified", it means "the state of the artefact is unknown". # Without tests/ and run_tests.sh such a run is impossible in principle. [tool.hatch.build.targets.sdist] include = [ "/.gitattributes", "/docs", "/src/scholion", "/src/tools", # The data-preparation scripts: the genome build guide points at them, and # part of the test suite leans on them too (the n-of-1 maths lives in # src/ingest/nof1.py). Without them the sdist builds fine and the run inside # it dies on an import — which is what the gate caught on its first run. "/src/ingest", "/tests", "/run_tests.sh", "/README.md", "/CHANGELOG.md", "/CONTRIBUTING.md", "/DISCLAIMER.md", "/SECURITY.md", "/THREAT_MODEL.md", "/PRIVACY.md", "/CITATION.cff", "/ASSISTANT-RULES.md", "/VERSION", ] exclude = [ "**/__pycache__", "**/*.pyc", "**/.DS_Store", ] # Lint — run in CI only (`.github/workflows/tests.yml`, job `lint`); the package # itself still depends on nothing, and `run_tests.sh` asks nothing of the person # who unpacked it. Rule set F (pyflakes): names used but never defined, imports # never used, a name redefined before use. The first of these is the class that # `set -u` catches in shell only when the branch runs — here it is caught on # every push, whatever branch the name sits in. [tool.ruff] target-version = "py310" [tool.ruff.lint] select = ["F"] [tool.ruff.lint.per-file-ignores] # The engine package is a facade: it re-exports the names every face calls it by. "src/scholion/engine/__init__.py" = ["F401"]