Skip to content

Commit 7ec0767

Browse files
authored
Merge pull request #93 from martenson/iwc-parseinstall
IWC parse and install all tools
2 parents 6d3f217 + 91ad2a1 commit 7ec0767

File tree

4 files changed

+291
-4
lines changed

4 files changed

+291
-4
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Install and update IWC tools
2+
3+
on:
4+
schedule:
5+
# At 10:00 every day
6+
- cron: "0 10 * * *"
7+
8+
# Allows you to run this workflow manually from the Actions tab
9+
workflow_dispatch:
10+
11+
jobs:
12+
update-repos:
13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
toolset: [galaxy-qa1.galaxy.cloud.e-infra.cz]
20+
steps:
21+
- uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.12'
24+
architecture: 'x64'
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
- name: Install dependencies
28+
run: pip install -r requirements.txt
29+
- name: Clone IWC repo
30+
run: git clone https://github.com/galaxyproject/iwc /tmp/iwc
31+
- name: Run update script
32+
run: python scripts/get_iwc_tools.py -w /tmp/iwc -s ${{ matrix.toolset }} -u uncategorized.yml
33+
- name: Get current date
34+
id: date
35+
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
36+
- name: Create Pull Request
37+
uses: peter-evans/create-pull-request@v7
38+
with:
39+
branch: iwc-update-${{ matrix.toolset }}
40+
committer: CESNETbot <[email protected]>
41+
title: Install IWC tools for ${{ matrix.toolset }} ${{ steps.date.outputs.date }}
42+
commit-message: "output of get_iwc_tools.py"
43+
labels: automated
44+
assignees: martenson
45+
reviewers: martenson

requirements.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
bioblend
2-
ephemeris
3-
pykwalify
4-
PyYAML
1+
bioblend>=1.2.0
2+
ephemeris>=0.10.10
3+
pykwalify>=1.6.1
4+
PyYAML>=4.2
5+
setuptools>=76.1.0

scripts/get_iwc_tools.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import argparse
2+
import glob
3+
import json
4+
import os
5+
from collections import defaultdict
6+
7+
import yaml
8+
9+
from ephemeris.generate_tool_list_from_ga_workflow_files import (
10+
generate_repo_list_from_workflow,
11+
)
12+
from steal_sections import steal_section
13+
from fix_lockfile import update_file as fix_lockfile
14+
from update_tool import update_file
15+
16+
GALAXY_URL = "https://usegalaxy.eu"
17+
18+
19+
def find_workflows(workflow_path):
20+
workflow_files = []
21+
for dirpath, _, filenames in os.walk(workflow_path):
22+
workflow_files.extend(
23+
(
24+
os.path.join(dirpath, filename)
25+
for filename in filenames
26+
if filename.endswith(".ga")
27+
)
28+
)
29+
return workflow_files
30+
31+
32+
def add_repos(workflow_path, toolset, uncategorized_file):
33+
workflow_paths = find_workflows(workflow_path)
34+
repo_list = generate_repo_list_from_workflow(workflow_paths, "Uncategorized")
35+
steal_section(
36+
{"tools": repo_list},
37+
toolset,
38+
leftovers_file=os.path.join(toolset, uncategorized_file),
39+
galaxy_url=GALAXY_URL,
40+
verbose=True,
41+
)
42+
section_files = glob.glob(f"{toolset}/*.yml")
43+
for section_file in section_files:
44+
fix_lockfile(
45+
section_file,
46+
install_repository_dependencies=False,
47+
install_resolver_dependencies=False,
48+
)
49+
update_file(section_file, without=True)
50+
lock_files = glob.glob(f"{toolset}/*.yml.lock")
51+
lock_file_contents = {}
52+
# Keep a global lookup to find which lock file contains each tool
53+
global_tool_lookup = {} # (owner, name) -> lock_file
54+
55+
# Load all lock files
56+
for lock_file in lock_files:
57+
with open(lock_file) as lock_file_fh:
58+
lock_contents = yaml.safe_load(lock_file_fh)
59+
lock_file_contents[lock_file] = lock_contents
60+
61+
# Build global lookup for finding tools
62+
for repo in lock_contents["tools"]:
63+
key = (repo["owner"], repo["name"])
64+
if key not in global_tool_lookup:
65+
global_tool_lookup[key] = lock_file
66+
67+
# Add revisions from workflow repos to the appropriate lock files
68+
for workflow_repo in repo_list:
69+
key = (workflow_repo["owner"], workflow_repo["name"])
70+
if key in global_tool_lookup:
71+
lock_file = global_tool_lookup[key]
72+
lock_contents = lock_file_contents[lock_file]
73+
# Find the tool in this specific lock file and add revisions
74+
for repo in lock_contents["tools"]:
75+
if repo["owner"] == workflow_repo["owner"] and repo["name"] == workflow_repo["name"]:
76+
repo["revisions"] = sorted(
77+
list(set(repo.get("revisions", []) + workflow_repo["revisions"]))
78+
)
79+
break
80+
81+
# Deduplicate tools within each lock file separately
82+
for lock_file, entries in lock_file_contents.items():
83+
# Create deduplicated tools list for this specific file
84+
tool_map = {} # key: (owner, name) -> value: merged tool dict
85+
86+
for tool in entries["tools"]:
87+
key = (tool["owner"], tool["name"])
88+
if key not in tool_map:
89+
# First occurrence in this file - store it
90+
tool_map[key] = tool
91+
else:
92+
# Duplicate in this file - merge revisions into first occurrence
93+
existing_tool = tool_map[key]
94+
existing_tool["revisions"] = sorted(
95+
list(set(existing_tool.get("revisions", []) + tool.get("revisions", [])))
96+
)
97+
98+
# Rebuild the tools list from the deduplicated map, preserving original order
99+
deduplicated_tools = []
100+
seen = set()
101+
for tool in entries["tools"]:
102+
key = (tool["owner"], tool["name"])
103+
if key not in seen:
104+
seen.add(key)
105+
deduplicated_tools.append(tool_map[key])
106+
107+
entries["tools"] = deduplicated_tools
108+
109+
with open(lock_file, "w") as lock_file_fh:
110+
yaml.safe_dump(json.loads(json.dumps(entries)), stream=lock_file_fh)
111+
112+
113+
if __name__ == "__main__":
114+
115+
parser = argparse.ArgumentParser(description="")
116+
parser.add_argument("-w", "--workflow-path", help="Path to directory with workflows")
117+
parser.add_argument("-s", "--toolset", default="usegalaxy.org", help="The toolset dir to add versions to")
118+
parser.add_argument("-u", "--uncategorized-file", default="leftovers.yaml", help="The file to store leftover (uninstalled) repos in.")
119+
120+
args = parser.parse_args()
121+
122+
add_repos(workflow_path=args.workflow_path, toolset=args.toolset, uncategorized_file=args.uncategorized_file)

scripts/steal_sections.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python
2+
#
3+
# given an input yaml and a toolset, find tools in sections on another server and add to toolset
4+
5+
import yaml
6+
import glob
7+
import os
8+
import string
9+
import argparse
10+
import requests
11+
12+
13+
def steal_section(repo_dict, toolset: str, leftovers_file: str, galaxy_url: str, verbose: bool = False):
14+
section_files = glob.glob(os.path.join(toolset, "*.yml"))
15+
16+
other_tools = {}
17+
other_labels = {}
18+
19+
url = f"{galaxy_url}/api/tools?in_panel=false"
20+
if verbose:
21+
print(f"Loading tools from: {url}")
22+
for tool in requests.get(url).json():
23+
if 'tool_shed_repository' not in tool:
24+
continue
25+
# this overwrites multi-tool repos but that's not a biggie
26+
tool_key = (tool['tool_shed_repository']['name'], tool['tool_shed_repository']['owner'])
27+
section_label = tool['panel_section_name']
28+
section_id = ''.join(c if c in string.ascii_letters + string.digits else '_' for c in section_label).lower()
29+
other_tools[tool_key] = section_id
30+
other_labels[section_id] = section_label
31+
32+
existing = {}
33+
leftover_tools = []
34+
new = {}
35+
36+
for section_file in section_files:
37+
if verbose:
38+
print(f"Reading section file: {section_file}")
39+
a = yaml.safe_load(open(section_file, 'r'))
40+
tools = a['tools']
41+
for tool in tools:
42+
tool_key = (tool['name'], tool['owner'])
43+
existing[tool_key] = section_file
44+
45+
tools = repo_dict['tools']
46+
for tool in tools:
47+
tool_key = (tool['name'], tool['owner'])
48+
if tool_key in existing:
49+
if verbose:
50+
print(f"Skipping existing tool: {tool['owner']}/{tool['name']}")
51+
continue
52+
elif tool_key in other_tools:
53+
try:
54+
new[other_tools[tool_key]].append(tool_key)
55+
except:
56+
new[other_tools[tool_key]] = [tool_key]
57+
else:
58+
leftover_tools.append(tool)
59+
60+
print(f"Found sections for {len(new)} tools ({len(leftover_tools)} left over)")
61+
62+
for section, repos in new.items():
63+
section_file = os.path.join(toolset, section + ".yml")
64+
if not os.path.exists(section_file):
65+
a = {'tool_panel_section_label': other_labels[section], 'tools': []}
66+
if verbose:
67+
print(f"Adding to new section file: {section_file}")
68+
else:
69+
a = yaml.safe_load(open(section_file, 'r'))
70+
if verbose:
71+
print(f"Adding to existing section file: {section_file}")
72+
tools = a['tools']
73+
# Get existing tool keys to avoid duplicates
74+
existing_tools = {(tool['name'], tool['owner']) for tool in tools}
75+
# Deduplicate repos list (same tool may appear in multiple workflows)
76+
unique_repos = list(dict.fromkeys(repos)) # Preserves order while removing duplicates
77+
# Only add tools that don't already exist in this section file
78+
new_tools = [{"name": t[0], "owner": t[1]} for t in unique_repos if t not in existing_tools]
79+
tools.extend(new_tools)
80+
81+
with open(section_file, 'w') as out:
82+
yaml.dump(a, out, default_flow_style=False)
83+
84+
if leftover_tools:
85+
# Keep only name and owner fields to match the standard .yml format
86+
cleaned_tools = []
87+
for tool in leftover_tools:
88+
cleaned_tool = {'name': tool['name'], 'owner': tool['owner']}
89+
cleaned_tools.append(cleaned_tool)
90+
91+
with open(leftovers_file, 'w') as out:
92+
yaml.dump({'tool_panel_section_label': 'Uncategorized', 'tools': cleaned_tools}, out, default_flow_style=False)
93+
94+
def main():
95+
96+
VERSION = 0.1
97+
98+
parser = argparse.ArgumentParser(description="")
99+
parser.add_argument("-t", "--tools", default="tools.yaml", help="Input tools.yaml")
100+
parser.add_argument("-s", "--toolset", default="usegalaxy.org", help="The toolset dir to add versions to")
101+
parser.add_argument("-l", "--leftovers-file", default="leftovers.yaml", help="The file to store leftover (unmatched) repos in.")
102+
parser.add_argument("-g", "--galaxy-url", default="https://usegalaxy.eu", help="The Galaxy server to steal from")
103+
parser.add_argument("--version", action='store_true')
104+
parser.add_argument("--verbose", action='store_true')
105+
106+
args = parser.parse_args()
107+
108+
if args.version:
109+
print("merge_versions.py version: %.1f" % VERSION)
110+
return
111+
112+
with open(args.tools) as fh:
113+
repo_dict = yaml.safe_load(fh)
114+
toolset = args.toolset
115+
steal_section(repo_dict=repo_dict, toolset=toolset, leftovers_file=args.leftovers_file, galaxy_url=args.galaxy_url, verbose=args.verbose)
116+
117+
118+
if __name__ == "__main__":
119+
main()

0 commit comments

Comments
 (0)