diff --git a/.bandit.yml b/.bandit.yml index f487e59..e5bd7f3 100644 --- a/.bandit.yml +++ b/.bandit.yml @@ -1,9 +1,9 @@ exclude_dirs: - - .tox - .git + - .tox + - "tests/*" - build - dist - - "tests/*" skips: - B101 \ No newline at end of file diff --git a/README.md b/README.md index 9843c09..209de8d 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,91 @@ - +# Pain001: Automate ISO 20022-Compliant Payment File Creation -Pain001 Logo +![Pain001 banner][banner] - +[![PyPI][pypi-badge]][3] [![PyPI Downloads][pypi-downloads-badge]][7] [![License][license-badge]][1] [![Codecov][codecov-badge]][6] -# Pain001 - A Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Data +## Overview -![Pain001 banner][banner] +**Pain001** is an open-source Python Library that you can use to create +**ISO 20022-Compliant Payment Files** directly from your **CSV** or **SQLite** +Data Files. -[![PyPI][pypi-badge]][3] [![License][license-badge]][1] -[![Codecov][codecov-badge]][6] +- **Website:** +- **Source code:** +- **Bug reports:** -**Pain001** is a Python Library for Automating ISO 20022-Compliant Payment -Files Using CSV Data. +The Python library focuses specifically on +**Payment Initiation and Advice Messages**, commonly known as **Pain**. In a +very simplified way, a **pain.001** is a message that initiates the customer +payment. -**Pain001** offers a streamlined solution for reducing complexity and costs -associated with payment processing. By providing a simple and efficient method -to create ISO 20022-compliant payment files, it eliminates the manual effort of -file creation and validation. This not only saves valuable time and resources -but also minimizes the risk of errors, ensuring accurate and seamless payment -processing. +As of today the library is designed to be compatible with the +**pain.001.001.03** and **pain.001.001.09** message types and will support more +message types in the future. -If you are seeking to simplify and automate your payment processing, consider -leveraging the capabilities of **Pain001**. +Payments usually start with a **pain.001 payment initiation message**. The +payer sends it to the payee (or the payee’s bank) via a secure network. This +network could be **SWIFT** or **SEPA (Single Euro Payments Area) network**, or +other payment networks such as **CHAPS**, **BACS**, **Faster Payments**, etc. +The message contains the payer’s and payee’s bank account details, payment +amount, and other information required to process the payment. -## Features ✨ +The **Pain001** library can reduce payment processing complexity and costs by +generating ISO 20022-compliant payment files. These files automatically remove +the need to create and validate them manually, making the payment process more +efficient and cost-effective. It will save you time and resources and minimises +the risk of errors, making sure accurate and seamless payment processing. + +Use the **Pain001** library to simplify, accelerate and automate your payment +processing. -- **Easy to use:** The library is easy to use and requires minimal coding - knowledge, making it suitable for both developers and non-developers. +## Table of Contents + +- [Pain001: Automate ISO 20022-Compliant Payment File Creation](#pain001-automate-iso-20022-compliant-payment-file-creation) + - [Overview](#overview) + - [Table of Contents](#table-of-contents) + - [Features](#features) + - [Requirements](#requirements) + - [Installation](#installation) + - [Quick Start](#quick-start) + - [Arguments](#arguments) + - [Examples](#examples) + - [Using a CSV Data File as the source](#using-a-csv-data-file-as-the-source) + - [Using a SQLite Data File as the source](#using-a-sqlite-data-file-as-the-source) + - [Using the Source code](#using-the-source-code) + - [Embedded in an Application](#embedded-in-an-application) + - [Validation](#validation) + - [Documentation](#documentation) + - [Supported messages](#supported-messages) + - [Bank-to-Customer Cash Management](#bank-to-customer-cash-management) + - [Payments Clearing and Settlement](#payments-clearing-and-settlement) + - [Payments Initiation](#payments-initiation) + - [License](#license) + - [Contribution](#contribution) + - [Acknowledgements](#acknowledgements) + +## Features + +- **Easy to use:** Both developers and non-developers can easily use the + library, as it requires minimal coding knowledge. - **Open-source**: The library is open-source and free to use, making it accessible to everyone. - **Secure**: The library is secure and does not store any sensitive data, - ensuring that all information remains confidential. -- **Customizable**: The library allows developers to customize the output, + making sure that all information remains confidential. +- **Customizable**: The library allows developers to customise the output, making it adaptable to specific business requirements and preferences. - **Scalable solution**: The **Pain001** library can handle varying volumes of payment files, making it suitable for businesses of different sizes and transaction volumes. - **Time-saving**: The automated file creation process reduces the time spent on manual data entry and file generation, increasing overall productivity. -- **Seamless integration**: As a Python package, the **Pain001** library is +- **Seamless integration**: As a Python package, the Pain001 library is compatible with various Python-based applications and easily integrates into any existing projects or workflows. - **Cross-border compatibility**: The library supports both Single Euro Payments Area (SEPA) and non-SEPA credit transfers, making it versatile for use in different countries and regions. -- **Improve accuracy** by providing precise data, the library reduces errors in +- **Improve accuracy** by providing precise data; the library reduces errors in payment file creation and processing. - **Enhance efficiency** by automating the creation of Payment Initiation message files @@ -59,65 +93,126 @@ leveraging the capabilities of **Pain001**. the time required to create payment files. - **Guarantee the highest quality and compliance** by validating all payment files to meet the ISO 20022 standards. -- **Provide flexibility and choice to migrate to any supported ISO 20022 - messaging standard definitions** by simplifying the message creation process - and providing a standardized format for payment files. +- **Simplify ISO 20022-compliant payment initiation message creation** by + providing a standardised payment file format. +- **Reduce costs** by removing manual data entry and file generation, reducing + payment processing time, and reducing errors. + +## Requirements + +**Pain001** works with macOS, Linux and Windows and requires Python 3.9.0 and +above. ## Installation -It takes just a few seconds to get up and running with **Pain001**. Open your -terminal and run the following command: +It takes just a few seconds to get up and running with **Pain001**. You can +install Pain001 from PyPI with pip or your favourite package manager: + +Open your terminal and run the following command: ```sh pip install pain001 ``` -## Usage +Add the -U switch to update to the current version, if `pain001` is already +installed. + +## Quick Start After installation, you can run **Pain001** directly from the command line. -Simply call the main function with the path of your XML template file, XSD -schema file and the path of your CSV file containing the payment data. +Simply call the main module pain001 with the paths of your: + +- **XML template file** containing the various parameters you want to pass from + your Data file, +- **XSD schema file** to validate the generated XML file, and +- **Data file (CSV or SQLite)** containing the payment instructions that you + want to submit. -Once you have installed **Pain001**, you can generate and validate XML files -using the following command: +Here’s how you would do that: ```sh python3 -m pain001 \ \ - \ - \ - + \ + \ + ``` -## Arguments +### Arguments When running **Pain001**, you will need to specify four arguments: -- `xml_message_type`: This is the type of XML message you want to generate. - Currently, the valid options are: +- An `xml_message_type`: This is the type of XML message you want to generate. + + The currently supported types are: - pain.001.001.03 - pain.001.001.09 -- `xml_file_path`: This is the path to the XML template file you are using. -- `xsd_file_path`: This is the path to the XSD template file you are using. -- `csv_file_path`: This is the path to the CSV data file you want to convert - to XML. +- An `xml_template_file_path`: This is the path to the XML template file you + are using that contains variables that will be replaced by the values in your + Data file. +- An `xsd_schema_file_path`: This is the path to the XSD schema file you are + using to validate the generated XML file. +- A `data_file_path`: This is the path to the CSV or SQLite Data file you want + to convert to XML format. ## Examples -Here are a few example on how to use **Pain001** to generate a -pain.001.001.03 XML file from a CSV data file: +The following examples demonstrate how to use **Pain001** to generate a payment +initiation message from a CSV file and a SQLite Data file. + +### Using a CSV Data File as the source + +```sh +python3 -m pain001 \ + pain.001.001.03 \ + /path/to/your/template.xml \ + /path/to/your/pain.001.001.03.xsd \ + /path/to/your/template.csv +``` -### Via the Command Line +### Using a SQLite Data File as the source ```sh python3 -m pain001 \ pain.001.001.03 \ - /path/to/your/pain.001.001.03.xml \ + /path/to/your/template.xml \ /path/to/your/pain.001.001.03.xsd \ - /path/to/your/pain.001.001.03.csv + /path/to/your/template.db +``` + +### Using the Source code + +You can clone the source code and run the example code in your +terminal/command-line. To check out the source code, clone the repository from +GitHub: + +```sh +git clone https://github.com/sebastienrousseau/pain001.git +``` + + Then, navigate to the `pain001` directory and run the following command: + + ```sh + python3 -m pain001 \ + pain.001.001.03 \ + templates/pain.001.001.03/template.xml \ + templates/pain.001.001.03/pain.001.001.03.xsd \ + templates/pain.001.001.03/template.csv + ``` + +This will generate a payment initiation message from the sample CSV Data file. + +You can do the same with the sample SQLite Data file: + +```sh +python3 -m pain001 \ + pain.001.001.03 \ + templates/pain.001.001.03/template.xml \ + templates/pain.001.001.03/pain.001.001.03.xsd \ + templates/pain.001.001.03/template.db ``` -**Note:** The XML file that **Pain001** generates will be automatically +> **Note:** The XML file that **Pain001** generates will automatically be validated against the XSD template file before the new XML file is saved. If the validation fails, **Pain001** will stop running and display an error message in your terminal. @@ -134,10 +229,15 @@ from pain001 import main if __name__ == '__main__': xml_message_type = 'pain.001.001.03' - xml_file_path = 'template.xml' - xsd_file_path = 'schema.xsd' - csv_file_path = 'data.csv' - main(xml_message_type, xml_file_path, xsd_file_path, csv_file_path) + xml_template_file_path = 'template.xml' + xsd_schema_file_path = 'schema.xsd' + data_file_path = 'data.csv' + main( + xml_message_type, + xml_template_file_path, + xsd_schema_file_path, + data_file_path + ) ``` ### Validation @@ -160,39 +260,9 @@ is_valid = validate_xml_against_xsd( print(f"XML validation result: {is_valid}") ``` -## Documentation 📖 - -> ℹ️ **Info:** Do check out our [website][0] for more information. - -### Payment Messages - -The following **ISO 20022 Payment Initiation message types** are -currently supported: +## Documentation -- **pain.001.001.03** - Customer Credit Transfer Initiation - -This message is used to transmit credit transfer instructions from the -originator (the party initiating the payment) to the originator's bank. The -message supports both bulk and single payment instructions, allowing for the -transmission of multiple payments in a batch or individual payments separately. -The pain.001.001.03 message format is part of the ISO 20022 standard and is -commonly used for SEPA Credit Transfers within the Single Euro Payments Area. -It includes relevant information such as the originator's and beneficiary's -details, payment amounts, payment references, and other transaction-related -information required for processing the credit transfers. - -- **pain.001.001.09** - Customer Credit Transfer Initiation - -This message format is part of the ISO 20022 standard and is commonly used for -SEPA Credit Transfers within the Single Euro Payments Area. It enables the -transmission of credit transfer instructions from the originator to the -originator's bank. The message includes essential information such as the -originator's and beneficiary's details, payment amounts, payment references, -and other transaction-related information required for processing the credit -transfers. - -More message types will be added in the future. Please refer to the section -below for more details. +> **Info:** Do check out our [website][0] for comprehensive documentation. ### Supported messages @@ -207,10 +277,10 @@ customer. | Status | Message type | Name | |---|---|---| -| ⏳ | [camt.052.001.10] | Bank-to-Customer Account Statement | -| ⏳ | [camt.060.001.10] | Customer Account Notification | -| ⏳ | [camt.054.001.10] | Customer Account Statement Request | -| ⏳ | [camt.053.001.10] | Customer Account Identification | +| ⏳ | camt.052.001.10 | Bank-to-Customer Account Statement | +| ⏳ | camt.053.001.10 | Customer Account Identification | +| ⏳ | camt.054.001.10 | Customer Account Statement Request | +| ⏳ | camt.060.001.10 | Customer Account Notification | #### Payments Clearing and Settlement @@ -219,14 +289,14 @@ settlement of payment transactions. | Status | Message type | Name | |---|---|---| -| ⏳ | [pacs.002.001.12] | Credit Transfer Notification | -| ⏳ | [pacs.003.001.09] | Direct Debit Initiation | -| ⏳ | [pacs.004.001.11] | Direct Debit Reversal | -| ⏳ | [pacs.007.001.11] | Customer Direct Debit Confirmation | -| ⏳ | [pacs.008.001.10] | Credit Transfer Initiation | -| ⏳ | [pacs.009.001.10] | Credit Transfer Reversal | -| ⏳ | [pacs.010.001.05] | Account Identification | -| ⏳ | [pacs.028.001.05] | Account Statement Request | +| ⏳ | pacs.002.001.12 | Credit Transfer Notification | +| ⏳ | pacs.003.001.09 | Direct Debit Initiation | +| ⏳ | pacs.004.001.11 | Direct Debit Reversal | +| ⏳ | pacs.007.001.11 | Customer Direct Debit Confirmation | +| ⏳ | pacs.008.001.10 | Credit Transfer Initiation | +| ⏳ | pacs.009.001.10 | Credit Transfer Reversal | +| ⏳ | pacs.010.001.05 | Account Identification | +| ⏳ | pacs.028.001.05 | Account Statement Request | #### Payments Initiation @@ -237,16 +307,16 @@ and monitor payments. | Status | Message type | Name | |---|---|---| | ✅ | [pain.001.001.03][pain.001.001.03] | Customer Credit Transfer Initiation | -| ⏳ | [pain.001.001.04][pain.001.001.04] | Customer Direct Debit Initiation | -| ⏳ | [pain.001.001.05][pain.001.001.05] | Customer Direct Debit Reversal | -| ⏳ | [pain.001.001.06][pain.001.001.06] | Customer Credit Transfer Reversal | -| ⏳ | [pain.001.001.07][pain.001.001.07] | Customer Account Notification | -| ⏳ | [pain.001.001.08][pain.001.001.08] | Customer Account Statement | +| ⏳ | pain.001.001.04 | Customer Direct Debit Initiation | +| ⏳ | pain.001.001.05 | Customer Direct Debit Reversal | +| ⏳ | pain.001.001.06 | Customer Credit Transfer Reversal | +| ⏳ | pain.001.001.07 | Customer Account Notification | +| ⏳ | pain.001.001.08 | Customer Account Statement | | ✅ | [pain.001.001.09][pain.001.001.09] | Customer Credit Transfer Initiation | -| ⏳ | [pain.001.001.10][pain.001.001.10] | Customer Account Closure Request | -| ⏳ | [pain.001.001.11][pain.001.001.11] | Customer Account Change Request | +| ⏳ | pain.001.001.10 | Customer Account Closure Request | +| ⏳ | pain.001.001.11 | Customer Account Change Request | -## License 📝 +## License The project is licensed under the terms of both the MIT license and the Apache License (Version 2.0). @@ -254,7 +324,7 @@ Apache License (Version 2.0). - [Apache License, Version 2.0][1] - [MIT license][2] -## Contribution 🤝 +## Contribution We welcome contributions to **Pain001**. Please see the [contributing instructions][4] for more information. @@ -264,42 +334,25 @@ submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -## Acknowledgements 💙 +## Acknowledgements We would like to extend a big thank you to all the awesome contributors of [Pain001][5] for their help and support. -[0]: https://Pain001.co +[0]: https://pain001.com [1]: https://opensource.org/license/apache-2-0/ [2]: http://opensource.org/licenses/MIT [3]: https://github.com/sebastienrousseau/pain001 [4]: https://github.com/sebastienrousseau/pain001/blob/main/CONTRIBUTING.md [5]: https://github.com/sebastienrousseau/pain001/graphs/contributors [6]: https://codecov.io/github/sebastienrousseau/pain001?branch=main +[7]: https://pypi.org/project/pain001/ + +[pain.001.001.03]: https://pain001.com/pain.001.001.03/index.html +[pain.001.001.09]: https://pain001.com/pain.001.001.09/index.html -[camt.052.001.10]: docs/bank-to-customer-cash-management/messages/camt.052.001.10/README.md -[camt.060.001.10]: docs/bank-to-customer-cash-management/messages/camt.053.001.10/README.md -[camt.054.001.10]: docs/bank-to-customer-cash-management/messages/camt.054.001.10/README.md -[camt.053.001.10]: docs/bank-to-customer-cash-management/messages/camt.053.001.10/README.md -[pacs.002.001.12]: docs/payments-clearing-and-settlement/messages/pacs.002.001.12/README.md -[pacs.003.001.09]: docs/payments-clearing-and-settlement/messages/pacs.003.001.09/README.md -[pacs.004.001.11]: docs/payments-clearing-and-settlement/messages/pacs.004.001.11/README.md -[pacs.007.001.11]: docs/payments-clearing-and-settlement/messages/pacs.007.001.11/README.md -[pacs.008.001.10]: docs/payments-clearing-and-settlement/messages/pacs.008.001.10/README.md -[pacs.009.001.10]: docs/payments-clearing-and-settlement/messages/pacs.009.001.10/README.md -[pacs.010.001.05]: docs/payments-clearing-and-settlement/messages/pacs.010.001.05/README.md -[pacs.028.001.05]: docs/payments-clearing-and-settlement/messages/pacs.028.001.05/README.md -[pain.001.001.03]: docs/payments-initiation/messages/pain.001.001.03/README.md -[pain.001.001.04]: docs/payments-initiation/messages/pain.001.001.04/README.md -[pain.001.001.05]: docs/payments-initiation/messages/pain.001.001.05/README.md -[pain.001.001.06]: docs/payments-initiation/messages/pain.001.001.06/README.md -[pain.001.001.07]: docs/payments-initiation/messages/pain.001.001.07/README.md -[pain.001.001.08]: docs/payments-initiation/messages/pain.001.001.08/README.md -[pain.001.001.09]: docs/payments-initiation/messages/pain.001.001.09/README.md -[pain.001.001.10]: docs/payments-initiation/messages/pain.001.001.10/README.md -[pain.001.001.11]: docs/payments-initiation/messages/pain.001.001.11/README.md - -[banner]: https://kura.pro/pain001/images/titles/title-pain001.svg 'Pain001' +[banner]: https://kura.pro/pain001/images/banners/banner-pain001.svg 'Pain001, A Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Or SQlite Data Files.' [codecov-badge]: https://img.shields.io/codecov/c/github/sebastienrousseau/pain001?style=for-the-badge&token=AaUxKfRiou 'Codecov badge' [license-badge]: https://img.shields.io/pypi/l/pain001?style=for-the-badge 'License badge' [pypi-badge]: https://img.shields.io/pypi/pyversions/pain001.svg?style=for-the-badge 'PyPI badge' +[pypi-downloads-badge]:https://img.shields.io/pypi/dm/pain001.svg?style=for-the-badge 'PyPI Downloads badge' diff --git a/TEMPLATE.md b/TEMPLATE.md index 6f10c54..71a66d5 100644 --- a/TEMPLATE.md +++ b/TEMPLATE.md @@ -1,81 +1,34 @@ - - -pain001 logo - - - -# Pain001 (v0.0.19) +# Pain001: Automate ISO 20022-Compliant Payment File Creation ![Pain001 banner][banner] -[![PyPI][pypi]][2] [![License][license]][1] [![Codecov][codecov]][3] +## Overview -**Pain001** is a Python Library for Automating ISO 20022-Compliant Payment -Files Using CSV Data. +**Pain001** is an open-source Python Library that you can use to create +**ISO 20022-Compliant Payment Files** directly from your **CSV** or **SQLite** +Data Files. -**Pain001** offers a streamlined solution for reducing complexity and costs -associated with payment processing. By providing a simple and efficient method -to create ISO 20022-compliant payment files, it eliminates the manual effort of -file creation and validation. This not only saves valuable time and resources -but also minimizes the risk of errors, ensuring accurate and seamless payment -processing. +- **Website:** +- **Source code:** +- **Bug reports:** -If you are seeking to simplify and automate your payment processing, consider -leveraging the capabilities of **Pain001**. +## Requirements -## Features ✨ - -- **Easy to use:** The library is easy to use and requires minimal coding - knowledge, making it suitable for both developers and non-developers. -- **Open-source**: The library is open-source and free to use, making it - accessible to everyone. -- **Secure**: The library is secure and does not store any sensitive data, - ensuring that all information remains confidential. -- **Customizable**: The library allows developers to customize the output, - making it adaptable to specific business requirements and preferences. -- **Scalable solution**: The **Pain001** library can handle varying volumes of - payment files, making it suitable for businesses of different sizes and - transaction volumes. -- **Time-saving**: The automated file creation process reduces the time spent - on manual data entry and file generation, increasing overall productivity. -- **Seamless integration**: As a Python package, the **Pain001** library is - compatible with various Python-based applications and easily integrates into - any existing projects or workflows. -- **Cross-border compatibility**: The library supports both Single Euro - Payments Area (SEPA) and non-SEPA credit transfers, making it versatile for - use in different countries and regions. -- **Improve accuracy** by providing precise data, the library reduces errors in - payment file creation and processing. -- **Enhance efficiency** by automating the creation of Payment Initiation - message files -- **Accelerate payment file creation** by automating the process and reducing - the time required to create payment files. -- **Guarantee the highest quality and compliance** by validating all payment - files to meet the ISO 20022 standards. -- **Provide flexibility and choice to migrate to any supported ISO 20022 - messaging standard definitions** by simplifying the message creation process - and providing a standardized format for payment files. +**Pain001** works with macOS, Linux and Windows and requires Python 3.9.0 and +above. ## Installation -It takes just a few seconds to get up and running with **Pain001**. Open your -terminal and run the following command: +It takes just a few seconds to get up and running with **Pain001**. You can +install Pain001 from PyPI with pip or your favourite package manager: + +Open your terminal and run the following command: ```sh pip install pain001 ``` -[1]: https://opensource.org/license/apache-2-0/ -[2]: https://github.com/sebastienrousseau/pain001 -[3]: https://codecov.io/github/sebastienrousseau/pain001?branch=main +Add the -U switch to update to the current version, if `pain001` is already +installed. -[banner]: https://kura.pro/pain001/images/titles/title-pain001.svg 'Pain001 banner' -[codecov]: https://img.shields.io/codecov/c/github/sebastienrousseau/pain001?style=for-the-badge&token=AaUxKfRiou 'Codecov badge' -[license]: https://img.shields.io/pypi/l/pain001?style=for-the-badge 'License badge' -[pypi]: https://img.shields.io/pypi/pyversions/pain001.svg?style=for-the-badge 'PyPI badge' +[banner]: https://kura.pro/pain001/images/banners/banner-pain001.svg 'Pain001, A Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Or SQlite Data Files.' diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..a0d088c --- /dev/null +++ b/docs/404.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/pain001/__init__.py b/pain001/__init__.py index 858b938..51a2f95 100644 --- a/pain001/__init__.py +++ b/pain001/__init__.py @@ -15,4 +15,4 @@ """The Python pain001 module.""" __all__ = ["pain001"] -__version__ = "0.0.19" +__version__ = "0.0.20" diff --git a/pain001/__main__.py b/pain001/__main__.py index fa3ca4b..d0b313c 100644 --- a/pain001/__main__.py +++ b/pain001/__main__.py @@ -17,142 +17,143 @@ """ Enables use of Python Pain001 as a "main" function (i.e. "python3 -m pain001 - "). + +"). This allows using Pain001 with third-party libraries without modifying their code. """ -from pain001.core import process_files -from pain001.context import context -from pain001.constants.constants import valid_xml_types import os import sys -import argparse - -from pain001.xml.validate_via_xsd import validate_via_xsd - - -cli_string = """ -**Pain001** is a Python Library for Automating ISO 20022-Compliant Payment -Files Using CSV Data. - -**Pain001** offers a streamlined solution for reducing complexity and costs -associated with payment processing. By providing a simple and efficient method -to create ISO 20022-compliant payment files, it eliminates the manual effort of -file creation and validation. This not only saves valuable time and resources -but also minimizes the risk of errors, ensuring accurate and seamless payment -processing. - -If you are seeking to simplify and automate your payment processing, consider -leveraging the capabilities of **Pain001**. +import click -## Installation - -To install **Pain001**, run this command in your terminal: - -```sh -pip install pain001 -``` - -## Usage +from pain001.constants.constants import valid_xml_types +from pain001.context.context import Context +from pain001.core.core import process_files -To use **Pain001**, run this command in your terminal: +# from pain001.xml.validate_via_xsd import validate_via_xsd -```sh -python3 -m pain001 \ - \ - \ - \ - -``` +from rich.console import Console +from rich.table import Table +from rich import box -## Arguments: +console = Console() -- `xml_message_type`: The type of XML message. The current valid values are: - - pain.001.001.03 and - - pain.001.001.09 -- `xml_file_path`: The path to the XML template file. -- `xsd_file_path`: The path to the XSD template file. -- `csv_file_path`: The path to the CSV data file. +description = """ +A powerful Python library that enables you to create +ISO 20022-compliant payment files directly from CSV or SQLite Data files.\n +https://pain001.com +""" +title = "Pain001" + +table = Table( + box=box.ROUNDED, + safe_box=True, + show_header=False, + title=title, +) + +table.add_column(justify="center", no_wrap=False, vertical="middle") +table.add_row(description) +table.width = 80 +console.print(table) + + +@click.command( + help=( + "To use Pain001, you must specify the following options:\n\n" + ), + context_settings=dict(help_option_names=["-h", "--help"]), +) +@click.option( + "-t", + "--xml_message_type", + default=None, + help="Type of XML message (required)", +) +@click.option( + "-m", + "--xml_template_file_path", + default=None, + type=click.Path(), + help="Path to XML template file (required)", +) +@click.option( + "-s", + "--xsd_schema_file_path", + default=None, + type=click.Path(), + help="Path to XSD template file (required)", +) +@click.option( + "-d", + "--data_file_path", + default=None, + type=click.Path(), + help="Path to data file (CSV or SQLite) (required)", +) +def main( + xml_message_type, + xml_template_file_path, + xsd_schema_file_path, + data_file_path, +): + """Initialize the context and log a message.""" + logger = Context.get_instance().get_logger() -## Example: + # print("Inside main function") -To generate a pain.001.001.03 XML file from the CSV data file you can run the -following command in your terminal: + def check_variable(variable, name): + if variable is None: + print(f"Error: {name} is required.") + sys.exit(1) -```sh -python3 -m pain001 \ - pain.001.001.03 \ - /path/to/your/pain.001.001.03.xml \ - /path/to/your/pain.001.001.03.xsd \ - /path/to/your/pain.001.001.03.csv -``` + # Check that xml_message_type is provided + check_variable(xml_message_type, "xml_message_type") -Note: The generated XML file will be validated against the XSD template -file before being saved. If the validation fails, the program will exit -with an error message. + # Check that xsd_schema_file_path is provided + check_variable(xsd_schema_file_path, "xsd_schema_file_path") -For more information, please visit the project's GitHub page at: -. -""" + # Check that data_file_path is provided + check_variable(data_file_path, "data_file_path") + # Check that xml_template_file_path is not invalid + if not os.path.isfile(xml_template_file_path): + print( + f"The XML template file '{xml_template_file_path}' does not exist." + ) + sys.exit(1) -def main( - xml_message_type=None, - xml_file_path=None, - xsd_file_path=None, - data_file_path=None, - output_file_path=None, -): - """ - Entrypoint for pain001 when invoked as a module with - python3 -m pain001 - . - """ + # Check that xsd_schema_file_path is not invalid + if not os.path.isfile(xsd_schema_file_path): + print( + f"The XSD template file '{xsd_schema_file_path}' does not exist." + ) + sys.exit(1) - """Initialize the context and log a message.""" - logger = context.Context.get_instance().get_logger() + # Check that data_file_path is not invalid + if not os.path.isfile(data_file_path): + print(f"The data file '{data_file_path}' does not exist.") + sys.exit(1) + # Check that other necessary arguments are provided if ( - xml_file_path is None - or xsd_file_path is None + xml_template_file_path is None + or xsd_schema_file_path is None or data_file_path is None - or output_file_path is None ): - parser = argparse.ArgumentParser( - description="Generate Pain.001 file from data" - ) - parser.add_argument( - "xml_message_type", help="Type of XML message" - ) - parser.add_argument( - "xml_file_path", help="Path to XML template file" - ) - parser.add_argument( - "xsd_file_path", help="Path to XSD template file" - ) - parser.add_argument( - "data_file_path", help="Path to data file (CSV or SQLite)" - ) - parser.add_argument( - "output_file_path", help="Path to output XML file" - ) - args = parser.parse_args() - - logger.info("Parsing command line arguments.") - xml_message_type = args.xml_message_type - xml_file_path = args.xml_file_path - xsd_file_path = args.xsd_file_path - data_file_path = args.data_file_path - output_file_path = args.output_file_path - - """Check that the files or values passed as arguments exist.""" - if not xml_message_type: - logger.info("The XML message type is not specified.") - print("The XML message type is not specified.") + print(click.get_current_context().get_help()) sys.exit(1) + """ + Entrypoint for pain001 when invoked as a module with + python3 -m pain001 + . + """ + logger = Context.get_instance().get_logger() + + logger.info("Parsing command line arguments.") # Check that the XML message type is valid if xml_message_type not in valid_xml_types: @@ -160,18 +161,22 @@ def main( print(f"Invalid XML message type: {xml_message_type}.") sys.exit(1) - if not os.path.isfile(xml_file_path): + if not os.path.isfile(xml_template_file_path): logger.info( - "The XML template file '{xml_file_path}' does not exist." + f"The XML template file '{xml_template_file_path}' does not exist." + ) + print( + f"The XML template file '{xml_template_file_path}' does not exist." ) - print("The XML template file '{xml_file_path}' does not exist.") sys.exit(1) - if not os.path.isfile(xsd_file_path): + if not os.path.isfile(xsd_schema_file_path): logger.info( - "The XSD template file '{xsd_file_path}' does not exist." + f"The XSD template file '{xsd_schema_file_path}' does not exist." + ) + print( + f"The XSD template file '{xsd_schema_file_path}' does not exist." ) - print("The XSD template file '{xsd_file_path}' does not exist.") sys.exit(1) if not os.path.isfile(data_file_path): @@ -180,18 +185,20 @@ def main( sys.exit(1) # Validate the XML file and raise a SystemExit exception if invalid - if not validate_via_xsd(xml_file_path, xsd_file_path): - logger.info( - f"Error: XML located at {xml_file_path} is invalid." - ) - sys.exit(1) + # is_valid = validate_via_xsd( + # xml_template_file_path, xsd_schema_file_path + # ) + # if not is_valid: + # logger.error( + # f"Error: XML located at {xml_template_file_path} is invalid." + # ) + # sys.exit(1) process_files( xml_message_type, - xml_file_path, - xsd_file_path, + xml_template_file_path, + xsd_schema_file_path, data_file_path, - output_file_path, ) diff --git a/pain001/context/context.py b/pain001/context/context.py index ef6a761..0b625a8 100644 --- a/pain001/context/context.py +++ b/pain001/context/context.py @@ -75,27 +75,33 @@ def set_log_level(self, log_level): Raises: Exception: If the log level is invalid. """ + valid_log_levels = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + } + if isinstance( log_level, int ): # Check if log_level is an integer - self.log_level = log_level + if log_level in valid_log_levels.values(): + self.log_level = log_level + else: + raise Exception("Invalid log level") else: log_level = ( log_level.strip().upper() ) # Strip and convert to uppercase - if log_level == "DEBUG": - self.log_level = logging.DEBUG - elif log_level == "INFO": - self.log_level = logging.INFO - elif log_level == "WARNING": - self.log_level = logging.WARNING - elif log_level == "ERROR": - self.log_level = logging.ERROR - elif log_level == "CRITICAL": - self.log_level = logging.CRITICAL + if log_level in valid_log_levels: + self.log_level = valid_log_levels[log_level] else: raise Exception("Invalid log level") + if self.logger: + self.logger.setLevel(self.log_level) + def init_logger(self): """Initializes the logger. diff --git a/pain001/core/__init__.py b/pain001/core/__init__.py new file mode 100644 index 0000000..f66c4d6 --- /dev/null +++ b/pain001/core/__init__.py @@ -0,0 +1,14 @@ +# Copyright (C) 2023 Sebastien Rousseau. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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/pain001/core.py b/pain001/core/core.py similarity index 71% rename from pain001/core.py rename to pain001/core/core.py index d4cff4f..13b2b46 100644 --- a/pain001/core.py +++ b/pain001/core/core.py @@ -20,7 +20,7 @@ # Import the pain001 library functions from pain001.constants.constants import valid_xml_types -from pain001.context import context +from pain001.context.context import Context from pain001.csv.load_csv_data import load_csv_data from pain001.csv.validate_csv_data import validate_csv_data from pain001.db.load_db_data import load_db_data @@ -31,10 +31,9 @@ def process_files( xml_message_type, - xml_file_path, - xsd_file_path, + xml_template_file_path, + xsd_schema_file_path, data_file_path, - output_file_path, ): """ This function generates an ISO 20022 payment message from a CSV or SQLite @@ -43,11 +42,10 @@ def process_files( Args: xml_message_type (str): The type of XML message to generate. Valid options are 'pain.001.001.03' and 'pain.001.001.09'. - xml_file_path (str): The path of the XML template file. - xsd_file_path (str): The path of the XSD schema file. + xml_template_file_path (str): The path of the XML template file. + xsd_schema_file_path (str): The path of the XSD schema file. data_file_path (str): The path of the CSV or SQLite file containing the payment data. - output_file_path (str): The path of the output XML file. Returns: None @@ -56,11 +54,11 @@ def process_files( ValueError: If the XML message type is not supported. FileNotFoundError: If the XML template file does not exist. FileNotFoundError: If the XSD schema file does not exist. - FileNotFoundError: If the data file does not exist. + FileNotFoundError: If the Data file does not exist. """ # Initialize the context and log a message. - logger = context.Context.get_instance().get_logger() + logger = Context.get_instance().get_logger() # Loop through the payment initiation message types and check if the XML # message type is supported. @@ -72,17 +70,19 @@ def process_files( raise ValueError(error_message) # Check if the XML template file exists - if not os.path.exists(xml_file_path): + if not os.path.exists(xml_template_file_path): error_message = ( - f"Error: XML template '{xml_file_path}' does not exist." + f"Error: XML template '{xml_template_file_path}' " + f"does not exist." ) logger.error(error_message) raise FileNotFoundError(error_message) # Check if the XSD schema file exists - if not os.path.exists(xsd_file_path): + if not os.path.exists(xsd_schema_file_path): error_message = ( - f"Error: XSD schema file '{xsd_file_path}' does not exist." + f"Error: XSD schema file '{xsd_schema_file_path}' " + f"does not exist." ) logger.error(error_message) raise FileNotFoundError(error_message) @@ -112,53 +112,57 @@ def process_files( # Load data into a list of dictionaries based on the file type if is_csv: data = load_csv_data(data_file_path) - validate_csv_data + if not validate_csv_data(data): + error_message = "Error: Invalid CSV data." + logger.error(error_message) + raise ValueError(error_message) elif is_sqlite: data = load_db_data(data_file_path, table_name="pain001") + if not validate_db_data(data): + error_message = "Error: Invalid SQLite data." + logger.error(error_message) + raise ValueError(error_message) else: error_message = "Error: Unsupported data file type." logger.error(error_message) raise ValueError(error_message) - # Validate the data - if not validate_db_data(data): - error_message = "Error: Invalid data." - logger.error(error_message) - raise ValueError(error_message) - # Register the namespace prefixes and URIs for the XML message type register_namespaces(xml_message_type) # Generate the updated XML file path xml_generator( - data, mapping, xml_message_type, xml_file_path, xsd_file_path + data, + mapping, + xml_message_type, + xml_template_file_path, + xsd_schema_file_path, ) - # Log a message - logger.info( - f"Generating XML file '{output_file_path}'" - f"from data file '{data_file_path}'." - ) - logger.info( - f"Successfully generated XML file '{output_file_path}'." - ) + # Confirm the XML file has been created + if os.path.exists(xml_template_file_path): + logger.info( + f"Successfully generated XML file '{xml_template_file_path}'" + ) + else: + logger.error( + f"Failed to generate XML file at '{xml_template_file_path}'" + ) if __name__ == "__main__": - if len(sys.argv) < 6: + if len(sys.argv) < 5: print( "Usage: python3 -m pain001 " + " ".join( [ "", - "", - "", + "", + "", "", - "", ] ) ) + sys.exit(1) - process_files( - sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] - ) + process_files(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) diff --git a/pain001/xml/create_root_element.py b/pain001/xml/create_root_element.py index d428de1..1eaff8c 100644 --- a/pain001/xml/create_root_element.py +++ b/pain001/xml/create_root_element.py @@ -20,7 +20,6 @@ def create_root_element(payment_initiation_message_type): - # Create the namespace for the payment initiation message type. namespace = ( "urn:iso:std:iso:20022:tech:xsd:" @@ -34,9 +33,7 @@ def create_root_element(payment_initiation_message_type): # Set the schema location. schema_location = ( - namespace + " " - + payment_initiation_message_type - + ".xsd" + namespace + " " + payment_initiation_message_type + ".xsd" ) root.set("xsi:schemaLocation", schema_location) diff --git a/pain001/xml/generate_updated_xml_file_path.py b/pain001/xml/generate_updated_xml_file_path.py index b9c7f64..271817b 100644 --- a/pain001/xml/generate_updated_xml_file_path.py +++ b/pain001/xml/generate_updated_xml_file_path.py @@ -20,10 +20,9 @@ def generate_updated_xml_file_path( - xml_file_path, - payment_initiation_message_type + xml_file_path, payment_initiation_message_type ): - print(os.path.splitext(xml_file_path)[0]) + # print(os.path.splitext(xml_file_path)[0]) base_directory = os.path.dirname(xml_file_path) base_name = os.path.basename(xml_file_path) file_name, _ = os.path.splitext(base_name) diff --git a/pain001/xml/generate_xml.py b/pain001/xml/generate_xml.py index d435ac8..73e64a2 100644 --- a/pain001/xml/generate_xml.py +++ b/pain001/xml/generate_xml.py @@ -27,7 +27,7 @@ def create_common_elements(parent, row, mapping): def create_xml_v3(root, data, mapping): - print("XML v3") + # print("XML v3") # Create CstmrCdtTrfInitn element cstmr_cdt_trf_initn_element = ET.Element("CstmrCdtTrfInitn") @@ -41,9 +41,7 @@ def create_xml_v3(root, data, mapping): for xml_tag, csv_column in mapping.items(): if xml_tag in ["MsgId", "CreDtTm", "NbOfTxs"]: create_xml_element( - GrpHdr_element, - xml_tag, - data[0][csv_column] + GrpHdr_element, xml_tag, data[0][csv_column] ) # Create new "InitgPty" element in the XML tree using data from the @@ -74,9 +72,7 @@ def create_xml_v3(root, data, mapping): # Create new "BtchBookg" element in the XML tree using data # from the CSV file create_xml_element( - PmtInf_element, - "BtchBookg", - row["batch_booking"].lower() + PmtInf_element, "BtchBookg", row["batch_booking"].lower() ) # Create new "NbOfTxs" element in the XML tree using data from @@ -86,9 +82,7 @@ def create_xml_v3(root, data, mapping): # Create new "CtrlSum" element in the XML tree using data from # the CSV file create_xml_element( - PmtInf_element, - "CtrlSum", - f"{row['control_sum']}" + PmtInf_element, "CtrlSum", f"{row['control_sum']}" ) # Create new "PmtTpInf" element in the XML tree using data from @@ -106,7 +100,7 @@ def create_xml_v3(root, data, mapping): create_xml_element( PmtInf_element, "ReqdExctnDt", - row["requested_execution_date"] + row["requested_execution_date"], ) # Create new "Dbtr" element in the XML tree using data from @@ -214,7 +208,9 @@ def create_xml_v9(root, data, mapping): # Add the MsgId, CreDtTm, and NbOfTxs elements to the GrpHdr element for xml_tag, csv_column in mapping.items(): if xml_tag in ["MsgId", "CreDtTm", "NbOfTxs"]: - create_xml_element(GrpHdr_element, xml_tag, data[0][csv_column]) + create_xml_element( + GrpHdr_element, xml_tag, data[0][csv_column] + ) # Create new "InitgPty" element in the XML tree using data from the # CSV file @@ -265,7 +261,8 @@ def create_xml_v9(root, data, mapping): # replace with the appropriate value child_element2.text = row["debtor_agent_BIC"] child_element2.set( - "xmlns", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09") + "xmlns", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" + ) child_element.append(child_element2) DbtrAgt_element.append(child_element) PmtInf_element.append(DbtrAgt_element) @@ -306,7 +303,8 @@ def create_xml_v9(root, data, mapping): # replace with the appropriate value child_element2.text = row["creditor_agent_BIC"] child_element2.set( - "xmlns", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09") + "xmlns", "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09" + ) child_element.append(child_element2) CdtrAgt_element.append(child_element) CdtTrfTxInf_element.append(CdtrAgt_element) diff --git a/pain001/xml/validate_via_xsd.py b/pain001/xml/validate_via_xsd.py index 197d86f..0ee147e 100644 --- a/pain001/xml/validate_via_xsd.py +++ b/pain001/xml/validate_via_xsd.py @@ -15,19 +15,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -import xml.etree.ElementTree as ET - -# Validate XML file against XSD schema using xmlschema package -# (https://pypi.org/project/xmlschema/) and ElementTree package -# (https://docs.python.org/3/library/xml.etree.elementtree.html) +import defusedxml.ElementTree as ET def validate_via_xsd(xml_file_path, xsd_file_path): - # Load XML and XSD files - xml_tree = ET.parse(xml_file_path) + """ + Validates an XML file against an XSD schema. + + Args: + xml_file_path (str): Path to the XML file to validate. + xsd_file_path (str): Path to the XSD schema file. + + Returns: + bool: True if the XML file is valid, False otherwise. + """ + + # Load XML file into an ElementTree object. + try: + xml_tree = ET.parse(xml_file_path) + except Exception as e: + print(f"Error: {e}") + return False + + # Load XSD schema into an XMLSchema object. xsd = xmlschema.XMLSchema(xsd_file_path) - # Validate XML file against XSD schema - is_valid = xsd.is_valid(xml_tree) + # Validate XML file against XSD schema. + try: + is_valid = xsd.is_valid(xml_tree) + except Exception as e: + print(f"Error: {e}") + return False + # Return True if XML file is valid, False otherwise. return is_valid diff --git a/pain001/xml/xml_generator.py b/pain001/xml/xml_generator.py index 5854997..2b90c4b 100644 --- a/pain001/xml/xml_generator.py +++ b/pain001/xml/xml_generator.py @@ -69,20 +69,23 @@ def xml_generator( # Write the updated XML tree to a file write_xml_to_file(updated_xml_file_path, root) + print( + f"A new XML file has been created at {updated_xml_file_path}" + ) + # Validate the updated XML file against the XSD schema is_valid = validate_via_xsd( updated_xml_file_path, xsd_file_path ) if not is_valid: - print("❌ Error: Invalid XML data.") + print("Error: Invalid XML data.") sys.exit(1) else: - print(f"❯ XML located at {updated_xml_file_path} is valid.") + print(f"The XML has been validated against {xsd_file_path}") else: # Handle the case when the payment_initiation_message_type is # not valid print( - "❌", "Error: Invalid XML message type:", payment_initiation_message_type, ) diff --git a/poetry.lock b/poetry.lock index f8facae..9a35864 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,41 +1,142 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] [[package]] name = "elementpath" -version = "2.5.3" -description = "XPath 1.0/2.0/3.0 parsers and selectors for ElementTree and lxml" -category = "main" +version = "4.1.2" +description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and lxml" +optional = false +python-versions = ">=3.7" +files = [ + {file = "elementpath-4.1.2-py3-none-any.whl", hash = "sha256:e8a6c5685e1843c620f426c85ad21ff25cfc7554790116b1046adae7dc252458"}, + {file = "elementpath-4.1.2.tar.gz", hash = "sha256:0bd0ef5bad559b677ba499e9c7342ca1f2ae2bace90808ee52528ec8d9f6e12b"}, +] + +[package.extras] +dev = ["Sphinx", "coverage", "flake8", "lxml", "lxml-stubs", "memory-profiler", "memray", "mypy", "tox", "xmlschema (>=2.0.0)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" optional = false python-versions = ">=3.7" files = [ - {file = "elementpath-2.5.3-py3-none-any.whl", hash = "sha256:5ef1d51e8daa670f007914ff0f78ca7b2ecaa47e0ea0c5c699a29e6bc5f50385"}, - {file = "elementpath-2.5.3.tar.gz", hash = "sha256:b8aeb6f27dddc10fb9201b62090628a846cbae8577f3544cb1075fa38d0817f6"}, + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "pygments" +version = "2.15.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, + {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "rich" +version = "13.4.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"}, + {file = "rich-13.4.2.tar.gz", hash = "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + [package.extras] -dev = ["Sphinx", "coverage", "flake8", "lxml", "memory-profiler", "mypy (==0.950)", "tox", "xmlschema (>=1.9.0)"] +jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "xmlschema" -version = "1.11.3" +version = "2.3.1" description = "An XML Schema validator and decoder" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "xmlschema-1.11.3-py3-none-any.whl", hash = "sha256:56cb8c028457fa8948587339a1c3fa75a0317472ab6710f34ba2c0cb57d6b710"}, - {file = "xmlschema-1.11.3.tar.gz", hash = "sha256:28a135028f7ab1e0c934fc0c6717a66b2dc5f166d123dfe6ce61afc671ad113f"}, + {file = "xmlschema-2.3.1-py3-none-any.whl", hash = "sha256:eac0e10957723689ff0691785da4ffee1e95df3a874e685a179047f7bf07f8fb"}, + {file = "xmlschema-2.3.1.tar.gz", hash = "sha256:2eb426c5710833a05610c22c8766713a1b87e9405e3eca0b7c658375bf7ec810"}, ] [package.dependencies] -elementpath = ">=2.5.0,<3.0.0" +elementpath = ">=4.1.2,<5.0.0" [package.extras] -codegen = ["elementpath (>=2.5.0,<3.0.0)", "jinja2"] -dev = ["Sphinx", "coverage", "elementpath (>=2.5.0,<3.0.0)", "flake8", "jinja2", "lxml", "lxml-stubs", "memory-profiler", "mypy", "sphinx-rtd-theme", "tox"] -docs = ["Sphinx", "elementpath (>=2.5.0,<3.0.0)", "jinja2", "sphinx-rtd-theme"] +codegen = ["elementpath (>=4.1.2,<5.0.0)", "jinja2"] +dev = ["Sphinx", "coverage", "elementpath (>=4.1.2,<5.0.0)", "flake8", "jinja2", "lxml", "lxml-stubs", "memory-profiler", "mypy", "sphinx-rtd-theme", "tox"] +docs = ["Sphinx", "elementpath (>=4.1.2,<5.0.0)", "jinja2", "sphinx-rtd-theme"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "5b9996124b720ddc00471a99a0cedec3a3c98d88b0eb31ec3e6ed56b3b28ab07" +content-hash = "a658f3aa8267479aea31fe66e6c80d643ffea5d40ada3d4be3e1b13e97f4b6bf" diff --git a/pyproject.toml b/pyproject.toml index 07cff7b..fa1ab4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pain001" -version = "0.0.19" +version = "0.0.20" description = "Pain001 is a Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Data." authors = ["Sebastien Rousseau "] license = "Apache Software License" @@ -11,6 +11,9 @@ homepage = "https://pain001.com" [tool.poetry.dependencies] python = "^3.9" xmlschema = "^2.3.0" +click = "^8.1.3" +defusedxml = "^0.7.1" +rich = "^13.4.2" [build-system] requires = ["poetry-core"] diff --git a/requirements.txt b/requirements.txt index 45cae35..bf2cd4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ -xmlschema==2.3.0 \ No newline at end of file +click==8.1.3 +defusedxml==0.7.1 +rich==13.4.2 +xmlschema==2.3.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index b496816..fe30a67 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,47 +1,72 @@ +# Copyright (C) 2023 Sebastien Rousseau. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + [metadata] name = pain001 -version = 0.0.19 +version = 0.0.20 +description = Pain001, A Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Or SQlite Data Files. +keywords = pain001,iso20022,payment-processing,automate-payments,sepa,financial,banking-payments,csv,sqlite author = Sebastian Rousseau author_email = sebastian.rousseau@gmail.com -description = pain001 is a Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Data. +url = https://github.com/sebastienrousseau/pain001 +license = Apache License 2.0 +license_files = + LICENSE-APACHE + LICENSE-MIT long_description = file: README.md long_description_content_type = text/markdown -url = https://github.com/sebastienrousseau/pain001 -license = Apache Software License -license_file = LICENSE-APACHE + classifiers = Development Status :: 4 - Beta Intended Audience :: Developers Intended Audience :: Financial and Insurance Industry - Topic :: Software Development :: Libraries :: Python Modules License :: OSI Approved :: Apache Software License + Operating System :: MacOS + Operating System :: OS Independent + Operating System :: POSIX + Operating System :: Unix Programming Language :: Python Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 - Operating System :: OS Independent - Operating System :: POSIX - Operating System :: MacOS - Operating System :: Unix + Topic :: Software Development :: Libraries :: Python Modules + project_urls = - Documentation = https://pain001.com/documentation/ + Documentation = https://pain001.com/ Funding = https://paypal.me/wwdseb - Release notes = https://pain001.com/release-notes/ Source = https://github.com/sebastienrousseau/pain001/releases -keywords = iso 20022 pain.001 credit transfer financial banking payments csv sepa [options] packages = pain001 - +zip_safe = False +include_package_data = True +python_requires = ~=3.9 install_requires = + click + defusedxml + rich xmlschema -[wheel] -universal = 1 - [aliases] test = pytest [tool:pytest] testpaths = tests + +[wheel] +universal = 1 + diff --git a/setup.py b/setup.py index 92027ea..fb25d25 100644 --- a/setup.py +++ b/setup.py @@ -18,173 +18,35 @@ from setuptools import setup -LONG_DESCRIPTION = """ -**Pain001** is a Python Library for Automating ISO 20022-Compliant Payment -Files Using CSV Data. - -**Pain001** offers a streamlined solution for reducing complexity and costs -associated with payment processing. By providing a simple and efficient method -to create ISO 20022-compliant payment files, it eliminates the manual effort of -file creation and validation. This not only saves valuable time and resources -but also minimizes the risk of errors, ensuring accurate and seamless payment -processing. - -If you are seeking to simplify and automate your payment processing, consider -leveraging the capabilities of **Pain001**. - -## Features ✨ - -- **Easy to use:** The library is easy to use and requires minimal coding -knowledge, making it suitable for both developers and non-developers. -- **Open-source**: The library is open-source and free to use, making it -accessible to everyone. -- **Secure**: The library is secure and does not store any sensitive data, -ensuring that all information remains confidential. -- **Customizable**: The library allows developers to customize the output, -making it adaptable to specific business requirements and preferences. -- **Scalable solution**: The **Pain001** library can handle varying volumes of -payment files, making it suitable for businesses of different sizes and -transaction volumes. -- **Time-saving**: The automated file creation process reduces the time spent -on manual data entry and file generation, increasing overall productivity. -- **Seamless integration**: As a Python package, the **Pain001** library is -compatible with various Python-based applications and easily integrates into -any existing projects or workflows. -- **Cross-border compatibility**: The library supports both Single Euro -Payments Area (SEPA) and non-SEPA credit transfers, making it versatile for -use in different countries and regions. -- **Improve accuracy** by providing precise data, the library reduces errors in -payment file creation and processing. -- **Enhance efficiency** by automating the creation of Payment Initiation -message files -- **Accelerate payment file creation** by automating the process and reducing -the time required to create payment files. -- **Guarantee the highest quality and compliance** by validating all payment -files to meet the ISO 20022 standards. -- **Provide flexibility and choice to migrate to any supported ISO 20022 -messaging standard definitions** by simplifying the message creation process -and providing a standardized format for payment files. - -## Installation - -It takes just a few seconds to get up and running with **Pain001**. Open your -terminal and run the following command: - -```sh -pip install pain001 -``` - -## Usage - -After installation, you can run **Pain001** directly from the command line. -Simply call the main function with the path of your XML template file, XSD -schema file and the path of your CSV file containing the payment data. - -Once you have installed **Pain001**, you can generate and validate XML files -using the following command: - -```sh -python3 -m pain001 \ - \ - \ - \ - -``` - -## Arguments - -When running **Pain001**, you will need to specify four arguments: - -- `xml_message_type`: This is the type of XML message you want to generate. -Currently, the valid options are: - - pain.001.001.03 - - pain.001.001.09 -- `xml_file_path`: This is the path to the XML template file you are using. -- `xsd_file_path`: This is the path to the XSD template file you are using. -- `csv_file_path`: This is the path to the CSV data file you want to convert to -XML. - -## Examples - -Here are a few example on how to use **Pain001** to generate a -pain.001.001.03 XML file from a CSV data file: - -### Via the Command Line - -```sh -python3 -m pain001 \ - pain.001.001.03 \ - /path/to/your/pain.001.001.03.xml \ - /path/to/your/pain.001.001.03.xsd \ - /path/to/your/pain.001.001.03.csv -``` - -**Note:** The XML file that **Pain001** generates will be automatically -validated against the XSD template file before the new XML file is saved. If -the validation fails, **Pain001** will stop running and display an error -message in your terminal. - -### Embedded in an Application - -To embed **Pain001** in a new or existing application, import the main function -and use it in your code. - -Here's an example: - -```python -from pain001 import main - -if __name__ == '__main__': - xml_message_type = 'pain.001.001.03' - xml_file_path = 'template.xml' - xsd_file_path = 'schema.xsd' - csv_file_path = 'data.csv' - main(xml_message_type, xml_file_path, xsd_file_path, csv_file_path) -``` - -### Validation - -To validate the generated XML file against a given xsd schema, use the -following method: - -```python -from pain001.core import validate_xml_against_xsd - -xml_message_type = 'pain.001.001.03' -xml_file = 'generated.xml' -xsd_file = 'schema.xsd' - -is_valid = validate_xml_against_xsd( - xml_message_type, - xml_file, - xsd_file -) -print(f"XML validation result: {is_valid}") -``` - -## Documentation 📖 - -> ℹ️ **Info:** Do check out our for more information on -the Pain001 documentation. -""".strip() +with open("README.md") as f: + LONG_DESCRIPTION = f.read() SHORT_DESCRIPTION = """ -Pain001 is a Python Library for Automating ISO 20022-Compliant Payment Files -Using CSV Data.""".strip() - -DEPENDENCIES = ["xmlschema>=2.3.0"] +Pain001, A Python Library for Automating ISO 20022-Compliant Payment Files +Using CSV Or SQlite Data Files. +""".strip() -TEST_DEPENDENCIES = ["xmlschema>=2.3.0", "pytest>=7.3.1"] +DEPENDENCIES = [ + "click==8.1.3", + "defusedxml==0.7.1", + "rich==13.4.2", + "xmlschema==2.3.1", +] -VERSION = "0.0.19" +TEST_DEPENDENCIES = [ + "pytest>=7.3.1", +] +NAME = "pain001" URL = "https://github.com/sebastienrousseau/pain001" +VERSION = "0.0.20" setup( - name="pain001", + name=NAME, version=VERSION, description=SHORT_DESCRIPTION, long_description=LONG_DESCRIPTION, + long_description_content_type="text/markdown", url=URL, author="Sebastien Rousseau", author_email="sebastian.rousseau@gmail.com", @@ -193,21 +55,19 @@ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Financial and Insurance Industry", - "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", + "Operating System :: MacOS", "Operating System :: OS Independent", "Operating System :: POSIX", - "Operating System :: MacOS", "Operating System :: Unix", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", ], keywords=""" - Pain001, finance python library, ISO 20022, payment files, payment - processing, automate payments, ISO 20022-compliant, SWIFT, SEPA, payment - initiation messages, + pain001,iso20022,payment-processing,automate-payments,sepa,financial,banking-payments,csv,sqlite """, packages=["pain001"], install_requires=DEPENDENCIES, diff --git a/templates/pain.001.001.03/template.db b/templates/pain.001.001.03/template.db new file mode 100644 index 0000000..ab37c32 Binary files /dev/null and b/templates/pain.001.001.03/template.db differ diff --git a/templates/pain.001.001.03/template.xml b/templates/pain.001.001.03/template.xml index 7156a03..2313743 100644 --- a/templates/pain.001.001.03/template.xml +++ b/templates/pain.001.001.03/template.xml @@ -34,6 +34,16 @@ + + BANKXXXXX + Bank Name + + Country Code + Postal Code + Street Name + Building Number + Town Name + {% for tx in transactions %} diff --git a/templates/template_updated.xml b/templates/template_updated.xml deleted file mode 100644 index 39808ad..0000000 --- a/templates/template_updated.xml +++ /dev/null @@ -1,473 +0,0 @@ - - - - - 1 - 2023-03-10T15:30:47.000Z - 2 - - John Doe - - - - Payment-Info-12345 - TRF - true - 2 - 2 - - - SEPA - - - 2023-03-12 - - Acme Corp - - - - DE75512108001245126162 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID6789 - - - 150.00 - - - - SPUEDE2UXXX - - - - Global Tech - - - Invoice-98765 - - - - - Payment-Info-12345 - TRF - true - 2 - 2 - - - SEPA - - - 2023-03-12 - - Acme Corp - - - - DE75512108001245126162 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID6789 - - - 150.00 - - - - SPUEDE2UXXX - - - - Global Tech - - - Invoice-98765 - - - - - Payment-Info-67890 - TRF - true - 3 - 3 - - - SEPA - - - 2023-03-14 - - Brown Industries - - - - DE44500105175407324931 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID4321 - - - 300.00 - - - - SPUEDE2UXXX - - - - Green Energy - - - Invoice-12345 - - - - - Payment-Info-67890 - TRF - true - 3 - 3 - - - SEPA - - - 2023-03-14 - - Brown Industries - - - - DE44500105175407324931 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID4321 - - - 300.00 - - - - SPUEDE2UXXX - - - - Green Energy - - - Invoice-12345 - - - - - Payment-Info-24680 - TRF - true - 1 - 1 - - - SEPA - - - 2023-03-13 - - Alpha Electronics - - - - DE47500105175711000100 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID1357 - - - 250.00 - - - - SPUEDE2UXXX - - - - Beta Chemicals - - - Invoice-24680 - - - - - Payment-Info-24680 - TRF - true - 1 - 1 - - - SEPA - - - 2023-03-13 - - Alpha Electronics - - - - DE47500105175711000100 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID1357 - - - 250.00 - - - - SPUEDE2UXXX - - - - Beta Chemicals - - - Invoice-24680 - - - - - Payment-Info-86420 - TRF - true - 2 - 2 - - - SEPA - - - 2023-03-15 - - Delta Manufacturing - - - - DE25500105176602214896 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID2468 - - - 175.00 - - - - SPUEDE2UXXX - - - - Gamma Logistics - - - Invoice-86420 - - - - - Payment-Info-86420 - TRF - true - 2 - 2 - - - SEPA - - - 2023-03-15 - - Delta Manufacturing - - - - DE25500105176602214896 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID2468 - - - 175.00 - - - - SPUEDE2UXXX - - - - Gamma Logistics - - - Invoice-86420 - - - - - Payment-Info-59162 - TRF - true - 3 - 3 - - - SEPA - - - 2023-03-16 - - Smart Solutions - - - - DE86500105174603245678 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID4815 - - - 225.00 - - - - SPUEDE2UXXX - - - - Eco Innovations - - - Invoice-59162 - - - - - Payment-Info-59162 - TRF - true - 3 - 3 - - - SEPA - - - 2023-03-16 - - Smart Solutions - - - - DE86500105174603245678 - - - - - BANKDEFFXXX - - - SLEV - - - PaymentID4815 - - - 225.00 - - - - SPUEDE2UXXX - - - - Eco Innovations - - - Invoice-59162 - - - - - diff --git a/tests/data/invalid.xml b/tests/data/invalid.xml deleted file mode 100644 index ff64da6..0000000 --- a/tests/data/invalid.xml +++ /dev/null @@ -1,2 +0,0 @@ - -

Hello world!

\ No newline at end of file diff --git a/tests/test_context.py b/tests/test_context.py index 9aa1bec..507b1b1 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -5,49 +5,92 @@ class TestContext(unittest.TestCase): + """Unit tests for the Context class.""" + def setUp(self): - self.context = Context.get_instance() - self.context.set_name("test_context") + """Set up the test fixture.""" + Context.instance = None def tearDown(self): - self.context.logger = None + """Tear down the test fixture.""" + if Context.instance: + Context.instance.logger = None Context.instance = None - def test_get_instance(self): - # Test that the first call to get_instance() creates a new instance - # of the class. - first_instance = Context.get_instance() - self.assertIsNotNone(first_instance) - - # Test that subsequent calls to get_instance() return the same - # instance. - second_instance = Context.get_instance() - self.assertEqual(first_instance, second_instance) + def test_singleton(self): + """Test that Context is a singleton.""" + context1 = Context() + context2 = Context.get_instance() + self.assertEqual(context1, context2) + with self.assertRaises(Exception): + Context() def test_set_name(self): - # Test that set_name() sets the name of the logger. - context = self.context + """Test that set_name() sets the name of the logger.""" + context = Context.get_instance() context.set_name("my_context") self.assertEqual(context.name, "my_context") def test_set_log_level(self): - # Test that set_log_level() sets the log level of the logger. - context = self.context - context.set_log_level(logging.DEBUG) - self.assertEqual(context.log_level, logging.DEBUG) + """Test that set_log_level() sets the log level of the logger""" + context = Context.get_instance() - # Test that set_log_level() raises an exception if the log level is - # invalid. + """Test all valid log levels""" + valid_log_levels = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + } + + for level_str, level_int in valid_log_levels.items(): + context.set_log_level(level_str) + self.assertEqual(context.log_level, level_int) + context.set_log_level(level_int) + self.assertEqual(context.log_level, level_int) + + """ + Test that set_log_level() raises an exception if the log level is + invalid. + """ with self.assertRaises(Exception): context.set_log_level("INVALID") + with self.assertRaises(Exception): + context.set_log_level(12345) # some invalid int + + def test_init_logger(self): + """Test that init_logger() initializes the logger.""" + context = Context.get_instance() + + """Ensure the logger is not initialized""" + context.logger = None + context.init_logger() + self.assertIsNotNone(context.logger) + + """Test that init_logger() raises an exception if called again.""" + with self.assertRaises(Exception): + context.init_logger() def test_get_logger(self): - # Test that get_logger() returns the logger. - context = self.context + """Test that get_logger() returns the logger.""" + context = Context.get_instance() logger = context.get_logger() self.assertIsNotNone(logger) self.assertEqual(logger, context.logger) + """Test that get_logger() can initialize the logger.""" + context.logger = None + logger = context.get_logger() + self.assertIsNotNone(logger) + self.assertEqual(logger, context.logger) + + def test_log_level_propagation(self): + """Test that the log level is correctly propagated to the logger.""" + context = Context.get_instance() + context.set_log_level(logging.DEBUG) + self.assertEqual(context.logger.level, logging.DEBUG) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py index 57d8e3d..3792194 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,11 +1,10 @@ -import os import pytest import sys from contextlib import contextmanager from io import StringIO -from pain001.core import process_files +from pain001.core.core import process_files @contextmanager @@ -18,13 +17,6 @@ def catch_stdout(): class TestProcessFiles: - @pytest.fixture(autouse=True) - def setup_teardown(self): - self.output_file_path = "tests/data/pain.001.001.03.xml" - yield - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) - def test_invalid_csv_data(self): """ Test case for processing files with invalid CSV data. @@ -36,7 +28,6 @@ def test_invalid_csv_data(self): "tests/data/template.xml", "tests/data/template.xsd", "tests/data/invalid.csv", - self.output_file_path, ) assert exc_info.value.code == 1 @@ -51,7 +42,6 @@ def test_invalid_xml_message_type(self): "tests/data/template.xml", "tests/data/template.xsd", "tests/data/template.csv", - self.output_file_path, ) error_message = str(exc_info.value) @@ -70,7 +60,6 @@ def test_nonexistent_data_file_path(self): "tests/data/template.xml", "tests/data/template.xsd", "tests/data/nonexistent.csv", - self.output_file_path, ) assert ( str(exc_info.value) @@ -87,7 +76,6 @@ def test_nonexistent_xml_file_path(self): "tests/data/nonexistent.xml", "tests/data/template.xsd", "tests/data/template.csv", - self.output_file_path, ) # assert exc_info.value.code == 1 @@ -101,7 +89,6 @@ def test_nonexistent_xsd_file_path(self): "tests/data/template.xml", "tests/data/nonexistent.xsd", "tests/data/template.csv", - self.output_file_path, ) # assert exc_info.value.code == 1 @@ -114,13 +101,8 @@ def test_successful_execution(self): "tests/data/template.xml", "tests/data/template.xsd", "tests/data/template.csv", - self.output_file_path, ) - output_file_exists = os.path.exists(self.output_file_path) - if not output_file_exists: - raise AssertionError("Output file does not exist.") - def test_unsupported_data_file_type(self): """ Test case for processing files with an unsupported data file type. @@ -131,7 +113,6 @@ def test_unsupported_data_file_type(self): "tests/data/template.xml", "tests/data/template.xsd", "tests/data/invalid.rtf", - self.output_file_path, ) assert ( str(exc_info.value) == "Error: Unsupported data file type." @@ -151,11 +132,7 @@ def test_uses_sqlite_database(self): xml_file_path, xsd_file_path, data_file_path, - self.output_file_path, ) - output_file_exists = os.path.exists(self.output_file_path) - if not output_file_exists: - raise AssertionError("Output file does not exist.") def test_valid_xml_message_type(self): """ @@ -166,8 +143,4 @@ def test_valid_xml_message_type(self): "tests/data/template.xml", "tests/data/template.xsd", "tests/data/template.csv", - self.output_file_path, ) - output_file_exists = os.path.exists(self.output_file_path) - if not output_file_exists: - raise AssertionError("Output file does not exist.") diff --git a/tests/test_create_xml_element.py b/tests/test_create_xml_element.py index 91f3182..0c9f1c4 100644 --- a/tests/test_create_xml_element.py +++ b/tests/test_create_xml_element.py @@ -6,40 +6,53 @@ class TestCreateXmlElement(unittest.TestCase): - def test_create_element_with_tag_only(self): - root = ET.Element('root') - elem = create_xml_element(root, 'test') - self.assertEqual(elem.tag, 'test') + """ + Test if the XML element is created correctly with a tag only. + """ + root = ET.Element("root") + elem = create_xml_element(root, "test") + self.assertEqual(elem.tag, "test") self.assertIsNone(elem.text) - self.assertEqual(root.find('test'), elem) + self.assertEqual(root.find("test"), elem) def test_create_element_with_tag_and_text(self): - root = ET.Element('root') - elem = create_xml_element(root, 'test', text='Hello, world!') - self.assertEqual(elem.tag, 'test') - self.assertEqual(elem.text, 'Hello, world!') - self.assertEqual(root.find('test'), elem) + """ + Test if the XML element is created correctly with a tag and text. + """ + root = ET.Element("root") + elem = create_xml_element(root, "test", text="Hello, world!") + self.assertEqual(elem.tag, "test") + self.assertEqual(elem.text, "Hello, world!") + self.assertEqual(root.find("test"), elem) def test_create_element_with_tag_and_attributes(self): - root = ET.Element('root') - attributes = {'attr1': 'value1', 'attr2': 'value2'} - elem = create_xml_element(root, 'test', attributes=attributes) - self.assertEqual(elem.tag, 'test') + """ + Test if the XML element is created correctly with a tag and attributes. + """ + root = ET.Element("root") + attributes = {"attr1": "value1", "attr2": "value2"} + elem = create_xml_element(root, "test", attributes=attributes) + self.assertEqual(elem.tag, "test") self.assertIsNone(elem.text) self.assertEqual(elem.attrib, attributes) - self.assertEqual(root.find('test'), elem) + self.assertEqual(root.find("test"), elem) def test_create_element_with_tag_text_and_attributes(self): - root = ET.Element('root') - attributes = {'attr1': 'value1', 'attr2': 'value2'} + """ + Test if the XML element is created correctly with a tag, text and + attributes. + """ + root = ET.Element("root") + attributes = {"attr1": "value1", "attr2": "value2"} elem = create_xml_element( - root, 'test', text='Hello, world!', attributes=attributes) - self.assertEqual(elem.tag, 'test') - self.assertEqual(elem.text, 'Hello, world!') + root, "test", text="Hello, world!", attributes=attributes + ) + self.assertEqual(elem.tag, "test") + self.assertEqual(elem.text, "Hello, world!") self.assertEqual(elem.attrib, attributes) - self.assertEqual(root.find('test'), elem) + self.assertEqual(root.find("test"), elem) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_generate_xml.py b/tests/test_generate_xml.py index 559d4a6..96e1a06 100644 --- a/tests/test_generate_xml.py +++ b/tests/test_generate_xml.py @@ -11,6 +11,9 @@ class TestXMLCreation(unittest.TestCase): def setUp(self): + """ + Test setup + """ self.root = ET.Element("Root") self.row = { "initiator_name": "Initiator", @@ -40,6 +43,9 @@ def setUp(self): } def test_create_common_elements(self): + """ + Test create_common_elements + """ create_common_elements(self.root, self.row, self.mapping) self.assertEqual(len(self.root), 2) self.assertEqual(self.root[0].tag, "PmtInfId") @@ -48,6 +54,9 @@ def test_create_common_elements(self): self.assertEqual(self.root[1].text, "pain.001.001.09") def test_create_xml_v3(self): + """ + Test create_xml_v3 + """ create_xml_v3(self.root, [self.row], self.mapping) cstmr_cdt_trf_initn_element = self.root[0] self.assertEqual( @@ -56,6 +65,10 @@ def test_create_xml_v3(self): # You can continue to assert more conditions based on your expectations def test_create_xml_v9(self): + """ + Test create_xml_v9 + """ + create_xml_v9(self.root, [self.row], self.mapping) cstmr_cdt_trf_initn_element = self.root[0] self.assertEqual( diff --git a/tests/test_main.py b/tests/test_main.py index 7db484a..7f76ca1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,79 +1,154 @@ -import io -import pytest -import sys +from click.testing import CliRunner from pain001.__main__ import main -from unittest.mock import patch class TestMain: def setup_method(self): + self.runner = CliRunner() self.xml_message_type = "pain.001.001.03" self.xml_file = "tests/data/template.xml" self.xsd_file = "tests/data/template.xsd" self.csv_file = "tests/data/template.csv" - self.output_file = "tests/data/output.xml" def test_main_with_valid_files(self): - with patch.object( - sys, - "argv", + result = self.runner.invoke( + main, [ - "", + "--xml_message_type", self.xml_message_type, + "--xml_template_file_path", self.xml_file, + "--xsd_schema_file_path", self.xsd_file, + "--data_file_path", self.csv_file, - self.output_file, ], - ): - with patch( - "sys.stdout", new_callable=io.StringIO - ) as captured_output: - main() - assert ( - "❯ XML located at tests/data/pain.001.001.03.xml is valid." - in captured_output.getvalue() - ) + ) + assert ( + "The XML has been validated against tests/data/template.xsd\n" + in result.output + ) + assert result.exit_code == 0 - def test_main_with_invalid_csv_file(self): - with patch.object( - sys, - "argv", + def test_main_with_missing_xml_message_type(self): + result = self.runner.invoke( + main, [ - "", + "--xml_template_file_path", + self.xml_file, + "--xsd_schema_file_path", + self.xsd_file, + "--data_file_path", + self.csv_file, + ], + ) + assert result.exit_code == 1 + assert "Error: xml_message_type is required." in result.output + + def test_main_with_missing_xsd_template_file(self): + result = self.runner.invoke( + main, + [ + "--xml_message_type", self.xml_message_type, + "--xml_template_file_path", self.xml_file, + "--data_file_path", + self.csv_file, + ], + ) + assert result.exit_code == 1 + assert ( + "Error: xsd_schema_file_path is required." in result.output + ) + + def test_main_with_missing_data_file(self): + result = self.runner.invoke( + main, + [ + "--xml_message_type", + self.xml_message_type, + "--xml_template_file_path", + self.xml_file, + "--xsd_schema_file_path", self.xsd_file, - "tests/data/invalid.csv", - self.output_file, ], - ): - with patch( - "sys.stdout", new_callable=io.StringIO - ) as captured_output: - with pytest.raises(SystemExit) as exc_info: - main() + ) + assert result.exit_code == 1 + assert "Error: data_file_path is required." in result.output - assert exc_info.type == SystemExit - assert exc_info.value.code == 1 - expected_error_message = "Error: No data to process." - assert expected_error_message in captured_output.getvalue() + def test_main_with_invalid_xml_message_type(self): + result = self.runner.invoke( + main, + [ + "--xml_message_type", + "invalid", + "--xml_template_file_path", + self.xml_file, + "--xsd_schema_file_path", + self.xsd_file, + "--data_file_path", + self.csv_file, + ], + ) + assert result.exit_code == 1 + assert "Invalid XML message type: invalid." in result.output - def test_main_with_invalid_xml_file(self): - with pytest.raises(SystemExit) as exc_info: - with patch.object( - sys, - "argv", - [ - "", - self.xml_message_type, - "tests/data/invalid.xml", - self.xsd_file, - self.csv_file, - self.output_file, - ], - ): - main() + def test_main_with_invalid_xml_template_file(self): + result = self.runner.invoke( + main, + [ + "--xml_message_type", + self.xml_message_type, + "--xml_template_file_path", + "invalid", + "--xsd_schema_file_path", + self.xsd_file, + "--data_file_path", + self.csv_file, + ], + ) + assert result.exit_code == 1 + assert ( + "The XML template file 'invalid' does not exist." + in result.output + ) + + def test_main_with_invalid_xsd_template_file(self): + result = self.runner.invoke( + main, + [ + "--xml_message_type", + self.xml_message_type, + "--xml_template_file_path", + self.xml_file, + "--xsd_schema_file_path", + "invalid", + "--data_file_path", + self.csv_file, + ], + ) + assert result.exit_code == 1 + assert ( + "The XSD template file 'invalid' does not exist." + in result.output + ) - assert exc_info.type == SystemExit - assert exc_info.value.code == 1 + def test_main_with_invalid_data_file(self): + result = self.runner.invoke( + main, + [ + "--xml_message_type", + self.xml_message_type, + "--xml_template_file_path", + self.xml_file, + "--xsd_schema_file_path", + self.xsd_file, + "--data_file_path", + "invalid", + ], + ) + assert result.exit_code == 1 + assert ( + "The data file 'invalid' does not exist." in result.output + ) diff --git a/tests/test_validate_via_xsd.py b/tests/test_validate_via_xsd.py index 39e428b..5b727ef 100644 --- a/tests/test_validate_via_xsd.py +++ b/tests/test_validate_via_xsd.py @@ -6,29 +6,36 @@ class TestValidateViaXsd(unittest.TestCase): - def setUp(self): - self.valid_xml_file = 'valid_test.xml' - self.invalid_xml_file = 'invalid_test.xml' - self.xsd_file = 'test_schema.xsd' + """ + Test case setup method. + """ + self.valid_xml_file = "valid_test.xml" + self.invalid_xml_file = "invalid_test.xml" + self.xsd_file = "test_schema.xsd" # Create valid XML test file - with open(self.valid_xml_file, 'w') as f: - f.write(''' + with open(self.valid_xml_file, "w") as f: + f.write( + """ Valid data - ''') + """ + ) # Create invalid XML test file - with open(self.invalid_xml_file, 'w') as f: - f.write(''' + with open(self.invalid_xml_file, "w") as f: + f.write( + """ Invalid data - ''') + """ + ) # Create test XSD schema file - with open(self.xsd_file, 'w') as f: - f.write(''' + with open(self.xsd_file, "w") as f: + f.write( + """ @@ -42,17 +49,30 @@ def setUp(self): - ''') + """ + ) def tearDown(self): + """ + Test case tear down method. + """ os.remove(self.valid_xml_file) os.remove(self.invalid_xml_file) os.remove(self.xsd_file) def test_valid_xml(self): + """ + Test case for validating a valid XML file against an XSD schema. + """ assert validate_via_xsd(self.valid_xml_file, self.xsd_file) def test_invalid_xml(self): + """ + Test case for validating an invalid XML file against an XSD schema. + """ + assert not validate_via_xsd( + self.invalid_xml_file, self.xsd_file + ) assert not validate_via_xsd( self.invalid_xml_file, self.xsd_file ) diff --git a/tests/test_xml_generator.py b/tests/test_xml_generator.py index 48f3dc1..91f8c42 100644 --- a/tests/test_xml_generator.py +++ b/tests/test_xml_generator.py @@ -5,6 +5,11 @@ class TestXmlGenerator(unittest.TestCase): def test_xml_generator_with_invalid_input(self): + """ + Test if the XML generator exits with a non-zero exit code when + invalid input is provided. + """ + # Arrange data = { "amount": "100.00",