diff --git a/.github/workflows/update-reports.yml b/.github/workflows/update-reports.yml
index 3246db4..93ea1c0 100644
--- a/.github/workflows/update-reports.yml
+++ b/.github/workflows/update-reports.yml
@@ -4,68 +4,15 @@ 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
+ reports:
+ uses: NASA-IMPACT/dse-oss-reports/.github/workflows/reports.yml@33eb4f01af27504d3a35e32fbbc679435e300b5b # v0.2.0
permissions:
contents: write
pull-requests: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v5
-
- - name: Get current date
- id: date
- run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
-
- - 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 }}
- GH_PAT: ${{ secrets.GH_PAT }}
- run: uv run generate_config.py
-
- - name: Generate commit data
- working-directory: reports
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GH_PAT: ${{ secrets.GH_PAT }}
- 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 (${{ steps.date.outputs.date }})"
- 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
+ with:
+ dse-oss-reports-ref: further-simplification
+ secrets:
+ pat: ${{ secrets.GH_PAT }}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..511fea4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+# science-support
+
+Documentation site for the VEDA/EODC Science Support team. Published at .
+
+## Reporting pipeline
+
+Quarterly objectives, commit charts, and the [Objectives page](https://nasa-impact.github.io/science-support/objectives) are auto-generated by [`dse-oss-reports`](https://github.com/NASA-IMPACT/dse-oss-reports). All team-specific configuration lives in [`team.toml`](./team.toml). Adoption guide and architecture: .
+
+The weekly cron in [`.github/workflows/update-reports.yml`](./.github/workflows/update-reports.yml) calls the library's reusable workflow, which scrapes GitHub issues, fetches commit data, regenerates charts and `docs/objectives.md`, and opens a PR for review.
+
+## Running the pipeline locally
+
+`dse-oss-reports` is pinned in `pyproject.toml`; `uv sync` installs it alongside mkdocs. Then prefix every command with `uv run`.
+
+All commands take `--config team.toml` and run from the repo root.
+
+| Subcommand | What it does | Reads | Writes | PAT? |
+|---|---|---|---|---|
+| `current-pi` | Print the resolved current PI | `team.toml` | stdout | no |
+| `generate-config` | Scrape GitHub issues into the objectives data file | GitHub API | `reports/_objectives_data.json` | yes |
+| `fetch` | Pull authored commits + resolved items for a PI | GitHub API, `_objectives_data.json` | `reports/output/{pi}-*.csv` | yes |
+| `plot` | Render per-PI charts | `reports/output/{pi}-*.csv` | `docs/images/{pi}-*.png` | no |
+| `generate-docs` | Render the objectives page | `_objectives_data.json`, existing PNGs | `docs/objectives.md` | no |
+| `run-all` | Run all four pipeline stages in order | everything | everything | yes |
+
+PAT-requiring commands read `DSE_OSS_REPORTS_TOKEN` → `GH_PAT` → `GITHUB_TOKEN` (in that order). Create a fine-grained PAT with public-repo read access at .
+
+### Common workflows
+
+```bash
+# Full refresh from GitHub (PAT required, ~1-2 minutes)
+export GH_PAT=ghp_...
+uv run dse-oss-reports --config team.toml run-all
+
+# Regenerate figures from existing CSVs (no PAT, ~5 seconds)
+uv run dse-oss-reports --config team.toml plot # current PI
+uv run dse-oss-reports --config team.toml plot --pi pi-26.2 # a specific past PI
+
+# Regenerate the docs page after charts change (no PAT)
+uv run dse-oss-reports --config team.toml generate-docs
+```
+
+Once `dse-oss-reports` is released, bump the pin in `pyproject.toml` and both refs in `.github/workflows/update-reports.yml` from `@further-simplification` to a versioned tag.
+
+## Building the docs site
+
+```bash
+uv sync
+uv run mkdocs serve # preview at http://127.0.0.1:8000
+```
diff --git a/docs/images/pi-26.3-authored-commits.png b/docs/images/pi-26.3-authored-commits.png
new file mode 100644
index 0000000..264e375
Binary files /dev/null and b/docs/images/pi-26.3-authored-commits.png differ
diff --git a/docs/images/pi-26.3-resolved-issues-prs.png b/docs/images/pi-26.3-resolved-issues-prs.png
new file mode 100644
index 0000000..887cf5b
Binary files /dev/null and b/docs/images/pi-26.3-resolved-issues-prs.png differ
diff --git a/docs/objectives.md b/docs/objectives.md
index ed0ea9e..b2929f5 100644
--- a/docs/objectives.md
+++ b/docs/objectives.md
@@ -1,40 +1,52 @@
# Quarterly Objectives
-This page tracks quarterly objectives and their related repositories across Program Increments (PIs).
+This page tracks quarterly objectives for the VEDA/MAAP Science Support team and the open-source repositories they touch across Program Increments (PIs).
-## Current PI: 26.2
+## Current PI: 26.3
+
+
+
+
| # | Objective | Contributors | Repos |
|---|-----------|--------------|-------|
-| [#1](https://github.com/NASA-IMPACT/science-support/issues/1) | Hub Support | wildintellect, jsignell | repo2docker-action, pangeo-docker-images, pangeo-notebook-veda-image |
-| [#2](https://github.com/NASA-IMPACT/science-support/issues/2) | Cloud Optimized Workflows | wildintellect, jsignell | veda-docs, maap-documentation, cloud-optimized-geospatial-formats-guide |
-| [#3](https://github.com/NASA-IMPACT/science-support/issues/3) | Open-Source Contributions | jsignell, ircwaves, tylanderson | stac-best-practices, stac-spec, dask, pystac, pystac-client, xarray |
-| [#9](https://github.com/NASA-IMPACT/science-support/issues/9) | Data Retention Policy | smk0033 | - |
-| [#10](https://github.com/NASA-IMPACT/science-support/issues/10) | VEDA Forum (Stretch) | smk0033 | - |
-| [#11](https://github.com/NASA-IMPACT/science-support/issues/11) | AI Embedding Report (Stretch) | omshinde | - |
-| [#12](https://github.com/NASA-IMPACT/science-support/issues/12) | Merge MAAP Documentation into VEDA (Stretch) | | - |
+| [#31](https://github.com/NASA-IMPACT/science-support/issues/31) | Hub Upgrades | wildintellect, grallewellyn | repo2docker-action, pangeo-docker-images, pangeo-notebook-veda-image |
+| [#32](https://github.com/NASA-IMPACT/science-support/issues/32) | Cloud Optimized Workflows | wildintellect, tylanderson | veda-docs, maap-documentation, cloud-optimized-geospatial-formats-guide |
+| [#33](https://github.com/NASA-IMPACT/science-support/issues/33) | Open-Source Contributions | gadomski, tylanderson | stac-best-practices, stac-spec, dask, pystac, pystac-client, xarray |
+| [#34](https://github.com/NASA-IMPACT/science-support/issues/34) | Data Retention Policy | smk0033 | - |
---
----
+## Past PIs
+
+
+PI 26.2 (7 original objectives; 4 closed as completed; 3 closed as not planned)
-## Visualization
+| # | Objective | State | Contributors |
+|---|-----------|-------|--------------|
+| [#1](https://github.com/NASA-IMPACT/science-support/issues/1) | Hub Support | closed (completed) | wildintellect, jsignell |
+| [#2](https://github.com/NASA-IMPACT/science-support/issues/2) | Cloud Optimized Workflows | closed (completed) | wildintellect, jsignell |
+| [#3](https://github.com/NASA-IMPACT/science-support/issues/3) | Open-Source Contributions | closed (completed) | jsignell, ircwaves, tylanderson |
+| [#9](https://github.com/NASA-IMPACT/science-support/issues/9) | Data Retention Policy | closed (completed) | smk0033 |
+| [#10](https://github.com/NASA-IMPACT/science-support/issues/10) | VEDA Forum (Stretch) | closed (not planned) | smk0033 |
+| [#11](https://github.com/NASA-IMPACT/science-support/issues/11) | AI Embedding Report (Stretch) | closed (not planned) | omshinde |
+| [#12](https://github.com/NASA-IMPACT/science-support/issues/12) | Merge MAAP Documentation into VEDA (Stretch) | closed (not planned) | - |
-The charts use 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/science-support/blob/main/reports/config.py).
+Objectives data lives in [`reports/_objectives_data.py`](https://github.com/NASA-IMPACT/science-support/blob/main/reports/_objectives_data.py) — auto-generated from GitHub issues by `dse_oss_reports.generator.ObjectivesGenerator`.
-To regenerate this page from config:
+To regenerate this page:
```bash
cd reports
uv run generate_docs.py
-```
\ No newline at end of file
+```
diff --git a/pyproject.toml b/pyproject.toml
index f1dba72..a254625 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,10 +4,11 @@ version = "0.1.0"
description = "Science Support for NASA VEDA and EODC"
readme = "README.md"
dependencies = []
-requires-python = ">= 3.11"
+requires-python = ">= 3.12"
[dependency-groups]
dev = [
+ "dse-oss-reports @ git+https://github.com/NASA-IMPACT/dse-oss-reports.git@33eb4f01af27504d3a35e32fbbc679435e300b5b", #v0.2.0
"mike>=2.1.3",
"mkdocs-material[imaging]>=9.6.3",
"mkdocs-jupyter>=0.25.0",
diff --git a/reports/README.md b/reports/README.md
deleted file mode 100644
index 15394de..0000000
--- a/reports/README.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# Generating open source commit statistics for the VEDA ODD team
-
-## Setting up a fine-grained personal access token
-
-1. Navigate to https://github.com/settings/personal-access-tokens/new
-2. Select public repositories
-3. Add new token as the environment variable specified by `TOKEN_ENV_VAR` in `settings.py` (default: `GH_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`.
-
-## Generating data
-
-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 the objectives page on the deployed site for details).
-
-### Regenerating docs/objectives.md
-
-To regenerate the objectives documentation page from config:
-
-```bash
-uv run generate_docs.py
-```
-
-## 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/_objectives_data.json b/reports/_objectives_data.json
new file mode 100644
index 0000000..8007af0
--- /dev/null
+++ b/reports/_objectives_data.json
@@ -0,0 +1,274 @@
+{
+ "pi-26.2": [
+ {
+ "issue_number": 1,
+ "title": "[Sc] PI 26.2 Objective 1: Hub Support",
+ "state": "closed",
+ "state_reason": "completed",
+ "contributors": [
+ [
+ "Alex I. Mandel",
+ "wildintellect"
+ ],
+ [
+ "Julia Signell",
+ "jsignell"
+ ]
+ ],
+ "repos": [
+ [
+ "jupyterhub",
+ "repo2docker-action"
+ ],
+ [
+ "pangeo-data",
+ "pangeo-docker-images"
+ ],
+ [
+ "NASA-IMPACT",
+ "pangeo-notebook-veda-image"
+ ]
+ ]
+ },
+ {
+ "issue_number": 2,
+ "title": "[Sc] PI 26.2 Objective 2: Cloud Optimized Workflows",
+ "state": "closed",
+ "state_reason": "completed",
+ "contributors": [
+ [
+ "Alex I. Mandel",
+ "wildintellect"
+ ],
+ [
+ "Julia Signell",
+ "jsignell"
+ ]
+ ],
+ "repos": [
+ [
+ "NASA-IMPACT",
+ "veda-docs"
+ ],
+ [
+ "MAAP-Project",
+ "maap-documentation"
+ ],
+ [
+ "cloudnativegeo",
+ "cloud-optimized-geospatial-formats-guide"
+ ]
+ ]
+ },
+ {
+ "issue_number": 3,
+ "title": "[Sc] PI 26.2 Objective 3: Open-Source Contributions",
+ "state": "closed",
+ "state_reason": "completed",
+ "contributors": [
+ [
+ "Julia Signell",
+ "jsignell"
+ ],
+ [
+ "Ian Cooke",
+ "ircwaves"
+ ],
+ [
+ "Tyler",
+ "tylanderson"
+ ]
+ ],
+ "repos": [
+ [
+ "radiantearth",
+ "stac-best-practices"
+ ],
+ [
+ "radiantearth",
+ "stac-spec"
+ ],
+ [
+ "dask",
+ "dask"
+ ],
+ [
+ "stac-utils",
+ "pystac"
+ ],
+ [
+ "stac-utils",
+ "pystac-client"
+ ],
+ [
+ "pydata",
+ "xarray"
+ ]
+ ]
+ },
+ {
+ "issue_number": 9,
+ "title": "[Sc] PI 26.2 Objective 4: Data Retention Policy",
+ "state": "closed",
+ "state_reason": "completed",
+ "contributors": [
+ [
+ "Sheyenne Kirkland",
+ "smk0033"
+ ]
+ ],
+ "repos": []
+ },
+ {
+ "issue_number": 10,
+ "title": "[Sc] PI 26.2 Objective 5: VEDA Forum (Stretch)",
+ "state": "closed",
+ "state_reason": "not_planned",
+ "contributors": [
+ [
+ "Sheyenne Kirkland",
+ "smk0033"
+ ]
+ ],
+ "repos": []
+ },
+ {
+ "issue_number": 11,
+ "title": "[Sc] PI 26.2 Objective 6: AI Embedding Report (Stretch)",
+ "state": "closed",
+ "state_reason": "not_planned",
+ "contributors": [
+ [
+ "Rajat Shinde",
+ "omshinde"
+ ]
+ ],
+ "repos": []
+ },
+ {
+ "issue_number": 12,
+ "title": "[Sc] PI 26.2 Objective 7: Merge MAAP Documentation into VEDA (Stretch)",
+ "state": "closed",
+ "state_reason": "not_planned",
+ "contributors": [],
+ "repos": []
+ }
+ ],
+ "pi-26.3": [
+ {
+ "issue_number": 31,
+ "title": "[Sc] PI 26.3 Objective 1: Hub Upgrades",
+ "state": "open",
+ "state_reason": null,
+ "contributors": [
+ [
+ "Alex I. Mandel",
+ "wildintellect"
+ ],
+ [
+ "Grace Llewellyn",
+ "grallewellyn"
+ ]
+ ],
+ "repos": [
+ [
+ "jupyterhub",
+ "repo2docker-action"
+ ],
+ [
+ "pangeo-data",
+ "pangeo-docker-images"
+ ],
+ [
+ "NASA-IMPACT",
+ "pangeo-notebook-veda-image"
+ ]
+ ]
+ },
+ {
+ "issue_number": 32,
+ "title": "[Sc] PI 26.3 Objective 2: Cloud Optimized Workflows",
+ "state": "open",
+ "state_reason": "reopened",
+ "contributors": [
+ [
+ "Alex I. Mandel",
+ "wildintellect"
+ ],
+ [
+ "Tyler",
+ "tylanderson"
+ ]
+ ],
+ "repos": [
+ [
+ "NASA-IMPACT",
+ "veda-docs"
+ ],
+ [
+ "MAAP-Project",
+ "maap-documentation"
+ ],
+ [
+ "cloudnativegeo",
+ "cloud-optimized-geospatial-formats-guide"
+ ]
+ ]
+ },
+ {
+ "issue_number": 33,
+ "title": "[Sc] PI 26.3 Objective 3: Open-Source Contributions",
+ "state": "open",
+ "state_reason": null,
+ "contributors": [
+ [
+ "Pete Gadomski",
+ "gadomski"
+ ],
+ [
+ "Tyler",
+ "tylanderson"
+ ]
+ ],
+ "repos": [
+ [
+ "radiantearth",
+ "stac-best-practices"
+ ],
+ [
+ "radiantearth",
+ "stac-spec"
+ ],
+ [
+ "dask",
+ "dask"
+ ],
+ [
+ "stac-utils",
+ "pystac"
+ ],
+ [
+ "stac-utils",
+ "pystac-client"
+ ],
+ [
+ "pydata",
+ "xarray"
+ ]
+ ]
+ },
+ {
+ "issue_number": 34,
+ "title": "[Sc] PI 26.3 Objective 4: Data Retention Policy",
+ "state": "open",
+ "state_reason": null,
+ "contributors": [
+ [
+ "Sheyenne Kirkland",
+ "smk0033"
+ ]
+ ],
+ "repos": []
+ }
+ ]
+}
diff --git a/reports/generate_config.py b/reports/generate_config.py
deleted file mode 100644
index 06bcd14..0000000
--- a/reports/generate_config.py
+++ /dev/null
@@ -1,243 +0,0 @@
-#!/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
-from settings import REPO_FULL_NAME, TOKEN_ENV_VAR
-
-
-# Labels have length limits, to get around this use an abbreviation
-# and add it to this mapping
-LONG_ORG_NAME_MAPPING = {
- "cng": "cloudnativegeo"
-}
-
-
-def get_objective_issues(g: Github, repo_name: str = REPO_FULL_NAME):
- """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)
-
- if issues.totalCount < 1:
- raise (ValueError, "No PI issue found")
- 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)
- # Replace org abbreviations with full names
- if org in LONG_ORG_NAME_MAPPING:
- org = LONG_ORG_NAME_MAPPING[org]
- 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"),',
- ' "pi-26.3": ("20260426", "20260711"),',
- ' "pi-26.4": ("20260712", "20261017"),',
- "}",
- "",
- "",
- "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_repos_x_contributors_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(' for _, username in obj["contributors"]:')
- lines.append(" repos.add(tuple([*repo, username]))")
- 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(TOKEN_ENV_VAR) or os.environ.get("GITHUB_TOKEN")
- if not token:
- raise ValueError(f"Set {TOKEN_ENV_VAR} 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
deleted file mode 100644
index 2d42f25..0000000
--- a/reports/generate_docs.py
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/usr/bin/env python3
-"""
-Generate docs/objectives.md from config.py OBJECTIVES.
-
-Usage:
- uv run generate_docs.py
-"""
-
-from config import OBJECTIVES
-from settings import REPO_URL
-
-
-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}]({REPO_URL}/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}]({REPO_URL}/issues/{num}) | {title} | {state} | {contributors} |"
- )
-
- lines.append("")
- lines.append(" ")
- lines.append("")
-
- lines.append("---")
- lines.append("")
- lines.append("## Visualization")
- lines.append("")
- lines.append(
- "The charts use 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(
- f""
- )
- lines.append("")
- lines.append("---")
- lines.append("")
- lines.append("## Configuration")
- lines.append("")
- lines.append(
- f"Objectives are configured in [`reports/config.py`]({REPO_URL}/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("```")
-
- 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
deleted file mode 100644
index 7f6ca81..0000000
--- a/reports/main.py
+++ /dev/null
@@ -1,237 +0,0 @@
-#!/usr/bin/env python3
-"""
-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 (
- get_time_range,
- get_current_pi,
- get_contributors_for_pi,
- get_repos_x_contributors_for_pi,
-)
-from settings import TOKEN_ENV_VAR
-
-
-def get_commits_for_repo_author(
- g: Github,
- owner: str,
- repo: str,
- author: str,
- start_date: datetime,
- end_date: datetime,
-) -> List[dict]:
- """
- Query GitHub API for commits by a specific author in a repo.
-
- 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
- )
-
- # 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 get_resolved_for_contributor(
- g: Github,
- tasks: List[tuple],
- contributor: str,
- start_date: datetime,
- end_date: datetime,
-) -> List[dict]:
- """
- Query GitHub API for closed issues and PRs that a contributor was involved with across repos.
-
- "Involved" means the contributor was the author, assignee, mentioned, or commented.
- Uses the GitHub search API with the `involves:` qualifier and multiple `repo:` filters
- in a single query, then returns only results matching the configured repos.
-
- Returns list of issue/PR detail dicts (not PyGithub objects) to avoid
- thread safety issues.
- """
- try:
- start_str = start_date.strftime("%Y-%m-%d")
- end_str = end_date.strftime("%Y-%m-%d")
- repo_filters = " ".join(f"repo:{owner}/{repo}" for owner, repo, _ in tasks)
- base_query = (
- f"{repo_filters} "
- f"involves:{contributor} "
- f"closed:{start_str}..{end_str}"
- )
-
- issues = g.search_issues(f"is:issue {base_query}")
- prs = g.search_issues(f"is:pr {base_query} -author:{contributor}")
-
- results = []
- for item in [*issues, *prs]:
- # item.repository is a Repository object with owner.login and name
- item_owner = item.repository.owner.login
- item_repo = item.repository.name
- if (item_owner, item_repo, contributor) not in tasks:
- continue
- is_pr = item.pull_request is not None
- results.append(
- {
- "number": item.number,
- "title": item.title,
- "type": "PR" if is_pr else "Issue",
- "state": item.state,
- "author": item.user.login if item.user else None,
- "url": item.html_url,
- "created_at": item.created_at.isoformat() if item.created_at else None,
- "updated_at": item.updated_at.isoformat() if item.updated_at else None,
- "organization": item_owner,
- "repository": item_repo,
- "contributor": contributor,
- }
- )
- return results
- except Exception as e:
- print(f" Error fetching issues/PRs for {contributor}: {e}")
- return []
-
-
-def main(token: str = None, pi: str = None, max_workers: int = 10):
- """
- 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 tasks for the PI
- contributors = get_contributors_for_pi(pi)
- tasks = get_repos_x_contributors_for_pi(pi)
- print(
- f"PI: {pi} ({time_start.strftime('%Y-%m-%d')} to {time_end.strftime('%Y-%m-%d')})"
- )
- print(f" {len(tasks)} repos x contributors")
-
- if len(tasks) < 1:
- raise ValueError("No repos x contributors found in config.")
-
- print(
- f"Querying {len(tasks)} repo×contributor combinations with {max_workers} workers..."
- )
-
- all_commits = []
- all_resolved = []
-
- # Use thread pool for parallel API calls
- # Each thread gets its own Github client to avoid rate limit issues
- def make_client():
- if token:
- return Github(auth=Auth.Token(token))
- return Github()
-
- def process_commits_task(task):
- owner, repo, username = task
- g = make_client()
- try:
- return get_commits_for_repo_author(
- g, owner, repo, username, time_start, time_end
- )
- finally:
- g.close()
-
- def process_resolved_task(username):
- g = make_client()
- try:
- return get_resolved_for_contributor(
- g, tasks, username, time_start, time_end
- )
- finally:
- g.close()
-
- completed = 0
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
- commits_futures = {executor.submit(process_commits_task, task): task for task in tasks}
- resolved_issues_futures = {executor.submit(process_resolved_task, username): username for _, username in contributors}
-
- for future in as_completed(commits_futures):
- completed += 1
- print(f" Progress: {completed}/{len(tasks) + len(contributors)}")
- commits = future.result()
- all_commits.extend(commits)
-
- for future in as_completed(resolved_issues_futures):
- completed += 1
- print(f" Progress: {completed}/{len(tasks)+ len(contributors)}")
- resolved_issues = future.result()
- all_resolved.extend(resolved_issues)
-
- print(f"Found {len(all_commits)} authored commits")
- print(f"Found {len(all_resolved)} resolved issues/PRs")
-
- df = pd.DataFrame(all_commits)
- csv_filename = f"output/{pi}-authored-commits.csv"
- df.to_csv(csv_filename, index=False)
- print(f"Saved to {csv_filename}")
-
- df_resolved = (
- pd.DataFrame(all_resolved)
- .drop_duplicates(subset=["organization", "repository", "number"])
- .sort_values(by=["organization", "repository", "number"])
- )
- resolved_filename = f"output/{pi}-resolved-issues-prs.csv"
- df_resolved.to_csv(resolved_filename, index=False)
- print(f"Saved to {resolved_filename}")
-
-
-if __name__ == "__main__":
- token = os.environ.get(TOKEN_ENV_VAR) or os.environ.get("GITHUB_TOKEN")
- main(token=token)
diff --git a/reports/output/pi-26.3-authored-commits.csv b/reports/output/pi-26.3-authored-commits.csv
new file mode 100644
index 0000000..1637d38
--- /dev/null
+++ b/reports/output/pi-26.3-authored-commits.csv
@@ -0,0 +1,8 @@
+sha,message,author,committer,url,total_changes,organization,repository
+985b644aedd2930c1a72b3cb0f8130290b712020,Update authors and contributors in CITATION.cff (#197),Alex I. Mandel,GitHub,https://github.com/cloudnativegeo/cloud-optimized-geospatial-formats-guide/commit/985b644aedd2930c1a72b3cb0f8130290b712020,54,cloudnativegeo,cloud-optimized-geospatial-formats-guide
+118a3bda2d5719289452c269973bf77038b083be,fix(ci): build into the subdirectory's `dist/` (#1729),Pete Gadomski,GitHub,https://github.com/stac-utils/pystac/commit/118a3bda2d5719289452c269973bf77038b083be,2,stac-utils,pystac
+ecac0d0373c6c5d1beb73567c33b885a7bebd046,fix: remove python-version from release workflow (#1727),Pete Gadomski,GitHub,https://github.com/stac-utils/pystac/commit/ecac0d0373c6c5d1beb73567c33b885a7bebd046,2,stac-utils,pystac
+44f948d1f6c99deee85b43b851257bcf402f8489,fix: update all of our versions to be rc.0 (#1726),Pete Gadomski,GitHub,https://github.com/stac-utils/pystac/commit/44f948d1f6c99deee85b43b851257bcf402f8489,81,stac-utils,pystac
+81ff5a8bf62d85c9e7055fdf8f2ed422b92393be,chore: actually set release-please manifest correctly (#1723),Pete Gadomski,GitHub,https://github.com/stac-utils/pystac/commit/81ff5a8bf62d85c9e7055fdf8f2ed422b92393be,4,stac-utils,pystac
+750f064c4fb89d995dd9400c771a5cb06587753f,chore: manually set all the release versions (#1721),Pete Gadomski,GitHub,https://github.com/stac-utils/pystac/commit/750f064c4fb89d995dd9400c771a5cb06587753f,48,stac-utils,pystac
+697c021d76c00b34cdcb267cdd423f13b0795cb3,"fix(ci): use client-id, not app-id (#1720)",Pete Gadomski,GitHub,https://github.com/stac-utils/pystac/commit/697c021d76c00b34cdcb267cdd423f13b0795cb3,4,stac-utils,pystac
diff --git a/reports/output/pi-26.3-resolved-issues-prs.csv b/reports/output/pi-26.3-resolved-issues-prs.csv
new file mode 100644
index 0000000..4df1eac
--- /dev/null
+++ b/reports/output/pi-26.3-resolved-issues-prs.csv
@@ -0,0 +1,23 @@
+number,title,type,state,author,url,created_at,updated_at,organization,repository,contributor
+565,v5.1.0 release notes,PR,closed,grallewellyn,https://github.com/MAAP-Project/maap-documentation/pull/565,2026-03-03T23:49:53+00:00,2026-05-06T21:58:54+00:00,MAAP-Project,maap-documentation,wildintellect
+577,Merge Hub Documentation,PR,closed,smk0033,https://github.com/MAAP-Project/maap-documentation/pull/577,2026-05-04T20:54:23+00:00,2026-05-08T16:24:19+00:00,MAAP-Project,maap-documentation,wildintellect
+580,Docs Release,PR,closed,smk0033,https://github.com/MAAP-Project/maap-documentation/pull/580,2026-05-08T16:28:01+00:00,2026-05-13T15:56:44+00:00,MAAP-Project,maap-documentation,wildintellect
+581,Merge Hub version of Docs to default,Issue,closed,wildintellect,https://github.com/MAAP-Project/maap-documentation/issues/581,2026-05-08T17:59:25+00:00,2026-05-13T16:02:34+00:00,MAAP-Project,maap-documentation,wildintellect
+583,ADE Docs Notice,PR,closed,smk0033,https://github.com/MAAP-Project/maap-documentation/pull/583,2026-05-11T20:49:05+00:00,2026-05-12T15:57:09+00:00,MAAP-Project,maap-documentation,wildintellect
+52,Clarify Copyright Assignment for Future License Upgrades,Issue,closed,zacdezgeo,https://github.com/cloudnativegeo/cloud-optimized-geospatial-formats-guide/issues/52,2023-09-06T22:39:52+00:00,2026-05-13T20:54:18+00:00,cloudnativegeo,cloud-optimized-geospatial-formats-guide,wildintellect
+161,Standardize citation guidance,PR,closed,maxrjones,https://github.com/cloudnativegeo/cloud-optimized-geospatial-formats-guide/pull/161,2025-05-21T19:05:44+00:00,2026-05-13T20:54:19+00:00,cloudnativegeo,cloud-optimized-geospatial-formats-guide,wildintellect
+173,Add Virtual Zarr Stores content,Issue,closed,jsignell,https://github.com/cloudnativegeo/cloud-optimized-geospatial-formats-guide/issues/173,2025-08-27T20:44:15+00:00,2026-04-30T20:01:32+00:00,cloudnativegeo,cloud-optimized-geospatial-formats-guide,wildintellect
+188,Initial Data Producer Guide for VEDA (seeking feedback),PR,closed,siddharth0248,https://github.com/cloudnativegeo/cloud-optimized-geospatial-formats-guide/pull/188,2026-04-03T19:51:13+00:00,2026-05-13T20:14:35+00:00,cloudnativegeo,cloud-optimized-geospatial-formats-guide,wildintellect
+189,Add new virtual zarr page and remove dedicated kerchunk page,PR,closed,jsignell,https://github.com/cloudnativegeo/cloud-optimized-geospatial-formats-guide/pull/189,2026-04-16T18:08:29+00:00,2026-04-30T20:01:36+00:00,cloudnativegeo,cloud-optimized-geospatial-formats-guide,wildintellect
+196,Roadmap,PR,closed,abarciauskas-bgse,https://github.com/cloudnativegeo/cloud-optimized-geospatial-formats-guide/pull/196,2026-05-13T17:43:41+00:00,2026-05-15T00:00:51+00:00,cloudnativegeo,cloud-optimized-geospatial-formats-guide,wildintellect
+1382,schemas.stacspec.org DNS configuration,Issue,closed,GermanHydrogen,https://github.com/radiantearth/stac-spec/issues/1382,2026-04-24T07:02:51+00:00,2026-05-04T14:26:22+00:00,radiantearth,stac-spec,gadomski
+1653,"feat: implement earthquake, insar, order, processing and product STAC Extensions",PR,closed,sim13pods,https://github.com/stac-utils/pystac/pull/1653,2026-03-10T16:37:56+00:00,2026-05-07T01:31:04+00:00,stac-utils,pystac,gadomski
+1708,feat(extensions): Deprecation of eo.Band and RasterBand in favor of common metadata band,PR,closed,s-boomi,https://github.com/stac-utils/pystac/pull/1708,2026-04-23T13:34:41+00:00,2026-05-07T01:32:43+00:00,stac-utils,pystac,gadomski
+1709,fix: V2 item collection,PR,closed,jsignell,https://github.com/stac-utils/pystac/pull/1709,2026-04-23T15:31:15+00:00,2026-04-28T14:52:12+00:00,stac-utils,pystac,gadomski
+1710,fix: V2 summaries,PR,closed,jsignell,https://github.com/stac-utils/pystac/pull/1710,2026-04-23T18:44:17+00:00,2026-05-05T15:24:13+00:00,stac-utils,pystac,gadomski
+1717,fix: V2 add .ext to stac objects,PR,closed,jsignell,https://github.com/stac-utils/pystac/pull/1717,2026-04-30T19:18:24+00:00,2026-05-05T15:25:09+00:00,stac-utils,pystac,gadomski
+1718,build(deps): bump the actions-deps group across 1 directory with 3 updates,PR,closed,dependabot[bot],https://github.com/stac-utils/pystac/pull/1718,2026-05-04T18:17:38+00:00,2026-05-08T12:07:22+00:00,stac-utils,pystac,gadomski
+1728,build(deps-dev): bump jupyterlab from 4.5.6 to 4.5.7,PR,closed,dependabot[bot],https://github.com/stac-utils/pystac/pull/1728,2026-05-11T21:38:08+00:00,2026-05-11T21:45:02+00:00,stac-utils,pystac,gadomski
+1729,fix(ci): build into the subdirectory's `dist/`,PR,closed,gadomski,https://github.com/stac-utils/pystac/pull/1729,2026-05-12T13:20:26+00:00,2026-05-13T15:31:23+00:00,stac-utils,pystac,tylanderson
+1730,build(deps): bump mistune from 3.2.0 to 3.2.1,PR,closed,dependabot[bot],https://github.com/stac-utils/pystac/pull/1730,2026-05-14T16:50:20+00:00,2026-05-14T18:30:55+00:00,stac-utils,pystac,gadomski
+887,build(deps-dev): bump ruff from 0.15.11 to 0.15.12,PR,closed,dependabot[bot],https://github.com/stac-utils/pystac-client/pull/887,2026-04-27T02:20:38+00:00,2026-04-27T11:56:53+00:00,stac-utils,pystac-client,gadomski
diff --git a/reports/plot.py b/reports/plot.py
deleted file mode 100644
index cb2f7ae..0000000
--- a/reports/plot.py
+++ /dev/null
@@ -1,207 +0,0 @@
-import re
-from pathlib import Path
-
-import pandas as pd
-import matplotlib.pyplot as plt
-from matplotlib.patches import Patch
-from matplotlib.ticker import MaxNLocator
-
-from config import get_current_pi, OBJECTIVES
-from settings import TEAM_NAME, TEAM_DISPLAY_NAME, OBJECTIVES_PAGE_URL
-
-
-# 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 "TEAM 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()
-
- plot_counts(f"output/{pi}-resolved-issues-prs.csv", pi, title="resolved issues and PRs")
- plot_counts(f"output/{pi}-authored-commits.csv", pi, title="authored commits")
-
-
-def plot_counts(csv_filename: str, pi: str, title: str, show_labels: bool = False):
- 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))
-
- # 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("Count", 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)
-
- # 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)
-
- ax.set_title(
- f"{pi.upper()} {TEAM_NAME} {title}",
- fontsize=24,
- fontweight="bold",
- )
-
- # 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\n"
- "- Merged PRs counted as one\n"
- "- Individual changes may span multiple PRs\n"
- "- Split bars indicate repos in multiple objectives\n"
- f"- Includes all open source work by {TEAM_DISPLAY_NAME} team members\n\n"
- f"Objective details: {OBJECTIVES_PAGE_URL.removeprefix('https://')}"
- )
- 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
- ),
- )
-
- # Save to docs for website
- docs_images = Path(__file__).parent.parent / "docs" / "images"
- docs_images.mkdir(exist_ok=True)
- plt.savefig(
- docs_images / Path(csv_filename).name.replace(".csv", ".png"),
- bbox_inches="tight",
- dpi=150,
- )
-
-
-if __name__ == "__main__":
- main()
diff --git a/reports/pyproject.toml b/reports/pyproject.toml
deleted file mode 100644
index a30a107..0000000
--- a/reports/pyproject.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[project]
-name = "reports"
-version = "0.1.0"
-description = "Quarterly reporting utilities for the VEDA Science Support team"
-readme = "README.md"
-requires-python = ">=3.11"
-dependencies = [
- "matplotlib>=3.10.3",
- "pandas>=2.3.0",
- "pygithub>=2.6.1",
-]
diff --git a/reports/settings.py b/reports/settings.py
deleted file mode 100644
index 777b098..0000000
--- a/reports/settings.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""
-Team-specific settings for the reporting system.
-
-When adopting this reporting structure for a new team, edit the values
-below. These are the ONLY values you need to change in the Python
-scripts. See docs/adopting.md for the full adoption guide.
-"""
-
-# ── Core identifiers ──────────────────────────────────────────────
-GITHUB_ORG = "NASA-IMPACT"
-GITHUB_REPO = "science-support" # repo where objective issues live
-TEAM_NAME = "Science Support" # short name, used in chart titles
-TEAM_DISPLAY_NAME = "VEDA/EODC Science Support" # full name, used in chart caveats
-SITE_URL = "nasa-impact.github.io/science-support" # GitHub Pages URL (no https://)
-
-# ── Authentication ────────────────────────────────────────────────
-TOKEN_ENV_VAR = "GH_PAT" # env var name for the GitHub PAT
-# Also update the secret name in .github/workflows/update-reports.yml
-
-# ── Derived values (do not edit) ──────────────────────────────────
-REPO_FULL_NAME = f"{GITHUB_ORG}/{GITHUB_REPO}"
-REPO_URL = f"https://github.com/{GITHUB_ORG}/{GITHUB_REPO}"
-OBJECTIVES_PAGE_URL = f"https://{SITE_URL}/objectives"
diff --git a/team.toml b/team.toml
new file mode 100644
index 0000000..05430f5
--- /dev/null
+++ b/team.toml
@@ -0,0 +1,11 @@
+[team]
+name = "Science Support"
+display_name = "VEDA/MAAP Science Support"
+github_org = "NASA-IMPACT"
+github_repo = "science-support"
+site_url = "nasa-impact.github.io/science-support"
+objectives_page_url = "https://nasa-impact.github.io/science-support/objectives"
+token_env_var = "GH_PAT"
+
+[long_org_name_mapping]
+cng = "cloudnativegeo"