-
Notifications
You must be signed in to change notification settings - Fork 61
ENH: Added CLI command to update skops files #343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c0238de
c23b4d0
f0923c1
04a1738
157170d
121c7ca
683281d
5c382da
7e8ef1f
3153180
26204bb
0ca4128
e5d190d
987eeec
aa1f145
c681d58
4df6c61
8f6248e
dcb55c1
7846077
854e0ba
a4be07b
734ed43
50ab3a4
d9aac83
b44dca4
3a30721
42006b6
68e5e67
1136741
ae869cd
1455380
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -117,6 +117,8 @@ node_modules | |
| # Vim | ||
| *.swp | ||
|
|
||
| # MacOS | ||
| .DS_Store | ||
|
|
||
| exports | ||
| trash | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| import logging | ||
| import shutil | ||
| import tempfile | ||
| import zipfile | ||
| from pathlib import Path | ||
|
|
||
| from skops.cli._utils import get_log_level | ||
| from skops.io import dump, load | ||
| from skops.io._protocol import PROTOCOL | ||
|
|
||
|
|
||
| def _update_file( | ||
| input_file: str | Path, | ||
| output_file: str | Path | None = None, | ||
| inplace: bool = False, | ||
| logger: logging.Logger = logging.getLogger(), | ||
| ) -> None: | ||
| """Function that is called by ``skops update`` entrypoint. | ||
|
|
||
| Loads a skops model from the input path, updates it to the current skops format, and | ||
| saves to an output file. It will overwrite the input file if `inplace` is True. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| input_file : str, or Path | ||
| Path of input skops model to load. | ||
|
|
||
| output_file : str, or Path, default=None | ||
| Path to save the updated skops model to. | ||
|
|
||
| inplace : bool, default=False | ||
| Whether to update and overwrite the input file in place. | ||
|
|
||
| logger : logging.Logger, default=logging.getLogger() | ||
| Logger to use for logging. | ||
| """ | ||
| if inplace: | ||
| if output_file is None: | ||
| output_file = input_file | ||
| else: | ||
| raise ValueError( | ||
| "Cannot specify both an output file path and the inplace flag. Please" | ||
| " choose whether you want to create a new file or overwrite the input" | ||
| " file." | ||
| ) | ||
|
|
||
|
EdAbati marked this conversation as resolved.
|
||
| input_model = load(input_file, trusted=True) | ||
| with zipfile.ZipFile(input_file, "r") as zip_file: | ||
| input_file_schema = json.loads(zip_file.read("schema.json")) | ||
|
|
||
| if input_file_schema["protocol"] == PROTOCOL: | ||
| logger.warning( | ||
| "File was not updated because already up to date with the current protocol:" | ||
| f" {PROTOCOL}" | ||
| ) | ||
| return None | ||
|
|
||
| if input_file_schema["protocol"] > PROTOCOL: | ||
| logger.warning( | ||
| "File cannot be updated because its protocol is more recent than the " | ||
| f"current protocol: {PROTOCOL}" | ||
| ) | ||
| return None | ||
|
|
||
| if output_file is None: | ||
| logger.warning( | ||
| f"File can be updated to the current protocol: {PROTOCOL}. Please" | ||
| " specify an output file path or use the `inplace` flag to create the" | ||
| " updated Skops file." | ||
| ) | ||
| return None | ||
|
|
||
| with tempfile.TemporaryDirectory() as tmp_dir: | ||
| tmp_output_file = Path(tmp_dir) / f"{output_file}.tmp" | ||
| dump(input_model, tmp_output_file) | ||
| shutil.move(str(tmp_output_file), str(output_file)) | ||
| logger.info(f"Updated skops file written to {output_file}") | ||
|
|
||
|
|
||
| def format_parser( | ||
| parser: argparse.ArgumentParser | None = None, | ||
| ) -> argparse.ArgumentParser: | ||
| """Adds arguments and help to parent CLI parser for the `update` method.""" | ||
|
|
||
| if not parser: # used in tests | ||
| parser = argparse.ArgumentParser() | ||
|
|
||
| parser_subgroup = parser.add_argument_group("update") | ||
| parser_subgroup.add_argument("input", help="Path to an input file to update.") | ||
|
|
||
| parser_subgroup.add_argument( | ||
| "-o", | ||
| "--output-file", | ||
| help="Specify the output file name for the updated skops file.", | ||
| default=None, | ||
| ) | ||
| parser_subgroup.add_argument( | ||
| "--inplace", | ||
| help="Update and overwrite the input file in place.", | ||
| action="store_true", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want inplace to be the default?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea that
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That behavior would be fine. It would also be acceptable IMO to update inplace by default, since it would only be done if the current version is higher, but leaving it like this is also good. Just one question: Right now, if using the defaults (no change to log level), a user would typically not get feedback at all, right? Do we want the default behavior to be not do anything, nor message anything?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah yes you are right. Because the messages are logged as "info" and the default logging level should be "warning". They will not show up if one doesn't set Actually we could log certain messages as "warning", not sure if "info" is the correct level for somenthing like this for example.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, maybe it's better to elevate that message to "warning". As a user, if I call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool I changed some levels here: b44dca4 |
||
| ) | ||
| parser_subgroup.add_argument( | ||
| "-v", | ||
| "--verbose", | ||
| help=( | ||
| "Increases verbosity of logging. Can be used multiple times to increase " | ||
| "verbosity further." | ||
| ), | ||
| action="count", | ||
| dest="loglevel", | ||
| default=0, | ||
| ) | ||
| return parser | ||
|
|
||
|
|
||
| def main( | ||
| parsed_args: argparse.Namespace, | ||
| logger: logging.Logger = logging.getLogger(), | ||
| ) -> None: | ||
| output_file = Path(parsed_args.output_file) if parsed_args.output_file else None | ||
| input_file = Path(parsed_args.input) | ||
| inplace = parsed_args.inplace | ||
|
|
||
| logging.basicConfig(format="%(levelname)-8s: %(message)s") | ||
| logger.setLevel(level=get_log_level(parsed_args.loglevel)) | ||
|
|
||
| _update_file( | ||
| input_file=input_file, | ||
| output_file=output_file, | ||
| inplace=inplace, | ||
| logger=logger, | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.