Skip to content

Commit 59fa59c

Browse files
committed
Init Plugin integration
1 parent 7e36f20 commit 59fa59c

3 files changed

Lines changed: 130 additions & 51 deletions

File tree

src/hermes/commands/init/base.py

Lines changed: 88 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
import os
88
import re
99
import sys
10+
import traceback
11+
import requests
12+
import toml
13+
1014
from dataclasses import dataclass
1115
from enum import Enum, auto
1216
from importlib import metadata
1317
from pathlib import Path
1418
from urllib.parse import urljoin, urlparse
15-
16-
import requests
17-
import toml
1819
from pydantic import BaseModel
1920
from requests import HTTPError
2021

@@ -174,6 +175,18 @@ def __init__(self, parser: argparse.ArgumentParser):
174175
"deposit_extra_files": "",
175176
"push_branch": "main"
176177
}
178+
self.hermes_toml_data = {
179+
"harvest": {
180+
"sources": ["cff"]
181+
},
182+
"deposit": {
183+
"target": "invenio_rdm",
184+
"invenio_rdm": {
185+
"site_url": "",
186+
"access_right": "open"
187+
}
188+
}
189+
}
177190
self.plugin_relevant_commands = ["harvest", "deposit"]
178191
self.builtin_plugins: dict[str: HermesPlugin] = get_builtin_plugins(self.plugin_relevant_commands)
179192
self.selected_plugins: list[marketplace.PluginInfo] = []
@@ -210,40 +223,51 @@ def __call__(self, args: argparse.Namespace) -> None:
210223
if args.template_branch != "":
211224
self.template_branch = args.template_branch
212225

213-
# Test if init is valid in current folder
214-
self.test_initialization()
226+
try:
227+
# Test if init is valid in current folder
228+
self.test_initialization()
229+
230+
sc.echo(f"Starting to initialize HERMES in {self.folder_info.absolute_path}\n")
231+
sc.max_steps = 8
215232

216-
sc.echo(f"Starting to initialize HERMES in {self.folder_info.absolute_path}")
217-
sc.max_steps = 8
233+
sc.next_step("Configure HERMES plugins")
234+
self.choose_plugins()
235+
self.integrate_plugins()
218236

219-
sc.next_step("Configure HERMES plugins")
220-
self.choose_plugins()
237+
sc.next_step("Configure deposition platform and setup method")
238+
self.choose_deposit_platform()
239+
self.integrate_deposit_platform()
240+
self.choose_setup_method()
221241

222-
sc.next_step("Configure deposition platform and setup method")
223-
self.choose_deposit_platform()
224-
self.choose_setup_method()
242+
sc.next_step("Configure HERMES behaviour")
243+
self.choose_push_branch()
244+
self.choose_deposit_files()
225245

226-
sc.next_step("Configure HERMES behaviour")
227-
self.choose_push_branch()
228-
self.choose_deposit_files()
246+
sc.next_step("Create hermes.toml file")
247+
self.create_hermes_toml()
229248

230-
sc.next_step("Create hermes.toml file")
231-
self.create_hermes_toml()
249+
sc.next_step("Create CITATION.cff file")
250+
self.create_citation_cff()
232251

233-
sc.next_step("Create CITATION.cff file")
234-
self.create_citation_cff()
252+
sc.next_step("Create git CI files")
253+
self.update_gitignore()
254+
self.create_ci_template()
235255

236-
sc.next_step("Create git CI files")
237-
self.update_gitignore()
238-
self.create_ci_template()
256+
sc.next_step("Connect with deposition platform")
257+
self.connect_deposit_platform()
239258

240-
sc.next_step("Connect with deposition platform")
241-
self.connect_deposit_platform()
259+
sc.next_step("Connect with git hoster")
260+
self.configure_git_project()
242261

243-
sc.next_step("Connect with git hoster")
244-
self.configure_git_project()
262+
sc.echo("\nHERMES is now initialized and ready to be used.\n",
263+
formatting=sc.Formats.OKGREEN+sc.Formats.BOLD)
245264

246-
sc.echo("\nHERMES is now initialized and ready to be used.\n", formatting=sc.Formats.OKGREEN+sc.Formats.BOLD)
265+
except Exception as e:
266+
# More useful message on error
267+
sc.echo(f"An error occurred during execution of HERMES init: {e}",
268+
formatting=sc.Formats.FAIL+sc.Formats.BOLD)
269+
sc.debug_info(traceback.format_exc())
270+
sys.exit(2)
247271

248272
def test_initialization(self) -> None:
249273
"""Test if init is possible and wanted. If not: sys.exit()"""
@@ -284,6 +308,7 @@ def test_initialization(self) -> None:
284308
if self.git_remote:
285309
self.git_remote_url = git_info.get_remote_url(self.git_remote)
286310
self.git_hoster = get_git_hoster_from_url(self.git_remote_url)
311+
287312
# Abort with no remote
288313
else:
289314
sc.echo("Your git project does not have a remote. It is recommended for HERMES to "
@@ -311,26 +336,12 @@ def test_initialization(self) -> None:
311336
sys.exit()
312337

313338
def create_hermes_toml(self) -> None:
314-
"""Creates the hermes.toml file based on a dictionary"""
315-
deposit_url = DepositPlatformUrls.get(self.deposit_platform)
316-
default_values = {
317-
"harvest": {
318-
"sources": ["cff"]
319-
},
320-
"deposit": {
321-
"target": "invenio_rdm",
322-
"invenio_rdm": {
323-
"site_url": deposit_url,
324-
"access_right": "open"
325-
}
326-
}
327-
}
328-
339+
"""Creates the hermes.toml file based on a self.hermes_toml_data"""
329340
if (not self.folder_info.has_hermes_toml) \
330341
or sc.confirm("Do you want to replace your `hermes.toml` with a new one?", default=True):
331342
with open('hermes.toml', 'w') as toml_file:
332343
# noinspection PyTypeChecker
333-
toml.dump(default_values, toml_file)
344+
toml.dump(self.hermes_toml_data, toml_file)
334345
sc.echo("`hermes.toml` was created.", formatting=sc.Formats.OKGREEN)
335346

336347
def create_citation_cff(self) -> None:
@@ -384,7 +395,6 @@ def create_ci_template(self) -> None:
384395
"""Downloads and configures the ci workflow files using templates from the chosen template branch."""
385396
match self.git_hoster:
386397
case GitHoster.GitHub:
387-
# TODO Replace this later with the link to the real templates (not the feature branch)
388398
template_url = self.get_template_url("TEMPLATE_hermes_github_to_zenodo.yml")
389399
ci_file_folder = ".github/workflows"
390400
ci_file_name = "hermes_github.yml"
@@ -429,10 +439,11 @@ def configure_ci_template(self, ci_file_path) -> None:
429439
parameters = list(set(re.findall(r'{%(.*?)%}', content)))
430440
for parameter in parameters:
431441
if parameter in self.ci_parameters:
432-
content = content.replace(f'{{%{parameter}%}}', self.ci_parameters[parameter])
442+
value = str(self.ci_parameters[parameter])
443+
content = content.replace(f'{{%{parameter}%}}', value)
433444
else:
434-
sc.echo(f"Warning: CI File Parameter {{%{parameter}%}} was not set.",
435-
formatting=sc.Formats.WARNING)
445+
sc.debug_info(f"CI File Parameter {{%{parameter}%}} was not set.", formatting=sc.Formats.WARNING)
446+
content = content.replace(f'{{%{parameter}%}}', '')
436447
with open(ci_file_path, 'w') as file:
437448
file.write(content)
438449

@@ -572,6 +583,11 @@ def choose_deposit_platform(self) -> None:
572583
)
573584
self.deposit_platform = deposit_platform_list[deposit_platform_index]
574585

586+
def integrate_deposit_platform(self) -> None:
587+
"""Makes changes to the toml data or something else based on the chosen deposit platform."""
588+
deposit_url = DepositPlatformUrls.get(self.deposit_platform)
589+
self.hermes_toml_data["deposit"]["invenio_rdm"]["site_url"] = deposit_url
590+
575591
def choose_setup_method(self) -> None:
576592
"""User chooses his desired setup method: Either preferring automatic (if available) or manual."""
577593
setup_method_index = sc.choose(
@@ -615,18 +631,42 @@ def choose_plugins(self):
615631
sc.echo("The following plugins are available for installation:")
616632
for info in plugins_available:
617633
sc.echo(str(info), formatting=sc.Formats.WARNING, no_log=True)
634+
if info.abstract:
635+
sc.echo("-> " + info.abstract, formatting=sc.Formats.ITALIC+sc.Formats.WARNING, no_log=True)
618636
sc.echo("")
619637
else:
620638
self.selected_plugins = plugins_selected
621639
break
622-
choice = sc.choose("Do you want to add a plugin?", ["No"] + [p.name for p in plugins_available])
640+
no_text = "No further plugins needed"
641+
choice = sc.choose("Do you want to add a plugin?",
642+
[no_text] + [f"Add {p.name}" for p in plugins_available])
623643
if choice == 0:
624644
self.selected_plugins = plugins_selected
625645
break
626646
else:
627647
chosen_plugin = plugins_available.pop(choice - 1)
628648
plugins_selected.append(chosen_plugin)
629649

650+
def integrate_plugins(self):
651+
"""
652+
Plugin installation is added to the ci-parameters.
653+
Also for now we use this method to do custom plugin installation steps.
654+
"""
655+
for plugin_info in self.selected_plugins:
656+
if not plugin_info.is_valid():
657+
sc.echo(f"Could not install plugin: {plugin_info.name}", formatting=sc.Formats.FAIL)
658+
continue
659+
pip_install = plugin_info.get_pip_install_command()
660+
self.ci_parameters["pip_install_plugins_github"] = \
661+
self.ci_parameters.get("pip_install_plugins_github", "") + " - run: " + pip_install + "\n"
662+
self.ci_parameters["pip_install_plugins_gitlab"] = \
663+
self.ci_parameters.get("pip_install_plugins_gitlab", "") + " - " + pip_install + "\n"
664+
match plugin_info.name:
665+
case "hermes-plugin-python":
666+
self.hermes_toml_data["harvest"]["sources"].append("toml")
667+
case "hermes-plugin-git":
668+
self.hermes_toml_data["harvest"]["sources"].append("git")
669+
630670
def no_git_setup(self, start_question: str = "") -> None:
631671
"""Makes the init for a gitless project (basically just creating hermes.toml)"""
632672
if start_question == "":

src/hermes/commands/init/util/slim_click.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def get_log_type(self, default: int = logging.INFO) -> int:
6161
return logging.INFO
6262
return default
6363

64+
def wrap_around(self, text: str) -> str:
65+
return self.get_ansi() + text + Formats.ENDC.get_ansi()
66+
6467

6568
def echo(text: str, formatting: Formats = Formats.EMPTY, log_as: int = logging.NOTSET, no_log: bool = False):
6669
"""
@@ -74,10 +77,12 @@ def echo(text: str, formatting: Formats = Formats.EMPTY, log_as: int = logging.N
7477
if AUTO_LOG_ON_ECHO and log_as == logging.NOTSET and text != "":
7578
log_as = formatting.get_log_type(logging.INFO)
7679
# Add text to log if there is a logger
77-
if log_as != logging.NOTSET and default_file_logger and no_log == False:
80+
if log_as != logging.NOTSET and default_file_logger and not no_log:
7881
default_file_logger.log(log_as, text)
7982
# Format the text for the console
8083
if formatting != Formats.EMPTY:
84+
if Formats.ENDC.get_ansi() in text:
85+
text = text.replace(Formats.ENDC.get_ansi(), Formats.ENDC.get_ansi() + formatting.get_ansi())
8186
text = f"{formatting.get_ansi()}{text}{Formats.ENDC.get_ansi()}"
8287
# Print it
8388
if (log_as != logging.DEBUG) or PRINT_DEBUG:
@@ -165,7 +170,9 @@ def next_step(description: str):
165170

166171
def create_console_hyperlink(url: str, word: str) -> str:
167172
"""Use this to have a consistent display of hyperlinks."""
168-
return f"\033]8;;{url}\033\\{word}\033]8;;\033\\" if USE_FANCY_HYPERLINKS else f"{word} ({url})"
173+
if USE_FANCY_HYPERLINKS:
174+
return f"\033]8;;{url}\033\\{word}\033]8;;\033\\"
175+
return f"{word} ({url})"
169176

170177

171178
class ColorLogHandler(logging.Handler):

src/hermes/commands/marketplace.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pydantic import BaseModel, ConfigDict, Field
1212
from pydantic.alias_generators import to_camel
1313

14+
from hermes.commands.init.util import slim_click
1415
from hermes.utils import hermes_doi, hermes_user_agent
1516

1617
MARKETPLACE_URL = "https://hermes.software-metadata.pub/marketplace"
@@ -99,14 +100,43 @@ def _sort_plugins_by_step(plugins: list[SchemaOrgSoftwareApplication]) -> dict[s
99100
def _plugin_loc(_plugin: SchemaOrgSoftwareApplication) -> str:
100101
return "builtin" if _plugin.is_part_of == schema_org_hermes else (_plugin.url or "")
101102

103+
102104
class PluginInfo:
105+
"""
106+
This class contains all the information about a plugin which are needed for the init-Command.
107+
"""
103108
def __init__(self):
104109
self.name: str = ""
105110
self.location: str = ""
106111
self.step: str = ""
107112
self.builtin: bool = True
113+
self.install_url: str = ""
114+
self.abstract: str = ""
115+
108116
def __str__(self):
109-
return f"[{self.step}] {self.name} ({self.location})"
117+
step_text = f"[{self.step}]"
118+
return f"{step_text} {slim_click.Formats.BOLD.wrap_around(self.name)} ({self.location})"
119+
120+
def get_pip_install_command(self) -> str:
121+
"""
122+
Returns the pip install command which can be used to install the plugin.
123+
Tries to extract the project name from the install_url (PyPI-URL) if possible.
124+
Otherwise, it tries to use the location (Git-Project-URL) for the pip install command.
125+
"""
126+
if self.install_url and self.install_url.startswith("https://pypi.org/project/"):
127+
project_name = self.install_url.rstrip("/").removeprefix("https://pypi.org/project/")
128+
return f"pip install {project_name}"
129+
if self.location and self.location.startswith(("https://", "git@", "ssh://")):
130+
git_url = self.location.rstrip("/")
131+
return f"pip install git+{git_url}"
132+
return ""
133+
134+
def is_valid(self) -> bool:
135+
"""
136+
Returns True if the plugin can be installed. Maybe we'll check the actual repository here later
137+
to make sure that other things are valid too.
138+
"""
139+
return self.get_pip_install_command() != ""
110140

111141

112142
def get_plugin_infos() -> list[PluginInfo]:
@@ -124,6 +154,8 @@ def get_plugin_infos() -> list[PluginInfo]:
124154
info.step = step
125155
info.location = _plugin_loc(plugin)
126156
info.builtin = plugin.is_part_of == schema_org_hermes
157+
info.install_url = plugin.install_url
158+
info.abstract = plugin.abstract
127159
infos.append(info)
128160
return infos
129161

0 commit comments

Comments
 (0)