-
Notifications
You must be signed in to change notification settings - Fork 28.1k
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
Model versioning #8324
Model versioning #8324
Changes from 15 commits
558190b
a10fb6c
1a037f0
0604e83
2790c6b
d3c6382
604f7b3
d2143ae
fe85412
8d3cd55
caa5aeb
7610640
35329c8
87d9087
0e42b97
8367100
9749636
f0bc41d
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 |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import os | ||
import subprocess | ||
import sys | ||
from argparse import ArgumentParser | ||
from getpass import getpass | ||
|
@@ -21,8 +22,10 @@ def register_subcommand(parser: ArgumentParser): | |
whoami_parser.set_defaults(func=lambda args: WhoamiCommand(args)) | ||
logout_parser = parser.add_parser("logout", help="Log out") | ||
logout_parser.set_defaults(func=lambda args: LogoutCommand(args)) | ||
# s3 | ||
s3_parser = parser.add_parser("s3", help="{ls, rm} Commands to interact with the files you upload on S3.") | ||
# s3_datasets (s3-based system) | ||
s3_parser = parser.add_parser( | ||
"s3_datasets", help="{ls, rm} Commands to interact with the files you upload on S3." | ||
) | ||
s3_subparsers = s3_parser.add_subparsers(help="s3 related commands") | ||
ls_parser = s3_subparsers.add_parser("ls") | ||
ls_parser.add_argument("--organization", type=str, help="Optional: organization namespace.") | ||
|
@@ -31,17 +34,42 @@ def register_subcommand(parser: ArgumentParser): | |
rm_parser.add_argument("filename", type=str, help="individual object filename to delete from S3.") | ||
rm_parser.add_argument("--organization", type=str, help="Optional: organization namespace.") | ||
rm_parser.set_defaults(func=lambda args: DeleteObjCommand(args)) | ||
# upload | ||
upload_parser = parser.add_parser("upload", help="Upload a model to S3.") | ||
upload_parser.add_argument( | ||
"path", type=str, help="Local path of the model folder or individual file to upload." | ||
) | ||
upload_parser = s3_subparsers.add_parser("upload", help="Upload a file to S3.") | ||
upload_parser.add_argument("path", type=str, help="Local path of the folder or individual file to upload.") | ||
upload_parser.add_argument("--organization", type=str, help="Optional: organization namespace.") | ||
upload_parser.add_argument( | ||
"--filename", type=str, default=None, help="Optional: override individual object filename on S3." | ||
) | ||
upload_parser.add_argument("-y", "--yes", action="store_true", help="Optional: answer Yes to the prompt") | ||
upload_parser.set_defaults(func=lambda args: UploadCommand(args)) | ||
# deprecated model upload | ||
upload_parser = parser.add_parser( | ||
"upload", | ||
help=( | ||
"Deprecated: used to be the way to upload a model to S3." | ||
" We now use a git-based system for storing models and other artifacts." | ||
" Use the `repo create` command instead." | ||
), | ||
) | ||
upload_parser.set_defaults(func=lambda args: DeprecatedUploadCommand(args)) | ||
|
||
# new system: git-based repo system | ||
repo_parser = parser.add_parser( | ||
"repo", help="{create, ls-files} Commands to interact with your huggingface.co repos." | ||
) | ||
repo_subparsers = repo_parser.add_subparsers(help="huggingface.co repos related commands") | ||
ls_parser = repo_subparsers.add_parser("ls-files", help="List all your files on huggingface.co") | ||
ls_parser.add_argument("--organization", type=str, help="Optional: organization namespace.") | ||
ls_parser.set_defaults(func=lambda args: ListReposObjsCommand(args)) | ||
repo_create_parser = repo_subparsers.add_parser("create", help="Create a new repo on huggingface.co") | ||
repo_create_parser.add_argument( | ||
"name", | ||
type=str, | ||
help="Name for your model's repo. Will be namespaced under your username to build the model id.", | ||
) | ||
repo_create_parser.add_argument("--organization", type=str, help="Optional: organization namespace.") | ||
repo_create_parser.add_argument("-y", "--yes", action="store_true", help="Optional: answer Yes to the prompt") | ||
repo_create_parser.set_defaults(func=lambda args: RepoCreateCommand(args)) | ||
|
||
|
||
class ANSI: | ||
|
@@ -51,6 +79,7 @@ class ANSI: | |
|
||
_bold = "\u001b[1m" | ||
_red = "\u001b[31m" | ||
_gray = "\u001b[90m" | ||
_reset = "\u001b[0m" | ||
|
||
@classmethod | ||
|
@@ -61,6 +90,27 @@ def bold(cls, s): | |
def red(cls, s): | ||
return "{}{}{}".format(cls._bold + cls._red, s, cls._reset) | ||
|
||
@classmethod | ||
def gray(cls, s): | ||
return "{}{}{}".format(cls._gray, s, cls._reset) | ||
|
||
|
||
def tabulate(rows: List[List[Union[str, int]]], headers: List[str]) -> str: | ||
""" | ||
Inspired by: | ||
|
||
- stackoverflow.com/a/8356620/593036 | ||
- stackoverflow.com/questions/9535954/printing-lists-as-tabular-data | ||
""" | ||
col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)] | ||
row_format = ("{{:{}}} " * len(headers)).format(*col_widths) | ||
lines = [] | ||
lines.append(row_format.format(*headers)) | ||
lines.append(row_format.format(*["-" * w for w in col_widths])) | ||
for row in rows: | ||
lines.append(row_format.format(*row)) | ||
return "\n".join(lines) | ||
|
||
|
||
class BaseUserCommand: | ||
def __init__(self, args): | ||
|
@@ -124,22 +174,6 @@ def run(self): | |
|
||
|
||
class ListObjsCommand(BaseUserCommand): | ||
def tabulate(self, rows: List[List[Union[str, int]]], headers: List[str]) -> str: | ||
""" | ||
Inspired by: | ||
|
||
- stackoverflow.com/a/8356620/593036 | ||
- stackoverflow.com/questions/9535954/printing-lists-as-tabular-data | ||
""" | ||
col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)] | ||
row_format = ("{{:{}}} " * len(headers)).format(*col_widths) | ||
lines = [] | ||
lines.append(row_format.format(*headers)) | ||
lines.append(row_format.format(*["-" * w for w in col_widths])) | ||
for row in rows: | ||
lines.append(row_format.format(*row)) | ||
return "\n".join(lines) | ||
|
||
def run(self): | ||
token = HfFolder.get_token() | ||
if token is None: | ||
|
@@ -155,7 +189,7 @@ def run(self): | |
print("No shared file yet") | ||
exit() | ||
rows = [[obj.filename, obj.LastModified, obj.ETag, obj.Size] for obj in objs] | ||
print(self.tabulate(rows, headers=["Filename", "LastModified", "ETag", "Size"])) | ||
print(tabulate(rows, headers=["Filename", "LastModified", "ETag", "Size"])) | ||
|
||
|
||
class DeleteObjCommand(BaseUserCommand): | ||
|
@@ -173,6 +207,85 @@ def run(self): | |
print("Done") | ||
|
||
|
||
class ListReposObjsCommand(BaseUserCommand): | ||
def run(self): | ||
token = HfFolder.get_token() | ||
if token is None: | ||
print("Not logged in") | ||
exit(1) | ||
try: | ||
objs = self._api.list_repos_objs(token, organization=self.args.organization) | ||
except HTTPError as e: | ||
print(e) | ||
print(ANSI.red(e.response.text)) | ||
exit(1) | ||
if len(objs) == 0: | ||
print("No shared file yet") | ||
exit() | ||
rows = [[obj.filename, obj.lastModified, obj.commit, obj.size] for obj in objs] | ||
print(tabulate(rows, headers=["Filename", "LastModified", "Commit-Sha", "Size"])) | ||
|
||
|
||
class RepoCreateCommand(BaseUserCommand): | ||
def run(self): | ||
token = HfFolder.get_token() | ||
if token is None: | ||
print("Not logged in") | ||
exit(1) | ||
try: | ||
stdout = subprocess.run(["git", "--version"], capture_output=True).stdout.decode("utf-8") | ||
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'm currently getting a:
thrown here. I'll start investigating it now! 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. Related to: https://stackoverflow.com/q/53209127 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. it's not supported in py3.6 it seems. The simplest most supported way is 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. Good catch, will make sure it runs on PY3.6 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 previously added a comment about default encodings (which is cp1252 on Windows by default). However, there are many factors that determine the encoding for specific things ( I do not have the time to look into this further to figure out what the best, cross-platform, cross-Python-version way is to deal with this. As for now, since the output of For those interested, this is a nice PEP that talks about the slow move to defaulting utf-8 in Python across the board. |
||
print(ANSI.gray(stdout.strip())) | ||
except FileNotFoundError: | ||
print("Looks like you do not have git installed, please install.") | ||
|
||
try: | ||
stdout = subprocess.run(["git-lfs", "--version"], capture_output=True).stdout.decode("utf-8") | ||
print(ANSI.gray(stdout.strip())) | ||
except FileNotFoundError: | ||
print( | ||
ANSI.red( | ||
"Looks like you do not have git-lfs installed, please install." | ||
" You can install from https://git-lfs.github.com/." | ||
" Then run `git lfs install` (you only have to do this once)." | ||
) | ||
) | ||
print("") | ||
|
||
user, _ = self._api.whoami(token) | ||
namespace = self.args.organization if self.args.organization is not None else user | ||
|
||
print("You are about to create {}".format(ANSI.bold(namespace + "/" + self.args.name))) | ||
|
||
if not self.args.yes: | ||
choice = input("Proceed? [Y/n] ").lower() | ||
if not (choice == "" or choice == "y" or choice == "yes"): | ||
print("Abort") | ||
exit() | ||
try: | ||
url = self._api.create_repo(token, name=self.args.name, organization=self.args.organization) | ||
except HTTPError as e: | ||
print(e) | ||
print(ANSI.red(e.response.text)) | ||
exit(1) | ||
print("\nYour repo now lives at:") | ||
print(" {}".format(ANSI.bold(url))) | ||
print("\nYou can clone it locally with the command below," " and commit/push as usual.") | ||
print(f"\n git clone {url}") | ||
julien-c marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print("") | ||
|
||
|
||
class DeprecatedUploadCommand(BaseUserCommand): | ||
def run(self): | ||
print( | ||
ANSI.red( | ||
"Deprecated: used to be the way to upload a model to S3." | ||
" We now use a git-based system for storing models and other artifacts." | ||
" Use the `repo create` command instead." | ||
) | ||
) | ||
exit(1) | ||
|
||
|
||
class UploadCommand(BaseUserCommand): | ||
def walk_dir(self, rel_path): | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also cc'ing @stas00 on this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the heads up, @julien-c - that's a wonderful news/change!