Skip to content

Commit cd4e4a5

Browse files
author
btraven00
committed
feat: show compat and commit version
1 parent 44b7911 commit cd4e4a5

7 files changed

Lines changed: 163 additions & 14 deletions

File tree

source/_static/custom.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
.image-reference img {
22
display: inline;
3+
margin-right: 4px;
34
}
45

56
.image-reference {
67
border-bottom: none !important;
8+
font-size: 0;
79
}
810

911
table code.literal {

source/_templates/executor_plugin.rst.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{% block type %}executor{% endblock %}
44

55
{% block usage %}
6-
In order to use the plugin, run Snakemake (>=8.0) in the folder where your workflow code and config resides (containing either ``workflow/Snakefile`` or ``Snakefile``) with the corresponding value for the executor flag::
6+
In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=8.0" }}) in the folder where your workflow code and config resides (containing either ``workflow/Snakefile`` or ``Snakefile``) with the corresponding value for the executor flag::
77

88
snakemake --executor {{ plugin_name }} --default-resources --jobs N ...
99

source/_templates/logger_plugin.rst.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
{% block type %}logger{% endblock %}
44

55
{% block usage %}
6-
In order to use the plugin, run Snakemake (>=9.0) with the corresponding value for the logger flag::
6+
In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=9.0" }}) with the corresponding value for the logger flag::
77

88
snakemake --logger {{ plugin_name }} ...
99

1010
with ``...`` being any additional arguments you want to use.
11-
{% endblock %}
11+
{% endblock %}

source/_templates/plugin_base.rst.j2

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,36 @@
44
Snakemake {% block type %}{% endblock %} plugin: {{ plugin_name }}
55
###########################################
66

7-
{% if repository is not none %}
7+
{% if repository is not none -%}
88
.. image:: https://img.shields.io/badge/repository-{{ repository_type }}-blue?color=%23022c22
99
:target: {{ repository }}
10-
{% else %}
10+
{% else -%}
1111
.. image:: https://img.shields.io/badge/repository-unknown-blue?color=%23022c22
1212
:target: #
13-
{% endif %}
14-
15-
{% for author in authors %}
13+
{% endif -%}
14+
{% if commit_info is not none -%}
15+
.. image:: https://img.shields.io/badge/last_commit-{{ commit_info["sha"] }}-green?color={{ commit_age_color }}
16+
:alt: Last commit
17+
:target: {{ repository }}
18+
.. image:: https://img.shields.io/badge/last_updated-{{ commit_date_label }}-green?color={{ commit_age_color }}
19+
:alt: Last updated
20+
:target: {{ repository }}
21+
{% endif -%}
22+
{% for author in authors -%}
1623
.. image:: https://img.shields.io/badge/author-{{author|replace("-","--")|urlencode}}-purple?color=%23064e3b
1724
:target: https://pypi.org/project/{{ package_name }}
18-
{% endfor %}
19-
25+
{% endfor -%}
2026
.. image:: https://img.shields.io/pypi/v/{{ package_name }}?color=%23047857
2127
:alt: PyPI - Version
2228
:target: https://pypi.org/project/{{ package_name }}
23-
2429
.. image:: https://img.shields.io/pypi/l/{{ package_name }}?color=%2310b981
2530
:alt: PyPI - License
2631
:target: https://pypi.org/project/{{ package_name }}
32+
{% if snakemake_version is not none %}
33+
.. image:: https://img.shields.io/badge/snakemake-{{ snakemake_version|replace("-", "--") }}-blue?color=%230ea5e9
34+
:alt: Snakemake
35+
:target: https://snakemake.readthedocs.io
36+
{% endif %}
2737

2838
{% if repository is not none and not repository.startswith("https://github.com/snakemake/") %}
2939
.. warning::

source/_templates/report_plugin.rst.j2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
{% block type %}report{% endblock %}
44

55
{% block usage %}
6-
In order to use the plugin, run Snakemake (>=8.5) with the corresponding value for the reporter flag::
6+
In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=8.5" }}) with the corresponding value for the reporter flag::
77

88
snakemake --reporter {{ plugin_name }} ...
99

1010
with ``...`` being any additional arguments you want to use.
11-
{% endblock %}
11+
{% endblock %}

source/_templates/scheduler_plugin.rst.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{% block type %}scheduler{% endblock %}
44

55
{% block usage %}
6-
In order to use the plugin, run Snakemake (>=9.0) with the corresponding value for the scheduler flag::
6+
In order to use the plugin, run Snakemake ({{ snakemake_version if snakemake_version else ">=9.0" }}) with the corresponding value for the scheduler flag::
77

88
snakemake --scheduler {{ plugin_name }} ...
99

source/collect_plugins.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from abc import ABC, abstractmethod
22
from collections import defaultdict
3+
from datetime import datetime, timezone
34
import json
45
import os
6+
import re
57
from pathlib import Path
68
import shutil
79
import subprocess
810
import sys
911
import tempfile
1012
import textwrap
1113
from typing import Any, Dict, List
14+
import urllib.parse
1215
import uuid
1316
from pypi_simple import PyPISimple
1417

@@ -178,6 +181,11 @@ def collect_plugins(self, plugins, packages, templates):
178181
desc = "\n".join(meta["info"]["description"].split("\n")[2:])
179182
version = meta["info"]["version"]
180183

184+
# Parse Snakemake compatibility from interface package requirements
185+
snakemake_version = parse_snakemake_compatibility(
186+
meta["info"].get("requires_dist")
187+
)
188+
181189
# convert to rst
182190
desc = m2r2.convert(desc)
183191

@@ -225,6 +233,7 @@ def get_setting_meta(setting, key, default="", verb=False):
225233
repository_type = "gitlab"
226234
docs_intro = get_docs(repository, section="intro")
227235
docs_further = get_docs(repository, section="further")
236+
228237
if repository is not None and docs_intro is None and docs_further is None:
229238
docs_warning = (
230239
f"No documentation found in repository {repository}. The plugin should "
@@ -233,6 +242,14 @@ def get_setting_meta(setting, key, default="", verb=False):
233242
"auto-generated usage instructions presented in this catalog."
234243
)
235244

245+
commit_info = get_commit(repository, repository_type)
246+
commit_date_label = (
247+
_commit_date_label(commit_info["date"]) if commit_info else None
248+
)
249+
commit_age_color = (
250+
_commit_age_color(commit_info["date"]) if commit_info else None
251+
)
252+
236253
settings = {}
237254
aux_info = {}
238255

@@ -257,6 +274,10 @@ def get_setting_meta(setting, key, default="", verb=False):
257274
authors=authors,
258275
repository=repository,
259276
repository_type=repository_type,
277+
commit_info=commit_info,
278+
commit_age_color=commit_age_color,
279+
commit_date_label=commit_date_label,
280+
snakemake_version=snakemake_version,
260281
meta=meta,
261282
desc=desc,
262283
docs_intro=docs_intro,
@@ -310,6 +331,24 @@ def plugin_type(self):
310331
return "scheduler"
311332

312333

334+
def _commit_date_label(date_str: str) -> str:
335+
"""Return a shields.io-safe date label like 'March_2026' from an ISO date string."""
336+
return datetime.fromisoformat(date_str.replace("Z", "+00:00")).strftime("%B_%Y")
337+
338+
339+
def _commit_age_color(date_str: str) -> str:
340+
"""Return a shields.io color hex based on how old the commit date is."""
341+
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
342+
now = datetime.now(timezone.utc)
343+
months = (now.year - dt.year) * 12 + (now.month - dt.month)
344+
if months < 6:
345+
return "%2316a34a" # green
346+
elif months < 24:
347+
return "%23ea580c" # orange
348+
else:
349+
return "%23dc2626" # red
350+
351+
313352
def collect_plugins():
314353
templates = Environment(
315354
loader=FileSystemLoader("_templates"),
@@ -372,3 +411,101 @@ def retrieve():
372411
return m2r2.convert(retrieved, renderer=renderer)
373412
else:
374413
return None
414+
415+
416+
def _fetch_commit_info(repository: str, repository_type: str, branch: str):
417+
"""Fetch commit info for a specific branch using regex to parse URLs.
418+
419+
Args:
420+
repository: The repository URL
421+
repository_type: Either "github" or "gitlab"
422+
branch: The branch name to query
423+
424+
Returns:
425+
Dict with 'sha' and 'date' keys, or None if request fails
426+
"""
427+
if repository_type == "github":
428+
match = re.match(r"https://github\.com/([^/]+)/([^/]+?)/?$", repository)
429+
if not match:
430+
return None
431+
owner, repo = match.groups()
432+
433+
api_url = f"https://api.github.com/repos/{owner}/{repo}/commits/{branch}?per_page=1&page=1"
434+
response = requests.get(api_url)
435+
if response.status_code == 200:
436+
data = response.json()
437+
return {
438+
"sha": data["sha"][:7],
439+
"date": data["commit"]["author"]["date"],
440+
}
441+
442+
elif repository_type == "gitlab":
443+
match = re.match(r"https://gitlab\.com/(.+?)/?$", repository)
444+
if not match:
445+
return None
446+
project_path = match.group(1)
447+
448+
encoded_path = urllib.parse.quote(project_path, safe="")
449+
api_url = f"https://gitlab.com/api/v4/projects/{encoded_path}/repository/commits/{branch}"
450+
response = requests.get(api_url)
451+
if response.status_code == 200:
452+
data = response.json()
453+
return {
454+
"sha": data["short_id"],
455+
"date": data["committed_date"],
456+
}
457+
458+
return None
459+
460+
461+
def get_commit(
462+
repository: str | None, repository_type: str | None, branches=["main", "master"]
463+
):
464+
"""Fetch the latest commit information from a GitHub or GitLab repository.
465+
466+
Returns a dict with commit info:
467+
- sha: short commit hash (7 chars)
468+
- date: commit date
469+
Returns None if repository is None or if the request fails.
470+
"""
471+
if repository is None or repository_type is None:
472+
return None
473+
474+
for branch in branches:
475+
try:
476+
result = _fetch_commit_info(repository, repository_type, branch)
477+
if result is not None:
478+
return result
479+
except Exception:
480+
# Continue to next branch on any error
481+
continue
482+
483+
return None
484+
485+
486+
def parse_snakemake_compatibility(requires_dist: list[str] | None) -> str | None:
487+
"""Parse Snakemake version compatibility from interface package requirements.
488+
489+
The major version of interface packages matches the major version of Snakemake.
490+
For example, snakemake-interface-executor-plugins 9.x is compatible with Snakemake 9.x.
491+
492+
Returns version constraint like ">=9.0" or None if not found.
493+
"""
494+
if not requires_dist:
495+
return None
496+
497+
# Look for any interface plugin dependency
498+
interface_pattern = r"snakemake-interface-(?:executor|storage|report|logger|scheduler)-plugins\s*(?:\()?([><=!~\d\.,\s<>]+)"
499+
500+
for dep in requires_dist:
501+
match = re.search(interface_pattern, dep)
502+
if match:
503+
constraint = match.group(1).strip()
504+
# get the major version from a constraint: ">=9.0.0,<10.0.0"
505+
major_version_match = re.search(r">=?(\d+)\.", constraint)
506+
if major_version_match:
507+
major_version = major_version_match.group(1)
508+
# version for interface matches major Snakemake ver
509+
return f">={major_version}.0"
510+
511+
return None

0 commit comments

Comments
 (0)