Skip to content
Snippets Groups Projects
Commit df7f25c2 authored by Mayr, Hannes's avatar Mayr, Hannes
Browse files

Merge branch 'main' into 'dev'

# Conflicts:
#   .gitlab-ci.yml
parents 103d2ed2 05e761b5
No related branches found
No related tags found
3 merge requests!41Include latest changes in main branch,!37Merge dev upstream changes into improve/metadata,!34Include architecture diagram in docs
Pipeline #824540 waiting for manual action
...@@ -2,8 +2,8 @@ image: python:latest ...@@ -2,8 +2,8 @@ image: python:latest
stages: stages:
- linting - linting
- testing - testing
- security
- docs - docs
- test
# Change pip's cache directory to be inside the project directory since we can # Change pip's cache directory to be inside the project directory since we can
# only cache local items. # only cache local items.
...@@ -12,7 +12,8 @@ variables: ...@@ -12,7 +12,8 @@ variables:
before_script: before_script:
- python --version # For debugging - python --version # For debugging
- pip install -r requirements.txt # install dependencies from file - pip install --upgrade pip
- pip install --user -r requirements.txt # install dependencies from file
PEP8: PEP8:
stage: linting stage: linting
script: script:
...@@ -46,9 +47,18 @@ pages: ...@@ -46,9 +47,18 @@ pages:
sast: sast:
variables: variables:
SAST_EXCLUDED_PATHS: spec, test, tmp SAST_EXCLUDED_PATHS: spec, test, tmp
stage: test stage: security
tags:
- docker
dependency_scanning:
stage: security
include: include:
- template: Security/SAST.gitlab-ci.yml - template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
# You can override the included template(s) by including variable overrides # You can override the included template(s) by including variable overrides
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
......
## Implement a new plot engine # Contributing
If you want to add another plot engine "$engine" to plotID, this document helps you to do it. Feel free to **report** all **issues** you encounter with plotID in our [issue tracker](https://git.rwth-aachen.de/plotid/plotid_python/-/issues). This will greatly help to improve plotID.
Contributing to plotID via **merge requests** is also highly appreciated. Please make sure that your code complies with the [PEP 8 - Style Guide](https://peps.python.org/pep-0008/) before creating a merge request. We try to have the whole code of plotID covered by unittests. So if you contribute new code to plotid please also provide unittests that cover the new functionality. Should you be interested in adding another plot engine to be supported by plotID, have a look at the section [Implement a new plot engine](#add-plot-engine). There are some hints how to proceed in this case.
## Setup development environment
Clone the repository and install the dependencies:
```bash
git clone https://git.rwth-aachen.de/plotid/plotid_python.git
cd plotid_python
pip install -r requirements.txt
```
Optionally, create a virtual environment as recommended in the [README.md](https://git.rwth-aachen.de/plotid/plotid_python/-/blob/main/README.md).
You can run all the unittests locally by calling the *tests/runner_tests.py* script. For linting we recommend using *flake8* and *pylint*.
The documentation is automatically built from docstrings in the code. So always document your code properly. The documentation can be built locally and can be found afterwards in *docs/build*:
```bash
cd docs
make html
```
## Implement a new plot engine {#add-plot-engine}
If you want to add another plot engine "$engine" to plotID, this section helps you to do it. For comparison have a look at already supported engines, e.g. in tagplot_matplotlib.py or tagplot_image.py.
### tagplot ### tagplot
Create a new module named "tagplot_$engine.py". Paste the following code and replace every "$engine" with the name of your engine: Create a new module named "tagplot_$engine.py". Paste the following code and replace every "$engine" with the name of your engine:
...@@ -8,11 +27,11 @@ Create a new module named "tagplot_$engine.py". Paste the following code and rep ...@@ -8,11 +27,11 @@ Create a new module named "tagplot_$engine.py". Paste the following code and rep
Tag your picture with an ID. Tag your picture with an ID.
Functions: Functions:
tagplot_$engine(PlotOptions instance) -> list tagplot_$engine(PlotOptions instance) -> PlotIDTransfer instance
""" """
import $engine import $engine
from plotid.create_id import create_id from plotid.create_id import create_id
from plotid.plotoptions import PlotOptions from plotid.plotoptions import PlotOptions, PlotIDTransfer
def tagplot_$engine(plotid_object): def tagplot_$engine(plotid_object):
...@@ -21,32 +40,44 @@ def tagplot_$engine(plotid_object): ...@@ -21,32 +40,44 @@ def tagplot_$engine(plotid_object):
The ID is placed visual on the figure window and returned as string in a The ID is placed visual on the figure window and returned as string in a
list together with the figures. list together with the figures.
TagPlot can tag multiple figures at once.
Parameters
----------
plotid_object : instance of PlotOptions
Returns
-------
PlotIDTransfer object
""" """
# Check if plotid_object is a valid instance of PlotOptions # Check if plotid_object is a valid instance of PlotOptions
if not isinstance(plotid_object, PlotOptions): if not isinstance(plotid_object, PlotOptions):
raise TypeError('The given options container is not an instance' raise TypeError('The given options container is not an instance'
'of PlotOptions.') 'of PlotOptions.')
ids_as_list = [] # Check if figs is a list of valid figures
for figure in plotid_object.figs:
if not isinstance(figure, $engine_figure_class):
raise TypeError('Figure is not a valid $engine-figure.')
# Loop to create and position the IDs
for fig in plotid_object.figs: for fig in plotid_object.figs:
figure_id = create_id(plotid_object.id_method) fig_id = create_id(plotid_object.id_method)
ids_as_list.append(figure_id) fig_id = plotid_object.prefix + fig_id
plotid_object.figure_ids.append(fig_id)
""" """
Insert here the tagging with $engine: Insert here the tagging with $engine:
Open the figure fig. Open the figure fig.
Place the string figure_id on it. Place the string figure_id on it.
Use plotid_object.position and plotid_object.rotation for position and rotation. Use plotid_object.position and plotid_object.rotation for position and rotation of the ID.
Save the tagged figure to plotid_object.figs. Save the tagged figure to plotid_object.figs.
""" """
figs_and_ids = PlotIDTransfer(plotid_object.figs, plotid_object.figure_ids)
return [plotid_object.figs, ids_as_list] return figs_and_ids
```
Last step: Last step:
Add the following code in tagplot.py: Add the following code in tagplot.py:
```python
match engine: match engine:
[...] [...]
case '$engine': case '$engine':
...@@ -60,7 +91,7 @@ To include a new plot engine in the publish function only save_plot.py has to be ...@@ -60,7 +91,7 @@ To include a new plot engine in the publish function only save_plot.py has to be
Import the plot engine at the top of the file. Import the plot engine at the top of the file.
In the beginning of the function save_plot() create the following line: In the beginning of the function save_plot() create the following line:
```python ```python
if isinstance(figures, $type_of_figure): if isinstance(figures, $engine_figure_class):
figures = [figures] figures = [figures]
[...] [...]
if not isinstance(figures, list): if not isinstance(figures, list):
...@@ -77,3 +108,5 @@ for i, fig in enumerate(figures): ...@@ -77,3 +108,5 @@ for i, fig in enumerate(figures):
plot_path.append(plot_names[i] + '.' + extension) plot_path.append(plot_names[i] + '.' + extension)
# Save the figure fig to plot_path[i] # Save the figure fig to plot_path[i]
``` ```
Additionally, please add some unittests for your code inside the *tests* directory.
In the end, you can also include a simple example in the *examples* directory how the newly plot engine can be used with plotID.
\ No newline at end of file
...@@ -8,7 +8,7 @@ plotID is a program connected to Research Data Management (RDM). It has two main ...@@ -8,7 +8,7 @@ plotID is a program connected to Research Data Management (RDM). It has two main
**Note:** To run plotID python version ≥ 3.10 is required. **Note:** To run plotID python version ≥ 3.10 is required.
## Installation ## Installation
Currently there are two options to run plotID. Either install it via pip from the Python Package Index (PyPi) or install plotID from the source code. Currently there are two options to run plotID. Either install it via pip from the Python Package Index (PyPi) or install plotID from the source code. Apart from setting up an optional virtual environment, installation is the same for Windows and Unix systems.
1. [Optional] Create a virtual environment and activate it: 1. [Optional] Create a virtual environment and activate it:
```bash ```bash
...@@ -99,6 +99,9 @@ If you want to build plotID yourself, follow these steps: ...@@ -99,6 +99,9 @@ If you want to build plotID yourself, follow these steps:
4. Build the package 4. Build the package
`python3 -m build` `python3 -m build`
## Contributing
Contributions to plotID are very welcome. If you encounter any issues with plotID please report them in our [issue tracker](https://git.rwth-aachen.de/plotid/plotid_python/-/issues). Code contributions via merge request are also highly appreciated. Please have a look at [CONTRIBUTING](https://git.rwth-aachen.de/plotid/plotid_python/-/blob/main/CONTRIBUTING.md) first.
## Documentation ## Documentation
If you have more questions about plotID, please have a look at the [documentation](https://plotid.pages.rwth-aachen.de/plotid_python). If you have more questions about plotID, please have a look at the [documentation](https://plotid.pages.rwth-aachen.de/plotid_python).
......
...@@ -46,6 +46,6 @@ FIGS_AND_IDS = tagplot(FIGS_AS_LIST, 'matplotlib', location='west', ...@@ -46,6 +46,6 @@ FIGS_AND_IDS = tagplot(FIGS_AS_LIST, 'matplotlib', location='west',
# %% Publish # %% Publish
publish(FIGS_AND_IDS, ['../README.md', '../docs', '../LICENSE'], publish(FIGS_AND_IDS, ['../README.md', '../docs', '../LICENSE'],
'./data', 'my_plot') 'data', 'my_plot')
# Required arguments: publish(output of tagplot(), list of files, # Required arguments: publish(output of tagplot(), list of files,
# path to destination folder, name(s) for the resulting images) # path to destination folder, name(s) for the resulting images)
...@@ -42,18 +42,11 @@ class PublishOptions: ...@@ -42,18 +42,11 @@ class PublishOptions:
self.figure = figs_and_ids.figs self.figure = figs_and_ids.figs
self.figure_ids = figs_and_ids.figure_ids self.figure_ids = figs_and_ids.figure_ids
self.src_datapaths = src_datapaths self.src_datapaths = src_datapaths
self.dst_path = dst_path self.dst_path = os.path.abspath(dst_path)
self.plot_names = plot_names self.plot_names = plot_names
self.data_storage = kwargs.get('data_storage', 'individual') self.data_storage = kwargs.get('data_storage', 'individual')
self.dst_path_head, self.dst_dirname = os.path.split(self.dst_path) self.dst_path_head, self.dst_dirname = os.path.split(self.dst_path)
# If the second string after os.path.split is empty,
# a trailing slash was given.
# To get the dir name correctly, split the first string again.
if not self.dst_dirname:
self.dst_path_head, self.dst_dirname = os.path.split(
self.dst_path_head)
def __str__(self): def __str__(self):
"""Representation if an object of this class is printed.""" """Representation if an object of this class is printed."""
return str(self.__class__) + ": " + str(self.__dict__) return str(self.__class__) + ": " + str(self.__dict__)
...@@ -109,7 +102,7 @@ class PublishOptions: ...@@ -109,7 +102,7 @@ class PublishOptions:
# Check if destination directory is allowed path # Check if destination directory is allowed path
if not os.path.exists(self.dst_path_head): if not os.path.exists(self.dst_path_head):
raise FileNotFoundError('The specified destination directory ' raise FileNotFoundError('The specified destination directory '
'does not exist.') f'{self.dst_path_head} does not exist.')
# Check if plot_name is a string or a list of strings # Check if plot_name is a string or a list of strings
if isinstance(self.plot_names, str): if isinstance(self.plot_names, str):
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Tag your picture with an ID. Tag your picture with an ID.
Functions: Functions:
tagplot_image(PlotOptions instance) -> list tagplot_image(PlotOptions instance) -> PlotIDTransfer instance
""" """
import os import os
from PIL import Image, ImageDraw, ImageFont, ImageOps from PIL import Image, ImageDraw, ImageFont, ImageOps
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Tag your matplotlib plot with an ID. Tag your matplotlib plot with an ID.
Functions: Functions:
tagplot_matplotlib(PlotOptions instance) -> list tagplot_matplotlib(PlotOptions instance) -> PlotIDTransfer instance
""" """
import matplotlib import matplotlib
......
...@@ -3,6 +3,7 @@ cycler==0.11.0 ...@@ -3,6 +3,7 @@ cycler==0.11.0
fonttools==4.32.0 fonttools==4.32.0
kiwisolver==1.4.2 kiwisolver==1.4.2
matplotlib==3.5.2 matplotlib==3.5.2
myst-parser==0.18.0
numpy==1.22.3 numpy==1.22.3
packaging==21.3 packaging==21.3
Pillow==9.1.0 Pillow==9.1.0
...@@ -10,3 +11,6 @@ pyparsing==3.0.8 ...@@ -10,3 +11,6 @@ pyparsing==3.0.8
python-dateutil==2.8.2 python-dateutil==2.8.2
qrcode==7.3.1 qrcode==7.3.1
six==1.16.0 six==1.16.0
Sphinx==5.0.2
sphinx-autoapi==1.8.4
sphinx-rtd-theme==1.0.0
...@@ -110,9 +110,13 @@ class TestPublish(unittest.TestCase): ...@@ -110,9 +110,13 @@ class TestPublish(unittest.TestCase):
publish(FIGS_AND_IDS, 4, DST_PATH, PIC_NAME) publish(FIGS_AND_IDS, 4, DST_PATH, PIC_NAME)
def test_dst_directory(self): def test_dst_directory(self):
""" Test if Error is raised when destination dir does not exist.""" """
Test if Error is raised when the directory above the
destination dir does not exist.
"""
with self.assertRaises(FileNotFoundError): with self.assertRaises(FileNotFoundError):
publish(FIGS_AND_IDS, SRC_DIR, 'not_existing_folder', PIC_NAME) publish(FIGS_AND_IDS, SRC_DIR, 'not_existing_folder/data',
PIC_NAME)
def test_script_exists(self): def test_script_exists(self):
""" """
...@@ -251,16 +255,13 @@ class TestPublish(unittest.TestCase): ...@@ -251,16 +255,13 @@ class TestPublish(unittest.TestCase):
""" """
self.maxDiff = None self.maxDiff = None
publish_obj = PublishOptions(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME) publish_obj = PublishOptions(FIGS_AND_IDS, SRC_DIR, DST_PATH, PIC_NAME)
self.assertEqual(str(publish_obj), self.assertIn(
"<class 'plotid.publish.PublishOptions'>: {'figure': " "<class 'plotid.publish.PublishOptions'>: {'figure': "
"[<Figure size 640x480 with 0 Axes>, <Figure size" "[<Figure size 640x480 with 0 Axes>, <Figure size"
" 640x480 with 0 Axes>], 'figure_ids': " " 640x480 with 0 Axes>], 'figure_ids': "
"['MR05_0x63203c6f', 'MR05_0x63203c70'], " "['MR05_0x63203c6f', 'MR05_0x63203c70'], "
"'src_datapaths': 'test_src_folder', 'dst_path': " "'src_datapaths': 'test_src_folder'",
"'test_parent/test_dst_folder', 'plot_names': " str(publish_obj))
"'test_picture', 'data_storage': 'individual', "
"'dst_path_head': 'test_parent', 'dst_dirname': "
"'test_dst_folder'}")
def tearDown(self): def tearDown(self):
""" Delete all files created in setUp. """ """ Delete all files created in setUp. """
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment