git: 4d7c13d482b2 - 2026Q3 - finance/ktoblzcheck-data: Update bank data

From: Jason E. Hale <jhale_at_FreeBSD.org>
Date: Tue, 14 Jul 2026 04:17:45 UTC
The branch 2026Q3 has been updated by jhale:

URL: https://cgit.FreeBSD.org/ports/commit/?id=4d7c13d482b202b6e61c625b6d011c3d125d50da

commit 4d7c13d482b202b6e61c625b6d011c3d125d50da
Author:     Jason E. Hale <jhale@FreeBSD.org>
AuthorDate: 2026-07-14 03:47:02 +0000
Commit:     Jason E. Hale <jhale@FreeBSD.org>
CommitDate: 2026-07-14 04:17:14 +0000

    finance/ktoblzcheck-data: Update bank data
    
    Bank data is valid from 2026-06-08 until 2026-09-06.
    
    Integrate my data update script into the port to keep it under version
    control, easier future updates of the port, and for full transparency.
    
    MFH:            2026Q3
    
    (cherry picked from commit 4439524120c6e2c84d3d7394e000024624dd31a2)
---
 finance/ktoblzcheck-data/Makefile                  |  51 ++++-
 finance/ktoblzcheck-data/distinfo                  |   6 +-
 .../files/ktoblzcheck-data-update.py               | 226 +++++++++++++++++++++
 3 files changed, 275 insertions(+), 8 deletions(-)

diff --git a/finance/ktoblzcheck-data/Makefile b/finance/ktoblzcheck-data/Makefile
index 0859095b1bce..4903425124fb 100644
--- a/finance/ktoblzcheck-data/Makefile
+++ b/finance/ktoblzcheck-data/Makefile
@@ -5,7 +5,7 @@ MASTER_SITES=	SF/${PORTNAME} \
 		LOCAL/jhale/${PORTNAME}:bankdata
 PKGNAMESUFFIX=	-data
 DISTFILES=	${PORTNAME}${PKGNAMESUFFIX}-${KBCD_DATE}${EXTRACT_SUFX} \
-		${PORTNAME}-bankdata-${BANKDATA_START_DATE}${EXTRACT_SUFX}:bankdata
+		${BANKDATA_DISTFILE}:bankdata
 
 MAINTAINER=	jhale@FreeBSD.org
 COMMENT=	Bank data used by ktoblzcheck
@@ -33,11 +33,16 @@ PLIST_SUB=	BANKDATA_START_DATE=${BANKDATA_START_DATE}
 # the most recent bank data. This is just used for build system changes.
 KBCD_DATE=		20250515
 # Custom: This is the date when the bank data is officially recocognized and
-# supported.
-BANKDATA_START_DATE=	20251208
+# supported. Do not edit manually, unless you really know what you are doing!
+# Run `make ktoblzcheck-data-update` to update this value automatically.
+BANKDATA_START_DATE=	20260608
 # Custom: This is the date when the bank data is officially void and can no
-# longer be relied upon.
-BANKDATA_END_DATE=	20260308
+# longer be relied upon. Do not edit manually, unless you really know what you
+# are doing! Run `make ktoblzcheck-data-update` to update this value
+# automatically.
+BANKDATA_END_DATE=	20260906
+# Custom: Raw distfile name for the bank data.
+BANKDATA_DISTFILE=	${PORTNAME}-bankdata-${BANKDATA_START_DATE}${EXTRACT_SUFX}
 
 post-extract:
 # Out of an abundance of caution, remove the pre-built DBs and shipped SEPA data.
@@ -52,4 +57,40 @@ post-patch:
 	@${REINPLACE_CMD} -e 's|%%BANKDATA_END_DATE%%|'"$$(date -j -f "%Y%m%d" "+%d.%m.%Y" ${BANKDATA_END_DATE})"'|' \
 		${WRKSRC}/src/CMakeLists.txt
 
+# Custom target to check for python mods needed by ktoblzcheck-data-update.py.
+# This is a helper, since the FreeBSD ports infrastructure doesn't currently
+# have a built-in method to deal with dependencies required by custom targets.
+ktoblzcheck-data-update-py-mod-check:
+	@for mod in "bs4,beautifulsoup" "requests,requests"; do \
+	    IFS=","; \
+	    set -- $${mod}; \
+	    if ! ${PYTHON_CMD} -c "import $${1}" 2>/dev/null; then \
+		${ECHO_MSG} "====> Error: Missing Python module $${1}. Please install it: \`pkg install ${PYTHON_PKGNAMEPREFIX}$${2}\`"; \
+		exit 1; \
+	    else \
+		${ECHO_MSG} "====> Found Python module $${1}"; \
+	    fi \
+	done
+
+# Custom target to create a new distfile, edit the 'BANKDATA_START_DATE' and
+# 'BANKDATA_END_DATE' variables in this Makefile, and update distinfo.
+ktoblzcheck-data-update: ktoblzcheck-data-update-py-mod-check
+	${PYTHON_CMD} ${FILESDIR}/${@}.py -m ${.CURDIR}/Makefile -o ${DISTDIR} -s ${EXTRACT_SUFX}
+	${MAKE} -C ${.CURDIR} makesum
+	@${ECHO_CMD} "====> Don't forget to upload the distfile to freefall after verifying:"
+	@${ECHO_CMD} "====> \`${MAKE} -C ${.CURDIR} ktoblzcheck-data-upload\`"
+
+# Custom target to upload the new distfile to freefall. This only works for
+# FreeBSD committers with access to freefall. Manually verify the distfile
+# before running this target!!!
+ktoblzcheck-data-upload:
+	@if [ -f ${DISTDIR}/${BANKDATA_DISTFILE} ]; then \
+	    ssh freefall.freebsd.org "${MKDIR} public_distfiles/${PORTNAME}"; \
+	    scp ${DISTDIR}/${BANKDATA_DISTFILE} freefall.FreeBSD.org:public_distfiles/${PORTNAME}/; \
+	else \
+	    ${ECHO_CMD} "====> Error: distfile ${DISTDIR}/${BANKDATA_DISTFILE} not found."; \
+	    ${ECHO_CMD} "====> Run \`${MAKE} -C ${.CURDIR} ktoblzcheck-data-update\` first."; \
+	    exit 1; \
+	fi
+
 .include <bsd.port.mk>
diff --git a/finance/ktoblzcheck-data/distinfo b/finance/ktoblzcheck-data/distinfo
index a6b865f08db9..8a2b9710290c 100644
--- a/finance/ktoblzcheck-data/distinfo
+++ b/finance/ktoblzcheck-data/distinfo
@@ -1,5 +1,5 @@
-TIMESTAMP = 1767925106
+TIMESTAMP = 1783999898
 SHA256 (ktoblzcheck-data-20250515.tar.gz) = 307479cd3c487ba6d6c4f5966634a6023c1f29d4386b93a5e96cea7541bebe4c
 SIZE (ktoblzcheck-data-20250515.tar.gz) = 267821
-SHA256 (ktoblzcheck-bankdata-20251208.tar.gz) = 8cfbe82f18461aa182fcc7201a9337ae34c93dd652df874f9018adc7ef1463eb
-SIZE (ktoblzcheck-bankdata-20251208.tar.gz) = 425612
+SHA256 (ktoblzcheck-bankdata-20260608.tar.gz) = e8581ee70a0db1e6001b98156f45407413bc9c06d453aa3a967ab8fe4cbaae31
+SIZE (ktoblzcheck-bankdata-20260608.tar.gz) = 408258
diff --git a/finance/ktoblzcheck-data/files/ktoblzcheck-data-update.py b/finance/ktoblzcheck-data/files/ktoblzcheck-data-update.py
new file mode 100644
index 000000000000..b18b548db4fe
--- /dev/null
+++ b/finance/ktoblzcheck-data/files/ktoblzcheck-data-update.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+
+import argparse
+import concurrent.futures
+import os
+import re
+import subprocess
+import sys
+import tarfile
+import tempfile
+import textwrap
+from datetime import datetime
+
+import requests
+from bs4 import BeautifulSoup
+
+# Globals
+BANK_WEBSITE_HOST = "https://www.bundesbank.de"
+BANK_WEBSITE_PATH = "/de/aufgaben/unbarer-zahlungsverkehr/serviceangebot/bankleitzahlen/download-bankleitzahlen-602592"
+BANK_WEBSITE_URL_DE = f"{BANK_WEBSITE_HOST}{BANK_WEBSITE_PATH}"
+BANK_WEBSITE_SEPA_URL = f"{BANK_WEBSITE_HOST}/scl-directory"
+BANK_WEBSITE_URL_CH = "https://api.six-group.com/api/epcd/bankmaster/v3/bankmaster_V3.csv"
+BANK_WEBSITE_URL_NL = "https://www.betaalvereniging.nl/wp-content/uploads/BIC-lijst-NL.xlsx"
+
+# Standard browser User-Agent to bypass basic 403 blocks
+HEADERS = {
+    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
+}
+
+def download_file(url, dest, quiet):
+    if not quiet:
+        print(f"Downloading {url} -> {dest}")
+    response = requests.get(url, headers=HEADERS, stream=True, timeout=30)
+    response.raise_for_status()
+    with open(dest, 'wb') as f:
+        for chunk in response.iter_content(chunk_size=8192):
+            f.write(chunk)
+
+def set_tarinfo(tarinfo):
+    tarinfo.uid = 0
+    tarinfo.gid = 0
+    tarinfo.uname = "root"
+    tarinfo.gname = "root"
+    return tarinfo
+
+def update_makefile(makefile_path, start_date, end_date, quiet):
+    """Parses and updates the BANKDATA dates in the port Makefile."""
+    if not os.path.exists(makefile_path):
+        if not quiet:
+            print(f"Warning: Makefile not found at {makefile_path}. Skipping update.")
+        return False
+
+    with open(makefile_path, 'r') as f:
+        content = f.read()
+
+    # Capture the variable name, the '=', and ANY optional whitespace ([ \t]*).
+    # Discard whatever garbage (.*) comes after it until the end of the line ($).
+    new_content = re.sub(r'^(BANKDATA_START_DATE=[ \t]*).*$', rf'\g<1>{start_date}', content, flags=re.MULTILINE)
+    new_content = re.sub(r'^(BANKDATA_END_DATE=[ \t]*).*$', rf'\g<1>{end_date}', new_content, flags=re.MULTILINE)
+
+    if content != new_content:
+        with open(makefile_path, 'w') as f:
+            f.write(new_content)
+        if not quiet:
+            print(f"Updated Makefile variables: BANKDATA_START_DATE={start_date}, BANKDATA_END_DATE={end_date}")
+        return True
+    else:
+        if not quiet:
+            print("Makefile variables are already up to date.")
+        return False
+
+def main():
+    parser = argparse.ArgumentParser(description="Update ktoblzcheck bank data and Makefile.")
+    parser.add_argument("-m", "--makefile", default=None, help="Path to the Makefile to update. If omitted, skipped.")
+    parser.add_argument("-o", "--outdir", default=os.getcwd(), help="Directory to output the tarball (default: current directory).")
+    parser.add_argument("-q", "--quiet", action="store_true", help="Run in quiet mode.")
+    parser.add_argument("-s", "--suffix", default=".tar.gz", help="Compression suffix to use (e.g., .tar.gz, .tar.xz).")
+    args = parser.parse_args()
+
+    os.makedirs(args.outdir, exist_ok=True)
+
+    # Scrape the Bundesbank website for URLs and timestamps
+    if not args.quiet:
+        print(f"Fetching {BANK_WEBSITE_URL_DE}...")
+    try:
+        res = requests.get(BANK_WEBSITE_URL_DE, headers=HEADERS, timeout=15)
+        res.raise_for_status()
+        soup = BeautifulSoup(res.text, "html.parser")
+    except requests.RequestException as e:
+        print(f"ERROR: Fetching Bundesbank website failed: {e}", file=sys.stderr)
+        sys.exit(1)
+
+    link_tag = soup.find('a', href=re.compile(r'blz-aktuell-txt-data\.txt$'))
+    if not link_tag:
+        print("ERROR: Could not find bank data URL on the website.", file=sys.stderr)
+        sys.exit(1)
+
+    bankdata_url = BANK_WEBSITE_HOST + link_tag['href']
+
+    # Extract dates by searching the surrounding text block
+    parent_container = link_tag.find_parent('div')
+    search_text = parent_container.get_text(separator=" ") if parent_container else soup.get_text()
+
+    dates = re.findall(r'(\d{2}\.\d{2}\.\d{4})', search_text)
+    if len(dates) < 2:
+         dates = re.findall(r'(\d{2}\.\d{2}\.\d{4})', soup.get_text())
+
+    if len(dates) < 2:
+         print("ERROR: Could not extract start and end timestamps.", file=sys.stderr)
+         sys.exit(1)
+
+    try:
+        dt_start = datetime.strptime(dates[0], "%d.%m.%Y")
+        dt_end = datetime.strptime(dates[1], "%d.%m.%Y")
+    except ValueError as e:
+         print(f"ERROR: Date parsing failed - {e}", file=sys.stderr)
+         sys.exit(1)
+
+    # Format dates
+    timestamp = dt_start.strftime("%Y%m%d")
+    timestamp_end_raw = dt_end.strftime("%Y%m%d")
+    timestamp_start_str = dt_start.strftime("%Y-%m-%d")
+    timestamp_end_str = dt_end.strftime("%Y-%m-%d")
+
+# Update Makefile only if a path was explicitly provided
+    if args.makefile:
+        update_makefile(args.makefile, timestamp, timestamp_end_raw, args.quiet)
+    else:
+        if not args.quiet:
+            print("No Makefile path provided (-m). Skipping Makefile update.")
+
+    # Set up final tarball path
+    datadir_rel = f"ktoblzcheck-bankdata-{timestamp}"
+    tar_filename = f"{datadir_rel}{args.suffix}"
+    tar_path = os.path.join(args.outdir, tar_filename)
+
+    # Check if tarball exists before downloading
+    if os.path.exists(tar_path):
+         if not args.quiet:
+             print(f"Tarball {tar_path} is already current. Nothing to do.")
+         sys.exit(0)
+
+    # Use a temporary directory for safe downloading and building
+    with tempfile.TemporaryDirectory() as tmpdirname:
+        datadir = os.path.join(tmpdirname, datadir_rel)
+        os.makedirs(datadir)
+
+        blz_file = os.path.join(datadir, f"blz_{timestamp}.txt")
+        readme_file = os.path.join(datadir, "README")
+
+        # Concurrent downloads
+        downloads = [
+            (bankdata_url, blz_file),
+            (BANK_WEBSITE_SEPA_URL, os.path.join(datadir, f"sepa_{timestamp}.txt")),
+            (BANK_WEBSITE_URL_CH, os.path.join(datadir, "ch_data.txt")),
+            (BANK_WEBSITE_URL_NL, os.path.join(datadir, "nl_data.xlsx"))
+        ]
+
+        try:
+            with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
+                futures = [executor.submit(download_file, url, dest, args.quiet) for url, dest in downloads]
+                for future in concurrent.futures.as_completed(futures):
+                    future.result()
+        except requests.RequestException as e:
+            print(f"ERROR: File download failed - {e}", file=sys.stderr)
+            sys.exit(1)
+
+        # Generate README cleanly using textwrap
+        readme_content = textwrap.dedent(f"""\
+            The file blz_{timestamp}.txt contains bank code data published by
+            Deutsche Bundesbank (German central bank) (https://www.bundesbank.de).
+
+            Valid from {timestamp_start_str} to {timestamp_end_str}.
+
+            Obtained from:
+            {BANK_WEBSITE_URL_DE}
+            (Bankleitzahlendateien ungepackt, TXT)
+
+            The file sepa_{timestamp}.txt contains a list of reachable payment
+            service providers, also published by Deutsche Bundesbank.
+
+            Obtained from:
+            {BANK_WEBSITE_SEPA_URL}
+
+            The file ch_data.txt contains bank code data published by SIX
+            (https://www.six-group.com), which operates the infrastructure
+            for the Swiss and Spanish financial centers.
+
+            Obtained from:
+            {BANK_WEBSITE_URL_CH}
+
+            The file nl_data.xlsx contains bank code data published by
+            Betaalvereniging Nederland (Dutch Payments Association)
+            (https://www.betaalvereniging.nl/)
+
+            Obtained from:
+            {BANK_WEBSITE_URL_NL}
+            """)
+
+        with open(readme_file, "w") as f:
+            f.write(readme_content)
+
+        for filename in os.listdir(datadir):
+            os.chmod(os.path.join(datadir, filename), 0o664)
+
+        # Determine compression algorthm to use based upon the suffix
+        if args.suffix in [".tar.xz", ".txz"]:
+            tar_mode = "w:xz"
+        elif args.suffix in [".tar.bz2", ".tbz"]:
+            tar_mode = "w:bz2"
+        elif args.suffix in [".tar.gz", ".tgz"]:
+            tar_mode = "w:gz"
+        else:
+            print("ERROR: Compression format could not be determined.")
+            sys.exit(1)
+
+        if not args.quiet:
+            print(f"Creating archive {tar_path}...")
+
+        with tarfile.open(tar_path, tar_mode) as tar:
+            tar.add(datadir, arcname=datadir_rel, filter=set_tarinfo)
+
+    sys.exit(0)
+
+if __name__ == "__main__":
+    main()