From 75cecc04f6f9696f15f05a24aeca3aae3f12fdfe Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 17:01:57 +0200 Subject: [PATCH 01/12] Project import. (#1) * First commit (project import). * Added GitHub workflows. --- .coveragerc | 3 + .github/workflows/codecov.yml | 34 ++++ .github/workflows/mkdocs.yml | 32 ++++ .gitignore | 7 + LICENSE | 287 ++++++++++++++++++++++++++++++ README.md | 51 +++++- docs/guide.md | 151 ++++++++++++++++ media/logo.png | Bin 0 -> 78892 bytes pyproject.toml | 40 +++++ requirements.txt | Bin 0 -> 1358 bytes src/efsa_tools/__init__.py | 10 ++ src/efsa_tools/_utils/__init__.py | 1 + src/efsa_tools/_utils/_checks.py | 44 +++++ src/efsa_tools/dataframe_utils.py | 161 +++++++++++++++++ src/efsa_tools/scd.py | 152 ++++++++++++++++ tests/test__checks.py | 41 +++++ tests/test_dataframe_utils.py | 106 +++++++++++ tests/test_scd.py | 161 +++++++++++++++++ 18 files changed, 1279 insertions(+), 2 deletions(-) create mode 100644 .coveragerc create mode 100644 .github/workflows/codecov.yml create mode 100644 .github/workflows/mkdocs.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 docs/guide.md create mode 100644 media/logo.png create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/efsa_tools/__init__.py create mode 100644 src/efsa_tools/_utils/__init__.py create mode 100644 src/efsa_tools/_utils/_checks.py create mode 100644 src/efsa_tools/dataframe_utils.py create mode 100644 src/efsa_tools/scd.py create mode 100644 tests/test__checks.py create mode 100644 tests/test_dataframe_utils.py create mode 100644 tests/test_scd.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a7cba45 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + tests/* diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..38963da --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,34 @@ +name: Run tests and upload coverage report to Codecov + +on: + push: + branches: [dev, main] + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Python dependencies + run: | + pip install -r requirements.txt + pip install codecov + + - name: Run tests with coverage + run: | + coverage run -m pytest + coverage xml + + - name: Upload score to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml new file mode 100644 index 0000000..1e52b9f --- /dev/null +++ b/.github/workflows/mkdocs.yml @@ -0,0 +1,32 @@ +name: Build and deploy mkdocs site to GitHub Pages + +on: + push: + branches: [main] + +jobs: + mkdocs: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install docs dependencies + run: | + pip install mkdocs + + - name: Build mkdocs site + run: mkdocs build + + - name: Deploy site to gh-pages branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31910b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +*.egg-info/ +.env +__pycache__/ +.coverage +dist/ +coverage.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c29ce2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. \ No newline at end of file diff --git a/README.md b/README.md index a9dba23..c456433 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ -# efsa_tools -EFSA ensemble of data collections tools. +# efsa_tools + +[![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![codecov](https://codecov.io/gh/openefsa/efsa_tools/branch/main/graph/badge.svg?token=0YQIJKISMA)](https://codecov.io/gh/openefsa/efsa_tools) + +## Overview + +The **efsa_tools** package brings together all the functions developed for +EFSA's ad hoc data collections, providing tools for dataset operations as well +as utilities designed to preserve data history. + +The package is intended for researchers, analysts, and practitioners who +require convenient programmatic access to data collection utilities. + +## Installation + +### From PyPi + +``` +pip install efsa_tools +``` + +### Development version + +To install the latest development version: + +``` +pip install git+https://github.com/openefsa/efsa_tools.git +``` + +## Usage + +Once installed, load the package as usual: + +```python +from efsa_tools import * +``` + +Basic usage examples and full documentation are available in the package +[guide](docs/guide.md). + +## Authors and maintainers + +- **Lorenzo Copelli** (author, [ORCID](https://orcid.org/0009-0002-4305-065X)). +- **Luca Belmonte** (author, maintainer, [ORCID](https://orcid.org/0000-0002-7977-9170)). + +## Links + +- **Homepage**: [GitHub](https://github.com/openefsa/efsa_tools). +- **Bug Tracker**: [Issues on GitHub](https://github.com/openefsa/efsa_tools/issues). diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..df1382b --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,151 @@ +# Introduction to efsa_tools + +## Overview + +The **efsa_tools** package brings together all the functions developed for +EFSA's ad hoc data collections, providing tools for dataset operations as well +as utilities designed to preserve data history. + +The package is intended for researchers, analysts, and practitioners who +require convenient programmatic access to data collection utilities. + +## Installation + +### From PyPi + +``` +pip install efsa_tools +``` + +### Development version + +To install the latest development version: + +``` +pip install git+https://github.com/openefsa/efsa_tools.git +``` + +## Basic usage + +The main purpose of *efsa_tools* is to provide tools for managing datasets and +tracking data history within the context of data collections. + +Below are examples demonstrating how to use the functions in this package. +First, load the *efsa_tools* package: + +```python +from efsa_tools import * +``` + +To explore the arguments and usage of a specific function, you can run: + +```python +help(function_name) +``` + +This will show the full documentation for the function, including its +arguments, return values, and usage examples. + +For example, if you are working with the `SCD2()` function, you can check its +documentation with: + +```python +help(SCD2) +``` + +## Dropping empty rows and columns from a data frame + +If a data frame contain empty rows or columns, you can remove them using the +`drop_empty()` function, as follows: + +```python +iris_dropped = drop_empty(iris) + +print(iris_dropped.head()) +``` + +## Enriching a data frame with an EFSA's catalogue + +The `enrich()` function enables the augmentation of a data frame using +information stored in an EFSA's catalogue. It requires specifying the column +used to join the two datasets, as well as the name of the column that will +contain the enriched information (namely, the 'NAME' field of EFSA's +catalogues). + +```python +enriched_data_frame = enrich( + dataframe=dataframe_, + catalogue=CV_MTX_, + join_by="CODE", + enriched_column_name="enrichedColumn" +) + +print(enriched_data_frame.head()) +``` + +## Removing replicated columns from a data frame + +The `remove_replicated_columns()` function merges all the replicated columns in +a data frame into a single column whose name includes the "_deduplicated" +suffix. After the merge, the original replicated columns are removed from the +data frame. + +In the following example, we present a data frame containing the columns +*region_1*, *region_2*, ..., *region_n* with *n* > 100. Using the +`remove_replicated_columns()` function, these columns can be efficiently +consolidated into a single *region_deduplicated* column, assuming that for each +row only one of the *n* columns contains a meaningful (non-NA) value. + +```python +iris["Species_1"] = iris["Species"] +iris["Species_2"] = iris["Species"] +iris.drop(columns=["Species"], inplace=True) + +iris_deduplicated = remove_replicated_columns( + dataframe=iris, + prefix="Species_" +) + +print(iris_deduplicated.head()) +``` + +## Implementing a "Simple" Slowly Changing Dimension Type 2 (SSCD2) + +The `SSCD2()` function makes it possible to preserve data history when new data +becomes available by implementing a simplified version of Slowly Changing +Dimension Type 2. It marks all records in the current data frame as inactive +and appends the new data, flagging each newly added record as active. + +Unlike the `SCD2()` function, `SSCD2()` does not check which records have +actually changed. Instead, it marks all existing records as inactive and treats +all incoming records as new, setting the previous ones to inactive status even +if they are still included in the updated dataset. + +An example of how to use the function is provided below: + +```python +sscd2_dataframe = SSCD2(new_data=new_dataframe, current_data=old_dataframe) + +print(sscd2_dataframe.head())) +``` + +## Implementing a Slowly Changing Dimension Type 2 (SCD2) + +The `SCD2()` function makes it possible to preserve data history when new data +becomes available by implementing a Slowly Changing Dimension Type 2. It +compares the current records with the new ones, marking as inactive any +existing records that no longer appear in the updated dataset. Then, it flags +as active any new records that are not present among the currently active data. + +Unlike the `SSCD2()` function, `SCD2()` checks which records have actually +changed. It marks as inactive any existing records that no longer appear in the +updated dataset, and flags as active any new records that are not present among +the currently active data. + +An example of how to use the function is provided below: + +```python +scd2_dataframe = SCD2(new_data=new_dataframe, current_data=old_ataframe) + +print(scd2_dataframe.head()) +``` diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e82fde90d5a09edaa1cfde5b103baf13bf94e129 GIT binary patch literal 78892 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", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "efsa_tools" +version = "1.0.0" +description = "EFSA ensemble of data collections tools" +readme = "README.md" +authors = [ + { name = "Lorenzo Copelli" }, + { name = "Luca Belmonte" } +] +maintainers = [ + { name = "Luca Belmonte", email = "luca.belmonte@efsa.europa.eu" } +] +license = "EUPL-1.2" +license-files = ["LICENSE"] +requires-python = ">=3.11" +dependencies = [ + "numpy>=2.0", + "pandas>=2.2" +] + +[project.optional-dependencies] +dev = [ + "coverage>=7.6", + "pytest>=8.0", +] + +[project.urls] +"Homepage" = "https://github.com/openefsa/efsa_tools" +"Repository" = "https://github.com/openefsa/efsa_tools" +"Bug Tracker" = "https://github.com/openefsa/efsa_tools/issues" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3b0e2081605efe9076dcdf1514f0115ce44766db GIT binary patch literal 1358 zcmZvc?^5DG5X9%RRre@Z2GGL~zQetN6#@bhA{W9T_weekXLgg^pR$6L-JYKAp3Q!K zEbVMNt8HaZ{1tXOHnGOucssCG zQ}^3hR6R@V52@;neIZN73UT)j;+I#Kw&A}J-$4|KNg7u$RCtie8U>kU>I&!HX*Y5w z?M1)Z&^s$D`t=5`3f{(jZyX`N(jUpZy5G`KxBS+;pWJ^a$WH%{7ITN6cYf79G$~U# z`jh(wZw(zf^eC_3*Nlca?cutT;|^q2dk;ccg#yi@wY{fx0W|@uNgb(^g5Vm^E4sFH z%W3Ldgt`MNTKWY;oR6`x!e_YXJ2$o^x6Bi{$Yn!^L*HNNN<;KGqP9Yi>ELHHE}J{rd~PKIGajUlPT#S zchCN&lr$wh!p}$NMyo&35xRpfxQ3JdH)evenD>&pnm4g34fzetE#FPWOdf4P?%CgX zU*Fd@Rch|)H}lgUcgPusxlia?ct!)qiA>)6-o@ywcPX#9FEn>~3S}L)QRE-recWgI eR-${Hdfi$}R>> from efsa_tools import * + + >>> iris.iloc[0] = np.nan + >>> iris = drop_empty(dataframe=iris) + + >>> iris["Species"] = np.nan + >>> iris = drop_empty(dataframe=iris) + """ + + _checks._require_type(value=dataframe, expected_type=pd.DataFrame) + + dataframe = dataframe[dataframe.isna().sum(axis=1) != dataframe.shape[1]] + dataframe = dataframe.loc[:, ~dataframe.isna().all()] + + dataframe = dataframe.convert_dtypes() + dataframe = dataframe.astype(str) + + return dataframe + + +def remove_replicated_columns(dataframe, prefix): + """Drop and merge replicated columns from the specified data frame. + + This function drops and merges all the replicated columns from the + specified data frame. All the occurrences of "NA", "N/a", and empty strings + (case-insensitive) inside the provided data frame are replaced with NAs of + type character. Then, all and only the columns starting with the specified + prefix are selected and united into a single column with name ending per + "_deduplicated". All empty entries in the new deduplicated column are + replaced with NAs. Finally, the new column is bound with the other columns + of the initial dataframe. + + Args: + dataframe (pandas.DataFrame): The data frame from which to drop the + replicated columns. + prefix (str): The prefix with which the name of the replicated columns + starts. + + Returns: + (pandas.DataFrame): The specified data frame with an additional + deduplicated column and all the types transformed to string. + + Examples: + >>> from efsa_tools import * + + >>> iris["Species_1"] = iris["Species"] + >>> iris["Species_2"] = iris["Species"] + >>> iris.drop("Species", axis=1, inplace=True) + + >>> deduplicated_dataframe_ = remove_replicated_columns( + ... dataframe=iris, + ... prefix="Species_" + ... ) + """ + + _checks._require_type(value=dataframe, expected_type=pd.DataFrame) + _checks._require_type(value=prefix, expected_type=str) + + columns_ = [column_ for column_ in dataframe.columns + if column_.startswith(prefix)] + + dataframe[columns_] = dataframe[columns_].astype(str) + + dataframe[columns_] = dataframe[columns_].replace( + r"(?i)^\s*(n/a|na)?\s*$", np.nan, regex=True) + + deduplicated_column_name = f"{prefix}_deduplicated" + + dataframe[deduplicated_column_name] = dataframe[columns_].apply( + lambda row_: ''.join(row_.dropna()), axis=1) + + dataframe[deduplicated_column_name] = ( + dataframe[deduplicated_column_name].replace('', np.nan)) + + deduplicated_column = dataframe.pop(deduplicated_column_name) + dataframe.insert(0, deduplicated_column_name, deduplicated_column) + + dataframe = dataframe.drop(columns=columns_) + + dataframe = dataframe.convert_dtypes() + dataframe = dataframe.astype(str) + + return dataframe + + +def enrich(dataframe, catalogue, join_by, enriched_column_name): + """Enrich a data frame with an EFSA catalogue. + + This function takes a data frame and joins it with an EFSA catalog. The + EFSA catalog must be itself a data frame. + + Args: + dataframe (pandas.DataFrame): The data frame to be enriched. + catalogue (pandas.DataFrame): The data frame that contains the EFSA + catalogue to be used for the enrichment. It must contain at least + two columns, namely: NAME and CODE. + join_by (str): The variable to be used as the join key. + enriched_column_name (str): The name of the column added to the + original data. + + Returns: + pandas.DataFrame: The specified data frame enriched with the catalogue + data. + + Examples: + >>> from efsa_tools import * + + >>> # Assuming that 'dataframe' and 'catalogue' are already set up as + >>> # Pandas DataFrames. + >>> enriched_data = enrich( + ... dataframe=dataframe, + ... catalogue=catalogue, + ... join_by="CODE", + ... enriched_column_name="enrichedColumn" + ... ) + """ + + _checks._require_type(value=dataframe, expected_type=pd.DataFrame) + _checks._require_type(value=catalogue, expected_type=pd.DataFrame) + _checks._must_include(dataframe=catalogue, names=["NAME", "CODE"]) + _checks._require_type(value=join_by, expected_type=str) + _checks._require_type(value=enriched_column_name, expected_type=str) + + catalogue = catalogue[["NAME", "CODE"]].copy() + catalogue[join_by] = catalogue["CODE"] + + enriched_ = dataframe.merge(catalogue, how="left", on=join_by) + enriched_ = enriched_.rename(columns={"NAME": enriched_column_name}) + + columns_order_ = ( + [enriched_column_name] + + [column_ for column_ in dataframe.columns] + ) + enriched_ = enriched_[columns_order_] + + return enriched_ diff --git a/src/efsa_tools/scd.py b/src/efsa_tools/scd.py new file mode 100644 index 0000000..c11a6e8 --- /dev/null +++ b/src/efsa_tools/scd.py @@ -0,0 +1,152 @@ +import pandas as pd +import datetime + +from efsa_tools._utils import _checks + + +def _activate(dataframe): + """Activate the records of the specified data frame. + + This helper function is used in the context of a Slowly Changing Dimension + (SCD) to mark new records of a data frame as active with a start date. + + Args: + dataframe (pandas.DataFrame): The data frame to activate. + + Returns: + pandas.DataFrame: The specified data frame with ACTIVE set to True, + START_DATE set to current time, and END_DATE set to NaT. + """ + + _checks._require_type(value=dataframe, expected_type=pd.DataFrame) + + activated_dataframe_ = dataframe.assign( + IS_ACTIVE=True, + START_DATE=datetime.datetime.now(), + END_DATE=pd.NaT) + + return activated_dataframe_ + + +def _deactivate(dataframe): + """Deactivate the records of the specified data frame. + + This helper function is used in the context of a Slowly Changing Dimension + (SCD) to mark active records of a data frame as not active with an end + date. + + Args: + dataframe (pandas.DataFrame): The data frame to deactivate. + + Returns: + pandas.DataFrame: The specified data frame with IS_ACTIVE set to False + and END_DATE set to current time. + """ + + _checks._require_type(value=dataframe, expected_type=pd.DataFrame) + + deactivated_dataframe_ = dataframe.assign( + IS_ACTIVE=False, + END_DATE=datetime.datetime.now()) + + return deactivated_dataframe_ + + +def sscd2(new_data, current_data): + """Implement a "Simple" Slowly Changing Dimension Type 2. + + This function implements a Simplified version of Slowly Changing Dimension + Type 2 to merge new and current data while maintaining historical records. + The function deactivates all the old records and activates new ones, + ensuring a history-preserving update strategy. The difference between a + standard SCD2 is that this simplified version applies no checks on the + data, deactivating all the old records and activating the new ones, even if + some of the old records are still active. + + Args: + new_data (pandas.DataFrame): The data frame containing new records. + current_data (pandas.DataFrame): The data frame containing existing + records. + + Returns: + pandas.DataFrame: A combined data frame with all old data marked as not + active and new data marked as active. + + Examples: + >>> from efsa_tools import * + + >>> merged_data = sscd2(new_data=new_dataset, current_data=old_dataset) + """ + + _checks._require_type(value=new_data, expected_type=pd.DataFrame) + _checks._require_type(value=current_data, expected_type=pd.DataFrame) + + merged_data_ = pd.concat([ + _deactivate(dataframe=current_data), + _activate(dataframe=new_data) + ], ignore_index=True) + + return merged_data_ + + +def scd2(new_data, current_data, key=None): + """Implement a Slowly Changing Dimension Type 2. + + This function implements a Slowly Changing Dimension Type 2 to merge new + and current data while maintaining historical records. The function + deactivates all the old records and activates new ones, ensuring a + history-preserving update strategy. Only the changing records are marked as + not active and replaced by new active ones. + + Args: + new_data (pandas.DataFrame): The data frame containing new records. + current_data (pandas.DataFrame): The data frame containing existing + records. + key (list, optional): The columns to be used as key. Defaults to None. + + Returns: + pandas.DataFrame: A combined data frame with old data marked as not + active and new data marked as active. + + Examples: + >>> from efsa_tools import * + + >>> merged_data = scd2(new_data=new_dataset, current_data=old_dataset) + """ + + _checks._require_type(value=new_data, expected_type=pd.DataFrame) + _checks._require_type(value=current_data, expected_type=pd.DataFrame) + if key is not None: + _checks._require_type(value=key, expected_type=list) + + if key is None: + key = list(new_data.columns) + + current_inactive_data_ = current_data[current_data["IS_ACTIVE"] == False] + current_active_data_ = current_data[current_data["IS_ACTIVE"] == True] + + still_active_data_ = ( + current_active_data_.drop_duplicates() + .merge(new_data.drop_duplicates(), on=key, how="inner") + ) + + newly_active_data_ = new_data.merge( + current_active_data_[key], on=key, how="left", indicator=True) + newly_active_data_ = newly_active_data_[ + newly_active_data_["_merge"] == "left_only"].drop(columns="_merge") + newly_active_data_ = _activate(newly_active_data_) + + deactivated_data_ = current_active_data_.merge( + new_data[key], on=key, how="left", indicator=True) + deactivated_data_ = deactivated_data_[ + deactivated_data_["_merge"] == "left_only"].drop(columns="_merge") + deactivated_data_ = _deactivate(deactivated_data_) + + merged_data_ = pd.concat([ + still_active_data_, + newly_active_data_, + deactivated_data_, + current_inactive_data_ + ], ignore_index=True) + + return merged_data_ diff --git a/tests/test__checks.py b/tests/test__checks.py new file mode 100644 index 0000000..0ec5ab1 --- /dev/null +++ b/tests/test__checks.py @@ -0,0 +1,41 @@ +import unittest +import pandas as pd + +from efsa_tools._utils._checks import _require_type, _must_include + + +class TestChecks(unittest.TestCase): + + ################### + # _require_type() # + ################### + + def test__require_type_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, _require_type, value=123, + expected_type=str) + + def test__require_type_output(self): + """Test the behaviour for valid data.""" + self.assertIsNone(_require_type(value=123, expected_type=int)) + + ################### + # _must_include() # + ################### + + def test__must_include_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, _must_include, dataframe=123, names=[]) + self.assertRaises(TypeError, _must_include, dataframe=pd.DataFrame(), + names=123) + + def test__must_include_output(self): + """Test the behaviour for valid data.""" + self.assertIsNone(_must_include( + dataframe=pd.DataFrame({'a': [1], 'b': [2]}), + names=['a'])) + + def test__must_include_wrong(self): + """Test the behaviour for valid data.""" + self.assertRaises(ValueError, _must_include, + dataframe=pd.DataFrame({'a': [1], 'b': [2]}), names=['c']) diff --git a/tests/test_dataframe_utils.py b/tests/test_dataframe_utils.py new file mode 100644 index 0000000..7c1bae4 --- /dev/null +++ b/tests/test_dataframe_utils.py @@ -0,0 +1,106 @@ +import unittest +import pandas as pd +import numpy as np +from pandas.testing import assert_frame_equal + +from efsa_tools.dataframe_utils import (drop_empty, remove_replicated_columns, + enrich) + + +class TestDataframeUtils(unittest.TestCase): + + ################ + # drop_empty() # + ################ + + def test_drop_empty_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, drop_empty, dataframe=123) + + def test_drop_empty_output(self): + """Test the behaviour for valid data.""" + dataframe_ = pd.DataFrame({ + 'a': [1, 2, np.nan], + 'b': [3, 4, np.nan], + 'c': [np.nan, np.nan, np.nan] + }) + expected_result_ = pd.DataFrame({ + 'a': ['1', '2'], + 'b': ['3', '4'] + }) + result_ = drop_empty(dataframe=dataframe_) + self.assertIsInstance(result_, pd.DataFrame) + assert_frame_equal(result_, expected_result_) + + ############################### + # remove_replicated_columns() # + ############################### + + def test_remove_replicated_columns_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, remove_replicated_columns, dataframe=123, + prefix="") + self.assertRaises(TypeError, remove_replicated_columns, + dataframe=pd.DataFrame(), prefix=123) + + def test_remove_replicated_columns_output(self): + """Test the behaviour for valid data.""" + dataframe_ = pd.DataFrame({ + "prefix_1": [1, "NA", np.nan], + "prefix_2": [np.nan, 2, "N/a"], + "prefix_3": ['', np.nan, 3], + "another_col": [np.nan, np.nan, 3] + }) + expected_result_ = pd.DataFrame({ + "prefix_deduplicated": ['1', '2', '3'], + "another_col": [np.nan, np.nan, '3'] + }) + result_ = remove_replicated_columns( + dataframe=dataframe_, + prefix="prefix" + ) + self.assertIsInstance(result_, pd.DataFrame) + assert_frame_equal(result_, expected_result_) + + ############ + # enrich() # + ############ + + def test_enrich_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, enrich, dataframe=123, + catalogue=pd.DataFrame(), join_by="", + enriched_column_name="") + self.assertRaises(TypeError, enrich, dataframe=pd.DataFrame(), + catalogue=123, join_by="", enriched_column_name="") + self.assertRaises(ValueError, enrich, dataframe=pd.DataFrame(), + catalogue=pd.DataFrame(), join_by="", + enriched_column_name="") + self.assertRaises(TypeError, enrich, dataframe=pd.DataFrame(), + catalogue=pd.DataFrame({"NAME": [1], "CODE": [2]}), + join_by=123, enriched_column_name="") + self.assertRaises(TypeError, enrich, dataframe=pd.DataFrame(), + catalogue=pd.DataFrame({"NAME": [1], "CODE": [2]}), + join_by="", enriched_column_name=123) + + def test_enrich_output(self): + """Test the behaviour for valid data.""" + dataframe_ = pd.DataFrame({ + "CODE": [1, 2, 3] + }) + catalogue_ = pd.DataFrame({ + "CODE": [1, 2, 3], + "NAME": ['a', 'b', 'c'] + }) + expected_result_ = pd.DataFrame({ + "enriched_column": ['a', 'b', 'c'], + "CODE": [1, 2, 3] + }) + result_ = enrich( + dataframe=dataframe_, + catalogue=catalogue_, + join_by="CODE", + enriched_column_name="enriched_column" + ) + self.assertIsInstance(result_, pd.DataFrame) + assert_frame_equal(result_, expected_result_) diff --git a/tests/test_scd.py b/tests/test_scd.py new file mode 100644 index 0000000..611bd5e --- /dev/null +++ b/tests/test_scd.py @@ -0,0 +1,161 @@ +import unittest +import pandas as pd + +from efsa_tools.scd import _activate, _deactivate, sscd2, scd2 + + +class TestSCD2(unittest.TestCase): + + ############### + # _activate() # + ############### + + def test__activate_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, _activate, dataframe=123) + + def test__activate_output_1(self): + """Test the behaviour for valid data.""" + dataframe_ = pd.DataFrame({'a': [1]}) + self.assertIsInstance(_activate(dataframe=dataframe_), pd.DataFrame) + + def test__activate_output_2(self): + """Test the presence of the START_DATE column.""" + dataframe_ = pd.DataFrame({'a': [1]}) + dataframe_ = _activate(dataframe=dataframe_) + self.assertTrue("START_DATE" in dataframe_.columns) + self.assertTrue(all(dataframe_["START_DATE"].notnull())) + + def test__activate_output_3(self): + """Test the presence of the END_DATE column.""" + dataframe_ = pd.DataFrame({'a': [1]}) + dataframe_ = _activate(dataframe=dataframe_) + self.assertTrue("END_DATE" in dataframe_.columns) + self.assertTrue(all(dataframe_["END_DATE"].isnull())) + + def test__activate_output_4(self): + """Test the presence of the IS_ACTIVE column.""" + dataframe_ = pd.DataFrame({'a': [1]}) + dataframe_ = _activate(dataframe=dataframe_) + self.assertTrue("IS_ACTIVE" in dataframe_.columns) + self.assertTrue(all(dataframe_["IS_ACTIVE"])) + + ################# + # _deactivate() # + ################# + + def test__deactivate_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, _deactivate, dataframe=123) + + def test__deactivate_output_1(self): + """Test the behaviour for valid data.""" + dataframe_ = pd.DataFrame({'a': [1]}) + dataframe_ = _activate(dataframe=dataframe_) + self.assertIsInstance(_deactivate(dataframe=dataframe_), pd.DataFrame) + + def test__deactivate_output_2(self): + """Test the presence of the END_DATE column.""" + dataframe_ = pd.DataFrame({'a': [1]}) + dataframe_ = _activate(dataframe=dataframe_) + dataframe_ = _deactivate(dataframe=dataframe_) + self.assertTrue("END_DATE" in dataframe_.columns) + self.assertTrue(all(dataframe_["END_DATE"].notnull())) + + def test__deactivate_output_3(self): + """Test the presence of the IS_ACTIVE column.""" + dataframe_ = pd.DataFrame({'a': [1]}) + dataframe_ = _activate(dataframe=dataframe_) + dataframe_ = _deactivate(dataframe=dataframe_) + self.assertTrue("IS_ACTIVE" in dataframe_.columns) + self.assertTrue(not all(dataframe_["IS_ACTIVE"])) + + ############ + # _sscd2() # + ############ + + def test_sscd2_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, sscd2, new_data=123, + current_data=pd.DataFrame()) + self.assertRaises(TypeError, sscd2, new_data=pd.DataFrame(), + current_data=123) + + def test_sscd2_output(self): + """The function must output as expected.""" + current_data_ = pd.DataFrame({ + "id": [1, 2, 3], + "colA": ["a1", "a2", "a3"], + "colB": ["b1", "b2", "b3"], + "colC": ["c1", "c2", "c3"] + }) + current_data_ = _activate(dataframe=current_data_) + + new_data_ = pd.DataFrame({ + "id": [1, 2, 3], + "colA": ["a1", "a2", "a4"], + "colB": ["b1", "b2", "b4"], + "colC": ["c1", "c20", "c4"] + }) + + merged_data_ = sscd2(new_data=new_data_, current_data=current_data_) + + expected_result_ = pd.DataFrame({ + "id": [1, 2, 3, 1, 2, 3], + "colA": ["a1", "a2", "a3", "a1", "a2", "a4"], + "colB": ["b1", "b2", "b3", "b1", "b2", "b4"], + "colC": ["c1", "c2", "c3", "c1", "c20", "c4"], + "IS_ACTIVE": [False, False, False, True, True, True] + }).sort_values("id") + + merged_data_ = merged_data_.drop( + columns=merged_data_.filter( + regex="DATE$").columns).sort_values("id") + + self.assertTrue(merged_data_.equals(expected_result_)) + + ########### + # _scd2() # + ########### + + def test_scd2_invalid(self): + """Test the behaviour for invalid data.""" + self.assertRaises(TypeError, scd2, new_data=123, + current_data=pd.DataFrame()) + self.assertRaises(TypeError, scd2, new_data=pd.DataFrame(), + current_data=123) + self.assertRaises(TypeError, scd2, new_data=pd.DataFrame(), + current_data=pd.DataFrame(), key=123) + + def test_scd2_output(self): + """The function must output as expected.""" + current_data_ = pd.DataFrame({ + "id": [1, 2, 3], + "colA": ["a1", "a2", "a3"], + "colB": ["b1", "b2", "b3"], + "colC": ["c1", "c2", "c3"] + }) + current_data_ = _activate(dataframe=current_data_) + + new_data_ = pd.DataFrame({ + "id": [1, 2, 3], + "colA": ["a1", "a2", "a4"], + "colB": ["b1", "b2", "b4"], + "colC": ["c1", "c20", "c4"] + }) + + merged_data_ = scd2(new_data=new_data_, current_data=current_data_) + + expected_result_ = pd.DataFrame({ + "id": [1, 2, 3, 2, 3], + "colA": ["a1", "a2", "a4", "a2", "a3"], + "colB": ["b1", "b2", "b4", "b2", "b3"], + "colC": ["c1", "c20", "c4", "c2", "c3"], + "IS_ACTIVE": [True, True, True, False, False] + }).sort_values("id") + + merged_data_ = merged_data_.drop( + columns=merged_data_.filter( + regex="DATE$").columns).sort_values("id") + + self.assertTrue(merged_data_.equals(expected_result_)) From 37744464ce48c4b93b3fa1fa4f219568e800a9ee Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 17:06:35 +0200 Subject: [PATCH 02/12] Fixed codecov.yml. (#2) --- .github/workflows/codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 38963da..5e7cecb 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -21,6 +21,7 @@ jobs: run: | pip install -r requirements.txt pip install codecov + pip install -e . - name: Run tests with coverage run: | From 2c1c63b30b1c1f4fab849560ca571e20356c247a Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 17:23:54 +0200 Subject: [PATCH 03/12] Fixed mkdocs configuration and action. (#4) --- .github/workflows/mkdocs.yml | 4 ++++ mkdocs.yml | 9 +++++++++ 2 files changed, 13 insertions(+) create mode 100644 mkdocs.yml diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 1e52b9f..3d47365 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -22,6 +22,10 @@ jobs: run: | pip install mkdocs + - name: Generate index.md from README.md + run: | + cp README.md docs/index.md + - name: Build mkdocs site run: mkdocs build diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..3d50c7f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,9 @@ +site_name: efsa_tools +site_description: EFSA ensemble of data collections tools +site_url: https://openefsa.github.io/efsa_tools +repo_url: https://github.com/openefsa/efsa_tools +docs_dir: docs +site_dir: site + +theme: + name: material From 0054782102d72f6792f9eff4ce987e547d49516b Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 17:30:41 +0200 Subject: [PATCH 04/12] Fixed mkdocs theme. (#6) --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3d50c7f..e8fa938 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,4 +6,4 @@ docs_dir: docs site_dir: site theme: - name: material + name: mkdocs From 3a8f1646a7ef40958e5860dcad560cbbd6442b40 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 17:54:42 +0200 Subject: [PATCH 05/12] Fixed mkdocs configuration. (#8) --- .github/workflows/mkdocs.yml | 2 ++ mkdocs.yml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 3d47365..2b7e89c 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -25,6 +25,8 @@ jobs: - name: Generate index.md from README.md run: | cp README.md docs/index.md + mkdir docs/media + cp media/logo.png docs/media/logo.png - name: Build mkdocs site run: mkdocs build diff --git a/mkdocs.yml b/mkdocs.yml index e8fa938..f3efe57 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,3 +7,7 @@ site_dir: site theme: name: mkdocs + +nav: + Home: 'index.md' + 'Get started': 'guide.md' From 0c5d2b9eeb8db50b4e0a236536d8878008e68ac5 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 17:58:49 +0200 Subject: [PATCH 06/12] Minor fixes. (#10) --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index f3efe57..20cb5a8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,5 +9,5 @@ theme: name: mkdocs nav: - Home: 'index.md' - 'Get started': 'guide.md' + - Home: 'index.md' + - 'Get started': 'guide.md' From 8ccdd08d4de67bb7f9a0558f107e084dba4d2c65 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 7 Apr 2026 18:14:02 +0200 Subject: [PATCH 07/12] Fixed links in mkdocs. (#12) --- .github/workflows/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 2b7e89c..b40ad50 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -25,6 +25,7 @@ jobs: - name: Generate index.md from README.md run: | cp README.md docs/index.md + sed -i 's|docs/guide.md|guide/|g' docs/index.md mkdir docs/media cp media/logo.png docs/media/logo.png From 30d468c2321d719d1448b0029c3a13ef8fbbea63 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 14 Apr 2026 15:22:26 +0200 Subject: [PATCH 08/12] Minor fixes. (#14) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c456433..95daf50 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# efsa_tools +# efsa_tools [![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![codecov](https://codecov.io/gh/openefsa/efsa_tools/branch/main/graph/badge.svg?token=0YQIJKISMA)](https://codecov.io/gh/openefsa/efsa_tools) From bef07a41a92045a5508f5075066943cc57f8f8e4 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Tue, 14 Apr 2026 16:48:31 +0200 Subject: [PATCH 09/12] Minor fixes. (#16) --- .github/workflows/mkdocs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index b40ad50..fac16a9 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -26,8 +26,6 @@ jobs: run: | cp README.md docs/index.md sed -i 's|docs/guide.md|guide/|g' docs/index.md - mkdir docs/media - cp media/logo.png docs/media/logo.png - name: Build mkdocs site run: mkdocs build From 3afd9aedd486dcde1104441964bac22c4b71ef1d Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Wed, 15 Apr 2026 09:58:17 +0200 Subject: [PATCH 10/12] Fixed pyproject.toml. (#18) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4dcba3d..d40afbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dev = [ ] [project.urls] -"Homepage" = "https://github.com/openefsa/efsa_tools" +"Homepage" = "https://openefsa.github.io/efsa_tools/" "Repository" = "https://github.com/openefsa/efsa_tools" "Bug Tracker" = "https://github.com/openefsa/efsa_tools/issues" From e48bfc3fbb9476a70d7b451b02c36b88043b22a4 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli Date: Wed, 15 Apr 2026 16:12:56 +0200 Subject: [PATCH 11/12] Minor fixes. (#20) --- .github/workflows/mkdocs.yml | 1 - README.md | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index fac16a9..3d47365 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -25,7 +25,6 @@ jobs: - name: Generate index.md from README.md run: | cp README.md docs/index.md - sed -i 's|docs/guide.md|guide/|g' docs/index.md - name: Build mkdocs site run: mkdocs build diff --git a/README.md b/README.md index 95daf50..cbe96f3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ from efsa_tools import * ``` Basic usage examples and full documentation are available in the package -[guide](docs/guide.md). +[guide](https://openefsa.github.io/efsa_tools/guide/). ## Authors and maintainers @@ -45,5 +45,5 @@ Basic usage examples and full documentation are available in the package ## Links -- **Homepage**: [GitHub](https://github.com/openefsa/efsa_tools). -- **Bug Tracker**: [Issues on GitHub](https://github.com/openefsa/efsa_tools/issues). +- **Source code**: [GitHub - openefsa/efsa_tools](https://github.com/openefsa/efsa_tools). +- **Bug tracker**: [Issues on GitHub](https://github.com/openefsa/efsa_tools/issues). From 820e7a82fa34e3fca2a81354972e22b54da92606 Mon Sep 17 00:00:00 2001 From: Lorenzo Copelli <238189283+lorenzocopelliEFSA@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:11:24 +0200 Subject: [PATCH 12/12] Added eppoPynder and pystiller as dependencies. (#22) --- README.md | 14 ++++++++++++++ docs/guide.md | 14 ++++++++++++++ pyproject.toml | 4 +++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cbe96f3..edcd62e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,20 @@ as utilities designed to preserve data history. The package is intended for researchers, analysts, and practitioners who require convenient programmatic access to data collection utilities. +During installation, the following packages developed by EFSA are also +installed: +- **eppoPynder** - [Website](https://openefsa.github.io/eppoPynder/) | [PyPI](https://pypi.org/project/eppoPynder/). +- **pystiller** - [Website](https://openefsa.github.io/pystiller/) | [PyPI](https://pypi.org/project/pystiller/). + +These packages are not required to use **efsa_tools**, but are included for +convenience and can be imported and used directly in the code if needed: + +```python +import eppoPynder +# and/or +import pystiller +``` + ## Installation ### From PyPi diff --git a/docs/guide.md b/docs/guide.md index df1382b..3b88103 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -9,6 +9,20 @@ as utilities designed to preserve data history. The package is intended for researchers, analysts, and practitioners who require convenient programmatic access to data collection utilities. +During installation, the following packages developed by EFSA are also +installed: +- **eppoPynder** - [Website](https://openefsa.github.io/eppoPynder/) | [PyPI](https://pypi.org/project/eppoPynder/). +- **pystiller** - [Website](https://openefsa.github.io/pystiller/) | [PyPI](https://pypi.org/project/pystiller/). + +These packages are not required to use **efsa_tools**, but are included for +convenience and can be imported and used directly in the code if needed: + +```python +import eppoPynder +# and/or +import pystiller +``` + ## Installation ### From PyPi diff --git a/pyproject.toml b/pyproject.toml index d40afbd..c68ad68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,9 @@ license-files = ["LICENSE"] requires-python = ">=3.11" dependencies = [ "numpy>=2.0", - "pandas>=2.2" + "pandas>=2.2", + "eppoPynder>=2.0.0", + "pystiller>=1.0.0" ] [project.optional-dependencies]