banip 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
banip/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """banip package."""
banip/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Runnable module entry point for ``python -m banip``."""
2
+
3
+ from banip.app import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
banip/app.py ADDED
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Entry point for banip."""
4
+
5
+ import argparse
6
+ import importlib
7
+ import importlib.util
8
+ import sys
9
+ from importlib.metadata import version
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+
13
+ from banip.constants import APP_NAME
14
+ from banip.constants import ARG_PARSERS_BASE
15
+ from banip.constants import CUSTOM_CODE
16
+ from banip.constants import CUSTOM_PARSERS
17
+ from banip.constants import DATA
18
+ from banip.utilities import print_docstring
19
+
20
+ __version__ = version("banip")
21
+
22
+ LEGACY_PLUGIN_WARNING = (
23
+ "Warning: banip plugins are deprecated and will be removed in banip 3.0. "
24
+ "Legacy plugins remain supported throughout banip 2.x."
25
+ )
26
+
27
+ # ======================================================================
28
+
29
+
30
+ def check_setup() -> bool:
31
+ """Check whether the local environment is configured.
32
+
33
+ Returns
34
+ -------
35
+ bool
36
+ True if the required directories exist; otherwise False.
37
+ """
38
+ proper_setup = (DATA / "geolite").exists()
39
+ if not proper_setup:
40
+ msg = """
41
+ The local environment is not configured correctly. Make sure
42
+ the following structure exists in your home directory:
43
+
44
+ .banip
45
+ └── geolite
46
+ """
47
+ print_docstring(msg=msg)
48
+ return False
49
+ return True
50
+
51
+
52
+ # ======================================================================
53
+
54
+
55
+ def requires_setup(args: argparse.Namespace) -> bool:
56
+ """Return whether a command requires initialized local data paths.
57
+
58
+ Parameters
59
+ ----------
60
+ args : argparse.Namespace
61
+ Parsed command-line arguments.
62
+
63
+ Returns
64
+ -------
65
+ bool
66
+ True when setup should be checked before dispatch.
67
+ """
68
+ return not (args.cmd == "database" and args.action == "init")
69
+
70
+
71
+ # ======================================================================
72
+
73
+
74
+ def load_custom_module(mod_name: str, location: Path) -> ModuleType:
75
+ """Load a custom module.
76
+
77
+ Parameters
78
+ ----------
79
+ mod_name : str
80
+ The name of the module to load.
81
+ location : Path
82
+ The absolute path to the Python code for the module.
83
+
84
+ Returns
85
+ -------
86
+ ModuleType
87
+ The loaded module.
88
+ """
89
+ mod_path = f"{location}/{mod_name}.py"
90
+ if spec := importlib.util.spec_from_file_location(mod_name, mod_path):
91
+ if (module := importlib.util.module_from_spec(spec)) and spec.loader:
92
+ spec.loader.exec_module(module)
93
+ return module
94
+
95
+
96
+ # ======================================================================
97
+
98
+
99
+ def collect_parsers(start: Path) -> list[str]:
100
+ """Collect the module names of all argument parsers to import.
101
+
102
+ Parameters
103
+ ----------
104
+ start : Path
105
+ The directory where parser collection starts.
106
+
107
+ Returns
108
+ -------
109
+ list[str]
110
+ Argument parser module names.
111
+ """
112
+ parser_names: list[str] = []
113
+ if not start.exists():
114
+ return parser_names
115
+ for p in start.iterdir():
116
+ if p.is_file() and p.name.endswith(".py") and p.name != "__init__.py":
117
+ if "plugins" in str(p):
118
+ prefix = "plugins.parsers"
119
+ else:
120
+ prefix = "parsers"
121
+ parser_names.append(f"{prefix}.{p.stem}")
122
+ return parser_names
123
+
124
+
125
+ # ======================================================================
126
+
127
+
128
+ def legacy_plugins_present() -> bool:
129
+ """Return whether either legacy plugin directory contains Python code.
130
+
131
+ Returns
132
+ -------
133
+ bool
134
+ True when a legacy parser or command implementation is present.
135
+ """
136
+ return any(
137
+ path.is_file() and path.suffix == ".py" and path.name != "__init__.py"
138
+ for directory in (CUSTOM_PARSERS, CUSTOM_CODE)
139
+ if directory.exists()
140
+ for path in directory.iterdir()
141
+ )
142
+
143
+
144
+ # ======================================================================
145
+
146
+
147
+ def main() -> int:
148
+ """Parse user input and run the requested command."""
149
+ msg = """
150
+ Generate and query IP blocklists for use with proxy servers such as
151
+ HAProxy. See https://geozeke.github.io/banip/ for
152
+ setup instructions.
153
+ """
154
+ epi = f"Version: {__version__}"
155
+ parser = argparse.ArgumentParser(prog=APP_NAME, description=msg, epilog=epi)
156
+ parser.add_argument(
157
+ "-v", "--version", action="version", version=f"{APP_NAME} {__version__}"
158
+ )
159
+ msg = "For help on any command below, run: banip {command} -h."
160
+ subparsers = parser.add_subparsers(title="commands", dest="cmd", description=msg)
161
+
162
+ # Dynamically load argument subparsers and process command line
163
+ # arguments.
164
+
165
+ parser_names: list[str] = []
166
+ mod: ModuleType | None = None
167
+ parser_names = collect_parsers(ARG_PARSERS_BASE)
168
+ parser_names += collect_parsers(CUSTOM_PARSERS)
169
+ if legacy_plugins_present():
170
+ print(LEGACY_PLUGIN_WARNING, file=sys.stderr)
171
+ parser_names = sorted(parser_names, key=lambda x: x.split(".")[-1])
172
+ for p_name in parser_names:
173
+ if "plugins" not in p_name:
174
+ parser_code = importlib.import_module(f"banip.{p_name}")
175
+ else:
176
+ parser_code = load_custom_module(
177
+ p_name.split(".")[-1], location=CUSTOM_PARSERS
178
+ )
179
+ parser_code.load_command_args(subparsers)
180
+ args = parser.parse_args()
181
+
182
+ # Make sure the local setup is complete after parsing so help and
183
+ # version output work in a fresh environment.
184
+ if args.cmd and requires_setup(args) and not check_setup():
185
+ return 1
186
+
187
+ # Run the selected command. Python's argparse module guarantees that
188
+ # we'll get either: (1) a valid command (base or custom) or (2) no
189
+ # command at all. Given that, we can determine whether the entered
190
+ # command is built in or custom based on its companion in the list of
191
+ # argument parser names. We then adjust the prefix based on that.
192
+
193
+ if args.cmd:
194
+ try:
195
+ if f"parsers.{args.cmd}_args" in parser_names:
196
+ mod_name = f"{APP_NAME}.{args.cmd}"
197
+ mod = importlib.import_module(mod_name)
198
+ else:
199
+ mod = load_custom_module(args.cmd, location=CUSTOM_CODE)
200
+ except (ModuleNotFoundError, FileNotFoundError):
201
+ msg = f"""
202
+ Code for a custom command must have the same filename as the
203
+ command itself. Make sure you have a Python file called
204
+ \"{args.cmd}.py\" in: {CUSTOM_CODE}
205
+ """
206
+ print("\n".join([line.strip() for line in msg.split("\n")]))
207
+ sys.exit(1)
208
+ else:
209
+ mod = importlib.import_module(f"{APP_NAME}.null")
210
+
211
+ mod.task_runner(args)
212
+
213
+ return 0
214
+
215
+
216
+ if __name__ == "__main__":
217
+ raise SystemExit(main())
@@ -0,0 +1,68 @@
1
+ from argparse import ArgumentTypeError
2
+
3
+ # ======================================================================
4
+
5
+
6
+ def threshold_type(x: str) -> int:
7
+ """Validate the threshold input.
8
+
9
+ Parameters
10
+ ----------
11
+ x : str
12
+ User input for the threshold option.
13
+
14
+ Returns
15
+ -------
16
+ int
17
+ The validated user input.
18
+
19
+ Raises
20
+ ------
21
+ argparse.ArgumentTypeError
22
+ If the user input is not an integer.
23
+ argparse.ArgumentTypeError
24
+ If the user input is not within the acceptable range [1, 10].
25
+ """
26
+ try:
27
+ x_int = int(x)
28
+ except ValueError:
29
+ raise ArgumentTypeError("Value must be an integer")
30
+
31
+ if x_int not in range(1, 11):
32
+ raise ArgumentTypeError("Value must be between 1 and 10")
33
+
34
+ return x_int
35
+
36
+
37
+ # ======================================================================
38
+
39
+
40
+ def compact_type(x: str) -> int:
41
+ """Validate the compact input.
42
+
43
+ Parameters
44
+ ----------
45
+ x : str
46
+ User input for the compact option.
47
+
48
+ Returns
49
+ -------
50
+ int
51
+ The validated user input.
52
+
53
+ Raises
54
+ ------
55
+ argparse.ArgumentTypeError
56
+ If the user input is not an integer.
57
+ argparse.ArgumentTypeError
58
+ If the user input is not within the acceptable range [1, 255].
59
+ """
60
+ try:
61
+ x_int = int(x)
62
+ except ValueError:
63
+ raise ArgumentTypeError("Value must be an integer")
64
+
65
+ if x_int not in range(1, 256):
66
+ raise ArgumentTypeError("Value must be between 1 and 255")
67
+
68
+ return x_int