diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 33249e9..b644424 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -4,8 +4,9 @@ on:
push:
branches:
- main
- - fixup-docs
- - ab/fy26-roadmap
+ pull_request:
+ branches:
+ - main
jobs:
build:
diff --git a/.github/workflows/update-reports.yml b/.github/workflows/update-reports.yml
new file mode 100644
index 0000000..c5e1fef
--- /dev/null
+++ b/.github/workflows/update-reports.yml
@@ -0,0 +1,65 @@
+name: Update Reports
+
+on:
+ schedule:
+ # Run every Monday at 9 AM ET (14:00 UTC)
+ - cron: '0 14 * * 1'
+ push:
+ branches:
+ - main
+ paths:
+ - 'reports/*.py'
+ - 'reports/pyproject.toml'
+ workflow_dispatch:
+
+jobs:
+ update-reports:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ version: "0.9.*"
+ enable-cache: true
+
+ - name: Generate config data
+ working-directory: reports
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: uv run generate_config.py
+
+ - name: Generate commit data
+ working-directory: reports
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: uv run main.py
+
+ - name: Generate plot
+ working-directory: reports
+ run: uv run plot.py
+
+ - name: Generate docs page
+ working-directory: reports
+ run: uv run generate_docs.py
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v7.0.11
+ with:
+ commit-message: "Update reports for ${{ github.run_id }}"
+ title: "Update reports"
+ body: |
+ Automated update of commit reports and visualization.
+
+ Generated by GitHub Actions workflow.
+ branch: update-reports
+ add-paths: |
+ reports/output/
+ docs/images/
+ docs/objectives.md
diff --git a/docs/images/pi-26.1.png b/docs/images/pi-26.1.png
new file mode 100644
index 0000000..25f075a
Binary files /dev/null and b/docs/images/pi-26.1.png differ
diff --git a/docs/objectives.md b/docs/objectives.md
new file mode 100644
index 0000000..267f206
--- /dev/null
+++ b/docs/objectives.md
@@ -0,0 +1,84 @@
+# Quarterly Objectives
+
+This page tracks quarterly objectives and their related repositories across Program Increments (PIs).
+
+## Current PI: 26.1
+
+| # | Objective | Contributors | Repos |
+|---|-----------|--------------|-------|
+| [#244](https://github.com/NASA-IMPACT/veda-odd/issues/244) | 🗺️ Add dynamic tiling and timeseries support for Virtual Zar... | jbusecke, hrodmn | titiler, icechunk |
+| [#245](https://github.com/NASA-IMPACT/veda-odd/issues/245) | 🌍 Add dynamic tiling and timeseries support for datasets in ... | abarciauskas-bgse, hrodmn | titiler, titiler-cmr, titiler-lambda-layer, titiler-md-demo, python_cmr |
+| [#246](https://github.com/NASA-IMPACT/veda-odd/issues/246) | 🤖 Support virtualization of additional data products | sharkinsspatial, maxrjones, jbusecke | virtualizarr, obspec-utils, virtual-tiff, hrrr-parser, async-tiff, virtualizarr-data-pipelines, icechunk |
+| [#247](https://github.com/NASA-IMPACT/veda-odd/issues/247) | 🛰 Explore scalable, cloud native approaches for search, disc... | sharkinsspatial, kylebarron | obstore, obspec, zarr-datafusion-search, geoarrow-rs, arrow-zarr |
+| [#248](https://github.com/NASA-IMPACT/veda-odd/issues/248) | 🤗 Support community adoption of the technologies incubated b... | sharkinsspatial, chuckwondo, maxrjones, abarciauskas-bgse | geozarr-spec, zarr-python, multiscales, geo-proj, spatial, datacube-guide, geozarr-examples, warp-resample-profiling, pangeo.io, pangeo-docker-images, earthdata-cloud-cookbook |
+
+---
+
+
+PI 25.4 (8 objectives, 5 closed)
+
+| # | Objective | State | Contributors |
+|---|-----------|-------|--------------|
+| [#121](https://github.com/NASA-IMPACT/veda-odd/issues/121) | Visualize Web-Optimized Zarr (WOZ) in VEDA (previe... | closed | maxrjones |
+| [#122](https://github.com/NASA-IMPACT/veda-odd/issues/122) | Research, develop and document methods for Zarr an... | closed | maxrjones, kylebarron |
+| [#197](https://github.com/NASA-IMPACT/veda-odd/issues/197) | 🎬 TiTiler-CMR is production ready | open | abarciauskas-bgse, hrodmn |
+| [#198](https://github.com/NASA-IMPACT/veda-odd/issues/198) | 🚀 Dataset support for VEDA instances | closed | maxrjones, jbusecke |
+| [#203](https://github.com/NASA-IMPACT/veda-odd/issues/203) | 🗺️Research, develop and document methods for Zarr ... | open | maxrjones |
+| [#204](https://github.com/NASA-IMPACT/veda-odd/issues/204) | 🛠️ Zarr Development | open | d-v-b, maxrjones |
+| [#205](https://github.com/NASA-IMPACT/veda-odd/issues/205) | 🤗 Community engagement | closed | sharkinsspatial, chuckwondo, maxrjones, abarciauskas-bgse |
+| [#206](https://github.com/NASA-IMPACT/veda-odd/issues/206) | 📦 Obstore outreach | closed | chuckwondo, kylebarron |
+
+
+
+
+PI 25.3 (6 objectives, 4 closed)
+
+| # | Objective | State | Contributors |
+|---|-----------|-------|--------------|
+| [#118](https://github.com/NASA-IMPACT/veda-odd/issues/118) | Support CMR Modernization | open | sharkinsspatial, kylebarron |
+| [#119](https://github.com/NASA-IMPACT/veda-odd/issues/119) | Continue to Build Out the VirtualiZarr Ecosystem | closed | sharkinsspatial, maxrjones |
+| [#124](https://github.com/NASA-IMPACT/veda-odd/issues/124) | Publish Cloud-Optimized Datasets | open | chuckwondo, abarciauskas-bgse |
+| [#126](https://github.com/NASA-IMPACT/veda-odd/issues/126) | Support TiTiler-CMR Adoption | closed | sharkinsspatial, hrodmn |
+| [#127](https://github.com/NASA-IMPACT/veda-odd/issues/127) | Community Involvement | closed | maxrjones, abarciauskas-bgse, hrodmn |
+| [#165](https://github.com/NASA-IMPACT/veda-odd/issues/165) | Foundational Zarr-Python and Xarray Contributions | closed | d-v-b, maxrjones |
+
+
+
+
+PI 25.2 (8 objectives, 8 closed)
+
+| # | Objective | State | Contributors |
+|---|-----------|-------|--------------|
+| [#31](https://github.com/NASA-IMPACT/veda-odd/issues/31) | Increase data format support in VirtualiZarr | closed | chuckwondo, maxrjones |
+| [#34](https://github.com/NASA-IMPACT/veda-odd/issues/34) | Visualize OCO-3 Datasets in VEDA | closed | abarciauskas-bgse |
+| [#35](https://github.com/NASA-IMPACT/veda-odd/issues/35) | Deliver Virtual Zarr Stores for NASA Datasets Usin... | closed | abarciauskas-bgse |
+| [#36](https://github.com/NASA-IMPACT/veda-odd/issues/36) | Support for Modernizing VirtualiZarr to use zarr-p... | closed | sharkinsspatial, abarciauskas-bgse |
+| [#37](https://github.com/NASA-IMPACT/veda-odd/issues/37) | Support CMR Modernization | closed | sharkinsspatial, kylebarron |
+| [#40](https://github.com/NASA-IMPACT/veda-odd/issues/40) | Upgrade titiler and titiler-xarray to zarr-Python ... | closed | maxrjones |
+| [#41](https://github.com/NASA-IMPACT/veda-odd/issues/41) | Draft Web-Optimized Zarr (WOZ) Standard | closed | maxrjones |
+| [#76](https://github.com/NASA-IMPACT/veda-odd/issues/76) | Demonstrate how to tile HLS using titiler-cmr | closed | hrodmn |
+
+
+
+---
+
+## Commits Per Repository
+
+The commits per repository chart uses color-coding to show which objective each repo contributes to. Repos that contribute to multiple objectives are shown with split bars.
+
+
+
+---
+
+## Configuration
+
+Objectives are configured in [`reports/config.py`](https://github.com/NASA-IMPACT/veda-odd/blob/main/reports/config.py).
+
+To regenerate this page from config:
+
+```bash
+cd reports
+uv run generate_docs.py
+```
+
+See [FY26 Roadmap](./fy26-roadmap.md) for the broader context of these objectives.
diff --git a/mkdocs.yml b/mkdocs.yml
index aff1143..b2322ba 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -11,6 +11,7 @@ extra:
nav:
- "index.md"
- FY26 Roadmap: "fy26-roadmap.md"
+ - PI Objectives: "objectives.md"
- ODD Products: "products.md"
theme:
diff --git a/reports/README.md b/reports/README.md
index e730539..5357d6a 100644
--- a/reports/README.md
+++ b/reports/README.md
@@ -6,10 +6,40 @@
2. Select public repositories
3. Add new token as the environment variable `GH_ODD_PAT`
+## Configuration
+
+The `config.py` file contains:
+- `TIME_RANGE`: Start and end dates for commit analysis
+- `OBJECTIVES`: Quarterly objectives with repos and contributors per objective
+
+### Regenerating objectives from GitHub
+
+To fetch the latest objectives from GitHub issues:
+
+```bash
+uv run generate_config.py
+```
+
+This generates `objectives_config.py` with objectives and contributors from issues labeled `pi-*-objective`. You'll need to manually add repos to each objective, then copy to `config.py`.
+
+### Regenerating docs/objectives.md
+
+To regenerate the objectives documentation page from config:
+
+```bash
+uv run generate_docs.py
+```
+
## Generating data
-1. Update the dates in config.py
-2. Add any new contributors to config.py
-3. Add any new repositories to config.py
-4. Run `uv run main.py`
-5. Run `uv run plot.py`
+1. Run `uv run main.py` (uses 10 parallel workers by default)
+2. Run `uv run plot.py`
+
+`TIME_RANGE` is automatically set to the current fiscal quarter (Q1: Oct-Dec, Q2: Jan-Mar, Q3: Apr-Jun, Q4: Jul-Sep).
+
+The generated chart colors bars by PI objective (see [objectives page](https://nasa-impact.github.io/veda-odd/objectives) for details).
+
+## Performance
+
+- **generate_config.py**: Uses GitHub search API to fetch only objective issues (~2-3 seconds)
+- **main.py**: Parallelizes API calls with ThreadPoolExecutor (10x faster than sequential)
diff --git a/reports/config.py b/reports/config.py
index a9dde74..3e84ac1 100644
--- a/reports/config.py
+++ b/reports/config.py
@@ -1,84 +1,398 @@
-TIME_RANGE = ("20251106", "20251215")
-
-# Name, username, start date, end date
-USERS = [
- # ODD
- ("Max Jones", "maxrjones", None, None),
- ("Kyle Barron", "kylebarron", None, None),
- ("Aimee Barciauskas", "abarciauskas-bgse", None, None),
- ("Chuck Daniels", "chuckwondo", None, None),
- ("Sean Harkins", "sharkinsspatial", None, None),
- ("Henry Rodman", "hrodmn", None, None),
- ("Julius Busecke", "jbusecke", "20250623", None),
- # Science support
- ("Alex Mandel", "wildintellect", None, None),
- ("Julia Signell", "jsignell", None, None),
- ("Sheyenne Kirkland", "smk0033", None, None),
- ("Zac Deziel", "zacdezgeo", None, None),
- ("Nathan Zimmermann", "moradology", None, None),
- # JupyterHub
- ("Sanjay Bhangar", "batpad", None, None),
- ("Tarashish Mishra", "sunu", None, None),
-]
-
-
-REPOS = [
- ("apache", "arrow-rs"),
- ("boettiger-lab", "earthdatalogin"),
- ("conda-forge", "r-lasr-feedstock"),
- ("cloudnativegeo", "cloud-optimized-geospatial-formats-guide"),
- ("datafusion-contrib", "arrow-zarr"),
- ("developmentseed", "async-tiff"),
- ("developmentseed", "datacube-guide"),
- ("developmentseed", "eoAPI"),
- ("developmentseed", "eoAPI-cdk"),
- ("developmentseed", "geozarr-examples"),
- ("developmentseed", "lonboard"),
- ("developmentseed", "rio-stac"),
- ("developmentseed", "tilebench"),
- ("developmentseed", "titiler"),
- ("developmentseed", "titiler-cmr"),
- ("developmentseed", "titiler-lambda-layer"),
- ("developmentseed", "titiler-md-demo"),
- ("developmentseed", "titiler-stacapi"),
- ("developmentseed", "warp-resample-profiling"),
- ("developmentseed", "obspec"),
- ("developmentseed", "obstore"),
- ("developmentseed", "virtualizarr-data-pipelines"),
- ("developmentseed", "zarr-datafusion-search"),
- ("earth-mover", "icechunk"),
- ("flatgeobuf", "flatgeobuf"),
- ("geoarrow", "deck.gl-layers"),
- ("geoarrow", "geoarrow-rs"),
- ("geopandas", "geopandas"),
- ("georust", "geo-svg"),
- ("holoviz", "hvplot"),
- ("jupyterhub", "repo2docker"),
- ("pangeo-data", "pangeo-docker-images"),
- ("pangeo-data", "pangeo.io"),
- ("maap-project", "gedi-geoparquet"),
- ("nasa", "python_cmr"),
- ("nasa-openscapes", "earthdata-cloud-cookbook"),
- ("nsidc", "earthaccess"),
- ("nsidc", "cloud-optimized-icesat2"),
- ("numbagg", "numbagg"),
- ("pydata", "xarray"),
- ("radiantearth", "stac-spec"),
- ("stac-extensions", "zarr"),
- ("stac-utils", "stac-fastapi-pgstac"),
- ("stac-utils", "rustac-py"),
- ("stac-utils", "pgstac"),
- ("stac-utils", "pystac"),
- ("stac-utils", "pystac-client"),
- ("stac-utils", "xpystac"),
- ("virtual-zarr", "hrrr-parser"),
- ("virtual-zarr", "virtual-tiff"),
- ("virtual-zarr", "obspec-utils"),
- ("zarr-conventions", "spatial"),
- ("zarr-conventions", "geo-proj"),
- ("zarr-conventions", "multiscales"),
- ("zarr-developers", "zarr-python"),
- ("zarr-developers", "geozarr-spec"),
- ("zarr-developers", "VirtualiZarr"),
- ("2i2c-org", "jupyterhub-fancy-profiles"),
-]
+from datetime import date
+
+# Manually maintained PI date ranges
+# Update these when new PIs are planned
+PI_DATES = {
+ "pi-25.2": ("20250119", "20250418"),
+ "pi-25.3": ("20250419", "20250718"),
+ "pi-25.4": ("20250719", "20251018"),
+ "pi-26.1": ("20251019", "20260117"),
+ "pi-26.2": ("20260118", "20260425"),
+}
+
+
+def get_current_pi():
+ """Find the current PI based on today's date."""
+ today = date.today().strftime("%Y%m%d")
+ for pi_name, (start, end) in PI_DATES.items():
+ if start <= today <= end:
+ return pi_name
+ return None
+
+
+def get_time_range(pi: str = None):
+ """Get date range for a PI, or current PI if not specified."""
+ if pi:
+ return PI_DATES.get(pi)
+ current = get_current_pi()
+ if current:
+ return PI_DATES[current]
+ # Fallback to most recent PI if not in any range
+ return list(PI_DATES.values())[-1]
+
+
+TIME_RANGE = get_time_range()
+
+# Quarterly objectives with repos and contributors per objective
+# Run `uv run generate_config.py` to regenerate from GitHub issues
+# - Objectives: Issues with pi-X.Y-objective labels
+# - Contributors: Issue assignees
+# - Repos: Labels matching repo:org/repo-name
+OBJECTIVES = {
+ "pi-25.2": [
+ {
+ "issue_number": 31,
+ "title": "ODD PI 25.2 Objective 7: Increase data format support in VirtualiZarr",
+ "state": "closed",
+ "contributors": [
+ ("Chuck Daniels", "chuckwondo"),
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 34,
+ "title": "ODD PI 25.2 Objective 3: Visualize OCO-3 Datasets in VEDA",
+ "state": "closed",
+ "contributors": [
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 35,
+ "title": "ODD PI 25.2 Objective 5: Deliver Virtual Zarr Stores for NASA Datasets Using Icechunk",
+ "state": "closed",
+ "contributors": [
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 36,
+ "title": "ODD PI 25.2 Objective 6: Support for Modernizing VirtualiZarr to use zarr-python 3.0",
+ "state": "closed",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 37,
+ "title": "ODD PI 25.2 Objective 8: Support CMR Modernization",
+ "state": "closed",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Kyle Barron", "kylebarron"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 40,
+ "title": "ODD PI 25.2 Objective 1: Upgrade titiler and titiler-xarray to zarr-Python 3.0 and deploy to staging",
+ "state": "closed",
+ "contributors": [
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 41,
+ "title": "ODD PI 25.2 Objective 4: Draft Web-Optimized Zarr (WOZ) Standard",
+ "state": "closed",
+ "contributors": [
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 76,
+ "title": "ODD PI 25.2 Objective 2: Demonstrate how to tile HLS using titiler-cmr",
+ "state": "closed",
+ "contributors": [
+ ("Henry Rodman", "hrodmn"),
+ ],
+ "repos": [],
+ },
+ ],
+ "pi-25.3": [
+ {
+ "issue_number": 118,
+ "title": "ODD PI 25.3 Objective 1: Support CMR Modernization",
+ "state": "open",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Kyle Barron", "kylebarron"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 119,
+ "title": "ODD PI 25.3 Objective 2: Continue to Build Out the VirtualiZarr Ecosystem",
+ "state": "closed",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 124,
+ "title": "ODD PI 25.3 Objective 3: Publish Cloud-Optimized Datasets",
+ "state": "open",
+ "contributors": [
+ ("Chuck Daniels", "chuckwondo"),
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 126,
+ "title": "ODD PI 25.3 Objective 4: Support TiTiler-CMR Adoption",
+ "state": "closed",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Henry Rodman", "hrodmn"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 127,
+ "title": "ODD PI 25.3 Objective 6: Community Involvement",
+ "state": "closed",
+ "contributors": [
+ ("Max Jones", "maxrjones"),
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ("Henry Rodman", "hrodmn"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 165,
+ "title": "ODD PI 25.3 Objective 5: Foundational Zarr-Python and Xarray Contributions",
+ "state": "closed",
+ "contributors": [
+ ("Davis Bennett", "d-v-b"),
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ ],
+ "pi-25.4": [
+ {
+ "issue_number": 121,
+ "title": "Potential ODD PI 25.4 Objective: Visualize Web-Optimized Zarr (WOZ) in VEDA (preview)",
+ "state": "closed",
+ "contributors": [
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 122,
+ "title": "Potential 25.4 ODD Objective: Research, develop and document methods for Zarr and VirtualiZarr visualization",
+ "state": "closed",
+ "contributors": [
+ ("Max Jones", "maxrjones"),
+ ("Kyle Barron", "kylebarron"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 197,
+ "title": "ODD PI 25.4 Objective 1: 🎬 TiTiler-CMR is production ready",
+ "state": "open",
+ "contributors": [
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ("Henry Rodman", "hrodmn"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 198,
+ "title": "ODD PI 25.4 Objective 2: 🚀 Dataset support for VEDA instances",
+ "state": "closed",
+ "contributors": [
+ ("Max Jones", "maxrjones"),
+ ("Julius Busecke", "jbusecke"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 203,
+ "title": "ODD PI 25.4 Objective 3: 🗺️Research, develop and document methods for Zarr and VirtualiZarr data services",
+ "state": "open",
+ "contributors": [
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 204,
+ "title": "ODD PI 25.4 Objective 4: 🛠️ Zarr Development",
+ "state": "open",
+ "contributors": [
+ ("Davis Bennett", "d-v-b"),
+ ("Max Jones", "maxrjones"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 205,
+ "title": "ODD PI 25.4 Objective 5: 🤗 Community engagement",
+ "state": "closed",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Chuck Daniels", "chuckwondo"),
+ ("Max Jones", "maxrjones"),
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ],
+ "repos": [],
+ },
+ {
+ "issue_number": 206,
+ "title": "ODD PI 25.4 Objective 6: 📦 Obstore outreach",
+ "state": "closed",
+ "contributors": [
+ ("Chuck Daniels", "chuckwondo"),
+ ("Kyle Barron", "kylebarron"),
+ ],
+ "repos": [],
+ },
+ ],
+ "pi-26.1": [
+ {
+ "issue_number": 244,
+ "title": "ODD PI 26.1 Objective 1: 🗺️ Add dynamic tiling and timeseries support for Virtual Zarr Stores",
+ "state": "open",
+ "contributors": [
+ ("Julius Busecke", "jbusecke"),
+ ("Henry Rodman", "hrodmn"),
+ ],
+ "repos": [
+ ("developmentseed", "titiler"),
+ ("earth-mover", "icechunk"),
+ ],
+ },
+ {
+ "issue_number": 245,
+ "title": "ODD PI 26.1 Objective 2: 🌍 Add dynamic tiling and timeseries support for datasets in CMR",
+ "state": "open",
+ "contributors": [
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ("Henry Rodman", "hrodmn"),
+ ],
+ "repos": [
+ ("developmentseed", "titiler"),
+ ("developmentseed", "titiler-cmr"),
+ ("developmentseed", "titiler-lambda-layer"),
+ ("developmentseed", "titiler-md-demo"),
+ ("nasa", "python_cmr"),
+ ],
+ },
+ {
+ "issue_number": 246,
+ "title": "ODD PI 26.1 Objective 3: 🤖 Support virtualization of additional data products",
+ "state": "open",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Max Jones", "maxrjones"),
+ ("Julius Busecke", "jbusecke"),
+ ],
+ "repos": [
+ ("zarr-developers", "virtualizarr"),
+ ("virtual-zarr", "obspec-utils"),
+ ("virtual-zarr", "virtual-tiff"),
+ ("virtual-zarr", "hrrr-parser"),
+ ("developmentseed", "async-tiff"),
+ ("developmentseed", "virtualizarr-data-pipelines"),
+ ("earth-mover", "icechunk"),
+ ],
+ },
+ {
+ "issue_number": 247,
+ "title": "ODD PI 26.1 Objective 4: 🛰 Explore scalable, cloud native approaches for search, discovery and access of non-gridded data",
+ "state": "closed",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Kyle Barron", "kylebarron"),
+ ],
+ "repos": [
+ ("developmentseed", "obstore"),
+ ("developmentseed", "obspec"),
+ ("developmentseed", "zarr-datafusion-search"),
+ ("geoarrow", "geoarrow-rs"),
+ ("datafusion-contrib", "arrow-zarr"),
+ ],
+ },
+ {
+ "issue_number": 248,
+ "title": "ODD PI 26.1 Objective 5: 🤗 Support community adoption of the technologies incubated by EODC and VEDA",
+ "state": "open",
+ "contributors": [
+ ("Sean Harkins", "sharkinsspatial"),
+ ("Chuck Daniels", "chuckwondo"),
+ ("Max Jones", "maxrjones"),
+ ("Aimee Barciauskas", "abarciauskas-bgse"),
+ ],
+ "repos": [
+ ("zarr-developers", "geozarr-spec"),
+ ("zarr-developers", "zarr-python"),
+ ("zarr-conventions", "multiscales"),
+ ("zarr-conventions", "geo-proj"),
+ ("zarr-conventions", "spatial"),
+ ("developmentseed", "datacube-guide"),
+ ("developmentseed", "geozarr-examples"),
+ ("developmentseed", "warp-resample-profiling"),
+ ("pangeo-data", "pangeo.io"),
+ ("pangeo-data", "pangeo-docker-images"),
+ ("nasa-openscapes", "earthdata-cloud-cookbook"),
+ ],
+ },
+ ],
+}
+
+
+def get_all_repos():
+ """Derive unique repos from all objectives."""
+ repos = set()
+ for pi_objectives in OBJECTIVES.values():
+ for obj in pi_objectives:
+ for repo in obj["repos"]:
+ repos.add(repo)
+ return sorted(repos)
+
+
+def get_all_contributors():
+ """Derive unique contributors from all objectives."""
+ contributors = {}
+ for pi_objectives in OBJECTIVES.values():
+ for obj in pi_objectives:
+ for name, username in obj["contributors"]:
+ contributors[username] = name
+ return [
+ (name, username)
+ for username, name in sorted(contributors.items(), key=lambda x: x[1])
+ ]
+
+
+def get_repos_for_pi(pi: str):
+ """Get all repos for a specific PI."""
+ repos = set()
+ for obj in OBJECTIVES.get(pi, []):
+ for repo in obj["repos"]:
+ repos.add(repo)
+ return sorted(repos)
+
+
+def get_contributors_for_pi(pi: str):
+ """Get all contributors for a specific PI."""
+ contributors = {}
+ for obj in OBJECTIVES.get(pi, []):
+ for name, username in obj["contributors"]:
+ contributors[username] = name
+ return [
+ (name, username)
+ for username, name in sorted(contributors.items(), key=lambda x: x[1])
+ ]
diff --git a/reports/generate_config.py b/reports/generate_config.py
new file mode 100644
index 0000000..3bd6135
--- /dev/null
+++ b/reports/generate_config.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+"""
+Generate OBJECTIVES config from GitHub issues with pi-*-objective labels.
+
+Data sources:
+- Objectives: Issues with `pi-X.Y-objective` labels
+- Contributors: Issue assignees
+- Repos: Labels matching `repo:org/repo-name` pattern
+
+Usage:
+ uv run generate_config.py
+"""
+
+import os
+import re
+from github import Github, Auth
+
+
+def get_objective_issues(g: Github, repo_name: str = "NASA-IMPACT/veda-odd"):
+ """Fetch all issues with pi-*-objective labels using search API."""
+ objectives_by_pi = {}
+
+ # Use search API - much faster than iterating all issues
+ # Search for issues with any pi-*-objective label
+ query = f"repo:{repo_name} is:issue label:pi-25.2-objective,pi-25.3-objective,pi-25.4-objective,pi-26.1-objective,pi-26.2-objective,pi-26.3-objective,pi-26.4-objective"
+ issues = g.search_issues(query)
+
+ for issue in issues:
+ pi = None
+ repos = []
+
+ for label in issue.labels:
+ # Check for PI objective label
+ match = re.match(r"pi-(\d+\.\d+)-objective", label.name)
+ if match:
+ pi = f"pi-{match.group(1)}"
+
+ # Check for repo label (format: repo:org/repo-name)
+ if label.name.startswith("repo:"):
+ repo_str = label.name[5:] # Remove "repo:" prefix
+ if "/" in repo_str:
+ org, repo_name_part = repo_str.split("/", 1)
+ repos.append((org, repo_name_part))
+
+ if pi:
+ if pi not in objectives_by_pi:
+ objectives_by_pi[pi] = []
+
+ # Get assignees
+ contributors = [
+ (assignee.name or assignee.login, assignee.login)
+ for assignee in issue.assignees
+ ]
+
+ objectives_by_pi[pi].append(
+ {
+ "issue_number": issue.number,
+ "title": issue.title,
+ "contributors": contributors,
+ "state": issue.state,
+ "repos": repos,
+ }
+ )
+
+ return objectives_by_pi
+
+
+def generate_config(objectives_by_pi: dict) -> str:
+ """Generate Python config code from objectives data."""
+ lines = [
+ "from datetime import date",
+ "",
+ "# Manually maintained PI date ranges",
+ "# Update these when new PIs are planned",
+ "PI_DATES = {",
+ ' "pi-25.2": ("20250119", "20250418"),',
+ ' "pi-25.3": ("20250419", "20250718"),',
+ ' "pi-25.4": ("20250719", "20251018"),',
+ ' "pi-26.1": ("20251019", "20260117"),',
+ ' "pi-26.2": ("20260118", "20260425"),',
+ "}",
+ "",
+ "",
+ "def get_current_pi():",
+ ' """Find the current PI based on today\'s date."""',
+ ' today = date.today().strftime("%Y%m%d")',
+ " for pi_name, (start, end) in PI_DATES.items():",
+ " if start <= today <= end:",
+ " return pi_name",
+ " return None",
+ "",
+ "",
+ "def get_time_range(pi: str = None):",
+ ' """Get date range for a PI, or current PI if not specified."""',
+ " if pi:",
+ " return PI_DATES.get(pi)",
+ " current = get_current_pi()",
+ " if current:",
+ " return PI_DATES[current]",
+ " # Fallback to most recent PI if not in any range",
+ " return list(PI_DATES.values())[-1]",
+ "",
+ "",
+ "TIME_RANGE = get_time_range()",
+ "",
+ "# Quarterly objectives with repos and contributors per objective",
+ "# Run `uv run generate_config.py` to regenerate from GitHub issues",
+ "# - Objectives: Issues with pi-X.Y-objective labels",
+ "# - Contributors: Issue assignees",
+ "# - Repos: Labels matching repo:org/repo-name",
+ "OBJECTIVES = {",
+ ]
+
+ # Sort PIs chronologically
+ sorted_pis = sorted(objectives_by_pi.keys(), key=lambda x: float(x.split("-")[1]))
+
+ for pi in sorted_pis:
+ objectives = objectives_by_pi[pi]
+ lines.append(f' "{pi}": [')
+
+ # Sort objectives by issue number
+ for obj in sorted(objectives, key=lambda x: x["issue_number"]):
+ lines.append(" {")
+ lines.append(f' "issue_number": {obj["issue_number"]},')
+ title = obj["title"].replace('"', '\\"')
+ lines.append(f' "title": "{title}",')
+ lines.append(f' "state": "{obj["state"]}",')
+ lines.append(' "contributors": [')
+ for name, username in obj["contributors"]:
+ name = (name or username).replace('"', '\\"')
+ lines.append(f' ("{name}", "{username}"),')
+ lines.append(" ],")
+ lines.append(' "repos": [')
+ for org, repo in obj.get("repos", []):
+ lines.append(f' ("{org}", "{repo}"),')
+ lines.append(" ],")
+ lines.append(" },")
+
+ lines.append(" ],")
+
+ lines.append("}")
+ lines.append("")
+ lines.append("")
+ lines.append("def get_all_repos():")
+ lines.append(' """Derive unique repos from all objectives."""')
+ lines.append(" repos = set()")
+ lines.append(" for pi_objectives in OBJECTIVES.values():")
+ lines.append(" for obj in pi_objectives:")
+ lines.append(' for repo in obj["repos"]:')
+ lines.append(" repos.add(repo)")
+ lines.append(" return sorted(repos)")
+ lines.append("")
+ lines.append("")
+ lines.append("def get_all_contributors():")
+ lines.append(' """Derive unique contributors from all objectives."""')
+ lines.append(" contributors = {}")
+ lines.append(" for pi_objectives in OBJECTIVES.values():")
+ lines.append(" for obj in pi_objectives:")
+ lines.append(' for name, username in obj["contributors"]:')
+ lines.append(" contributors[username] = name")
+ lines.append(
+ " return [(name, username) for username, name in sorted(contributors.items(), key=lambda x: x[1])]"
+ )
+ lines.append("")
+ lines.append("")
+ lines.append("def get_repos_for_pi(pi: str):")
+ lines.append(' """Get all repos for a specific PI."""')
+ lines.append(" repos = set()")
+ lines.append(" for obj in OBJECTIVES.get(pi, []):")
+ lines.append(' for repo in obj["repos"]:')
+ lines.append(" repos.add(repo)")
+ lines.append(" return sorted(repos)")
+ lines.append("")
+ lines.append("")
+ lines.append("def get_contributors_for_pi(pi: str):")
+ lines.append(' """Get all contributors for a specific PI."""')
+ lines.append(" contributors = {}")
+ lines.append(" for obj in OBJECTIVES.get(pi, []):")
+ lines.append(' for name, username in obj["contributors"]:')
+ lines.append(" contributors[username] = name")
+ lines.append(
+ " return [(name, username) for username, name in sorted(contributors.items(), key=lambda x: x[1])]"
+ )
+
+ return "\n".join(lines)
+
+
+def main():
+ token = os.environ.get("GH_ODD_PAT") or os.environ.get("GITHUB_TOKEN")
+ if not token:
+ raise ValueError("Set GH_ODD_PAT or GITHUB_TOKEN environment variable")
+
+ auth = Auth.Token(token)
+ g = Github(auth=auth)
+
+ print("Fetching objective issues from GitHub (using search API)...")
+ objectives_by_pi = get_objective_issues(g)
+
+ g.close()
+
+ print(f"Found {len(objectives_by_pi)} PIs:")
+ for pi, objs in sorted(objectives_by_pi.items()):
+ repos_count = sum(len(o["repos"]) for o in objs)
+ print(f" {pi}: {len(objs)} objectives, {repos_count} repo mappings")
+
+ config_code = generate_config(objectives_by_pi)
+
+ output_file = "config.py"
+ with open(output_file, "w") as f:
+ f.write(config_code)
+ print(f"\nGenerated config written to {output_file}")
+ print("\nTo add repos to an objective, add labels like:")
+ print(" repo:zarr-developers/VirtualiZarr")
+ print(" repo:developmentseed/titiler-cmr")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/reports/generate_docs.py b/reports/generate_docs.py
new file mode 100644
index 0000000..98081c6
--- /dev/null
+++ b/reports/generate_docs.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""
+Generate docs/objectives.md from config.py OBJECTIVES.
+
+Usage:
+ uv run generate_docs.py
+"""
+
+from config import OBJECTIVES
+
+
+def generate_objectives_md() -> str:
+ """Generate markdown content for objectives page."""
+ lines = [
+ "# Quarterly Objectives",
+ "",
+ "This page tracks quarterly objectives and their related repositories across Program Increments (PIs).",
+ "",
+ ]
+
+ # Sort PIs reverse chronologically (newest first)
+ sorted_pis = sorted(
+ OBJECTIVES.keys(), key=lambda x: float(x.split("-")[1]), reverse=True
+ )
+
+ for i, pi in enumerate(sorted_pis):
+ objectives = OBJECTIVES[pi]
+ pi_upper = pi.upper().replace("-", " ")
+
+ if i == 0:
+ # Current PI - show full details
+ lines.append(f"## Current PI: {pi.split('-')[1]}")
+ lines.append("")
+ lines.append("| # | Objective | Contributors | Repos |")
+ lines.append("|---|-----------|--------------|-------|")
+
+ for obj in sorted(objectives, key=lambda x: x["issue_number"]):
+ num = obj["issue_number"]
+ # Clean up title (remove PI prefix if present)
+ title = obj["title"]
+ if "Objective" in title and ":" in title:
+ title = title.split(":", 1)[1].strip()
+ title = title[:60] + "..." if len(title) > 60 else title
+
+ contributors = ", ".join(u for _, u in obj["contributors"])
+ repos = ", ".join(r for _, r in obj["repos"]) if obj["repos"] else "-"
+
+ lines.append(
+ f"| [#{num}](https://github.com/NASA-IMPACT/veda-odd/issues/{num}) | {title} | {contributors} | {repos} |"
+ )
+
+ lines.append("")
+ lines.append("---")
+ lines.append("")
+ else:
+ # Historical PIs - collapsible
+ closed_count = sum(1 for o in objectives if o["state"] == "closed")
+
+ lines.append("")
+ lines.append(
+ f"{pi_upper} ({len(objectives)} objectives, {closed_count} closed)
"
+ )
+ lines.append("")
+ lines.append("| # | Objective | State | Contributors |")
+ lines.append("|---|-----------|-------|--------------|")
+
+ for obj in sorted(objectives, key=lambda x: x["issue_number"]):
+ num = obj["issue_number"]
+ title = obj["title"]
+ if "Objective" in title and ":" in title:
+ title = title.split(":", 1)[1].strip()
+ title = title[:50] + "..." if len(title) > 50 else title
+
+ state = obj["state"]
+ contributors = ", ".join(u for _, u in obj["contributors"])
+
+ lines.append(
+ f"| [#{num}](https://github.com/NASA-IMPACT/veda-odd/issues/{num}) | {title} | {state} | {contributors} |"
+ )
+
+ lines.append("")
+ lines.append(" ")
+ lines.append("")
+
+ lines.append("---")
+ lines.append("")
+ lines.append("## Visualization")
+ lines.append("")
+ lines.append(
+ "The commits per repository chart uses color-coding to show which objective each repo contributes to. Repos that contribute to multiple objectives are shown with split bars."
+ )
+ lines.append("")
+ # Add image for the current PI
+ current_pi = sorted_pis[0]
+ lines.append(
+ f""
+ )
+ lines.append("")
+ lines.append("---")
+ lines.append("")
+ lines.append("## Configuration")
+ lines.append("")
+ lines.append(
+ "Objectives are configured in [`reports/config.py`](https://github.com/NASA-IMPACT/veda-odd/blob/main/reports/config.py)."
+ )
+ lines.append("")
+ lines.append("To regenerate this page from config:")
+ lines.append("")
+ lines.append("```bash")
+ lines.append("cd reports")
+ lines.append("uv run generate_docs.py")
+ lines.append("```")
+ lines.append("")
+ lines.append(
+ "See [FY26 Roadmap](./fy26-roadmap.md) for the broader context of these objectives."
+ )
+
+ return "\n".join(lines)
+
+
+def main():
+ content = generate_objectives_md()
+
+ output_file = "../docs/objectives.md"
+ with open(output_file, "w") as f:
+ f.write(content)
+
+ print(f"Generated {output_file}")
+
+ # Print summary
+ total_objectives = sum(len(objs) for objs in OBJECTIVES.values())
+ print(f" {len(OBJECTIVES)} PIs, {total_objectives} total objectives")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/reports/main.py b/reports/main.py
index 2fd033d..58dccd9 100644
--- a/reports/main.py
+++ b/reports/main.py
@@ -1,136 +1,157 @@
#!/usr/bin/env python3
"""
-Query GitHub API for commits to a given repository
+Query GitHub API for commits to repositories in parallel.
"""
from github import Github, Auth
from datetime import datetime
from typing import List
+from concurrent.futures import ThreadPoolExecutor, as_completed
import os
import pandas as pd
-from config import USERS, REPOS, TIME_RANGE
-
-
-def get_commits_for_author(
- repository,
+from config import (
+ get_time_range,
+ get_current_pi,
+ get_repos_for_pi,
+ get_contributors_for_pi,
+)
+
+
+def get_commits_for_repo_author(
+ g: Github,
+ owner: str,
+ repo: str,
author: str,
start_date: datetime,
end_date: datetime,
-) -> List:
+) -> List[dict]:
"""
- Query GitHub API for commits by a specific author within a date range
+ Query GitHub API for commits by a specific author in a repo.
- Args:
- repository: GitHub repository object (already connected)
- author: GitHub username/email to filter commits by
- start_date: Start date for commit search (inclusive)
- end_date: End date for commit search (inclusive)
+ Returns list of commit detail dicts (not commit objects) to avoid
+ thread safety issues with PyGithub objects.
+ """
+ try:
+ repository = g.get_repo(f"{owner}/{repo}")
+ commits = repository.get_commits(
+ author=author, since=start_date, until=end_date
+ )
- Returns:
- List of commit objects
+ # Group commits by PR
+ prs = []
+ pr_commits = []
+ standalone_commits = []
+
+ for commit in commits:
+ pulls = commit.get_pulls()
+ if pulls.totalCount == 1:
+ if (number := pulls[0].number) not in prs:
+ pr_commits.append(commit)
+ prs.append(number)
+ elif pulls.totalCount == 0:
+ standalone_commits.append(commit)
+
+ # Extract details immediately (avoid returning PyGithub objects)
+ results = []
+ for commit in pr_commits + standalone_commits:
+ results.append(
+ {
+ "sha": commit.sha,
+ "message": commit.commit.message.split("\n")[0],
+ "author": commit.commit.author.name,
+ "committer": commit.commit.committer.name,
+ "url": commit.html_url,
+ "total_changes": commit.stats.total if commit.stats else 0,
+ "organization": owner,
+ "repository": repo,
+ }
+ )
+ return results
+ except Exception as e:
+ print(f" Error processing {owner}/{repo} for {author}: {e}")
+ return []
+
+
+def main(token: str = None, pi: str = None, max_workers: int = 10):
"""
- # Get commits with filters using the existing repository connection
- commits = repository.get_commits(author=author, since=start_date, until=end_date)
- # Group commits by PR
- prs = []
- pr_commits = []
- standalone_commits = []
-
- for commit in commits:
- # Get PRs associated with this commit
- pulls = commit.get_pulls()
-
- if pulls.totalCount == 1:
- # Commit is part of one or more PRs
- if (number := pulls[0].number) not in prs:
- pr_commits.append(commit)
- prs.append(number)
- elif pulls.totalCount == 0:
- # Commit is not part of any PR (direct to branch)
- standalone_commits.append(commit)
- else:
- raise ValueError(f"Unexpected pulls.totalCount: {pulls.totalCount}")
- # Convert PaginatedList to regular list
- commit_list = pr_commits + standalone_commits
- return commit_list
-
-
-def get_commit_details(commit) -> dict:
- """Extract detailed commit information"""
- return {
- "sha": commit.sha,
- "message": commit.commit.message.split("\n")[0],
- "author": commit.commit.author.name,
- "committer": commit.commit.committer.name,
- "url": commit.html_url,
- "total_changes": commit.stats.total if commit.stats else 0,
- }
-
-
-def main(token: str = None):
- time_start = datetime.strptime(TIME_RANGE[0], "%Y%m%d")
- time_end = datetime.strptime(TIME_RANGE[1], "%Y%m%d")
- all_commits = []
+ Query GitHub for commits using parallel requests.
+
+ Args:
+ token: GitHub personal access token
+ pi: Optional PI to filter repos/contributors (e.g., "pi-26.1").
+ If None, uses current PI based on today's date.
+ max_workers: Number of parallel threads (default 10)
+ """
+ # Default to current PI if not specified
+ if pi is None:
+ pi = get_current_pi()
+
+ time_range = get_time_range(pi)
+ if not time_range:
+ raise ValueError(f"No date range found for PI: {pi}")
+
+ time_start = datetime.strptime(time_range[0], "%Y%m%d")
+ time_end = datetime.strptime(time_range[1], "%Y%m%d")
+
+ # Get repos and contributors for the PI
+ repos = get_repos_for_pi(pi)
+ contributors = get_contributors_for_pi(pi)
+ print(
+ f"PI: {pi} ({time_start.strftime('%Y-%m-%d')} to {time_end.strftime('%Y-%m-%d')})"
+ )
+ print(f" {len(repos)} repos, {len(contributors)} contributors")
- # Initialize GitHub client once
- if token:
- auth = Auth.Token(token)
- g = Github(auth=auth)
- else:
- g = Github() # Unauthenticated (lower rate limits)
+ if len(contributors) < 1:
+ raise ValueError("No contributors found in config.")
- if len(USERS) < 1:
- raise ValueError(
- "No users were included in the config. See README for instructions on populating the USER list."
- )
+ # Build list of (repo, contributor) tasks
+ tasks = []
+ for owner, repo in repos:
+ for name, username in contributors:
+ tasks.append((owner, repo, username))
- # Iterate through repositories first
- for owner, repo in REPOS:
- print(f"Processing repository: {owner}/{repo}")
+ print(
+ f"Querying {len(tasks)} repo×contributor combinations with {max_workers} workers..."
+ )
- # Get repository object once per repository
- repository = g.get_repo(f"{owner}/{repo}")
+ all_commits = []
- # Iterate through all users and their emails for this repository
- for name, username, start_date_str, end_date_str in USERS:
- # Parse dates for this user
- start_date = (
- datetime.strptime(start_date_str, "%Y%m%d")
- if start_date_str
- else time_start
- )
- end_date = (
- datetime.strptime(end_date_str, "%Y%m%d") if end_date_str else time_end
+ # Use thread pool for parallel API calls
+ # Each thread gets its own Github client to avoid rate limit issues
+ def process_task(task):
+ owner, repo, username = task
+ if token:
+ auth = Auth.Token(token)
+ g = Github(auth=auth)
+ else:
+ g = Github()
+ try:
+ return get_commits_for_repo_author(
+ g, owner, repo, username, time_start, time_end
)
+ finally:
+ g.close()
- print(f" Processing user: {username}")
- commits = get_commits_for_author(
- repository=repository,
- author=username,
- start_date=start_date,
- end_date=end_date,
- )
- for commit in commits:
- commit_details = get_commit_details(commit)
- commit_details.update(
- {
- "organization": owner,
- "repository": repo,
- }
- )
- all_commits.append(commit_details)
-
- g.close()
+ completed = 0
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
+ futures = {executor.submit(process_task, task): task for task in tasks}
+ for future in as_completed(futures):
+ completed += 1
+ if completed % 50 == 0:
+ print(f" Progress: {completed}/{len(tasks)}")
+ commits = future.result()
+ all_commits.extend(commits)
+
+ print(f"Found {len(all_commits)} commits")
df = pd.DataFrame(all_commits)
- csv_filename = (
- f"output/{time_start.strftime('%Y-%m-%d')}-{time_end.strftime('%Y-%m-%d')}.csv"
- )
+ csv_filename = f"output/{pi}.csv"
df.to_csv(csv_filename, index=False)
+ print(f"Saved to {csv_filename}")
return df
if __name__ == "__main__":
- token = os.environ["GH_ODD_PAT"]
+ token = os.environ.get("GH_ODD_PAT") or os.environ.get("GITHUB_TOKEN")
main(token=token)
diff --git a/reports/output/pi-26.1.csv b/reports/output/pi-26.1.csv
new file mode 100644
index 0000000..18f84fd
--- /dev/null
+++ b/reports/output/pi-26.1.csv
@@ -0,0 +1,120 @@
+sha,message,author,committer,url,total_changes,organization,repository
+2b155d610ec62b610a4edb66061cb03543ad126a,Fix reproducibility issues (#17),Max Jones,GitHub,https://github.com/developmentseed/datacube-guide/commit/2b155d610ec62b610a4edb66061cb03543ad126a,3712,developmentseed,datacube-guide
+3397ad783bd6789f638177d1406e8e30e28ab7c8,Remove titiler cmr benchmarking (#16),Aimee Barciauskas,GitHub,https://github.com/developmentseed/datacube-guide/commit/3397ad783bd6789f638177d1406e8e30e28ab7c8,12547,developmentseed,datacube-guide
+ef2381ebc5646ef27091c4f49d4d85138789f535,Remove broken symlink when building windows wheels (#120),Max Jones,GitHub,https://github.com/developmentseed/async-tiff/commit/ef2381ebc5646ef27091c4f49d4d85138789f535,65,developmentseed,async-tiff
+b7f0e6137d4df2f63c215949b3688bb3464b64a5,Latitude and Longitude should be an option for dim names (#1268),Aimee Barciauskas,GitHub,https://github.com/developmentseed/titiler/commit/b7f0e6137d4df2f63c215949b3688bb3464b64a5,4,developmentseed,titiler
+1e4bdd65e02b3e36261a10d0c5a4f96b18e76ac5,fix: add opener_options arg to titiler.xarray.io.Reader (#1248),Henry Rodman,GitHub,https://github.com/developmentseed/titiler/commit/1e4bdd65e02b3e36261a10d0c5a4f96b18e76ac5,70,developmentseed,titiler
+899f104ddd40f3e8a1a36da487d3825e04da68bd,Bump minimum supported python version to 3.11 (#1254),Max Jones,GitHub,https://github.com/developmentseed/titiler/commit/899f104ddd40f3e8a1a36da487d3825e04da68bd,1073,developmentseed,titiler
+e6389e6e9636e262b41b0e2b0d6db0fdd5e81e7e,Add a upstream workflow with a dispatch trigger; follow SPEC0 (#1245),Max Jones,GitHub,https://github.com/developmentseed/titiler/commit/e6389e6e9636e262b41b0e2b0d6db0fdd5e81e7e,46,developmentseed,titiler
+24350b3d8baa47b41e95db44c7a842742f5ded3c,Remove deprecated FastAPI on_event decorator. (#107),Chuck Daniels,GitHub,https://github.com/developmentseed/titiler-cmr/commit/24350b3d8baa47b41e95db44c7a842742f5ded3c,43,developmentseed,titiler-cmr
+66a9a806e5ac85bbc419ba6e90c6fbc0009efce3,Avoid recording EDL token during testing. (#105),Chuck Daniels,GitHub,https://github.com/developmentseed/titiler-cmr/commit/66a9a806e5ac85bbc419ba6e90c6fbc0009efce3,258,developmentseed,titiler-cmr
+b9214801fd760a2a3af22bd26a423300f8577d74,chore: update changelog (#97),Henry Rodman,GitHub,https://github.com/developmentseed/titiler-cmr/commit/b9214801fd760a2a3af22bd26a423300f8577d74,13,developmentseed,titiler-cmr
+e0e87b8cf3e205be282992402f10147fac68ac8c,feat: add variable stats to /compatibility output for xarray datasets (#82),Henry Rodman,GitHub,https://github.com/developmentseed/titiler-cmr/commit/e0e87b8cf3e205be282992402f10147fac68ac8c,396,developmentseed,titiler-cmr
+b763a3f428a0161c635ad6005a6c54dcb58a0a7f,"feat: convert to container function, add OpenTelemetry + X-Ray tracing (#81)",Henry Rodman,GitHub,https://github.com/developmentseed/titiler-cmr/commit/b763a3f428a0161c635ad6005a6c54dcb58a0a7f,746,developmentseed,titiler-cmr
+c2ed5fea20cb05f1909e9e4c8f28242c34a6bae1,chore(docs): fix mkdocs action,hrodmn,hrodmn,https://github.com/developmentseed/titiler-cmr/commit/c2ed5fea20cb05f1909e9e4c8f28242c34a6bae1,39,developmentseed,titiler-cmr
+aec2ed560e7253449f5f09eb7025f01403218e69,feat: Allow S3 HTTP URLs without region (#590),Kyle Barron,GitHub,https://github.com/developmentseed/obstore/commit/aec2ed560e7253449f5f09eb7025f01403218e69,4,developmentseed,obstore
+5819904cb4f2ccffd4ccd4718adc69162f70a5e2,chore: Bump pyo3-object_store to 0.7 (#586),Kyle Barron,GitHub,https://github.com/developmentseed/obstore/commit/5819904cb4f2ccffd4ccd4718adc69162f70a5e2,9,developmentseed,obstore
+f1970d9508254f7cf9677e232ddd86fe6a6653cd,chore: Bump pyo3 to 0.27 (#584),Kyle Barron,GitHub,https://github.com/developmentseed/obstore/commit/f1970d9508254f7cf9677e232ddd86fe6a6653cd,680,developmentseed,obstore
+956add147d24eb15ea934d50c68b47d64442770c,chore: Bump pyo3-bytes to pyo3 0.27 (#583),Kyle Barron,GitHub,https://github.com/developmentseed/obstore/commit/956add147d24eb15ea934d50c68b47d64442770c,21,developmentseed,obstore
+69031c222f8817045914ef01fdc60d1a05c34d15,Add view of algorithms overview diagram (#30),Max Jones,GitHub,https://github.com/developmentseed/warp-resample-profiling/commit/69031c222f8817045914ef01fdc60d1a05c34d15,13,developmentseed,warp-resample-profiling
+682d2bfb4b9ef9d1254011025000b8d747c0c440,Icechunk update for S3 requester pays support. (#27),Sean Harkins,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/682d2bfb4b9ef9d1254011025000b8d747c0c440,1542,developmentseed,zarr-datafusion-search
+126e60e52fe90cf192d729e490b2d0fc43227c91,Include missing construct scope parameters for event target.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/126e60e52fe90cf192d729e490b2d0fc43227c91,2,developmentseed,virtualizarr-data-pipelines
+83d3b5f4e07e97d9590862ff9be7a59aa644fc4b,README updates.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/83d3b5f4e07e97d9590862ff9be7a59aa644fc4b,12,developmentseed,virtualizarr-data-pipelines
+0931b183b0ed151cc4707130664d6f93ddbbbb0b,Make max_concurrency user configurable.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/0931b183b0ed151cc4707130664d6f93ddbbbb0b,4,developmentseed,virtualizarr-data-pipelines
+c4e05ff790593f57b2edad686caad0b119bf18c9,Add AWS Batch infrastructure for managing Icechunk garbage collection.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/c4e05ff790593f57b2edad686caad0b119bf18c9,550,developmentseed,virtualizarr-data-pipelines
+6bd5a3b4ab2e51fe191d1c209c75bbb384c271a4,Fix process file typo.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/6bd5a3b4ab2e51fe191d1c209c75bbb384c271a4,2,developmentseed,virtualizarr-data-pipelines
+a866a2162db2581ee4d3965eaba1ef4be1aa1665,Change VirtualizarrProcessor protocol from append to process_file.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/a866a2162db2581ee4d3965eaba1ef4be1aa1665,44,developmentseed,virtualizarr-data-pipelines
+688e880d5b90e72b517a677a3d861b8701d808ce,README updates.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/688e880d5b90e72b517a677a3d861b8701d808ce,4,developmentseed,virtualizarr-data-pipelines
+90313ab88c53e88d088ecbb027d4480ba82b168e,README updates.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/90313ab88c53e88d088ecbb027d4480ba82b168e,22,developmentseed,virtualizarr-data-pipelines
+30e865581777f1e7179af5e129e5f49b4e291605,Initial README.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/30e865581777f1e7179af5e129e5f49b4e291605,4914,developmentseed,virtualizarr-data-pipelines
+0cd1feedc9d3c4e03b4421d81fa3fa9c2faae124,Add initial cdk infrastructure and update lambda Dockerfiles.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/0cd1feedc9d3c4e03b4421d81fa3fa9c2faae124,986,developmentseed,virtualizarr-data-pipelines
+1ff163e2271f58c4f1bca18519480c31bf1626fe,Add example garbage collection implementation.,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/1ff163e2271f58c4f1bca18519480c31bf1626fe,43,developmentseed,virtualizarr-data-pipelines
+8ac6a1132fabddc0affbffd829883a6229bb1231,First,sharkinsspatial,sharkinsspatial,https://github.com/developmentseed/virtualizarr-data-pipelines/commit/8ac6a1132fabddc0affbffd829883a6229bb1231,1649,developmentseed,virtualizarr-data-pipelines
+b987f25719722351c92ef43423b796995f5d6172,chore: Prepare 0.7.0 release (#1418),Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/b987f25719722351c92ef43423b796995f5d6172,947,geoarrow,geoarrow-rs
+e17e9e48a6e1b357d5c71839dd33c7cb72d853c0,chore: Clippy updates (#1415),Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/e17e9e48a6e1b357d5c71839dd33c7cb72d853c0,7,geoarrow,geoarrow-rs
+e98313a6cfe6549cec9f8cbcd0092a59ab40436e,"chore(js): Clean up JS bindings, deleting most unused code (#1406)",Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/e98313a6cfe6549cec9f8cbcd0092a59ab40436e,1690,geoarrow,geoarrow-rs
+7abc908aad799542e1cc5ddef129842a72424190,chore(python): prepare 0.6.2 (#1405),Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/7abc908aad799542e1cc5ddef129842a72424190,6,geoarrow,geoarrow-rs
+56e1f96a969e30ff56e0d345397d3990b277a19e,chore(python): Prepare 0.6.1 (#1404),Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/56e1f96a969e30ff56e0d345397d3990b277a19e,8,geoarrow,geoarrow-rs
+aa89078b014ab734df5e399f4ff7af906d262c5c,feat(Python): Update macOS runners in GitHub workflows; build intel wheels (#1403),Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/aa89078b014ab734df5e399f4ff7af906d262c5c,11,geoarrow,geoarrow-rs
+c3d151c9a63f956c5c8068acbf9ef493cb23d245,chore(python): Build wheels for python 3.14 (#1399),Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/c3d151c9a63f956c5c8068acbf9ef493cb23d245,20,geoarrow,geoarrow-rs
+2e7dea71d105d368a9d94c1d5f5b8b1f65ba195f,fix(geoarrow-array): Fix validation of sliced geometry arrays (#1391),Kyle Barron,GitHub,https://github.com/geoarrow/geoarrow-rs/commit/2e7dea71d105d368a9d94c1d5f5b8b1f65ba195f,194,geoarrow,geoarrow-rs
+44d9a1ea84c06eefbfd43b0f1117e53ad7ff9b33,Add EGU session (#116),Max Jones,GitHub,https://github.com/pangeo-data/pangeo.io/commit/44d9a1ea84c06eefbfd43b0f1117e53ad7ff9b33,23,pangeo-data,pangeo.io
+d70f965d93555795c6ac16af8d7426cbf77d449d,Fix link (#115),Max Jones,GitHub,https://github.com/pangeo-data/pangeo.io/commit/d70f965d93555795c6ac16af8d7426cbf77d449d,2,pangeo-data,pangeo.io
+de06ca0b584a687497abde6910c0aaa6360dc3f6,Add link to lightning talk (#114),Max Jones,GitHub,https://github.com/pangeo-data/pangeo.io/commit/de06ca0b584a687497abde6910c0aaa6360dc3f6,2,pangeo-data,pangeo.io
+3f94b03b8a1e384c46dbe9b414310c1ce467817a,Remove unclaimed talk (#113),Max Jones,GitHub,https://github.com/pangeo-data/pangeo.io/commit/3f94b03b8a1e384c46dbe9b414310c1ce467817a,9,pangeo-data,pangeo.io
+64dc75029b0395da6e59737290563ecc4d8b2013,Disable banner (#112),Max Jones,GitHub,https://github.com/pangeo-data/pangeo.io/commit/64dc75029b0395da6e59737290563ecc4d8b2013,2,pangeo-data,pangeo.io
+38d28d1494212125b547ddc6a6b7f0478b004cbb,Add links to discourse for showcases (#111),Max Jones,GitHub,https://github.com/pangeo-data/pangeo.io/commit/38d28d1494212125b547ddc6a6b7f0478b004cbb,8,pangeo-data,pangeo.io
+e73d9817cafb8cdec8592f346423f6f0dec6a507,Support using HTTPStore (#56),Max Jones,GitHub,https://github.com/virtual-zarr/virtual-tiff/commit/e73d9817cafb8cdec8592f346423f6f0dec6a507,22,virtual-zarr,virtual-tiff
+fe517b5fbad0946db05ed75404b7e185fc6e90b2,Make layout of IFDs as Zarr groups configurable (#54),Max Jones,GitHub,https://github.com/virtual-zarr/virtual-tiff/commit/fe517b5fbad0946db05ed75404b7e185fc6e90b2,195,virtual-zarr,virtual-tiff
+9143d3b346e762099ec3ee1731bbd071fb305223,Add PyPI install instructions and example to readme (#50),Max Jones,GitHub,https://github.com/virtual-zarr/virtual-tiff/commit/9143d3b346e762099ec3ee1731bbd071fb305223,189,virtual-zarr,virtual-tiff
+4a2123e2d5e3b221ee3694ea4ca9dfb99da8b151,Add py.typed (#49),Max Jones,GitHub,https://github.com/virtual-zarr/virtual-tiff/commit/4a2123e2d5e3b221ee3694ea4ca9dfb99da8b151,10,virtual-zarr,virtual-tiff
+0113bc491198ff99e0caa6e402878a9d3058d1fb,Separate coordinate transformation information into 'spatial' convention (#16),Max Jones,GitHub,https://github.com/zarr-conventions/geo-proj/commit/0113bc491198ff99e0caa6e402878a9d3058d1fb,379,zarr-conventions,geo-proj
+bb75d72b558aa39f540a96d5e9c5a9265e5d5031,Update according to zarr conventions spec changes (#15),Max Jones,GitHub,https://github.com/zarr-conventions/geo-proj/commit/bb75d72b558aa39f540a96d5e9c5a9265e5d5031,475,zarr-conventions,geo-proj
+29fdac68ccfbadf5e2a01e7fdb26505459cff224,Update schema.json (#12),Max Jones,GitHub,https://github.com/zarr-conventions/geo-proj/commit/29fdac68ccfbadf5e2a01e7fdb26505459cff224,7,zarr-conventions,geo-proj
+1d23cf321f12961ff97421ba399fc7fd6865fc65,Use the spatial convention (#25),Max Jones,GitHub,https://github.com/zarr-conventions/multiscales/commit/1d23cf321f12961ff97421ba399fc7fd6865fc65,114,zarr-conventions,multiscales
+1c20751adea63016ccd1fa07c65f8dc608091d55,Sync with updates to zarr conventions spec (#20),Max Jones,GitHub,https://github.com/zarr-conventions/multiscales/commit/1c20751adea63016ccd1fa07c65f8dc608091d55,377,zarr-conventions,multiscales
+574a3f4d75d9d102bf6dbadac370a8ac11cd19b8,fix: dynamically load correct array types given inferred schema (#26),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/574a3f4d75d9d102bf6dbadac370a8ac11cd19b8,175,developmentseed,zarr-datafusion-search
+c04b2712553a4a28b9aedbc0fbf4191f72b128e8,Support for icechunk (#24),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/c04b2712553a4a28b9aedbc0fbf4191f72b128e8,8844,developmentseed,zarr-datafusion-search
+8990087e25f03f77586952490315ca230e83467c,feat: Add `ZarrTable.from_obstore` to python API to support object-store based sources (#22),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/8990087e25f03f77586952490315ca230e83467c,843,developmentseed,zarr-datafusion-search
+dd5b9439731e377d8d95898f69b6d211cdb5dbfa,feat: Use schema inference code path in Zarr table provider (#21),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/dd5b9439731e377d8d95898f69b6d211cdb5dbfa,496,developmentseed,zarr-datafusion-search
+972cd6f69e869f2bb2331225e419a1304ceae5f2,"feat: Set GeoArrow WKT extension type and WGS84 on Zarr arrays named ""`bbox`"" (#20)",Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/972cd6f69e869f2bb2331225e419a1304ceae5f2,118,developmentseed,zarr-datafusion-search
+b1907b135175785659b40c942b640c7c776cfb77,feat: infer schema from Zarrs Group (#19),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/b1907b135175785659b40c942b640c7c776cfb77,825,developmentseed,zarr-datafusion-search
+e992e13e34800736e5319accadd8939fcc621903,fix: Fix failing python test (#18),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/e992e13e34800736e5319accadd8939fcc621903,10,developmentseed,zarr-datafusion-search
+69d6f61b5d0d797e1b2ad261da362d1241f71263,chore: Rename `zarr-datafusion-internal` to `zarr-datafusion-search` (#17),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/69d6f61b5d0d797e1b2ad261da362d1241f71263,52,developmentseed,zarr-datafusion-search
+5c91159dde2acbdd34eeeb92b653dc8b894d5768,ci: Add Python CI (#8),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/5c91159dde2acbdd34eeeb92b653dc8b894d5768,411,developmentseed,zarr-datafusion-search
+70508eab9068306ad335da62aef2e0ddf6de302b,chore: Bump dependencies (#7),Kyle Barron,GitHub,https://github.com/developmentseed/zarr-datafusion-search/commit/70508eab9068306ad335da62aef2e0ddf6de302b,237,developmentseed,zarr-datafusion-search
+3f1fd795b0580cc0f02c3fd941d362abe3068d80,Add link to maintenance team in the contributing guide (#104),Max Jones,GitHub,https://github.com/zarr-developers/geozarr-spec/commit/3f1fd795b0580cc0f02c3fd941d362abe3068d80,2,zarr-developers,geozarr-spec
+b920d336b6ac68a7aef760f3f7caf0020b836dd9,Add docs site (#7),Max Jones,GitHub,https://github.com/virtual-zarr/obspec-utils/commit/b920d336b6ac68a7aef760f3f7caf0020b836dd9,330,virtual-zarr,obspec-utils
+7380ee5d3ab318178b53e2efa441eccf5ac3c8b7,Support py3.13/py3.14 (#6),Max Jones,GitHub,https://github.com/virtual-zarr/obspec-utils/commit/7380ee5d3ab318178b53e2efa441eccf5ac3c8b7,4,virtual-zarr,obspec-utils
+c73974011963df04d388f1f4a94d3a24773e619c,feature(typing): add py.typed file to package root (#5),Max Jones,GitHub,https://github.com/virtual-zarr/obspec-utils/commit/c73974011963df04d388f1f4a94d3a24773e619c,0,virtual-zarr,obspec-utils
+ced0b63ed34e4fdf6bd74a39f382685b6d0d7ee3,Add ObjectStoreRegistry (#4),Max Jones,GitHub,https://github.com/virtual-zarr/obspec-utils/commit/ced0b63ed34e4fdf6bd74a39f382685b6d0d7ee3,325,virtual-zarr,obspec-utils
+fc8438ebf4c670a090c34315216ce1789bafe3c6,Expose buffer_size kwarg in open_reader (#3),Max Jones,GitHub,https://github.com/virtual-zarr/obspec-utils/commit/fc8438ebf4c670a090c34315216ce1789bafe3c6,8,virtual-zarr,obspec-utils
+e3b20e1608920bb16d50ec0e49fac6ff9a1c9af1,Add ObstoreMemCacheReader (#2),Max Jones,GitHub,https://github.com/virtual-zarr/obspec-utils/commit/e3b20e1608920bb16d50ec0e49fac6ff9a1c9af1,87,virtual-zarr,obspec-utils
+54bc68540742c624adbf295731441f4d11d350a7,Define reader usable by Xarray (#1),Max Jones,GitHub,https://github.com/virtual-zarr/obspec-utils/commit/54bc68540742c624adbf295731441f4d11d350a7,214,virtual-zarr,obspec-utils
+3d09b61876e5aa88f6d667cd9914677a1e971f61,Merge pull request #98 from developmentseed/feat/titiler-cmr-compatibility-report,Aimee Barciauskas,GitHub,https://github.com/developmentseed/titiler-cmr/commit/3d09b61876e5aa88f6d667cd9914677a1e971f61,4272,developmentseed,titiler-cmr
+8fb1d81a383d1ae0778855336b1f21de6b5a8506,Merge pull request #101 from developmentseed/fix/bidx-indexes-error-message,Aimee Barciauskas,GitHub,https://github.com/developmentseed/titiler-cmr/commit/8fb1d81a383d1ae0778855336b1f21de6b5a8506,8,developmentseed,titiler-cmr
+81225a4e46f086d80dad3a0eabf8c559df9d080a,Merge pull request #93 from developmentseed/ab/dry-backend-functions,Aimee Barciauskas,GitHub,https://github.com/developmentseed/titiler-cmr/commit/81225a4e46f086d80dad3a0eabf8c559df9d080a,135,developmentseed,titiler-cmr
+ef7c2171eff3128b253e31cd4507c76133d07bcf,Merge pull request #89 from developmentseed/ab/fix-bands-error,Aimee Barciauskas,GitHub,https://github.com/developmentseed/titiler-cmr/commit/ef7c2171eff3128b253e31cd4507c76133d07bcf,122,developmentseed,titiler-cmr
+dd8f2d0f9068846c941cc331132db3ddd74df372,ci: Bump macos runner version in wheel build (#160),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/dd8f2d0f9068846c941cc331132db3ddd74df372,4,developmentseed,async-tiff
+dc345abe5a92a97f7e63877d5e7e0f8fc1163309,chore: Prepare python 0.3 release (#159),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/dc345abe5a92a97f7e63877d5e7e0f8fc1163309,308,developmentseed,async-tiff
+5086669ff02f0168b54cedb27f7c0bc9fb8111ea,refactor: Use `pyclass(get_all)` for cleaner code (#158),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/5086669ff02f0168b54cedb27f7c0bc9fb8111ea,97,developmentseed,async-tiff
+5e6858c76609e4e1b17a66892079884aa08afd36,chore: move tiff tag structs to top level (#150),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/5e6858c76609e4e1b17a66892079884aa08afd36,1345,developmentseed,async-tiff
+28d729a6e08d06a2829a5546f51c26aa4461bf37,feat: Include Endianness as property of TIFF struct (#149),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/28d729a6e08d06a2829a5546f51c26aa4461bf37,81,developmentseed,async-tiff
+d803d0f6de8ce01a2320cc3868fae41ba9f3ab39,feat(python): implement Mapping protocol for IFD and GeoKeyDirectory (#148),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/d803d0f6de8ce01a2320cc3868fae41ba9f3ab39,452,developmentseed,async-tiff
+b53ffbda027dd0427957b4eb3c1d386cef375482,feat: Use `async-trait` instead of boxed futures for simpler trait interface (#147),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/b53ffbda027dd0427957b4eb3c1d386cef375482,216,developmentseed,async-tiff
+fa4b52c13ad52826c1e514b671aec059f0ba25e4,feat: Exponential read-ahead cache (#140),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/fa4b52c13ad52826c1e514b671aec059f0ba25e4,499,developmentseed,async-tiff
+32131e6237c9ca1331e521923376b08c58a18087,fix: Skip unknown GeoTag keys (#134),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/32131e6237c9ca1331e521923376b08c58a18087,128,developmentseed,async-tiff
+fac684fcd3cb866b64e0f22588283a593433fe57,"ci: Deprecate Python 3.9, add testing on Python 3.13 (#129)",Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/fac684fcd3cb866b64e0f22588283a593433fe57,1803,developmentseed,async-tiff
+1ad420c13451439dfbda5215a9810e6d4918bad7,ci: Fix tag name to initiate rust release (#130),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/1ad420c13451439dfbda5215a9810e6d4918bad7,4,developmentseed,async-tiff
+3c77377af7909f4740832f503c7e7b8eb576b843,ci: Set up Rust trusted publishing (#128),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/3c77377af7909f4740832f503c7e7b8eb576b843,19,developmentseed,async-tiff
+270e370ea8ee7ba99415468fc4c6d8b64d37425c,ci: Fix wheel build by switching to pypy 3.11 (#127),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/270e370ea8ee7ba99415468fc4c6d8b64d37425c,12,developmentseed,async-tiff
+374b72748650675c4f370e00402b2fd8b4216866,chore: Bump python binding version to 0.2.0 (#124),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/374b72748650675c4f370e00402b2fd8b4216866,25,developmentseed,async-tiff
+9da0d7769bb435f0aa735401786317b5e53433ce,chore: Bump pyo3 to 0.27 (#126),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/9da0d7769bb435f0aa735401786317b5e53433ce,72,developmentseed,async-tiff
+6397a9aca840bf883ca2ecaf60a08251d25b7dee,chore: Bump _obstore submodule (#125),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/6397a9aca840bf883ca2ecaf60a08251d25b7dee,3,developmentseed,async-tiff
+512d86b28128fae0bca5997cea4dff2519862d1e,ci: Build abi3 wheels where possible (#123),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/512d86b28128fae0bca5997cea4dff2519862d1e,63,developmentseed,async-tiff
+b9d802e2d93ea4e20d1953b573478483f5bac041,chore: Bump pyo3 to 0.26 (#121),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/b9d802e2d93ea4e20d1953b573478483f5bac041,984,developmentseed,async-tiff
+e9b23766976306d2767ce72908e046075f6234ad,chore!: Bump minimum Python version to 3.10 (#122),Kyle Barron,GitHub,https://github.com/developmentseed/async-tiff/commit/e9b23766976306d2767ce72908e046075f6234ad,2,developmentseed,async-tiff
+d5d404c8e967449a7b78df23596350a8a74146c3,Add property for denoting raster space/grid registration (#4),Max Jones,GitHub,https://github.com/zarr-conventions/spatial/commit/d5d404c8e967449a7b78df23596350a8a74146c3,146,zarr-conventions,spatial
+32110cb24231b47c37bc30d98e1a84604e02d229,Add linters/formatters (#3),Max Jones,GitHub,https://github.com/zarr-conventions/spatial/commit/32110cb24231b47c37bc30d98e1a84604e02d229,179,zarr-conventions,spatial
+c5e1d29a40f10a580e46a0cecd846dbfe34d1fc1,Fix bold formatting (#2),Max Jones,GitHub,https://github.com/zarr-conventions/spatial/commit/c5e1d29a40f10a580e46a0cecd846dbfe34d1fc1,2,zarr-conventions,spatial
+5ca001b2903ecdde01e8058730e2f3406c8b18d9,Initial design for spatial convention (#1),Max Jones,GitHub,https://github.com/zarr-conventions/spatial/commit/5ca001b2903ecdde01e8058730e2f3406c8b18d9,806,zarr-conventions,spatial
+e35be479a561d6c37b3735fa067f1f2d59357948,Initial commit,Max Jones,GitHub,https://github.com/zarr-conventions/spatial/commit/e35be479a561d6c37b3735fa067f1f2d59357948,639,zarr-conventions,spatial
+acdd892234dbb62833c5d57e59b94d180cd1115d,Add instructions to contributing guide about executable docs (#3645),Max Jones,GitHub,https://github.com/zarr-developers/zarr-python/commit/acdd892234dbb62833c5d57e59b94d180cd1115d,34,zarr-developers,zarr-python
+87d2041b2cd52639d7d64d4711b700e921403a63,Add experimental section to the API docs (#3642),Max Jones,GitHub,https://github.com/zarr-developers/zarr-python/commit/87d2041b2cd52639d7d64d4711b700e921403a63,27,zarr-developers,zarr-python
+8eb244f3957f59210d2fb73a26cf9aeac12a7fa8,Reduce number of documentation redirects needed (#3584),Max Jones,GitHub,https://github.com/zarr-developers/zarr-python/commit/8eb244f3957f59210d2fb73a26cf9aeac12a7fa8,218,zarr-developers,zarr-python
+c8d8e6430b67264eb4457dc337746856a46965cc,Improve compatibility between old sphinx and new mkdocs setup (#3544),Max Jones,GitHub,https://github.com/zarr-developers/zarr-python/commit/c8d8e6430b67264eb4457dc337746856a46965cc,109,zarr-developers,zarr-python
+fc8e8ad1a143b1f39a2e08b470857a5e48a38bca,Fix 404 page (#3543),Max Jones,GitHub,https://github.com/zarr-developers/zarr-python/commit/fc8e8ad1a143b1f39a2e08b470857a5e48a38bca,2,zarr-developers,zarr-python
+c67dcc1b89f69d4ae59c92276a4ead5963c944ac,Remove wrapper for kerchunk's tiff parsing (#849),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/c67dcc1b89f69d4ae59c92276a4ead5963c944ac,68,zarr-developers,virtualizarr
+aa66445137b186028266f7cc14e6d6ec6cbadd98,Fix typing errors (#847),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/aa66445137b186028266f7cc14e6d6ec6cbadd98,15,zarr-developers,virtualizarr
+4529e99851a9e8e5b3af46dd47884272774d8135,Fix broken cross-references (#846),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/4529e99851a9e8e5b3af46dd47884272774d8135,8,zarr-developers,virtualizarr
+0a18a00a4e7947c3a3e1a8354baa85b25bde17fe,Polish release notes for v2.2.1 (#839),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/0a18a00a4e7947c3a3e1a8354baa85b25bde17fe,10,zarr-developers,virtualizarr
+38ef649c531d8097695fe3ed42f8f062c24684f7,Improve ManifestStore.list_dir for arrays and nested groups (#837),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/38ef649c531d8097695fe3ed42f8f062c24684f7,87,zarr-developers,virtualizarr
+c6de7b30dfc8c16c902e72059abbd00289cfb8ba,Allow storing scalar arrays under 'c' key (#836),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/c6de7b30dfc8c16c902e72059abbd00289cfb8ba,192,zarr-developers,virtualizarr
+847dbdef590fd5ea46e55962e54ecdbc0f5446a7,Polish release notes for v2.2.0 (#832),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/847dbdef590fd5ea46e55962e54ecdbc0f5446a7,10,zarr-developers,virtualizarr
+a8491761e8c3d95680fe9c476b0af26e86914075,Bump workflow actions versions (#830),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/a8491761e8c3d95680fe9c476b0af26e86914075,44,zarr-developers,virtualizarr
+2a13ee379990816702c96cb74e8b0ef2e23c4daa,Raise informative error on Zarr V2 parsing with Zarr-Python<3.1.3 (#829),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/2a13ee379990816702c96cb74e8b0ef2e23c4daa,82,zarr-developers,virtualizarr
+61ca10b81f27a8685af974d3baf5e6b0bb06ed2a,"Add back supports_partial_writes property, returning False (#828)",Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/61ca10b81f27a8685af974d3baf5e6b0bb06ed2a,5,zarr-developers,virtualizarr
+565d40fa1553a20bcac18f1ddcade04ec247ed18,Return None for Zarr V2/consolidated metadata requests (#827),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/565d40fa1553a20bcac18f1ddcade04ec247ed18,22,zarr-developers,virtualizarr
+cb2912e650ddea8ed700fe1d45375e407e2ba99c,Add virtual tiff as an optional dependency (#810),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/cb2912e650ddea8ed700fe1d45375e407e2ba99c,49,zarr-developers,virtualizarr
+57c346a86e26fc20284aec6fde2e11047706ce0d,Fix typing in ManifestStore (#820),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/57c346a86e26fc20284aec6fde2e11047706ce0d,25,zarr-developers,virtualizarr
+b01cd0aae4fde995235f1d324992c77087664b33,Add link to ESIP talk (#819),Max Jones,GitHub,https://github.com/zarr-developers/VirtualiZarr/commit/b01cd0aae4fde995235f1d324992c77087664b33,1,zarr-developers,virtualizarr
diff --git a/reports/plot.py b/reports/plot.py
index 6a00a5a..ff9448c 100644
--- a/reports/plot.py
+++ b/reports/plot.py
@@ -1,63 +1,201 @@
+import re
+from pathlib import Path
+
import pandas as pd
import matplotlib.pyplot as plt
-from datetime import datetime
+from matplotlib.patches import Patch
+from matplotlib.ticker import MaxNLocator
-from config import TIME_RANGE
+from config import get_current_pi, OBJECTIVES
-def main():
- time_start = datetime.strptime(TIME_RANGE[0], "%Y%m%d")
- time_end = datetime.strptime(TIME_RANGE[1], "%Y%m%d")
- csv_filename = (
- f"output/{time_start.strftime('%Y-%m-%d')}-{time_end.strftime('%Y-%m-%d')}.csv"
- )
+# Color palette for objectives (cycles if more than 10 objectives)
+COLORS = [
+ "#e74c3c", # red
+ "#3498db", # blue
+ "#2ecc71", # green
+ "#9b59b6", # purple
+ "#f39c12", # orange
+ "#1abc9c", # teal
+ "#e91e63", # pink
+ "#00bcd4", # cyan
+ "#ff5722", # deep orange
+ "#607d8b", # blue grey
+]
+
+
+def get_repo_objectives(pi: str) -> dict:
+ """
+ Build a mapping from repo to list of objectives it belongs to.
+
+ Returns:
+ Dict mapping "org/repo" to list of (issue_number, title) tuples
+ """
+ repo_to_objectives = {}
+ for obj in OBJECTIVES.get(pi, []):
+ for org, repo in obj["repos"]:
+ key = f"{org}/{repo}"
+ if key not in repo_to_objectives:
+ repo_to_objectives[key] = []
+ repo_to_objectives[key].append((obj["issue_number"], obj["title"]))
+ return repo_to_objectives
+
+
+def get_objective_colors(pi: str) -> dict:
+ """Generate color mapping for objectives in a PI."""
+ objectives = OBJECTIVES.get(pi, [])
+ return {
+ obj["issue_number"]: COLORS[i % len(COLORS)] for i, obj in enumerate(objectives)
+ }
+
+
+def get_objective_titles(pi: str) -> dict:
+ """Get short titles for objectives (strip PI prefix and emojis)."""
+ objectives = OBJECTIVES.get(pi, [])
+ titles = {}
+ length = 100
+ for obj in objectives:
+ title = obj["title"]
+ # Strip "ODD PI X.Y Objective N: " prefix if present
+ if ": " in title:
+ title = title.split(": ", 1)[1]
+ # Strip emojis (unicode emoji ranges)
+ title = re.sub(r"[\U0001F300-\U0001F9FF]", "", title).strip()
+ # Truncate if too long
+ if len(title) > length:
+ title = title[: length - 3] + "..."
+ titles[obj["issue_number"]] = title
+ return titles
+
+
+def main(pi: str = None, show_labels: bool = False):
+ # Default to current PI if not specified
+ if pi is None:
+ pi = get_current_pi()
+
+ csv_filename = f"output/{pi}.csv"
df = pd.read_csv(csv_filename)
+ # Build repo to objectives mapping and colors
+ repo_to_objectives = get_repo_objectives(pi)
+ objective_colors = get_objective_colors(pi)
+
+ # Get commits per repo with full path
+ df["full_repo"] = df["organization"] + "/" + df["repository"]
commits_per_repo = df["repository"].value_counts()
+ full_repo_map = df.groupby("repository")["full_repo"].first().to_dict()
fig, ax = plt.subplots(1, 1, figsize=(16, 10))
- ax.barh(
- commits_per_repo.index,
- commits_per_repo.values,
- color=["#9b59b6", "#f39c12", "#1abc9c"],
- alpha=0.8,
- edgecolor="black",
- linewidth=1.2,
- )
- ax.set_xlabel("Number of Commits", fontsize=16)
- ax.tick_params(axis="y", labelsize=15)
+
+ # Plot bars with objective-based coloring
+ for i, (repo, count) in enumerate(commits_per_repo.items()):
+ full_repo = full_repo_map.get(repo, repo)
+ objectives = repo_to_objectives.get(full_repo, [])
+
+ if len(objectives) == 0:
+ # No objective mapping - gray
+ ax.barh(
+ i, count, color="#95a5a6", alpha=0.8, edgecolor="black", linewidth=1.2
+ )
+ elif len(objectives) == 1:
+ # Single objective - solid color
+ color = objective_colors.get(objectives[0][0], "#95a5a6")
+ ax.barh(i, count, color=color, alpha=0.8, edgecolor="black", linewidth=1.2)
+ else:
+ # Multiple objectives - split bar by color
+ width_per_obj = count / len(objectives)
+ current_x = 0
+ for j, (issue_num, _) in enumerate(objectives):
+ color = objective_colors.get(issue_num, "#95a5a6")
+ ax.barh(
+ i,
+ width_per_obj,
+ left=current_x,
+ color=color,
+ alpha=0.8,
+ edgecolor="black",
+ linewidth=1.2,
+ )
+ current_x += width_per_obj
+
+ ax.set_yticks(range(len(commits_per_repo)))
+ ax.set_yticklabels(commits_per_repo.index)
+ ax.set_xlabel("Number of Commits", fontsize=16, loc="left")
+ ax.tick_params(axis="y", labelsize=13)
+ ax.xaxis.set_major_locator(MaxNLocator(integer=True))
ax.grid(axis="x", alpha=0.3)
- for i, v in enumerate(commits_per_repo.values):
- ax.text(
- v + 0.5, i, str(v), ha="left", va="center", fontweight="bold", fontsize=11
- )
+ # Add value labels if requested
+ if show_labels:
+ for i, v in enumerate(commits_per_repo.values):
+ ax.text(
+ v + 0.5,
+ i,
+ str(v),
+ ha="left",
+ va="center",
+ fontweight="bold",
+ fontsize=11,
+ )
+
plt.subplots_adjust(left=0.3)
- # Bold title with date range
- date_range = f"{time_start.strftime('%Y-%m-%d')} to {time_end.strftime('%Y-%m-%d')}"
- fig.text(
- 0.5,
- 0.93,
- f"Commits per Repository ({date_range})",
+ ax.set_title(
+ f"{pi.upper()} ODD's commits to the default branch",
fontsize=24,
fontweight="bold",
- horizontalalignment="center",
- transform=fig.transFigure,
)
- # Regular subtitle
- fig.text(
- 0.5,
- 0.90,
- "Merged pull requests are counted as one commit",
- fontsize=16,
- style="normal",
- horizontalalignment="center",
- transform=fig.transFigure,
+ # Legend for objectives with titles
+ objective_titles = get_objective_titles(pi)
+ legend_elements = [
+ Patch(
+ facecolor=color,
+ edgecolor="black",
+ label=objective_titles.get(num, f"#{num}"),
+ )
+ for num, color in objective_colors.items()
+ ]
+ ax.legend(
+ handles=legend_elements,
+ loc="upper right",
+ fontsize=9,
+ title=f"{pi.upper()} Objectives",
+ title_fontsize=10,
+ )
+
+ # Caveats and link in bottom right of plot area
+ caveats = (
+ "Caveats:\n"
+ "- Only community-governed open source repositories are tracked (not ODSI-specific repos)\n"
+ "- Merged PRs counted as one commit\n"
+ "- Individual changes may span multiple PRs\n"
+ "- Split bars indicate repos in multiple objectives\n"
+ "- Includes all open source work by VEDA/EODC ODD team members, not only ODSI-funded work\n\n"
+ "Objective details: nasa-impact.github.io/veda-odd/objectives"
+ )
+ ax.text(
+ 1.0,
+ -0.06,
+ caveats,
+ fontsize=8,
+ style="italic",
+ horizontalalignment="right",
+ verticalalignment="top",
+ transform=ax.transAxes,
+ bbox=dict(
+ boxstyle="round,pad=0.3", facecolor="white", edgecolor="gray", alpha=0.8
+ ),
)
- plt.savefig(f"output/{TIME_RANGE[0]}-{TIME_RANGE[1]}_commits_per_repo_hist.png")
+ # Save to docs for website
+ docs_images = Path(__file__).parent.parent / "docs" / "images"
+ docs_images.mkdir(exist_ok=True)
+ plt.savefig(
+ docs_images / f"{pi}.png",
+ bbox_inches="tight",
+ dpi=150,
+ )
if __name__ == "__main__":