diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a126e02 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + # run CI only if files in these whitelisted paths are changed + paths: + - '.github/workflows/**' + - 'rdmo_plugins/**' + - .pre-commit-config.yaml + - pyproject.toml + +# Ref: https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHONDONTWRITEBYTECODE: 1 + FORCE_COLOR: 1 # colored output by pytest etc. + CLICOLOR_FORCE: 1 # colored output by ruff + +jobs: + + lint: + name: Lint + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.12" + cache: pip + - run: python -m pip install --upgrade pip setuptools wheel + - run: python -m pip install -e .[dev] + - name: Set up pre-commit cache + uses: actions/cache@v3 + with: + path: ~/.cache/pre-commit + key: lint-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Run linters via pre-commit (ruff, eslint) + run: pre-commit run --all-files --color=always + + dev-setup: + # Ref: structlog (MIT licensed) + name: "Test dev setup on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.12" + cache: pip + - run: python -Im pip install -e .[dev] + - run: python -Ic 'import rdmo_plugins; print(rdmo_plugins.__version__)' + + required-checks-pass: + if: always() + needs: + - lint + - dev-setup + runs-on: ubuntu-22.04 + steps: + - uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml deleted file mode 100644 index 58ef0ab..0000000 --- a/.github/workflows/django.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Django CI - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - python-version: [3.7, 3.8, 3.9] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Run Tests - run: | - python manage.py test diff --git a/.gitignore b/.gitignore index ad1149f..ddcb7d4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ dist *.egg-info /env + +.pytest_cache +.ruff_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cd51c2e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +repos: + - repo: meta + hooks: + - id: check-hooks-apply + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-ast + - id: check-xml + - id: check-yaml + - id: end-of-file-fixer + exclude: \.html$|\.txt$ + - id: trailing-whitespace + - id: debug-statements + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.6 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] diff --git a/LICENSE b/LICENSE index 6b0b127..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -200,4 +200,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 15edf8a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -graft rdmo_plugins - -global-exclude __pycache__ -global-exclude *.py[co] - -include requirements.txt diff --git a/README.md b/README.md index 2ff99b8..b6b5fbf 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,48 @@ # rdmo-plugins - +[![Python Versions](https://img.shields.io/pypi/pyversions/rdmo.svg?style=flat)](https://www.python.org/) +[![Django Versions](https://img.shields.io/pypi/frameworkversions/django/rdmo)](https://pypi.python.org/pypi/rdmo/) +[![License](https://img.shields.io/github/license/rdmorganiser/rdmo?style=flat)](https://github.com/rdmorganiser/rdmo/blob/master/LICENSE) \ +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![CI Workflow Status](https://github.com/rdmorganiser/rdmo-plugins/actions/workflows/ci.yml/badge.svg)](https://github.com/rdmorganiser/rdmo-plugins/actions/workflows/ci.yml) + 1. [Synopsis](#synopsis) 2. [Setup](#setup) 3. [Other plugins](#other-plugins) - 1. [RDMO Sensor AWI optionset plugin](#rdmo-sensor-awi-optionset-plugin) + 1. [RDMO Sensor AWI optionset plugin](#rdmo-sensor-awi-optionset-plugin) + ## Synopsis Import and export plugins for [RDMO](https://github.com/rdmorganiser/rdmo). Included are plugins for [maDMP](https://github.com/RDA-DMP-Common/RDA-DMP-Common-Standard), [DataCite (Kernel 4.3)](https://schema.datacite.org/meta/kernel-4.3/), and the [Radar metadata schema](https://www.radar-service.eu/de/radar-schema). -**Since the RDMO questionaires and the domain does not contain all information needed for maDMP, DataCite, or Radar, the exports will not produce valid files. We will fix this in the future.** +**Since the RDMO questionnaires and the domain does not contain all information needed for maDMP, DataCite, or Radar, the exports will not produce valid files. We will fix this in the future.** Please visit the [RDMO documentation](https://rdmo.readthedocs.io/en/latest/plugins/index.html#project-export-plugins) for detailed information. -Please note that the re3data plugin was moved to a [seperate repository](https://github.com/rdmorganiser/rdmo-re3data). +Please note that the re3data plugin was moved to a [separate repository](https://github.com/rdmorganiser/rdmo-re3data). ## Setup Install the plugins in your RDMO virtual environment using pip (directly from GitHub): ```bash -pip install git+https://github.com/rdmorganiser/rdmo-plugins +python -m pip install git+https://github.com/rdmorganiser/rdmo-plugins ``` Add the `rdmo_plugins` to the `INSTALLED_APPS` in `config/settings/local.py`: ```python from . import INSTALLED_APPS -INSTALLED_APPS = ['rdmo_plugins'] + INSTALLED_APPS +INSTALLED_APPS = ["rdmo_plugins", *INSTALLED_APPS] ``` Add the export plugins to the `PROJECT_EXPORTS` in `config/settings/local.py`: ```python -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from . import PROJECT_EXPORTS PROJECT_EXPORTS += [ @@ -50,7 +57,7 @@ PROJECT_EXPORTS += [ Add the import plugins to the `PROJECT_IMPORTS` in `config/settings/local.py`: ```python -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from . import PROJECT_IMPORTS PROJECT_IMPORTS += [ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c6bc86 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,108 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools", "wheel"] + +[project] +name = "rdmo-plugins" +description = "Import and export plugins for RDMO." +readme = "README.md" +keywords = [ + "data management plan", + "dmp", + "rdmo", + "research data", + "research data management", +] +license = {text = "Apache-2.0"} +authors = [ + {name = "RDMO Arbeitsgemeinschaft", email = "rdmo-team@listserv.dfn.de"}, +] +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django :: 4.2", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dynamic = [ + "version", +] +dependencies = [ + "rdmo" +] + +[project.optional-dependencies] +ci = [ + "rdmo-plugins[dev]", +] +dev = [ + "pre-commit~=3.5", + "rdmo[allauth]", + "rdmo-plugins[pytest]", +] + +[project.urls] +documentation = "https://rdmo.readthedocs.io/en/latest/plugins/" +homepage = "https://rdmorganiser.github.io" +issues = "https://github.com/rdmorganiser/rdmo-plugins/issues" +repository = "https://github.com/rdmorganiser/rdmo-plugins.git" +slack = "https://rdmo.slack.com" + +[tool.setuptools.packages.find] +include = ["rdmo_plugins*"] + +[tool.setuptools.package-data] +"*" = ["*"] + +[tool.setuptools.dynamic] +version = {attr = "rdmo_plugins.__version__"} + +[tool.ruff] +target-version = "py38" +line-length = 120 +select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "PGH", # pygrep-hooks + "RUF", # ruff + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + "B006", # mutable-argument-default + "B007", # unused-loop-control-variable + "B018", # useless-expression + "E501", # line-too-long + "RUF012", # mutable-class-default +] + +[tool.ruff.isort] +known-first-party = ["rdmo_plugins"] +section-order = [ + "future", + "standard-library", + "pytest", + "django", + "rdmo", + "third-party", + "first-party", + "local-folder" +] + +[tool.ruff.isort.sections] +pytest = ["pytest"] +django = ["django"] +rdmo = ["rdmo"] diff --git a/rdmo_plugins/__init__.py b/rdmo_plugins/__init__.py index 26e8adb..5c4105c 100644 --- a/rdmo_plugins/__init__.py +++ b/rdmo_plugins/__init__.py @@ -1,8 +1 @@ -__title__ = 'rdmo-plugins' -__version__ = '1.0.0' -__author__ = 'Jochen Klar' -__email__ = 'mail@jochenklar.de' -__license__ = 'Apache-2.0' -__copyright__ = 'Copyright 2020 RDMO-Arbeitsgemeinschaft' - -VERSION = __version__ +__version__ = "1.0.1" diff --git a/rdmo_plugins/exports/datacite.py b/rdmo_plugins/exports/datacite.py index cce2d75..7449743 100644 --- a/rdmo_plugins/exports/datacite.py +++ b/rdmo_plugins/exports/datacite.py @@ -2,6 +2,7 @@ from collections import defaultdict from django.http import HttpResponse + from rdmo.core.exports import prettify_xml from rdmo.core.renderers import BaseXMLRenderer from rdmo.projects.exports import Export @@ -374,7 +375,7 @@ def get_datasets(self): 'title': self.get_text('project/dataset/title', set_index=set_index) or self.get_text('project/dataset/id', set_index=set_index) or - 'Dataset #{}'.format(set_index + 1) + f'Dataset #{set_index + 1}' }] # publisher diff --git a/rdmo_plugins/exports/madmp.py b/rdmo_plugins/exports/madmp.py index a162ce6..fd6bbd8 100644 --- a/rdmo_plugins/exports/madmp.py +++ b/rdmo_plugins/exports/madmp.py @@ -2,6 +2,7 @@ from collections import defaultdict from django.http import HttpResponse + from rdmo.projects.exports import Export @@ -137,7 +138,7 @@ def get_dmp(self): # dmp/title, dmp/created, dmp/modified, dmp/language dmp = defaultdict(list) dmp.update({ - 'title': 'maDMP for {}'.format(self.project.title), + 'title': f'maDMP for {self.project.title}', 'created': self.project.created.isoformat(), 'modified': self.project.updated.isoformat(), }) @@ -149,16 +150,16 @@ def get_dmp(self): # dmp/contributor for partner in self.get_set('project/partner/id'): - role = 'Contact person for {}'.format(partner.text) + role = f'Contact person for {partner.text}' contributor = self.get_person('project/partner/contact_person', set_index=partner.set_index, roles=[role]) if contributor: dmp['contributor'].append(contributor) for dataset in self.get_set('project/dataset/id'): for role, attribute in [ - ('Responsible for backup for {}'.format(dataset.text), 'project/dataset/data_security/backup_responsible'), - ('Responsible for metadata for {}'.format(dataset.text), 'project/dataset/metadata/responsible_person'), - ('Responsible for PIDs for {}'.format(dataset.text), 'project/dataset/pids/responsible_person'), + (f'Responsible for backup for {dataset.text}', 'project/dataset/data_security/backup_responsible'), + (f'Responsible for metadata for {dataset.text}', 'project/dataset/metadata/responsible_person'), + (f'Responsible for PIDs for {dataset.text}', 'project/dataset/pids/responsible_person'), ]: contributor = self.get_person(attribute, set_index=dataset.set_index, roles=[role]) if contributor: @@ -270,7 +271,7 @@ def get_cost(self, title, attribute): cost['value'] = 0 if value.unit: - cost['description'] = '{} in {}'.format(title, value.unit) + cost['description'] = f'{title} in {value.unit}' # this is a hack to make 'Euro' work if value.unit.upper()[:3] in self.currency_codes: @@ -418,6 +419,6 @@ def get_dataset(self, dataset): dmp_dataset['type'] = dmp_type # dmp/dataset/title - dmp_dataset['title'] = self.get_text('project/dataset/id', dataset.set_index) or 'Dataset #{}'.format(dataset.set_index + 1) + dmp_dataset['title'] = self.get_text('project/dataset/id', dataset.set_index) or f'Dataset #{dataset.set_index + 1}' return dmp_dataset diff --git a/rdmo_plugins/exports/radar/__init__.py b/rdmo_plugins/exports/radar/__init__.py index 5bbff37..0e351e7 100644 --- a/rdmo_plugins/exports/radar/__init__.py +++ b/rdmo_plugins/exports/radar/__init__.py @@ -1,2 +1,7 @@ from .exports import RadarExport from .providers import RadarExportProvider + +__all__ = [ + RadarExport, + RadarExportProvider +] diff --git a/rdmo_plugins/exports/radar/mixins.py b/rdmo_plugins/exports/radar/mixins.py index 1a8e306..9ae91c2 100644 --- a/rdmo_plugins/exports/radar/mixins.py +++ b/rdmo_plugins/exports/radar/mixins.py @@ -1,4 +1,4 @@ -class RadarMixin(object): +class RadarMixin: identifier_type_options = { 'identifier_type/doi': 'DOI', @@ -197,7 +197,7 @@ def get_dataset(self, set_index): dataset['title'] = \ self.get_text('project/dataset/title', set_index=set_index) or \ self.get_text('project/dataset/id', set_index=set_index) or \ - 'Dataset #{}'.format(set_index + 1) + f'Dataset #{set_index + 1}' # publisher publisher = \ @@ -255,7 +255,7 @@ def get_dataset(self, set_index): dataset['title'] = \ self.get_text('project/dataset/title', set_index=set_index) or \ self.get_text('project/dataset/id', set_index=set_index) or \ - 'Dataset #{}'.format(set_index + 1) + f'Dataset #{set_index + 1}' # alternate_identifiers alternate_identifier_sets = self.get_set('project/dataset/alternate_identifier/identifier', set_prefix=str(set_index)) @@ -394,7 +394,7 @@ def get_name(self, prefix, attribute, set_prefix='', set_index=0): name_text = self.get_text(attribute + '/name', set_prefix=set_prefix, set_index=set_index) if name_text: name = { - '{}Name'.format(prefix): name_text, + f'{prefix}Name': name_text, 'nameType': self.get_option(self.name_type_options, attribute + '/name_type', set_prefix=set_prefix, set_index=set_index, default='Personal'), } @@ -429,7 +429,7 @@ def get_name(self, prefix, attribute, set_prefix='', set_index=0): # affiliations affiliations = self.get_list(attribute + '/affiliation', set_prefix=set_prefix, set_index=set_index) if affiliations: - name['{}Affiliation'.format(prefix)] = affiliations[0] + name[f'{prefix}Affiliation'] = affiliations[0] return name else: diff --git a/rdmo_plugins/exports/radar/providers.py b/rdmo_plugins/exports/radar/providers.py index cc36b19..03e156c 100644 --- a/rdmo_plugins/exports/radar/providers.py +++ b/rdmo_plugins/exports/radar/providers.py @@ -3,8 +3,8 @@ from django import forms from django.conf import settings from django.shortcuts import redirect, render -from django.utils.translation import gettext_lazy as _ from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from rdmo.domain.models import Attribute from rdmo.projects.exports import Export @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs): for dataset, radar_url in zip(dataset_choices, radar_urls): set_index, label = dataset if radar_url is not None: - label += ' (Already exported to RADAR: {radar_url})'.format(radar_url=radar_url) + label += f' (Already exported to RADAR: {radar_url})' dataset_choices_with_radar_urls.append((set_index, mark_safe(label))) self.fields['dataset'].widget = forms.RadioSelect(choices=dataset_choices_with_radar_urls) @@ -85,7 +85,7 @@ def submit(self): return render(self.request, 'plugins/exports_radar.html', {'form': form}, status=200) def get_get_url(self): - return '{}/radar/api/workspaces'.format(self.radar_url) + return f'{self.radar_url}/radar/api/workspaces' def get_success(self, request, response): workspace_choices = [ @@ -97,7 +97,7 @@ def get_success(self, request, response): return redirect('project_export', self.get_from_session(request, 'project_id'), self.key) def get_post_url(self, workspace_id): - return '{}/radar/api/workspaces/{}/datasets'.format(self.radar_url, workspace_id) + return f'{self.radar_url}/radar/api/workspaces/{workspace_id}/datasets' def get_post_data(self, set_index): now = int(time.time()) @@ -125,9 +125,9 @@ def post_success(self, request, response): set_index = self.get_from_session(self.request, 'set_index') if request.LANGUAGE_CODE == 'de': - radar_url = '{}/radar/de/dataset/{}'.format(self.radar_url, radar_id) + radar_url = f'{self.radar_url}/radar/de/dataset/{radar_id}' else: - radar_url = '{}/radar/en/dataset/{}'.format(self.radar_url, radar_id) + radar_url = f'{self.radar_url}/radar/en/dataset/{radar_id}' try: attribute = Attribute.objects.get(path='project/dataset/radar_id') @@ -166,11 +166,11 @@ def radar_url(self): @property def authorize_url(self): - return '{}/radar-backend/oauth/authorize'.format(self.radar_url) + return f'{self.radar_url}/radar-backend/oauth/authorize' @property def token_url(self): - return '{}/radar-backend/oauth/token'.format(self.radar_url) + return f'{self.radar_url}/radar-backend/oauth/token' @property def client_id(self): diff --git a/rdmo_plugins/exports/zenodo.py b/rdmo_plugins/exports/zenodo.py index 9c30eec..3251984 100644 --- a/rdmo_plugins/exports/zenodo.py +++ b/rdmo_plugins/exports/zenodo.py @@ -2,7 +2,7 @@ from django import forms from django.conf import settings -from django.shortcuts import reverse, redirect, render +from django.shortcuts import redirect, render, reverse from django.utils.translation import gettext_lazy as _ from rdmo.projects.exports import Export @@ -73,15 +73,15 @@ def zenodo_url(self): @property def authorize_url(self): - return '{}/oauth/authorize'.format(self.zenodo_url) + return f'{self.zenodo_url}/oauth/authorize' @property def token_url(self): - return '{}/oauth/token'.format(self.zenodo_url) + return f'{self.zenodo_url}/oauth/token' @property def deposit_url(self): - return '{}/api/deposit/depositions'.format(self.zenodo_url) + return f'{self.zenodo_url}/api/deposit/depositions' @property def redirect_path(self): @@ -94,7 +94,7 @@ def get_post_data(self, set_index): title = \ self.get_text('project/dataset/title', set_index=set_index) or \ self.get_text('project/dataset/id', set_index=set_index) or \ - 'Dataset #{}'.format(set_index + 1) + f'Dataset #{set_index + 1}' description = self.get_text('project/dataset/description', set_index=set_index) diff --git a/rdmo_plugins/imports/datacite.py b/rdmo_plugins/imports/datacite.py index fa7b34c..bd96876 100644 --- a/rdmo_plugins/imports/datacite.py +++ b/rdmo_plugins/imports/datacite.py @@ -1,7 +1,8 @@ import mimetypes from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ + from rdmo.core.constants import VALUE_TYPE_DATETIME from rdmo.core.xml import get_ns_map, read_xml_file from rdmo.projects.imports import Import diff --git a/rdmo_plugins/imports/radar.py b/rdmo_plugins/imports/radar.py index fe673b9..b1e00b5 100644 --- a/rdmo_plugins/imports/radar.py +++ b/rdmo_plugins/imports/radar.py @@ -1,7 +1,8 @@ import mimetypes from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ + from rdmo.core.constants import VALUE_TYPE_DATETIME from rdmo.core.xml import get_ns_map, read_xml_file from rdmo.projects.imports import Import diff --git a/setup.py b/setup.py deleted file mode 100644 index 813dc53..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -import re - -from setuptools import find_packages, setup - -# get metadata from mudule using a regexp -with open('rdmo_plugins/__init__.py') as f: - metadata = dict(re.findall(r'__(.*)__ = [\']([^\']*)[\']', f.read())) - -setup( - name=metadata['title'], - version=metadata['version'], - author=metadata['author'], - author_email=metadata['email'], - maintainer=metadata['author'], - maintainer_email=metadata['email'], - license=metadata['license'], - url='https://github.com/rdmorganiser/rdmo-plugins', - description=u'Import and export plugins for RDMO.', - long_description=open('README.md').read(), - long_description_content_type='text/markdown', - classifiers=[ - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5' - ], - packages=find_packages(), - include_package_data=True -)