|
1 | 1 | import argparse
|
2 |
| -import glob |
| 2 | +import functools |
3 | 3 | import importlib
|
4 | 4 | import logging
|
5 |
| -import os |
6 | 5 | import sys
|
| 6 | +from types import ModuleType |
7 | 7 | import warnings
|
8 |
| -from datetime import datetime |
9 |
| -from http import cookiejar |
10 |
| -from typing import Callable, Optional |
| 8 | +from datetime import datetime, timezone |
| 9 | +from typing import Callable |
11 | 10 |
|
12 |
| -import requests |
13 |
| -from requests.auth import AuthBase, HTTPBasicAuth, HTTPDigestAuth, HTTPProxyAuth |
14 |
| -from requests.sessions import Session |
15 |
| - |
16 |
| -from STACpopulator import __version__ |
| 11 | +from STACpopulator import __version__, implementations |
17 | 12 | from STACpopulator.exceptions import STACPopulatorError
|
18 | 13 | from STACpopulator.logging import setup_logging
|
19 | 14 |
|
20 |
| -POPULATORS = {} |
21 |
| - |
22 |
| - |
23 |
| -class HTTPBearerTokenAuth(AuthBase): |
24 |
| - def __init__(self, token: str) -> None: |
25 |
| - self._token = token |
26 |
| - |
27 |
| - def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest: |
28 |
| - r.headers["Authorization"] = f"Bearer {self._token}" |
29 |
| - return r |
30 |
| - |
31 |
| - |
32 |
| -class HTTPCookieAuth(cookiejar.MozillaCookieJar): |
33 |
| - """ |
34 |
| - Employ a cookie-jar file for authorization. |
35 |
| -
|
36 |
| - Examples of useful command: |
37 |
| -
|
38 |
| - .. code-block:: shell |
39 |
| -
|
40 |
| - curl --cookie-jar /path/to/cookie-jar.txt [authorization-provider-arguments] |
41 |
| -
|
42 |
| - curl \ |
43 |
| - -k \ |
44 |
| - -X POST \ |
45 |
| - --cookie-jar /tmp/magpie-cookie.txt \ |
46 |
| - -d '{"user_name":"...","password":"..."}' \ |
47 |
| - -H 'Accept:application/json' \ |
48 |
| - -H 'Content-Type:application/json' \ |
49 |
| - 'https://{hostname}/magpie/signin' |
50 |
| -
|
51 |
| - .. note:: |
52 |
| - Due to implementation details with :mod:`requests`, this must be passed directly to the ``cookies`` |
53 |
| - attribute rather than ``auth`` as in the case for other authorization handlers. |
54 |
| - """ |
55 |
| - |
56 |
| - |
57 |
| -def add_request_options(parser: argparse.ArgumentParser) -> None: |
58 |
| - """ |
59 |
| - Adds arguments to a parser to allow update of a request session definition used across a populator procedure. |
60 |
| - """ |
61 |
| - parser.add_argument( |
62 |
| - "--no-verify", |
63 |
| - "--no-ssl", |
64 |
| - "--no-ssl-verify", |
65 |
| - dest="verify", |
66 |
| - action="store_false", |
67 |
| - help="Disable SSL verification (not recommended unless for development/test servers).", |
68 |
| - ) |
69 |
| - parser.add_argument("--cert", type=argparse.FileType(), required=False, help="Path to a certificate file to use.") |
70 |
| - parser.add_argument( |
71 |
| - "--auth-handler", |
72 |
| - choices=["basic", "digest", "bearer", "proxy", "cookie"], |
73 |
| - required=False, |
74 |
| - help="Authentication strategy to employ for the requests session.", |
75 |
| - ) |
76 |
| - parser.add_argument( |
77 |
| - "--auth-identity", |
78 |
| - required=False, |
79 |
| - help="Bearer token, cookie-jar file or proxy/digest/basic username:password for selected authorization handler.", |
80 |
| - ) |
81 |
| - |
82 | 15 |
|
83 |
| -def apply_request_options(session: Session, namespace: argparse.Namespace) -> None: |
84 |
| - """ |
85 |
| - Applies the relevant request session options from parsed input arguments. |
86 |
| - """ |
87 |
| - session.verify = namespace.verify |
88 |
| - session.cert = namespace.cert |
89 |
| - if namespace.auth_handler in ["basic", "digest", "proxy"]: |
90 |
| - usr, pwd = namespace.auth_identity.split(":", 1) |
91 |
| - if namespace.auth_handler == "basic": |
92 |
| - session.auth = HTTPBasicAuth(usr, pwd) |
93 |
| - elif namespace.auth_handler == "digest": |
94 |
| - session.auth = HTTPDigestAuth(usr, pwd) |
95 |
| - else: |
96 |
| - session.auth = HTTPProxyAuth(usr, pwd) |
97 |
| - elif namespace.auth_handler == "bearer": |
98 |
| - session.auth = HTTPBearerTokenAuth(namespace.auth_identity) |
99 |
| - elif namespace.auth_handler == "cookie": |
100 |
| - session.cookies = HTTPCookieAuth(namespace.auth_identity) |
101 |
| - session.cookies.load(namespace.auth_identity) |
102 |
| - |
103 |
| - |
104 |
| -def make_main_parser() -> argparse.ArgumentParser: |
105 |
| - parser = argparse.ArgumentParser(prog="stac-populator", description="STACpopulator operations.") |
| 16 | +def add_parser_args(parser: argparse.ArgumentParser) -> dict[str, Callable]: |
106 | 17 | parser.add_argument(
|
107 | 18 | "--version",
|
108 | 19 | "-V",
|
109 | 20 | action="version",
|
110 | 21 | version=f"%(prog)s {__version__}",
|
111 | 22 | help="prints the version of the library and exits",
|
112 | 23 | )
|
113 |
| - commands = parser.add_subparsers(title="command", dest="command", description="STAC populator command to execute.") |
114 |
| - |
115 |
| - run_cmd_parser = make_run_command_parser(parser.prog) |
116 |
| - commands.add_parser( |
117 |
| - "run", |
118 |
| - prog=f"{parser.prog} {run_cmd_parser.prog}", |
119 |
| - parents=[run_cmd_parser], |
120 |
| - formatter_class=run_cmd_parser.formatter_class, |
121 |
| - usage=run_cmd_parser.usage, |
122 |
| - add_help=False, |
123 |
| - help=run_cmd_parser.description, |
124 |
| - description=run_cmd_parser.description, |
| 24 | + parser.add_argument("--debug", action="store_const", const=logging.DEBUG, help="set logger level to debug") |
| 25 | + parser.add_argument( |
| 26 | + "--log_file", help="file to write log output to. By default logs will be written to the current directory." |
125 | 27 | )
|
| 28 | + commands_subparser = parser.add_subparsers( |
| 29 | + title="command", dest="command", description="STAC populator command to execute.", required=True |
| 30 | + ) |
| 31 | + run_parser = commands_subparser.add_parser("run", description="Run a STACpopulator implementation") |
| 32 | + populators_subparser = run_parser.add_subparsers( |
| 33 | + title="populator", dest="populator", description="Implementation to run." |
| 34 | + ) |
| 35 | + for implementation_module_name, module in implementation_modules().items(): |
| 36 | + implementation_parser = populators_subparser.add_parser(implementation_module_name) |
| 37 | + module.add_parser_args(implementation_parser) |
126 | 38 |
|
127 |
| - # add more commands as needed... |
128 |
| - parser.add_argument("--debug", action="store_true", help="Set logger level to debug") |
129 |
| - |
130 |
| - return parser |
131 |
| - |
132 |
| - |
133 |
| -def make_run_command_parser(parent) -> argparse.ArgumentParser: |
134 |
| - """ |
135 |
| - Groups all sub-populator CLI listed in :py:mod:`STACpopulator.implementations` as a common ``stac-populator`` CLI. |
136 |
| -
|
137 |
| - Dispatches the provided arguments to the appropriate sub-populator CLI as requested. Each sub-populator CLI must |
138 |
| - implement functions ``make_parser`` and ``main`` to generate the arguments and dispatch them to the corresponding |
139 |
| - caller. The ``main`` function should accept a sequence of string arguments, which can be passed to the parser |
140 |
| - obtained from ``make_parser``. |
141 | 39 |
|
142 |
| - An optional ``runner`` can also be defined in each populator module. If provided, the namespace arguments that have |
143 |
| - already been parsed to resolve the populator to run will be used directly, avoiding parsing arguments twice. |
144 |
| - """ |
145 |
| - parser = argparse.ArgumentParser(prog="run", description="STACpopulator implementation runner.") |
146 |
| - subparsers = parser.add_subparsers(title="populator", dest="populator", description="Implementation to run.") |
147 |
| - populators_impl = "implementations" |
148 |
| - populators_dir = os.path.join(os.path.dirname(__file__), populators_impl) |
149 |
| - populator_mods = glob.glob(f"{populators_dir}/**/[!__init__]*.py", recursive=True) # potential candidate scripts |
150 |
| - for populator_path in sorted(populator_mods): |
151 |
| - populator_script = populator_path.split(populators_dir, 1)[1][1:] |
152 |
| - populator_py_mod = os.path.splitext(populator_script)[0].replace(os.sep, ".") |
153 |
| - populator_name, pop_mod_file = populator_py_mod.rsplit(".", 1) |
154 |
| - populator_root = f"STACpopulator.{populators_impl}.{populator_name}" |
155 |
| - pop_mod_file_loc = f"{populator_root}.{pop_mod_file}" |
| 40 | +@functools.cache |
| 41 | +def implementation_modules() -> dict[str, ModuleType]: |
| 42 | + modules = {} |
| 43 | + for implementation_module_name in implementations.__all__: |
156 | 44 | try:
|
157 |
| - populator_module = importlib.import_module(pop_mod_file_loc, populator_root) |
158 |
| - except STACPopulatorError as e: |
159 |
| - warnings.warn(f"Could not load extension {populator_name} because of error {e}") |
160 |
| - continue |
161 |
| - parser_maker: Callable[[], argparse.ArgumentParser] = getattr(populator_module, "make_parser", None) |
162 |
| - populator_runner = getattr(populator_module, "runner", None) # optional, call main directly if not available |
163 |
| - populator_caller = getattr(populator_module, "main", None) |
164 |
| - if callable(parser_maker) and callable(populator_caller): |
165 |
| - populator_parser = parser_maker() |
166 |
| - populator_prog = f"{parent} {parser.prog} {populator_name}" |
167 |
| - subparsers.add_parser( |
168 |
| - populator_name, |
169 |
| - prog=populator_prog, |
170 |
| - parents=[populator_parser], |
171 |
| - formatter_class=populator_parser.formatter_class, |
172 |
| - add_help=False, # add help disabled otherwise conflicts with this main populator help |
173 |
| - help=populator_parser.description, |
174 |
| - description=populator_parser.description, |
175 |
| - usage=populator_parser.usage, |
| 45 | + modules[implementation_module_name] = importlib.import_module( |
| 46 | + f".{implementation_module_name}", implementations.__package__ |
176 | 47 | )
|
177 |
| - POPULATORS[populator_name] = { |
178 |
| - "name": populator_name, |
179 |
| - "caller": populator_caller, |
180 |
| - "parser": populator_parser, |
181 |
| - "runner": populator_runner, |
182 |
| - } |
183 |
| - return parser |
| 48 | + except STACPopulatorError as e: |
| 49 | + warnings.warn(f"Could not load extension {implementation_module_name} because of error {e}") |
| 50 | + return modules |
184 | 51 |
|
185 | 52 |
|
186 |
| -def main(*args: str) -> Optional[int]: |
187 |
| - parser = make_main_parser() |
188 |
| - args = args or sys.argv[1:] # same as was parse args does, but we must provide them to subparser |
189 |
| - ns = parser.parse_args(args=args) # if 'command' or 'populator' unknown, auto prints the help message with exit(2) |
190 |
| - params = vars(ns) |
191 |
| - populator_cmd = params.pop("command") |
192 |
| - if not populator_cmd: |
193 |
| - parser.print_help() |
194 |
| - return 0 |
195 |
| - result = None |
196 |
| - if populator_cmd == "run": |
197 |
| - populator_name = params.pop("populator") |
| 53 | +def run(ns: argparse.Namespace) -> int: |
| 54 | + if ns.command == "run": |
| 55 | + logfile_name = ns.log_file or f"{ns.populator}_log_{datetime.now(timezone.utc).isoformat() + 'Z'}.jsonl" |
| 56 | + setup_logging(logfile_name, ns.debug or logging.INFO) |
| 57 | + return implementation_modules()[ns.populator].runner(ns) or 0 |
198 | 58 |
|
199 |
| - # Setup the application logger: |
200 |
| - fname = f"{populator_name}_log_{datetime.utcnow().isoformat() + 'Z'}.jsonl" |
201 |
| - log_level = logging.DEBUG if ns.debug else logging.INFO |
202 |
| - setup_logging(fname, log_level) |
203 | 59 |
|
204 |
| - if not populator_name: |
205 |
| - parser.print_help() |
206 |
| - return 0 |
207 |
| - populator_args = args[2:] # skip [command] [populator] |
208 |
| - populator_caller = POPULATORS[populator_name]["caller"] |
209 |
| - populator_runner = POPULATORS[populator_name]["runner"] |
210 |
| - if populator_runner: |
211 |
| - result = populator_runner(ns) |
212 |
| - else: |
213 |
| - result = populator_caller(*populator_args) |
214 |
| - return 0 if result is None else result |
| 60 | +def main(*args: str) -> int: |
| 61 | + parser = argparse.ArgumentParser() |
| 62 | + add_parser_args(parser) |
| 63 | + ns = parser.parse_args(args or None) |
| 64 | + return run(ns) |
215 | 65 |
|
216 | 66 |
|
217 | 67 | if __name__ == "__main__":
|
|
0 commit comments