From e98eb8b7a430403effe9d4ddc819f7cba2f94087 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Tue, 15 Jul 2025 22:49:09 +0200 Subject: [PATCH 1/2] Add GitHub Actions workflows for testing and checks --- .github/workflows/codespell.yml | 18 ++++++++++++++++++ .github/workflows/docker.yml | 22 ++++++++++++++++++++++ .github/workflows/package.yml | 21 +++++++++++++++++++++ .github/workflows/ruff.yml | 23 +++++++++++++++++++++++ .github/workflows/tests.yml | 27 +++++++++++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 .github/workflows/codespell.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/package.yml create mode 100644 .github/workflows/ruff.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000..8d21fdd --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,18 @@ +name: Codespell + +on: + pull_request: + push: + branches: [main] + +jobs: + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install codespell + run: | + python -m pip install --upgrade pip + pip install codespell + - name: Run codespell + run: codespell --check-hidden -q 3 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..a03eb18 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,22 @@ +name: Docker Build + +on: + pull_request: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: qkay:test diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..00dbe26 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,21 @@ +name: Build Package + +on: + pull_request: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install build tool + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..18c1fba --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,23 @@ +name: Ruff Checks + +on: + pull_request: + push: + branches: [main] + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install ruff + run: | + python -m pip install --upgrade pip + pip install ruff + - name: Ruff check + run: ruff check . + - name: Ruff format + run: ruff format --diff . diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f7bded6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,27 @@ +name: Run Tests + +on: + pull_request: + push: + branches: [main] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest + - name: Run tests + env: + PYTHONPATH: ${{ github.workspace }} + run: pytest -v From 2516a2dcaa69eed927646ead010e3e62e1a135b7 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Tue, 15 Jul 2025 23:02:20 +0200 Subject: [PATCH 2/2] Add packaging config and fix lint issues --- docs/source/conf.py | 17 +++--- pyproject.toml | 30 ++++++++++ qkay/__init__.py | 1 + qkay/config.py | 9 +-- qkay/qkay.py | 121 ++++++++++++++++++++++++-------------- qkay/templates/index.html | 2 +- test/test_reshuffling.py | 2 +- 7 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 pyproject.toml create mode 100644 qkay/__init__.py diff --git a/docs/source/conf.py b/docs/source/conf.py index 75008c3..e63427d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,12 +17,12 @@ # -- Project information ----------------------------------------------------- -project = 'qkay' -copyright = 'The NiPreps Developers' -author = 'The NiPreps Developers' +project = "qkay" +copyright = "The NiPreps Developers" +author = "The NiPreps Developers" # The full version, including alpha/beta/rc tags -release = 'v.0.0.0' +release = "v.0.0.0" # -- General configuration --------------------------------------------------- @@ -30,11 +30,10 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ -] +extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -47,9 +46,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file +html_static_path = ["_static"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e6d07ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "qkay" +version = "0.1.0" +description = "Flask web application" +authors = [{name = "The NiPreps Developers", email = "nipreps@gmail.com"}] +license = {file = "LICENSE"} +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "beautifulsoup4==4.11.2", + "Flask==2.2.2", + "Flask_Login==0.6.2", + "flask_mongoengine==1.0.0", + "Flask_WTF==1.1.1", + "Jinja2==3.1.2", + "mongoengine==0.26.0", + "numpy", + "Werkzeug==2.2.2", + "WTForms==3.0.1", +] + +[tool.setuptools.packages.find] +where = ["."] + +[project.scripts] +qkay = "qkay.wsgi:app" diff --git a/qkay/__init__.py b/qkay/__init__.py new file mode 100644 index 0000000..372614c --- /dev/null +++ b/qkay/__init__.py @@ -0,0 +1 @@ +"""qkay package.""" diff --git a/qkay/config.py b/qkay/config.py index 5a304cb..7fad25f 100644 --- a/qkay/config.py +++ b/qkay/config.py @@ -30,6 +30,7 @@ # https://www.nipreps.org/community/licensing/ # """Utilities: Jinja2 templates.""" + from io import open # pylint: disable=W0622 @@ -65,15 +66,11 @@ class IndividualTemplate(Template): """Specific template for the individual report. From MRIQC""" def __init__(self): - super(IndividualTemplate, self).__init__( - "./templates/reports.html" - ) + super(IndividualTemplate, self).__init__("./templates/reports.html") class IndexTemplate(Template): """Specific template for the index file of the reports.""" def __init__(self): - super(IndexTemplate, self).__init__( - "./templates/index.html" - ) + super(IndexTemplate, self).__init__("./templates/index.html") diff --git a/qkay/qkay.py b/qkay/qkay.py index 59ab47c..8b6ccc5 100644 --- a/qkay/qkay.py +++ b/qkay/qkay.py @@ -178,35 +178,35 @@ def validate_dataset(self): class Inspection(db.Document): """ - Class to define an inspection - ... - - Attributes - ---------- - meta : dict - mongodb collection - dataset : str - name of the dataset to be inspected - username : str - name of the user assigned to the inspection - randomize : bool - If files must be shuffled - rate_all : bool - If all files must be rated - blind : bool - If reports must be anonymized - names_files : string list - list of filenames to grade - names_shuffled : string list - shuffled list of filenames - names_anonymized : string list - list of filenames anonymized and shuffled if randomize==True - names_subsample: string list - subsample of file to inspect if rate_all==False - random_seed: int - random seed used to shuffle the filename - index_rated_reports: list - index of reports already rated + Class to define an inspection + ... + + Attributes + ---------- + meta : dict + mongodb collection + dataset : str + name of the dataset to be inspected + username : str + name of the user assigned to the inspection + randomize : bool + If files must be shuffled + rate_all : bool + If all files must be rated + blind : bool + If reports must be anonymized + names_files : string list + list of filenames to grade + names_shuffled : string list + shuffled list of filenames + names_anonymized : string list + list of filenames anonymized and shuffled if randomize==True + names_subsample: string list + subsample of file to inspect if rate_all==False + random_seed: int + random seed used to shuffle the filename + index_rated_reports: list + index of reports already rated """ meta = {"collection": "inspections"} @@ -687,15 +687,25 @@ def display_report_non_anonymized(username, report_name): dataset = user.current_dataset dataset_path = str(Dataset.objects(name=dataset).values_list("path_dataset")[0]) - app.logger.debug("Searching recursively for a report named %s under %s.", report_name, dataset_path) + app.logger.debug( + "Searching recursively for a report named %s under %s.", + report_name, + dataset_path, + ) mriqc_report = "" - path_mriqc_report = glob.glob(os.path.join(dataset_path, "**", "sub-" + report_name), recursive=True) + path_mriqc_report = glob.glob( + os.path.join(dataset_path, "**", "sub-" + report_name), recursive=True + ) if len(path_mriqc_report) == 0: - app.logger.error("No report named %s was found in the children of %s.","sub-" + report_name, dataset_path) + app.logger.error( + "No report named %s was found in the children of %s.", + "sub-" + report_name, + dataset_path, + ) else: path_mriqc_report = path_mriqc_report[0] - # Modify the html to adapt it to Q'kay + # Modify the html to adapt it to Q'kay mriqc_report = modify_mriqc_report(path_mriqc_report) return render_template( @@ -757,7 +767,11 @@ def display_index_inspection(username, dataset): user.save() path_index = "./templates/index.html" - app.logger.debug("Searching for inspection matching dataset %s and username %s.", dataset, username) + app.logger.debug( + "Searching for inspection matching dataset %s and username %s.", + dataset, + username, + ) # Find in the inspection which reports have been rated current_inspection = Inspection.objects(Q(dataset=dataset) & Q(username=username)) @@ -816,28 +830,47 @@ def create_dataset(): selected_datasets = request.form.getlist("datasets[]") for d in selected_datasets: dataset_path = op.join("/datasets", d) - app.logger.debug("Searching recursively for dataset_description.json under %s.", dataset_path) + app.logger.debug( + "Searching recursively for dataset_description.json under %s.", + dataset_path, + ) # Get dataset name from the data_description.json file if it exists # otherwise, use the folder name desc_file = "" - desc_files = glob.glob(os.path.join(dataset_path, "**", "dataset_description.json"), recursive=True) + desc_files = glob.glob( + os.path.join(dataset_path, "**", "dataset_description.json"), + recursive=True, + ) if len(desc_files) > 1: - app.logger.warning("More than one dataset_description.json was found!: %s .", desc_files) - + app.logger.warning( + "More than one dataset_description.json was found!: %s .", + desc_files, + ) + desc_file = desc_files[0] - app.logger.debug("dataset_description.json found at %s.", desc_file) + app.logger.debug("dataset_description.json found at %s.", desc_file) if desc_file: with open(desc_file, "r") as file: data_description = json.load(file) dataset_name = data_description["Name"] - app.logger.info("The dataset name %s was assigned based on the name in %s", dataset_name, desc_file) + app.logger.info( + "The dataset name %s was assigned based on the name in %s", + dataset_name, + desc_file, + ) # If the name of the dataset is the default MRIQC value, use the folder name instead if dataset_name == "MRIQC - MRI Quality Control": - app.logger.info("The dataset name is the default of MRIQC which is not informative, using folder name instead: %s.", d) + app.logger.info( + "The dataset name is the default of MRIQC which is not informative, using folder name instead: %s.", + d, + ) dataset_name = d else: - app.logger.info("No dataset_description.json found, assigning dataset name to folder name: %s.", d) + app.logger.info( + "No dataset_description.json found, assigning dataset name to folder name: %s.", + d, + ) dataset_name = d dataset = Dataset(name=dataset_name, path_dataset=dataset_path) @@ -908,9 +941,7 @@ def assign_dataset(): ) names_files = list_individual_reports(dataset_path) - app.logger.debug( - "%s reports found at %s", len(names_files), dataset_path - ) + app.logger.debug("%s reports found at %s", len(names_files), dataset_path) new_names = names_files if repeat: names_repeated = repeat_reports(new_names, number_rep) diff --git a/qkay/templates/index.html b/qkay/templates/index.html index 5261bcc..e7f759c 100644 --- a/qkay/templates/index.html +++ b/qkay/templates/index.html @@ -110,7 +110,7 @@ // Check sender origin to be trusted console.log("message received") //if (event.origin !== "https://example.com") return; - if (event.srcElement.location.protocol != "file:") return; // Is this a security abberation? + if (event.srcElement.location.protocol != "file:") return; // Is this a security aberration? // I want to get any message that the rating is done on the iframe. var data = event.data; var keys = Object.keys(data) diff --git a/test/test_reshuffling.py b/test/test_reshuffling.py index 5f31473..ca8289e 100644 --- a/test/test_reshuffling.py +++ b/test/test_reshuffling.py @@ -21,6 +21,7 @@ # https://www.nipreps.org/community/licensing/ # """Unit test testing the ramspling functions in index.py.""" + from qkay.index import ( anonymize_reports, repeat_reports, @@ -65,4 +66,3 @@ def test_repeat_reports(): assert len(repeated_list) == len(original_list) + number_of_subjects_to_repeat assert all(item in repeated_list for item in original_list) -