diff --git a/.github/CODE-OF-CONDUCT.md b/.github/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..2d809d3 --- /dev/null +++ b/.github/CODE-OF-CONDUCT.md @@ -0,0 +1,76 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our +project and our community a harassment-free experience for everyone, +regardless of age, body size, disability, ethnicity, gender identity and +expression, level of experience, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behaviour that contributes to creating a positive +environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behaviour by participants include: + +- The use of sexualized language or imagery and unwelcome sexual + attention or advances +- Trolling, insulting/derogatory comments, and personal or political + attacks +- Public or private harassment +- Publishing others' private information, such as a physical or + electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of +acceptable behaviour and are expected to take appropriate and fair +corrective action in response to any instances of unacceptable +behaviour. + +Project maintainers have the right and responsibility to remove, edit, +or reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, or to ban +temporarily or permanently any contributor for other behaviors that they +deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public +spaces when an individual is representing the project or its community. +Examples of representing a project or community include using an +official project e-mail address, posting via an official social media +account, or acting as an appointed representative at an online or +offline event. Representation of a project may be further defined and +clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may +be reported by contacting the project team. The project team will review +and investigate all complaints, and will respond in a way that it deems +appropriate to the circumstances. The project team is obligated to +maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted +separately. + +Project maintainers who do not follow or enforce the Code of Conduct in +good faith may face temporary or permanent repercussions as determined +by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant homepage, +version 1.4, available at +[version](http://contributor-covenant.org/version/1/4) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0f7c5e5 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +@sebastienrousseau/bankstatementparser \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..08cf9b8 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,26 @@ +# Security + +We take the security of our software products and services seriously, +which includes all source code repositories managed through our GitHub +repositories. + +If you believe you have found a security vulnerability in any of our +repository, please report it to us as described below. + +## Reporting Security Issues + +Please include the requested information listed below (as much as you +can provide) to help us better understand the nature and scope of the +possible issue: + +- Type of issue (e.g. buffer overflow, SQL injection, cross-site + scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct + URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. diff --git a/.github/funding.yml b/.github/funding.yml new file mode 100644 index 0000000..e337c29 --- /dev/null +++ b/.github/funding.yml @@ -0,0 +1,2 @@ +github: sebastienrousseau +custom: https://paypal.me/wwdseb diff --git a/bankstatementparser/__init__.py b/bankstatementparser/__init__.py index f7e0cd3..11242e8 100644 --- a/bankstatementparser/__init__.py +++ b/bankstatementparser/__init__.py @@ -8,19 +8,25 @@ # # 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. +# 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. """ -`bankstatementparser` package provides useful tools for finance and treasury -specialists. +`bankstatementparser` package provides useful tools for finance and +treasury specialists. -This package includes modules for parsing bank statements in various formats, -as well as other utilities commonly used in finance and treasury operations. +This package includes modules for parsing bank statements in various +formats, as well as other utilities commonly used in finance and treasury +operations. """ -from .bank_statement_parsers import * +from .bank_statement_parsers import __init__ from .camt_parser import CamtParser from .pain001_parser import Pain001Parser + +__all__ = [ + 'CamtParser', + 'Pain001Parser', + '__init__' +] diff --git a/bankstatementparser/bank_statement_parsers.py b/bankstatementparser/bank_statement_parsers.py index 4a91720..cc0529e 100644 --- a/bankstatementparser/bank_statement_parsers.py +++ b/bankstatementparser/bank_statement_parsers.py @@ -16,7 +16,8 @@ """ bank_statement_parsers.py -This module provides classes for parsing different types of bank statement files. +This module provides classes for parsing different types of bank statement +files. """ import os @@ -24,6 +25,7 @@ from lxml import etree import pandas as pd + class FileParserError(Exception): """Custom exception for file parsing errors.""" pass @@ -37,7 +39,8 @@ class Pain001Parser: batches (list): List of batch elements parsed from the file. payments (list): List of parsed payment dictionaries. batches_count (int): The number of payment batches in the file. - total_payments_count (int): The total number of payments across all batches. + total_payments_count (int): The total number of payments across all + batches. """ def __init__(self, file_name): @@ -58,7 +61,7 @@ def __init__(self, file_name): raise FileNotFoundError(f"File {file_name} not found!") from e # Clean up the XML namespaces to simplify the XPath expressions. - data = re.sub('', '', data) + data = re.sub('', '', data) data = bytes(data, 'utf-8') # Parse the XML content. @@ -82,7 +85,8 @@ def _parse_batch_header(self, batch): Parses header data for a payment batch. Parameters: - batch (etree._Element): The XML element representing a payment batch. + batch (etree._Element): The XML element representing a payment + batch. Returns: dict: A dictionary containing header information of the batch. @@ -90,7 +94,11 @@ def _parse_batch_header(self, batch): # Extract relevant information from the batch header. execution_date = batch.xpath('.//ReqdExctnDt')[0].text debtor_name = batch.xpath('.//Dbtr/Nm')[0].text - debtor_account = batch.xpath('.//DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id')[0].text if batch.xpath('.//DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id') else '' + debtor_account = ( + batch.xpath('.//DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id')[0].text + if batch.xpath('.//DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id') + else '' + ) return { 'debtor_name': debtor_name, @@ -103,7 +111,8 @@ def _parse_batch(self, batch): Parses all payments in a payment batch. Parameters: - batch (etree._Element): The XML element representing a payment batch. + batch (etree._Element): The XML element representing a payment + batch. Returns: list: A list of dictionaries, each representing a payment. @@ -125,7 +134,8 @@ def _parse_payment(self, payment): Parses a single payment within a payment batch. Parameters: - payment (etree._Element): The XML element representing a single payment. + payment (etree._Element): The XML element representing a single + payment. Returns: dict: A dictionary containing information about the payment. @@ -134,8 +144,16 @@ def _parse_payment(self, payment): amount = payment.xpath('.//InstdAmt')[0].text currency = payment.xpath('.//InstdAmt/@Ccy')[0] name = payment.xpath('.//Cdtr/Nm')[0].text - account = payment.xpath('.//CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id')[0].text if payment.xpath('.//CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id') else '' - country = payment.xpath('.//Ctry')[0].text if payment.xpath('.//Ctry') else '' + account = ( + payment.xpath('.//CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id')[0].text + if payment.xpath('.//CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id') + else '' + ) + country = ( + payment.xpath( + './/Ctry' + )[0].text if payment.xpath('.//Ctry') else '' + ) references = [ref.text for ref in payment.xpath('.//RmtInf/Ustrd')] reference = ' '.join(references) address_lines = [line.text for line in payment.xpath('.//AdrLine')] @@ -158,7 +176,10 @@ def __repr__(self): Returns: str: A string representation of the instance. """ - return f"Pain001Parser(batches={self.batches_count}, payments={self.total_payments_count})" + return ( + f"Pain001Parser(batches={self.batches_count}, " + f"payments={self.total_payments_count})" + ) class Camt053Parser: @@ -166,8 +187,10 @@ class Camt053Parser: Parser for CAMT.053 bank account statement files. Attributes: - statements (list): A list of dictionaries, each representing a statement. - transactions (list): A list of dictionaries, each representing a transaction. + statements (list): A list of dictionaries, each representing a + statement. + transactions (list): A list of dictionaries, each representing a + transaction. """ # Balance type definitions. @@ -179,14 +202,16 @@ class Camt053Parser: def __init__(self, file_name): """ - Initializes the parser and parses statements and transactions from the given file. + Initializes the parser and parses statements and transactions from the + given file. Parameters: file_name (str): The path to the CAMT.053 XML file. Raises: FileNotFoundError: If the specified file cannot be found. - FileParserError: If the file is not a valid CAMT.053 file or if it does not contain any statements. + FileParserError: If the file is not a valid CAMT.053 file or if it + does not contain any statements. """ # Attempt to open and read the file content. try: @@ -196,7 +221,9 @@ def __init__(self, file_name): raise FileNotFoundError(f"File {file_name} not found!") from e # Clean up the XML namespaces to simplify the XPath expressions. - data = data.replace('xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"', '') + data = data.replace( + 'xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"', '' + ) data = bytes(data, 'utf-8') # Parse the XML content. @@ -220,14 +247,17 @@ def _parse_statement_header(self, stmt): Parses header data for a bank statement. Parameters: - stmt (etree._Element): The XML element representing a bank statement. + stmt (etree._Element): The XML element representing a bank + statement. Returns: dict: A dictionary containing header information of the statement. """ # Extract relevant information from the statement header. account_id = stmt.xpath('./Acct/Id/IBAN|./Acct/Id/Othr/Id')[0].text - name = stmt.xpath('./Acct/Nm')[0].text if stmt.xpath('./Acct/Nm') else '' + name = stmt.xpath( + './Acct/Nm' + )[0].text if stmt.xpath('./Acct/Nm') else '' stmt_id = stmt.xpath('./Id')[0].text return { @@ -241,7 +271,8 @@ def _parse_statement_balances(self, stmt): Parses balance amounts from a bank statement. Parameters: - stmt (etree._Element): The XML element representing a bank statement. + stmt (etree._Element): The XML element representing a bank + statement. Returns: dict: A dictionary containing balance information of the statement. @@ -265,7 +296,8 @@ def _parse_statement_summary(self, stmt): Parses summary statistics for a bank statement. Parameters: - stmt (etree._Element): The XML element representing a bank statement. + stmt (etree._Element): The XML element representing a bank + statement. Returns: dict: A dictionary containing summary statistics of the statement. @@ -282,7 +314,8 @@ def _parse_statements(self, stmt_list): Parses a list of bank statement elements. Parameters: - stmt_list (list of etree._Element): A list of XML elements, each representing a bank statement. + stmt_list (list of etree._Element): A list of XML elements, each + representing a bank statement. Returns: list: A list of dictionaries, each representing a bank statement. @@ -301,16 +334,29 @@ def _parse_transaction(self, entry): Parses a single transaction entry within a bank statement. Parameters: - entry (etree._Element): The XML element representing a transaction entry. + entry (etree._Element): The XML element representing a transaction + entry. Returns: dict: A dictionary containing information about the transaction. """ # Extract relevant information from the transaction entry. - debtor_name = entry.xpath('.//Dbtr/Nm')[0].text if entry.xpath('.//Dbtr/Nm') else '' - debtor_acct = entry.xpath('.//DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id')[0].text if entry.xpath('.//DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id') else '' - creditor_name = entry.xpath('.//Cdtr/Nm')[0].text if entry.xpath('.//Cdtr/Nm') else '' - creditor_acct = entry.xpath('.//CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id')[0].text if entry.xpath('.//CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id') else '' + debtor_name = entry.xpath( + './/Dbtr/Nm' + )[0].text if entry.xpath('.//Dbtr/Nm') else '' + debtor_acct = entry.xpath( + './/DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id' + )[0].text if entry.xpath( + './/DbtrAcct/Id/IBAN|.//DbtrAcct/Id/Othr/Id' + ) else '' + creditor_name = entry.xpath( + './/Cdtr/Nm' + )[0].text if entry.xpath('.//Cdtr/Nm') else '' + creditor_acct = entry.xpath( + './/CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id' + )[0].text if entry.xpath( + './/CdtrAcct/Id/IBAN|.//CdtrAcct/Id/Othr/Id' + ) else '' amount = float(entry.xpath('./Amt')[0].text) currency = entry.xpath('./Amt/@Ccy')[0] cdt_dbt = entry.xpath('./CdtDbtInd')[0].text @@ -318,8 +364,14 @@ def _parse_transaction(self, entry): amount *= -1 references = [ref.text for ref in entry.xpath('.//RmtInf/Ustrd')] reference = ' '.join(references) - value_date = entry.xpath('./ValDt/Dt')[0].text if entry.xpath('./ValDt/Dt') else None - booking_date = entry.xpath('./BookgDt/Dt')[0].text if entry.xpath('./BookgDt/Dt') else None + value_date = entry.xpath( + './ValDt/Dt' + )[0].text if entry.xpath( + './ValDt/Dt' + ) else None + booking_date = entry.xpath( + './BookgDt/Dt' + )[0].text if entry.xpath('./BookgDt/Dt') else None return { 'DebtorName': debtor_name, @@ -339,7 +391,8 @@ def _parse_transactions(self, stmt_list): Parses all transactions from a list of bank statement elements. Parameters: - stmt_list (list of etree._Element): A list of XML elements, each representing a bank statement. + stmt_list (list of etree._Element): A list of XML elements, each + representing a bank statement. Returns: list: A list of dictionaries, each representing a transaction. @@ -359,7 +412,11 @@ def __repr__(self): Returns: str: A string representation of the instance. """ - return f"Camt053Parser(statements={len(self.statements)}, transactions={len(self.transactions)})" + return ( + f"Camt053Parser(" + f"statements={len(self.statements)}, " + f"transactions={len(self.transactions)})" + ) def process_camt053_folder(folder): @@ -389,9 +446,13 @@ def process_camt053_folder(folder): # Append parsed data to the respective DataFrames. statement_rows = [s for s in parser.statements] - statements_df = pd.concat([statements_df, pd.DataFrame(statement_rows)]) + statements_df = pd.concat( + [statements_df, pd.DataFrame(statement_rows)] + ) transaction_rows = [t for t in parser.transactions] - transactions_df = pd.concat([transactions_df, pd.DataFrame(transaction_rows)]) + transactions_df = pd.concat( + [transactions_df, pd.DataFrame(transaction_rows)] + ) # Record the successful processing of the file. files_df.append({ diff --git a/bankstatementparser/camt_parser.py b/bankstatementparser/camt_parser.py index 7e4176c..8d3d722 100644 --- a/bankstatementparser/camt_parser.py +++ b/bankstatementparser/camt_parser.py @@ -32,7 +32,8 @@ class CamtParser: Class to parse CAMT format bank statement files. Attributes: - tree (etree.Element): The Element object representing the parsed XML file. + tree (etree.Element): The Element object representing the parsed XML + file. definitions (dict): Dictionary mapping balance codes to descriptions. """ @@ -264,7 +265,8 @@ def _get_element_text(self, parent, xpath): xpath (str): XPath expression to find the child element. Returns: - str: Text content of the child element if it exists, else an empty string. + str: Text content of the child element if it exists, else an empty + string. """ element = parent.xpath(xpath) return element[0].text if element else '' @@ -274,7 +276,8 @@ def _get_account_id(self, statement): Extracts the account ID from a bank statement. Parameters: - statement (etree.Element): XML Element representing the bank statement. + statement (etree.Element): XML Element representing the bank + statement. Returns: str: Account ID. @@ -307,7 +310,8 @@ def _get_statement_stats(self, statement): Extracts statistics for a single bank statement. Parameters: - statement (etree.Element): XML Element representing the bank statement. + statement (etree.Element): XML Element representing the bank + statement. Returns: dict: Statement statistics. @@ -316,7 +320,8 @@ def _get_statement_stats(self, statement): account_id = self._get_account_id(statement) created = self._get_element_text(statement, './CreDtTm') - # Get all transactions for the statement and calculate summary statistics + # Get all transactions for the statement and calculate summary + # statistics transactions = self._get_transactions_for_statement(statement) tx_df = pd.DataFrame(transactions) net_amount = tx_df['Amount'].sum() if len(tx_df) > 0 else 0 @@ -345,13 +350,17 @@ def camt_to_excel(self, filename): Parameters: filename (str): Path to the output Excel file. """ - # Retrieve dataframes for balances, transactions, and statement statistics + # Retrieve dataframes for balances, transactions, and statement + # statistics balances = self.get_account_balances() transactions = self.get_transactions() stats = self.get_statement_stats() # Write the dataframes to the Excel file using the openpyxl engine - with pd.ExcelWriter(filename, engine='openpyxl') as writer: # pylint: disable=E0110 + # pylint: disable=E0110 + with pd.ExcelWriter(filename, engine='openpyxl') as writer: balances.to_excel(writer, sheet_name='Balances', index=False) - transactions.to_excel(writer, sheet_name='Transactions', index=False) + transactions.to_excel( + writer, sheet_name='Transactions', index=False + ) stats.to_excel(writer, sheet_name='Stats', index=False) diff --git a/bankstatementparser/cli.py b/bankstatementparser/cli.py index a72cabe..8ef2191 100644 --- a/bankstatementparser/cli.py +++ b/bankstatementparser/cli.py @@ -14,8 +14,9 @@ # limitations under the License. """ -This module provides a command line interface for parsing bank statement files in various formats. -Currently, it supports CAMT (ISO 20022) format, with potential to extend support to other formats. +This module provides a command line interface for parsing bank statement +files in various formats. Currently, it supports CAMT (ISO 20022) format, with +potential to extend support to other formats. """ import argparse @@ -24,6 +25,7 @@ import sys from bankstatementparser import CamtParser, Pain001Parser + class BankStatementCLI: """A command line interface for parsing bank statement files.""" @@ -40,12 +42,15 @@ def setup_arg_parser(self): """ parser = argparse.ArgumentParser( description='Parse bank statement files.') - parser.add_argument('--type', type=str, required=True, choices=['camt', 'pain001'], - help='Type of the bank statement file. Choices: "camt" or "pain001".') - parser.add_argument('--input', type=str, required=True, - help='Path to the bank statement file.') - parser.add_argument('--output', type=str, required=False, - help='Path to save the parsed data. If not provided, data is printed to console.') + parser.add_argument( + '--type', type=str, required=True, choices=['camt', 'pain001'], + help='Type of the bank statement file: "camt" or "pain001".') + parser.add_argument( + '--input', type=str, required=True, + help='Path to the bank statement file.') + parser.add_argument( + '--output', type=str, required=False, + help='Path to save parsed data; if not provided, data is printed.') return parser def parse_camt(self, file_path, output_path=None): @@ -54,7 +59,8 @@ def parse_camt(self, file_path, output_path=None): Args: file_path (str): Path to the CAMT file. - output_path (str, optional): Path to save the parsed data. If None, data is printed to console. + output_path (str, optional): Path to save the parsed data. If None, + data is printed to console. """ try: parser = CamtParser(file_path) @@ -79,11 +85,13 @@ def parse_camt(self, file_path, output_path=None): def parse_pain(self, file_path, output_path=None): """ - Parse a PAIN.001 format bank statement file and print or save the results. + Parse a PAIN.001 format bank statement file and print or save the + results. Args: file_path (str): Path to the PAIN.001 file. - output_path (str, optional): Path to save the parsed data. If None, data is printed to console. + output_path (str, optional): Path to save the parsed data. If None, + data is printed to console. """ try: # Instantiate the PAIN.001 parser diff --git a/bankstatementparser/pain001_parser.py b/bankstatementparser/pain001_parser.py index 4a5b609..30b640c 100644 --- a/bankstatementparser/pain001_parser.py +++ b/bankstatementparser/pain001_parser.py @@ -34,7 +34,9 @@ def __init__(self, file_name): logger.error("File %s not found!", file_name) raise except Exception as e: - logger.error("An error occurred while reading the file: %s", str(e)) + logger.error( + "An error occurred while reading the file: %s", str(e) + ) raise try: @@ -62,27 +64,39 @@ def parse(self, output_file=None): group_header = root.find('.//CstmrCdtTrfInitn/GrpHdr') # Get the message identification - message_id = group_header.find('MsgId').text if group_header.find('MsgId') is not None else None + message_id = group_header.find( + 'MsgId' + ).text if group_header.find('MsgId') is not None else None print("Message Identification:", message_id) # Get the creation date and time - creation_date_time = group_header.find('CreDtTm').text if group_header.find('CreDtTm') is not None else None + creation_date_time = group_header.find( + 'CreDtTm' + ).text if group_header.find('CreDtTm') is not None else None print("Creation Date and Time:", creation_date_time) # Get the number of transactions - number_of_transactions = group_header.find('NbOfTxs').text if group_header.find('NbOfTxs') is not None else None + number_of_transactions = group_header.find( + 'NbOfTxs' + ).text if group_header.find('NbOfTxs') is not None else None print("Number of Transactions:", number_of_transactions) # Get the initiating party - initiating_party = group_header.find('.//InitgPty/Nm').text if group_header.find('.//InitgPty/Nm') is not None else None + initiating_party = group_header.find( + './/InitgPty/Nm' + ).text if group_header.find('.//InitgPty/Nm') is not None else None print("Initiating Party:", initiating_party) # Parse the payment information records - payment_info_records = root.findall('.//CstmrCdtTrfInitn/PmtInf') + payment_info_records = root.findall( + './/CstmrCdtTrfInitn/PmtInf' + ) payments = [] for pmt in payment_info_records: payment = {} - payment['PmtInfId'] = pmt.find('PmtInfId').text if pmt.find('PmtInfId') is not None else None + payment['PmtInfId'] = pmt.find( + 'PmtInfId' + ).text if pmt.find('PmtInfId') is not None else None # Additional payment information parsing omitted for brevity payments.append(payment) @@ -96,4 +110,4 @@ def parse(self, output_file=None): return df except Exception as e: - raise ParseError(f"Error parsing PAIN.001 file: {e}") \ No newline at end of file + raise ParseError(f"Error parsing PAIN.001 file: {e}") diff --git a/setup.py b/setup.py index 15aa4ff..67c5075 100644 --- a/setup.py +++ b/setup.py @@ -29,9 +29,9 @@ description=""" BankStatementParser is your essential tool for easy bank statement management. Designed with finance and treasury experts in mind, it offers a simple way to -handle CAMT (ISO 20022) formats and more. Get quick, accurate insights from your -financial data and spend less time on processing. It's the smart, hassle-free -way to stay on top of your transactions. +handle CAMT (ISO 20022) formats and more. Get quick, accurate insights from +your financial data and spend less time on processing. It's the smart, hassle- +free way to stay on top of your transactions. """, long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/test_data/camt_file.csv b/tests/test_data/camt_file.csv new file mode 100644 index 0000000..0d40285 --- /dev/null +++ b/tests/test_data/camt_file.csv @@ -0,0 +1,2 @@ +AccountId,StatementCreated,NumTransactions,NetAmount +50000000054910000003,2010-10-18T17:00:00+01:00,3,-64321.5 diff --git a/tests/test_data/pain_file.csv b/tests/test_data/pain_file.csv new file mode 100644 index 0000000..2ad68ba --- /dev/null +++ b/tests/test_data/pain_file.csv @@ -0,0 +1,7 @@ +PmtInfId +Payment-Info-12345 +Payment-Info-12345 +Payment-Info-67890 +Payment-Info-67890 +Payment-Info-24680 +Payment-Info-24680