Ship it. Then keep shipping it.
The package works. The tests are green. Now turn the working
repository into an artifact other people can pip install
and use. This lesson is different from every prior one: almost no
new code, but every file you add matters disproportionately for
whether anyone adopts your work. We’ll set up a license, a
README that earns trust, a CHANGELOG that documents what each
release contains, an academic citation file, and a GitHub Actions
workflow that publishes to PyPI on a git tag. Then we’ll
cut v0.1.0 — not as a one-off ceremony, but as
the first turn of a loop you’ll repeat for every future release.
· Objective
Six new files, one workflow, one version bump. Then a tag, a push, and PyPI does the rest.
LICENSE— Apache License 2.0 verbatim. Permissive, recognized, protects attribution.NOTICE— the short attribution file Apache 2.0 expects alongside the license.README.md— the front door. Concise, with install + a real quickstart code example.CHANGELOG.md— Keep a Changelog format. First entry: 0.1.0.CITATION.cff— academic citation metadata. GitHub renders a "Cite this repository" button once public..github/workflows/release.yml— tag-triggered build, test, and publish-to-PyPI.pyproject.toml— version 0.1.0, license metadata, classifiers, project URLs.MANIFEST.in— tells setuptools to include LICENSE/NOTICE/etc. in the sdist.
· Build it, step by step
1 License: Apache 2.0
Apache 2.0 is the right tool when the goal is maximum adoption with proper attribution. Free for researchers, students, public entities, AND companies, with a strict attribution requirement, an explicit patent grant, and an industry-standard footprint that no legal team will object to. Source-available non-commercial licenses (PolyForm and friends) make sense when you want to charge companies; if you don’t, Apache is the better fit.
- Download the canonical text from apache.org/licenses/LICENSE-2.0.txt and save as
LICENSE. - Fill in your copyright line in the appendix (e.g.,
Copyright 2026 Your Name / Your Organization). - Add a short
NOTICEfile with your attribution and a pointer to the LICENSE.
2 README.md — the front door
The single most important file in the repository for adoption. People decide whether to engage in 30 seconds based on what they see here. Concise, with three blocks that earn trust:
- What it is, in 2–3 sentences. No marketing language. State the function.
- Install instructions immediately under the description. Don’t make readers scroll.
- A real quickstart code example that proves the package does something concrete. Not pseudocode — actual imports, actual function calls, actual output paths.
Then: architecture diagram, doc links, license, citation, acknowledgments. Resist the urge to put everything in the README; deep documentation belongs on the docs site.
3 CHANGELOG.md — document what each release contains
Keep a Changelog format with semver. First entry:
## [0.1.0] — 2026-06-07
First public release.
### Added
- `opentrash.core` — CRS, DuckDB session, vehicle IDs
- `opentrash.engine` — the routing engine
- `opentrash.patterns` — per-parcel signature detection
- `opentrash.routeview` — interactive HTML maps
- ...
Future entries follow the same template: ### Added, ### Changed, ### Deprecated, ### Removed, ### Fixed, ### Security. The discipline pays off when someone asks "what changed in 0.4.0?" and you point at the file.
4 CITATION.cff — for academic users
A simple YAML file that GitHub uses to render a "Cite this repository" button (once the repo is public). Tells researchers exactly how to cite the package in papers:
cff-version: 1.2.0
title: "opentrash: open-source geospatial analytics for residential waste collection"
authors:
- given-names: Your
family-names: Name
affiliation: "Your Organization"
license: Apache-2.0
version: "0.1.0"
date-released: "2026-06-07"
url: "https://opentrash.app/"
Small file, big payoff: it’s the difference between getting properly cited and being lumped under "various open-source tools."
5 pyproject.toml — the metadata that lands on PyPI
Three changes from the dev version:
- Version:
0.1.0.dev0→0.1.0. - License:
{ text = "Apache-2.0" }. Classifier:"License :: OSI Approved :: Apache Software License". - Project URLs + classifiers: Homepage, Documentation, Changelog, Issues; classifiers for Development Status, Intended Audience, Topic. These show up on the PyPI page.
One TOML gotcha caught during this lesson: [project.urls] must come AFTER dependencies and [project.optional-dependencies] in the TOML — otherwise dependencies ends up parsed as a key inside project.urls. Setuptools yells. Easy to fix; easy to miss.
6 MANIFEST.in — what gets shipped in the sdist
Setuptools’ defaults don’t include LICENSE, NOTICE, CHANGELOG, or CITATION in the sdist. Tell it to:
include LICENSE
include NOTICE
include CHANGELOG.md
include CITATION.cff
include README.md
recursive-include opentrash/routeview/templates *.html
Verify by building and inspecting the artifact:
pip install build
python -m build
tar tzf dist/opentrash-0.1.0.tar.gz | grep -E "(LICENSE|NOTICE|CHANGELOG|CITATION|README|template)"
7 Release workflow — .github/workflows/release.yml
Tag-triggered automation. Four jobs in sequence:
verify-version— refuses to proceed if the git tag doesn’t match the version in pyproject.toml. Catches typos before they hit PyPI.test— runs the full suite + lint on Python 3.11 and 3.12. Fail fast if something regressed.build— produces sdist + wheel intodist/, uploads as a CI artifact.publish-pypi— downloads the dist artifact, runstwine uploadusing thePYPI_API_TOKENrepository secret.
One-time PyPI setup before the first release:
- Create a PyPI account at pypi.org.
- Generate a project-scoped API token at pypi.org/manage/account/token/. Scope it to the
opentrashproject once it exists; for the very first upload, use an account-wide token, then narrow it. - In the repo, add a secret named
PYPI_API_TOKENat Settings → Secrets and variables → Actions → New repository secret.
Private repo → public PyPI is fine. They’re independent systems. The PyPI artifact contains the source code snapshot, but the git history, issues, and drafts stay private until you flip the repository’s public switch.
8 Cut the release
# 1. CHANGELOG entry already added in step 3
# 2. pyproject.toml version already bumped to 0.1.0 in step 5
# 3. Commit the release-prep changes
git add LICENSE NOTICE README.md CHANGELOG.md CITATION.cff \
MANIFEST.in pyproject.toml \
.github/workflows/release.yml
git commit -m "Release 0.1.0: license, README, CHANGELOG, citation, release workflow"
git push origin main
# 4. Tag and push
git tag -a v0.1.0 -m "First public release"
git push origin v0.1.0
# 5. Watch GitHub Actions run the workflow. It verifies, tests, builds,
# and publishes to PyPI automatically.
# 6. Verify (from any clean environment, ~5 minutes after the workflow finishes)
pip install opentrash
python -c "import opentrash; print(opentrash.__version__)"
# expected: 0.1.0
· The release loop (for every future version)
v0.1.0 was the first turn. Every future release is the same six steps. Memorize this; you’ll do it many times.
# 1. Update CHANGELOG with the new entry
# (keep [Unreleased] at top; add a new ## [0.2.0] — YYYY-MM-DD section below)
# 2. Bump version in pyproject.toml AND opentrash/__init__.py to 0.2.0
# 3. Commit
git add CHANGELOG.md pyproject.toml opentrash/__init__.py
git commit -m "Release 0.2.0"
git push origin main
# 4. Tag and push
git tag -a v0.2.0 -m "Release 0.2.0: "
git push origin v0.2.0
# 5. Watch the workflow run. It does the rest.
# 6. Verify
pip install --upgrade opentrash
python -c "import opentrash; print(opentrash.__version__)"
That’s it. The discipline isn’t in the steps; it’s in following them every time. Six steps, every release, forever — through 0.2.0, 0.3.0, the eventual 1.0.0, and beyond.
· Package anatomy after this lesson
The final shape. new marks files added this lesson.
· What you built — the whole course
12 lessons. One working open-source package. Here’s the entire arc:
- L0–L2: notebook → package, repo skeleton, core foundations (CRS, DuckDB, parcels).
- L3–L5: static GIS layers, sites, tonnage pipeline with idempotent upsert.
- L6–L7: GPS adapters, cache, master index, WKB substrate.
- L8–L9: the routing engine — one spatial-join pass — and load-organized segments timeline.
- L10: route-agnostic per-parcel pattern detection.
- L11: RouteView interactive HTML map with patterns overlay.
- L12: license, README, CHANGELOG, citation, release workflow, and the published
pip install opentrashartifact.
The package is now live on PyPI, citable, properly licensed, and ready for the next turn of the loop. Congratulations: you’ve built and shipped a real open-source artifact.
· Companion resources
For going further.
- Apache 2.0 license guide: canonical text + how-to from the ASF. The patent-grant clause is the under-appreciated value.
- Choosing a license: choosealicense.com — GitHub’s own walkthrough. Run through the questionnaire if you’re unsure.
- Keep a Changelog: keepachangelog.com — the format we use.
- Semantic Versioning: semver.org — the contract that lets users pin
opentrash >= 0.1, < 0.2and trust what they get. - PyPI publishing guide: Python Packaging Authority tutorial — for when you want to understand what the workflow is doing under the hood.
- Citation.cff spec: citation-file-format.github.io — the schema is small and clear.
· What’s next
The course ends here, but the package’s life is just starting. Some honest next directions if you’d like to keep building:
- 0.2.0: the next release. Could be road-network mileage to replace the haversine approximation; could be a tonnage-into-segments product; could be tile-server alternatives in RouteView. Whatever real use surfaces first.
- Per-commodity awareness: the engine currently
collapses to one
route_id; production has three (refuse, organics, recycling). A future release could carry the commodity dimension through the engine. - conda-forge package: PyPI handles pip users; conda-forge reaches the scientific-Python conda crowd. The recipe is small once PyPI is live.
- Public repo flip: when you’re ready, making the GitHub repo public unlocks issues, PRs, GitHub Stars for visibility, and the "Cite this repository" button.
Thanks for walking through 12 lessons. Now go build the next one. 🚀