From f2237852625593eddf685961a66ad29598f65ea8 Mon Sep 17 00:00:00 2001 From: Peter Nardi Date: Sat, 11 Jan 2025 17:04:52 -0500 Subject: [PATCH] Changelog (#34) * Improved version number management. * README linting. * Fixed #32 * Updated CHANGELOG * Finished CHANGELOG migration. * CHANGELOG updates. * Dependency Updates. * Fixed help message in stats.py. * Fixed help message in build.py. * Fixed help message for check.py * CHANGELOG update. * Merge remote-tracking branch 'origin/main' into changelog --- CHANGELOG.md | 139 ++++++++++++++++++++++++++++++++++++++ README.md | 87 ++++++++++++------------ pyproject.toml | 18 +++-- samples/targets.txt | 4 +- src/banip/app.py | 13 ++-- src/banip/build.py | 4 +- src/banip/check.py | 11 +-- src/banip/constants.py | 1 - src/banip/stats.py | 80 ++++++++++++++++++++++ src/banip/version.py | 46 +++++++++++++ src/parsers/check_args.py | 12 +--- src/parsers/stats_args.py | 30 ++++++++ uv.lock | 47 ++++++------- 13 files changed, 392 insertions(+), 100 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/banip/stats.py create mode 100644 src/banip/version.py create mode 100644 src/parsers/stats_args.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c8dc0ed --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,139 @@ +# Changelog + + + +## [1.2.0][1.2.0] - 2025-01-11 + +### Changed + +* Implement [tomli compatability layer][tomli]. +* Optimize version numbering. + +### Added + +* Add statistics for a given country. ([#32][issue32]) +* Establish and maintain a proper changelog. + +### Fixed + +* Lint documentation. +* Display properly sorted options when getting help. + + + +## [1.1.3][1.1.3] - 2024-12-19 + +### Added + +* Add the ability to compact ipsum entries into /24 subnets. + +### Fixed + +* Improve input validation. +* Lint documentation. +* Refactor and optimize code. + + + +## [1.1.2][1.1.2] - 2024-12-02 + +### Changed + +* Tune binary search algorithm. +* Enhance `banip check` with prettier output using [rich][rich]. + +### Fixed + +* Lint documentation. + + + +## [1.1.1][1.1.1] - 2024-11-15 + +### Changed + +* Get better output with the [rich][rich] library. + + + +## [1.1.0][1.1.0] - 2024-10-23 + +### Changed + +* Refine display of final metrics. + +### Fixed + +* Refactor code for better maintainability. + + + +## [1.0.2][1.0.2] - 2024-08-09 + +### Changed + +* Optimize binary search. ([#13][issue13]) +* Bumped tqdm library to v4.66.5. + +### Added + +* Add functionality to remove IP addresses captured in a subnets. +* Add license and acknowledgements for the tqmd library + +### Fixed + +* Fix calculation error in summary metrics. +* Choose Better Variable Names. ([#14][issue14]) +* Improve IP checking to include membership in subnets. ([#15][issue15]) + + + +## [1.0.1][1.0.1] - 2024-07-30 + +### Added + +* Add separate command line option to display version information. +* Add additional help indicators to the subcommands. + +### Fixed + +* Refactor code for better maintainability. + + + +## [1.0.0][1.0.0] - 2024-03-15 + +### Changed + +* Update documentation with page anchors. + +### Added + +* Introduce a plugin architecture. + +### Fixed + +* Fixed uncaught exception. +* Lint documentation. + + + +## [0.1.0][0.1.0] - 2024-05-12 + +_Initial Release._ + +[0.1.0]: https://github.com/geozeke/banip/releases/tag/v0.1.0 +[1.0.0]: https://github.com/geozeke/banip/releases/tag/V1.0.0 +[1.0.1]: https://github.com/geozeke/banip/releases/tag/v1.0.1 +[1.0.2]: https://github.com/geozeke/banip/releases/tag/v1.0.2 +[issue13]: https://github.com/geozeke/banip/issues/13 +[issue14]: https://github.com/geozeke/banip/issues/14 +[issue15]: https://github.com/geozeke/banip/issues/15 +[1.1.0]: https://github.com/geozeke/banip/releases/tag/v1.1.0 +[1.1.1]: https://github.com/geozeke/banip/releases/tag/v1.1.1 +[rich]: https://github.com/Textualize/rich +[1.1.2]: https://github.com/geozeke/banip/releases/tag/v1.1.2 +[1.1.3]: https://github.com/geozeke/banip/releases/tag/v1.1.3 +[tomli]: https://pypi.org/project/tomli/ +[1.2.0]: https://github.com/geozeke/banip/releases/tag/v1.2.0 +[issue32]: https://github.com/geozeke/banip/issues/32 diff --git a/README.md b/README.md index a767b94..afd79be 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ alt = "Dinobox logo" width="120"/> This tool will create a customized list of IP addresses that are cross-referenced between two sources: -1. [This list][def10] of worldwide identified blacklisted IPs. +1. [This list][ipsum] of worldwide identified blacklisted IPs. 2. A list of the IP subnets associated with each country. The result is a customized blacklist of IP addresses based on @@ -19,11 +19,11 @@ You could, but where's the fun in that? You may want to create a list of bad actors for specific countries. The global list contains several hundred thousand entries, and you may need -more targeted list for testing or deployment in production. For example, -I've configured my HAProxy server to drop IP connections from all -countries except those that I've whitelisted. I also want the ability to -create a customized IP list to block any bad actors from those -whitelisted countries. This tool accomplishes that. +a more targeted list for testing or deployment in production. For +example, I've configured my HAProxy server to drop IP connections from +all countries except those that I've whitelisted. I also want the +ability to create a customized IP list to block any bad actors from +those whitelisted countries. This tool accomplishes that. ## Contents @@ -39,15 +39,15 @@ whitelisted countries. This tool accomplishes that. ### Operating System *banip* runs in Unix-like OSes. Either macOS, a Linux PC, Linux Virtual -Machine, or [Windows Subsystem for Linux (WSL)][def7] is required. +Machine, or [Windows Subsystem for Linux (WSL)][wsl] is required. ### MaxMind Database -You'll need a copy of the [MaxMind][def8] GeoLite2 database for +You'll need a copy of the [MaxMind][mmh] GeoLite2 database for country-level geotagging of IP addresses. If you have a premium or corporate MaxMind account, you're all set. If not, the free GeoLite2 -account will work just fine ([sign up here][def5]). Once you login, -using the menu on the top right select: +account will work just fine ([sign up here][mmgeo]). Once you log in, +using the menu on the top right, select: ```text My Account > My Account @@ -62,9 +62,9 @@ GeoLite2 Country: CSV format ### uv -*banip* requires [uv][def2] for dependency management. It is well -behaved and extremely fast, and if you're a Python developer you should -check it out. Visit the [uv site][def2] and install it using the +*banip* requires [uv][astral] for dependency management. It is well +behaved and extremely fast, and if you're a Python developer, you should +check it out. Visit the [uv site][astral] and install it using the instructions for your operating system. ### gitignore (optional) @@ -73,15 +73,15 @@ If you want to fork and develop this repo, I've included a file called `global-gitignore.txt` which is a copy of the `.gitignore` I placed in my home directory and configured globally for all my development projects. The `global-gitignore.txt` file reflects my development setup -(for example using tools like vscode), but yours may be different. Just -cherrypick any necessary elements from `global-gitignore.txt` for your +(for example, using tools like vscode), but yours may be different. Just +cherry-pick any necessary elements from `global-gitignore.txt` for your own use. -*Details on gitignore files are available on [GitHub][def3].* +*Details on gitignore files are available on [GitHub][git-ignore].* ### Global List of Blacklisted IPs -*banip* uses the [ipsum][def9] threat intelligence blacklist. You can +*banip* uses the [ipsum][ipsum] threat intelligence blacklist. You can direct download it using: ```shell @@ -90,8 +90,8 @@ curl -sL https://raw.githubusercontent.com/stamparm/ipsum/master/ipsum.txt > ips ### make -You'll need the [make][def6] utility installed (*it probably -already is*). If not, install it with: +You'll need the [make][make] utility installed (it probably already is). +If not, install it with: ```shell sudo apt install make @@ -113,7 +113,7 @@ Unpack the GeoLite2-Country zip archive and save the files to a location you can easily get to. *Note: if you're looking for a quick way to download the MaxMind data -using `curl` and a direct download permalink, [SEE HERE][def4].* +using `curl` and a direct download permalink, [SEE HERE][mmd].* ### Clone the Repository @@ -164,10 +164,10 @@ cp ./samples/custom_whitelist.txt ./data/custom_whitelist.txt ``` There may be IP addresses that *banip* will flag as malicious, but you -still want to whitelist them (for example to use for testing). This file -should contain specific IP addresses, one per line, that you want to -allow. This file is optional and if you choose not to use it, *banip* -will create a blank one for you. +still want to whitelist them (for example, to use for testing). This +file should contain specific IP addresses, one per line, that you want +to allow. This file is optional, and if you choose not to use it, +*banip* will create a blank one for you. #### Custom Blacklist (Optional) @@ -176,16 +176,16 @@ cp ./samples/custom_blacklist.txt ./data/custom_blacklist.txt ``` The ipsum database isn't perfect. You may determine that there's an IP -address you want to ban this is not found in `ipsum.txt`. Also, the -`ipsum.txt` file only contains IP addresses and you may want to ban an +address you want to ban that is not found in `ipsum.txt`. Also, the +`ipsum.txt` file only contains IP addresses, and you may want to ban an entire subnet. The custom blacklist allows you to capture specific IP -addresses or subnets (in [CIDR][def] format), one per line, that you +addresses or subnets (in [CIDR][cidr] format), one per line, that you want to block. Some of your custom blacklist IPs may be found when you run the *banip*, so this file (`custom_blacklist.txt`) will be overwritten to remove the duplicates. The contents of the de-duplicated file will then be appended to the list generated when you run the program. Like the whitelist, this file is optional. If you choose not to -use it *banip* will create a blank one when you run it. +use it, *banip* will create a blank one when you run it. *Note: If you're concerned about keeping your original list of custom blacklisted IPs, save a copy of it somewhere outside the repository.* @@ -218,7 +218,7 @@ data ## Running After copying/tweaking all the required files, start by activating the -python virtual environment: +Python virtual environment: ```shell source .venv/bin/activate @@ -234,7 +234,7 @@ banip -h ## Updating -MaxMind updates the GeoLite2 Country database on Tuesdays and Fridays +MaxMind updates the GeoLite2 Country database on Tuesdays and Fridays, and the list of blacklisted IPs (`ipsum.txt`) is updated daily. Pull updated copies of both and put them in `banip/data/geolite` (for the GeoLite2 data) and `banip/data` (for the `ipsum.txt` file). Run *banip* @@ -247,16 +247,16 @@ again to generate an updated blacklist. ## Plugins *banip* generates some useful build products that you may want to use -for other purposes. For example, everytime you build a new blacklist -*banip* also creates and saves a textfile of all worldwide subnets, each -tagged with a two-letter country code. The file is saved in: +for other purposes. For example, every time you build a new blacklist, +*banip* also creates and saves a text file of all worldwide subnets, +each tagged with a two-letter country code. The file is saved in: ```'text ./banip/data/haproxy_geo_ip.txt ``` Next time you run *banip*, open that file and take a look at it. Since -you may have a very specific usecase for that data, you can write a +you may have a very specific use case for that data, you can write a plugin for *banip* which will make use of the build products for your purposes. @@ -288,13 +288,12 @@ rm -rf ~/banip [top](#top) -[def]: https://aws.amazon.com/what-is/cidr/#:~:text=CIDR%20notation%20represents%20an%20IP,as%20192.168.1.0%2F22. -[def2]: https://docs.astral.sh/uv/ -[def3]: https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files -[def4]: https://dev.maxmind.com/geoip/updating-databases#directly-downloading-databases -[def5]: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data -[def6]: https://man7.org/linux/man-pages/man1/make.1p.html -[def7]: https://docs.microsoft.com/en-us/windows/wsl/install -[def8]: https://www.maxmind.com/en/home -[def9]: https://github.com/stamparm/ipsum -[def10]: https://github.com/stamparm/ipsum +[cidr]: https://aws.amazon.com/what-is/cidr/#:~:text=CIDR%20notation%20represents%20an%20IP,as%20192.168.1.0%2F22. +[astral]: https://docs.astral.sh/uv/ +[git-ignore]: https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files +[mmd]: https://dev.maxmind.com/geoip/updating-databases#directly-downloading-databases +[mmgeo]: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data +[make]: https://man7.org/linux/man-pages/man1/make.1p.html +[wsl]: https://docs.microsoft.com/en-us/windows/wsl/install +[mmh]: https://www.maxmind.com/en/home +[ipsum]: https://github.com/stamparm/ipsum diff --git a/pyproject.toml b/pyproject.toml index 0043d24..ec88273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "banip" -version = "1.1.3" +version = "1.2.0" description = "Create a list of banned IPs for specific countries" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -8,11 +8,18 @@ requires-python = ">=3.12,<3.13" dependencies = [ "banip", "rich>=13.9.4", + "tomli>=1.1.0 ; python_full_version < '3.11'", ] authors = [ {name = "Peter Nardi", email = "geozeke@gmail.com"}, ] +[dependency-groups] +dev = [ + "mypy>=1.13.0", + "ruff>=0.7.1", +] + [tool.ruff.lint.isort] force-single-line = true @@ -26,12 +33,6 @@ banip = "banip.app:main" requires = ["hatchling"] build-backend = "hatchling.build" -[dependency-groups] -dev = [ - "mypy>=1.13.0", - "ruff>=0.7.1", -] - [tool.hatch.build] exclude = [ ".gitignore", @@ -40,3 +41,6 @@ exclude = [ "samples/**", "uv.lock", ] + +[tool.hatch.build.targets.wheel.force-include] +"./pyproject.toml" = "banip/pyproject.toml" diff --git a/samples/targets.txt b/samples/targets.txt index 63b68f4..2564589 100644 --- a/samples/targets.txt +++ b/samples/targets.txt @@ -3,8 +3,8 @@ # are ignored. NOTE: These are the ISO-3166 ALPHA2 codes, not the # two-letter Top Level Domain names (which may be different). For # example, the two-letter TLD for the United Kingdom is "uk", but the -# ISO-3166 code for United Kingdom is "gb". You can find a list of all -# the codes here: https://www.geonames.org/countries/ +# ISO-3166 code for the United Kingdom is "gb". You can find a list of +# all the codes here: https://www.geonames.org/countries/ # Norway no diff --git a/src/banip/app.py b/src/banip/app.py index 6a7f138..8d9a8aa 100755 --- a/src/banip/app.py +++ b/src/banip/app.py @@ -5,7 +5,6 @@ import argparse import importlib import sys -import textwrap from pathlib import Path from types import ModuleType @@ -13,7 +12,7 @@ from banip.constants import ARG_PARSERS_BASE from banip.constants import ARG_PARSERS_CUSTOM from banip.constants import CUSTOM_CODE -from banip.constants import VERSION +from banip.version import get_version # ====================================================================== @@ -53,7 +52,7 @@ def main() -> None: https://github.com/geozeke/banip for detailed instructions on setting up banip. """ - epi = f"Version: {VERSION}" + epi = f"Version: {get_version()}" parser = argparse.ArgumentParser( description=msg, epilog=epi, @@ -62,7 +61,7 @@ def main() -> None: "-v", "--version", action="version", - version=f"{APP_NAME} {VERSION}", + version=f"{APP_NAME} {get_version()}", ) msg = "For help on any command below, run: banip {command} -h." subparsers = parser.add_subparsers( @@ -78,6 +77,7 @@ def main() -> None: mod: ModuleType | None = None parser_names = collect_parsers(ARG_PARSERS_BASE) parser_names += collect_parsers(ARG_PARSERS_CUSTOM) + parser_names = sorted(parser_names, key=lambda x: x.split(".")[1]) for p_name in parser_names: parser_code = importlib.import_module(p_name) parser_code.load_command_args(subparsers) @@ -101,9 +101,10 @@ def main() -> None: msg = f""" Code for a custom command must have the same filename as the command itself. Make sure you have a program file called - \"{args.cmd}.py\" in {CUSTOM_CODE}/ + \"{args.cmd}.py\" in: + {CUSTOM_CODE.parent}/ """ - print(textwrap.fill(text=" ".join(msg.split()))) + print("\n".join([line.strip() for line in msg.split("\n")])) sys.exit(1) else: mod = importlib.import_module(f"{APP_NAME}.null") diff --git a/src/banip/build.py b/src/banip/build.py index 78b1303..e4e6225 100755 --- a/src/banip/build.py +++ b/src/banip/build.py @@ -123,7 +123,7 @@ def task_runner(args: Namespace) -> None: with console.status(msg): countries.sort() with open(COUNTRY_WHITELIST, "w") as f: - f.write(f"{"\n".join(countries)}\n") + f.write(f"{'\n'.join(countries)}\n") print(f"{msg:.<{PAD}}done") # ------------------------------------------------------------------ @@ -264,7 +264,7 @@ def task_runner(args: Namespace) -> None: table.add_column(justify="right") table.add_column(justify="right") - table.add_row("Target Countries", f"{",".join(countries)}", end_section=True) + table.add_row("Target Countries", f"{','.join(countries)}", end_section=True) table.add_row("IPs - ipsum.txt", f"{(ipsum_ips_size):,d}") table.add_row("Subnets - ipsum.txt", f"{(ipsum_nets_size):,d}") table.add_row("IPs - custom", f"{(custom_ips_size):,d}") diff --git a/src/banip/check.py b/src/banip/check.py index 4e27d8c..1985f61 100644 --- a/src/banip/check.py +++ b/src/banip/check.py @@ -3,7 +3,6 @@ import argparse import ipaddress as ipa import pickle -import textwrap from rich.console import Console from rich.style import Style @@ -34,11 +33,13 @@ def task_runner(args: argparse.Namespace) -> None: if not COUNTRY_NETS_DICT.exists(): msg = """ - Some required files are missing. Make sure to build the - databases before checking for a particular IP address. Run - \'banip build -h\' for more information. + Some required files are missing. Make sure to run the \'build\' + command before generating statistics for a given IP. Run this + command for more information: + + \'banip build -h\' """ - print(textwrap.fill(text=" ".join(msg.split()))) + print("\n".join([line.strip() for line in msg.split("\n")])) return console = Console() diff --git a/src/banip/constants.py b/src/banip/constants.py index b28406c..7e7f50e 100644 --- a/src/banip/constants.py +++ b/src/banip/constants.py @@ -26,7 +26,6 @@ GEOLITE_LOC = DATA / "geolite" / "GeoLite2-Country-Locations-en.csv" IPSUM = DATA / "ipsum.txt" TARGETS = DATA / "targets.txt" -VERSION = "1.1.3" # Padding for pretty printing PAD = 30 diff --git a/src/banip/stats.py b/src/banip/stats.py new file mode 100644 index 0000000..33c7f2d --- /dev/null +++ b/src/banip/stats.py @@ -0,0 +1,80 @@ +"""Taskrunner for stats command.""" + +import argparse +import pickle + +from rich import box +from rich.console import Console +from rich.style import Style +from rich.table import Table + +from banip.constants import COUNTRY_NETS_DICT +from banip.constants import PAD +from banip.constants import NetworkType + + +def task_runner(args: argparse.Namespace) -> None: + """Display statistics for a given country. + + Parameters + ---------- + args : argparse.Namespace + args.country_code will be the two-letter country code of + interest. + """ + if not COUNTRY_NETS_DICT.exists(): + msg = """ + Some required files are missing. Make sure to run the \'build\' + command before generating statistics for a given country. Run + this command for more information: + + \'banip build -h\' + """ + print("\n".join([line.strip() for line in msg.split("\n")])) + return + + target_country = args.country_code.upper() + console = Console() + + print() + msg = "Loading data" + with console.status(msg): + D: dict[NetworkType, str] = {} + with open(COUNTRY_NETS_DICT, "rb") as f: + D = pickle.load(f) + print(f"{msg:.<{PAD}}done") + + msg = "Analyzing" + results = {"nets_4": 0, "ips_4": 0, "nets_6": 0, "ips_6": 0} + with console.status(msg): + for net, country in D.items(): + if country == target_country: + results[f"nets_{net.version}"] += 1 + results[f"ips_{net.version}"] += ( + 1 if net.num_addresses == 1 else net.num_addresses - 2 + ) + print(f"{msg:.<{PAD}}done") + print() + + if results == [0] * 4: + print(f"{target_country} not found") + return + + no_italic = Style(italic=False) + table = Table( + title=f"Results for: {target_country}", + box=box.SQUARE, + title_style=no_italic, + show_header=False, + ) + + table.add_column(justify="right") + table.add_column(justify="right") + + table.add_row("Nets (v4)", f"{results['nets_4']:,d}") + table.add_row("Nets (v6)", f"{results['nets_6']:,d}", end_section=True) + table.add_row("IPs (v4)", f"{results['ips_4']:,d}") + table.add_row("IPs (v6)", f"{results['ips_6']:.2e}") + console.print(table) + + return diff --git a/src/banip/version.py b/src/banip/version.py new file mode 100644 index 0000000..e796cf8 --- /dev/null +++ b/src/banip/version.py @@ -0,0 +1,46 @@ +import sys +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + + +def get_version() -> str: + """Return the version number of the project. + + Starting at the current directory, search for pyproject.toml, + working your way up the parents in the directory tree. This allows + the version number to be maintained in one place. + + IMPORTANT: For this to work properly when you distribute your + package, you must include the following in your `pyproject.toml` + file: + + ``` + [tool.hatch.build.targets.wheel.force-include] + "./pyproject.toml" = "/pyproject.toml" + ``` + + Returns + ------- + str + Version number of the project. + """ + start = Path(__file__).absolute().parents + + for path in start: + for file in path.glob("*.toml"): + if file.name == "pyproject.toml": + try: + with open(file, "rb") as f: + tom = tomllib.load(f) + return tom["project"]["version"] + except Exception: + return "unknown" + + return "unknown" + + +# ====================================================================== diff --git a/src/parsers/check_args.py b/src/parsers/check_args.py index b596b4c..ba830f9 100644 --- a/src/parsers/check_args.py +++ b/src/parsers/check_args.py @@ -15,11 +15,7 @@ def load_command_args(sp: _SubParsersAction) -> None: rendered blacklist only reflects those countries that are in targets.txt """ - parser = sp.add_parser( - name=COMMAND_NAME, - help=msg, - description=msg, - ) + parser = sp.add_parser(name=COMMAND_NAME, help=msg, description=msg) msg = """ This is the IPv4 or IPv6 address you're interested in. After you run @@ -28,10 +24,6 @@ def load_command_args(sp: _SubParsersAction) -> None: to generate new data will use the updated information for future IP checking. """ - parser.add_argument( - "ip", - type=str, - help=msg, - ) + parser.add_argument("ip", type=str, help=msg) return diff --git a/src/parsers/stats_args.py b/src/parsers/stats_args.py new file mode 100644 index 0000000..add85d2 --- /dev/null +++ b/src/parsers/stats_args.py @@ -0,0 +1,30 @@ +"""Argument parser for country command.""" + +from argparse import _SubParsersAction + +COMMAND_NAME = "stats" + + +# ====================================================================== + + +def load_command_args(sp: _SubParsersAction) -> None: + """Assemble the argument parser.""" + msg = """ + Produce statistics for a given country code. + """ + parser = sp.add_parser(name=COMMAND_NAME, help=msg, description=msg) + msg = """ + This is the two-letter ISO-3166 ALPHA2 country code, not the + two-letter Top Level Domain name (which may be different). For + example, the two-letter TLD for the United Kingdom is "uk", but the + ISO-3166 code for the United Kingdom is "gb". You can find a list of + all the codes here: https://www.geonames.org/countries/ + """ + parser.add_argument("country_code", type=str, help=msg) + + return + + +if __name__ == "__main__": + pass diff --git a/uv.lock b/uv.lock index d64653d..86bc0ab 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ requires-python = "==3.12.*" [[package]] name = "banip" -version = "1.1.3" +version = "1.1.4" source = { editable = "." } dependencies = [ { name = "rich" }, @@ -19,6 +19,7 @@ dev = [ requires-dist = [ { name = "banip", editable = "." }, { name = "rich", specifier = ">=13.9.4" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1.1.0" }, ] [package.metadata.requires-dev] @@ -78,11 +79,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] @@ -100,27 +101,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.8.5" +version = "0.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/5d/4b5403f3e89837decfd54c51bea7f94b7d3fae77e08858603d0e04d7ad17/ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317", size = 3454835 } +sdist = { url = "https://files.pythonhosted.org/packages/67/3e/e89f736f01aa9517a97e2e7e0ce8d34a4d8207087b3cfdec95133fee13b5/ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17", size = 3498844 } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f8/03391745a703ce11678eb37c48ae89ec60396ea821e9d0bcea7c8e88fd91/ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88", size = 10626889 }, - { url = "https://files.pythonhosted.org/packages/55/74/83bb74a44183b904216f3edfb9995b89830c83aaa6ce84627f74da0e0cf8/ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7", size = 10398233 }, - { url = "https://files.pythonhosted.org/packages/e8/7a/a162a4feb3ef85d594527165e366dde09d7a1e534186ff4ba5d127eda850/ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb", size = 10001843 }, - { url = "https://files.pythonhosted.org/packages/e7/9f/5ee5dcd135411402e35b6ec6a8dfdadbd31c5cd1c36a624d356a38d76090/ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50", size = 10872507 }, - { url = "https://files.pythonhosted.org/packages/b6/67/db2df2dd4a34b602d7f6ebb1b3744c8157f0d3579973ffc58309c9c272e8/ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4", size = 10377200 }, - { url = "https://files.pythonhosted.org/packages/fe/ff/fe3a6a73006bced73e60d171d154a82430f61d97e787f511a24bd6302611/ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd", size = 11433155 }, - { url = "https://files.pythonhosted.org/packages/e3/95/c1d1a1fe36658c1f3e1b47e1cd5f688b72d5786695b9e621c2c38399a95e/ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc", size = 12139227 }, - { url = "https://files.pythonhosted.org/packages/1b/fe/644b70d473a27b5112ac7a3428edcc1ce0db775c301ff11aa146f71886e0/ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b", size = 11697941 }, - { url = "https://files.pythonhosted.org/packages/00/39/4f83e517ec173e16a47c6d102cd22a1aaebe80e1208a1f2e83ab9a0e4134/ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd", size = 12967686 }, - { url = "https://files.pythonhosted.org/packages/1a/f6/52a2973ff108d74b5da706a573379eea160bece098f7cfa3f35dc4622710/ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb", size = 11253788 }, - { url = "https://files.pythonhosted.org/packages/ce/1f/3b30f3c65b1303cb8e268ec3b046b77ab21ed8e26921cfc7e8232aa57f2c/ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725", size = 10860360 }, - { url = "https://files.pythonhosted.org/packages/a5/a8/2a3ea6bacead963f7aeeba0c61815d9b27b0d638e6a74984aa5cc5d27733/ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea", size = 10457922 }, - { url = "https://files.pythonhosted.org/packages/17/47/8f9514b670969aab57c5fc826fb500a16aee8feac1bcf8a91358f153a5ba/ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8", size = 10958347 }, - { url = "https://files.pythonhosted.org/packages/0d/d6/78a9af8209ad99541816d74f01ce678fc01ebb3f37dd7ab8966646dcd92b/ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5", size = 11328882 }, - { url = "https://files.pythonhosted.org/packages/54/77/5c8072ec7afdfdf42c7a4019044486a2b6c85ee73617f8875ec94b977fed/ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed", size = 8802515 }, - { url = "https://files.pythonhosted.org/packages/bc/b6/47d2b06784de8ae992c45cceb2a30f3f205b3236a629d7ca4c0c134839a2/ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47", size = 9684231 }, - { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, + { url = "https://files.pythonhosted.org/packages/dc/05/c3a2e0feb3d5d394cdfd552de01df9d3ec8a3a3771bbff247fab7e668653/ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743", size = 10645241 }, + { url = "https://files.pythonhosted.org/packages/dd/da/59f0a40e5f88ee5c054ad175caaa2319fc96571e1d29ab4730728f2aad4f/ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f", size = 10391066 }, + { url = "https://files.pythonhosted.org/packages/b7/fe/85e1c1acf0ba04a3f2d54ae61073da030f7a5dc386194f96f3c6ca444a78/ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb", size = 10012308 }, + { url = "https://files.pythonhosted.org/packages/6f/9b/780aa5d4bdca8dcea4309264b8faa304bac30e1ce0bcc910422bfcadd203/ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca", size = 10881960 }, + { url = "https://files.pythonhosted.org/packages/12/f4/dac4361afbfe520afa7186439e8094e4884ae3b15c8fc75fb2e759c1f267/ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce", size = 10414803 }, + { url = "https://files.pythonhosted.org/packages/f0/a2/057a3cb7999513cb78d6cb33a7d1cc6401c82d7332583786e4dad9e38e44/ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969", size = 11464929 }, + { url = "https://files.pythonhosted.org/packages/eb/c6/1ccfcc209bee465ced4874dcfeaadc88aafcc1ea9c9f31ef66f063c187f0/ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd", size = 12170717 }, + { url = "https://files.pythonhosted.org/packages/84/97/4a524027518525c7cf6931e9fd3b2382be5e4b75b2b61bec02681a7685a5/ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a", size = 11708921 }, + { url = "https://files.pythonhosted.org/packages/a6/a4/4e77cf6065c700d5593b25fca6cf725b1ab6d70674904f876254d0112ed0/ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b", size = 13058074 }, + { url = "https://files.pythonhosted.org/packages/f9/d6/fcb78e0531e863d0a952c4c5600cc5cd317437f0e5f031cd2288b117bb37/ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831", size = 11281093 }, + { url = "https://files.pythonhosted.org/packages/e4/3b/7235bbeff00c95dc2d073cfdbf2b871b5bbf476754c5d277815d286b4328/ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab", size = 10882610 }, + { url = "https://files.pythonhosted.org/packages/2a/66/5599d23257c61cf038137f82999ca8f9d0080d9d5134440a461bef85b461/ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1", size = 10489273 }, + { url = "https://files.pythonhosted.org/packages/78/85/de4aa057e2532db0f9761e2c2c13834991e087787b93e4aeb5f1cb10d2df/ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366", size = 11003314 }, + { url = "https://files.pythonhosted.org/packages/00/42/afedcaa089116d81447347f76041ff46025849fedb0ed2b187d24cf70fca/ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f", size = 11342982 }, + { url = "https://files.pythonhosted.org/packages/39/c6/fe45f3eb27e3948b41a305d8b768e949bf6a39310e9df73f6c576d7f1d9f/ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72", size = 8819750 }, + { url = "https://files.pythonhosted.org/packages/38/8d/580db77c3b9d5c3d9479e55b0b832d279c30c8f00ab0190d4cd8fc67831c/ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19", size = 9701331 }, + { url = "https://files.pythonhosted.org/packages/b2/94/0498cdb7316ed67a1928300dd87d659c933479f44dec51b4f62bfd1f8028/ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7", size = 9145708 }, ] [[package]]