Skip to content

Commit b9799d7

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

7 files changed

Lines changed: 155 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: 129 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,9 @@ 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(meta["info"].get("requires_dist"))
186+
181187
# convert to rst
182188
desc = m2r2.convert(desc)
183189

@@ -225,6 +231,7 @@ def get_setting_meta(setting, key, default="", verb=False):
225231
repository_type = "gitlab"
226232
docs_intro = get_docs(repository, section="intro")
227233
docs_further = get_docs(repository, section="further")
234+
228235
if repository is not None and docs_intro is None and docs_further is None:
229236
docs_warning = (
230237
f"No documentation found in repository {repository}. The plugin should "
@@ -233,6 +240,10 @@ def get_setting_meta(setting, key, default="", verb=False):
233240
"auto-generated usage instructions presented in this catalog."
234241
)
235242

243+
commit_info = get_commit(repository, repository_type)
244+
commit_date_label = _commit_date_label(commit_info["date"]) if commit_info else None
245+
commit_age_color = _commit_age_color(commit_info["date"]) if commit_info else None
246+
236247
settings = {}
237248
aux_info = {}
238249

@@ -257,6 +268,10 @@ def get_setting_meta(setting, key, default="", verb=False):
257268
authors=authors,
258269
repository=repository,
259270
repository_type=repository_type,
271+
commit_info=commit_info,
272+
commit_age_color=commit_age_color,
273+
commit_date_label=commit_date_label,
274+
snakemake_version=snakemake_version,
260275
meta=meta,
261276
desc=desc,
262277
docs_intro=docs_intro,
@@ -310,6 +325,24 @@ def plugin_type(self):
310325
return "scheduler"
311326

312327

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

0 commit comments

Comments
 (0)