mirror of
https://github.com/modelscope/modelscope.git
synced 2026-07-09 20:09:17 +02:00
[Feat & Refactor] Refactor hub and CLI modules (#1732)
* refactor(hub): shim layer delegating to modelscope-hub - Replace hub/api.py (4674→250 lines) with shim inheriting LegacyHubApi - Replace hub/snapshot_download.py, callback.py with thin shims - Partial shim hub/file_download.py (retain http_get_file) - Shim hub/constants.py and errors.py with legacy aliases - Shim hub/git.py, repository.py, cache_manager.py, upload_*.py - Migrate CLI entry to modelscope_hub.cli.main:run_cmd - Adapt 6 CLI commands as modelscope_hub.cli_plugins - Delete redundant CLI files (download/upload/login/create/etc) - Add modelscope-hub>=0.2.0 dependency, Python>=3.10 - Add __getattr__ proxy for forward-compatible method access - Propagate timeout/max_retries to internal LegacyClient - Bridge MODELSCOPE_CREDENTIALS_PATH env var to HubConfig * fix lint: isort/yapf formatting + exclude hub/api.py from hooks * set modelscope-hub>=0.0.5 * remove unused code * refactor(hub): standardize token naming — git_token vs token Disambiguate git token and SDK/API token naming across the hub layer: - ModelScopeConfig: get_token/save_token → get_git_token/save_git_token (old names kept as deprecated aliases with DeprecationWarning) - GitCommandWrapper: rename token params to git_token in clone/push/config - Repository/DatasetRepository: auth_token → git_token (deprecated compat kept) - data_loader.py: update caller to use get_git_token() SDK token references (HubApi(token=...), get_cookies(access_token=...), commit_scheduler.token) remain unchanged as they correctly use `token` naming. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * remove(msdatasets): remove all Virgo-related implementation Remove the entire Virgo dataset subsystem which is no longer needed: - Remove VirgoDataset class and VirgoDownloader - Remove VirgoAuthConfig and VirgoDatasetConfig - Remove Hubs.virgo enum value - Remove fetch_virgo_meta from DataMetaManager - Remove download_virgo_files from DatasetContextConfig - Remove test_virgo_dataset.py test file - Clean up unused imports (pandas, MaxComputeUtil, valid_url, etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(hub): add OSS dataset operations and meta-file download to HubApi Add methods that msdatasets depends on but don't belong in modelscope_hub: - _legacy_request: internal helper combining legacy HTTP transport with application-level envelope validation (Code/Data/Message) - list_oss_dataset_objects: list OSS storage objects for a dataset - delete_oss_dataset_object / delete_oss_dataset_dir: delete OSS objects - fetch_meta_files_from_url: download and cache meta CSV/JSONL files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix imports issue * fix: address PR review feedback - cli/plugins.py: change --yes and --all flags to action='store_true' - hub/git.py: replace os.linesep with .splitlines() for cross-platform safety - hub/__init__.py: use is_file() with fallback for robust credentials path detection * fix lint * update ms hub version * fix(ci): add PyPI official as fallback index for pip Aliyun mirror may lag behind PyPI for newly published packages, causing dependency resolution failures (e.g. modelscope-hub>=0.0.6). Add pypi.org/simple as extra-index-url so new versions are immediately available while keeping the Aliyun mirror as the primary source. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix UTs * remove unused UTs * fix ut * update modelscope-hub installation for source code * fix UT * fix uts * fix ut --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
if [ "$MODELSCOPE_SDK_DEBUG" == "True" ]; then
|
||||
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
|
||||
pip config set global.extra-index-url https://pypi.org/simple/
|
||||
pip config set install.trusted-host mirrors.aliyun.com
|
||||
pip install -r requirements/tests.txt
|
||||
git config --global --add safe.directory /Maas-lib
|
||||
|
||||
@@ -21,7 +21,8 @@ repos:
|
||||
examples/|
|
||||
modelscope/utils/ast_index_file.py|
|
||||
modelscope/fileio/format/jsonplus.py|
|
||||
modelscope/msdatasets/utils/_module_factories\.py
|
||||
modelscope/msdatasets/utils/_module_factories\.py|
|
||||
modelscope/hub/api\.py
|
||||
)$
|
||||
- repo: https://github.com/pre-commit/mirrors-yapf.git
|
||||
rev: v0.30.0
|
||||
@@ -33,7 +34,8 @@ repos:
|
||||
examples/|
|
||||
modelscope/utils/ast_index_file.py|
|
||||
modelscope/fileio/format/jsonplus.py|
|
||||
modelscope/msdatasets/utils/_module_factories\.py
|
||||
modelscope/msdatasets/utils/_module_factories\.py|
|
||||
modelscope/hub/api\.py
|
||||
)$
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks.git
|
||||
rev: v3.1.0
|
||||
|
||||
@@ -18,10 +18,11 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple && \
|
||||
pip config set global.extra-index-url "https://pypi.org/simple" && \
|
||||
pip config set install.trusted-host mirrors.aliyun.com && \
|
||||
ARCH=$(uname -m) && \
|
||||
if [ "$ARCH" = "x86_64" ]; then \
|
||||
pip config set global.extra-index-url "https://download.pytorch.org/whl/cpu/"; \
|
||||
pip config set global.extra-index-url "https://pypi.org/simple https://download.pytorch.org/whl/cpu/"; \
|
||||
fi
|
||||
|
||||
{extra_content}
|
||||
|
||||
@@ -60,6 +60,7 @@ RUN bash /tmp/install.sh {version_args} && \
|
||||
pip install --no-cache-dir transformers diffusers 'timm>=0.9.0' && pip cache purge; \
|
||||
pip install --no-cache-dir omegaconf==2.3.0 && pip cache purge; \
|
||||
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple && \
|
||||
pip config set global.extra-index-url https://pypi.org/simple && \
|
||||
pip config set install.trusted-host mirrors.aliyun.com && \
|
||||
cp /tmp/resources/ubuntu2204.aliyun /etc/apt/sources.list
|
||||
|
||||
|
||||
@@ -47,4 +47,5 @@ else
|
||||
fi
|
||||
|
||||
pip config set global.index-url https://mirrors.cloud.aliyuncs.com/pypi/simple
|
||||
pip config set global.extra-index-url https://pypi.org/simple
|
||||
pip config set install.trusted-host mirrors.cloud.aliyuncs.com
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""CLI base class — re-exports :class:`CLICommand` from ``modelscope_hub``.
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from argparse import ArgumentParser
|
||||
Kept as a thin alias so existing imports such as
|
||||
``from modelscope.cli.base import CLICommand`` continue to work after the
|
||||
CLI engine moved into ``modelscope_hub``.
|
||||
"""
|
||||
|
||||
from modelscope_hub.cli.base import CLICommand # noqa: F401
|
||||
|
||||
class CLICommand(ABC):
|
||||
"""
|
||||
Base class for command line tool.
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def execute(self):
|
||||
raise NotImplementedError()
|
||||
__all__ = ['CLICommand']
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Clear-cache CLI command — retained for backward compatibility.
|
||||
|
||||
The ``modelscope_hub`` CLI now owns ``clear-cache`` as an alias for
|
||||
``cache clear``, but this module preserves the legacy :class:`ClearCacheCMD`
|
||||
class so that existing tests and callers that import it directly continue
|
||||
to work.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.hub.constants import TEMPORARY_FOLDER_NAME
|
||||
from modelscope.hub.utils.utils import get_model_masked_directory
|
||||
from modelscope.utils.file_utils import (get_dataset_cache_root,
|
||||
get_model_cache_root,
|
||||
get_modelscope_cache_dir)
|
||||
@@ -24,6 +30,11 @@ class ClearCacheCMD(CLICommand):
|
||||
self.args = args
|
||||
self.cache_dir = get_modelscope_cache_dir()
|
||||
|
||||
@staticmethod
|
||||
def register(subparsers) -> None:
|
||||
"""Register clear-cache subcommand (CLICommand ABC contract)."""
|
||||
ClearCacheCMD.define_args(subparsers)
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for clear-cache command.
|
||||
@@ -33,17 +44,15 @@ class ClearCacheCMD(CLICommand):
|
||||
group.add_argument(
|
||||
'--model',
|
||||
type=str,
|
||||
help=
|
||||
'The id of the model whose cache will be cleared. For clear-cache, '
|
||||
'if neither model or dataset id is provided, entire cache will be cleared.'
|
||||
)
|
||||
help='The id of the model whose cache will be cleared. '
|
||||
'If neither model or dataset id is provided, entire cache '
|
||||
'will be cleared.')
|
||||
group.add_argument(
|
||||
'--dataset',
|
||||
type=str,
|
||||
help=
|
||||
'The id of the dataset whose cache will be cleared. For clear-cache, '
|
||||
'if neither model or dataset id is provided, entire cache will be cleared.'
|
||||
)
|
||||
help='The id of the dataset whose cache will be cleared. '
|
||||
'If neither model or dataset id is provided, entire cache '
|
||||
'will be cleared.')
|
||||
|
||||
parser.set_defaults(func=subparser_func)
|
||||
|
||||
@@ -64,7 +73,8 @@ class ClearCacheCMD(CLICommand):
|
||||
id = self.args.dataset
|
||||
prompt = prompt + f'local cache for dataset {id}. '
|
||||
else:
|
||||
prompt = prompt + f'entire ModelScope cache at {self.cache_dir}, including ALL models and dataset.\n'
|
||||
prompt = prompt + (f'entire ModelScope cache at {self.cache_dir}, '
|
||||
f'including ALL models and dataset.\n')
|
||||
all = True
|
||||
user_input = input(
|
||||
prompt
|
||||
|
||||
@@ -1,62 +1,21 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""ModelScope CLI — delegates to the modelscope_hub CLI engine.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
The legacy ``modelscope`` / ``ms`` console-script entry points historically
|
||||
lived here as a hand-rolled argparse tree. The hub CLI in ``modelscope_hub``
|
||||
now owns command registration, plugin discovery, and error translation;
|
||||
this module exists solely to preserve the import path used by the
|
||||
``[project.scripts]`` entries in ``pyproject.toml``.
|
||||
"""
|
||||
|
||||
from modelscope.cli.clearcache import ClearCacheCMD
|
||||
from modelscope.cli.create import CreateCMD
|
||||
from modelscope.cli.download import DownloadCMD
|
||||
from modelscope.cli.llamafile import LlamafileCMD
|
||||
from modelscope.cli.login import LoginCMD
|
||||
from modelscope.cli.modelcard import ModelCardCMD
|
||||
from modelscope.cli.pipeline import PipelineCMD
|
||||
from modelscope.cli.plugins import PluginsCMD
|
||||
from modelscope.cli.scancache import ScanCacheCMD
|
||||
from modelscope.cli.server import ServerCMD
|
||||
from modelscope.cli.skills import SkillsCMD
|
||||
from modelscope.cli.studio import StudioCMD
|
||||
from modelscope.cli.upload import UploadCMD
|
||||
from modelscope.hub.constants import MODELSCOPE_ASCII
|
||||
from modelscope.utils.logger import get_logger
|
||||
from modelscope.version import __version__
|
||||
import sys
|
||||
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
from modelscope_hub.cli.main import run_cmd as _run_cmd
|
||||
|
||||
|
||||
def run_cmd():
|
||||
print(MODELSCOPE_ASCII)
|
||||
parser = argparse.ArgumentParser(
|
||||
'ModelScope Command Line tool', usage='modelscope <command> [<args>]')
|
||||
parser.add_argument(
|
||||
'-V',
|
||||
'--version',
|
||||
action='version',
|
||||
version=f'ModelScope CLI {__version__}')
|
||||
parser.add_argument(
|
||||
'--token', default=None, help='Specify ModelScope SDK token.')
|
||||
subparsers = parser.add_subparsers(help='modelscope commands helpers')
|
||||
|
||||
CreateCMD.define_args(subparsers)
|
||||
DownloadCMD.define_args(subparsers)
|
||||
SkillsCMD.define_args(subparsers)
|
||||
UploadCMD.define_args(subparsers)
|
||||
ClearCacheCMD.define_args(subparsers)
|
||||
PluginsCMD.define_args(subparsers)
|
||||
PipelineCMD.define_args(subparsers)
|
||||
ModelCardCMD.define_args(subparsers)
|
||||
ServerCMD.define_args(subparsers)
|
||||
LoginCMD.define_args(subparsers)
|
||||
LlamafileCMD.define_args(subparsers)
|
||||
ScanCacheCMD.define_args(subparsers)
|
||||
StudioCMD.define_args(subparsers)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not hasattr(args, 'func'):
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
cmd = args.func(args)
|
||||
cmd.execute()
|
||||
"""Delegate to ``modelscope_hub.cli.main.run_cmd`` and propagate its exit code."""
|
||||
sys.exit(_run_cmd())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
from argparse import ArgumentParser, _SubParsersAction
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.constants import (GatedMode, Licenses, ModelVisibility,
|
||||
Visibility, VisibilityMap)
|
||||
from modelscope.hub.utils.aigc import AigcModel
|
||||
from modelscope.hub.utils.utils import resolve_endpoint
|
||||
from modelscope.utils.constant import (REPO_TYPE_MODEL, REPO_TYPE_STUDIO,
|
||||
REPO_TYPE_SUPPORT, StudioHardware,
|
||||
StudioSDKType)
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return CreateCMD(args)
|
||||
|
||||
|
||||
class CreateCMD(CLICommand):
|
||||
"""
|
||||
Command for creating a new repository, supporting both model and dataset.
|
||||
"""
|
||||
|
||||
name = 'create'
|
||||
|
||||
def __init__(self, args: _SubParsersAction):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: _SubParsersAction):
|
||||
|
||||
parser: ArgumentParser = parsers.add_parser(CreateCMD.name)
|
||||
|
||||
parser.add_argument(
|
||||
'repo_id',
|
||||
type=str,
|
||||
help='The ID of the repo to create (e.g. `username/repo-name`)')
|
||||
parser.add_argument(
|
||||
'--token',
|
||||
type=str,
|
||||
default=None,
|
||||
help=
|
||||
'A User Access Token generated from https://modelscope.cn/my/myaccesstoken to authenticate the user. '
|
||||
'If not provided, the CLI will use the local credentials if available.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--repo_type',
|
||||
choices=REPO_TYPE_SUPPORT,
|
||||
default=REPO_TYPE_MODEL,
|
||||
help=
|
||||
'Type of the repo to create (e.g. `dataset`, `model`). Default to `model`.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--visibility',
|
||||
choices=[
|
||||
Visibility.PUBLIC, Visibility.INTERNAL, Visibility.PRIVATE
|
||||
],
|
||||
default=Visibility.PUBLIC,
|
||||
help='Visibility of the repo to create. Default to `public`.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--chinese_name',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Optional, Chinese name of the repo. Default to `None`.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--license',
|
||||
type=str,
|
||||
choices=Licenses.to_list(),
|
||||
default=Licenses.APACHE_V2,
|
||||
help=
|
||||
'Optional, License of the repo. Default to `Apache License 2.0`.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--exist_ok',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=
|
||||
'If True, do not raise error when repo already exists. Defaults to False.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--gated',
|
||||
dest='gated_mode',
|
||||
action='store_true',
|
||||
default=None,
|
||||
help=
|
||||
'Enable gated mode (application-based download) for private repos.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-gated',
|
||||
dest='gated_mode',
|
||||
action='store_false',
|
||||
help='Disable gated mode for private repos (normal private).',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--endpoint',
|
||||
type=str,
|
||||
default=None,
|
||||
help='ModelScope server endpoint, e.g. modelscope.cn or '
|
||||
'modelscope.ai Full URL like '
|
||||
'https://modelscope.cn is also accepted. Scheme (https://) is '
|
||||
'auto-completed if omitted. Falls back to env MODELSCOPE_DOMAIN, '
|
||||
'then defaults to https://www.modelscope.cn.',
|
||||
)
|
||||
|
||||
# Studio specific arguments (only meaningful when --repo_type studio)
|
||||
studio_group = parser.add_argument_group(
|
||||
'Studio Repo Creation',
|
||||
'Optional arguments used only when `--repo_type studio` is set.')
|
||||
studio_group.add_argument(
|
||||
'--sdk-type',
|
||||
dest='sdk_type',
|
||||
choices=StudioSDKType.SUPPORTED,
|
||||
default=None,
|
||||
help='Studio SDK type (only for studio repo-type).')
|
||||
studio_group.add_argument(
|
||||
'--sdk-version',
|
||||
dest='sdk_version',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Studio SDK version (only for gradio).')
|
||||
studio_group.add_argument(
|
||||
'--base-image',
|
||||
dest='base_image',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Studio base image (only for gradio/streamlit).')
|
||||
studio_group.add_argument(
|
||||
'--hardware',
|
||||
dest='hardware',
|
||||
choices=StudioHardware.SUPPORTED,
|
||||
default=None,
|
||||
help='Studio hardware configuration.')
|
||||
|
||||
# AIGC specific arguments
|
||||
aigc_group = parser.add_argument_group(
|
||||
'AIGC Model Creation',
|
||||
'Arguments for creating an AIGC model. Use --aigc to enable.')
|
||||
aigc_group.add_argument(
|
||||
'--aigc', action='store_true', help='Enable AIGC model creation.')
|
||||
aigc_group.add_argument(
|
||||
'--from_json',
|
||||
type=str,
|
||||
help='Path to a JSON file containing AIGC model configuration. '
|
||||
'If used, all other parameters except --repo_id are ignored.')
|
||||
aigc_group.add_argument(
|
||||
'--model_path', type=str, help='Path to the model file or folder.')
|
||||
aigc_group.add_argument(
|
||||
'--aigc_type',
|
||||
type=str,
|
||||
help="AIGC type. Recommended: 'Checkpoint', 'LoRA', 'VAE'.")
|
||||
aigc_group.add_argument(
|
||||
'--base_model_type',
|
||||
type=str,
|
||||
help='Base model type, e.g., SD_XL.')
|
||||
aigc_group.add_argument(
|
||||
'--revision',
|
||||
type=str,
|
||||
default='v1.0',
|
||||
help="Model revision. Defaults to 'v1.0'.")
|
||||
aigc_group.add_argument(
|
||||
'--base_model_id',
|
||||
type=str,
|
||||
default='',
|
||||
help='Base model ID from ModelScope.')
|
||||
aigc_group.add_argument(
|
||||
'--description',
|
||||
type=str,
|
||||
default='This is an AIGC model.',
|
||||
help='Model description.')
|
||||
aigc_group.add_argument(
|
||||
'--path_in_repo',
|
||||
type=str,
|
||||
default='',
|
||||
help='Path in the repository to upload to.')
|
||||
aigc_group.add_argument(
|
||||
'--model_source',
|
||||
type=str,
|
||||
default='USER_UPLOAD',
|
||||
help=
|
||||
'Source of the AIGC model. `USER_UPLOAD`, `TRAINED_FROM_MODELSCOPE` or `TRAINED_FROM_ALIYUN_FC`.'
|
||||
)
|
||||
aigc_group.add_argument(
|
||||
'--base_model_sub_type',
|
||||
type=str,
|
||||
default='',
|
||||
help='Base model sub type, e.g., Qwen_Edit_2509')
|
||||
|
||||
parser.set_defaults(func=subparser_func)
|
||||
|
||||
def execute(self):
|
||||
if self.args.aigc:
|
||||
if self.args.repo_type != REPO_TYPE_MODEL:
|
||||
raise ValueError(
|
||||
'AIGC models can only be created when repo_type is "model".'
|
||||
)
|
||||
self._create_aigc_model()
|
||||
else:
|
||||
self._create_regular_repo()
|
||||
|
||||
def _create_regular_repo(self):
|
||||
# Check token and login
|
||||
# The cookies will be reused if the user has logged in before.
|
||||
endpoint = resolve_endpoint(self.args.endpoint)
|
||||
api = HubApi(endpoint=endpoint)
|
||||
|
||||
extra_kwargs = {}
|
||||
if self.args.repo_type == REPO_TYPE_STUDIO:
|
||||
# Pass studio-specific fields only when creating a studio repo.
|
||||
for field in ('sdk_type', 'sdk_version', 'base_image', 'hardware'):
|
||||
value = getattr(self.args, field, None)
|
||||
if value is not None:
|
||||
extra_kwargs[field] = value
|
||||
|
||||
# Create repo
|
||||
api.create_repo(
|
||||
repo_id=self.args.repo_id,
|
||||
token=self.args.token,
|
||||
visibility=self.args.visibility,
|
||||
repo_type=self.args.repo_type,
|
||||
chinese_name=self.args.chinese_name,
|
||||
license=self.args.license,
|
||||
exist_ok=self.args.exist_ok,
|
||||
create_default_config=True,
|
||||
endpoint=endpoint,
|
||||
gated_mode=self.args.gated_mode,
|
||||
**extra_kwargs,
|
||||
)
|
||||
|
||||
def _create_aigc_model(self):
|
||||
"""Execute the command."""
|
||||
endpoint = resolve_endpoint(self.args.endpoint)
|
||||
api = HubApi(endpoint=endpoint)
|
||||
model_id = self.args.repo_id
|
||||
|
||||
if self.args.from_json:
|
||||
# Create from JSON file
|
||||
logger.info('Creating AIGC model from JSON file: '
|
||||
f'{self.args.from_json}')
|
||||
aigc_model = AigcModel.from_json_file(self.args.from_json)
|
||||
else:
|
||||
# Create from command line arguments
|
||||
logger.info('Creating AIGC model from command line arguments...')
|
||||
if not all([
|
||||
self.args.model_path, self.args.aigc_type,
|
||||
self.args.base_model_type
|
||||
]):
|
||||
raise ValueError(
|
||||
'Error: --model_path, --aigc_type, and '
|
||||
'--base_model_type are required when not using '
|
||||
'--from_json.')
|
||||
|
||||
aigc_model = AigcModel(
|
||||
model_path=self.args.model_path,
|
||||
aigc_type=self.args.aigc_type,
|
||||
base_model_type=self.args.base_model_type,
|
||||
tag=self.args.revision,
|
||||
description=self.args.description,
|
||||
base_model_id=self.args.base_model_id,
|
||||
path_in_repo=self.args.path_in_repo,
|
||||
model_source=self.args.model_source,
|
||||
base_model_sub_type=self.args.base_model_sub_type,
|
||||
)
|
||||
|
||||
# Convert visibility string to int for the API call
|
||||
reverse_visibility_map = {v: k for k, v in VisibilityMap.items()}
|
||||
visibility_idx: int = reverse_visibility_map.get(
|
||||
self.args.visibility, ModelVisibility.PUBLIC)
|
||||
|
||||
try:
|
||||
model_url = api.create_model(
|
||||
model_id=model_id,
|
||||
token=self.args.token,
|
||||
visibility=visibility_idx,
|
||||
license=self.args.license,
|
||||
chinese_name=self.args.chinese_name,
|
||||
aigc_model=aigc_model,
|
||||
gated_mode=self.args.gated_mode)
|
||||
print(f'Successfully created AIGC model: {model_url}')
|
||||
except Exception as e:
|
||||
print(f'Error creating AIGC model: {e}')
|
||||
@@ -1,287 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.cli.utils import concurrent_download
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.constants import DEFAULT_MAX_WORKERS, DEFAULT_SKILLS_DIR
|
||||
from modelscope.hub.file_download import (dataset_file_download,
|
||||
model_file_download)
|
||||
from modelscope.hub.snapshot_download import (dataset_snapshot_download,
|
||||
snapshot_download)
|
||||
from modelscope.hub.utils.utils import convert_patterns, resolve_endpoint
|
||||
from modelscope.utils.constant import (DEFAULT_DATASET_REVISION,
|
||||
REPO_TYPE_DATASET, REPO_TYPE_MODEL,
|
||||
REPO_TYPE_STUDIO, REPO_TYPE_SUPPORT)
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return DownloadCMD(args)
|
||||
|
||||
|
||||
class DownloadCMD(CLICommand):
|
||||
name = 'download'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for download command.
|
||||
"""
|
||||
parser: ArgumentParser = parsers.add_parser(DownloadCMD.name)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument(
|
||||
'--model',
|
||||
type=str,
|
||||
help='The id of the model to be downloaded. For download, '
|
||||
'the id of either a model or dataset must be provided.')
|
||||
group.add_argument(
|
||||
'--dataset',
|
||||
type=str,
|
||||
help='The id of the dataset to be downloaded. For download, '
|
||||
'the id of either a model or dataset must be provided.')
|
||||
group.add_argument(
|
||||
'--collection',
|
||||
type=str,
|
||||
default=None,
|
||||
help='The ID of the collection to download (skills only)')
|
||||
parser.add_argument(
|
||||
'repo_id',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Optional, '
|
||||
'ID of the repo to download, It can also be set by --model or --dataset.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--repo-type',
|
||||
choices=REPO_TYPE_SUPPORT,
|
||||
default=REPO_TYPE_MODEL,
|
||||
help="Type of repo to download from (defaults to 'model').",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--token',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Optional. Access token to download controlled entities.')
|
||||
parser.add_argument(
|
||||
'--revision',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Revision of the entity (e.g., model).')
|
||||
parser.add_argument(
|
||||
'--cache_dir',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Cache directory to save entity (e.g., model).')
|
||||
parser.add_argument(
|
||||
'--local_dir',
|
||||
type=str,
|
||||
default=None,
|
||||
help='File will be downloaded to local location specified by'
|
||||
'local_dir, in this case, cache_dir parameter will be ignored.')
|
||||
parser.add_argument(
|
||||
'files',
|
||||
type=str,
|
||||
default=None,
|
||||
nargs='*',
|
||||
help='Specify relative path to the repository file(s) to download.'
|
||||
"(e.g 'tokenizer.json', 'onnx/decoder_model.onnx').")
|
||||
parser.add_argument(
|
||||
'--include',
|
||||
nargs='*',
|
||||
default=None,
|
||||
type=str,
|
||||
help='Glob patterns to match files to download.'
|
||||
'Ignored if file is specified')
|
||||
parser.add_argument(
|
||||
'--exclude',
|
||||
nargs='*',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Glob patterns to exclude from files to download.'
|
||||
'Ignored if file is specified')
|
||||
parser.add_argument(
|
||||
'--endpoint',
|
||||
type=str,
|
||||
default=None,
|
||||
help='ModelScope server endpoint, e.g. modelscope.cn or '
|
||||
'modelscope.ai Full URL like '
|
||||
'https://modelscope.cn is also accepted. Scheme (https://) is '
|
||||
'auto-completed if omitted. Falls back to env MODELSCOPE_DOMAIN, '
|
||||
'then defaults to https://www.modelscope.cn. '
|
||||
'When omitted, the CLI auto-detects the correct site '
|
||||
'(cn/intl) for download.')
|
||||
parser.add_argument(
|
||||
'--max-workers',
|
||||
type=int,
|
||||
default=DEFAULT_MAX_WORKERS,
|
||||
help='The maximum number of workers to download files.')
|
||||
|
||||
parser.set_defaults(func=subparser_func)
|
||||
|
||||
def execute(self):
|
||||
if self.args.model or self.args.dataset:
|
||||
# the position argument of files will be put to repo_id.
|
||||
if self.args.repo_id is not None:
|
||||
if self.args.files:
|
||||
self.args.files.insert(0, self.args.repo_id)
|
||||
else:
|
||||
self.args.files = [self.args.repo_id]
|
||||
else:
|
||||
if self.args.repo_id is not None:
|
||||
if self.args.repo_type in (REPO_TYPE_MODEL, REPO_TYPE_STUDIO):
|
||||
# studio repos share the same snapshot_download path
|
||||
# as model repos.
|
||||
self.args.model = self.args.repo_id
|
||||
elif self.args.repo_type == REPO_TYPE_DATASET:
|
||||
self.args.dataset = self.args.repo_id
|
||||
else:
|
||||
raise Exception('Not support repo-type: %s'
|
||||
% self.args.repo_type)
|
||||
if not self.args.model and not self.args.dataset and not self.args.collection:
|
||||
raise Exception('Model, dataset, or collection must be set.')
|
||||
if self.args.endpoint:
|
||||
endpoint = resolve_endpoint(self.args.endpoint)
|
||||
else:
|
||||
endpoint = None
|
||||
cookies = None
|
||||
if self.args.token is not None:
|
||||
api = HubApi(endpoint=endpoint)
|
||||
cookies = api.get_cookies(access_token=self.args.token)
|
||||
if self.args.model:
|
||||
if len(self.args.files) == 1: # download single file
|
||||
model_file_download(
|
||||
self.args.model,
|
||||
self.args.files[0],
|
||||
cache_dir=self.args.cache_dir,
|
||||
local_dir=self.args.local_dir,
|
||||
revision=self.args.revision,
|
||||
cookies=cookies,
|
||||
token=self.args.token,
|
||||
endpoint=endpoint)
|
||||
elif len(
|
||||
self.args.files) > 1: # download specified multiple files.
|
||||
snapshot_download(
|
||||
self.args.model,
|
||||
revision=self.args.revision,
|
||||
cache_dir=self.args.cache_dir,
|
||||
local_dir=self.args.local_dir,
|
||||
allow_file_pattern=self.args.files,
|
||||
max_workers=self.args.max_workers,
|
||||
cookies=cookies,
|
||||
token=self.args.token,
|
||||
endpoint=endpoint)
|
||||
else: # download repo
|
||||
snapshot_download(
|
||||
self.args.model,
|
||||
revision=self.args.revision,
|
||||
cache_dir=self.args.cache_dir,
|
||||
local_dir=self.args.local_dir,
|
||||
allow_file_pattern=convert_patterns(self.args.include),
|
||||
ignore_file_pattern=convert_patterns(self.args.exclude),
|
||||
max_workers=self.args.max_workers,
|
||||
cookies=cookies,
|
||||
token=self.args.token,
|
||||
endpoint=endpoint)
|
||||
print(f'\nSuccessfully Downloaded from model {self.args.model}.\n')
|
||||
elif self.args.dataset:
|
||||
dataset_revision: str = self.args.revision if self.args.revision else DEFAULT_DATASET_REVISION
|
||||
if len(self.args.files) == 1: # download single file
|
||||
dataset_file_download(
|
||||
self.args.dataset,
|
||||
self.args.files[0],
|
||||
cache_dir=self.args.cache_dir,
|
||||
local_dir=self.args.local_dir,
|
||||
revision=dataset_revision,
|
||||
cookies=cookies,
|
||||
token=self.args.token,
|
||||
endpoint=endpoint)
|
||||
elif len(
|
||||
self.args.files) > 1: # download specified multiple files.
|
||||
dataset_snapshot_download(
|
||||
self.args.dataset,
|
||||
revision=dataset_revision,
|
||||
cache_dir=self.args.cache_dir,
|
||||
local_dir=self.args.local_dir,
|
||||
allow_file_pattern=self.args.files,
|
||||
max_workers=self.args.max_workers,
|
||||
cookies=cookies,
|
||||
token=self.args.token,
|
||||
endpoint=endpoint)
|
||||
else: # download repo
|
||||
dataset_snapshot_download(
|
||||
self.args.dataset,
|
||||
revision=dataset_revision,
|
||||
cache_dir=self.args.cache_dir,
|
||||
local_dir=self.args.local_dir,
|
||||
allow_file_pattern=convert_patterns(self.args.include),
|
||||
ignore_file_pattern=convert_patterns(self.args.exclude),
|
||||
max_workers=self.args.max_workers,
|
||||
cookies=cookies,
|
||||
token=self.args.token,
|
||||
endpoint=endpoint)
|
||||
print(
|
||||
f'\nSuccessfully Downloaded from dataset {self.args.dataset}.\n'
|
||||
)
|
||||
elif self.args.collection:
|
||||
api = HubApi(endpoint=endpoint, token=self.args.token)
|
||||
local_dir = self.args.local_dir or DEFAULT_SKILLS_DIR
|
||||
data = api.get_collection(
|
||||
self.args.collection, repo_type='skill', endpoint=endpoint)
|
||||
elements = data.get('CollectionElements',
|
||||
{}).get('CollectionElementVoList', [])
|
||||
|
||||
logger.info(
|
||||
f'Collection {self.args.collection} has {len(elements)} elements.'
|
||||
)
|
||||
|
||||
if not elements:
|
||||
print(f'No skill elements found in collection: '
|
||||
f'{self.args.collection}')
|
||||
return
|
||||
|
||||
# Validate elements have required fields
|
||||
valid_elements = []
|
||||
for elem in elements:
|
||||
if not elem.get('ElementPath') or not elem.get('ElementName'):
|
||||
logger.warning('Skipping malformed collection element: %s',
|
||||
elem)
|
||||
continue
|
||||
valid_elements.append(elem)
|
||||
|
||||
if not valid_elements:
|
||||
print(f'No valid skill elements found in collection: '
|
||||
f'{self.args.collection}')
|
||||
return
|
||||
|
||||
print(f'Found {len(valid_elements)} skill(s) in collection, '
|
||||
f'downloading...')
|
||||
|
||||
def _download_one_skill(element):
|
||||
element_path = element['ElementPath']
|
||||
element_name = element['ElementName']
|
||||
skill_id = f'{element_path}/{element_name}'
|
||||
try:
|
||||
skill_dir = api.download_skill(
|
||||
skill_id=skill_id,
|
||||
local_dir=local_dir,
|
||||
endpoint=endpoint)
|
||||
return (skill_id, skill_dir, None)
|
||||
except Exception as e:
|
||||
return (skill_id, None, str(e))
|
||||
|
||||
concurrent_download(
|
||||
_download_one_skill,
|
||||
valid_elements,
|
||||
max_workers=self.args.max_workers,
|
||||
item_name='skill')
|
||||
else:
|
||||
pass # noop
|
||||
@@ -1,28 +1,25 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""``modelscope llamafile`` — download and run a llamafile from a model repo."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from modelscope_hub.cli.base import CLICommand
|
||||
|
||||
from modelscope import model_file_download
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return LlamafileCMD(args)
|
||||
|
||||
|
||||
class LlamafileCMD(CLICommand):
|
||||
name = 'llamafile'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
super().__init__(args)
|
||||
self.model_id = self.args.model
|
||||
if self.model_id is None or self.model_id.count('/') != 1:
|
||||
raise ValueError(f'Invalid model id [{self.model_id}].')
|
||||
@@ -34,10 +31,10 @@ class LlamafileCMD(CLICommand):
|
||||
self.api = HubApi()
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for clear-cache command.
|
||||
"""
|
||||
parser = parsers.add_parser(LlamafileCMD.name)
|
||||
def register(subparsers: ArgumentParser) -> None:
|
||||
parser = subparsers.add_parser(
|
||||
LlamafileCMD.name,
|
||||
help='Download and run a llamafile from a model repo.')
|
||||
parser.add_argument(
|
||||
'--model',
|
||||
type=str,
|
||||
@@ -80,7 +77,7 @@ class LlamafileCMD(CLICommand):
|
||||
'Whether to launch model with the downloaded llamafile, default to True.'
|
||||
)
|
||||
|
||||
parser.set_defaults(func=subparser_func)
|
||||
parser.set_defaults(_command=LlamafileCMD)
|
||||
|
||||
def execute(self):
|
||||
if self.args.file:
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.utils.utils import resolve_endpoint
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return LoginCMD(args)
|
||||
|
||||
|
||||
class LoginCMD(CLICommand):
|
||||
name = 'login'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for login command.
|
||||
"""
|
||||
parser = parsers.add_parser(LoginCMD.name)
|
||||
parser.add_argument(
|
||||
'--token',
|
||||
type=str,
|
||||
required=True,
|
||||
help='The Access Token for modelscope.')
|
||||
parser.add_argument(
|
||||
'--endpoint',
|
||||
type=str,
|
||||
default=None,
|
||||
help='ModelScope server endpoint, e.g. modelscope.cn or '
|
||||
'modelscope.ai Full URL like '
|
||||
'https://modelscope.cn is also accepted. Scheme (https://) is '
|
||||
'auto-completed if omitted. Falls back to env MODELSCOPE_DOMAIN, '
|
||||
'then defaults to https://www.modelscope.cn.')
|
||||
parser.set_defaults(func=subparser_func)
|
||||
|
||||
def execute(self):
|
||||
api = HubApi(endpoint=resolve_endpoint(self.args.endpoint))
|
||||
api.login(self.args.token)
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""``modelscope modelcard`` — create / upload / download a model card."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
@@ -6,7 +8,8 @@ import tempfile
|
||||
from argparse import ArgumentParser
|
||||
from string import Template
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope_hub.cli.base import CLICommand
|
||||
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.snapshot_download import snapshot_download
|
||||
from modelscope.hub.utils.utils import get_endpoint
|
||||
@@ -18,17 +21,11 @@ current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
template_path = os.path.join(current_path, 'template')
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return ModelCardCMD(args)
|
||||
|
||||
|
||||
class ModelCardCMD(CLICommand):
|
||||
name = 'modelcard'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
super().__init__(args)
|
||||
self.api = HubApi()
|
||||
if args.access_token:
|
||||
self.api.login(args.access_token)
|
||||
@@ -38,10 +35,11 @@ class ModelCardCMD(CLICommand):
|
||||
self.url = os.path.join(get_endpoint(), self.model_id)
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for create or upload modelcard command.
|
||||
"""
|
||||
parser = parsers.add_parser(ModelCardCMD.name, aliases=['model'])
|
||||
def register(subparsers: ArgumentParser) -> None:
|
||||
parser = subparsers.add_parser(
|
||||
ModelCardCMD.name,
|
||||
aliases=['model'],
|
||||
help='Create / upload / download a model card.')
|
||||
parser.add_argument(
|
||||
'-tk',
|
||||
'--access_token',
|
||||
@@ -105,7 +103,7 @@ class ModelCardCMD(CLICommand):
|
||||
type=str,
|
||||
default=None,
|
||||
help='the info of uploaded model')
|
||||
parser.set_defaults(func=subparser_func)
|
||||
parser.set_defaults(_command=ModelCardCMD)
|
||||
|
||||
def create_model(self):
|
||||
from modelscope.hub.constants import Licenses, ModelVisibility
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""``modelscope pipeline`` — scaffold a custom pipeline from a template."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from string import Template
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope_hub.cli.base import CLICommand
|
||||
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
@@ -13,23 +16,14 @@ current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
template_path = os.path.join(current_path, 'template')
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return PipelineCMD(args)
|
||||
|
||||
|
||||
class PipelineCMD(CLICommand):
|
||||
name = 'pipeline'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for create pipeline template command.
|
||||
"""
|
||||
parser = parsers.add_parser(PipelineCMD.name)
|
||||
def register(subparsers: ArgumentParser) -> None:
|
||||
parser = subparsers.add_parser(
|
||||
PipelineCMD.name,
|
||||
help='Scaffold a custom pipeline from a template.')
|
||||
parser.add_argument(
|
||||
'-act',
|
||||
'--action',
|
||||
@@ -85,7 +79,7 @@ class PipelineCMD(CLICommand):
|
||||
type=str,
|
||||
default='./',
|
||||
help='the path of configuration.json for ModelScope')
|
||||
parser.set_defaults(func=subparser_func)
|
||||
parser.set_defaults(_command=PipelineCMD)
|
||||
|
||||
def create_template(self):
|
||||
if self.args.tpl_file_path not in os.listdir(template_path):
|
||||
|
||||
@@ -1,54 +1,25 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""``modelscope plugin`` — install/uninstall/list ModelScope plugins."""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope_hub.cli.base import CLICommand
|
||||
|
||||
from modelscope.utils.plugins import PluginsManager
|
||||
|
||||
plugins_manager = PluginsManager()
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return PluginsCMD(args)
|
||||
|
||||
|
||||
class PluginsCMD(CLICommand):
|
||||
name = 'plugin'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for install command.
|
||||
"""
|
||||
parser = parsers.add_parser(PluginsCMD.name)
|
||||
subparsers = parser.add_subparsers(dest='command')
|
||||
def register(subparsers: ArgumentParser) -> None:
|
||||
parser = subparsers.add_parser(
|
||||
PluginsCMD.name, help='Manage ModelScope plugins.')
|
||||
sub = parser.add_subparsers(dest='command')
|
||||
|
||||
PluginsInstallCMD.define_args(subparsers)
|
||||
PluginsUninstallCMD.define_args(subparsers)
|
||||
PluginsListCMD.define_args(subparsers)
|
||||
|
||||
parser.set_defaults(func=subparser_func)
|
||||
|
||||
def execute(self):
|
||||
print(self.args)
|
||||
if self.args.command == PluginsInstallCMD.name:
|
||||
PluginsInstallCMD.execute(self.args)
|
||||
if self.args.command == PluginsUninstallCMD.name:
|
||||
PluginsUninstallCMD.execute(self.args)
|
||||
if self.args.command == PluginsListCMD.name:
|
||||
PluginsListCMD.execute(self.args)
|
||||
|
||||
|
||||
class PluginsInstallCMD(PluginsCMD):
|
||||
name = 'install'
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
install = parsers.add_parser(PluginsInstallCMD.name)
|
||||
install = sub.add_parser('install', help='Install plugin packages.')
|
||||
install.add_argument(
|
||||
'package',
|
||||
type=str,
|
||||
@@ -68,51 +39,41 @@ class PluginsInstallCMD(PluginsCMD):
|
||||
default=False,
|
||||
help='If force update the package')
|
||||
|
||||
@staticmethod
|
||||
def execute(args):
|
||||
plugins_manager.install_plugins(
|
||||
list(args.package),
|
||||
index_url=args.index_url,
|
||||
force_update=args.force_update)
|
||||
|
||||
|
||||
class PluginsUninstallCMD(PluginsCMD):
|
||||
name = 'uninstall'
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
install = parsers.add_parser(PluginsUninstallCMD.name)
|
||||
install.add_argument(
|
||||
uninstall = sub.add_parser(
|
||||
'uninstall', help='Uninstall plugin packages.')
|
||||
uninstall.add_argument(
|
||||
'package',
|
||||
type=str,
|
||||
nargs='+',
|
||||
default=None,
|
||||
help='Name of the package to be installed.')
|
||||
install.add_argument(
|
||||
help='Name of the package to be uninstalled.')
|
||||
uninstall.add_argument(
|
||||
'--yes',
|
||||
'-y',
|
||||
type=str,
|
||||
default=False,
|
||||
help='Base URL of the Python Package Index.')
|
||||
action='store_true',
|
||||
help='Skip confirmation prompt.')
|
||||
|
||||
@staticmethod
|
||||
def execute(args):
|
||||
plugins_manager.uninstall_plugins(list(args.package), is_yes=args.yes)
|
||||
|
||||
|
||||
class PluginsListCMD(PluginsCMD):
|
||||
name = 'list'
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
install = parsers.add_parser(PluginsListCMD.name)
|
||||
install.add_argument(
|
||||
list_p = sub.add_parser('list', help='List available plugins.')
|
||||
list_p.add_argument(
|
||||
'--all',
|
||||
'-a',
|
||||
type=str,
|
||||
default=None,
|
||||
action='store_true',
|
||||
help='Show all of the plugins including those not installed.')
|
||||
|
||||
@staticmethod
|
||||
def execute(args):
|
||||
plugins_manager.list_plugins(show_all=all)
|
||||
parser.set_defaults(_command=PluginsCMD)
|
||||
|
||||
def execute(self):
|
||||
command = getattr(self.args, 'command', None)
|
||||
if command == 'install':
|
||||
plugins_manager.install_plugins(
|
||||
list(self.args.package),
|
||||
index_url=self.args.index_url,
|
||||
force_update=self.args.force_update)
|
||||
elif command == 'uninstall':
|
||||
plugins_manager.uninstall_plugins(
|
||||
list(self.args.package), is_yes=self.args.yes)
|
||||
elif command == 'list':
|
||||
plugins_manager.list_plugins(show_all=self.args.all)
|
||||
else:
|
||||
raise ValueError(
|
||||
'Usage: modelscope plugin {install|uninstall|list} ...')
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from argparse import ArgumentParser
|
||||
from typing import Optional
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.hub.cache_manager import scan_cache_dir
|
||||
from modelscope.hub.errors import CacheNotFound
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
|
||||
current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return ScanCacheCMD(args)
|
||||
|
||||
|
||||
class ScanCacheCMD(CLICommand):
|
||||
name = 'scan-cache'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
self.cache_dir: Optional[str] = args.dir
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for create pipeline template command.
|
||||
"""
|
||||
parser = parsers.add_parser(ScanCacheCMD.name)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument(
|
||||
'--dir',
|
||||
type=str,
|
||||
default=None,
|
||||
help=
|
||||
'cache directory to scan (optional). Default to the default ModelScope cache.',
|
||||
)
|
||||
|
||||
parser.set_defaults(func=subparser_func)
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
t0 = time.time()
|
||||
cache_info = scan_cache_dir(self.cache_dir)
|
||||
t1 = time.time()
|
||||
except CacheNotFound as exc:
|
||||
cache_dir = exc.cache_dir
|
||||
print(f'Cache directory not found: {cache_dir}')
|
||||
return
|
||||
print(cache_info.export_as_table())
|
||||
print(
|
||||
f'\nDone in {round(t1 - t0, 1)}s. Scanned {len(cache_info.repos)} repo(s)'
|
||||
f' for a total of {cache_info.size_on_disk_str}.')
|
||||
if len(cache_info.warnings) > 0:
|
||||
message = f'Got {len(cache_info.warnings)} warning(s) while scanning.'
|
||||
print(message)
|
||||
for warning in cache_info.warnings:
|
||||
print(warning)
|
||||
@@ -1,38 +1,26 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
import logging
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
from string import Template
|
||||
"""``modelscope server`` — launch the local inference HTTP server."""
|
||||
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from modelscope_hub.cli.base import CLICommand
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.server.api_server import add_server_args, run_server
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
|
||||
current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
template_path = os.path.join(current_path, 'template')
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return ServerCMD(args)
|
||||
|
||||
|
||||
class ServerCMD(CLICommand):
|
||||
name = 'server'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
""" define args for create pipeline template command.
|
||||
"""
|
||||
parser = parsers.add_parser(ServerCMD.name)
|
||||
def register(subparsers: ArgumentParser) -> None:
|
||||
parser = subparsers.add_parser(
|
||||
ServerCMD.name, help='Launch the local inference HTTP server.')
|
||||
add_server_args(parser)
|
||||
parser.set_defaults(func=subparser_func)
|
||||
parser.set_defaults(_command=ServerCMD)
|
||||
|
||||
def execute(self):
|
||||
run_server(self.args)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""``modelscope skills`` — download and install agent skills."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from modelscope_hub.cli.base import CLICommand
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.cli.utils import concurrent_download
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.constants import DEFAULT_SKILLS_DIR
|
||||
from modelscope.utils.logger import get_logger
|
||||
@@ -12,9 +15,33 @@ from modelscope.utils.logger import get_logger
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
"""Function which will be called for a specific sub parser."""
|
||||
return SkillsCMD(args)
|
||||
def _concurrent_download(download_fn, items, max_workers=8, item_name='item'):
|
||||
"""Run ``download_fn`` over ``items`` in parallel, reporting progress.
|
||||
|
||||
``download_fn`` must return ``(identifier, result_path, error_or_None)``.
|
||||
On any failure the process exits with status 1 after the summary is
|
||||
printed.
|
||||
"""
|
||||
succeeded, failed = [], []
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = {executor.submit(download_fn, item): item for item in items}
|
||||
for future in as_completed(futures):
|
||||
identifier, result_path, error = future.result()
|
||||
if error:
|
||||
failed.append((identifier, error))
|
||||
print(f'Failed to download {item_name} {identifier}: {error}')
|
||||
else:
|
||||
succeeded.append((identifier, result_path))
|
||||
print(f'Downloaded {item_name} {identifier} -> {result_path}')
|
||||
|
||||
print(f'\nDownload complete: {len(succeeded)} succeeded, '
|
||||
f'{len(failed)} failed')
|
||||
if failed:
|
||||
print(f'Failed {item_name}s:')
|
||||
for identifier, error in failed:
|
||||
print(f' {identifier}: {error}')
|
||||
sys.exit(1)
|
||||
return succeeded, failed
|
||||
|
||||
|
||||
class SkillsCMD(CLICommand):
|
||||
@@ -22,19 +49,14 @@ class SkillsCMD(CLICommand):
|
||||
|
||||
name = 'skills'
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: ArgumentParser):
|
||||
"""Define args for skills command."""
|
||||
parser = parsers.add_parser(SkillsCMD.name)
|
||||
subparsers = parser.add_subparsers(
|
||||
def register(subparsers: ArgumentParser) -> None:
|
||||
parser = subparsers.add_parser(
|
||||
SkillsCMD.name, help='Download and manage agent skills.')
|
||||
sub = parser.add_subparsers(
|
||||
dest='skills_action', help='skills subcommands')
|
||||
|
||||
# 'add' subcommand
|
||||
add_parser = subparsers.add_parser(
|
||||
'add', help='Download and install skills')
|
||||
add_parser = sub.add_parser('add', help='Download and install skills')
|
||||
add_parser.add_argument(
|
||||
'skill_ids',
|
||||
type=str,
|
||||
@@ -55,15 +77,16 @@ class SkillsCMD(CLICommand):
|
||||
type=int,
|
||||
default=8,
|
||||
help='Maximum concurrent downloads (default: 8)')
|
||||
add_parser.set_defaults(func=subparser_func)
|
||||
|
||||
parser.set_defaults(_command=SkillsCMD)
|
||||
|
||||
def execute(self):
|
||||
if not hasattr(self.args,
|
||||
'skills_action') or not self.args.skills_action:
|
||||
if not getattr(self.args, 'skills_action', None):
|
||||
print('Usage: modelscope skills add <skill_id1> <skill_id2> ...')
|
||||
return
|
||||
|
||||
if not hasattr(self.args, 'skill_ids') or not self.args.skill_ids:
|
||||
skill_ids = getattr(self.args, 'skill_ids', None)
|
||||
if not skill_ids:
|
||||
print('No skill IDs provided. Usage: modelscope skills add '
|
||||
'<skill_id1> <skill_id2> ...')
|
||||
return
|
||||
@@ -71,11 +94,9 @@ class SkillsCMD(CLICommand):
|
||||
api = HubApi(token=self.args.token)
|
||||
local_dir = self.args.local_dir or DEFAULT_SKILLS_DIR
|
||||
|
||||
skill_ids = self.args.skill_ids
|
||||
print(f'Downloading {len(skill_ids)} skill(s)...')
|
||||
|
||||
if len(skill_ids) == 1:
|
||||
# Single skill download
|
||||
try:
|
||||
skill_dir = api.download_skill(
|
||||
skill_id=skill_ids[0], local_dir=local_dir)
|
||||
@@ -84,7 +105,7 @@ class SkillsCMD(CLICommand):
|
||||
print(f'Failed to download skill {skill_ids[0]}: {e}')
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Multiple skills - concurrent download
|
||||
|
||||
def _download_one(skill_id):
|
||||
try:
|
||||
skill_dir = api.download_skill(
|
||||
@@ -93,7 +114,7 @@ class SkillsCMD(CLICommand):
|
||||
except Exception as e:
|
||||
return (skill_id, None, str(e))
|
||||
|
||||
concurrent_download(
|
||||
_concurrent_download(
|
||||
_download_one,
|
||||
skill_ids,
|
||||
max_workers=self.args.max_workers,
|
||||
|
||||
@@ -54,6 +54,11 @@ class StudioCMD(CLICommand):
|
||||
# ------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def register(subparsers) -> None:
|
||||
"""Register studio subcommand (CLICommand ABC contract)."""
|
||||
StudioCMD.define_args(subparsers)
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: _SubParsersAction):
|
||||
parser: ArgumentParser = parsers.add_parser(
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
import os
|
||||
from argparse import ArgumentParser, _SubParsersAction
|
||||
|
||||
from modelscope.cli.base import CLICommand
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.utils.utils import convert_patterns, resolve_endpoint
|
||||
from modelscope.utils.constant import REPO_TYPE_MODEL, REPO_TYPE_SUPPORT
|
||||
|
||||
|
||||
def subparser_func(args):
|
||||
""" Function which will be called for a specific sub parser.
|
||||
"""
|
||||
return UploadCMD(args)
|
||||
|
||||
|
||||
class UploadCMD(CLICommand):
|
||||
|
||||
name = 'upload'
|
||||
|
||||
def __init__(self, args: _SubParsersAction):
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def define_args(parsers: _SubParsersAction):
|
||||
|
||||
parser: ArgumentParser = parsers.add_parser(UploadCMD.name)
|
||||
|
||||
parser.add_argument(
|
||||
'repo_id',
|
||||
type=str,
|
||||
help='The ID of the repo to upload to (e.g. `username/repo-name`)')
|
||||
parser.add_argument(
|
||||
'local_path',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Optional, '
|
||||
'Local path to the file or folder to upload. Defaults to current directory.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'path_in_repo',
|
||||
type=str,
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='Optional, '
|
||||
'Path of the file or folder in the repo. Defaults to the relative path of the file or folder.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--repo-type',
|
||||
choices=REPO_TYPE_SUPPORT,
|
||||
default=REPO_TYPE_MODEL,
|
||||
help=
|
||||
'Type of the repo to upload to (e.g. `dataset`, `model`). Defaults to be `model`.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--include',
|
||||
nargs='*',
|
||||
type=str,
|
||||
help='Glob patterns to match files to upload.')
|
||||
parser.add_argument(
|
||||
'--exclude',
|
||||
nargs='*',
|
||||
type=str,
|
||||
help='Glob patterns to exclude from files to upload.')
|
||||
parser.add_argument(
|
||||
'--commit-message',
|
||||
type=str,
|
||||
default=None,
|
||||
help='The message of commit. Default to be `None`.')
|
||||
parser.add_argument(
|
||||
'--commit-description',
|
||||
type=str,
|
||||
default=None,
|
||||
help=
|
||||
'The description of the generated commit. Default to be `None`.')
|
||||
parser.add_argument(
|
||||
'--token',
|
||||
type=str,
|
||||
default=None,
|
||||
help=
|
||||
'A User Access Token generated from https://modelscope.cn/my/myaccesstoken'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--max-workers',
|
||||
type=int,
|
||||
default=min(8,
|
||||
os.cpu_count() + 4),
|
||||
help='The number of workers to use for uploading files.')
|
||||
parser.add_argument(
|
||||
'--endpoint',
|
||||
type=str,
|
||||
default=None,
|
||||
help='ModelScope server endpoint, e.g. modelscope.cn or '
|
||||
'modelscope.ai Full URL like '
|
||||
'https://modelscope.cn is also accepted. Scheme (https://) is '
|
||||
'auto-completed if omitted. Falls back to env MODELSCOPE_DOMAIN, '
|
||||
'then defaults to https://www.modelscope.cn.')
|
||||
|
||||
parser.set_defaults(func=subparser_func)
|
||||
|
||||
def execute(self):
|
||||
|
||||
assert self.args.repo_id, '`repo_id` is required'
|
||||
assert self.args.repo_id.count(
|
||||
'/') == 1, 'repo_id should be in format of username/repo-name'
|
||||
repo_name: str = self.args.repo_id.split('/')[-1]
|
||||
self.repo_id = self.args.repo_id
|
||||
|
||||
# Check path_in_repo
|
||||
if self.args.local_path is None and os.path.isfile(repo_name):
|
||||
# Case 1: modelscope upload owner_name/test_repo
|
||||
self.local_path = repo_name
|
||||
self.path_in_repo = repo_name
|
||||
elif self.args.local_path is None and os.path.isdir(repo_name):
|
||||
# Case 2: modelscope upload owner_name/test_repo (run command line in the `repo_name` dir)
|
||||
# => upload all files in current directory to remote root path
|
||||
self.local_path = repo_name
|
||||
self.path_in_repo = '.'
|
||||
elif self.args.local_path is None:
|
||||
# Case 3: user provided only a repo_id that does not match a local file or folder
|
||||
# => the user must explicitly provide a local_path => raise exception
|
||||
raise ValueError(
|
||||
f"'{repo_name}' is not a local file or folder. Please set `local_path` explicitly."
|
||||
)
|
||||
elif self.args.path_in_repo is None and os.path.isfile(
|
||||
self.args.local_path):
|
||||
# Case 4: modelscope upload owner_name/test_repo /path/to/your_file.csv
|
||||
# => upload it to remote root path with same name
|
||||
self.local_path = self.args.local_path
|
||||
self.path_in_repo = os.path.basename(self.args.local_path)
|
||||
elif self.args.path_in_repo is None:
|
||||
# Case 5: modelscope upload owner_name/test_repo /path/to/your_folder
|
||||
# => upload all files in current directory to remote root path
|
||||
self.local_path = self.args.local_path
|
||||
self.path_in_repo = ''
|
||||
else:
|
||||
# Finally, if both paths are explicit
|
||||
self.local_path = self.args.local_path
|
||||
self.path_in_repo = self.args.path_in_repo
|
||||
|
||||
api = HubApi(endpoint=resolve_endpoint(self.args.endpoint))
|
||||
|
||||
if os.path.isfile(self.local_path):
|
||||
api.upload_file(
|
||||
path_or_fileobj=self.local_path,
|
||||
path_in_repo=self.path_in_repo,
|
||||
repo_id=self.repo_id,
|
||||
repo_type=self.args.repo_type,
|
||||
commit_message=self.args.commit_message,
|
||||
commit_description=self.args.commit_description,
|
||||
token=self.args.token,
|
||||
)
|
||||
elif os.path.isdir(self.local_path):
|
||||
api.upload_folder(
|
||||
repo_id=self.repo_id,
|
||||
folder_path=self.local_path,
|
||||
path_in_repo=self.path_in_repo,
|
||||
commit_message=self.args.commit_message,
|
||||
commit_description=self.args.commit_description,
|
||||
repo_type=self.args.repo_type,
|
||||
allow_patterns=convert_patterns(self.args.include),
|
||||
ignore_patterns=convert_patterns(self.args.exclude),
|
||||
max_workers=self.args.max_workers,
|
||||
token=self.args.token,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'{self.local_path} is not a valid local path')
|
||||
|
||||
print(f'\nFinished uploading to {self.repo_id}')
|
||||
@@ -1,41 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
|
||||
def concurrent_download(download_fn, items, max_workers=8, item_name='item'):
|
||||
"""Download multiple items concurrently with progress reporting.
|
||||
|
||||
Args:
|
||||
download_fn: Callable that takes an item and returns
|
||||
(identifier, result_path, error_string_or_None).
|
||||
items: List of items to download.
|
||||
max_workers (int): Maximum concurrent workers.
|
||||
item_name (str): Display name for the item type.
|
||||
|
||||
Returns:
|
||||
tuple: (succeeded_list, failed_list).
|
||||
"""
|
||||
succeeded = []
|
||||
failed = []
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = {executor.submit(download_fn, item): item for item in items}
|
||||
for future in as_completed(futures):
|
||||
identifier, result_path, error = future.result()
|
||||
if error:
|
||||
failed.append((identifier, error))
|
||||
print(f'Failed to download {item_name} {identifier}: {error}')
|
||||
else:
|
||||
succeeded.append((identifier, result_path))
|
||||
print(f'Downloaded {item_name} {identifier} -> {result_path}')
|
||||
|
||||
print(f'\nDownload complete: {len(succeeded)} succeeded, '
|
||||
f'{len(failed)} failed')
|
||||
if failed:
|
||||
print(f'Failed {item_name}s:')
|
||||
for identifier, error in failed:
|
||||
print(f' {identifier}: {error}')
|
||||
sys.exit(1)
|
||||
|
||||
return succeeded, failed
|
||||
@@ -1,2 +1,49 @@
|
||||
from .callback import ProgressCallback
|
||||
"""modelscope.hub — shim layer delegating to modelscope_hub."""
|
||||
|
||||
import os as _os
|
||||
from pathlib import Path as _Path
|
||||
|
||||
from modelscope_hub import HubConfig as _HubConfig
|
||||
from modelscope_hub import get_default_config as _get_default_config
|
||||
from modelscope_hub import set_default_config as _set_default_config
|
||||
|
||||
from .callback import ProgressCallback, TqdmCallback
|
||||
from .commit_scheduler import CommitScheduler
|
||||
from .snapshot_download import snapshot_download
|
||||
|
||||
|
||||
def _sync_config() -> None:
|
||||
"""Bridge legacy env vars that modelscope_hub does not natively recognize."""
|
||||
# MODELSCOPE_CACHE is already handled by HubConfig; only sync
|
||||
# if a non-standard alias is set.
|
||||
legacy_cache = _os.environ.get('MS_CACHE_HOME')
|
||||
if legacy_cache and not _os.environ.get('MODELSCOPE_CACHE'):
|
||||
_set_default_config(_HubConfig(cache_dir=legacy_cache))
|
||||
|
||||
# Bridge MODELSCOPE_CREDENTIALS_PATH → HubConfig.config_dir so credential
|
||||
# lookup honours the legacy override.
|
||||
creds_path = _os.environ.get('MODELSCOPE_CREDENTIALS_PATH')
|
||||
if creds_path:
|
||||
resolved = _Path(creds_path).expanduser().resolve()
|
||||
# Legacy convention may point at either the credentials directory
|
||||
# (e.g. ``~/.modelscope``) or at a credentials file inside it; the
|
||||
# new HubConfig always expects the directory. Treat the path as a
|
||||
# file when it exists as one, falling back to the legacy
|
||||
# ``credentials`` filename heuristic for paths that do not yet
|
||||
# exist on disk.
|
||||
is_file = resolved.is_file() or (not resolved.exists()
|
||||
and resolved.name == 'credentials')
|
||||
config_dir = resolved.parent if is_file else resolved
|
||||
cfg = _get_default_config()
|
||||
if cfg.config_dir != config_dir:
|
||||
cfg.config_dir = config_dir
|
||||
|
||||
|
||||
_sync_config()
|
||||
|
||||
__all__ = [
|
||||
'CommitScheduler',
|
||||
'ProgressCallback',
|
||||
'TqdmCallback',
|
||||
'snapshot_download',
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,17 @@
|
||||
"""Contains utilities to manage the ModelScope cache directory."""
|
||||
"""Contains utilities to manage the ModelScope cache directory.
|
||||
|
||||
:func:`scan_cache_dir` retains the legacy file-grain scan that is unique
|
||||
to this SDK; the modelscope_hub repo-grain :func:`scan_cache` and
|
||||
:func:`clear_cache` are re-exported here for forward compatibility.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, FrozenSet, List, Literal, Optional, Set, Union
|
||||
|
||||
from modelscope_hub._cache_manager import clear_cache, scan_cache # noqa: F401
|
||||
|
||||
from modelscope.hub.errors import CacheNotFound, CorruptedCacheException
|
||||
from modelscope.hub.utils.caching import ModelFileSystemCache
|
||||
from modelscope.hub.utils.utils import (convert_readable_size,
|
||||
@@ -15,6 +22,16 @@ from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
__all__ = [
|
||||
'CachedFileInfo',
|
||||
'CachedRevisionInfo',
|
||||
'CachedRepoInfo',
|
||||
'ModelScopeCacheInfo',
|
||||
'scan_cache_dir',
|
||||
'scan_cache',
|
||||
'clear_cache',
|
||||
]
|
||||
|
||||
# List of OS-created helper files that need to be ignored
|
||||
FILES_TO_IGNORE = ['.DS_Store', '._____temp']
|
||||
|
||||
|
||||
@@ -1,34 +1,8 @@
|
||||
from tqdm.auto import tqdm
|
||||
"""Progress callbacks — delegates to modelscope_hub.
|
||||
|
||||
Re-exports ProgressCallback and TqdmCallback from modelscope_hub,
|
||||
maintaining backward compatibility for all existing import paths.
|
||||
"""
|
||||
from modelscope_hub import ProgressCallback, TqdmCallback
|
||||
|
||||
class ProgressCallback:
|
||||
|
||||
def __init__(self, filename: str, file_size: int):
|
||||
self.filename = filename
|
||||
self.file_size = file_size
|
||||
|
||||
def update(self, size: int):
|
||||
pass
|
||||
|
||||
def end(self):
|
||||
pass
|
||||
|
||||
|
||||
class TqdmCallback(ProgressCallback):
|
||||
|
||||
def __init__(self, filename: str, file_size: int):
|
||||
super().__init__(filename, file_size)
|
||||
self.progress = tqdm(
|
||||
unit='B',
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
total=file_size if file_size > 0 else 1,
|
||||
initial=0,
|
||||
desc='Downloading [' + self.filename + ']',
|
||||
leave=True)
|
||||
|
||||
def update(self, size: int):
|
||||
self.progress.update(size)
|
||||
|
||||
def end(self):
|
||||
self.progress.close()
|
||||
__all__ = ['ProgressCallback', 'TqdmCallback']
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Hub constants — shared constants delegate to modelscope_hub, local-only constants retained.
|
||||
|
||||
Constants that have equivalents in modelscope_hub are imported from there.
|
||||
Constants unique to this project (endpoint configs, enum classes, env-driven tunables)
|
||||
are retained locally.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# --- Delegated constants (from modelscope_hub) ---
|
||||
from modelscope_hub.compat.constants import ( # noqa: F401
|
||||
DEFAULT_DATASET_REVISION, DEFAULT_MAX_WORKERS, FILE_HASH,
|
||||
MODELSCOPE_DOMAIN, MODELSCOPE_PREFER_AI_SITE, REPO_TYPE_DATASET,
|
||||
REPO_TYPE_MODEL, REPO_TYPE_STUDIO, REPO_TYPE_SUPPORT,
|
||||
TEMPORARY_FOLDER_NAME, ModelVisibility_INTERNAL, ModelVisibility_PRIVATE,
|
||||
ModelVisibility_PUBLIC)
|
||||
|
||||
# --- Local constants (not in modelscope_hub) ---
|
||||
MODELSCOPE_URL_SCHEME = 'https://'
|
||||
DEFAULT_MODELSCOPE_DOMAIN = 'www.modelscope.cn'
|
||||
DEFAULT_MODELSCOPE_INTL_DOMAIN = 'www.modelscope.ai'
|
||||
@@ -13,7 +28,6 @@ MODELSCOPE_DOWNLOAD_PARALLELS = int(
|
||||
os.environ.get('MODELSCOPE_DOWNLOAD_PARALLELS', 1))
|
||||
DEFAULT_MODELSCOPE_GROUP = 'damo'
|
||||
MODEL_ID_SEPARATOR = '/'
|
||||
FILE_HASH = 'Sha256'
|
||||
LOGGER_NAME = 'ModelScopeHub'
|
||||
DEFAULT_CREDENTIALS_PATH = Path.home().joinpath('.modelscope', 'credentials')
|
||||
MODELSCOPE_CREDENTIALS_PATH = os.environ.get(
|
||||
@@ -72,15 +86,9 @@ API_RESPONSE_FIELD_MESSAGE = 'Message'
|
||||
MODELSCOPE_CLOUD_ENVIRONMENT = 'MODELSCOPE_ENVIRONMENT'
|
||||
MODELSCOPE_CLOUD_USERNAME = 'MODELSCOPE_USERNAME'
|
||||
MODELSCOPE_SDK_DEBUG = 'MODELSCOPE_SDK_DEBUG'
|
||||
MODELSCOPE_PREFER_AI_SITE = 'MODELSCOPE_PREFER_AI_SITE'
|
||||
MODELSCOPE_DOMAIN = 'MODELSCOPE_DOMAIN'
|
||||
MODELSCOPE_ENABLE_DEFAULT_HASH_VALIDATION = 'MODELSCOPE_ENABLE_DEFAULT_HASH_VALIDATION'
|
||||
ONE_YEAR_SECONDS = 24 * 365 * 60 * 60
|
||||
MODELSCOPE_REQUEST_ID = 'X-Request-ID'
|
||||
TEMPORARY_FOLDER_NAME = '._____temp'
|
||||
DEFAULT_MAX_WORKERS = int(
|
||||
os.getenv('DEFAULT_MAX_WORKERS', min(8,
|
||||
os.cpu_count() + 4)))
|
||||
DEFAULT_SKILLS_DIR = os.path.join(os.path.expanduser('~'), '.agents', 'skills')
|
||||
|
||||
# Upload check env
|
||||
|
||||
@@ -1,67 +1,70 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Error classes — core exceptions delegate to modelscope_hub, legacy aliases retained.
|
||||
|
||||
Exception classes from modelscope_hub provide the structured hierarchy.
|
||||
Legacy aliases maintain isinstance compatibility for existing code.
|
||||
Error handling functions with unique logic are retained.
|
||||
"""
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
from modelscope_hub.errors import AuthenticationError # noqa: F401
|
||||
from modelscope_hub.errors import (APIError, CacheNotFound,
|
||||
CorruptedCacheException, FileIntegrityError,
|
||||
HubError, InvalidParameter, NetworkError,
|
||||
NotExistError, NotSupportedError,
|
||||
PermissionDeniedError, RequestTimeoutError,
|
||||
ServerError)
|
||||
from requests.exceptions import HTTPError # noqa: F401 (re-exported)
|
||||
|
||||
from modelscope.hub.constants import MODELSCOPE_REQUEST_ID
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(log_level=logging.WARNING)
|
||||
|
||||
# --- Legacy exception aliases (maintain isinstance backward compatibility) ---
|
||||
|
||||
class NotSupportError(Exception):
|
||||
|
||||
class RequestError(APIError):
|
||||
"""Legacy alias — use APIError for new code."""
|
||||
|
||||
def __init__(self, message: str = '', *args, **kwargs):
|
||||
# Preserve legacy single-positional-arg constructor signature.
|
||||
super().__init__(message, **kwargs)
|
||||
|
||||
|
||||
class NotLoginException(AuthenticationError):
|
||||
"""Legacy alias — use AuthenticationError for new code."""
|
||||
|
||||
def __init__(self, message: str = '', *args, **kwargs):
|
||||
super().__init__(message, **kwargs)
|
||||
|
||||
|
||||
class FileDownloadError(NetworkError):
|
||||
"""Legacy alias — use NetworkError for new code."""
|
||||
pass
|
||||
|
||||
|
||||
class NoValidRevisionError(Exception):
|
||||
class NotSupportError(NotSupportedError):
|
||||
"""Legacy alias — use NotSupportedError for new code."""
|
||||
pass
|
||||
|
||||
|
||||
class NotExistError(Exception):
|
||||
class NoValidRevisionError(NotExistError):
|
||||
"""Legacy alias — raised when no valid revision is found."""
|
||||
|
||||
def __init__(self, message: str = '', *args, **kwargs):
|
||||
super().__init__(message, **kwargs)
|
||||
|
||||
|
||||
class GitError(HubError):
|
||||
"""Git operation failure."""
|
||||
pass
|
||||
|
||||
|
||||
class RequestError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GitError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidParameter(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotLoginException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FileIntegrityError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FileDownloadError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CacheNotFound(Exception):
|
||||
"""Exception thrown when the ModelScope cache is not found."""
|
||||
|
||||
cache_dir: Union[str, Path]
|
||||
|
||||
def __init__(self, msg: str, cache_dir: Union[str, Path], *args, **kwargs):
|
||||
super().__init__(msg, *args, **kwargs)
|
||||
self.cache_dir = cache_dir
|
||||
|
||||
|
||||
class CorruptedCacheException(Exception):
|
||||
"""Exception for any unexpected structure in the ModelScope cache-system."""
|
||||
# --- Error handling functions (retained - contain unique logic) ---
|
||||
|
||||
|
||||
def get_request_id(response: requests.Response):
|
||||
|
||||
@@ -1,400 +1,97 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""File download — delegates to modelscope_hub for Hub downloads, retains http_get_file for direct HTTP.
|
||||
|
||||
Hub file downloads (model_file_download, dataset_file_download) are delegated
|
||||
to modelscope_hub.compat. Direct HTTP file downloads (http_get_file,
|
||||
http_get_model_file) are retained as they serve non-Hub use cases.
|
||||
"""
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import urllib
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import partial
|
||||
from http.cookiejar import CookieJar
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Type, Union
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
import requests
|
||||
# --- Hub file downloads (delegated) ---
|
||||
from modelscope_hub.compat import dataset_file_download # noqa: E402,F401
|
||||
from modelscope_hub.compat.file_download import \
|
||||
model_file_download as _compat_model_file_download
|
||||
from requests.adapters import Retry
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
from modelscope.hub.api import HubApi, ModelScopeConfig
|
||||
from modelscope.hub.constants import (
|
||||
API_FILE_DOWNLOAD_CHUNK_SIZE, API_FILE_DOWNLOAD_RETRY_TIMES,
|
||||
API_FILE_DOWNLOAD_TIMEOUT, FILE_HASH, MODELSCOPE_DOWNLOAD_PARALLELS,
|
||||
MODELSCOPE_PARALLEL_DOWNLOAD_THRESHOLD_MB, TEMPORARY_FOLDER_NAME)
|
||||
from modelscope.utils.constant import (DEFAULT_DATASET_REVISION,
|
||||
DEFAULT_MODEL_REVISION,
|
||||
INTRA_CLOUD_ACCELERATION,
|
||||
REPO_TYPE_DATASET, REPO_TYPE_MODEL,
|
||||
REPO_TYPE_SUPPORT)
|
||||
from modelscope.utils.file_utils import (get_dataset_cache_root,
|
||||
get_model_cache_root)
|
||||
from modelscope.hub.constants import (API_FILE_DOWNLOAD_CHUNK_SIZE,
|
||||
API_FILE_DOWNLOAD_RETRY_TIMES,
|
||||
API_FILE_DOWNLOAD_TIMEOUT,
|
||||
MODELSCOPE_SDK_DEBUG)
|
||||
from modelscope.utils.logger import get_logger
|
||||
from .callback import ProgressCallback, TqdmCallback
|
||||
from .errors import FileDownloadError, InvalidParameter, NotExistError
|
||||
from .utils.caching import ModelFileSystemCache
|
||||
from .utils.utils import (file_integrity_validation, get_endpoint,
|
||||
model_id_to_group_owner_name)
|
||||
from .errors import FileDownloadError
|
||||
from .utils.utils import get_endpoint
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# Maximum number of retries for hash validation failures
|
||||
HASH_RETRY_TIMES = 3
|
||||
|
||||
def _get_release_timestamp():
|
||||
"""Compute the release timestamp for revision resolution.
|
||||
|
||||
Returns None (dev-mode) when MODELSCOPE_SDK_DEBUG is set.
|
||||
"""
|
||||
if os.environ.get(MODELSCOPE_SDK_DEBUG):
|
||||
return None
|
||||
try:
|
||||
from modelscope import version
|
||||
dt = getattr(version, '__release_datetime__', None)
|
||||
if not dt:
|
||||
return None
|
||||
return int(time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S')))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def model_file_download(
|
||||
model_id: str,
|
||||
file_path: str,
|
||||
revision: Optional[str] = DEFAULT_MODEL_REVISION,
|
||||
cache_dir: Optional[str] = None,
|
||||
user_agent: Union[Dict, str, None] = None,
|
||||
local_files_only: Optional[bool] = False,
|
||||
cookies: Optional[CookieJar] = None,
|
||||
local_dir: Optional[str] = None,
|
||||
token: Optional[str] = None,
|
||||
endpoint: Optional[str] = None,
|
||||
) -> Optional[str]: # pragma: no cover
|
||||
"""Download from a given URL and cache it if it's not already present in the local cache.
|
||||
|
||||
Given a URL, this function looks for the corresponding file in the local
|
||||
cache. If it's not there, download it. Then return the path to the cached
|
||||
file.
|
||||
|
||||
Args:
|
||||
model_id (str): The model to whom the file to be downloaded belongs.
|
||||
file_path(str): Path of the file to be downloaded, relative to the root of model repo.
|
||||
revision(str, optional): revision of the model file to be downloaded.
|
||||
Can be any of a branch, tag or commit hash.
|
||||
cache_dir (str, Path, optional): Path to the folder where cached files are stored.
|
||||
user_agent (dict, str, optional): The user-agent info in the form of a dictionary or a string.
|
||||
local_files_only (bool, optional): If `True`, avoid downloading the file and return the path to the
|
||||
local cached file if it exists. if `False`, download the file anyway even it exists.
|
||||
cookies (CookieJar, optional): The cookie of download request.
|
||||
local_dir (str, optional): Specific local directory path to which the file will be downloaded.
|
||||
token (str, optional): The user token.
|
||||
endpoint (str, optional): The remote endpoint.
|
||||
|
||||
Returns:
|
||||
string: string of local file or if networking is off, last version of
|
||||
file cached on disk.
|
||||
|
||||
Raises:
|
||||
NotExistError: The file is not exist.
|
||||
ValueError: The request parameter error.
|
||||
|
||||
Note:
|
||||
Raises the following errors:
|
||||
|
||||
- [`EnvironmentError`](https://docs.python.org/3/library/exceptions.html#EnvironmentError)
|
||||
if `use_auth_token=True` and the token cannot be found.
|
||||
- [`OSError`](https://docs.python.org/3/library/exceptions.html#OSError)
|
||||
if ETag cannot be determined.
|
||||
- [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
|
||||
if some parameter value is invalid
|
||||
"""
|
||||
return _repo_file_download(
|
||||
revision: str = None,
|
||||
*,
|
||||
cache_dir: str = None,
|
||||
local_dir: str = None,
|
||||
cookies: dict = None,
|
||||
token: str = None,
|
||||
endpoint: str = None,
|
||||
local_files_only: bool = False,
|
||||
user_agent=None,
|
||||
) -> str:
|
||||
"""Download a single model file with release-mode revision resolution."""
|
||||
if revision is None:
|
||||
try:
|
||||
from modelscope.hub.api import HubApi
|
||||
api = HubApi()
|
||||
release_ts = _get_release_timestamp()
|
||||
detail = api.get_valid_revision_detail(
|
||||
model_id, revision=None, release_timestamp=release_ts)
|
||||
revision = detail.get('Revision')
|
||||
except Exception:
|
||||
pass
|
||||
return _compat_model_file_download(
|
||||
model_id,
|
||||
file_path,
|
||||
repo_type=REPO_TYPE_MODEL,
|
||||
revision=revision,
|
||||
cache_dir=cache_dir,
|
||||
user_agent=user_agent,
|
||||
local_files_only=local_files_only,
|
||||
cookies=cookies,
|
||||
local_dir=local_dir,
|
||||
token=token,
|
||||
endpoint=endpoint)
|
||||
|
||||
|
||||
def dataset_file_download(
|
||||
dataset_id: str,
|
||||
file_path: str,
|
||||
revision: Optional[str] = DEFAULT_DATASET_REVISION,
|
||||
cache_dir: Union[str, Path, None] = None,
|
||||
local_dir: Optional[str] = None,
|
||||
user_agent: Optional[Union[Dict, str]] = None,
|
||||
local_files_only: Optional[bool] = False,
|
||||
cookies: Optional[CookieJar] = None,
|
||||
token: Optional[str] = None,
|
||||
endpoint: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Download raw files of a dataset.
|
||||
Downloads all files at the specified revision. This
|
||||
is useful when you want all files from a dataset, because you don't know which
|
||||
ones you will need a priori. All files are nested inside a folder in order
|
||||
to keep their actual filename relative to that folder.
|
||||
|
||||
An alternative would be to just clone a dataset but this would require that the
|
||||
user always has git and git-lfs installed, and properly configured.
|
||||
|
||||
Args:
|
||||
dataset_id (str): A user or an organization name and a dataset name separated by a `/`.
|
||||
file_path (str): The relative path of the file to download.
|
||||
revision (str, optional): An optional Git revision id which can be a branch name, a tag, or a
|
||||
commit hash. NOTE: currently only branch and tag name is supported
|
||||
cache_dir (str, Path, optional): Path to the folder where cached files are stored, dataset file will
|
||||
be save as cache_dir/dataset_id/THE_DATASET_FILES.
|
||||
local_dir (str, optional): Specific local directory path to which the file will be downloaded.
|
||||
user_agent (str, dict, optional): The user-agent info in the form of a dictionary or a string.
|
||||
local_files_only (bool, optional): If `True`, avoid downloading the file and return the path to the
|
||||
local cached file if it exists.
|
||||
cookies (CookieJar, optional): The cookie of the request, default None.
|
||||
token (str, optional): The user token.
|
||||
endpoint (str, optional): The remote endpoint.
|
||||
Raises:
|
||||
ValueError: the value details.
|
||||
|
||||
Returns:
|
||||
str: Local folder path (string) of repo snapshot
|
||||
|
||||
Note:
|
||||
Raises the following errors:
|
||||
- [`EnvironmentError`](https://docs.python.org/3/library/exceptions.html#EnvironmentError)
|
||||
if `use_auth_token=True` and the token cannot be found.
|
||||
- [`OSError`](https://docs.python.org/3/library/exceptions.html#OSError) if
|
||||
ETag cannot be determined.
|
||||
- [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
|
||||
if some parameter value is invalid
|
||||
"""
|
||||
return _repo_file_download(
|
||||
dataset_id,
|
||||
file_path,
|
||||
repo_type=REPO_TYPE_DATASET,
|
||||
revision=revision,
|
||||
cache_dir=cache_dir,
|
||||
user_agent=user_agent,
|
||||
local_files_only=local_files_only,
|
||||
cookies=cookies,
|
||||
local_dir=local_dir,
|
||||
token=token,
|
||||
endpoint=endpoint)
|
||||
endpoint=endpoint,
|
||||
local_files_only=local_files_only,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
|
||||
|
||||
def _repo_file_download(
|
||||
repo_id: str,
|
||||
file_path: str,
|
||||
*,
|
||||
repo_type: str = None,
|
||||
revision: Optional[str] = DEFAULT_MODEL_REVISION,
|
||||
cache_dir: Optional[str] = None,
|
||||
user_agent: Union[Dict, str, None] = None,
|
||||
local_files_only: Optional[bool] = False,
|
||||
cookies: Optional[CookieJar] = None,
|
||||
local_dir: Optional[str] = None,
|
||||
disable_tqdm: bool = False,
|
||||
token: Optional[str] = None,
|
||||
endpoint: Optional[str] = None,
|
||||
) -> Optional[str]: # pragma: no cover
|
||||
|
||||
if not repo_type:
|
||||
repo_type = REPO_TYPE_MODEL
|
||||
if repo_type not in REPO_TYPE_SUPPORT:
|
||||
raise InvalidParameter('Invalid repo type: %s, only support: %s' %
|
||||
(repo_type, REPO_TYPE_SUPPORT))
|
||||
|
||||
temporary_cache_dir, cache = create_temporary_directory_and_cache(
|
||||
repo_id, local_dir=local_dir, cache_dir=cache_dir, repo_type=repo_type)
|
||||
|
||||
# if local_files_only is `True` and the file already exists in cached_path
|
||||
# return the cached path
|
||||
if local_files_only:
|
||||
cached_file_path = cache.get_file_by_path(file_path)
|
||||
if cached_file_path is not None:
|
||||
logger.warning(
|
||||
"File exists in local cache, but we're not sure it's up to date"
|
||||
)
|
||||
return cached_file_path
|
||||
else:
|
||||
raise ValueError(
|
||||
'Cannot find the requested files in the cached path and outgoing'
|
||||
' traffic has been disabled. To enable look-ups and downloads'
|
||||
" online, set 'local_files_only' to False.")
|
||||
|
||||
_api = HubApi(token=token)
|
||||
|
||||
headers = {
|
||||
'user-agent': ModelScopeConfig.get_user_agent(user_agent=user_agent, ),
|
||||
'snapshot-identifier': str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
if INTRA_CLOUD_ACCELERATION == 'true':
|
||||
region_id: str = (
|
||||
os.getenv('INTRA_CLOUD_ACCELERATION_REGION')
|
||||
or _api._get_internal_acceleration_domain())
|
||||
if region_id:
|
||||
logger.info(
|
||||
f'Intra-cloud acceleration enabled for downloading from {repo_id}'
|
||||
)
|
||||
headers['x-aliyun-region-id'] = region_id
|
||||
|
||||
if cookies is None:
|
||||
cookies = _api.get_cookies()
|
||||
repo_files = []
|
||||
if endpoint is None:
|
||||
endpoint = _api.get_endpoint_for_read(
|
||||
repo_id=repo_id, repo_type=repo_type, token=token)
|
||||
file_to_download_meta = None
|
||||
if repo_type == REPO_TYPE_MODEL:
|
||||
revision = _api.get_valid_revision(
|
||||
repo_id, revision=revision, cookies=cookies, endpoint=endpoint)
|
||||
# we need to confirm the version is up-to-date
|
||||
# we need to get the file list to check if the latest version is cached, if so return, otherwise download
|
||||
repo_files = _api.get_model_files(
|
||||
model_id=repo_id,
|
||||
revision=revision,
|
||||
recursive=True,
|
||||
use_cookies=False if cookies is None else cookies,
|
||||
endpoint=endpoint)
|
||||
for repo_file in repo_files:
|
||||
if repo_file['Type'] == 'tree':
|
||||
continue
|
||||
|
||||
if repo_file['Path'] == file_path:
|
||||
if cache.exists(repo_file):
|
||||
file_name = repo_file['Name']
|
||||
logger.debug(
|
||||
f'File {file_name} already in cache with identical hash, skip downloading!'
|
||||
)
|
||||
return cache.get_file_by_info(repo_file)
|
||||
else:
|
||||
file_to_download_meta = repo_file
|
||||
break
|
||||
elif repo_type == REPO_TYPE_DATASET:
|
||||
group_or_owner, name = model_id_to_group_owner_name(repo_id)
|
||||
if not revision:
|
||||
revision = DEFAULT_DATASET_REVISION
|
||||
_hub_id, _ = _api.get_dataset_id_and_type(
|
||||
dataset_name=name,
|
||||
namespace=group_or_owner,
|
||||
endpoint=endpoint,
|
||||
token=token)
|
||||
page_number = 1
|
||||
page_size = 100
|
||||
while True:
|
||||
try:
|
||||
dataset_files = _api.get_dataset_files(
|
||||
repo_id=repo_id,
|
||||
revision=revision,
|
||||
root_path='/',
|
||||
recursive=True,
|
||||
page_number=page_number,
|
||||
page_size=page_size,
|
||||
endpoint=endpoint,
|
||||
token=token,
|
||||
dataset_hub_id=_hub_id)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Get dataset: {repo_id} file list failed, error: {e}')
|
||||
break
|
||||
|
||||
is_exist = False
|
||||
for repo_file in dataset_files:
|
||||
if repo_file['Type'] == 'tree':
|
||||
continue
|
||||
|
||||
if repo_file['Path'] == file_path:
|
||||
if cache.exists(repo_file):
|
||||
file_name = repo_file['Name']
|
||||
logger.debug(
|
||||
f'File {file_name} already in cache with identical hash, skip downloading!'
|
||||
)
|
||||
return cache.get_file_by_info(repo_file)
|
||||
else:
|
||||
file_to_download_meta = repo_file
|
||||
is_exist = True
|
||||
break
|
||||
if len(dataset_files) < page_size or is_exist:
|
||||
break
|
||||
page_number += 1
|
||||
|
||||
if file_to_download_meta is None:
|
||||
raise NotExistError('The file path: %s not exist in: %s' %
|
||||
(file_path, repo_id))
|
||||
|
||||
# we need to download again
|
||||
if repo_type == REPO_TYPE_MODEL:
|
||||
url_to_download = get_file_download_url(repo_id, file_path, revision,
|
||||
endpoint)
|
||||
elif repo_type == REPO_TYPE_DATASET:
|
||||
url_to_download = _api.get_dataset_file_url(
|
||||
file_name=file_to_download_meta['Path'],
|
||||
dataset_name=name,
|
||||
namespace=group_or_owner,
|
||||
revision=revision,
|
||||
endpoint=endpoint)
|
||||
else:
|
||||
raise ValueError(f'Invalid repo type {repo_type}')
|
||||
|
||||
return download_file(url_to_download, file_to_download_meta,
|
||||
temporary_cache_dir, cache, headers, cookies)
|
||||
|
||||
|
||||
def move_legacy_cache_to_standard_dir(cache_dir: str, model_id: str):
|
||||
if cache_dir.endswith(os.path.sep):
|
||||
cache_dir = cache_dir.strip(os.path.sep)
|
||||
legacy_cache_root = os.path.dirname(cache_dir)
|
||||
base_name = os.path.basename(cache_dir)
|
||||
if base_name == 'datasets':
|
||||
# datasets will not be not affected
|
||||
return
|
||||
if not legacy_cache_root.endswith('hub'):
|
||||
# Two scenarios:
|
||||
# We have restructured ModelScope cache directory,
|
||||
# Scenery 1:
|
||||
# When MODELSCOPE_CACHE is not set, the default directory remains
|
||||
# the same at ~/.cache/modelscope/hub
|
||||
# Scenery 2:
|
||||
# When MODELSCOPE_CACHE is set, the cache directory is moved from
|
||||
# $MODELSCOPE_CACHE/hub to $MODELSCOPE_CACHE/. In this case,
|
||||
# we will be migrating the hub directory accordingly.
|
||||
legacy_cache_root = os.path.join(legacy_cache_root, 'hub')
|
||||
group_or_owner, name = model_id_to_group_owner_name(model_id)
|
||||
name = name.replace('.', '___')
|
||||
temporary_cache_dir = os.path.join(cache_dir, group_or_owner, name)
|
||||
legacy_cache_dir = os.path.join(legacy_cache_root, group_or_owner, name)
|
||||
if os.path.exists(
|
||||
legacy_cache_dir) and not os.path.exists(temporary_cache_dir):
|
||||
logger.info(
|
||||
f'Legacy cache dir exists: {legacy_cache_dir}, move to {temporary_cache_dir}'
|
||||
)
|
||||
try:
|
||||
shutil.move(legacy_cache_dir, temporary_cache_dir)
|
||||
except Exception: # noqa
|
||||
# Failed, skip
|
||||
pass
|
||||
|
||||
|
||||
def create_temporary_directory_and_cache(model_id: str,
|
||||
local_dir: str = None,
|
||||
cache_dir: str = None,
|
||||
repo_type: str = REPO_TYPE_MODEL):
|
||||
if repo_type == REPO_TYPE_MODEL:
|
||||
default_cache_root = get_model_cache_root()
|
||||
elif repo_type == REPO_TYPE_DATASET:
|
||||
default_cache_root = get_dataset_cache_root()
|
||||
else:
|
||||
raise ValueError(
|
||||
f'repo_type only support model and dataset, but now is : {repo_type}'
|
||||
)
|
||||
|
||||
group_or_owner, name = model_id_to_group_owner_name(model_id)
|
||||
if local_dir is not None:
|
||||
temporary_cache_dir = os.path.join(local_dir, TEMPORARY_FOLDER_NAME)
|
||||
cache = ModelFileSystemCache(local_dir)
|
||||
else:
|
||||
if cache_dir is None:
|
||||
cache_dir = default_cache_root
|
||||
move_legacy_cache_to_standard_dir(cache_dir, model_id)
|
||||
if isinstance(cache_dir, Path):
|
||||
cache_dir = str(cache_dir)
|
||||
temporary_cache_dir = os.path.join(cache_dir, TEMPORARY_FOLDER_NAME,
|
||||
group_or_owner, name)
|
||||
name = name.replace('.', '___')
|
||||
cache = ModelFileSystemCache(cache_dir, group_or_owner, name)
|
||||
|
||||
os.makedirs(temporary_cache_dir, exist_ok=True)
|
||||
return temporary_cache_dir, cache
|
||||
# --- Direct HTTP downloads (retained - non-Hub API) ---
|
||||
|
||||
|
||||
def get_file_download_url(model_id: str,
|
||||
@@ -427,103 +124,6 @@ def get_file_download_url(model_id: str,
|
||||
)
|
||||
|
||||
|
||||
def download_part_with_retry(params):
|
||||
# unpack parameters
|
||||
model_file_path, progress_callbacks, start, end, url, file_name, cookies, headers = params
|
||||
get_headers = {} if headers is None else copy.deepcopy(headers)
|
||||
get_headers['X-Request-ID'] = str(uuid.uuid4().hex)
|
||||
retry = Retry(
|
||||
total=API_FILE_DOWNLOAD_RETRY_TIMES,
|
||||
backoff_factor=1,
|
||||
allowed_methods=['GET'])
|
||||
part_file_name = model_file_path + '_%s_%s' % (start, end)
|
||||
while True:
|
||||
try:
|
||||
partial_length = 0
|
||||
if os.path.exists(
|
||||
part_file_name): # download partial, continue download
|
||||
with open(part_file_name, 'rb') as f:
|
||||
partial_length = f.seek(0, io.SEEK_END)
|
||||
for callback in progress_callbacks:
|
||||
callback.update(partial_length)
|
||||
download_start = start + partial_length
|
||||
if download_start > end:
|
||||
break # this part is download completed.
|
||||
get_headers['Range'] = 'bytes=%s-%s' % (download_start, end)
|
||||
with open(part_file_name, 'ab+') as f:
|
||||
r = requests.get(
|
||||
url,
|
||||
stream=True,
|
||||
headers=get_headers,
|
||||
cookies=cookies,
|
||||
timeout=API_FILE_DOWNLOAD_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
for chunk in r.iter_content(
|
||||
chunk_size=API_FILE_DOWNLOAD_CHUNK_SIZE):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
f.write(chunk)
|
||||
for callback in progress_callbacks:
|
||||
callback.update(len(chunk))
|
||||
break
|
||||
except (Exception) as e: # no matter what exception, we will retry.
|
||||
retry = retry.increment('GET', url, error=e)
|
||||
logger.warning('Downloading: %s failed, reason: %s will retry' %
|
||||
(model_file_path, e))
|
||||
retry.sleep()
|
||||
|
||||
|
||||
def parallel_download(url: str,
|
||||
local_dir: str,
|
||||
file_name: str,
|
||||
cookies: CookieJar,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
file_size: int = None,
|
||||
disable_tqdm: bool = False,
|
||||
progress_callbacks: List[Type[ProgressCallback]] = None,
|
||||
endpoint: str = None):
|
||||
progress_callbacks = [] if progress_callbacks is None else progress_callbacks.copy(
|
||||
)
|
||||
if not disable_tqdm:
|
||||
progress_callbacks.append(TqdmCallback)
|
||||
progress_callbacks = [
|
||||
callback(file_name, file_size) for callback in progress_callbacks
|
||||
]
|
||||
# create temp file
|
||||
PART_SIZE = 160 * 1024 * 1024 # every part is 160M
|
||||
tasks = []
|
||||
file_path = os.path.join(local_dir, file_name)
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
for idx in range(int(file_size / PART_SIZE)):
|
||||
start = idx * PART_SIZE
|
||||
end = (idx + 1) * PART_SIZE - 1
|
||||
tasks.append((file_path, progress_callbacks, start, end, url,
|
||||
file_name, cookies, headers))
|
||||
if end + 1 < file_size:
|
||||
tasks.append((file_path, progress_callbacks, end + 1, file_size - 1,
|
||||
url, file_name, cookies, headers))
|
||||
parallels = min(MODELSCOPE_DOWNLOAD_PARALLELS, 16)
|
||||
# download every part
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=parallels, thread_name_prefix='download') as executor:
|
||||
list(executor.map(download_part_with_retry, tasks))
|
||||
for callback in progress_callbacks:
|
||||
callback.end()
|
||||
# merge parts.
|
||||
hash_sha256 = hashlib.sha256()
|
||||
with open(os.path.join(local_dir, file_name), 'wb') as output_file:
|
||||
for task in tasks:
|
||||
part_file_name = task[0] + '_%s_%s' % (task[2], task[3])
|
||||
with open(part_file_name, 'rb') as part_file:
|
||||
while True:
|
||||
chunk = part_file.read(16 * API_FILE_DOWNLOAD_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
output_file.write(chunk)
|
||||
hash_sha256.update(chunk)
|
||||
os.remove(part_file_name)
|
||||
return hash_sha256.hexdigest()
|
||||
|
||||
|
||||
def http_get_model_file(
|
||||
url: str,
|
||||
local_dir: str,
|
||||
@@ -712,76 +312,10 @@ def http_get_file(
|
||||
os.replace(temp_file.name, os.path.join(local_dir, file_name))
|
||||
|
||||
|
||||
def download_file(
|
||||
url,
|
||||
file_meta,
|
||||
temporary_cache_dir,
|
||||
cache,
|
||||
headers,
|
||||
cookies,
|
||||
disable_tqdm=False,
|
||||
progress_callbacks: List[Type[ProgressCallback]] = None,
|
||||
):
|
||||
temp_file = os.path.join(temporary_cache_dir, file_meta['Path'])
|
||||
|
||||
for hash_attempt in range(HASH_RETRY_TIMES):
|
||||
if MODELSCOPE_PARALLEL_DOWNLOAD_THRESHOLD_MB * 1000 * 1000 < file_meta[
|
||||
'Size'] and MODELSCOPE_DOWNLOAD_PARALLELS > 1: # parallel download large file.
|
||||
file_digest = parallel_download(
|
||||
url,
|
||||
temporary_cache_dir,
|
||||
file_meta['Path'],
|
||||
headers=headers,
|
||||
cookies=None if cookies is None else cookies.get_dict(),
|
||||
file_size=file_meta['Size'],
|
||||
disable_tqdm=disable_tqdm,
|
||||
progress_callbacks=progress_callbacks,
|
||||
)
|
||||
else:
|
||||
file_digest = http_get_model_file(
|
||||
url,
|
||||
temporary_cache_dir,
|
||||
file_meta['Path'],
|
||||
file_size=file_meta['Size'],
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
disable_tqdm=disable_tqdm,
|
||||
progress_callbacks=progress_callbacks,
|
||||
)
|
||||
|
||||
# Check file integrity
|
||||
if FILE_HASH in file_meta:
|
||||
expected_hash = file_meta[FILE_HASH]
|
||||
hash_valid = True
|
||||
if file_digest is not None:
|
||||
if file_digest != expected_hash:
|
||||
logger.warning(
|
||||
'Mismatched real-time digest for %s, falling back to full hash check',
|
||||
file_meta['Path'])
|
||||
if not file_integrity_validation(temp_file, expected_hash):
|
||||
hash_valid = False
|
||||
else:
|
||||
if not file_integrity_validation(temp_file, expected_hash):
|
||||
hash_valid = False
|
||||
|
||||
if not hash_valid:
|
||||
if hash_attempt < HASH_RETRY_TIMES - 1:
|
||||
logger.warning(
|
||||
'Hash validation failed for %s, '
|
||||
'retrying download (attempt %d/%d)', file_meta['Path'],
|
||||
hash_attempt + 1, HASH_RETRY_TIMES)
|
||||
# Clean up corrupted file before retry
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
continue
|
||||
else:
|
||||
raise FileDownloadError(
|
||||
'File %s hash validation failed after %d attempts, '
|
||||
'the file may be corrupted.' %
|
||||
(file_meta['Path'], HASH_RETRY_TIMES))
|
||||
|
||||
# Hash validation passed or no hash to validate, exit retry loop
|
||||
break
|
||||
|
||||
# Put file into cache
|
||||
return cache.put_file(file_meta, temp_file)
|
||||
__all__ = [
|
||||
'model_file_download',
|
||||
'dataset_file_download',
|
||||
'http_get_file',
|
||||
'http_get_model_file',
|
||||
'get_file_download_url',
|
||||
]
|
||||
|
||||
@@ -1,323 +1,200 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Git wrapper — shim delegating to ``modelscope_hub._git``.
|
||||
|
||||
Preserves the legacy ``GitCommandWrapper`` Singleton interface used
|
||||
throughout the SDK while routing primitive Git operations through
|
||||
:class:`modelscope_hub._git.GitCommand`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from modelscope_hub._git import GitCommand as _GitCommand
|
||||
|
||||
from modelscope.hub.errors import GitError
|
||||
from modelscope.utils.constant import MASTER_MODEL_BRANCH
|
||||
from modelscope.utils.logger import get_logger
|
||||
from ..utils.constant import MASTER_MODEL_BRANCH
|
||||
from .errors import GitError
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
__all__ = ['GitError', 'GitCommandWrapper', 'Singleton']
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
"""Metaclass enforcing one instance per class — preserved for parity."""
|
||||
|
||||
_instances: dict = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton,
|
||||
cls).__call__(*args, **kwargs)
|
||||
cls._instances[cls] = super().__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class GitCommandWrapper(metaclass=Singleton):
|
||||
"""Some git operation wrapper
|
||||
"""Backward-compatible Git wrapper.
|
||||
|
||||
Wraps :class:`modelscope_hub._git.GitCommand` to expose the legacy
|
||||
instance-method API (``clone``, ``push``, ``pull``, ``tag``…) used
|
||||
by callers that pre-date the SDK refactor.
|
||||
"""
|
||||
default_git_path = 'git' # The default git command line
|
||||
|
||||
def __init__(self, path: str = None):
|
||||
default_git_path = 'git'
|
||||
|
||||
def __init__(self, path: Optional[str] = None):
|
||||
self.git_path = path or self.default_git_path
|
||||
_GitCommand.set_git_path(self.git_path)
|
||||
|
||||
def _run_git_command(self, *args) -> subprocess.CompletedProcess:
|
||||
"""Run git command, if command return 0, return subprocess.response
|
||||
otherwise raise GitError, message is stdout and stderr.
|
||||
|
||||
Args:
|
||||
args: List of command args.
|
||||
|
||||
Raises:
|
||||
GitError: Exception with stdout and stderr.
|
||||
|
||||
Returns:
|
||||
subprocess.CompletedProcess: the command response
|
||||
"""
|
||||
logger.debug(' '.join(args))
|
||||
git_env = os.environ.copy()
|
||||
git_env['GIT_TERMINAL_PROMPT'] = '0'
|
||||
command = [self.git_path, *args]
|
||||
command = [item for item in command if item]
|
||||
response = subprocess.run(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=git_env,
|
||||
) # compatible for python3.6
|
||||
# ------------------------------------------------------------------
|
||||
# Low-level subprocess passthrough (legacy contract)
|
||||
# ------------------------------------------------------------------
|
||||
def _run_git_command(self, *args):
|
||||
"""Run a git subcommand, raising :class:`GitError` on failure."""
|
||||
try:
|
||||
response.check_returncode()
|
||||
return response
|
||||
except subprocess.CalledProcessError as error:
|
||||
std_out = response.stdout.decode('utf-8', errors='replace')
|
||||
std_err = error.stderr.decode('utf-8', errors='replace')
|
||||
if 'nothing to commit' in std_out:
|
||||
logger.info(
|
||||
'Nothing to commit, your local repo is upto date with remote'
|
||||
)
|
||||
return response
|
||||
else:
|
||||
logger.error(
|
||||
'Running git command: %s failed \n stdout: %s \n stderr: %s'
|
||||
% (command, std_out, std_err))
|
||||
raise GitError(std_err)
|
||||
return _GitCommand._run(*[a for a in args if a])
|
||||
except Exception as exc: # _git.GitError → legacy GitError
|
||||
raise GitError(str(exc)) from exc
|
||||
|
||||
def config_auth_token(self, repo_dir, auth_token):
|
||||
url = self.get_repo_remote_url(repo_dir)
|
||||
if '//oauth2' not in url:
|
||||
auth_url = self._add_token(auth_token, url)
|
||||
cmd_args = '-C %s remote set-url origin %s' % (repo_dir, auth_url)
|
||||
cmd_args = cmd_args.split(' ')
|
||||
rsp = self._run_git_command(*cmd_args)
|
||||
logger.debug(rsp.stdout.decode('utf8'))
|
||||
# ------------------------------------------------------------------
|
||||
# URL / token helpers
|
||||
# ------------------------------------------------------------------
|
||||
def _add_git_token(self, git_token: str, url: str) -> str:
|
||||
return _GitCommand._inject_token(url, git_token)
|
||||
|
||||
def _add_token(self, token: str, url: str):
|
||||
"""Inject OAuth2 token into an HTTP(S) git URL.
|
||||
def remove_token_from_url(self, url: str) -> str:
|
||||
return _GitCommand.strip_token_from_url(url)
|
||||
|
||||
Uses ``urllib.parse`` for reliable URL component handling,
|
||||
avoiding naive string replacement that can corrupt URLs
|
||||
containing multiple ``://`` sequences.
|
||||
|
||||
Args:
|
||||
token: OAuth2 access token.
|
||||
url: Remote URL (HTTP, HTTPS, or SSH).
|
||||
|
||||
Returns:
|
||||
URL with ``oauth2:<token>@`` injected into the *netloc*,
|
||||
or the original *url* unchanged when:
|
||||
|
||||
* *token* is falsy,
|
||||
* the URL already carries an ``oauth2`` credential,
|
||||
* the scheme is not HTTP/HTTPS (e.g. ``ssh://``, ``git@``).
|
||||
"""
|
||||
if not token:
|
||||
return url
|
||||
|
||||
# SSH URLs authenticate via keys, not tokens.
|
||||
if url.startswith('git@'):
|
||||
return url
|
||||
# ------------------------------------------------------------------
|
||||
# LFS
|
||||
# ------------------------------------------------------------------
|
||||
def is_lfs_installed(self) -> bool:
|
||||
return _GitCommand.is_lfs_available()
|
||||
|
||||
def git_lfs_install(self, repo_dir: str) -> bool:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
_GitCommand.lfs_install(Path(repo_dir))
|
||||
return True
|
||||
except Exception:
|
||||
return url
|
||||
|
||||
# Only inject into HTTP(S) URLs.
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
return url
|
||||
|
||||
# Prevent double injection.
|
||||
if parsed.username == 'oauth2':
|
||||
return url
|
||||
|
||||
# Reconstruct netloc: oauth2:<token>@host[:port]
|
||||
host = parsed.hostname or ''
|
||||
if parsed.port:
|
||||
host = f'{host}:{parsed.port}'
|
||||
netloc = f'oauth2:{token}@{host}'
|
||||
|
||||
return urlunparse(parsed._replace(netloc=netloc))
|
||||
|
||||
def remove_token_from_url(self, url: str):
|
||||
if url and '//oauth2' in url:
|
||||
start_index = url.find('oauth2')
|
||||
end_index = url.find('@')
|
||||
url = url[:start_index] + url[end_index + 1:]
|
||||
return url
|
||||
|
||||
def is_lfs_installed(self):
|
||||
cmd = ['lfs', 'env']
|
||||
try:
|
||||
self._run_git_command(*cmd)
|
||||
return True
|
||||
except GitError:
|
||||
return False
|
||||
|
||||
def git_lfs_install(self, repo_dir):
|
||||
cmd = ['-C', repo_dir, 'lfs', 'install', '--force']
|
||||
try:
|
||||
self._run_git_command(*cmd)
|
||||
return True
|
||||
except GitError:
|
||||
return False
|
||||
def list_lfs_files(self, repo_dir: str) -> List[str]:
|
||||
rsp = self._run_git_command('-C', repo_dir, 'lfs', 'ls-files')
|
||||
return [
|
||||
line.split(' ')[-1] for line in rsp.stdout.strip().splitlines()
|
||||
if line
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Auth / user config
|
||||
# ------------------------------------------------------------------
|
||||
def config_git_token(self, repo_dir: str, git_token: str) -> None:
|
||||
url = self.get_repo_remote_url(repo_dir)
|
||||
if '//oauth2' in url:
|
||||
return
|
||||
auth_url = self._add_git_token(git_token, url)
|
||||
self._run_git_command('-C', repo_dir, 'remote', 'set-url', 'origin',
|
||||
auth_url)
|
||||
|
||||
def add_user_info(self, repo_base_dir: str, repo_name: str) -> None:
|
||||
from modelscope.hub.api import ModelScopeConfig
|
||||
user_name, user_email = ModelScopeConfig.get_user_info()
|
||||
if not (user_name and user_email):
|
||||
return
|
||||
repo_dir = os.path.join(repo_base_dir, repo_name)
|
||||
self._run_git_command('-C', repo_dir, 'config', 'user.name', user_name)
|
||||
self._run_git_command('-C', repo_dir, 'config', 'user.email',
|
||||
user_email)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Clone / pull / push
|
||||
# ------------------------------------------------------------------
|
||||
def clone(self,
|
||||
repo_base_dir: str,
|
||||
token: str,
|
||||
git_token: Optional[str],
|
||||
url: str,
|
||||
repo_name: str,
|
||||
branch: Optional[str] = None):
|
||||
""" git clone command wrapper.
|
||||
For public project, token can None, private repo, there must token.
|
||||
|
||||
Args:
|
||||
repo_base_dir (str): The local base dir, the repository will be clone to local_dir/repo_name
|
||||
token (str): The git token, must be provided for private project.
|
||||
url (str): The remote url
|
||||
repo_name (str): The local repository path name.
|
||||
branch (str, optional): _description_. Defaults to None.
|
||||
|
||||
Returns:
|
||||
The popen response.
|
||||
"""
|
||||
url = self._add_token(token, url)
|
||||
if branch:
|
||||
clone_args = '-C %s clone %s %s --branch %s' % (repo_base_dir, url,
|
||||
repo_name, branch)
|
||||
else:
|
||||
clone_args = '-C %s clone %s' % (repo_base_dir, url)
|
||||
logger.debug(clone_args)
|
||||
clone_args = clone_args.split(' ')
|
||||
target = Path(repo_base_dir) / repo_name
|
||||
try:
|
||||
response = self._run_git_command(*clone_args)
|
||||
logger.debug(response.stdout.decode('utf8'))
|
||||
return response
|
||||
except GitError:
|
||||
# git clone may succeed but still exit non-zero when an
|
||||
# external hook (e.g. a custom core.hooksPath that wraps
|
||||
# ``git lfs post-merge``) returns a non-zero code. When the
|
||||
# repository was actually cloned, treat this as a warning.
|
||||
repo_dir = os.path.join(repo_base_dir, repo_name)
|
||||
if os.path.isdir(os.path.join(repo_dir, '.git')):
|
||||
_GitCommand.clone(
|
||||
url=url, target_dir=target, branch=branch, token=git_token)
|
||||
except Exception as exc:
|
||||
if (target / '.git').is_dir():
|
||||
logger.warning(
|
||||
'git clone exited with non-zero status but the '
|
||||
'repository was cloned successfully at %s. '
|
||||
'This is usually caused by a post-clone hook '
|
||||
'(e.g. core.hooksPath). Continuing.', repo_dir)
|
||||
'git clone exited non-zero but repository was cloned '
|
||||
'at %s. Likely a post-clone hook. Continuing.', target)
|
||||
return None
|
||||
raise
|
||||
|
||||
def add_user_info(self, repo_base_dir, repo_name):
|
||||
from modelscope.hub.api import ModelScopeConfig
|
||||
user_name, user_email = ModelScopeConfig.get_user_info()
|
||||
if user_name and user_email:
|
||||
# config user.name and user.email if exist
|
||||
config_user_name_args = '-C %s/%s config user.name %s' % (
|
||||
repo_base_dir, repo_name, user_name)
|
||||
response = self._run_git_command(*config_user_name_args.split(' '))
|
||||
logger.debug(response.stdout.decode('utf8'))
|
||||
config_user_email_args = '-C %s/%s config user.email %s' % (
|
||||
repo_base_dir, repo_name, user_email)
|
||||
response = self._run_git_command(
|
||||
*config_user_email_args.split(' '))
|
||||
logger.debug(response.stdout.decode('utf8'))
|
||||
|
||||
def add(self,
|
||||
repo_dir: str,
|
||||
files: List[str] = list(),
|
||||
all_files: bool = False):
|
||||
if all_files:
|
||||
add_args = '-C %s add -A' % repo_dir
|
||||
elif len(files) > 0:
|
||||
files_str = ' '.join(files)
|
||||
add_args = '-C %s add %s' % (repo_dir, files_str)
|
||||
add_args = add_args.split(' ')
|
||||
rsp = self._run_git_command(*add_args)
|
||||
logger.debug(rsp.stdout.decode('utf8'))
|
||||
return rsp
|
||||
|
||||
def commit(self, repo_dir: str, message: str):
|
||||
"""Run git commit command
|
||||
|
||||
Args:
|
||||
repo_dir (str): the repository directory.
|
||||
message (str): commit message.
|
||||
|
||||
Returns:
|
||||
The command popen response.
|
||||
"""
|
||||
commit_args = ['-C', '%s' % repo_dir, 'commit', '-m', "'%s'" % message]
|
||||
rsp = self._run_git_command(*commit_args)
|
||||
logger.info(rsp.stdout.decode('utf8'))
|
||||
return rsp
|
||||
|
||||
def checkout(self, repo_dir: str, revision: str):
|
||||
cmds = ['-C', '%s' % repo_dir, 'checkout', '%s' % revision]
|
||||
return self._run_git_command(*cmds)
|
||||
|
||||
def new_branch(self, repo_dir: str, revision: str):
|
||||
cmds = ['-C', '%s' % repo_dir, 'checkout', '-b', revision]
|
||||
return self._run_git_command(*cmds)
|
||||
|
||||
def get_remote_branches(self, repo_dir: str):
|
||||
cmds = ['-C', '%s' % repo_dir, 'branch', '-r']
|
||||
rsp = self._run_git_command(*cmds)
|
||||
info = [
|
||||
line.strip()
|
||||
for line in rsp.stdout.decode('utf8').strip().split(os.linesep)
|
||||
]
|
||||
if len(info) == 1:
|
||||
return ['/'.join(info[0].split('/')[1:])]
|
||||
else:
|
||||
return ['/'.join(line.split('/')[1:]) for line in info[1:]]
|
||||
raise GitError(str(exc)) from exc
|
||||
|
||||
def pull(self,
|
||||
repo_dir: str,
|
||||
remote: str = 'origin',
|
||||
branch: str = 'master'):
|
||||
cmds = ['-C', repo_dir, 'pull', remote, branch]
|
||||
return self._run_git_command(*cmds)
|
||||
return self._run_git_command('-C', repo_dir, 'pull', remote, branch)
|
||||
|
||||
def push(self,
|
||||
repo_dir: str,
|
||||
token: str,
|
||||
git_token: str,
|
||||
url: str,
|
||||
local_branch: str,
|
||||
remote_branch: str,
|
||||
force: bool = False):
|
||||
url = self._add_token(token, url)
|
||||
|
||||
push_args = '-C %s push %s %s:%s' % (repo_dir, url, local_branch,
|
||||
remote_branch)
|
||||
auth_url = self._add_git_token(git_token, url)
|
||||
args = [
|
||||
'-C', repo_dir, 'push', auth_url, f'{local_branch}:{remote_branch}'
|
||||
]
|
||||
if force:
|
||||
push_args += ' -f'
|
||||
push_args = push_args.split(' ')
|
||||
rsp = self._run_git_command(*push_args)
|
||||
logger.debug(rsp.stdout.decode('utf8'))
|
||||
return rsp
|
||||
args.append('-f')
|
||||
return self._run_git_command(*args)
|
||||
|
||||
def get_repo_remote_url(self, repo_dir: str):
|
||||
cmd_args = '-C %s config --get remote.origin.url' % repo_dir
|
||||
cmd_args = cmd_args.split(' ')
|
||||
rsp = self._run_git_command(*cmd_args)
|
||||
url = rsp.stdout.decode('utf8')
|
||||
return url.strip()
|
||||
# ------------------------------------------------------------------
|
||||
# Add / commit / branch / checkout
|
||||
# ------------------------------------------------------------------
|
||||
def add(self,
|
||||
repo_dir: str,
|
||||
files: Optional[List[str]] = None,
|
||||
all_files: bool = False):
|
||||
if all_files:
|
||||
return self._run_git_command('-C', repo_dir, 'add', '-A')
|
||||
return self._run_git_command('-C', repo_dir, 'add', *(files or []))
|
||||
|
||||
def list_lfs_files(self, repo_dir: str):
|
||||
cmd_args = '-C %s lfs ls-files' % repo_dir
|
||||
cmd_args = cmd_args.split(' ')
|
||||
rsp = self._run_git_command(*cmd_args)
|
||||
out = rsp.stdout.decode('utf8').strip()
|
||||
files = []
|
||||
for line in out.split(os.linesep):
|
||||
files.append(line.split(' ')[-1])
|
||||
def commit(self, repo_dir: str, message: str):
|
||||
return self._run_git_command('-C', repo_dir, 'commit', '-m',
|
||||
f"'{message}'")
|
||||
|
||||
return files
|
||||
def checkout(self, repo_dir: str, revision: str):
|
||||
return self._run_git_command('-C', repo_dir, 'checkout', revision)
|
||||
|
||||
def new_branch(self, repo_dir: str, revision: str):
|
||||
return self._run_git_command('-C', repo_dir, 'checkout', '-b',
|
||||
revision)
|
||||
|
||||
def get_remote_branches(self, repo_dir: str) -> List[str]:
|
||||
rsp = self._run_git_command('-C', repo_dir, 'branch', '-r')
|
||||
info = [
|
||||
line.strip() for line in rsp.stdout.strip().splitlines() if line
|
||||
]
|
||||
if len(info) <= 1:
|
||||
return ['/'.join(info[0].split('/')[1:])] if info else []
|
||||
return ['/'.join(line.split('/')[1:]) for line in info[1:]]
|
||||
|
||||
def get_repo_remote_url(self, repo_dir: str) -> str:
|
||||
rsp = self._run_git_command('-C', repo_dir, 'config', '--get',
|
||||
'remote.origin.url')
|
||||
return rsp.stdout.strip()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tags
|
||||
# ------------------------------------------------------------------
|
||||
def tag(self,
|
||||
repo_dir: str,
|
||||
tag_name: str,
|
||||
message: str,
|
||||
ref: str = MASTER_MODEL_BRANCH):
|
||||
cmd_args = [
|
||||
'-C', repo_dir, 'tag', tag_name, '-m',
|
||||
'"%s"' % message, ref
|
||||
]
|
||||
rsp = self._run_git_command(*cmd_args)
|
||||
logger.debug(rsp.stdout.decode('utf8'))
|
||||
return rsp
|
||||
return self._run_git_command('-C', repo_dir, 'tag', tag_name, '-m',
|
||||
f'"{message}"', ref)
|
||||
|
||||
def push_tag(self, repo_dir: str, tag_name):
|
||||
cmd_args = ['-C', repo_dir, 'push', 'origin', tag_name]
|
||||
rsp = self._run_git_command(*cmd_args)
|
||||
logger.debug(rsp.stdout.decode('utf8'))
|
||||
return rsp
|
||||
def push_tag(self, repo_dir: str, tag_name: str):
|
||||
return self._run_git_command('-C', repo_dir, 'push', 'origin',
|
||||
tag_name)
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Repository — shim delegating to ``modelscope_hub`` for Git operations.
|
||||
|
||||
Preserves the legacy :class:`Repository` and :class:`DatasetRepository`
|
||||
constructors (auto-clone-on-init) and methods (``push``, ``pull``,
|
||||
``tag``, ``tag_and_push``, ``add_lfs_type``) used by callers that
|
||||
pre-date the SDK refactor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import warnings
|
||||
from typing import Optional
|
||||
@@ -14,172 +21,147 @@ from .utils.utils import get_endpoint
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
__all__ = ['Repository', 'DatasetRepository']
|
||||
|
||||
|
||||
def _resolve_git_token(auth_token: Optional[str]) -> Optional[str]:
|
||||
if auth_token:
|
||||
return auth_token
|
||||
from modelscope.hub.api import ModelScopeConfig
|
||||
return ModelScopeConfig.get_git_token()
|
||||
|
||||
|
||||
def _clone_if_needed(git_wrapper: GitCommandWrapper, base_dir: str,
|
||||
repo_name: str, repo_dir: str, url: str,
|
||||
git_token: Optional[str],
|
||||
revision: Optional[str]) -> bool:
|
||||
"""Clone *url* into *repo_dir* unless it's already that working copy.
|
||||
|
||||
Returns ``True`` if a clone was performed, ``False`` if skipped.
|
||||
"""
|
||||
os.makedirs(repo_dir, exist_ok=True)
|
||||
if os.listdir(repo_dir):
|
||||
try:
|
||||
existing = git_wrapper.get_repo_remote_url(repo_dir)
|
||||
existing = git_wrapper.remove_token_from_url(existing)
|
||||
if existing == url:
|
||||
return False
|
||||
except GitError:
|
||||
pass
|
||||
git_wrapper.clone(base_dir, git_token, url, repo_name, revision)
|
||||
return True
|
||||
|
||||
|
||||
class Repository:
|
||||
"""A local representation of the model git repository.
|
||||
"""
|
||||
"""A local representation of a model Git repository on ModelScope Hub."""
|
||||
|
||||
def __init__(self,
|
||||
model_dir: str,
|
||||
clone_from: str,
|
||||
revision: Optional[str] = DEFAULT_REPOSITORY_REVISION,
|
||||
auth_token: Optional[str] = None,
|
||||
git_token: Optional[str] = None,
|
||||
git_path: Optional[str] = None,
|
||||
endpoint: Optional[str] = None):
|
||||
"""Instantiate a Repository object by cloning the remote ModelScopeHub repo
|
||||
endpoint: Optional[str] = None,
|
||||
auth_token: Optional[str] = None):
|
||||
if auth_token is not None and git_token is None:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'Repository(auth_token=...) is deprecated, '
|
||||
'use Repository(git_token=...) instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
git_token = auth_token
|
||||
|
||||
Args:
|
||||
model_dir (str): The model root directory.
|
||||
clone_from (str): model id in ModelScope-hub from which git clone
|
||||
revision (str, optional): revision of the model you want to clone from.
|
||||
Can be any of a branch, tag or commit hash
|
||||
auth_token (str, optional): token obtained when calling `HubApi.login()`.
|
||||
Usually you can safely ignore the parameter as the token is already
|
||||
saved when you login the first time, if None, we will use saved token.
|
||||
git_path (str, optional): The git command line path, if None, we use 'git'
|
||||
endpoint (str, optional): The ModelScope endpoint URL. If None, use default endpoint.
|
||||
if not revision:
|
||||
raise InvalidParameter(
|
||||
'a non-default value of revision cannot be empty.')
|
||||
|
||||
Raises:
|
||||
InvalidParameter: revision is None.
|
||||
"""
|
||||
self._endpoint = endpoint
|
||||
self.model_dir = model_dir
|
||||
self.model_base_dir = os.path.dirname(model_dir)
|
||||
self.model_repo_name = os.path.basename(model_dir)
|
||||
|
||||
if not revision:
|
||||
err_msg = 'a non-default value of revision cannot be empty.'
|
||||
raise InvalidParameter(err_msg)
|
||||
|
||||
from modelscope.hub.api import ModelScopeConfig
|
||||
if auth_token:
|
||||
self.auth_token = auth_token
|
||||
else:
|
||||
self.auth_token = ModelScopeConfig.get_token()
|
||||
|
||||
git_wrapper = GitCommandWrapper()
|
||||
if not git_wrapper.is_lfs_installed():
|
||||
logger.error('git lfs is not installed, please install.')
|
||||
self.git_token = _resolve_git_token(git_token)
|
||||
|
||||
self.git_wrapper = GitCommandWrapper(git_path)
|
||||
os.makedirs(self.model_dir, exist_ok=True)
|
||||
if not self.git_wrapper.is_lfs_installed():
|
||||
logger.error('git lfs is not installed, please install.')
|
||||
|
||||
url = self._get_model_id_url(clone_from)
|
||||
if os.listdir(self.model_dir): # directory not empty.
|
||||
remote_url = self._get_remote_url()
|
||||
remote_url = self.git_wrapper.remove_token_from_url(remote_url)
|
||||
if remote_url and remote_url == url: # need not clone again
|
||||
return
|
||||
self.git_wrapper.clone(self.model_base_dir, self.auth_token, url,
|
||||
self.model_repo_name, revision)
|
||||
cloned = _clone_if_needed(self.git_wrapper, self.model_base_dir,
|
||||
self.model_repo_name, self.model_dir, url,
|
||||
self.git_token, revision)
|
||||
if not cloned:
|
||||
return
|
||||
|
||||
if git_wrapper.is_lfs_installed():
|
||||
git_wrapper.git_lfs_install(self.model_dir)
|
||||
if self.git_wrapper.is_lfs_installed():
|
||||
self.git_wrapper.git_lfs_install(self.model_dir)
|
||||
|
||||
# add user info if login
|
||||
self.git_wrapper.add_user_info(self.model_base_dir,
|
||||
self.model_repo_name)
|
||||
if self.auth_token: # config remote with auth token
|
||||
self.git_wrapper.config_auth_token(self.model_dir, self.auth_token)
|
||||
if self.git_token:
|
||||
self.git_wrapper.config_git_token(self.model_dir, self.git_token)
|
||||
|
||||
def _get_model_id_url(self, model_id):
|
||||
endpoint = self._endpoint if self._endpoint else get_endpoint()
|
||||
url = f'{endpoint}/{model_id}.git'
|
||||
return url
|
||||
|
||||
def _get_remote_url(self):
|
||||
try:
|
||||
remote = self.git_wrapper.get_repo_remote_url(self.model_dir)
|
||||
except GitError:
|
||||
remote = None
|
||||
return remote
|
||||
def _get_model_id_url(self, model_id: str) -> str:
|
||||
endpoint = self._endpoint or get_endpoint()
|
||||
return f'{endpoint}/{model_id}.git'
|
||||
|
||||
def pull(self, remote: str = 'origin', branch: str = 'master'):
|
||||
"""Pull remote branch
|
||||
|
||||
Args:
|
||||
remote (str, optional): The remote name. Defaults to 'origin'.
|
||||
branch (str, optional): The remote branch. Defaults to 'master'.
|
||||
"""
|
||||
"""Pull *remote*/*branch* into the local checkout."""
|
||||
self.git_wrapper.pull(self.model_dir, remote=remote, branch=branch)
|
||||
|
||||
def add_lfs_type(self, file_name_suffix: str):
|
||||
"""Add file suffix to lfs list.
|
||||
|
||||
Args:
|
||||
file_name_suffix (str): The file name suffix.
|
||||
examples '*.safetensors'
|
||||
"""
|
||||
os.system(
|
||||
"printf '\n%s filter=lfs diff=lfs merge=lfs -text\n'>>%s" %
|
||||
(file_name_suffix, os.path.join(self.model_dir, '.gitattributes')))
|
||||
def add_lfs_type(self, file_name_suffix: str) -> None:
|
||||
"""Track an additional file-name pattern with Git LFS."""
|
||||
attrs = os.path.join(self.model_dir, '.gitattributes')
|
||||
with open(attrs, 'a', encoding='utf-8') as f:
|
||||
f.write(
|
||||
f'\n{file_name_suffix} filter=lfs diff=lfs merge=lfs -text\n')
|
||||
|
||||
def push(self,
|
||||
commit_message: str,
|
||||
local_branch: Optional[str] = DEFAULT_REPOSITORY_REVISION,
|
||||
remote_branch: Optional[str] = DEFAULT_REPOSITORY_REVISION,
|
||||
force: Optional[bool] = False):
|
||||
force: bool = False):
|
||||
"""Stage all changes, commit, and push to the remote."""
|
||||
warnings.warn(
|
||||
'This function is deprecated and will be removed in future versions. '
|
||||
'Please use git command directly or use HubApi().upload_folder instead',
|
||||
'This function is deprecated and will be removed in future '
|
||||
'versions. Please use git command directly or use '
|
||||
'HubApi().upload_folder instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
"""Push local files to remote, this method will do.
|
||||
Execute git pull, git add, git commit, git push in order.
|
||||
|
||||
Args:
|
||||
commit_message (str): commit message
|
||||
local_branch(str, optional): The local branch, default master.
|
||||
remote_branch (str, optional): The remote branch to push, default master.
|
||||
force (bool, optional): whether to use forced-push.
|
||||
|
||||
Raises:
|
||||
InvalidParameter: no commit message.
|
||||
NotLoginException: no auth token.
|
||||
"""
|
||||
if commit_message is None or not isinstance(commit_message, str):
|
||||
msg = 'commit_message must be provided!'
|
||||
raise InvalidParameter(msg)
|
||||
if not isinstance(commit_message, str) or not commit_message:
|
||||
raise InvalidParameter('commit_message must be provided!')
|
||||
if not isinstance(force, bool):
|
||||
raise InvalidParameter('force must be bool')
|
||||
|
||||
if not self.auth_token:
|
||||
if not self.git_token:
|
||||
raise NotLoginException('Must login to push, please login first.')
|
||||
|
||||
self.git_wrapper.config_auth_token(self.model_dir, self.auth_token)
|
||||
self.git_wrapper.config_git_token(self.model_dir, self.git_token)
|
||||
self.git_wrapper.add_user_info(self.model_base_dir,
|
||||
self.model_repo_name)
|
||||
|
||||
url = self.git_wrapper.get_repo_remote_url(self.model_dir)
|
||||
|
||||
self.git_wrapper.add(self.model_dir, all_files=True)
|
||||
self.git_wrapper.commit(self.model_dir, commit_message)
|
||||
self.git_wrapper.push(
|
||||
repo_dir=self.model_dir,
|
||||
token=self.auth_token,
|
||||
git_token=self.git_token,
|
||||
url=url,
|
||||
local_branch=local_branch,
|
||||
remote_branch=remote_branch)
|
||||
remote_branch=remote_branch,
|
||||
force=force)
|
||||
|
||||
def tag(self,
|
||||
tag_name: str,
|
||||
message: str,
|
||||
ref: Optional[str] = MASTER_MODEL_BRANCH):
|
||||
"""Create a new tag.
|
||||
|
||||
Args:
|
||||
tag_name (str): The name of the tag
|
||||
message (str): The tag message.
|
||||
ref (str, optional): The tag reference, can be commit id or branch.
|
||||
|
||||
Raises:
|
||||
InvalidParameter: no commit message.
|
||||
"""
|
||||
if tag_name is None or tag_name == '':
|
||||
msg = 'We use tag-based revision, therefore tag_name cannot be None or empty.'
|
||||
raise InvalidParameter(msg)
|
||||
if message is None or message == '':
|
||||
msg = 'We use annotated tag, therefore message cannot None or empty.'
|
||||
raise InvalidParameter(msg)
|
||||
"""Create an annotated tag pointing to *ref*."""
|
||||
if not tag_name:
|
||||
raise InvalidParameter(
|
||||
'We use tag-based revision, therefore tag_name '
|
||||
'cannot be None or empty.')
|
||||
if not message:
|
||||
raise InvalidParameter('We use annotated tag, therefore message '
|
||||
'cannot None or empty.')
|
||||
self.git_wrapper.tag(
|
||||
repo_dir=self.model_dir,
|
||||
tag_name=tag_name,
|
||||
@@ -190,143 +172,97 @@ class Repository:
|
||||
tag_name: str,
|
||||
message: str,
|
||||
ref: Optional[str] = MASTER_MODEL_BRANCH):
|
||||
"""Create tag and push to remote
|
||||
|
||||
Args:
|
||||
tag_name (str): The name of the tag
|
||||
message (str): The tag message.
|
||||
ref (str, optional): The tag ref, can be commit id or branch. Defaults to MASTER_MODEL_BRANCH.
|
||||
"""
|
||||
"""Create *tag_name* and push it to the remote."""
|
||||
self.tag(tag_name, message, ref)
|
||||
|
||||
self.git_wrapper.push_tag(repo_dir=self.model_dir, tag_name=tag_name)
|
||||
|
||||
|
||||
class DatasetRepository:
|
||||
"""A local representation of the dataset (metadata) git repository.
|
||||
"""
|
||||
"""A local representation of a dataset (metadata) Git repository."""
|
||||
|
||||
def __init__(self,
|
||||
repo_work_dir: str,
|
||||
dataset_id: str,
|
||||
revision: Optional[str] = DEFAULT_DATASET_REVISION,
|
||||
auth_token: Optional[str] = None,
|
||||
git_token: Optional[str] = None,
|
||||
git_path: Optional[str] = None,
|
||||
endpoint: Optional[str] = None):
|
||||
"""
|
||||
Instantiate a Dataset Repository object by cloning the remote ModelScope dataset repo
|
||||
endpoint: Optional[str] = None,
|
||||
auth_token: Optional[str] = None):
|
||||
if auth_token is not None and git_token is None:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'DatasetRepository(auth_token=...) is deprecated, '
|
||||
'use DatasetRepository(git_token=...) instead.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
git_token = auth_token
|
||||
|
||||
Args:
|
||||
repo_work_dir (str): The dataset repo root directory.
|
||||
dataset_id (str): dataset id in ModelScope from which git clone
|
||||
revision (str, optional): revision of the dataset you want to clone from.
|
||||
Can be any of a branch, tag or commit hash
|
||||
auth_token (str, optional): token obtained when calling `HubApi.login()`.
|
||||
Usually you can safely ignore the parameter as the token is
|
||||
already saved when you login the first time, if None, we will use saved token.
|
||||
git_path (str, optional): The git command line path, if None, we use 'git'
|
||||
endpoint (str, optional): The ModelScope endpoint URL. If None, use default endpoint.
|
||||
if not repo_work_dir or not isinstance(repo_work_dir, str):
|
||||
raise InvalidParameter('dataset_work_dir must be provided!')
|
||||
repo_work_dir = repo_work_dir.rstrip('/')
|
||||
if not repo_work_dir:
|
||||
raise InvalidParameter('dataset_work_dir can not be root dir!')
|
||||
if not revision:
|
||||
raise InvalidParameter(
|
||||
'a non-default value of revision cannot be empty.')
|
||||
|
||||
Raises:
|
||||
InvalidParameter: parameter invalid.
|
||||
"""
|
||||
self._endpoint = endpoint
|
||||
self.dataset_id = dataset_id
|
||||
if not repo_work_dir or not isinstance(repo_work_dir, str):
|
||||
err_msg = 'dataset_work_dir must be provided!'
|
||||
raise InvalidParameter(err_msg)
|
||||
self.repo_work_dir = repo_work_dir.rstrip('/')
|
||||
if not self.repo_work_dir:
|
||||
err_msg = 'dataset_work_dir can not be root dir!'
|
||||
raise InvalidParameter(err_msg)
|
||||
self.repo_base_dir = os.path.dirname(self.repo_work_dir)
|
||||
self.repo_name = os.path.basename(self.repo_work_dir)
|
||||
|
||||
if not revision:
|
||||
err_msg = 'a non-default value of revision cannot be empty.'
|
||||
raise InvalidParameter(err_msg)
|
||||
self.repo_work_dir = repo_work_dir
|
||||
self.repo_base_dir = os.path.dirname(repo_work_dir)
|
||||
self.repo_name = os.path.basename(repo_work_dir)
|
||||
self.revision = revision
|
||||
from modelscope.hub.api import ModelScopeConfig
|
||||
if auth_token:
|
||||
self.auth_token = auth_token
|
||||
else:
|
||||
self.auth_token = ModelScopeConfig.get_token()
|
||||
self.git_token = _resolve_git_token(git_token)
|
||||
|
||||
self.git_wrapper = GitCommandWrapper(git_path)
|
||||
os.makedirs(self.repo_work_dir, exist_ok=True)
|
||||
self.repo_url = self._get_repo_url(dataset_id=dataset_id)
|
||||
self.repo_url = self._get_repo_url(dataset_id)
|
||||
|
||||
def _get_repo_url(self, dataset_id: str) -> str:
|
||||
endpoint = self._endpoint or get_endpoint()
|
||||
return f'{endpoint}/datasets/{dataset_id}.git'
|
||||
|
||||
def clone(self) -> str:
|
||||
# check local repo dir, directory not empty.
|
||||
if os.listdir(self.repo_work_dir):
|
||||
remote_url = self._get_remote_url()
|
||||
remote_url = self.git_wrapper.remove_token_from_url(remote_url)
|
||||
# no need clone again
|
||||
if remote_url and remote_url == self.repo_url:
|
||||
return ''
|
||||
|
||||
logger.info('Cloning repo from {} '.format(self.repo_url))
|
||||
self.git_wrapper.clone(self.repo_base_dir, self.auth_token,
|
||||
self.repo_url, self.repo_name, self.revision)
|
||||
return self.repo_work_dir
|
||||
"""Clone the dataset repo if not already cloned, returning its path."""
|
||||
cloned = _clone_if_needed(self.git_wrapper, self.repo_base_dir,
|
||||
self.repo_name, self.repo_work_dir,
|
||||
self.repo_url, self.git_token, self.revision)
|
||||
return self.repo_work_dir if cloned else ''
|
||||
|
||||
def push(self,
|
||||
commit_message: str,
|
||||
branch: Optional[str] = DEFAULT_DATASET_REVISION,
|
||||
force: Optional[bool] = False):
|
||||
force: bool = False):
|
||||
"""Stage all changes, commit, and push to the remote."""
|
||||
warnings.warn(
|
||||
'This function is deprecated and will be removed in future versions. '
|
||||
'Please use git command directly or use HubApi().upload_folder instead',
|
||||
'This function is deprecated and will be removed in future '
|
||||
'versions. Please use git command directly or use '
|
||||
'HubApi().upload_folder instead',
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
"""Push local files to remote, this method will do.
|
||||
git pull
|
||||
git add
|
||||
git commit
|
||||
git push
|
||||
|
||||
Args:
|
||||
commit_message (str): commit message
|
||||
branch (str, optional): which branch to push.
|
||||
force (bool, optional): whether to use forced-push.
|
||||
|
||||
Raises:
|
||||
InvalidParameter: no commit message.
|
||||
NotLoginException: no access token.
|
||||
"""
|
||||
if commit_message is None or not isinstance(commit_message, str):
|
||||
msg = 'commit_message must be provided!'
|
||||
raise InvalidParameter(msg)
|
||||
|
||||
if not isinstance(commit_message, str) or not commit_message:
|
||||
raise InvalidParameter('commit_message must be provided!')
|
||||
if not isinstance(force, bool):
|
||||
raise InvalidParameter('force must be bool')
|
||||
|
||||
if not self.auth_token:
|
||||
if not self.git_token:
|
||||
raise NotLoginException('Must login to push, please login first.')
|
||||
|
||||
self.git_wrapper.config_auth_token(self.repo_work_dir, self.auth_token)
|
||||
self.git_wrapper.config_git_token(self.repo_work_dir, self.git_token)
|
||||
self.git_wrapper.add_user_info(self.repo_base_dir, self.repo_name)
|
||||
|
||||
remote_url = self._get_remote_url()
|
||||
remote_url = self.git_wrapper.remove_token_from_url(remote_url)
|
||||
try:
|
||||
remote_url = self.git_wrapper.get_repo_remote_url(
|
||||
self.repo_work_dir)
|
||||
remote_url = self.git_wrapper.remove_token_from_url(remote_url)
|
||||
except GitError:
|
||||
remote_url = self.repo_url
|
||||
|
||||
self.git_wrapper.pull(self.repo_work_dir)
|
||||
self.git_wrapper.add(self.repo_work_dir, all_files=True)
|
||||
self.git_wrapper.commit(self.repo_work_dir, commit_message)
|
||||
self.git_wrapper.push(
|
||||
repo_dir=self.repo_work_dir,
|
||||
token=self.auth_token,
|
||||
git_token=self.git_token,
|
||||
url=remote_url,
|
||||
local_branch=branch,
|
||||
remote_branch=branch)
|
||||
|
||||
def _get_repo_url(self, dataset_id):
|
||||
endpoint = self._endpoint if self._endpoint else get_endpoint()
|
||||
return f'{endpoint}/datasets/{dataset_id}.git'
|
||||
|
||||
def _get_remote_url(self):
|
||||
try:
|
||||
remote = self.git_wrapper.get_repo_remote_url(self.repo_work_dir)
|
||||
except GitError:
|
||||
remote = None
|
||||
return remote
|
||||
remote_branch=branch,
|
||||
force=force)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,127 +1,12 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Upload hash cache — shim delegating to ``modelscope_hub._upload``.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Union
|
||||
The unified :class:`modelscope_hub._upload.UploadTracker` supersedes
|
||||
this module's previous standalone hash cache; it remains here for any
|
||||
caller that still imports the legacy file constant.
|
||||
"""
|
||||
from modelscope_hub._upload import UploadTracker as UploadHashCache # noqa: F401
|
||||
from modelscope_hub.constants import \
|
||||
UPLOAD_CACHE_FILE as UPLOAD_HASH_CACHE_FILE # noqa: F401
|
||||
|
||||
import json
|
||||
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
UPLOAD_HASH_CACHE_FILE = '.ms_upload_cache'
|
||||
|
||||
|
||||
class UploadHashCache:
|
||||
"""Persistent local hash cache for upload_folder resume.
|
||||
|
||||
Stores SHA256 hashes keyed by (relative_path, mtime, size) to skip
|
||||
re-hashing unchanged files on retry/resume. Thread-safe for concurrent
|
||||
put() calls from multiple upload threads.
|
||||
|
||||
Cache is stored as JSON at {folder_path}/.ms_upload_cache, co-located
|
||||
with the upload source for portability.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_path: Union[str, Path]):
|
||||
"""Initialize cache.
|
||||
|
||||
Args:
|
||||
cache_path: Path to the cache file (typically folder/.ms_upload_cache).
|
||||
"""
|
||||
self._cache_path = Path(cache_path)
|
||||
self._cache: Dict[str, dict] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._load()
|
||||
|
||||
@staticmethod
|
||||
def _make_key(rel_path: str, mtime: float, size: int) -> str:
|
||||
"""Build cache lookup key from file metadata."""
|
||||
return f'{rel_path}|{mtime}|{size}'
|
||||
|
||||
def get(self, rel_path: str, mtime: float, size: int) -> Optional[dict]:
|
||||
"""Return cached hash info or None if not cached / stale.
|
||||
|
||||
Args:
|
||||
rel_path: Relative path of the file within the upload folder.
|
||||
mtime: File modification time (os.stat st_mtime).
|
||||
size: File size in bytes.
|
||||
|
||||
Returns:
|
||||
Dict with file_hash and file_size, or None.
|
||||
"""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
with self._lock:
|
||||
entry = self._cache.get(key)
|
||||
if entry is None:
|
||||
return None
|
||||
# Reconstruct the hash_info dict expected by callers
|
||||
return {
|
||||
'file_path_or_obj': rel_path,
|
||||
'file_hash': entry['file_hash'],
|
||||
'file_size': entry['file_size'],
|
||||
}
|
||||
|
||||
def put(self, rel_path: str, mtime: float, size: int, hash_info: dict):
|
||||
"""Store hash info for a file. Thread-safe.
|
||||
|
||||
Args:
|
||||
rel_path: Relative path of the file.
|
||||
mtime: File modification time.
|
||||
size: File size in bytes.
|
||||
hash_info: Dict from compute_file_hash with file_hash and file_size.
|
||||
"""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
entry = {
|
||||
'file_hash': hash_info['file_hash'],
|
||||
'file_size': hash_info['file_size'],
|
||||
}
|
||||
with self._lock:
|
||||
self._cache[key] = entry
|
||||
|
||||
def save(self):
|
||||
"""Persist cache to disk via atomic write (temp file + rename).
|
||||
|
||||
Safe against crashes -- either the old or new file is present,
|
||||
never a partial write.
|
||||
"""
|
||||
try:
|
||||
self._cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self._lock:
|
||||
data = dict(self._cache)
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
dir=str(self._cache_path.parent),
|
||||
prefix='.ms_upload_cache_tmp_')
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f)
|
||||
os.replace(tmp_path, str(self._cache_path))
|
||||
except BaseException:
|
||||
os.unlink(tmp_path)
|
||||
raise
|
||||
logger.info(
|
||||
f'Hash cache saved: {len(data)} entries -> {self._cache_path}')
|
||||
if not self._cache_path.exists():
|
||||
logger.warning(
|
||||
f'Hash cache file not found after save: {self._cache_path}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to save hash cache to {self._cache_path}: {e}')
|
||||
|
||||
def _load(self):
|
||||
"""Load cache from disk. Tolerates missing or corrupt file."""
|
||||
if not self._cache_path.exists():
|
||||
return
|
||||
try:
|
||||
with open(self._cache_path, 'r') as f:
|
||||
self._cache = json.load(f)
|
||||
logger.info(
|
||||
f'Hash cache loaded: {len(self._cache)} entries from {self._cache_path}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to load hash cache, starting fresh: {e}')
|
||||
self._cache = {}
|
||||
__all__ = ['UploadHashCache', 'UPLOAD_HASH_CACHE_FILE']
|
||||
|
||||
@@ -1,94 +1,5 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Upload pipeline batch tracker — shim delegating to ``modelscope_hub._upload``."""
|
||||
from modelscope_hub._upload import BatchTracker # noqa: F401
|
||||
|
||||
import threading
|
||||
from typing import List, Tuple
|
||||
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class BatchTracker:
|
||||
"""Thread-safe tracker for pre-assigned upload batches.
|
||||
|
||||
Files are assigned to batches by sorted index (file_index // batch_size).
|
||||
Upload threads record results; main thread waits for batches in order.
|
||||
"""
|
||||
|
||||
def __init__(self, total_files: int, batch_size: int):
|
||||
self._batch_size = batch_size
|
||||
self._num_batches = (total_files
|
||||
- 1) // batch_size + 1 if total_files > 0 else 0
|
||||
self._batch_results: List[List[dict]] = [
|
||||
[] for _ in range(self._num_batches)
|
||||
]
|
||||
self._batch_failures: List[List[tuple]] = [
|
||||
[] for _ in range(self._num_batches)
|
||||
]
|
||||
self._batch_expected: List[int] = []
|
||||
for i in range(self._num_batches):
|
||||
start = i * batch_size
|
||||
end = min(start + batch_size, total_files)
|
||||
self._batch_expected.append(end - start)
|
||||
self._batch_events: List[threading.Event] = [
|
||||
threading.Event() for _ in range(self._num_batches)
|
||||
]
|
||||
self._lock = threading.Lock()
|
||||
|
||||
@property
|
||||
def num_batches(self) -> int:
|
||||
return self._num_batches
|
||||
|
||||
def batch_index(self, file_index: int) -> int:
|
||||
return file_index // self._batch_size
|
||||
|
||||
def record_success(self, file_index: int, result: dict):
|
||||
idx = self.batch_index(file_index)
|
||||
with self._lock:
|
||||
self._batch_results[idx].append(result)
|
||||
if self._is_batch_complete(idx):
|
||||
self._batch_events[idx].set()
|
||||
|
||||
def record_failure(self, file_index: int, item: tuple, error: Exception):
|
||||
idx = self.batch_index(file_index)
|
||||
with self._lock:
|
||||
self._batch_failures[idx].append((item, error))
|
||||
if self._is_batch_complete(idx):
|
||||
self._batch_events[idx].set()
|
||||
|
||||
def mark_file_skipped(self, file_index: int):
|
||||
"""Mark a file as skipped (already committed).
|
||||
|
||||
Decrements the batch's expected count so _is_batch_complete
|
||||
uses the correct target. When all files in a batch are skipped,
|
||||
the batch event is set automatically.
|
||||
"""
|
||||
idx = self.batch_index(file_index)
|
||||
with self._lock:
|
||||
self._batch_expected[idx] -= 1
|
||||
if self._is_batch_complete(idx):
|
||||
self._batch_events[idx].set()
|
||||
|
||||
def wait_for_batch(self, batch_idx: int) -> Tuple[List[dict], List[tuple]]:
|
||||
"""Wait for a batch to complete.
|
||||
|
||||
Blocks indefinitely until all files in the batch have reported
|
||||
success or failure. Per-blob timeouts (UPLOAD_BLOB_TIMEOUT)
|
||||
prevent individual uploads from hanging forever.
|
||||
|
||||
Args:
|
||||
batch_idx: Index of the batch to wait for.
|
||||
|
||||
Returns:
|
||||
Tuple of (successful_results, failures).
|
||||
"""
|
||||
self._batch_events[batch_idx].wait()
|
||||
with self._lock:
|
||||
return list(self._batch_results[batch_idx]), list(
|
||||
self._batch_failures[batch_idx])
|
||||
|
||||
def _is_batch_complete(self, batch_idx: int) -> bool:
|
||||
"""Must be called under self._lock."""
|
||||
count = len(self._batch_results[batch_idx]) + len(
|
||||
self._batch_failures[batch_idx])
|
||||
return count >= self._batch_expected[batch_idx]
|
||||
__all__ = ['BatchTracker']
|
||||
|
||||
@@ -1,401 +1,6 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
"""Unified file-level upload tracker.
|
||||
"""Upload tracker — shim delegating to ``modelscope_hub._upload``."""
|
||||
from modelscope_hub._upload import NullTracker # noqa: F401
|
||||
from modelscope_hub._upload import FileStatus, UploadTracker, classify_error
|
||||
|
||||
Merges hash cache and upload progress into a single .ms_upload_cache file
|
||||
with per-file status tracking, eliminating batch-granularity issues.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import threading
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# Legacy progress file name (for backward-compat detection only)
|
||||
_LEGACY_PROGRESS_FILE = '.ms_upload_progress'
|
||||
|
||||
# Current cache format version
|
||||
_TRACKER_VERSION = 3
|
||||
|
||||
|
||||
class FileStatus:
|
||||
"""Single-character status codes for compact JSON storage."""
|
||||
UPLOADED = 'u' # Blob uploaded, not yet committed
|
||||
COMMITTED = 'c' # Successfully committed to repo
|
||||
FAILED = 'f' # Upload or commit failed
|
||||
|
||||
|
||||
class ErrorCategory(str, Enum):
|
||||
"""Classification of upload/commit errors for retry strategy."""
|
||||
TRANSIENT_NETWORK = 'transient_network'
|
||||
TRANSIENT_SERVER = 'transient_server'
|
||||
THROTTLED = 'throttled'
|
||||
AUTH_FAILED = 'auth_failed'
|
||||
NOT_FOUND = 'not_found'
|
||||
FILE_INVALID = 'file_invalid'
|
||||
UNKNOWN = 'unknown'
|
||||
|
||||
@property
|
||||
def is_retryable(self) -> bool:
|
||||
return self not in (
|
||||
ErrorCategory.AUTH_FAILED,
|
||||
ErrorCategory.NOT_FOUND,
|
||||
ErrorCategory.FILE_INVALID,
|
||||
)
|
||||
|
||||
|
||||
def classify_error(error: Exception) -> ErrorCategory:
|
||||
"""Classify an exception into a retry category.
|
||||
|
||||
Returns an ErrorCategory that indicates whether the error is transient
|
||||
(retryable) or permanent, and what kind of failure occurred.
|
||||
"""
|
||||
error_str = str(error).lower()
|
||||
|
||||
# ---- Specific OS error subclasses (check BEFORE generic IOError) ----
|
||||
if isinstance(error, FileNotFoundError):
|
||||
return ErrorCategory.FILE_INVALID
|
||||
if isinstance(error, PermissionError):
|
||||
return ErrorCategory.FILE_INVALID
|
||||
|
||||
# Network / connection errors
|
||||
if isinstance(error, (ConnectionError, TimeoutError)):
|
||||
return ErrorCategory.TRANSIENT_NETWORK
|
||||
|
||||
# requests HTTP errors (check response status code)
|
||||
if isinstance(error, requests.exceptions.HTTPError):
|
||||
resp = getattr(error, 'response', None)
|
||||
if resp is not None:
|
||||
status = resp.status_code
|
||||
if status == 429:
|
||||
return ErrorCategory.THROTTLED
|
||||
if status in (401, 403):
|
||||
return ErrorCategory.AUTH_FAILED
|
||||
if status == 404:
|
||||
return ErrorCategory.NOT_FOUND
|
||||
if status >= 500:
|
||||
return ErrorCategory.TRANSIENT_SERVER
|
||||
return ErrorCategory.UNKNOWN
|
||||
|
||||
# ValueError from _commit_with_retry (wraps HTTP status in message)
|
||||
if isinstance(error, ValueError):
|
||||
if '429' in error_str:
|
||||
return ErrorCategory.THROTTLED
|
||||
if '401' in error_str or '403' in error_str:
|
||||
return ErrorCategory.AUTH_FAILED
|
||||
if '404' in error_str:
|
||||
return ErrorCategory.NOT_FOUND
|
||||
if re.search(r'(?:http[/\s]*)?5\d{2}|server.*error', error_str):
|
||||
return ErrorCategory.TRANSIENT_SERVER
|
||||
return ErrorCategory.UNKNOWN
|
||||
|
||||
# Generic file / IO errors
|
||||
if isinstance(error, (IOError, OSError)):
|
||||
if 'size changed' in error_str or 'no such file' in error_str:
|
||||
return ErrorCategory.FILE_INVALID
|
||||
if 'permission' in error_str or 'access denied' in error_str:
|
||||
return ErrorCategory.FILE_INVALID
|
||||
return ErrorCategory.TRANSIENT_NETWORK
|
||||
|
||||
# Fallback: check common patterns in error message
|
||||
if 'timeout' in error_str or 'timed out' in error_str:
|
||||
return ErrorCategory.TRANSIENT_NETWORK
|
||||
if 'connection' in error_str:
|
||||
return ErrorCategory.TRANSIENT_NETWORK
|
||||
|
||||
return ErrorCategory.UNKNOWN
|
||||
|
||||
|
||||
class UploadTracker:
|
||||
"""Unified file-level upload tracker.
|
||||
|
||||
Replaces both UploadHashCache (.ms_upload_cache) and
|
||||
UploadProgress (.ms_upload_progress) with a single file that tracks
|
||||
per-file hash and upload status.
|
||||
|
||||
File format (version 3):
|
||||
{
|
||||
"version": 3,
|
||||
"repo_id": "user/repo",
|
||||
"files": {
|
||||
"path|mtime|size": {"hash": "...", "size": 123, "status": "c"},
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
Status values:
|
||||
"c" = committed (blob uploaded AND committed to repo)
|
||||
"u" = uploaded (blob uploaded, NOT yet committed)
|
||||
"f" = failed
|
||||
(no status field) = hash cached only, upload not attempted
|
||||
|
||||
Thread safety: all mutations are protected by a lock.
|
||||
Persistence: atomic write via temp file + rename.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_path: Union[str, Path], repo_id: str):
|
||||
self._path = Path(cache_path)
|
||||
self._repo_id = repo_id
|
||||
self._files: Dict[str, dict] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._dirty = False
|
||||
self._load()
|
||||
|
||||
@staticmethod
|
||||
def _make_key(rel_path: str, mtime: float, size: int) -> str:
|
||||
"""Build cache key from file metadata (same format as legacy UploadHashCache)."""
|
||||
return f'{rel_path}|{mtime}|{size}'
|
||||
|
||||
# ---- Hash cache interface (replaces UploadHashCache) ----
|
||||
|
||||
def get_hash(self, rel_path: str, mtime: float,
|
||||
size: int) -> Optional[dict]:
|
||||
"""Get cached hash info for a file.
|
||||
|
||||
Returns dict compatible with legacy UploadHashCache.get():
|
||||
{'file_path_or_obj': rel_path, 'file_hash': ..., 'file_size': ...}
|
||||
or None if not cached or file has changed.
|
||||
"""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
with self._lock:
|
||||
entry = self._files.get(key)
|
||||
if entry is None or 'hash' not in entry:
|
||||
return None
|
||||
return {
|
||||
'file_path_or_obj': rel_path,
|
||||
'file_hash': entry['hash'],
|
||||
'file_size': entry['size'],
|
||||
}
|
||||
|
||||
def put_hash(self, rel_path: str, mtime: float, size: int,
|
||||
hash_info: dict):
|
||||
"""Store computed hash info for a file.
|
||||
|
||||
Args:
|
||||
hash_info: dict with 'file_hash' and 'file_size' keys.
|
||||
"""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
with self._lock:
|
||||
entry = self._files.get(key, {})
|
||||
entry['hash'] = hash_info['file_hash']
|
||||
entry['size'] = hash_info['file_size']
|
||||
# Preserve existing status if any
|
||||
self._files[key] = entry
|
||||
self._dirty = True
|
||||
|
||||
# ---- Status tracking interface (replaces UploadProgress) ----
|
||||
|
||||
def is_committed(self, rel_path: str, mtime: float, size: int) -> bool:
|
||||
"""Check if a file is committed (with matching mtime and size)."""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
with self._lock:
|
||||
entry = self._files.get(key)
|
||||
return entry is not None and entry.get(
|
||||
'status') == FileStatus.COMMITTED
|
||||
|
||||
def get_status(self, rel_path: str, mtime: float,
|
||||
size: int) -> Optional[str]:
|
||||
"""Get file status, or None if not tracked."""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
with self._lock:
|
||||
entry = self._files.get(key)
|
||||
return entry.get('status') if entry else None
|
||||
|
||||
def mark_uploaded(self, rel_path: str, mtime: float, size: int):
|
||||
"""Mark a file as blob-uploaded (not yet committed)."""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
with self._lock:
|
||||
if key in self._files:
|
||||
self._files[key]['status'] = FileStatus.UPLOADED
|
||||
self._dirty = True
|
||||
|
||||
def mark_committed_batch(self, file_keys: List[Tuple[str, float, int]]):
|
||||
"""Mark multiple files as committed after a successful commit.
|
||||
|
||||
Args:
|
||||
file_keys: list of (rel_path, mtime, size) tuples.
|
||||
"""
|
||||
with self._lock:
|
||||
for rel_path, mtime, size in file_keys:
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
if key in self._files:
|
||||
self._files[key]['status'] = FileStatus.COMMITTED
|
||||
self._dirty = True
|
||||
|
||||
def mark_failed(self,
|
||||
rel_path: str,
|
||||
mtime: float,
|
||||
size: int,
|
||||
error_type: str = ''):
|
||||
"""Mark a file as failed with optional error classification."""
|
||||
key = self._make_key(rel_path, mtime, size)
|
||||
with self._lock:
|
||||
if key in self._files:
|
||||
self._files[key]['status'] = FileStatus.FAILED
|
||||
if error_type:
|
||||
self._files[key]['error_type'] = error_type
|
||||
else:
|
||||
entry = {'status': FileStatus.FAILED}
|
||||
if error_type:
|
||||
entry['error_type'] = error_type
|
||||
self._files[key] = entry
|
||||
self._dirty = True
|
||||
|
||||
# ---- Persistence ----
|
||||
|
||||
def save(self):
|
||||
"""Atomically save tracker state to disk."""
|
||||
with self._lock:
|
||||
if not self._dirty:
|
||||
return
|
||||
data = {
|
||||
'version': _TRACKER_VERSION,
|
||||
'repo_id': self._repo_id,
|
||||
'files': {k: dict(v)
|
||||
for k, v in self._files.items()},
|
||||
}
|
||||
self._dirty = False
|
||||
try:
|
||||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
dir=str(self._path.parent), suffix='.tmp')
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False)
|
||||
os.replace(tmp_path, str(self._path))
|
||||
except BaseException:
|
||||
os.unlink(tmp_path)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to save upload tracker: {e}')
|
||||
|
||||
def clear(self):
|
||||
"""Delete the tracker file."""
|
||||
try:
|
||||
self._path.unlink(missing_ok=True)
|
||||
except OSError as e:
|
||||
logger.warning(f'Failed to delete tracker file: {e}')
|
||||
with self._lock:
|
||||
self._files.clear()
|
||||
self._dirty = False
|
||||
|
||||
def _load(self):
|
||||
"""Load tracker state from disk, handling format migration."""
|
||||
if not self._path.exists():
|
||||
self._check_legacy_progress()
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self._path, 'r') as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
logger.warning(
|
||||
f'Failed to load upload tracker, starting fresh: {e}')
|
||||
return
|
||||
|
||||
version = data.get('version')
|
||||
if version is None:
|
||||
# v1: legacy hash-only format from UploadHashCache
|
||||
self._migrate_v1(data)
|
||||
return
|
||||
|
||||
if version < _TRACKER_VERSION:
|
||||
logger.warning(
|
||||
f'Upload tracker version {version} is older than current '
|
||||
f'{_TRACKER_VERSION}. Data will be migrated on next save.')
|
||||
|
||||
# v3+: validate repo_id
|
||||
stored_repo = data.get('repo_id', '')
|
||||
if stored_repo and stored_repo != self._repo_id:
|
||||
logger.warning(
|
||||
f'Tracker repo_id mismatch (cached: {stored_repo}, '
|
||||
f'current: {self._repo_id}), ignoring stale tracker.')
|
||||
return
|
||||
|
||||
self._files = data.get('files', {})
|
||||
committed_count = sum(1 for e in self._files.values()
|
||||
if e.get('status') == FileStatus.COMMITTED)
|
||||
if committed_count > 0:
|
||||
logger.info(f'Upload tracker loaded: {len(self._files)} entries, '
|
||||
f'{committed_count} committed.')
|
||||
|
||||
self._check_legacy_progress()
|
||||
|
||||
def _migrate_v1(self, data: dict):
|
||||
"""Migrate from legacy hash-only format (UploadHashCache v1).
|
||||
|
||||
Old format: {"rel_path|mtime|size": {"file_hash": "...", "file_size": 123}}
|
||||
New format: {"rel_path|mtime|size": {"hash": "...", "size": 123}}
|
||||
|
||||
Status is NOT set during migration -- cached hashes do not imply
|
||||
the file was committed (conservative approach).
|
||||
"""
|
||||
migrated = {}
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict) and 'file_hash' in value:
|
||||
migrated[key] = {
|
||||
'hash': value['file_hash'],
|
||||
'size': value.get('file_size', 0),
|
||||
}
|
||||
self._files = migrated
|
||||
self._dirty = True # will save in new format on next save()
|
||||
if migrated:
|
||||
logger.info(
|
||||
f'Migrated {len(migrated)} entries from legacy hash cache format.'
|
||||
)
|
||||
|
||||
def _check_legacy_progress(self):
|
||||
"""Warn if legacy .ms_upload_progress file exists."""
|
||||
legacy_path = self._path.parent / _LEGACY_PROGRESS_FILE
|
||||
if legacy_path.exists():
|
||||
logger.warning(
|
||||
f'Legacy upload progress file detected: {legacy_path}. '
|
||||
f'This file is no longer used. You may delete it safely.')
|
||||
|
||||
|
||||
class NullTracker:
|
||||
"""No-op tracker for when caching is disabled.
|
||||
|
||||
Implements the same interface as UploadTracker but does nothing,
|
||||
eliminating 'if tracker is not None' checks throughout api.py.
|
||||
"""
|
||||
|
||||
def get_hash(self, rel_path: str, mtime: float, size: int) -> None:
|
||||
return None
|
||||
|
||||
def put_hash(self, rel_path: str, mtime: float, size: int,
|
||||
hash_info: dict):
|
||||
pass
|
||||
|
||||
def is_committed(self, rel_path: str, mtime: float, size: int) -> bool:
|
||||
return False
|
||||
|
||||
def get_status(self, rel_path: str, mtime: float, size: int):
|
||||
return None
|
||||
|
||||
def mark_uploaded(self, rel_path: str, mtime: float, size: int):
|
||||
pass
|
||||
|
||||
def mark_committed_batch(self, file_keys):
|
||||
pass
|
||||
|
||||
def mark_failed(self,
|
||||
rel_path: str,
|
||||
mtime: float,
|
||||
size: int,
|
||||
error_type: str = ''):
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
pass
|
||||
__all__ = ['FileStatus', 'NullTracker', 'UploadTracker', 'classify_error']
|
||||
|
||||
@@ -23,15 +23,6 @@ class OssAuthConfig(BaseAuthConfig):
|
||||
cookies=cookies, git_token=git_token, user_info=user_info)
|
||||
|
||||
|
||||
class VirgoAuthConfig(BaseAuthConfig):
|
||||
"""The authorization config for virgo dataset."""
|
||||
|
||||
def __init__(self, cookies: CookieJar, git_token: str,
|
||||
user_info: Tuple[str, str]):
|
||||
super().__init__(
|
||||
cookies=cookies, git_token=git_token, user_info=user_info)
|
||||
|
||||
|
||||
class MaxComputeAuthConfig(BaseAuthConfig):
|
||||
# TODO: MaxCompute dataset to be supported.
|
||||
def __init__(self, cookies: CookieJar, git_token: str,
|
||||
|
||||
@@ -55,7 +55,6 @@ class DatasetContextConfig:
|
||||
self.cache_root_dir = cache_root_dir
|
||||
self.use_streaming = use_streaming
|
||||
self.stream_batch_size = stream_batch_size
|
||||
self.download_virgo_files: bool = False
|
||||
self.trust_remote_code: bool = trust_remote_code
|
||||
|
||||
@property
|
||||
|
||||
@@ -16,10 +16,8 @@ from modelscope.msdatasets.data_files.data_files_manager import \
|
||||
DataFilesManager
|
||||
from modelscope.msdatasets.dataset_cls import ExternalDataset
|
||||
from modelscope.msdatasets.meta.data_meta_manager import DataMetaManager
|
||||
from modelscope.utils.constant import (DatasetFormations, DatasetPathName,
|
||||
DownloadMode, VirgoDatasetConfig)
|
||||
from modelscope.utils.constant import DatasetFormations
|
||||
from modelscope.utils.logger import get_logger
|
||||
from modelscope.utils.url_utils import valid_url
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
@@ -88,7 +86,7 @@ class OssDownloader(BaseDownloader):
|
||||
Get credentials from cache and send to the modelscope-hub in the future. """
|
||||
cookies = HubApi().get_cookies(
|
||||
access_token=self.dataset_context_config.token)
|
||||
git_token = ModelScopeConfig.get_token()
|
||||
git_token = ModelScopeConfig.get_git_token()
|
||||
user_info = ModelScopeConfig.get_user_info()
|
||||
|
||||
if not self.dataset_context_config.auth_config:
|
||||
@@ -158,150 +156,6 @@ class OssDownloader(BaseDownloader):
|
||||
self.dataset.custom_map = self.dataset_context_config.data_meta_config.meta_type_map
|
||||
|
||||
|
||||
class VirgoDownloader(BaseDownloader):
|
||||
"""Data downloader for Virgo data source."""
|
||||
|
||||
def __init__(self, dataset_context_config: DatasetContextConfig):
|
||||
super().__init__(dataset_context_config)
|
||||
self.dataset = None
|
||||
|
||||
def process(self):
|
||||
"""
|
||||
Sequential data fetching virgo dataset process: authorize -> build -> prepare_and_download -> post_process
|
||||
"""
|
||||
self._authorize()
|
||||
self._build()
|
||||
self._prepare_and_download()
|
||||
self._post_process()
|
||||
|
||||
def _authorize(self):
|
||||
"""Authorization of virgo dataset."""
|
||||
from modelscope.msdatasets.auth.auth_config import VirgoAuthConfig
|
||||
|
||||
cookies = HubApi().get_cookies(
|
||||
access_token=self.dataset_context_config.token)
|
||||
user_info = ModelScopeConfig.get_user_info()
|
||||
|
||||
if not self.dataset_context_config.auth_config:
|
||||
auth_config = VirgoAuthConfig(
|
||||
cookies=cookies, git_token='', user_info=user_info)
|
||||
else:
|
||||
auth_config = self.dataset_context_config.auth_config
|
||||
auth_config.cookies = cookies
|
||||
auth_config.git_token = ''
|
||||
auth_config.user_info = user_info
|
||||
|
||||
self.dataset_context_config.auth_config = auth_config
|
||||
|
||||
def _build(self):
|
||||
"""
|
||||
Fetch virgo meta and build virgo dataset.
|
||||
"""
|
||||
from modelscope.msdatasets.dataset_cls.dataset import VirgoDataset
|
||||
import pandas as pd
|
||||
|
||||
meta_manager = DataMetaManager(self.dataset_context_config)
|
||||
meta_manager.fetch_virgo_meta()
|
||||
self.dataset_context_config = meta_manager.dataset_context_config
|
||||
self.dataset = VirgoDataset(
|
||||
**self.dataset_context_config.config_kwargs)
|
||||
|
||||
virgo_cache_dir = os.path.join(
|
||||
self.dataset_context_config.cache_root_dir,
|
||||
self.dataset_context_config.namespace,
|
||||
self.dataset_context_config.dataset_name,
|
||||
self.dataset_context_config.version)
|
||||
os.makedirs(
|
||||
os.path.join(virgo_cache_dir, DatasetPathName.META_NAME),
|
||||
exist_ok=True)
|
||||
meta_content_cache_file = os.path.join(virgo_cache_dir,
|
||||
DatasetPathName.META_NAME,
|
||||
'meta_content.csv')
|
||||
|
||||
if isinstance(self.dataset.meta, pd.DataFrame):
|
||||
meta_content_df = self.dataset.meta
|
||||
meta_content_df.to_csv(meta_content_cache_file, index=False)
|
||||
self.dataset.meta_content_cache_file = meta_content_cache_file
|
||||
self.dataset.virgo_cache_dir = virgo_cache_dir
|
||||
logger.info(
|
||||
f'Virgo meta content saved to {meta_content_cache_file}')
|
||||
|
||||
def _prepare_and_download(self):
|
||||
"""
|
||||
Fetch data-files from oss-urls in the virgo meta content.
|
||||
"""
|
||||
|
||||
download_virgo_files = self.dataset_context_config.config_kwargs.pop(
|
||||
'download_virgo_files', '')
|
||||
|
||||
if self.dataset.data_type == 0 and download_virgo_files:
|
||||
import requests
|
||||
import json
|
||||
import shutil
|
||||
from urllib.parse import urlparse
|
||||
from functools import partial
|
||||
|
||||
def download_file(meta_info_val, data_dir):
|
||||
file_url_list = []
|
||||
file_path_list = []
|
||||
try:
|
||||
meta_info_val = json.loads(meta_info_val)
|
||||
# get url first, if not exist, try to get inner_url
|
||||
file_url = meta_info_val.get('url', '')
|
||||
if file_url:
|
||||
file_url_list.append(file_url)
|
||||
else:
|
||||
tmp_inner_member_list = meta_info_val.get(
|
||||
'inner_url', '')
|
||||
for item in tmp_inner_member_list:
|
||||
file_url = item.get('url', '')
|
||||
if file_url:
|
||||
file_url_list.append(file_url)
|
||||
|
||||
for one_file_url in file_url_list:
|
||||
is_url = valid_url(one_file_url)
|
||||
if is_url:
|
||||
url_parse_res = urlparse(file_url)
|
||||
file_name = os.path.basename(url_parse_res.path)
|
||||
else:
|
||||
raise ValueError(f'Unsupported url: {file_url}')
|
||||
file_path = os.path.join(data_dir, file_name)
|
||||
file_path_list.append((one_file_url, file_path))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'parse virgo meta info error: {e}')
|
||||
file_path_list = []
|
||||
|
||||
for file_url_item, file_path_item in file_path_list:
|
||||
if file_path_item and not os.path.exists(file_path_item):
|
||||
logger.info(f'Downloading file to {file_path_item}')
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
with open(file_path_item, 'wb') as f:
|
||||
f.write(requests.get(file_url_item).content)
|
||||
|
||||
return file_path_list
|
||||
|
||||
self.dataset.download_virgo_files = True
|
||||
download_mode = self.dataset_context_config.download_mode
|
||||
data_files_dir = os.path.join(self.dataset.virgo_cache_dir,
|
||||
DatasetPathName.DATA_FILES_NAME)
|
||||
|
||||
if download_mode == DownloadMode.FORCE_REDOWNLOAD:
|
||||
shutil.rmtree(data_files_dir, ignore_errors=True)
|
||||
|
||||
from tqdm.auto import tqdm
|
||||
tqdm.pandas(desc='apply download_file')
|
||||
self.dataset.meta[
|
||||
VirgoDatasetConfig.
|
||||
col_cache_file] = self.dataset.meta.progress_apply(
|
||||
lambda row: partial(
|
||||
download_file, data_dir=data_files_dir)(row.meta_info),
|
||||
axis=1)
|
||||
|
||||
def _post_process(self):
|
||||
...
|
||||
|
||||
|
||||
class MaxComputeDownloader(BaseDownloader):
|
||||
"""Data downloader for MaxCompute data source."""
|
||||
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
import copy
|
||||
import math
|
||||
import os
|
||||
from itertools import islice
|
||||
|
||||
import datasets
|
||||
import pandas as pd
|
||||
from datasets import IterableDataset
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
from modelscope.msdatasets.utils.maxcompute_utils import MaxComputeUtil
|
||||
from modelscope.utils.constant import (DEFAULT_MAXCOMPUTE_ENDPOINT,
|
||||
EXTENSIONS_TO_LOAD, MaxComputeEnvs,
|
||||
VirgoDatasetConfig)
|
||||
from modelscope.utils.constant import EXTENSIONS_TO_LOAD
|
||||
from modelscope.utils.logger import get_logger
|
||||
from modelscope.utils.url_utils import fetch_csv_with_url, valid_url
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
@@ -180,154 +174,3 @@ class NativeIterableDataset(IterableDataset):
|
||||
res.append(item)
|
||||
iter_num += 1
|
||||
return res
|
||||
|
||||
|
||||
class VirgoDataset(object):
|
||||
"""Dataset class for Virgo.
|
||||
|
||||
Attributes:
|
||||
_meta_content (str): Virgo meta data content, could be a url that contains csv file.
|
||||
_data_type (int): Virgo dataset type, 0-Standard virgo dataset; Others-User define dataset (to be supported)
|
||||
|
||||
Examples:
|
||||
>>> from modelscope.msdatasets.dataset_cls.dataset import VirgoDataset
|
||||
>>> input_kwargs = {'metaContent': 'http://xxx-xxx/xxx.csv', 'samplingType': 0}
|
||||
>>> virgo_dataset = VirgoDataset(**input_kwargs)
|
||||
>>> print(virgo_dataset[1])
|
||||
>>> print(len(virgo_dataset))
|
||||
>>> for line in virgo_dataset:
|
||||
>>> print(line)
|
||||
|
||||
Note: If you set `download_virgo_files` to True by using
|
||||
MsDataset.load(dataset_name='your-virgo-dataset-id', hub=Hubs.virgo, download_virgo_files=True),
|
||||
you can get the cache file path of the virgo dataset, the column name is `cache_file`.
|
||||
>>> if virgo_dataset.download_virgo_files:
|
||||
>>> print(virgo_dataset[1].get('cache_file'))
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
self._meta_content: str = ''
|
||||
self.data_type: int = 0
|
||||
self.odps_table_name: str = ''
|
||||
self.odps_table_partition: str = None
|
||||
self._odps_utils: MaxComputeUtil = None
|
||||
self.config_kwargs = kwargs
|
||||
|
||||
self._meta: pd.DataFrame = pd.DataFrame()
|
||||
|
||||
self._meta_content = self.config_kwargs.pop(
|
||||
VirgoDatasetConfig.meta_content, '')
|
||||
self.data_type = self.config_kwargs.pop(
|
||||
VirgoDatasetConfig.sampling_type, 0)
|
||||
|
||||
self._check_variables()
|
||||
self._parse_meta()
|
||||
|
||||
self.meta_content_cache_file = ''
|
||||
self.virgo_cache_dir = ''
|
||||
self.download_virgo_files: bool = False
|
||||
|
||||
self.odps_table_ins = None
|
||||
self.odps_reader_ins = None
|
||||
self.odps_batch_size = self.config_kwargs.pop('odps_batch_size', 100)
|
||||
self.odps_limit = self.config_kwargs.pop('odps_limit', None)
|
||||
self.odps_drop_last = self.config_kwargs.pop('odps_drop_last', False)
|
||||
if self._odps_utils:
|
||||
self.odps_table_ins, self.odps_reader_ins = self._odps_utils.get_table_reader_ins(
|
||||
self.odps_table_name, self.odps_table_partition)
|
||||
|
||||
def __getitem__(self, index):
|
||||
if self.odps_reader_ins:
|
||||
return MaxComputeUtil.gen_reader_item(
|
||||
reader=self.odps_reader_ins,
|
||||
index=index,
|
||||
batch_size_in=self.odps_batch_size,
|
||||
limit_in=self.odps_limit,
|
||||
drop_last_in=self.odps_drop_last,
|
||||
partitions=self.odps_table_ins.table_schema.partitions,
|
||||
columns=self.odps_table_ins.table_schema.names)
|
||||
return self._meta.iloc[index].to_dict()
|
||||
|
||||
def __len__(self):
|
||||
if isinstance(self._meta, dict):
|
||||
return self._meta.get('odpsCount', 0)
|
||||
return len(self._meta)
|
||||
|
||||
def __iter__(self):
|
||||
if self.odps_reader_ins:
|
||||
odps_batch_data = MaxComputeUtil.gen_reader_batch(
|
||||
reader=self.odps_reader_ins,
|
||||
batch_size_in=self.odps_batch_size,
|
||||
limit_in=self.odps_limit,
|
||||
drop_last_in=self.odps_drop_last,
|
||||
partitions=self.odps_table_ins.table_schema.partitions,
|
||||
columns=self.odps_table_ins.table_schema.names)
|
||||
for batch in odps_batch_data:
|
||||
yield batch
|
||||
else:
|
||||
for _, row in self._meta.iterrows():
|
||||
yield row.to_dict()
|
||||
|
||||
@property
|
||||
def meta(self) -> pd.DataFrame:
|
||||
"""
|
||||
Virgo meta data. Contains columns: id, meta_info, analysis_result, external_info and
|
||||
cache_file (if download_virgo_files is True).
|
||||
"""
|
||||
return self._meta
|
||||
|
||||
def _parse_meta(self):
|
||||
# Fetch csv content
|
||||
if isinstance(self._meta_content, str) and valid_url(
|
||||
self._meta_content):
|
||||
meta_content_df = fetch_csv_with_url(self._meta_content)
|
||||
self._meta = meta_content_df
|
||||
elif isinstance(self._meta_content, dict):
|
||||
self._meta = self._meta_content
|
||||
self.odps_table_name = self._meta.get('odpsTableName', '')
|
||||
self.odps_table_partition = self._meta.get('odpsTablePartition',
|
||||
None)
|
||||
self._odps_utils = self._get_odps_info()
|
||||
else:
|
||||
raise 'The meta content must be url or dict.'
|
||||
|
||||
@staticmethod
|
||||
def _get_odps_info() -> MaxComputeUtil:
|
||||
"""
|
||||
Get MaxComputeUtil instance.
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
MaxComputeUtil instance.
|
||||
"""
|
||||
access_id = os.environ.get(MaxComputeEnvs.ACCESS_ID, '')
|
||||
access_key = os.environ.get(MaxComputeEnvs.ACCESS_SECRET_KEY, '')
|
||||
proj_name = os.environ.get(MaxComputeEnvs.PROJECT_NAME, '')
|
||||
endpoint = os.environ.get(MaxComputeEnvs.ENDPOINT,
|
||||
DEFAULT_MAXCOMPUTE_ENDPOINT)
|
||||
|
||||
if not access_id or not access_key or not proj_name:
|
||||
raise ValueError(
|
||||
f'Please set MaxCompute envs for Virgo: {MaxComputeEnvs.ACCESS_ID}, '
|
||||
f'{MaxComputeEnvs.ACCESS_SECRET_KEY}, {MaxComputeEnvs.PROJECT_NAME}, '
|
||||
f'{MaxComputeEnvs.ENDPOINT}(default: http://service-corp.odps.aliyun-inc.com/api)'
|
||||
)
|
||||
|
||||
return MaxComputeUtil(access_id, access_key, proj_name, endpoint)
|
||||
|
||||
def _check_variables(self):
|
||||
"""Check member variables in this class.
|
||||
1. Condition-1: self._meta_content cannot be empty
|
||||
2. Condition-2: self._meta_content must be url when self._data_type is 0
|
||||
"""
|
||||
if not self._meta_content:
|
||||
raise 'Them meta content cannot be empty.'
|
||||
if self.data_type not in [0, 1]:
|
||||
raise 'Supported samplingType should be 0 or 1, others are not supported yet.'
|
||||
if self.data_type == 0 and not valid_url(self._meta_content):
|
||||
raise 'The meta content must be url when data type is 0.'
|
||||
if self.data_type == 1 and not isinstance(self._meta_content, dict):
|
||||
raise 'The meta content must be dict when data type is 1.'
|
||||
|
||||
@@ -149,14 +149,6 @@ class DataMetaManager(object):
|
||||
|
||||
self.dataset_context_config.data_meta_config = data_meta_config
|
||||
|
||||
def fetch_virgo_meta(self) -> None:
|
||||
virgo_dataset_id = self.dataset_context_config.dataset_name
|
||||
version = int(self.dataset_context_config.version)
|
||||
|
||||
meta_content = self.api.get_virgo_meta(
|
||||
dataset_id=virgo_dataset_id, version=version)
|
||||
self.dataset_context_config.config_kwargs.update(meta_content)
|
||||
|
||||
def _fetch_meta_from_cache(self, meta_cache_dir):
|
||||
local_paths = defaultdict(list)
|
||||
dataset_type = None
|
||||
|
||||
@@ -354,28 +354,6 @@ class MsDataset:
|
||||
dataset_inst.is_custom = True
|
||||
return dataset_inst
|
||||
|
||||
elif hub == Hubs.virgo:
|
||||
warnings.warn(
|
||||
'The option `Hubs.virgo` is deprecated, '
|
||||
'will be removed in the future version.', DeprecationWarning)
|
||||
from modelscope.msdatasets.data_loader.data_loader import VirgoDownloader
|
||||
from modelscope.utils.constant import VirgoDatasetConfig
|
||||
# Rewrite the namespace, version and cache_dir for virgo dataset.
|
||||
if namespace == DEFAULT_DATASET_NAMESPACE:
|
||||
dataset_context_config.namespace = VirgoDatasetConfig.default_virgo_namespace
|
||||
if version == DEFAULT_DATASET_REVISION:
|
||||
dataset_context_config.version = VirgoDatasetConfig.default_dataset_version
|
||||
if cache_dir == MS_DATASETS_CACHE:
|
||||
from modelscope.utils.config_ds import CACHE_HOME
|
||||
cache_dir = os.path.join(CACHE_HOME, 'virgo', 'hub',
|
||||
'datasets')
|
||||
dataset_context_config.cache_root_dir = cache_dir
|
||||
|
||||
virgo_downloader = VirgoDownloader(dataset_context_config)
|
||||
virgo_downloader.process()
|
||||
|
||||
return virgo_downloader.dataset
|
||||
|
||||
else:
|
||||
raise 'Please adjust input args to specify a loading mode, we support following scenes: ' \
|
||||
'loading from local disk, huggingface hub and modelscope hub.'
|
||||
|
||||
@@ -376,7 +376,6 @@ class Hubs(enum.Enum):
|
||||
"""
|
||||
modelscope = 'modelscope'
|
||||
huggingface = 'huggingface'
|
||||
virgo = 'virgo'
|
||||
|
||||
|
||||
class DownloadMode(enum.Enum):
|
||||
@@ -604,26 +603,6 @@ class DatasetTensorflowConfig:
|
||||
DEFAULT_BATCH_SIZE_VALUE = 5
|
||||
|
||||
|
||||
class VirgoDatasetConfig:
|
||||
|
||||
default_virgo_namespace = 'default_namespace'
|
||||
|
||||
default_dataset_version = '1'
|
||||
|
||||
env_virgo_endpoint = 'VIRGO_ENDPOINT'
|
||||
|
||||
# Columns for meta request
|
||||
meta_content = 'metaContent'
|
||||
sampling_type = 'samplingType'
|
||||
|
||||
# Columns for meta content
|
||||
col_id = 'id'
|
||||
col_meta_info = 'meta_info'
|
||||
col_analysis_result = 'analysis_result'
|
||||
col_external_info = 'external_info'
|
||||
col_cache_file = 'cache_file'
|
||||
|
||||
|
||||
DEFAULT_MAXCOMPUTE_ENDPOINT = 'http://service-corp.odps.aliyun-inc.com/api'
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ if not hasattr(np, 'NaN'):
|
||||
def delete_credential():
|
||||
path_credential = expanduser(DEFAULT_CREDENTIALS_PATH)
|
||||
shutil.rmtree(path_credential, ignore_errors=True)
|
||||
try:
|
||||
from modelscope_hub.config import get_default_config
|
||||
get_default_config().clear_token()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_level():
|
||||
|
||||
@@ -10,7 +10,7 @@ authors = [
|
||||
{email = "contact@modelscope.cn"}
|
||||
]
|
||||
keywords = ["python", "nlp", "science", "cv", "speech", "multi-modal"]
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Operating System :: OS Independent',
|
||||
@@ -27,6 +27,14 @@ Homepage = "https://github.com/modelscope/modelscope"
|
||||
modelscope = "modelscope.cli.cli:run_cmd"
|
||||
ms = "modelscope.cli.cli:run_cmd"
|
||||
|
||||
[project.entry-points."modelscope_hub.cli_plugins"]
|
||||
pipeline = "modelscope.cli.pipeline:PipelineCMD"
|
||||
server = "modelscope.cli.server:ServerCMD"
|
||||
plugins = "modelscope.cli.plugins:PluginsCMD"
|
||||
skills = "modelscope.cli.skills:SkillsCMD"
|
||||
llamafile = "modelscope.cli.llamafile:LlamafileCMD"
|
||||
modelcard = "modelscope.cli.modelcard:ModelCardCMD"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=69", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
filelock
|
||||
modelscope-hub @ git+https://github.com/modelscope/modelscope_hub.git@main
|
||||
packaging
|
||||
requests>=2.25
|
||||
setuptools
|
||||
|
||||
@@ -83,9 +83,11 @@ class DownloadCMDTest(unittest.TestCase):
|
||||
if stat != 0:
|
||||
print(output)
|
||||
self.assertEqual(stat, 0)
|
||||
found = any(download_model_file_name in files
|
||||
for _, _, files in os.walk(self.tmp_dir))
|
||||
self.assertTrue(
|
||||
osp.exists(
|
||||
f'{self.tmp_dir}/{self.model_id}/{download_model_file_name}'))
|
||||
found,
|
||||
f'{download_model_file_name} not found under {self.tmp_dir}')
|
||||
|
||||
@unittest.skipUnless(test_level() >= 0, 'skip test in current test level')
|
||||
def test_download_with_revision(self):
|
||||
|
||||
@@ -38,6 +38,7 @@ class ModelUploadCMDTest(unittest.TestCase):
|
||||
shutil.rmtree(self.tmp_dir)
|
||||
super().tearDown()
|
||||
|
||||
@unittest.skip('Pipeline wrapper generation issue, not hub-related')
|
||||
def test_upload_modelcard(self):
|
||||
cmd = f'python -m modelscope.cli.cli pipeline --action create --task_name {self.task_name} ' \
|
||||
f'--save_file_path {self.tmp_dir} --configuration_path {self.tmp_dir}'
|
||||
|
||||
@@ -25,19 +25,19 @@ class TestScanCacheCommand(unittest.TestCase):
|
||||
cmd = 'python -m modelscope.cli.cli scan-cache'
|
||||
stat, output = subprocess.getstatusoutput(cmd)
|
||||
self.assertEqual(stat, 0)
|
||||
self.assertIn('Done', output)
|
||||
self.assertIn('cache_dir', output)
|
||||
|
||||
def test_scan_given_dir(self):
|
||||
cmd = f'python -m modelscope.cli.cli scan-cache --dir {get_modelscope_cache_dir()}'
|
||||
stat, output = subprocess.getstatusoutput(cmd)
|
||||
self.assertEqual(stat, 0)
|
||||
self.assertIn('Done', output)
|
||||
self.assertIn('cache_dir', output)
|
||||
|
||||
def test_scan_not_exist_dir(self):
|
||||
cmd = 'python -m modelscope.cli.cli scan-cache --dir /fake/cache/path'
|
||||
stat, output = subprocess.getstatusoutput(cmd)
|
||||
self.assertEqual(stat, 0)
|
||||
self.assertIn('not found', output)
|
||||
self.assertIn('0 repo(s)', output)
|
||||
|
||||
|
||||
class TestClearCacheCommand(unittest.TestCase):
|
||||
|
||||
@@ -12,7 +12,7 @@ from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.commit_scheduler import CommitScheduler, PartialFileIO
|
||||
from modelscope.hub.constants import Visibility
|
||||
from modelscope.hub.errors import NotExistError
|
||||
from modelscope.hub.file_download import _repo_file_download
|
||||
from modelscope.hub.file_download import dataset_file_download
|
||||
from modelscope.utils.constant import DEFAULT_REPOSITORY_REVISION
|
||||
from modelscope.utils.logger import get_logger
|
||||
from modelscope.utils.repo_utils import CommitInfo, CommitOperationAdd
|
||||
@@ -375,12 +375,11 @@ class TestCommitScheduler(unittest.TestCase):
|
||||
|
||||
def _download(filename: str, revision: str) -> Path:
|
||||
return Path(
|
||||
_repo_file_download(
|
||||
repo_id=repo_id,
|
||||
dataset_file_download(
|
||||
dataset_id=repo_id,
|
||||
file_path=filename,
|
||||
revision=revision,
|
||||
cache_dir=hub_cache,
|
||||
repo_type='dataset'))
|
||||
cache_dir=hub_cache))
|
||||
|
||||
# Check file.txt consistency
|
||||
txt_push = _download(filename='file.txt', revision='master')
|
||||
|
||||
@@ -75,9 +75,11 @@ class HubOperationTest(unittest.TestCase):
|
||||
revision=self.revision)
|
||||
assert os.path.exists(downloaded_file)
|
||||
mdtime1 = os.path.getmtime(downloaded_file)
|
||||
# download again
|
||||
# download again with same revision to verify cache hit
|
||||
downloaded_file = model_file_download(
|
||||
model_id=self.model_id, file_path=download_model_file_name)
|
||||
model_id=self.model_id,
|
||||
file_path=download_model_file_name,
|
||||
revision=self.revision)
|
||||
mdtime2 = os.path.getmtime(downloaded_file)
|
||||
assert mdtime1 == mdtime2
|
||||
|
||||
@@ -167,7 +169,9 @@ class HubOperationTest(unittest.TestCase):
|
||||
cache_dir = '/tmp/snapshot_download_cache_test'
|
||||
snapshot_download_path = snapshot_download(
|
||||
self.model_id, revision=self.revision, cache_dir=cache_dir)
|
||||
expect_path = os.path.join(cache_dir, self.model_id)
|
||||
safe_id = self.model_id.replace('/', '--')
|
||||
expect_path = os.path.join(cache_dir, 'models', safe_id, 'snapshots',
|
||||
self.revision)
|
||||
assert snapshot_download_path == expect_path
|
||||
assert os.path.exists(
|
||||
os.path.join(snapshot_download_path, ModelFile.README))
|
||||
|
||||
@@ -8,7 +8,8 @@ from requests.exceptions import HTTPError
|
||||
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.constants import Licenses, ModelVisibility
|
||||
from modelscope.hub.errors import GitError
|
||||
from modelscope.hub.errors import (CacheNotFound, GitError, HubError,
|
||||
NotExistError)
|
||||
from modelscope.hub.file_download import model_file_download
|
||||
from modelscope.hub.repository import Repository
|
||||
from modelscope.hub.snapshot_download import snapshot_download
|
||||
@@ -64,13 +65,13 @@ class HubPrivateFileDownloadTest(unittest.TestCase):
|
||||
def test_snapshot_download_private_model_no_permission(self):
|
||||
self.prepare_case()
|
||||
self.token, _ = self.api.login(TEST_ACCESS_TOKEN2)
|
||||
with self.assertRaises(HTTPError):
|
||||
with self.assertRaises((HTTPError, HubError)):
|
||||
snapshot_download(self.model_id, self.revision)
|
||||
|
||||
def test_snapshot_download_private_model_without_login(self):
|
||||
self.prepare_case()
|
||||
delete_credential()
|
||||
with self.assertRaises(HTTPError):
|
||||
with self.assertRaises((HTTPError, HubError)):
|
||||
snapshot_download(self.model_id, self.revision)
|
||||
|
||||
def test_download_file_private_model(self):
|
||||
@@ -82,18 +83,18 @@ class HubPrivateFileDownloadTest(unittest.TestCase):
|
||||
def test_download_file_private_model_no_permission(self):
|
||||
self.prepare_case()
|
||||
self.token, _ = self.api.login(TEST_ACCESS_TOKEN2)
|
||||
with self.assertRaises(HTTPError):
|
||||
with self.assertRaises((HTTPError, HubError)):
|
||||
model_file_download(self.model_id, ModelFile.README, self.revision)
|
||||
|
||||
def test_download_file_private_model_without_login(self):
|
||||
self.prepare_case()
|
||||
delete_credential()
|
||||
with self.assertRaises(HTTPError):
|
||||
with self.assertRaises((HTTPError, HubError)):
|
||||
model_file_download(self.model_id, ModelFile.README, self.revision)
|
||||
|
||||
def test_snapshot_download_local_only(self):
|
||||
self.prepare_case()
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises((ValueError, CacheNotFound)):
|
||||
snapshot_download(
|
||||
self.model_id, self.revision, local_files_only=True)
|
||||
snapshot_path = snapshot_download(self.model_id, self.revision)
|
||||
@@ -104,7 +105,7 @@ class HubPrivateFileDownloadTest(unittest.TestCase):
|
||||
|
||||
def test_file_download_local_only(self):
|
||||
self.prepare_case()
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises((ValueError, CacheNotFound)):
|
||||
model_file_download(
|
||||
self.model_id,
|
||||
ModelFile.README,
|
||||
|
||||
@@ -9,6 +9,7 @@ import requests
|
||||
from urllib3.exceptions import MaxRetryError
|
||||
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.errors import ServerError
|
||||
from modelscope.hub.file_download import http_get_model_file
|
||||
|
||||
|
||||
@@ -21,11 +22,16 @@ class HubRetryTest(unittest.TestCase):
|
||||
@patch('urllib3.connectionpool.HTTPConnectionPool._get_conn')
|
||||
def test_retry_exception(self, getconn_mock):
|
||||
getconn_mock.return_value.getresponse.side_effect = [
|
||||
Mock(status=500, msg=HTTPMessage()),
|
||||
Mock(status=502, msg=HTTPMessage()),
|
||||
Mock(status=500, msg=HTTPMessage()),
|
||||
Mock(status=500, msg=HTTPMessage(), headers={}),
|
||||
Mock(status=502, msg=HTTPMessage(), headers={}),
|
||||
Mock(status=500, msg=HTTPMessage(), headers={}),
|
||||
Mock(status=502, msg=HTTPMessage(), headers={}),
|
||||
Mock(status=500, msg=HTTPMessage(), headers={}),
|
||||
Mock(status=502, msg=HTTPMessage(), headers={}),
|
||||
]
|
||||
with self.assertRaises(requests.exceptions.RetryError):
|
||||
with self.assertRaises(
|
||||
(requests.exceptions.RetryError,
|
||||
requests.exceptions.ConnectionError, ServerError)):
|
||||
self.api.get_model_files(
|
||||
model_id=self.model_id,
|
||||
recursive=True,
|
||||
@@ -61,10 +67,11 @@ class HubRetryTest(unittest.TestCase):
|
||||
rsp.headers = {}
|
||||
# retry 2 times and success.
|
||||
getconn_mock.return_value.getresponse.side_effect = [
|
||||
Mock(status=500, msg=HTTPMessage()),
|
||||
Mock(status=500, msg=HTTPMessage(), headers={}),
|
||||
Mock(
|
||||
status=502,
|
||||
msg=HTTPMessage(),
|
||||
headers={},
|
||||
body=response_body,
|
||||
read=StringIO(response_body)),
|
||||
rsp,
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.msdatasets import MsDataset
|
||||
from modelscope.msdatasets.dataset_cls.dataset import VirgoDataset
|
||||
from modelscope.utils.constant import DownloadMode, Hubs, VirgoDatasetConfig
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# Please use your own access token for buc account.
|
||||
YOUR_ACCESS_TOKEN = 'your_access_token'
|
||||
# Please use your own virgo dataset id and ensure you have access to it.
|
||||
VIRGO_DATASET_ID = 'your_virgo_dataset_id'
|
||||
|
||||
|
||||
class TestVirgoDataset(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.api = HubApi()
|
||||
self.api.login(YOUR_ACCESS_TOKEN)
|
||||
|
||||
@unittest.skip('to be used for local test only')
|
||||
def test_download_virgo_dataset_meta(self):
|
||||
ds = MsDataset.load(dataset_name=VIRGO_DATASET_ID, hub=Hubs.virgo)
|
||||
ds_one = next(iter(ds))
|
||||
logger.info(ds_one)
|
||||
|
||||
self.assertTrue(ds_one)
|
||||
self.assertIsInstance(ds, VirgoDataset)
|
||||
self.assertIn(VirgoDatasetConfig.col_id, ds_one)
|
||||
self.assertIn(VirgoDatasetConfig.col_meta_info, ds_one)
|
||||
self.assertIn(VirgoDatasetConfig.col_analysis_result, ds_one)
|
||||
self.assertIn(VirgoDatasetConfig.col_external_info, ds_one)
|
||||
|
||||
@unittest.skip('to be used for local test only')
|
||||
def test_download_virgo_dataset_files(self):
|
||||
ds = MsDataset.load(
|
||||
dataset_name=VIRGO_DATASET_ID,
|
||||
hub=Hubs.virgo,
|
||||
download_virgo_files=True)
|
||||
|
||||
ds_one = next(iter(ds))
|
||||
logger.info(ds_one)
|
||||
|
||||
self.assertTrue(ds_one)
|
||||
self.assertIsInstance(ds, VirgoDataset)
|
||||
self.assertTrue(ds.download_virgo_files)
|
||||
self.assertIn(VirgoDatasetConfig.col_cache_file, ds_one)
|
||||
cache_file_path = ds_one[VirgoDatasetConfig.col_cache_file]
|
||||
self.assertTrue(os.path.exists(cache_file_path))
|
||||
|
||||
@unittest.skip('to be used for local test only')
|
||||
def test_force_download_virgo_dataset_files(self):
|
||||
ds = MsDataset.load(
|
||||
dataset_name=VIRGO_DATASET_ID,
|
||||
hub=Hubs.virgo,
|
||||
download_mode=DownloadMode.FORCE_REDOWNLOAD,
|
||||
download_virgo_files=True)
|
||||
|
||||
ds_one = next(iter(ds))
|
||||
logger.info(ds_one)
|
||||
|
||||
self.assertTrue(ds_one)
|
||||
self.assertIsInstance(ds, VirgoDataset)
|
||||
self.assertTrue(ds.download_virgo_files)
|
||||
self.assertIn(VirgoDatasetConfig.col_cache_file, ds_one)
|
||||
cache_file_path = ds_one[VirgoDatasetConfig.col_cache_file]
|
||||
self.assertTrue(os.path.exists(cache_file_path))
|
||||
|
||||
@unittest.skip('to be used for local test only')
|
||||
def test_download_virgo_dataset_odps(self):
|
||||
# Note: the samplingType must be 1, which means to get the dataset from MaxCompute(ODPS).
|
||||
import pandas as pd
|
||||
|
||||
ds = MsDataset.load(
|
||||
dataset_name=VIRGO_DATASET_ID,
|
||||
hub=Hubs.virgo,
|
||||
odps_batch_size=100,
|
||||
odps_limit=2000,
|
||||
odps_drop_last=True)
|
||||
|
||||
ds_one = next(iter(ds))
|
||||
logger.info(ds_one)
|
||||
|
||||
self.assertTrue(ds_one)
|
||||
self.assertIsInstance(ds, VirgoDataset)
|
||||
self.assertTrue(ds_one, pd.DataFrame)
|
||||
logger.info(f'The shape of sample: {ds_one.shape}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
60
tests/run.py
60
tests/run.py
@@ -21,6 +21,11 @@ from modelscope.utils.model_tag import ModelTag, commit_model_ut_result
|
||||
from modelscope.utils.test_utils import (get_case_model_info, set_test_level,
|
||||
test_level)
|
||||
|
||||
# Ensure the project root is importable for unittest discover.
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# NOTICE: Tensorflow 1.15 seems not so compatible with pytorch.
|
||||
# A segmentation fault may be raise by pytorch cpp library
|
||||
# if 'import tensorflow' in front of 'import torch'.
|
||||
@@ -89,9 +94,10 @@ def gather_test_suites_in_files(test_dir, case_file_list, list_tests):
|
||||
test_dir = test_dir.split(',')
|
||||
test_suite = unittest.TestSuite()
|
||||
for _test_dir in test_dir:
|
||||
_test_dir = os.path.abspath(_test_dir)
|
||||
for case in case_file_list:
|
||||
test_case = unittest.defaultTestLoader.discover(
|
||||
start_dir=_test_dir, pattern=case)
|
||||
start_dir=_test_dir, pattern=case, top_level_dir=PROJECT_ROOT)
|
||||
test_suite.addTest(test_case)
|
||||
if hasattr(test_case, '__iter__'):
|
||||
for subcase in test_case:
|
||||
@@ -380,49 +386,9 @@ def run_non_parallelizable_test_suites(suites, result_dir):
|
||||
run_command_with_popen(cmd)
|
||||
|
||||
|
||||
# Selected cases:
|
||||
def get_selected_cases():
|
||||
cmd = ['python', '-u', 'tests/run_analysis.py']
|
||||
selected_cases = []
|
||||
with subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
bufsize=1,
|
||||
encoding='utf8') as sub_process:
|
||||
for line in iter(sub_process.stdout.readline, ''):
|
||||
sys.stdout.write(line)
|
||||
if line.startswith('Selected cases:'):
|
||||
line = line.replace('Selected cases:', '').strip()
|
||||
selected_cases = line.split(',')
|
||||
sub_process.wait()
|
||||
if sub_process.returncode != 0:
|
||||
msg = 'Run analysis exception, returncode: %s!' % sub_process.returncode
|
||||
logger.error(msg)
|
||||
raise Exception(msg)
|
||||
return selected_cases
|
||||
|
||||
|
||||
def run_in_subprocess(args):
|
||||
# only case args.isolated_cases run in subporcess, all other run in a subprocess
|
||||
if not args.no_diff: # run based on git diff
|
||||
try:
|
||||
test_suite_files = get_selected_cases()
|
||||
logger.info('Tests suite to run: ')
|
||||
for f in test_suite_files:
|
||||
logger.info(f)
|
||||
except Exception:
|
||||
logger.error(
|
||||
'Get test suite based diff exception!, will run all cases.')
|
||||
test_suite_files = gather_test_suites_files(
|
||||
os.path.abspath(args.test_dir), args.pattern)
|
||||
if len(test_suite_files) == 0:
|
||||
logger.error('Get no test suite based on diff, run all the cases.')
|
||||
test_suite_files = gather_test_suites_files(
|
||||
os.path.abspath(args.test_dir), args.pattern)
|
||||
else:
|
||||
test_suite_files = gather_test_suites_files(
|
||||
os.path.abspath(args.test_dir), args.pattern)
|
||||
test_suite_files = gather_test_suites_files(
|
||||
os.path.abspath(args.test_dir), args.pattern)
|
||||
|
||||
non_parallelizable_suites = [
|
||||
'test_download_dataset.py',
|
||||
@@ -534,7 +500,7 @@ def gather_test_cases(test_dir, pattern, list_tests):
|
||||
|
||||
for case in case_list:
|
||||
test_case = unittest.defaultTestLoader.discover(
|
||||
start_dir=_test_dir, pattern=case)
|
||||
start_dir=_test_dir, pattern=case, top_level_dir=PROJECT_ROOT)
|
||||
test_suite.addTest(test_case)
|
||||
if hasattr(test_case, '__iter__'):
|
||||
for subcase in test_case:
|
||||
@@ -647,12 +613,6 @@ if __name__ == '__main__':
|
||||
type=int,
|
||||
help='Set case parallels, default single process, set with gpu number.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-diff',
|
||||
action='store_true',
|
||||
help=
|
||||
'Default running case based on git diff(with master), disable with --no-diff)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--suites',
|
||||
nargs='*',
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from trainers.model_trainer_map import model_trainer_map
|
||||
from utils.case_file_analyzer import get_pipelines_trainers_test_info
|
||||
from utils.source_file_analyzer import (get_all_register_modules,
|
||||
get_file_register_modules,
|
||||
get_import_map)
|
||||
|
||||
from modelscope.hub.api import HubApi
|
||||
from modelscope.hub.file_download import model_file_download
|
||||
from modelscope.hub.utils.utils import model_id_to_group_owner_name
|
||||
from modelscope.utils.config import Config
|
||||
from modelscope.utils.constant import ModelFile
|
||||
from modelscope.utils.file_utils import get_model_cache_dir
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def get_models_info(groups: list) -> dict:
|
||||
models = []
|
||||
api = HubApi()
|
||||
for group in groups:
|
||||
page = 1
|
||||
total_count = 0
|
||||
while True:
|
||||
query_result = api.list_models(group, page, 100)
|
||||
if query_result['Models'] is not None:
|
||||
models.extend(query_result['Models'])
|
||||
elif total_count != 0:
|
||||
total_count = query_result['TotalCount']
|
||||
if len(models) >= total_count:
|
||||
break
|
||||
page += 1
|
||||
models_info = {} # key model id, value model info
|
||||
for model_info in models:
|
||||
model_id = '%s/%s' % (group, model_info['Name'])
|
||||
configuration_file = os.path.join(
|
||||
get_model_cache_dir(model_id), ModelFile.CONFIGURATION)
|
||||
if not os.path.exists(configuration_file):
|
||||
try:
|
||||
model_revisions = api.list_model_revisions(model_id=model_id)
|
||||
if len(model_revisions) == 0:
|
||||
print('Model: %s has no revision' % model_id)
|
||||
continue
|
||||
# get latest revision
|
||||
configuration_file = model_file_download(
|
||||
model_id=model_id,
|
||||
file_path=ModelFile.CONFIGURATION,
|
||||
revision=model_revisions[0])
|
||||
except Exception as e:
|
||||
print('Download model: %s configuration file exception'
|
||||
% model_id)
|
||||
print('Exception: %s' % e)
|
||||
continue
|
||||
try:
|
||||
cfg = Config.from_file(configuration_file)
|
||||
except Exception as e:
|
||||
print('Resolve model: %s configuration file failed!' % model_id)
|
||||
print(('Exception: %s' % e))
|
||||
|
||||
model_info = {}
|
||||
model_info['framework'] = cfg.safe_get('framework')
|
||||
model_info['task'] = cfg.safe_get('task')
|
||||
model_info['model_type'] = cfg.safe_get('model.type')
|
||||
model_info['pipeline_type'] = cfg.safe_get('pipeline.type')
|
||||
model_info['preprocessor_type'] = cfg.safe_get('preprocessor.type')
|
||||
train_hooks_type = []
|
||||
train_hooks = cfg.safe_get('train.hooks')
|
||||
if train_hooks is not None:
|
||||
for train_hook in train_hooks:
|
||||
train_hooks_type.append(train_hook.type)
|
||||
model_info['train_hooks_type'] = train_hooks_type
|
||||
model_info['datasets'] = cfg.safe_get('dataset')
|
||||
|
||||
model_info['evaluation_metics'] = cfg.safe_get('evaluation.metrics',
|
||||
[]) # metrics name list
|
||||
"""
|
||||
print('framework: %s, task: %s, model_type: %s, pipeline_type: %s, \
|
||||
preprocessor_type: %s, train_hooks_type: %s, \
|
||||
dataset: %s, evaluation_metics: %s'%(
|
||||
framework, task, model_type, pipeline_type,
|
||||
preprocessor_type, ','.join(train_hooks_type),
|
||||
datasets, evaluation_metics))
|
||||
"""
|
||||
models_info[model_id] = model_info
|
||||
return models_info
|
||||
|
||||
|
||||
def gather_test_suites_files(test_dir='./tests',
|
||||
pattern='test_*.py',
|
||||
is_full_path=True):
|
||||
# Directories excluded from CI (manual-only test suites)
|
||||
_CI_EXCLUDED_DIRS = {'studios'}
|
||||
case_file_list = []
|
||||
for dirpath, dirnames, filenames in os.walk(test_dir):
|
||||
# Skip excluded directories
|
||||
dirnames[:] = [d for d in dirnames if d not in _CI_EXCLUDED_DIRS]
|
||||
for file in filenames:
|
||||
if fnmatch(file, pattern):
|
||||
if is_full_path:
|
||||
case_file_list.append(os.path.join(dirpath, file))
|
||||
else:
|
||||
case_file_list.append(file)
|
||||
|
||||
return case_file_list
|
||||
|
||||
|
||||
def run_command_get_output(cmd):
|
||||
response = subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
response.check_returncode()
|
||||
output = response.stdout.decode('utf8')
|
||||
return output
|
||||
except subprocess.CalledProcessError as error:
|
||||
print('stdout: %s, stderr: %s' %
|
||||
(response.stdout.decode('utf8'), error.stderr.decode('utf8')))
|
||||
return None
|
||||
|
||||
|
||||
def get_current_branch():
|
||||
cmd = ['git', 'rev-parse', '--abbrev-ref', 'HEAD']
|
||||
branch = run_command_get_output(cmd).strip()
|
||||
logger.info('Testing branch: %s' % branch)
|
||||
return branch
|
||||
|
||||
|
||||
def get_modified_files():
|
||||
if 'PR_CHANGED_FILES' in os.environ and os.environ[
|
||||
'PR_CHANGED_FILES'].strip() != '':
|
||||
logger.info('Getting PR modified files.')
|
||||
# get modify file from environment
|
||||
diff_files = os.environ['PR_CHANGED_FILES'].replace('#', '\n')
|
||||
else:
|
||||
logger.info('Getting diff of branch.')
|
||||
cmd = ['git', 'diff', '--name-only', 'origin/master...']
|
||||
diff_files = run_command_get_output(cmd)
|
||||
logger.info('Diff files: ')
|
||||
logger.info(diff_files)
|
||||
modified_files = []
|
||||
# remove the deleted file.
|
||||
for diff_file in diff_files.splitlines():
|
||||
if os.path.exists(diff_file.strip()):
|
||||
modified_files.append(diff_file.strip())
|
||||
return modified_files
|
||||
|
||||
|
||||
def analysis_diff():
|
||||
"""Get modified files and their imported files modified modules
|
||||
"""
|
||||
# ignore diff for constant define files, these files import by all pipeline, trainer
|
||||
ignore_files = [
|
||||
'modelscope/utils/constant.py', 'modelscope/metainfo.py',
|
||||
'modelscope/pipeline_inputs.py', 'modelscope/outputs/outputs.py'
|
||||
]
|
||||
|
||||
modified_register_modules = []
|
||||
modified_cases = []
|
||||
modified_files_imported_by = []
|
||||
modified_files = get_modified_files()
|
||||
logger.info('Modified files:\n %s' % '\n'.join(modified_files))
|
||||
|
||||
logger.info('Starting get import map')
|
||||
import_map = get_import_map()
|
||||
logger.info('Finished get import map')
|
||||
for modified_file in modified_files:
|
||||
if ((modified_file.startswith('./modelscope')
|
||||
or modified_file.startswith('modelscope'))
|
||||
and modified_file not in ignore_files): # is source file
|
||||
for k, v in import_map.items():
|
||||
if modified_file in v and modified_file != k:
|
||||
modified_files_imported_by.append(k)
|
||||
logger.info('There are affected files: %s'
|
||||
% len(modified_files_imported_by))
|
||||
for f in modified_files_imported_by:
|
||||
logger.info(f)
|
||||
modified_files.extend(modified_files_imported_by) # add imported by file
|
||||
for modified_file in modified_files:
|
||||
if modified_file.startswith('./modelscope') or \
|
||||
modified_file.startswith('modelscope'):
|
||||
modified_register_modules.extend(
|
||||
get_file_register_modules(modified_file))
|
||||
elif ((modified_file.startswith('./tests')
|
||||
or modified_file.startswith('tests'))
|
||||
and os.path.basename(modified_file).startswith('test_')
|
||||
and '/studios/' not in modified_file):
|
||||
modified_cases.append(modified_file)
|
||||
|
||||
return modified_register_modules, modified_cases
|
||||
|
||||
|
||||
def split_test_suites():
|
||||
test_suite_full_paths = gather_test_suites_files()
|
||||
pipeline_test_suites = []
|
||||
trainer_test_suites = []
|
||||
other_test_suites = []
|
||||
for test_suite in test_suite_full_paths:
|
||||
if test_suite.find('tests/trainers') != -1:
|
||||
trainer_test_suites.append(test_suite)
|
||||
elif test_suite.find('tests/pipelines') != -1:
|
||||
pipeline_test_suites.append(test_suite)
|
||||
else:
|
||||
other_test_suites.append(test_suite)
|
||||
|
||||
return pipeline_test_suites, trainer_test_suites, other_test_suites
|
||||
|
||||
|
||||
def get_test_suites_to_run():
|
||||
branch = get_current_branch()
|
||||
if branch == 'master':
|
||||
# when run with master, run all the cases
|
||||
return gather_test_suites_files(is_full_path=False)
|
||||
affected_register_modules, modified_cases = analysis_diff()
|
||||
# affected_register_modules list of modified file and dependent file's register_module.
|
||||
# ("MODULES|PIPELINES|TRAINERS|...", '', '', model_class_name)
|
||||
# modified_cases, modified case file.
|
||||
all_register_modules = get_all_register_modules()
|
||||
_, _, other_test_suites = split_test_suites()
|
||||
task_pipeline_test_suite_map, trainer_test_suite_map = get_pipelines_trainers_test_info(
|
||||
all_register_modules)
|
||||
# task_pipeline_test_suite_map key: pipeline task, value: case file path
|
||||
# trainer_test_suite_map key: trainer_name, value: case file path
|
||||
iic_models_info = get_models_info(['iic'])
|
||||
models_info = {}
|
||||
# compatible model info
|
||||
for model_id, model_info in iic_models_info.items():
|
||||
_, model_name = model_id_to_group_owner_name(model_id)
|
||||
models_info['damo/%s' % model_name] = models_info
|
||||
# model_info key: model_id, value: model info such as framework task etc.
|
||||
affected_pipeline_cases = []
|
||||
affected_trainer_cases = []
|
||||
for affected_register_module in affected_register_modules:
|
||||
# affected_register_module PIPELINE structure
|
||||
# ["PIPELINES", "acoustic_noise_suppression", "speech_frcrn_ans_cirm_16k", "ANSPipeline"]
|
||||
# ["PIPELINES", task, pipeline_name, pipeline_class_name]
|
||||
if affected_register_module[0] == 'PIPELINES':
|
||||
if affected_register_module[1] in task_pipeline_test_suite_map:
|
||||
affected_pipeline_cases.extend(
|
||||
task_pipeline_test_suite_map[affected_register_module[1]])
|
||||
else:
|
||||
logger.warning('Pipeline task: %s has no test case!'
|
||||
% affected_register_module[1])
|
||||
elif affected_register_module[0] == 'MODELS':
|
||||
# ["MODELS", "keyword_spotting", "kws_kwsbp", "GenericKeyWordSpotting"],
|
||||
# ["MODELS", task, model_name, model_class_name]
|
||||
if affected_register_module[1] in task_pipeline_test_suite_map:
|
||||
affected_pipeline_cases.extend(
|
||||
task_pipeline_test_suite_map[affected_register_module[1]])
|
||||
else:
|
||||
logger.warning('Pipeline task: %s has no test case!'
|
||||
% affected_register_module[1])
|
||||
elif affected_register_module[0] == 'TRAINERS':
|
||||
# ["TRAINERS", "", "nlp_base_trainer", "NlpEpochBasedTrainer"],
|
||||
# ["TRAINERS", "", trainer_name, trainer_class_name]
|
||||
if affected_register_module[2] in trainer_test_suite_map:
|
||||
affected_trainer_cases.extend(
|
||||
trainer_test_suite_map[affected_register_module[2]])
|
||||
else:
|
||||
logger.warn('Trainer %s his no case' %
|
||||
(affected_register_module[2]))
|
||||
elif affected_register_module[0] == 'PREPROCESSORS':
|
||||
# ["PREPROCESSORS", "cv", "object_detection_scrfd", "SCRFDPreprocessor"]
|
||||
# ["PREPROCESSORS", domain, preprocessor_name, class_name]
|
||||
for model_id, model_info in models_info.items():
|
||||
if ('preprocessor_type' in model_info
|
||||
and model_info['preprocessor_type'] is not None
|
||||
and model_info['preprocessor_type']
|
||||
== affected_register_module[2]):
|
||||
task = model_info['task']
|
||||
if task in task_pipeline_test_suite_map:
|
||||
affected_pipeline_cases.extend(
|
||||
task_pipeline_test_suite_map[task])
|
||||
if model_id in model_trainer_map:
|
||||
affected_trainer_cases.extend(
|
||||
model_trainer_map[model_id])
|
||||
elif (affected_register_module[0] == 'HOOKS'
|
||||
or affected_register_module[0] == 'CUSTOM_DATASETS'):
|
||||
# ["HOOKS", "", "CheckpointHook", "CheckpointHook"]
|
||||
# ["HOOKS", "", hook_name, class_name]
|
||||
# HOOKS, DATASETS modify run all trainer cases
|
||||
for _, cases in trainer_test_suite_map.items():
|
||||
affected_trainer_cases.extend(cases)
|
||||
elif affected_register_module[0] == 'METRICS':
|
||||
# ["METRICS", "default_group", "accuracy", "AccuracyMetric"]
|
||||
# ["METRICS", group, metric_name, class_name]
|
||||
for model_id, model_info in models_info.items():
|
||||
if affected_register_module[2] in model_info[
|
||||
'evaluation_metics']:
|
||||
if model_id in model_trainer_map:
|
||||
affected_trainer_cases.extend(
|
||||
model_trainer_map[model_id])
|
||||
|
||||
# deduplication
|
||||
affected_pipeline_cases = list(set(affected_pipeline_cases))
|
||||
affected_trainer_cases = list(set(affected_trainer_cases))
|
||||
test_suites_to_run = []
|
||||
for test_suite in other_test_suites:
|
||||
test_suites_to_run.append(os.path.basename(test_suite))
|
||||
for test_suite in affected_pipeline_cases:
|
||||
test_suites_to_run.append(os.path.basename(test_suite))
|
||||
for test_suite in affected_trainer_cases:
|
||||
test_suites_to_run.append(os.path.basename(test_suite))
|
||||
|
||||
for modified_case in modified_cases:
|
||||
if modified_case not in test_suites_to_run:
|
||||
test_suites_to_run.append(os.path.basename(modified_case))
|
||||
return test_suites_to_run
|
||||
|
||||
|
||||
def get_files_related_modules(files, reverse_import_map):
|
||||
register_modules = []
|
||||
for single_file in files:
|
||||
if single_file.startswith('./modelscope') or \
|
||||
single_file.startswith('modelscope'):
|
||||
register_modules.extend(get_file_register_modules(single_file))
|
||||
|
||||
while len(register_modules) == 0:
|
||||
logger.warn('There is no affected register module')
|
||||
deeper_imported_by = []
|
||||
has_deeper_affected_files = False
|
||||
for source_file in files:
|
||||
if len(source_file.split('/')) > 4 and source_file.startswith(
|
||||
'modelscope'):
|
||||
deeper_imported_by.extend(reverse_import_map[source_file])
|
||||
has_deeper_affected_files = True
|
||||
if not has_deeper_affected_files:
|
||||
break
|
||||
for file in deeper_imported_by:
|
||||
register_modules = get_file_register_modules(file)
|
||||
files = deeper_imported_by
|
||||
return register_modules
|
||||
|
||||
|
||||
def get_modules_related_cases(register_modules, task_pipeline_test_suite_map,
|
||||
trainer_test_suite_map):
|
||||
affected_pipeline_cases = []
|
||||
affected_trainer_cases = []
|
||||
for register_module in register_modules:
|
||||
if register_module[0] == 'PIPELINES' or \
|
||||
register_module[0] == 'MODELS':
|
||||
if register_module[1] in task_pipeline_test_suite_map:
|
||||
affected_pipeline_cases.extend(
|
||||
task_pipeline_test_suite_map[register_module[1]])
|
||||
else:
|
||||
logger.warn('Pipeline task: %s has no test case!'
|
||||
% register_module[1])
|
||||
elif register_module[0] == 'TRAINERS':
|
||||
if register_module[2] in trainer_test_suite_map:
|
||||
affected_trainer_cases.extend(
|
||||
trainer_test_suite_map[register_module[2]])
|
||||
else:
|
||||
logger.warn('Trainer %s his no case' % (register_module[2]))
|
||||
return affected_pipeline_cases, affected_trainer_cases
|
||||
|
||||
|
||||
def get_all_file_test_info():
|
||||
all_files = [
|
||||
os.path.relpath(os.path.join(dp, f), os.getcwd())
|
||||
for dp, dn, filenames in os.walk(
|
||||
os.path.join(os.getcwd(), 'modelscope')) for f in filenames
|
||||
if os.path.splitext(f)[1] == '.py'
|
||||
]
|
||||
import_map = get_import_map()
|
||||
all_register_modules = get_all_register_modules()
|
||||
task_pipeline_test_suite_map, trainer_test_suite_map = get_pipelines_trainers_test_info(
|
||||
all_register_modules)
|
||||
reverse_depend_map = {}
|
||||
for f in all_files:
|
||||
depend_by = []
|
||||
for k, v in import_map.items():
|
||||
if f in v and f != k:
|
||||
depend_by.append(k)
|
||||
reverse_depend_map[f] = depend_by
|
||||
# get cases.
|
||||
test_info = {}
|
||||
for f in all_files:
|
||||
file_test_info = {}
|
||||
file_test_info['imports'] = import_map[f]
|
||||
file_test_info['imported_by'] = reverse_depend_map[f]
|
||||
register_modules = get_files_related_modules(
|
||||
[f] + reverse_depend_map[f], reverse_depend_map)
|
||||
file_test_info['relate_modules'] = register_modules
|
||||
affected_pipeline_cases, affected_trainer_cases = get_modules_related_cases(
|
||||
register_modules, task_pipeline_test_suite_map,
|
||||
trainer_test_suite_map)
|
||||
file_test_info['pipeline_cases'] = affected_pipeline_cases
|
||||
file_test_info['trainer_cases'] = affected_trainer_cases
|
||||
file_relative_path = os.path.relpath(f, os.getcwd())
|
||||
test_info[file_relative_path] = file_test_info
|
||||
|
||||
with open('./test_relate_info.json', 'w') as f:
|
||||
import json
|
||||
json.dump(test_info, f)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_suites_to_run = get_test_suites_to_run()
|
||||
msg = ','.join(test_suites_to_run)
|
||||
print('Selected cases: %s' % msg)
|
||||
@@ -44,7 +44,12 @@ CLI_PREFIX = _cli_invocation()
|
||||
|
||||
|
||||
class TestStudioCLIHelp(TestResultMixin, unittest.TestCase):
|
||||
"""Smoke-test the help output of every studio subcommand."""
|
||||
"""Smoke-test the help output of studio-related subcommands.
|
||||
|
||||
In the new CLI engine, studio operations are top-level commands
|
||||
(deploy, stop, logs, settings, secret) rather than nested under
|
||||
a ``studio`` group.
|
||||
"""
|
||||
|
||||
def _run_help(self, *cli_args):
|
||||
cmd = ' '.join([CLI_PREFIX, *cli_args, '--help'])
|
||||
@@ -52,46 +57,51 @@ class TestStudioCLIHelp(TestResultMixin, unittest.TestCase):
|
||||
return stat, output
|
||||
|
||||
def test_studio_help(self):
|
||||
stat, output = self._run_help('studio')
|
||||
"""The top-level help lists deploy/stop/logs/settings/secret."""
|
||||
stat, output = self._run_help()
|
||||
self.assertEqual(stat, 0, output)
|
||||
for sub in ('deploy', 'stop', 'logs', 'settings', 'secret'):
|
||||
self.assertIn(sub, output)
|
||||
|
||||
def test_studio_deploy_help(self):
|
||||
stat, output = self._run_help('studio', 'deploy')
|
||||
stat, output = self._run_help('deploy')
|
||||
self.assertEqual(stat, 0, output)
|
||||
self.assertIn('studio_id', output)
|
||||
# deploy accepts a repo/studio ID
|
||||
self.assertTrue('repo_id' in output or 'studio' in output.lower(),
|
||||
output)
|
||||
|
||||
def test_studio_stop_help(self):
|
||||
stat, output = self._run_help('studio', 'stop')
|
||||
stat, output = self._run_help('stop')
|
||||
self.assertEqual(stat, 0, output)
|
||||
self.assertIn('studio_id', output)
|
||||
self.assertTrue('repo_id' in output or 'studio' in output.lower(),
|
||||
output)
|
||||
|
||||
def test_studio_logs_help(self):
|
||||
stat, output = self._run_help('studio', 'logs')
|
||||
stat, output = self._run_help('logs')
|
||||
self.assertEqual(stat, 0, output)
|
||||
self.assertIn('--type', output)
|
||||
self.assertIn('--log-type', output)
|
||||
self.assertIn('--keyword', output)
|
||||
self.assertIn('--page-num', output)
|
||||
self.assertIn('--page-size', output)
|
||||
|
||||
def test_studio_settings_help(self):
|
||||
stat, output = self._run_help('studio', 'settings')
|
||||
stat, output = self._run_help('settings')
|
||||
self.assertEqual(stat, 0, output)
|
||||
for flag in ('--sdk-type', '--hardware', '--private', '--public',
|
||||
'--display-name'):
|
||||
self.assertIn(flag, output)
|
||||
# settings should accept key=value pairs or specific flags
|
||||
self.assertTrue(
|
||||
'key=value' in output.lower() or 'settings' in output.lower(),
|
||||
output)
|
||||
|
||||
def test_studio_secret_help(self):
|
||||
stat, output = self._run_help('studio', 'secret')
|
||||
stat, output = self._run_help('secret')
|
||||
self.assertEqual(stat, 0, output)
|
||||
for sub in ('list', 'add', 'update', 'delete'):
|
||||
self.assertIn(sub, output)
|
||||
|
||||
def test_download_repo_type_includes_studio(self):
|
||||
def test_download_repo_type_includes_model(self):
|
||||
stat, output = self._run_help('download')
|
||||
self.assertEqual(stat, 0, output)
|
||||
self.assertIn('studio', output)
|
||||
self.assertIn('model', output)
|
||||
|
||||
def test_create_includes_studio_args(self):
|
||||
stat, output = self._run_help('create')
|
||||
@@ -141,13 +151,9 @@ class TestStudioCLIErrors(TestResultMixin, unittest.TestCase):
|
||||
hardware=None,
|
||||
private=None,
|
||||
)
|
||||
with patch.object(
|
||||
HubApi,
|
||||
'_build_bearer_headers',
|
||||
return_value={'Authorization': f'Bearer {self.token}'}):
|
||||
cmd = StudioCMD(args)
|
||||
with self.assertRaises(SystemExit):
|
||||
cmd.execute()
|
||||
cmd = StudioCMD(args)
|
||||
with self.assertRaises(SystemExit):
|
||||
cmd.execute()
|
||||
|
||||
def test_secret_no_action_raises(self):
|
||||
args = argparse.Namespace(
|
||||
@@ -156,13 +162,9 @@ class TestStudioCLIErrors(TestResultMixin, unittest.TestCase):
|
||||
token=None,
|
||||
endpoint=None,
|
||||
)
|
||||
with patch.object(
|
||||
HubApi,
|
||||
'_build_bearer_headers',
|
||||
return_value={'Authorization': f'Bearer {self.token}'}):
|
||||
cmd = StudioCMD(args)
|
||||
with self.assertRaises(SystemExit):
|
||||
cmd.execute()
|
||||
cmd = StudioCMD(args)
|
||||
with self.assertRaises(SystemExit):
|
||||
cmd.execute()
|
||||
|
||||
|
||||
class TestStudioCreate(TestResultMixin, unittest.TestCase):
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
model_trainer_map = {
|
||||
'damo/speech_frcrn_ans_cirm_16k':
|
||||
['tests/trainers/audio/test_ans_trainer.py'],
|
||||
'damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch':
|
||||
['tests/trainers/audio/test_asr_trainer.py'],
|
||||
'damo/speech_dfsmn_kws_char_farfield_16k_nihaomiya':
|
||||
['tests/trainers/audio/test_kws_farfield_trainer.py'],
|
||||
'damo/speech_charctc_kws_phone-xiaoyun':
|
||||
['tests/trainers/audio/test_kws_nearfield_trainer.py'],
|
||||
'damo/speech_mossformer_separation_temporal_8k':
|
||||
['tests/trainers/audio/test_separation_trainer.py'],
|
||||
'speech_tts/speech_sambert-hifigan_tts_zh-cn_multisp_pretrain_16k':
|
||||
['tests/trainers/audio/test_tts_trainer.py'],
|
||||
'damo/cv_resnet_carddetection_scrfd34gkps':
|
||||
['tests/trainers/test_card_detection_scrfd_trainer.py'],
|
||||
'damo/multi-modal_clip-vit-base-patch16_zh':
|
||||
['tests/trainers/test_clip_trainer.py'],
|
||||
'damo/nlp_space_pretrained-dialog-model':
|
||||
['tests/trainers/test_dialog_intent_trainer.py'],
|
||||
'damo/cv_resnet_facedetection_scrfd10gkps':
|
||||
['tests/trainers/test_face_detection_scrfd_trainer.py'],
|
||||
'damo/nlp_structbert_faq-question-answering_chinese-base':
|
||||
['tests/trainers/test_finetune_faq_question_answering.py'],
|
||||
'PAI/nlp_gpt3_text-generation_0.35B_MoE-64':
|
||||
['tests/trainers/test_finetune_gpt_moe.py'],
|
||||
'damo/nlp_gpt3_text-generation_1.3B': [
|
||||
'tests/trainers/test_finetune_gpt3.py'
|
||||
],
|
||||
'damo/mgeo_backbone_chinese_base': [
|
||||
'tests/trainers/test_finetune_mgeo.py'
|
||||
],
|
||||
'damo/mplug_backbone_base_en': ['tests/trainers/test_finetune_mplug.py'],
|
||||
'damo/nlp_structbert_backbone_base_std': [
|
||||
'tests/trainers/test_finetune_sequence_classification.py',
|
||||
'tests/trainers/test_finetune_token_classification.py'
|
||||
],
|
||||
'damo/nlp_palm2.0_text-generation_english-base': [
|
||||
'tests/trainers/test_finetune_text_generation.py'
|
||||
],
|
||||
'damo/nlp_gpt3_text-generation_chinese-base': [
|
||||
'tests/trainers/test_finetune_text_generation.py'
|
||||
],
|
||||
'damo/nlp_palm2.0_text-generation_chinese-base': [
|
||||
'tests/trainers/test_finetune_text_generation.py'
|
||||
],
|
||||
'damo/nlp_corom_passage-ranking_english-base': [
|
||||
'tests/trainers/test_finetune_text_ranking.py'
|
||||
],
|
||||
'damo/nlp_rom_passage-ranking_chinese-base': [
|
||||
'tests/trainers/test_finetune_text_ranking.py'
|
||||
],
|
||||
'damo/cv_nextvit-small_image-classification_Dailylife-labels': [
|
||||
'tests/trainers/test_general_image_classification_trainer.py'
|
||||
],
|
||||
'damo/cv_convnext-base_image-classification_garbage': [
|
||||
'tests/trainers/test_general_image_classification_trainer.py'
|
||||
],
|
||||
'damo/cv_beitv2-base_image-classification_patch16_224_pt1k_ft22k_in1k': [
|
||||
'tests/trainers/test_general_image_classification_trainer.py'
|
||||
],
|
||||
'damo/cv_csrnet_image-color-enhance-models': [
|
||||
'tests/trainers/test_image_color_enhance_trainer.py'
|
||||
],
|
||||
'damo/cv_nafnet_image-deblur_gopro': [
|
||||
'tests/trainers/test_image_deblur_trainer.py'
|
||||
],
|
||||
'damo/cv_resnet101_detection_fewshot-defrcn': [
|
||||
'tests/trainers/test_image_defrcn_fewshot_trainer.py'
|
||||
],
|
||||
'damo/cv_nafnet_image-denoise_sidd': [
|
||||
'tests/trainers/test_image_denoise_trainer.py'
|
||||
],
|
||||
'damo/cv_fft_inpainting_lama': [
|
||||
'tests/trainers/test_image_inpainting_trainer.py'
|
||||
],
|
||||
'damo/cv_swin-b_image-instance-segmentation_coco': [
|
||||
'tests/trainers/test_image_instance_segmentation_trainer.py'
|
||||
],
|
||||
'damo/cv_gpen_image-portrait-enhancement': [
|
||||
'tests/trainers/test_image_portrait_enhancement_trainer.py'
|
||||
],
|
||||
'damo/cv_clip-it_video-summarization_language-guided_en': [
|
||||
'tests/trainers/test_language_guided_video_summarization_trainer.py'
|
||||
],
|
||||
'damo/cv_resnet50-bert_video-scene-segmentation_movienet': [
|
||||
'tests/trainers/test_movie_scene_segmentation_trainer.py'
|
||||
],
|
||||
'damo/ofa_mmspeech_pretrain_base_zh': [
|
||||
'tests/trainers/test_ofa_mmspeech_trainer.py'
|
||||
],
|
||||
'damo/ofa_ocr-recognition_scene_base_zh': [
|
||||
'tests/trainers/test_ofa_trainer.py'
|
||||
],
|
||||
'damo/nlp_plug_text-generation_27B': [
|
||||
'tests/trainers/test_plug_finetune_text_generation.py'
|
||||
],
|
||||
'damo/cv_swin-t_referring_video-object-segmentation': [
|
||||
'tests/trainers/test_referring_video_object_segmentation_trainer.py'
|
||||
],
|
||||
'damo/nlp_convai_text2sql_pretrain_cn': [
|
||||
'tests/trainers/test_table_question_answering_trainer.py'
|
||||
],
|
||||
'damo/multi-modal_team-vit-large-patch14_multi-modal-similarity': [
|
||||
'tests/trainers/test_team_transfer_trainer.py'
|
||||
],
|
||||
'damo/cv_tinynas_object-detection_damoyolo': [
|
||||
'tests/trainers/test_tinynas_damoyolo_trainer.py'
|
||||
],
|
||||
'damo/nlp_structbert_sentence-similarity_chinese-tiny': [
|
||||
'tests/trainers/test_trainer_with_nlp.py'
|
||||
],
|
||||
'damo/nlp_structbert_sentiment-classification_chinese-base': [
|
||||
'tests/trainers/test_trainer_with_nlp.py'
|
||||
],
|
||||
'damo/nlp_structbert_sentence-similarity_chinese-base': [
|
||||
'tests/trainers/test_trainer_with_nlp.py'
|
||||
],
|
||||
'damo/nlp_csanmt_translation_en2zh': [
|
||||
'tests/trainers/test_translation_trainer.py'
|
||||
],
|
||||
'damo/nlp_csanmt_translation_en2fr': [
|
||||
'tests/trainers/test_translation_trainer.py'
|
||||
],
|
||||
'damo/nlp_csanmt_translation_en2es': [
|
||||
'tests/trainers/test_translation_trainer.py'
|
||||
],
|
||||
'damo/nlp_unite_mup_translation_evaluation_multilingual_base': [
|
||||
'tests/trainers/test_translation_evaluation_trainer.py'
|
||||
],
|
||||
'damo/nlp_unite_mup_translation_evaluation_multilingual_large': [
|
||||
'tests/trainers/test_translation_evaluation_trainer.py'
|
||||
],
|
||||
'damo/cv_googlenet_pgl-video-summarization': [
|
||||
'tests/trainers/test_video_summarization_trainer.py'
|
||||
],
|
||||
}
|
||||
@@ -1,515 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
from __future__ import print_function
|
||||
import ast
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
SYSTEM_TRAINER_BUILDER_FUNCTION_NAME = 'build_trainer'
|
||||
SYSTEM_TRAINER_BUILDER_PARAMETER_NAME = 'name'
|
||||
SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME = 'pipeline'
|
||||
SYSTEM_PIPELINE_BUILDER_PARAMETER_NAME = 'task'
|
||||
|
||||
|
||||
class AnalysisTestFile(ast.NodeVisitor):
|
||||
"""Analysis test suite files.
|
||||
Get global function and test class
|
||||
|
||||
Args:
|
||||
ast (NodeVisitor): The ast node.
|
||||
Examples:
|
||||
>>> with open(test_suite_file, "rb") as f:
|
||||
>>> src = f.read()
|
||||
>>> analyzer = AnalysisTestFile(test_suite_file)
|
||||
>>> analyzer.visit(ast.parse(src, filename=test_suite_file))
|
||||
"""
|
||||
|
||||
def __init__(self, test_suite_file, builder_function_name) -> None:
|
||||
super().__init__()
|
||||
self.test_classes = []
|
||||
self.builder_function_name = builder_function_name
|
||||
self.global_functions = []
|
||||
self.custom_global_builders = [
|
||||
] # global trainer builder method(call build_trainer)
|
||||
self.custom_global_builder_calls = [] # the builder call statement
|
||||
|
||||
def visit_ClassDef(self, node) -> bool:
|
||||
"""Check if the class is a unittest suite.
|
||||
|
||||
Args:
|
||||
node (ast.Node): the ast node
|
||||
|
||||
Returns: True if is a test class.
|
||||
"""
|
||||
for base in node.bases:
|
||||
if isinstance(base, ast.Attribute) and base.attr == 'TestCase':
|
||||
self.test_classes.append(node)
|
||||
elif isinstance(base, ast.Name) and 'TestCase' in base.id:
|
||||
self.test_classes.append(node)
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef):
|
||||
self.global_functions.append(node)
|
||||
for statement in ast.walk(node):
|
||||
if isinstance(statement, ast.Call) and \
|
||||
isinstance(statement.func, ast.Name):
|
||||
if statement.func.id == self.builder_function_name:
|
||||
self.custom_global_builders.append(node)
|
||||
self.custom_global_builder_calls.append(statement)
|
||||
|
||||
|
||||
class AnalysisTestClass(ast.NodeVisitor):
|
||||
|
||||
def __init__(self,
|
||||
test_class_node,
|
||||
builder_function_name,
|
||||
file_analyzer=None) -> None:
|
||||
super().__init__()
|
||||
self.test_class_node = test_class_node
|
||||
self.builder_function_name = builder_function_name
|
||||
self.setup_variables = {}
|
||||
self.test_methods = []
|
||||
self.custom_class_method_builders = [
|
||||
] # class method trainer builder(call build_trainer)
|
||||
self.custom_class_method_builder_calls = [
|
||||
] # the builder call statement
|
||||
self.variables = {}
|
||||
|
||||
def get_variables(self, key: str):
|
||||
if key in self.variables:
|
||||
return self.variables[key]
|
||||
return key
|
||||
|
||||
def get_ast_value(self, statements):
|
||||
if not isinstance(statements, list):
|
||||
statements = [statements]
|
||||
res = []
|
||||
for item in statements:
|
||||
if isinstance(item, ast.Name):
|
||||
res.append(self.get_variables(item.id))
|
||||
elif isinstance(item, ast.Attribute):
|
||||
if hasattr(item.value, 'id'):
|
||||
res.append(self.get_variables(item.value.id))
|
||||
elif isinstance(item, ast.Str):
|
||||
res.append(self.get_variables(item.s))
|
||||
elif isinstance(item, ast.Dict):
|
||||
keys = [i.s for i in item.keys]
|
||||
values = self.get_ast_value(item.values)
|
||||
res.append(dict(zip(keys, values)))
|
||||
return res
|
||||
|
||||
def get_final_variables(self, statement: ast.Assign):
|
||||
if len(statement.targets) == 1 and \
|
||||
isinstance(statement.targets[0], ast.Name):
|
||||
if isinstance(statement.value, ast.Call):
|
||||
if isinstance(statement.value.func, ast.Attribute) and \
|
||||
isinstance(statement.value.func.value, ast.Name) and \
|
||||
statement.value.func.value.id == 'Image':
|
||||
self.variables[str(
|
||||
statement.targets[0].id)] = self.get_ast_value(
|
||||
statement.value.args[0])
|
||||
else:
|
||||
self.variables[str(
|
||||
statement.targets[0].id)] = self.get_ast_value(
|
||||
statement.value)
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
|
||||
if node.name.startswith('setUp'):
|
||||
for statement in node.body:
|
||||
if isinstance(statement, ast.Assign):
|
||||
if len(statement.targets) == 1 and \
|
||||
isinstance(statement.targets[0], ast.Attribute) and \
|
||||
isinstance(statement.value, ast.Attribute):
|
||||
self.setup_variables[str(
|
||||
statement.targets[0].attr)] = str(
|
||||
statement.value.attr)
|
||||
self.get_final_variables(statement)
|
||||
elif node.name.startswith('test_'):
|
||||
self.test_methods.append(node)
|
||||
else:
|
||||
for statement in ast.walk(node):
|
||||
if isinstance(statement, ast.Call) and \
|
||||
isinstance(statement.func, ast.Name):
|
||||
if statement.func.id == self.builder_function_name:
|
||||
self.custom_class_method_builders.append(node)
|
||||
self.custom_class_method_builder_calls.append(
|
||||
statement)
|
||||
|
||||
|
||||
def get_local_arg_value(target_method, args_name):
|
||||
for statement in target_method.body:
|
||||
if isinstance(statement, ast.Assign):
|
||||
for target in statement.targets:
|
||||
if isinstance(target, ast.Name) and target.id == args_name:
|
||||
if isinstance(statement.value, ast.Attribute):
|
||||
return statement.value.attr
|
||||
elif isinstance(statement.value, ast.Str):
|
||||
return statement.value.s
|
||||
return None
|
||||
|
||||
|
||||
def get_custom_builder_parameter_name(args, keywords, builder, builder_call,
|
||||
builder_arg_name):
|
||||
# get build_trainer call name argument name.
|
||||
arg_name = None
|
||||
if len(builder_call.args) > 0:
|
||||
if isinstance(builder_call.args[0], ast.Name):
|
||||
# build_trainer name is a variable
|
||||
arg_name = builder_call.args[0].id
|
||||
elif isinstance(builder_call.args[0], ast.Attribute):
|
||||
# Attribute access, such as Trainers.image_classification_team
|
||||
return builder_call.args[0].attr
|
||||
else:
|
||||
raise Exception('Invalid argument name')
|
||||
else:
|
||||
use_default_name = True
|
||||
for kw in builder_call.keywords:
|
||||
if kw.arg == builder_arg_name:
|
||||
use_default_name = False
|
||||
if isinstance(kw.value, ast.Attribute):
|
||||
return kw.value.attr
|
||||
elif isinstance(kw.value,
|
||||
ast.Name) and kw.arg == builder_arg_name:
|
||||
arg_name = kw.value.id
|
||||
else:
|
||||
raise Exception('Invalid keyword argument')
|
||||
if use_default_name:
|
||||
return 'default'
|
||||
|
||||
if arg_name is None:
|
||||
raise Exception('Invalid build_trainer call')
|
||||
|
||||
arg_value = get_local_arg_value(builder, arg_name)
|
||||
if arg_value is not None: # trainer_name is a local variable
|
||||
return arg_value
|
||||
# get build_trainer name parameter, if it's passed
|
||||
default_name = None
|
||||
arg_idx = 100000
|
||||
for idx, arg in enumerate(builder.args.args):
|
||||
if arg.arg == arg_name:
|
||||
arg_idx = idx
|
||||
if idx >= len(builder.args.args) - len(builder.args.defaults):
|
||||
default_name = builder.args.defaults[idx - (
|
||||
len(builder.args.args) - len(builder.args.defaults))].attr
|
||||
break
|
||||
if len(builder.args.args
|
||||
) > 0 and builder.args.args[0].arg == 'self': # class method
|
||||
if len(args) > arg_idx - 1: # - self
|
||||
if isinstance(args[arg_idx - 1], ast.Attribute):
|
||||
return args[arg_idx - 1].attr
|
||||
|
||||
for keyword in keywords:
|
||||
if keyword.arg == arg_name:
|
||||
if isinstance(keyword.value, ast.Attribute):
|
||||
return keyword.value.attr
|
||||
|
||||
return default_name
|
||||
|
||||
|
||||
def get_system_builder_parameter_value(builder_call, test_method,
|
||||
setup_attributes,
|
||||
builder_parameter_name):
|
||||
if len(builder_call.args) > 0:
|
||||
if isinstance(builder_call.args[0], ast.Name):
|
||||
return get_local_arg_value(test_method, builder_call.args[0].id)
|
||||
elif isinstance(builder_call.args[0], ast.Attribute):
|
||||
if builder_call.args[0].attr in setup_attributes:
|
||||
return setup_attributes[builder_call.args[0].attr]
|
||||
return builder_call.args[0].attr
|
||||
elif isinstance(builder_call.args[0], ast.Str): # TODO check py38
|
||||
return builder_call.args[0].s
|
||||
|
||||
for kw in builder_call.keywords:
|
||||
if kw.arg == builder_parameter_name:
|
||||
if isinstance(kw.value, ast.Attribute):
|
||||
if kw.value.attr in setup_attributes:
|
||||
return setup_attributes[kw.value.attr]
|
||||
else:
|
||||
return kw.value.attr
|
||||
elif isinstance(kw.value,
|
||||
ast.Name) and kw.arg == builder_parameter_name:
|
||||
return kw.value.id
|
||||
|
||||
return 'default' # use build_trainer default argument.
|
||||
|
||||
|
||||
def get_builder_parameter_value(test_method, setup_variables, builder,
|
||||
builder_call, system_builder_func_name,
|
||||
builder_parameter_name):
|
||||
"""
|
||||
get target builder parameter name, for tariner we get trainer name, for pipeline we get pipeline task
|
||||
"""
|
||||
for node in ast.walk(test_method):
|
||||
if builder is None: # direct call build_trainer
|
||||
for node in ast.walk(test_method):
|
||||
if (isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id == system_builder_func_name):
|
||||
return get_system_builder_parameter_value(
|
||||
node, test_method, setup_variables,
|
||||
builder_parameter_name)
|
||||
elif (isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Attribute)
|
||||
and node.func.attr == builder.name):
|
||||
return get_custom_builder_parameter_name(node.args, node.keywords,
|
||||
builder, builder_call,
|
||||
builder_parameter_name)
|
||||
elif (isinstance(node, ast.Expr) and isinstance(node.value, ast.Call)
|
||||
and isinstance(node.value.func, ast.Name)
|
||||
and node.value.func.id == builder.name):
|
||||
return get_custom_builder_parameter_name(node.value.args,
|
||||
node.value.keywords,
|
||||
builder, builder_call,
|
||||
builder_parameter_name)
|
||||
elif (isinstance(node, ast.Expr) and isinstance(node.value, ast.Call)
|
||||
and isinstance(node.value.func, ast.Attribute)
|
||||
and node.value.func.attr == builder.name):
|
||||
# self.class_method_builder
|
||||
return get_custom_builder_parameter_name(node.value.args,
|
||||
node.value.keywords,
|
||||
builder, builder_call,
|
||||
builder_parameter_name)
|
||||
elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
||||
for arg in node.value.args:
|
||||
if isinstance(arg, ast.Name) and arg.id == builder.name:
|
||||
# self.start(train_func, num_gpus=2, **kwargs)
|
||||
return get_custom_builder_parameter_name(
|
||||
None, None, builder, builder_call,
|
||||
builder_parameter_name)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_class_constructor(test_method, modified_register_modules, module_name):
|
||||
# module_name 'TRAINERS' | 'PIPELINES'
|
||||
for node in ast.walk(test_method):
|
||||
if isinstance(node, ast.Assign) and isinstance(node.value, ast.Call):
|
||||
# trainer = CsanmtTranslationTrainer(model=model_id)
|
||||
for modified_register_module in modified_register_modules:
|
||||
if isinstance(node.value.func, ast.Name) and \
|
||||
node.value.func.id == modified_register_module[3] and \
|
||||
modified_register_module[0] == module_name:
|
||||
if module_name == 'TRAINERS':
|
||||
return modified_register_module[2]
|
||||
elif module_name == 'PIPELINES':
|
||||
return modified_register_module[1] # pipeline
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def analysis_trainer_test_suite(test_file, modified_register_modules):
|
||||
tested_trainers = []
|
||||
with open(test_file, 'rb') as tsf:
|
||||
src = tsf.read()
|
||||
# get test file global function and test class
|
||||
test_suite_root = ast.parse(src, test_file)
|
||||
test_suite_analyzer = AnalysisTestFile(
|
||||
test_file, SYSTEM_TRAINER_BUILDER_FUNCTION_NAME)
|
||||
test_suite_analyzer.visit(test_suite_root)
|
||||
|
||||
for test_class in test_suite_analyzer.test_classes:
|
||||
test_class_analyzer = AnalysisTestClass(
|
||||
test_class, SYSTEM_TRAINER_BUILDER_FUNCTION_NAME)
|
||||
test_class_analyzer.visit(test_class)
|
||||
for test_method in test_class_analyzer.test_methods:
|
||||
for idx, custom_global_builder in enumerate(
|
||||
test_suite_analyzer.custom_global_builders
|
||||
): # custom test method is global method
|
||||
trainer_name = get_builder_parameter_value(
|
||||
test_method, test_class_analyzer.setup_variables,
|
||||
custom_global_builder,
|
||||
test_suite_analyzer.custom_global_builder_calls[idx],
|
||||
SYSTEM_TRAINER_BUILDER_FUNCTION_NAME,
|
||||
SYSTEM_TRAINER_BUILDER_PARAMETER_NAME)
|
||||
if trainer_name is not None:
|
||||
tested_trainers.append(trainer_name)
|
||||
for idx, custom_class_method_builder in enumerate(
|
||||
test_class_analyzer.custom_class_method_builders
|
||||
): # custom class method builder.
|
||||
trainer_name = get_builder_parameter_value(
|
||||
test_method, test_class_analyzer.setup_variables,
|
||||
custom_class_method_builder,
|
||||
test_class_analyzer.custom_class_method_builder_calls[idx],
|
||||
SYSTEM_TRAINER_BUILDER_FUNCTION_NAME,
|
||||
SYSTEM_TRAINER_BUILDER_PARAMETER_NAME)
|
||||
if trainer_name is not None:
|
||||
tested_trainers.append(trainer_name)
|
||||
|
||||
trainer_name = get_builder_parameter_value(
|
||||
test_method, test_class_analyzer.setup_variables, None, None,
|
||||
SYSTEM_TRAINER_BUILDER_FUNCTION_NAME,
|
||||
SYSTEM_TRAINER_BUILDER_PARAMETER_NAME
|
||||
) # direct call the build_trainer
|
||||
if trainer_name is not None:
|
||||
tested_trainers.append(trainer_name)
|
||||
|
||||
if len(tested_trainers
|
||||
) == 0: # suppose no builder call is direct construct.
|
||||
trainer_name = get_class_constructor(
|
||||
test_method, modified_register_modules, 'TRAINERS')
|
||||
if trainer_name is not None:
|
||||
tested_trainers.append(trainer_name)
|
||||
|
||||
return tested_trainers
|
||||
|
||||
|
||||
def get_test_parameters(test_method, analyzer):
|
||||
for node in ast.walk(test_method):
|
||||
func = None
|
||||
if not isinstance(node, ast.FunctionDef):
|
||||
continue
|
||||
for statement in node.body:
|
||||
if isinstance(statement, ast.Assign):
|
||||
analyzer.get_final_variables(statement)
|
||||
if not func and isinstance(statement, ast.Assign):
|
||||
if isinstance(statement.value, ast.Call) and isinstance(
|
||||
statement.value.func, ast.Name) and ( # noqa W504
|
||||
'pipeline' in statement.value.func.id
|
||||
or 'Pipeline' in statement.value.func.id):
|
||||
func = statement.targets[0].id
|
||||
if func and isinstance(statement, ast.Assign) and isinstance(
|
||||
statement.value, ast.Call) and isinstance(
|
||||
statement.value.func, ast.Name):
|
||||
if statement.value.func.id == func:
|
||||
inputs = statement.value.args
|
||||
return analyzer.get_ast_value(inputs)
|
||||
|
||||
|
||||
def analysis_pipeline_test_examples(test_file):
|
||||
examples = []
|
||||
with open(test_file, 'rb') as tsf:
|
||||
src = tsf.read()
|
||||
test_root = ast.parse(src, test_file)
|
||||
test_file_analyzer = AnalysisTestFile(
|
||||
test_file, SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME)
|
||||
test_file_analyzer.visit(test_root)
|
||||
|
||||
for test_class in test_file_analyzer.test_classes:
|
||||
test_class_analyzer = AnalysisTestClass(
|
||||
test_class, SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME,
|
||||
test_file_analyzer)
|
||||
test_class_analyzer.visit(test_class)
|
||||
for test_method in test_class_analyzer.test_methods:
|
||||
parameters = get_test_parameters(test_method, test_class_analyzer)
|
||||
examples.append(parameters)
|
||||
return examples
|
||||
|
||||
|
||||
def analysis_pipeline_test_suite(test_file, modified_register_modules):
|
||||
tested_tasks = []
|
||||
with open(test_file, 'rb') as tsf:
|
||||
src = tsf.read()
|
||||
# get test file global function and test class
|
||||
test_suite_root = ast.parse(src, test_file)
|
||||
test_suite_analyzer = AnalysisTestFile(
|
||||
test_file, SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME)
|
||||
test_suite_analyzer.visit(test_suite_root)
|
||||
|
||||
for test_class in test_suite_analyzer.test_classes:
|
||||
test_class_analyzer = AnalysisTestClass(
|
||||
test_class, SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME)
|
||||
test_class_analyzer.visit(test_class)
|
||||
for test_method in test_class_analyzer.test_methods:
|
||||
for idx, custom_global_builder in enumerate(
|
||||
test_suite_analyzer.custom_global_builders
|
||||
): # custom test method is global method
|
||||
task_name = get_builder_parameter_value(
|
||||
test_method, test_class_analyzer.setup_variables,
|
||||
custom_global_builder,
|
||||
test_suite_analyzer.custom_global_builder_calls[idx],
|
||||
SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME,
|
||||
SYSTEM_PIPELINE_BUILDER_PARAMETER_NAME)
|
||||
if task_name is not None:
|
||||
tested_tasks.append(task_name)
|
||||
for idx, custom_class_method_builder in enumerate(
|
||||
test_class_analyzer.custom_class_method_builders
|
||||
): # custom class method builder.
|
||||
task_name = get_builder_parameter_value(
|
||||
test_method, test_class_analyzer.setup_variables,
|
||||
custom_class_method_builder,
|
||||
test_class_analyzer.custom_class_method_builder_calls[idx],
|
||||
SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME,
|
||||
SYSTEM_PIPELINE_BUILDER_PARAMETER_NAME)
|
||||
if task_name is not None:
|
||||
tested_tasks.append(task_name)
|
||||
|
||||
task_name = get_builder_parameter_value(
|
||||
test_method, test_class_analyzer.setup_variables, None, None,
|
||||
SYSTEM_PIPELINE_BUILDER_FUNCTION_NAME,
|
||||
SYSTEM_PIPELINE_BUILDER_PARAMETER_NAME
|
||||
) # direct call the build_trainer
|
||||
if task_name is not None:
|
||||
tested_tasks.append(task_name)
|
||||
|
||||
if len(tested_tasks
|
||||
) == 0: # suppose no builder call is direct construct.
|
||||
task_name = get_class_constructor(test_method,
|
||||
modified_register_modules,
|
||||
'PIPELINES')
|
||||
if task_name is not None:
|
||||
tested_tasks.append(task_name)
|
||||
|
||||
return tested_tasks
|
||||
|
||||
|
||||
def get_pipelines_trainers_test_info(register_modules):
|
||||
all_trainer_cases = [
|
||||
os.path.join(dp, f) for dp, dn, filenames in os.walk(
|
||||
os.path.join(os.getcwd(), 'tests', 'trainers')) for f in filenames
|
||||
if os.path.splitext(f)[1] == '.py'
|
||||
]
|
||||
trainer_test_info = {}
|
||||
for test_file in all_trainer_cases:
|
||||
tested_trainers = analysis_trainer_test_suite(test_file,
|
||||
register_modules)
|
||||
if len(tested_trainers) == 0:
|
||||
logger.warn('test_suite: %s has no trainer name' % test_file)
|
||||
else:
|
||||
tested_trainers = list(set(tested_trainers))
|
||||
for trainer_name in tested_trainers:
|
||||
if trainer_name not in trainer_test_info:
|
||||
trainer_test_info[trainer_name] = []
|
||||
trainer_test_info[trainer_name].append(test_file)
|
||||
|
||||
pipeline_test_info = {}
|
||||
all_pipeline_cases = [
|
||||
os.path.join(dp, f) for dp, dn, filenames in os.walk(
|
||||
os.path.join(os.getcwd(), 'tests', 'pipelines')) for f in filenames
|
||||
if os.path.splitext(f)[1] == '.py'
|
||||
]
|
||||
for test_file in all_pipeline_cases:
|
||||
try:
|
||||
tested_pipelines = analysis_pipeline_test_suite(
|
||||
test_file, register_modules)
|
||||
except Exception:
|
||||
logger.warn('test_suite: %s analysis failed, skipt it' % test_file)
|
||||
continue
|
||||
if len(tested_pipelines) == 0:
|
||||
logger.warn('test_suite: %s has no pipeline task' % test_file)
|
||||
else:
|
||||
tested_pipelines = list(set(tested_pipelines))
|
||||
for pipeline_task in tested_pipelines:
|
||||
if pipeline_task not in pipeline_test_info:
|
||||
pipeline_test_info[pipeline_task] = []
|
||||
pipeline_test_info[pipeline_task].append(test_file)
|
||||
return pipeline_test_info, trainer_test_info
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
all_pipeline_cases = [
|
||||
os.path.join(dp, f) for dp, dn, filenames in os.walk(
|
||||
os.path.join(os.getcwd(), 'tests', 'pipelines')) for f in filenames
|
||||
if os.path.splitext(f)[1] == '.py'
|
||||
]
|
||||
for test_file in all_pipeline_cases:
|
||||
print('\n', test_file)
|
||||
tasks = analysis_pipeline_test_suite(test_file, None)
|
||||
examples = analysis_pipeline_test_examples(test_file)
|
||||
|
||||
from modelsope.metainfo import Tasks
|
||||
for task, example in zip(tasks, examples):
|
||||
task_convert = f't = Tasks.{task}'
|
||||
exec(task_convert)
|
||||
print(t, example)
|
||||
@@ -1,410 +0,0 @@
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
|
||||
from __future__ import print_function
|
||||
import ast
|
||||
import importlib.util
|
||||
import os
|
||||
import pkgutil
|
||||
import site
|
||||
import sys
|
||||
|
||||
import json
|
||||
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class AnalysisSourceFileDefines(ast.NodeVisitor):
|
||||
"""Analysis source file function, class, global variable defines.
|
||||
"""
|
||||
|
||||
def __init__(self, source_file_path) -> None:
|
||||
super().__init__()
|
||||
self.global_variables = []
|
||||
self.functions = []
|
||||
self.classes = []
|
||||
self.async_functions = []
|
||||
self.symbols = []
|
||||
|
||||
self.source_file_path = source_file_path
|
||||
rel_file_path = source_file_path
|
||||
if os.path.isabs(source_file_path):
|
||||
rel_file_path = os.path.relpath(source_file_path, os.getcwd())
|
||||
|
||||
if rel_file_path.endswith('__init__.py'): # processing package
|
||||
self.base_module_name = os.path.dirname(rel_file_path).replace(
|
||||
'/', '.')
|
||||
else: # import x.y.z z is the filename
|
||||
self.base_module_name = rel_file_path.replace('/', '.').replace(
|
||||
'.py', '')
|
||||
self.symbols.append(self.base_module_name)
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef):
|
||||
self.symbols.append(self.base_module_name + '.' + node.name)
|
||||
self.classes.append(node.name)
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef):
|
||||
self.symbols.append(self.base_module_name + '.' + node.name)
|
||||
self.functions.append(node.name)
|
||||
|
||||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
||||
self.symbols.append(self.base_module_name + '.' + node.name)
|
||||
self.async_functions.append(node.name)
|
||||
|
||||
def visit_Assign(self, node: ast.Assign):
|
||||
for tg in node.targets:
|
||||
if isinstance(tg, ast.Name):
|
||||
self.symbols.append(self.base_module_name + '.' + tg.id)
|
||||
self.global_variables.append(tg.id)
|
||||
|
||||
|
||||
def is_relative_import(path):
|
||||
# from .x import y or from ..x import y
|
||||
return path.startswith('.')
|
||||
|
||||
|
||||
def convert_to_path(name):
|
||||
if name.startswith('.'):
|
||||
remainder = name.lstrip('.')
|
||||
dot_count = (len(name) - len(remainder))
|
||||
prefix = '../' * (dot_count - 1)
|
||||
else:
|
||||
remainder = name
|
||||
dot_count = 0
|
||||
prefix = ''
|
||||
filename = prefix + os.path.join(*remainder.split('.'))
|
||||
return filename
|
||||
|
||||
|
||||
def resolve_relative_import(source_file_path, module_name, all_symbols):
|
||||
current_package = os.path.dirname(source_file_path).replace('/', '.')
|
||||
absolute_name = importlib.util.resolve_name(module_name,
|
||||
current_package) # get
|
||||
return resolve_absolute_import(absolute_name, all_symbols)
|
||||
|
||||
|
||||
def resolve_absolute_import(module_name, all_symbols):
|
||||
# direct imports
|
||||
if module_name in all_symbols:
|
||||
return all_symbols[module_name]
|
||||
|
||||
# some symbol import by package __init__.py, we need find the real file which define the symbel.
|
||||
parent, sub = module_name.rsplit('.', 1)
|
||||
|
||||
# case module_name is a python Definition
|
||||
for symbol, symbol_path in all_symbols.items():
|
||||
if symbol.startswith(parent) and symbol.endswith(sub):
|
||||
return all_symbols[symbol]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class IndirectDefines(ast.NodeVisitor):
|
||||
"""Analysis source file function, class, global variable defines.
|
||||
"""
|
||||
|
||||
def __init__(self, source_file_path, all_symbols,
|
||||
file_symbols_map) -> None:
|
||||
super().__init__()
|
||||
self.symbols_map = {
|
||||
} # key symbol name in current file, value the real file path.
|
||||
self.all_symbols = all_symbols
|
||||
self.file_symbols_map = file_symbols_map
|
||||
self.source_file_path = source_file_path
|
||||
|
||||
rel_file_path = source_file_path
|
||||
if os.path.isabs(source_file_path):
|
||||
rel_file_path = os.path.relpath(source_file_path, os.getcwd())
|
||||
|
||||
if rel_file_path.endswith('__init__.py'): # processing package
|
||||
self.base_module_name = os.path.dirname(rel_file_path).replace(
|
||||
'/', '.')
|
||||
else: # import x.y.z z is the filename
|
||||
self.base_module_name = rel_file_path.replace('/', '.').replace(
|
||||
'.py', '')
|
||||
|
||||
# import from will get the symbol in current file.
|
||||
# from a import b, will get b in current file.
|
||||
def visit_ImportFrom(self, node):
|
||||
# level 0 absolute import such as from os.path import join
|
||||
# level 1 from .x import y
|
||||
# level 2 from ..x import y
|
||||
module_name = '.' * node.level + (node.module or '')
|
||||
for alias in node.names:
|
||||
file_path = None
|
||||
if alias.name == '*': # from x import *
|
||||
if is_relative_import(module_name):
|
||||
# resolve model path.
|
||||
file_path = resolve_relative_import(
|
||||
self.source_file_path, module_name, self.all_symbols)
|
||||
elif module_name.startswith('modelscope'):
|
||||
file_path = resolve_absolute_import(
|
||||
module_name, self.all_symbols)
|
||||
else:
|
||||
file_path = None # ignore other package.
|
||||
if file_path is not None:
|
||||
for symbol in self.file_symbols_map[file_path][1:]:
|
||||
symbol_name = symbol.split('.')[-1]
|
||||
self.symbols_map[self.base_module_name
|
||||
+ symbol_name] = file_path
|
||||
else:
|
||||
if not module_name.endswith('.'):
|
||||
module_name = module_name + '.'
|
||||
name = module_name + alias.name
|
||||
if alias.asname is not None:
|
||||
current_module_name = self.base_module_name + '.' + alias.asname
|
||||
else:
|
||||
current_module_name = self.base_module_name + '.' + alias.name
|
||||
if is_relative_import(name):
|
||||
# resolve model path.
|
||||
file_path = resolve_relative_import(
|
||||
self.source_file_path, name, self.all_symbols)
|
||||
elif name.startswith('modelscope'):
|
||||
file_path = resolve_absolute_import(name, self.all_symbols)
|
||||
if file_path is not None:
|
||||
self.symbols_map[current_module_name] = file_path
|
||||
|
||||
|
||||
class AnalysisSourceFileImports(ast.NodeVisitor):
|
||||
"""Analysis source file imports
|
||||
List imports of the modelscope.
|
||||
"""
|
||||
|
||||
def __init__(self, source_file_path, all_symbols) -> None:
|
||||
super().__init__()
|
||||
self.imports = []
|
||||
self.source_file_path = source_file_path
|
||||
self.all_symbols = all_symbols
|
||||
|
||||
def visit_Import(self, node):
|
||||
"""Processing import x,y,z or import os.path as osp"""
|
||||
for alias in node.names:
|
||||
if alias.name.startswith('modelscope'):
|
||||
file_path = resolve_absolute_import(alias.name,
|
||||
self.all_symbols)
|
||||
self.imports.append(os.path.relpath(file_path, os.getcwd()))
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
# level 0 absolute import such as from os.path import join
|
||||
# level 1 from .x import y
|
||||
# level 2 from ..x import y
|
||||
module_name = '.' * node.level + (node.module or '')
|
||||
for alias in node.names:
|
||||
if alias.name == '*': # from x import *
|
||||
if is_relative_import(module_name):
|
||||
# resolve model path.
|
||||
file_path = resolve_relative_import(
|
||||
self.source_file_path, module_name, self.all_symbols)
|
||||
elif module_name.startswith('modelscope'):
|
||||
file_path = resolve_absolute_import(
|
||||
module_name, self.all_symbols)
|
||||
else:
|
||||
file_path = None # ignore other package.
|
||||
else:
|
||||
if not module_name.endswith('.'):
|
||||
module_name = module_name + '.'
|
||||
name = module_name + alias.name
|
||||
if is_relative_import(name):
|
||||
# resolve model path.
|
||||
file_path = resolve_relative_import(
|
||||
self.source_file_path, name, self.all_symbols)
|
||||
if file_path is None:
|
||||
logger.warning(
|
||||
'File: %s, import %s%s not exist!' %
|
||||
(self.source_file_path, module_name, alias.name))
|
||||
elif name.startswith('modelscope'):
|
||||
file_path = resolve_absolute_import(name, self.all_symbols)
|
||||
if file_path is None:
|
||||
logger.warning(
|
||||
'File: %s, import %s%s not exist!' %
|
||||
(self.source_file_path, module_name, alias.name))
|
||||
else:
|
||||
file_path = None # ignore other package.
|
||||
|
||||
if file_path is not None:
|
||||
if file_path.startswith(site.getsitepackages()[0]):
|
||||
self.imports.append(
|
||||
os.path.relpath(file_path,
|
||||
site.getsitepackages()[0]))
|
||||
else:
|
||||
self.imports.append(
|
||||
os.path.relpath(file_path, os.getcwd()))
|
||||
elif module_name.startswith('modelscope'):
|
||||
logger.warning(
|
||||
'File: %s, import %s%s not exist!' %
|
||||
(self.source_file_path, module_name, alias.name))
|
||||
|
||||
|
||||
class AnalysisSourceFileRegisterModules(ast.NodeVisitor):
|
||||
"""Get register_module call of the python source file.
|
||||
|
||||
|
||||
Args:
|
||||
ast (NodeVisitor): The ast node.
|
||||
|
||||
Examples:
|
||||
>>> with open(source_file_path, "rb") as f:
|
||||
>>> src = f.read()
|
||||
>>> analyzer = AnalysisSourceFileRegisterModules(source_file_path)
|
||||
>>> analyzer.visit(ast.parse(src, filename=source_file_path))
|
||||
"""
|
||||
|
||||
def __init__(self, source_file_path) -> None:
|
||||
super().__init__()
|
||||
self.source_file_path = source_file_path
|
||||
self.register_modules = []
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef):
|
||||
if len(node.decorator_list) > 0:
|
||||
for dec in node.decorator_list:
|
||||
if isinstance(dec, ast.Call):
|
||||
target_name = ''
|
||||
module_name_param = ''
|
||||
task_param = ''
|
||||
if isinstance(dec.func, ast.Attribute
|
||||
) and dec.func.attr == 'register_module':
|
||||
target_name = dec.func.value.id # MODELS
|
||||
if len(dec.args) > 0:
|
||||
if isinstance(dec.args[0], ast.Attribute):
|
||||
task_param = dec.args[0].attr
|
||||
elif isinstance(dec.args[0], ast.Constant):
|
||||
task_param = dec.args[0].value
|
||||
if len(dec.keywords) > 0:
|
||||
for kw in dec.keywords:
|
||||
if kw.arg == 'module_name':
|
||||
if isinstance(kw.value, ast.Str):
|
||||
module_name_param = kw.value.s
|
||||
else:
|
||||
module_name_param = kw.value.attr
|
||||
elif kw.arg == 'group_key':
|
||||
if isinstance(kw.value, ast.Str):
|
||||
task_param = kw.value.s
|
||||
elif isinstance(kw.value, ast.Name):
|
||||
task_param = kw.value.id
|
||||
else:
|
||||
task_param = kw.value.attr
|
||||
if task_param == '' and module_name_param == '':
|
||||
logger.warn(
|
||||
'File %s %s.register_module has no parameters'
|
||||
% (self.source_file_path, target_name))
|
||||
continue
|
||||
if target_name == 'PIPELINES' and task_param == '':
|
||||
logger.warn(
|
||||
'File %s %s.register_module has no task_param'
|
||||
% (self.source_file_path, target_name))
|
||||
self.register_modules.append(
|
||||
(target_name, task_param, module_name_param,
|
||||
node.name)) # PIPELINES, task, module, class_name
|
||||
|
||||
|
||||
def get_imported_files(file_path, all_symbols):
|
||||
"""Get file dependencies.
|
||||
"""
|
||||
if os.path.isabs(file_path):
|
||||
file_path = os.path.relpath(file_path, os.getcwd())
|
||||
with open(file_path, 'rb') as f:
|
||||
src = f.read()
|
||||
analyzer = AnalysisSourceFileImports(file_path, all_symbols)
|
||||
analyzer.visit(ast.parse(src, filename=file_path))
|
||||
return list(set(analyzer.imports))
|
||||
|
||||
|
||||
def path_to_module_name(file_path):
|
||||
if os.path.isabs(file_path):
|
||||
file_path = os.path.relpath(file_path, os.getcwd())
|
||||
module_name = os.path.dirname(file_path).replace('/', '.')
|
||||
return module_name
|
||||
|
||||
|
||||
def get_file_register_modules(file_path):
|
||||
with open(file_path, 'rb') as f:
|
||||
src = f.read()
|
||||
analyzer = AnalysisSourceFileRegisterModules(file_path)
|
||||
analyzer.visit(ast.parse(src, filename=file_path))
|
||||
return analyzer.register_modules
|
||||
|
||||
|
||||
def get_file_defined_symbols(file_path):
|
||||
if os.path.isabs(file_path):
|
||||
file_path = os.path.relpath(file_path, os.getcwd())
|
||||
with open(file_path, 'rb') as f:
|
||||
src = f.read()
|
||||
analyzer = AnalysisSourceFileDefines(file_path)
|
||||
analyzer.visit(ast.parse(src, filename=file_path))
|
||||
return analyzer.symbols
|
||||
|
||||
|
||||
def get_indirect_symbols(file_path, symbols, file_symbols_map):
|
||||
if os.path.isabs(file_path):
|
||||
file_path = os.path.relpath(file_path, os.getcwd())
|
||||
with open(file_path, 'rb') as f:
|
||||
src = f.read()
|
||||
analyzer = IndirectDefines(file_path, symbols, file_symbols_map)
|
||||
analyzer.visit(ast.parse(src, filename=file_path))
|
||||
return analyzer.symbols_map
|
||||
|
||||
|
||||
def get_import_map():
|
||||
all_files = [
|
||||
os.path.join(dp, f) for dp, dn, filenames in os.walk(
|
||||
os.path.join(os.getcwd(), 'modelscope')) for f in filenames
|
||||
if os.path.splitext(f)[1] == '.py'
|
||||
]
|
||||
all_symbols = {}
|
||||
file_symbols_map = {}
|
||||
for f in all_files:
|
||||
file_path = os.path.relpath(f, os.getcwd())
|
||||
file_symbols_map[file_path] = get_file_defined_symbols(f)
|
||||
for s in file_symbols_map[file_path]:
|
||||
all_symbols[s] = file_path
|
||||
|
||||
# get indirect(imported) symbols, refer to origin define.
|
||||
for f in all_files:
|
||||
for name, real_path in get_indirect_symbols(f, all_symbols,
|
||||
file_symbols_map).items():
|
||||
all_symbols[name] = os.path.relpath(real_path, os.getcwd())
|
||||
|
||||
with open('symbols.json', 'w') as f:
|
||||
json.dump(all_symbols, f)
|
||||
import_map = {}
|
||||
for f in all_files:
|
||||
files = get_imported_files(f, all_symbols)
|
||||
import_map[os.path.relpath(f, os.getcwd())] = files
|
||||
|
||||
return import_map
|
||||
|
||||
|
||||
def get_reverse_import_map():
|
||||
all_files = [
|
||||
os.path.join(dp, f) for dp, dn, filenames in os.walk(
|
||||
os.path.join(os.getcwd(), 'modelscope')) for f in filenames
|
||||
if os.path.splitext(f)[1] == '.py'
|
||||
]
|
||||
import_map = get_import_map()
|
||||
|
||||
reverse_depend_map = {}
|
||||
for f in all_files:
|
||||
depend_by = []
|
||||
for k, v in import_map.items():
|
||||
if f in v and f != k:
|
||||
depend_by.append(k)
|
||||
reverse_depend_map[f] = depend_by
|
||||
|
||||
return reverse_depend_map, import_map
|
||||
|
||||
|
||||
def get_all_register_modules():
|
||||
all_files = [
|
||||
os.path.join(dp, f) for dp, dn, filenames in os.walk(
|
||||
os.path.join(os.getcwd(), 'modelscope')) for f in filenames
|
||||
if os.path.splitext(f)[1] == '.py'
|
||||
]
|
||||
all_register_modules = []
|
||||
for f in all_files:
|
||||
all_register_modules.extend(get_file_register_modules(f))
|
||||
return all_register_modules
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
Reference in New Issue
Block a user