├── .coveragerc ├── .dockerignore ├── .editorconfig ├── .github ├── lock.yml ├── no-response.yml └── workflows │ ├── automerge.yml │ ├── dev-release.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yml ├── .spdx-license-header.txt ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── SECURITY.rst ├── docker ├── .env.test ├── Dockerfile ├── docker-compose.test.yml ├── mlflow-entrypoint.sh ├── wait-for-it.sh └── wait-for-postgres.sh ├── docs ├── Makefile ├── changelog.rst ├── conf.py ├── contributing.rst ├── develop.rst ├── index.rst ├── install.rst ├── make.bat ├── mlflow_rest_client.artifact.rst ├── mlflow_rest_client.client.rst ├── mlflow_rest_client.experiment.rst ├── mlflow_rest_client.model.rst ├── mlflow_rest_client.page.rst ├── mlflow_rest_client.run.rst ├── mlflow_rest_client.tag.rst ├── security.rst └── usage.rst ├── mlflow_rest_client ├── VERSION ├── __init__.py ├── artifact.py ├── experiment.py ├── internal.py ├── mlflow_rest_client.py ├── model.py ├── page.py ├── run.py ├── tag.py ├── timestamp.py └── version.py ├── pyproject.toml ├── requirements-dev.txt ├── requirements-doc.txt ├── requirements-test.txt ├── requirements.txt ├── samples ├── __init__.py ├── sample.py └── sklearn_sample.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_integration ├── __init__.py ├── conftest.py └── test_mlflow_rest_client.py └── test_unit ├── __init__.py ├── conftest.py ├── test_model.py ├── test_page.py ├── test_run.py └── test_tag.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | tests/* 5 | parallel = true 6 | data_file = reports/.coverage 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | **/__pycache__ 3 | .env* 4 | .git 5 | .postgresql 6 | .pytest_cache 7 | .scannerwork 8 | .tox 9 | **/docker-compose* 10 | reports 11 | sonar* 12 | test_venv 13 | venv 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=true 5 | indent_style=space 6 | indent_size=4 7 | max_line_length=120 8 | 9 | [{.babelrc,.stylelintrc,.eslintrc,*.json,*.jsb3,*.jsb2,*.bowerrc}] 10 | indent_style=space 11 | indent_size=2 12 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 60 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: [enhancement, help wanted] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: [outdated] 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: false 18 | 19 | # Assign `resolved` as the reason for locking. Set to `false` to disable 20 | setLockReason: true 21 | 22 | # Limit to only `issues` or `pulls` 23 | # only: issues 24 | 25 | # Optionally, specify configuration settings just for `issues` or `pulls` 26 | # issues: 27 | # exemptLabels: 28 | # - help-wanted 29 | # lockLabel: outdated 30 | 31 | # pulls: 32 | # daysUntilLock: 30 33 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 30 5 | # Label requiring a response 6 | responseRequiredLabel: awaiting response 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because it has been awaiting a response for too long. 10 | When you have time to to work with the maintainers to resolve this issue, please post a new comment and it will be re-opened. 11 | If the issue has been locked for editing by the time you return to it, please open a new issue and reference this one. Thank you for taking the time to improve setuptools-git-versioning! 12 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Automerge 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | automerge: 8 | name: Enable pull request automerge 9 | runs-on: ubuntu-latest 10 | if: github.event.pull_request.user.login == 'pre-commit-ci[bot]' || github.event.pull_request.user.login == 'dependabot[bot]' 11 | 12 | steps: 13 | - uses: alexwilson/enable-github-automerge-action@2.0.0 14 | with: 15 | github-token: ${{ secrets.AUTOMERGE_TOKEN }} 16 | merge-method: REBASE 17 | 18 | autoapprove: 19 | name: Automatically approve pull request 20 | needs: [automerge] 21 | runs-on: ubuntu-latest 22 | if: github.event.pull_request.user.login == 'pre-commit-ci[bot]' || github.event.pull_request.user.login == 'dependabot[bot]' 23 | 24 | steps: 25 | - uses: hmarr/auto-approve-action@v4 26 | with: 27 | github-token: ${{ secrets.AUTOMERGE_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/dev-release.yml: -------------------------------------------------------------------------------- 1 | name: Dev release 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - dependabot/** 7 | - pre-commit-ci-update-config 8 | workflow_dispatch: 9 | 10 | env: 11 | DEFAULT_PYTHON: '3.12' 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | wait-code-analysis: 19 | name: Release package 20 | runs-on: ubuntu-latest 21 | if: github.repository == 'MobileTeleSystems/mlflow-rest-client' # prevent running on forks 22 | 23 | environment: 24 | name: test-pypi 25 | url: https://test.pypi.org/p/mlflow-rest-client 26 | permissions: 27 | id-token: write # to auth in Test PyPI 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 36 | id: python 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: ${{ env.DEFAULT_PYTHON }} 40 | 41 | - name: Upgrade pip 42 | run: python -m pip install --upgrade pip setuptools wheel 43 | 44 | - name: Build package 45 | run: python setup.py sdist bdist_wheel 46 | 47 | - name: Publish package 48 | uses: pypa/gh-action-pypi-publish@release/v1 49 | with: 50 | repository-url: https://test.pypi.org/legacy/ 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+.[0-9]+.[0-9]+' 7 | 8 | env: 9 | DEFAULT_PYTHON: '3.12' 10 | 11 | jobs: 12 | release: 13 | name: Release package 14 | runs-on: ubuntu-latest 15 | if: github.repository == 'MobileTeleSystems/mlflow-rest-client' # prevent running on forks 16 | 17 | environment: 18 | name: pypi 19 | url: https://pypi.org/p/mlflow-rest-client 20 | permissions: 21 | id-token: write # to auth in PyPI 22 | contents: write # to create Github release 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 31 | id: python 32 | uses: actions/setup-python@v5 33 | with: 34 | python-version: ${{ env.DEFAULT_PYTHON }} 35 | 36 | - name: Upgrade pip 37 | run: python -m pip install --upgrade pip setuptools wheel 38 | 39 | - name: Build package 40 | run: python setup.py sdist bdist_wheel 41 | 42 | - name: Publish package 43 | uses: pypa/gh-action-pypi-publish@release/v1 44 | 45 | - name: Create Github release 46 | id: create_release 47 | uses: softprops/action-gh-release@v1 48 | with: 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | draft: false 51 | prerelease: false 52 | files: | 53 | dist/* 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | DEFAULT_PYTHON: '3.12' 16 | 17 | jobs: 18 | tests: 19 | name: Run tests (${{ matrix.python-version }} on ${{ matrix.os }}, MLflow ${{ matrix.mlflow-version }}) 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | include: 25 | - os: ubuntu-latest 26 | python-version: '3.12' 27 | mlflow-version: 1.23.0 28 | - os: ubuntu-latest 29 | python-version: '3.7' 30 | mlflow-version: 1.17.0 31 | 32 | env: 33 | MLFLOW_HOST: localhost 34 | MLFLOW_PORT: 5000 35 | 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Set up Python ${{ matrix.python-version }} 43 | uses: actions/setup-python@v5 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | 47 | - name: Cache pip 48 | uses: actions/cache@v4 49 | with: 50 | path: ~/.cache/pip 51 | key: pip-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('requirements*.txt') }} 52 | restore-keys: | 53 | pip-${{ runner.os }}-${{ matrix.python-version }}- 54 | pip-${{ runner.os }}- 55 | 56 | - name: Upgrade pip 57 | run: python -m pip install --upgrade pip setuptools wheel 58 | 59 | - name: Install dependencies 60 | run: pip install -r requirements.txt -r requirements-test.txt -r requirements-dev.txt 61 | 62 | - name: Run pylint 63 | run: pylint mlflow_rest_client 64 | 65 | - name: Build package 66 | run: | 67 | python setup.py --version 68 | python setup.py bdist_wheel sdist 69 | 70 | - name: Set up Docker Buildx 71 | uses: docker/setup-buildx-action@v3 72 | 73 | - name: Cache Docker layers 74 | uses: actions/cache@v4 75 | with: 76 | path: /tmp/.buildx-cache 77 | key: buildx-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.mlflow-version }}-${{ hashFiles('docker/Dockerfile') }} 78 | restore-keys: | 79 | buildx-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.mlflow-version }}- 80 | buildx-${{ runner.os }}-${{ matrix.python-version }}- 81 | buildx-${{ runner.os }}- 82 | 83 | - name: Build MLflow image 84 | uses: docker/build-push-action@v5 85 | with: 86 | context: docker 87 | build-args: MLFLOW_VERSION=${{ matrix.mlflow-version }} 88 | push: false 89 | load: true 90 | tags: mlflow-test:${{ matrix.mlflow-version }} 91 | cache-from: type=local,src=/tmp/.buildx-cache 92 | cache-to: type=local,dest=/tmp/.buildx-cache-new 93 | 94 | - name: Move cache 95 | run: | 96 | rm -rf /tmp/.buildx-cache 97 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 98 | 99 | - name: Run MLflow container 100 | env: 101 | MLFLOW_VERSION: ${{ matrix.mlflow-version }} 102 | run: | 103 | docker-compose -f ./docker/docker-compose.test.yml up -d 104 | 105 | - name: Run tests 106 | run: | 107 | mkdir reports/ 108 | pip install -e . 109 | ./docker/wait-for-it.sh -h $MLFLOW_HOST -p $MLFLOW_PORT -t 0 110 | coverage run -m pytest --reruns 5 111 | 112 | - name: Stop MLflow container 113 | if: always() 114 | run: | 115 | docker-compose -f ./docker/docker-compose.test.yml down 116 | 117 | - name: Upload coverage results 118 | uses: actions/upload-artifact@v4 119 | with: 120 | name: code-coverage-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.mlflow-version}} 121 | path: reports/* 122 | 123 | all_done: 124 | name: Tests done 125 | runs-on: ubuntu-latest 126 | needs: [tests] 127 | 128 | steps: 129 | - name: Checkout code 130 | uses: actions/checkout@v4 131 | 132 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 133 | uses: actions/setup-python@v5 134 | with: 135 | python-version: ${{ env.DEFAULT_PYTHON }} 136 | 137 | - name: Cache pip 138 | uses: actions/cache@v4 139 | with: 140 | path: ~/.cache/pip 141 | key: ${{ runner.os }}-python-${{ env.DEFAULT_PYTHON }}-coverage 142 | 143 | - name: Upgrade pip 144 | run: python -m pip install --upgrade pip setuptools wheel 145 | 146 | - name: Install dependencies 147 | run: pip install -I coverage pytest 148 | 149 | - name: Download all coverage reports 150 | uses: actions/download-artifact@v4 151 | with: 152 | path: reports 153 | 154 | - name: Move coverage data to the root folder 155 | run: find reports -type f -exec mv '{}' reports \; 156 | 157 | - name: Generate coverate reports 158 | run: | 159 | coverage combine 160 | coverage xml -o reports/coverage.xml -i 161 | 162 | - name: Check coverage 163 | uses: codecov/codecov-action@v4 164 | with: 165 | token: ${{ secrets.CODECOV_TOKEN }} 166 | directory: ./reports 167 | fail_ci_if_error: true 168 | 169 | - name: All done 170 | run: echo 1 171 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *$py.class 2 | *,cover 3 | *.bak 4 | *.egg 5 | *.egg-info/ 6 | *.iws 7 | *.launch 8 | *.log 9 | *.manifest 10 | *.mo 11 | *.pot 12 | *.py[cod] 13 | *.pyc 14 | *.pydevproject 15 | *.sage.py 16 | *.so 17 | *.spec 18 | *.swp 19 | *.tmp 20 | *~.nib 21 | .Python 22 | .buildpath 23 | *cache 24 | .cache 25 | .cache-main 26 | .classpath 27 | .coverage 28 | .coverage.* 29 | .cproject 30 | .eggs/ 31 | .env 32 | .externalToolBuilders/ 33 | .factorypath 34 | .hypothesis/ 35 | .idea/ 36 | .idea/**/dataSources.ids 37 | .idea/**/dataSources.local.xml 38 | .idea/**/dataSources.xml 39 | .idea/**/dataSources/ 40 | .idea/**/dynamic.xml 41 | .idea/**/gradle.xml 42 | .idea/**/libraries 43 | .idea/**/mongoSettings.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/tasks.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/workspace.xml 48 | .idea/dictionaries 49 | .idea/replstate.xml 50 | .idea/sonarlint 51 | .idea/vcs.xml 52 | .idea_modules/ 53 | .installed.cfg 54 | .ipynb_checkpoints 55 | .loadpath 56 | .metadata 57 | .project 58 | .pydevproject 59 | .python-version 60 | .recommenders 61 | .recommenders/ 62 | .ropeproject 63 | .scala_dependencies 64 | .scannerwork/ 65 | .scrapy 66 | .settings/ 67 | .springBeans 68 | .spyderproject 69 | .spyproject 70 | .target 71 | .tern-project 72 | .texlipse 73 | .tox/ 74 | .venv 75 | .vscode 76 | .webassets-cache 77 | .worksheet 78 | /out/ 79 | /site 80 | ENV/ 81 | __pycache__/ 82 | ansible/ssh* 83 | atlassian-ide-plugin.xml 84 | build/ 85 | celerybeat-schedule 86 | cmake-build-debug/ 87 | com_crashlytics_export_strings.xml 88 | coverage.xml 89 | crashlytics-build.properties 90 | crashlytics.properties 91 | develop-eggs/ 92 | dist/ 93 | docs/*.key 94 | docs/*.tar.gz 95 | docs/_build/ 96 | docs/build 97 | downloads/ 98 | eggs/ 99 | env/ 100 | fabric.properties 101 | htmlcov/ 102 | instance/ 103 | lib/ 104 | lib64/ 105 | local.properties 106 | local_settings.py 107 | nosetests.xml 108 | parts/ 109 | pip-delete-this-directory.txt 110 | pip-log.txt 111 | report.xml 112 | reports/ 113 | sdist/ 114 | target/ 115 | tmp/ 116 | var/ 117 | venv/ 118 | wheels/ 119 | !/tests/bin/ 120 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-case-conflict 7 | - id: check-docstring-first 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: check-vcs-permalinks 12 | - id: check-yaml 13 | - id: file-contents-sorter 14 | files: ^(docker\/\.env.*)$ 15 | - id: requirements-txt-fixer 16 | files: ^(requirements.*\.txt)$ 17 | - id: end-of-file-fixer 18 | exclude: ^.*/VERSION$ 19 | - id: fix-byte-order-marker 20 | - id: fix-encoding-pragma 21 | args: [--remove] 22 | - id: name-tests-test 23 | files: ^tests/(test_integration|test_unit)/.*\.py$ 24 | args: [--django] 25 | - id: trailing-whitespace 26 | - id: detect-private-key 27 | 28 | - repo: https://github.com/Lucas-C/pre-commit-hooks 29 | rev: v1.5.5 30 | hooks: 31 | - id: forbid-tabs 32 | - id: remove-tabs 33 | args: [--whitespaces-count, '2'] 34 | - id: chmod 35 | args: ['644'] 36 | exclude_types: [shell] 37 | - id: chmod 38 | args: ['755'] 39 | types: [shell] 40 | - id: insert-license 41 | files: .*\.py$ 42 | exclude: ^(setup\.py|samples/.*\.py|docs/.*\.py|tests/.*\.py)$ 43 | args: 44 | - --license-filepath 45 | - .spdx-license-header.txt 46 | - --use-current-year 47 | - --no-extra-eol 48 | 49 | - repo: https://github.com/codespell-project/codespell 50 | rev: v2.2.6 51 | hooks: 52 | - id: codespell 53 | args: [-w] 54 | 55 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 56 | rev: v2.12.0 57 | hooks: 58 | - id: pretty-format-yaml 59 | args: [--autofix, --indent, '2', --offset, '2'] 60 | 61 | - repo: https://github.com/lovesegfault/beautysh 62 | rev: v6.2.1 63 | hooks: 64 | - id: beautysh 65 | 66 | - repo: https://github.com/pryorda/dockerfilelint-precommit-hooks 67 | rev: v0.1.0 68 | hooks: 69 | - id: dockerfilelint 70 | 71 | - repo: https://github.com/IamTheFij/docker-pre-commit 72 | rev: v3.0.1 73 | hooks: 74 | - id: docker-compose-check 75 | files: ^docker\/docker-compose.*\.(yaml|yml)$ 76 | 77 | - repo: https://github.com/pycqa/isort 78 | rev: 5.13.2 79 | hooks: 80 | - id: isort 81 | 82 | - repo: https://github.com/pre-commit/pygrep-hooks 83 | rev: v1.10.0 84 | hooks: 85 | - id: python-no-log-warn 86 | - id: python-no-eval 87 | - id: rst-backticks 88 | - id: rst-directive-colons 89 | - id: rst-inline-touching-normal 90 | - id: text-unicode-replacement-char 91 | 92 | - repo: https://github.com/asottile/pyupgrade 93 | rev: v3.15.1 94 | hooks: 95 | - id: pyupgrade 96 | args: [--py37-plus, --keep-runtime-typing] 97 | 98 | - repo: https://github.com/psf/black 99 | rev: 24.2.0 100 | hooks: 101 | - id: black 102 | language_version: python3 103 | 104 | - repo: https://github.com/asottile/blacken-docs 105 | rev: 1.16.0 106 | hooks: 107 | - id: blacken-docs 108 | additional_dependencies: 109 | - black==24.1.1 110 | 111 | - repo: https://github.com/pre-commit/mirrors-mypy 112 | rev: v1.8.0 113 | hooks: 114 | - id: mypy 115 | additional_dependencies: [types-requests] 116 | 117 | - repo: https://github.com/Lucas-C/pre-commit-hooks-bandit 118 | rev: v1.0.6 119 | hooks: 120 | - id: python-bandit-vulnerability-check 121 | # TODO: remove line below after https://github.com/PyCQA/bandit/issues/488 122 | args: [-lll, --recursive, -x, './venv/*,./tests/*,./.pytest_cache/*', .] 123 | 124 | - repo: meta 125 | hooks: 126 | - id: check-hooks-apply 127 | - id: check-useless-excludes 128 | 129 | ci: 130 | skip: 131 | - docker-compose-check # cannot run on pre-commit.ci 132 | - chmod # failing in pre-commit.ci 133 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=missing-class-docstring, 64 | missing-module-docstring, 65 | missing-function-docstring, 66 | duplicate-code, 67 | import-outside-toplevel, 68 | too-few-public-methods, 69 | logging-fstring-interpolation, 70 | unspecified-encoding 71 | 72 | # Enable the message, report, category or checker with the given id(s). You can 73 | # either give multiple identifier separated by comma (,) or put this option 74 | # multiple time (only on the command line, not in the configuration file where 75 | # it should appear only once). See also the "--disable" option for examples. 76 | enable=c-extension-no-member 77 | 78 | 79 | [REPORTS] 80 | 81 | # Python expression which should return a score less than or equal to 10. You 82 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 83 | # which contain the number of messages in each category, as well as 'statement' 84 | # which is the total number of statements analyzed. This score is used by the 85 | # global evaluation report (RP0004). 86 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 87 | 88 | # Template used to display messages. This is a python new-style format string 89 | # used to format the message information. See doc for all details. 90 | #msg-template= 91 | 92 | # Set the output format. Available formats are text, parseable, colorized, json 93 | # and msvs (visual studio). You can also give a reporter class, e.g. 94 | # mypackage.mymodule.MyReporterClass. 95 | output-format=text 96 | 97 | # Tells whether to display a full report or only the messages. 98 | reports=no 99 | 100 | # Activate the evaluation score. 101 | score=yes 102 | 103 | 104 | [REFACTORING] 105 | 106 | # Maximum number of nested blocks for function / method body 107 | max-nested-blocks=5 108 | 109 | # Complete name of functions that never returns. When checking for 110 | # inconsistent-return-statements if a never returning function is called then 111 | # it will be considered as an explicit return statement and no message will be 112 | # printed. 113 | never-returning-functions=sys.exit 114 | 115 | 116 | [VARIABLES] 117 | 118 | # List of additional names supposed to be defined in builtins. Remember that 119 | # you should avoid defining new builtins when possible. 120 | additional-builtins= 121 | 122 | # Tells whether unused global variables should be treated as a violation. 123 | allow-global-unused-variables=yes 124 | 125 | # List of strings which can identify a callback function by name. A callback 126 | # name must start or end with one of those strings. 127 | callbacks=cb_, 128 | _cb 129 | 130 | # A regular expression matching the name of dummy variables (i.e. expected to 131 | # not be used). 132 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 133 | 134 | # Argument names that match this expression will be ignored. Default to name 135 | # with leading underscore. 136 | ignored-argument-names=_.*|^ignored_|^unused_ 137 | 138 | # Tells whether we should check for unused import in __init__ files. 139 | init-import=no 140 | 141 | # List of qualified module names which can have objects that can redefine 142 | # builtins. 143 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 144 | 145 | 146 | [TYPECHECK] 147 | 148 | # List of decorators that produce context managers, such as 149 | # contextlib.contextmanager. Add to this list to register other decorators that 150 | # produce valid context managers. 151 | contextmanager-decorators=contextlib.contextmanager 152 | 153 | # List of members which are set dynamically and missed by pylint inference 154 | # system, and so shouldn't trigger E1101 when accessed. Python regular 155 | # expressions are accepted. 156 | generated-members= 157 | 158 | # Tells whether missing members accessed in mixin class should be ignored. A 159 | # mixin class is detected if its name ends with "mixin" (case insensitive). 160 | ignore-mixin-members=yes 161 | 162 | # Tells whether to warn about missing members when the owner of the attribute 163 | # is inferred to be None. 164 | ignore-none=yes 165 | 166 | # This flag controls whether pylint should warn about no-member and similar 167 | # checks whenever an opaque object is returned when inferring. The inference 168 | # can return multiple potential results while evaluating a Python object, but 169 | # some branches might not be evaluated, which results in partial inference. In 170 | # that case, it might be useful to still emit no-member and other checks for 171 | # the rest of the inferred objects. 172 | ignore-on-opaque-inference=yes 173 | 174 | # List of class names for which member attributes should not be checked (useful 175 | # for classes with dynamically set attributes). This supports the use of 176 | # qualified names. 177 | ignored-classes=optparse.Values,thread._local,_thread._local 178 | 179 | # List of module names for which member attributes should not be checked 180 | # (useful for modules/projects where namespaces are manipulated during runtime 181 | # and thus existing member attributes cannot be deduced by static analysis). It 182 | # supports qualified module names, as well as Unix pattern matching. 183 | ignored-modules= 184 | 185 | # Show a hint with possible names when a member name was not found. The aspect 186 | # of finding the hint is based on edit distance. 187 | missing-member-hint=yes 188 | 189 | # The minimum edit distance a name should have in order to be considered a 190 | # similar match for a missing member name. 191 | missing-member-hint-distance=1 192 | 193 | # The total number of similar names that should be taken in consideration when 194 | # showing a hint for a missing member. 195 | missing-member-max-choices=1 196 | 197 | # List of decorators that change the signature of a decorated function. 198 | signature-mutators= 199 | 200 | 201 | [STRING] 202 | 203 | # This flag controls whether inconsistent-quotes generates a warning when the 204 | # character used as a quote delimiter is used inconsistently within a module. 205 | check-quote-consistency=no 206 | 207 | # This flag controls whether the implicit-str-concat should generate a warning 208 | # on implicit string concatenation in sequences defined over several lines. 209 | check-str-concat-over-line-jumps=no 210 | 211 | 212 | [SPELLING] 213 | 214 | # Limits count of emitted suggestions for spelling mistakes. 215 | max-spelling-suggestions=4 216 | 217 | # Spelling dictionary name. Available dictionaries: none. To make it work, 218 | # install the python-enchant package. 219 | spelling-dict= 220 | 221 | # List of comma separated words that should not be checked. 222 | spelling-ignore-words= 223 | 224 | # A path to a file that contains the private dictionary; one word per line. 225 | spelling-private-dict-file= 226 | 227 | # Tells whether to store unknown words to the private dictionary (see the 228 | # --spelling-private-dict-file option) instead of raising a message. 229 | spelling-store-unknown-words=no 230 | 231 | 232 | [SIMILARITIES] 233 | 234 | # Ignore comments when computing similarities. 235 | ignore-comments=yes 236 | 237 | # Ignore docstrings when computing similarities. 238 | ignore-docstrings=yes 239 | 240 | # Ignore imports when computing similarities. 241 | ignore-imports=no 242 | 243 | # Minimum lines number of a similarity. 244 | min-similarity-lines=4 245 | 246 | 247 | [MISCELLANEOUS] 248 | 249 | # List of note tags to take in consideration, separated by a comma. 250 | notes=FIXME, 251 | XXX, 252 | TODO 253 | 254 | # Regular expression of note tags to take in consideration. 255 | #notes-rgx= 256 | 257 | 258 | [LOGGING] 259 | 260 | # The type of string formatting that logging methods do. `old` means using % 261 | # formatting, `new` is for `{}` formatting. 262 | logging-format-style=new 263 | 264 | # Logging modules to check that the string format arguments are in logging 265 | # function parameter format. 266 | logging-modules=logging 267 | 268 | 269 | [FORMAT] 270 | 271 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 272 | expected-line-ending-format= 273 | 274 | # Regexp for a line that is allowed to be longer than the limit. 275 | ignore-long-lines=^\s*(# )??$ 276 | 277 | # Number of spaces of indent required inside a hanging or continued line. 278 | indent-after-paren=4 279 | 280 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 281 | # tab). 282 | indent-string=' ' 283 | 284 | # Maximum number of characters on a single line. 285 | max-line-length=120 286 | 287 | # Maximum number of lines in a module. 288 | max-module-lines=1000 289 | 290 | # Allow the body of a class to be on the same line as the declaration if body 291 | # contains single statement. 292 | single-line-class-stmt=no 293 | 294 | # Allow the body of an if to be on the same line as the test if there is no 295 | # else. 296 | single-line-if-stmt=no 297 | 298 | 299 | [BASIC] 300 | 301 | # Naming style matching correct argument names. 302 | argument-naming-style=snake_case 303 | 304 | # Regular expression matching correct argument names. Overrides argument- 305 | # naming-style. 306 | #argument-rgx= 307 | 308 | # Naming style matching correct attribute names. 309 | attr-naming-style=snake_case 310 | 311 | # Regular expression matching correct attribute names. Overrides attr-naming- 312 | # style. 313 | #attr-rgx= 314 | 315 | # Bad variable names which should always be refused, separated by a comma. 316 | bad-names=foo, 317 | bar, 318 | baz, 319 | toto, 320 | tutu, 321 | tata 322 | 323 | # Bad variable names regexes, separated by a comma. If names match any regex, 324 | # they will always be refused 325 | bad-names-rgxs= 326 | 327 | # Naming style matching correct class attribute names. 328 | class-attribute-naming-style=any 329 | 330 | # Regular expression matching correct class attribute names. Overrides class- 331 | # attribute-naming-style. 332 | #class-attribute-rgx= 333 | 334 | # Naming style matching correct class names. 335 | class-naming-style=PascalCase 336 | 337 | # Regular expression matching correct class names. Overrides class-naming- 338 | # style. 339 | #class-rgx= 340 | 341 | # Naming style matching correct constant names. 342 | const-naming-style=UPPER_CASE 343 | 344 | # Regular expression matching correct constant names. Overrides const-naming- 345 | # style. 346 | #const-rgx= 347 | 348 | # Minimum line length for functions/classes that require docstrings, shorter 349 | # ones are exempt. 350 | docstring-min-length=-1 351 | 352 | # Naming style matching correct function names. 353 | function-naming-style=snake_case 354 | 355 | # Regular expression matching correct function names. Overrides function- 356 | # naming-style. 357 | #function-rgx= 358 | 359 | # Good variable names which should always be accepted, separated by a comma. 360 | good-names=i, 361 | j, 362 | k, 363 | f, 364 | ex, 365 | e, 366 | db, 367 | Run, 368 | _, 369 | df, 370 | pd, 371 | np, 372 | it, 373 | id, 374 | ip, 375 | dt 376 | 377 | # Good variable names regexes, separated by a comma. If names match any regex, 378 | # they will always be accepted 379 | good-names-rgxs= 380 | 381 | # Include a hint for the correct naming format with invalid-name. 382 | include-naming-hint=no 383 | 384 | # Naming style matching correct inline iteration names. 385 | inlinevar-naming-style=any 386 | 387 | # Regular expression matching correct inline iteration names. Overrides 388 | # inlinevar-naming-style. 389 | #inlinevar-rgx= 390 | 391 | # Naming style matching correct method names. 392 | method-naming-style=snake_case 393 | 394 | # Regular expression matching correct method names. Overrides method-naming- 395 | # style. 396 | #method-rgx= 397 | 398 | # Naming style matching correct module names. 399 | module-naming-style=snake_case 400 | 401 | # Regular expression matching correct module names. Overrides module-naming- 402 | # style. 403 | #module-rgx= 404 | 405 | # Colon-delimited sets of names that determine each other's naming style when 406 | # the name regexes allow several styles. 407 | name-group= 408 | 409 | # Regular expression which should only match function or class names that do 410 | # not require a docstring. 411 | no-docstring-rgx=^_ 412 | 413 | # List of decorators that produce properties, such as abc.abstractproperty. Add 414 | # to this list to register other decorators that produce valid properties. 415 | # These decorators are taken in consideration only for invalid-name. 416 | property-classes=abc.abstractproperty 417 | 418 | # Naming style matching correct variable names. 419 | variable-naming-style=snake_case 420 | 421 | # Regular expression matching correct variable names. Overrides variable- 422 | # naming-style. 423 | #variable-rgx= 424 | 425 | 426 | [IMPORTS] 427 | 428 | # List of modules that can be imported at any level, not just the top level 429 | # one. 430 | allow-any-import-level= 431 | 432 | # Allow wildcard imports from modules that define __all__. 433 | allow-wildcard-with-all=no 434 | 435 | # Analyse import fallback blocks. This can be used to support both Python 2 and 436 | # 3 compatible code, which means that the block might have code that exists 437 | # only in one or another interpreter, leading to false positives when analysed. 438 | analyse-fallback-blocks=no 439 | 440 | # Deprecated modules which should not be used, separated by a comma. 441 | deprecated-modules=optparse,tkinter.tix 442 | 443 | # Create a graph of external dependencies in the given file (report RP0402 must 444 | # not be disabled). 445 | ext-import-graph= 446 | 447 | # Create a graph of every (i.e. internal and external) dependencies in the 448 | # given file (report RP0402 must not be disabled). 449 | import-graph= 450 | 451 | # Create a graph of internal dependencies in the given file (report RP0402 must 452 | # not be disabled). 453 | int-import-graph= 454 | 455 | # Force import order to recognize a module as part of the standard 456 | # compatibility libraries. 457 | known-standard-library= 458 | 459 | # Force import order to recognize a module as part of a third party library. 460 | known-third-party=enchant 461 | 462 | # Couples of modules and preferred modules, separated by a comma. 463 | preferred-modules= 464 | 465 | 466 | [DESIGN] 467 | 468 | # Maximum number of arguments for function / method. 469 | max-args=5 470 | 471 | # Maximum number of attributes for a class (see R0902). 472 | max-attributes=7 473 | 474 | # Maximum number of boolean expressions in an if statement (see R0916). 475 | max-bool-expr=5 476 | 477 | # Maximum number of branch for function / method body. 478 | max-branches=12 479 | 480 | # Maximum number of locals for function / method body. 481 | max-locals=15 482 | 483 | # Maximum number of parents for a class (see R0901). 484 | max-parents=7 485 | 486 | # Maximum number of public methods for a class (see R0904). 487 | max-public-methods=20 488 | 489 | # Maximum number of return / yield for function / method body. 490 | max-returns=6 491 | 492 | # Maximum number of statements in function / method body. 493 | max-statements=50 494 | 495 | # Minimum number of public methods for a class (see R0903). 496 | min-public-methods=2 497 | 498 | 499 | [CLASSES] 500 | 501 | # List of method names used to declare (i.e. assign) instance attributes. 502 | defining-attr-methods=__init__, 503 | __new__, 504 | setUp, 505 | __post_init__ 506 | 507 | # List of member names, which should be excluded from the protected access 508 | # warning. 509 | exclude-protected=_asdict, 510 | _fields, 511 | _replace, 512 | _source, 513 | _make 514 | 515 | # List of valid names for the first argument in a class method. 516 | valid-classmethod-first-arg=cls 517 | 518 | # List of valid names for the first argument in a metaclass class method. 519 | valid-metaclass-classmethod-first-arg=cls 520 | 521 | 522 | [EXCEPTIONS] 523 | 524 | # Exceptions that will emit a warning when being caught. Defaults to 525 | # "BaseException, Exception". 526 | overgeneral-exceptions=BaseException, 527 | Exception 528 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: '3.12' 7 | jobs: 8 | post_checkout: 9 | - git fetch --unshallow || true 10 | 11 | python: 12 | install: 13 | - requirements: requirements-doc.txt 14 | - requirements: requirements-dev.txt 15 | - requirements: requirements.txt 16 | -------------------------------------------------------------------------------- /.spdx-license-header.txt: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ================================================================= 3 | 4 | 2.0 5 | -------------------- 6 | .. changelog:: 7 | :version: 2.0.0 8 | :released: 25.01.2021 11:45 9 | 10 | .. change:: 11 | :tags: general 12 | :tickets: DSX:288 13 | 14 | Change package author to ``__ 15 | 16 | .. change:: 17 | :tags: general 18 | :tickets: DSX:419 19 | 20 | Add ``CONTRIBUTING.rst`` file 21 | 22 | .. change:: 23 | :tags: general, breaking 24 | :tickets: DSX:384 25 | 26 | Drop Python 2.7 and 3.6 support 27 | 28 | .. change:: 29 | :tags: dependency, breaking 30 | :tickets: DSX:384 31 | 32 | Use ``pydantic`` to parse responses 33 | 34 | .. change:: 35 | :tags: client, feature 36 | :tickets: DSX:442 37 | 38 | Add ``Bearer`` token auth 39 | 40 | .. change:: 41 | :tags: general 42 | :tickets: DSX:409 43 | 44 | Add ``LICENSE.txt`` file 45 | 46 | .. change:: 47 | :tags: client, breaking 48 | :tickets: DSX:545, DSX:632 49 | 50 | Rename library: 51 | 52 | * ``mlflow-client`` -> ``mlflow-rest-client`` 53 | * ``MLflowApiClient`` -> ``MLflowRESTClient`` 54 | 55 | .. change:: 56 | :tags: general, feature 57 | :tickets: DSX:449 58 | 59 | Add ``SECURITY.rst`` file 60 | 61 | .. change:: 62 | :tags: general, feature 63 | :tickets: DSX:411 64 | 65 | Move repo to github.com 66 | 67 | .. change:: 68 | :tags: docs, feature 69 | :tickets: DSX:421 70 | 71 | Move documentation to readthedocs.org 72 | 73 | .. change:: 74 | :tags: dev, feature 75 | :tickets: DSX:599 76 | 77 | Upgrade source code to Python 3.7+ 78 | 79 | .. change:: 80 | :tags: ci, feature 81 | :tickets: DSX:412, 1 82 | 83 | Move from Gitlab CI to Github Actions 84 | 85 | .. change:: 86 | :tags: dev, feature 87 | :tickets: DSX:549 88 | 89 | Check type hints with ``mypy`` 90 | 91 | 1.1 92 | -------------------- 93 | .. changelog:: 94 | :version: 1.1.7 95 | :released: 26.05.2021 17:47 96 | 97 | .. change:: 98 | :tags: tests, bug 99 | :tickets: DSX:166 100 | 101 | Do not use relative paths to run tests 102 | 103 | .. change:: 104 | :tags: client, bug 105 | :tickets: DSX:262 106 | 107 | Do not use `LIKE` operator while searching model by name in `get_or_create_model` function 108 | 109 | .. change:: 110 | :tags: dev, feature 111 | :tickets: DSX:358 112 | 113 | Build and push dev versions for feature and bug branches too 114 | 115 | .. changelog:: 116 | :version: 1.1.6 117 | :released: 03.04.2021 14:21 118 | 119 | .. change:: 120 | :tags: ci, feature 121 | :tickets: DSX:166 122 | 123 | Use Jenkins declarative pipeline 124 | 125 | .. change:: 126 | :tags: client, feature 127 | :tickets: DSX:166 128 | 129 | Disable SSL ignore warnings 130 | 131 | .. change:: 132 | :tags: client, feature 133 | :tickets: DSX:166 134 | 135 | Create one session for all requests 136 | 137 | .. changelog:: 138 | :version: 1.1.5 139 | :released: 25.12.2020 15:55 140 | 141 | .. change:: 142 | :tags: ci, feature 143 | :tickets: DSX:34 144 | 145 | Pass project urls into setup.py 146 | 147 | .. change:: 148 | :tags: general, feature 149 | :tickets: DSX:34 150 | 151 | Test python 3.8 and 3.9 compatibility 152 | 153 | .. change:: 154 | :tags: ci, feature 155 | :tickets: DSX:34 156 | 157 | Improve Jenkinsfile 158 | 159 | .. change:: 160 | :tags: ci, feature 161 | :tickets: DSX:111 162 | 163 | Move CI/CD from bdbuilder04 to adm-ci 164 | 165 | .. change:: 166 | :tags: dev, feature 167 | :tickets: DSX:34 168 | 169 | Add requirements-dev.txt as ``dev`` extras into ``setup.py`` 170 | 171 | .. change:: 172 | :tags: ci, feature 173 | :tickets: DSX:128 174 | 175 | Download base python images before build 176 | 177 | .. change:: 178 | :tags: ci, feature 179 | :tickets: DSX:130 180 | 181 | Fix requirements caching in Docker image 182 | 183 | .. change:: 184 | :tags: docs, feature 185 | :tickets: DSX:130 186 | 187 | Add summary to documentation pages 188 | 189 | .. changelog:: 190 | :version: 1.1.4 191 | :released: 05.12.2020 13:06 192 | 193 | .. change:: 194 | :tags: ci, feature 195 | :tickets: DSX:66 196 | 197 | Allow to build and deploy versions from non-master branch 198 | 199 | .. change:: 200 | :tags: ci, feature 201 | :tickets: DSX:72 202 | 203 | Remove old dev versions from Artifactory 204 | 205 | .. change:: 206 | :tags: ci, feature 207 | :tickets: DSX:80 208 | 209 | Move documentation deployment step to separated Jenkins job 210 | 211 | .. change:: 212 | :tags: general, bug 213 | :tickets: DSX:80 214 | 215 | Include README.rst into PyPi package 216 | 217 | .. change:: 218 | :tags: ci 219 | :tickets: DSX:89 220 | 221 | Make test scripts a docker image entrypoints 222 | 223 | .. change:: 224 | :tags: ci, bug 225 | :tickets: DSX:89 226 | 227 | Publish package and documentation to Artifactory in one build info 228 | 229 | .. change:: 230 | :tags: ci, feature 231 | :tickets: DSX:34 232 | 233 | Pass real project version to SonarQube 234 | 235 | .. change:: 236 | :tags: ci, feature 237 | :tickets: DSX:34 238 | 239 | Pass project links to SonarQube 240 | 241 | .. change:: 242 | :tags: ci, bug 243 | :tickets: DSX:34 244 | 245 | Remove redundant proxying from Jenkinsfile 246 | 247 | .. change:: 248 | :tags: ci, feature 249 | :tickets: DSX:111 250 | 251 | Move CI/CD from bdbuilder04 to adm-ci 252 | 253 | .. change:: 254 | :tags: ci, bug 255 | :tickets: DSX:34 256 | 257 | Remove volumes after stopping test container 258 | 259 | .. change:: 260 | :tags: ci, bug 261 | :tickets: DSX:34 262 | 263 | Fix PyLint report upload to SonarQube 264 | 265 | .. change:: 266 | :tags: ci, feature 267 | :tickets: DSX:34 268 | 269 | Format source code with Black 270 | 271 | .. change:: 272 | :tags: ci, feature 273 | :tickets: DSX:34 274 | 275 | Check source code vulnerabilities with Bandit 276 | 277 | .. change:: 278 | :tags: dev, feature 279 | :tickets: DSX:34 280 | 281 | Add pre-commit hooks 282 | 283 | .. changelog:: 284 | :version: 1.1.3 285 | :released: 17.10.2020 03:40 286 | 287 | .. change:: 288 | :tags: ci 289 | :tickets: DSX:53 290 | 291 | Improve Jenkinsfile 292 | 293 | .. change:: 294 | :tags: client, feature 295 | :tickets: DSX:25 296 | 297 | Add ``list_model_all_versions`` and ``list_model_all_versions_iterator`` methods 298 | 299 | .. changelog:: 300 | :version: 1.1.2 301 | :released: 02.10.2020 19:06 302 | 303 | .. change:: 304 | :tags: dependency 305 | :tickets: DSX:45 306 | 307 | Don't hard code dependency versions 308 | 309 | .. change:: 310 | :tags: model 311 | :tickets: DSX:45 312 | 313 | Fix error with accessing model list by stage 314 | 315 | .. changelog:: 316 | :version: 1.1.1 317 | :released: 29.09.2020 18:08 318 | 319 | .. change:: 320 | :tags: docs 321 | :tickets: DSX:46 322 | 323 | Improve documentation 324 | 325 | .. changelog:: 326 | :version: 1.1.0 327 | :released: 29.09.2020 16:29 328 | 329 | .. change:: 330 | :tags: refactor 331 | :tickets: DSX:46 332 | 333 | Refactor code 334 | 335 | .. change:: 336 | :tags: tests 337 | :tickets: DSX:46 338 | 339 | Increase tests coverage 340 | 341 | .. change:: 342 | :tags: model, feature 343 | :tickets: DSX:46 344 | 345 | Allow to get version by stage from ``Model`` object 346 | 347 | .. change:: 348 | :tags: tag, feature 349 | :tickets: DSX:46 350 | 351 | Allow to get tag by name from any object 352 | 353 | .. change:: 354 | :tags: run, feature 355 | :tickets: DSX:46 356 | 357 | Allow to get param by key from ``RunData`` object 358 | 359 | .. change:: 360 | :tags: run, feature 361 | :tickets: DSX:46 362 | 363 | Allow to get metric by key from ``RunData`` object 364 | 365 | .. change:: 366 | :tags: docs 367 | :tickets: DSX:46 368 | 369 | Improve documentation 370 | 371 | 1.0 372 | -------------------- 373 | 374 | .. changelog:: 375 | :version: 1.0.8 376 | :released: 24.09.2020 16:42 377 | 378 | .. change:: 379 | :tags: general 380 | :tickets: DSX:16 381 | :changeset: d5e57951 382 | 383 | Added ``mlflow_client.__version__`` attribute 384 | 385 | .. change:: 386 | :tags: docs 387 | :tickets: DSX:16 388 | :changeset: 33121a8e 389 | 390 | Added CHANGELOG.rst file 391 | 392 | .. change:: 393 | :tags: general, bug 394 | :tickets: DSX:16 395 | :changeset: 67b641f6 396 | 397 | Fixed VERSION file include into package 398 | 399 | .. changelog:: 400 | :version: 1.0.7 401 | :released: 16.09.2020 12:14 402 | 403 | .. change:: 404 | :tags: general 405 | :tickets: DSX:24 406 | :changeset: e3d715da 407 | 408 | Add VERSION file 409 | 410 | .. change:: 411 | :tags: docs 412 | :tickets: SCRR:133 413 | :changeset: 0b32c40d 414 | 415 | Deploy dev version documentation 416 | 417 | .. change:: 418 | :tags: general, bug 419 | :tickets: SCRR:142 420 | :changeset: 0b32c40d 421 | 422 | Removed ``tests`` dir from release package 423 | 424 | .. changelog:: 425 | :version: 1.0.6 426 | :released: 14.08.2020 12:12 427 | 428 | .. change:: 429 | :tags: ci 430 | :tickets: SCRR:133 431 | :changeset: f7824f2a 432 | 433 | Update ansible from v2.2 to v2.9 434 | 435 | .. changelog:: 436 | :version: 1.0.5 437 | :released: 14.08.2020 12:12 438 | 439 | .. change:: 440 | :tags: ci 441 | :tickets: SCRR:111 442 | :changeset: 0aa457f9 443 | 444 | Development version is released on every push to ``dev`` branch 445 | 446 | .. change:: 447 | :tags: general, bug 448 | :tickets: SCRR:111 449 | :changeset: 0aa457f9 450 | 451 | Removed ``tests`` dir from release package 452 | 453 | .. changelog:: 454 | :version: 1.0.4 455 | :released: 07.08.2020 17:20 456 | 457 | .. change:: 458 | :tags: client, bug 459 | :tickets: SCRR:111 460 | :changeset: ca138fa5 461 | 462 | Logs are now passed to STDOUT instead of STDERR 463 | 464 | .. changelog:: 465 | :version: 1.0.3 466 | :released: 05.08.2020 18:01 467 | 468 | .. change:: 469 | :tags: client, bug 470 | :tickets: SCRR:111 471 | :changeset: e9d7759d 472 | 473 | Fixed ``MLflowApiClient.get_or_create_model`` method 474 | 475 | .. changelog:: 476 | :version: 1.0.2 477 | :released: 05.08.2020 18:01 478 | 479 | .. change:: 480 | :tags: tests, bug 481 | :tickets: SCRR:111 482 | :changeset: 5d345837 483 | 484 | Add timeout to integration tests 485 | 486 | .. change:: 487 | :tags: client, bug 488 | :tickets: SCRR:111 489 | :changeset: 3b7c1930 490 | 491 | Fixed ``ignore_ssl_check`` flag handling in ``MLflowApiClient`` methods 492 | 493 | .. changelog:: 494 | :version: 1.0.1 495 | :released: 31.07.2020 14:15 496 | 497 | .. change:: 498 | :tags: client, feature 499 | :tickets: SCRR:111 500 | :changeset: 22d95875 501 | 502 | Add ``MLflowApiClient.get_or_create_model`` method 503 | 504 | .. changelog:: 505 | :version: 1.0.0 506 | :released: 30.07.2020 19:01 507 | 508 | .. change:: 509 | :tags: general 510 | :tickets: SCRR:111 511 | :changeset: 77e7f798 512 | 513 | ``mlflow-rest-client`` package was created based on ``mlflow-python-client ``__ 514 | 515 | .. change:: 516 | :tags: artifact, feature 517 | :tickets: SCRR:111 518 | :changeset: 81484376 519 | 520 | ``artifact`` module was added with certain classes: 521 | * ``FileInfo`` 522 | 523 | .. change:: 524 | :tags: experiment, feature 525 | :tickets: SCRR:111 526 | :changeset: 81484376 527 | 528 | ``experiment`` module was added with certain classes: 529 | * ``Experiment`` 530 | * ``ExperimentTag`` 531 | * ``ExperimentStage`` 532 | 533 | .. change:: 534 | :tags: model, feature 535 | :tickets: SCRR:111 536 | :changeset: 81484376 537 | 538 | ``model`` module was added with certain classes: 539 | * ``Model`` 540 | * ``ModelVersion`` 541 | * ``ModelTag`` 542 | * ``ModelVersionTag`` 543 | * ``ModelVersionStage`` 544 | * ``ModelVersionState`` 545 | * ``ModelVersionStatus`` 546 | 547 | .. change:: 548 | :tags: page, feature 549 | :tickets: SCRR:111 550 | :changeset: 81484376 551 | 552 | ``page`` module was added with certain classes: 553 | * ``Page`` 554 | 555 | .. change:: 556 | :tags: run, feature 557 | :tickets: SCRR:111 558 | :changeset: 81484376 559 | 560 | ``run`` module was added with certain classes: 561 | * ``Run`` 562 | * ``RunInfo`` 563 | * ``RunData`` 564 | * ``Param`` 565 | * ``Metric`` 566 | * ``RunTag`` 567 | * ``RunStage`` 568 | * ``RunStatus`` 569 | * ``RunViewType`` 570 | 571 | .. change:: 572 | :tags: tag, feature 573 | :tickets: SCRR:111 574 | :changeset: 81484376 575 | 576 | ``tag`` module was added with certain classes: 577 | * ``Tag`` 578 | 579 | .. change:: 580 | :tags: client, feature 581 | :tickets: SCRR:111 582 | :changeset: 81484376 583 | 584 | ``client.MLflowApiClient`` class methods were created: 585 | * ``get*`` 586 | * ``get_experiment_by_name`` 587 | * ``get_or_create_experiment`` 588 | 589 | * ``get_model`` 590 | 591 | * ``get_model_version`` 592 | * ``get_model_version_download_url`` 593 | 594 | * ``list*`` 595 | * ``list_experiment_runs`` 596 | * ``list_models`` 597 | * ``list_model_versions`` 598 | 599 | * ``search*`` 600 | * ``search_models`` 601 | * ``search_model_versions`` 602 | 603 | * ``create*`` 604 | * ``create_model`` 605 | * ``create_model_version`` 606 | 607 | * ``update*`` 608 | * ``rename_experiment`` 609 | 610 | * ``start_run`` 611 | * ``schedule_run`` 612 | * ``finish_run`` 613 | * ``fail_run`` 614 | * ``kill_run`` 615 | 616 | * ``log_run_batch`` 617 | * ``log_run_model`` 618 | 619 | * ``rename_model`` 620 | * ``set_model_description`` 621 | 622 | * ``set_model_version_description`` 623 | 624 | * ``transition_model_version_stage`` 625 | * ``test_model_version`` 626 | * ``promote_model_version`` 627 | * ``promote_model_version`` 628 | 629 | * ``tag*`` 630 | * ``set_experiment_tag`` 631 | 632 | * ``set_run_tag`` 633 | * ``delete_run_tag`` 634 | 635 | * ``set_model_tag`` 636 | * ``delete_model_tag`` 637 | 638 | * ``set_model_version_tag`` 639 | * ``delete_model_version_tag`` 640 | 641 | * ``delete*`` 642 | * ``delete_experiment`` 643 | * ``delete_run`` 644 | * ``delete_model`` 645 | * ``delete_model_version`` 646 | 647 | * ``restore*`` 648 | * ``restore_experiment`` 649 | * ``restore_run`` 650 | 651 | Renamed: 652 | * ``update_run`` -> ``set_run_status`` 653 | * ``log_parameter`` -> ``log_run_parameter`` 654 | * ``log_metric`` -> ``log_run_metric`` 655 | * ``get_metric_history`` -> ``get_run_metric_history`` 656 | * ``list_artifacts`` -> ``list_run_artifacts`` 657 | * ``get_artifact`` -> ``get_run_artifact`` 658 | * ``search2`` -> ``search_runs`` 659 | 660 | Updated: 661 | * ``list_experiments`` 662 | * ``get_experiment`` 663 | * ``create_experiment`` 664 | * ``get_experiment_id`` 665 | * ``get_run`` 666 | * ``create_run`` 667 | 668 | Deleted: 669 | * ``get_or_create_experiment_id`` 670 | * ``search`` 671 | 672 | .. change:: 673 | :tags: page, feature 674 | :tickets: SCRR:111 675 | :changeset: 432be0ef 676 | 677 | * ``page.Page``: 678 | * Class can be constructed from list 679 | * Presence of an item can be checked with ``in`` operator 680 | * Item can be appended using ``+`` operator 681 | * Item can be removed using ``del`` operator 682 | * Items count can be determined using ``len`` function 683 | * Is comparable now with another Page, list or dict 684 | * Is iterable now 685 | 686 | .. change:: 687 | :tags: run, feature 688 | :tickets: SCRR:111 689 | :changeset: 432be0ef 690 | 691 | * ``run.RunInfo`` 692 | * experiment_id is not mandatory constructor argument anymore 693 | * Is comparable now with another Run, list, dict or str (=id) 694 | * Presence of an item in a dict can be checked using ``in`` operator 695 | 696 | * ``tag.Param`` 697 | * Is comparable now with another Param, list, dict or tuple (=(key, value)) 698 | * Presence of an item in a dict can be checked using ``in`` operator 699 | 700 | * ``run.Metric`` 701 | * Is comparable now with another Metric, list, dict or tuple (=(key, value, timestamp) or (key, value)) 702 | * Presence of an item in a dict can be checked using ``in`` operator 703 | 704 | * ``tag.RunTag`` 705 | * Is comparable now with another RunTag, list, dict or tuple (=(key, value)) 706 | * Presence of an item in a dict can be checked using ``in`` operator 707 | 708 | * ``run.RunData`` 709 | * Is comparable now with another RunData, list or dict 710 | * Presence of an item in a dict can be checked using ``in`` operator 711 | 712 | * ``run.Run`` 713 | * Is comparable now with another Run, list or dict 714 | * Presence of an item in a dict can be checked using ``in`` operator 715 | 716 | .. change:: 717 | :tags: tag, feature 718 | :tickets: SCRR:111 719 | :changeset: 432be0ef 720 | 721 | * ``tag.Tag`` 722 | * Is comparable now with another RunTag, list, dict or tuple (=(key, value)) 723 | * Presence of an item in a dict can be checked using ``in`` operator 724 | 725 | .. change:: 726 | :tags: sample, bug 727 | :tickets: SCRR:111 728 | :changeset: 432be0ef 729 | 730 | Fixed sample scripts 731 | 732 | .. change:: 733 | :tags: client, bug 734 | :tickets: SCRR:111 735 | :changeset: a01fe488 736 | 737 | Fixed ``MLflowApiClient`` methods: 738 | * ``list_experiments`` 739 | * ``log_run_model`` 740 | * ``delete_run_tag`` 741 | * ``get_run_metric_history`` 742 | * ``list_run_artifacts`` 743 | * ``search_runs`` 744 | * ``set_model_description`` 745 | * ``list_models`` 746 | * ``search_models`` 747 | * ``get_model_version`` 748 | * ``set_model_version_description`` 749 | * ``set_model_version_tag`` 750 | * ``delete_model_version_tag`` 751 | * ``delete_model_version`` 752 | * ``search_model_versions`` 753 | * ``get_model_version_download_url`` 754 | * ``transition_model_version_stage`` 755 | 756 | .. change:: 757 | :tags: tag, bug 758 | :tickets: SCRR:111 759 | :changeset: a01fe488 760 | 761 | Fixed ``MLflowApiClient`` methods tag handling: 762 | * ``list_experiments`` 763 | * ``get_run`` 764 | * ``create_model_version`` 765 | 766 | .. change:: 767 | :tags: client, feature 768 | :tickets: SCRR:111 769 | :changeset: a01fe488 770 | 771 | Added new ``MLflowApiClient`` methods: 772 | * ``list_experiment_runs_iterator`` 773 | * ``list_run_artifacts_iterator`` 774 | * ``search_runs_iterator`` 775 | * ``search_models_iterator`` 776 | * ``search_model_versions_iterator`` 777 | * ``archive_model_version`` 778 | 779 | .. change:: 780 | :tags: client, feature 781 | :tickets: SCRR:111 782 | :changeset: a01fe488 783 | 784 | Now it's possible to pass stages to ``MLflowApiClient.list_model_versions`` as list of strings 785 | 786 | .. change:: 787 | :tags: model, feature 788 | :tickets: SCRR:111 789 | :changeset: a01fe488 790 | 791 | * ``model.ModelVersionState`` 792 | * Is comparable now with another ModelVersionState or tuple (=(status, message)) 793 | * Presence of an item in a dict can be checked using ``in`` operator 794 | 795 | * ``model.ModelVersion`` 796 | * Is comparable now with another ModelVersion, list, dict or tuple (=(name, version)) 797 | * Presence of an item in a dict can be checked using ``in`` operator 798 | 799 | * ``model.Model`` 800 | * Is comparable now with another Model, list, dict or str (=name) 801 | * Presence of an item in a dict can be checked using ``in`` operator 802 | 803 | .. change:: 804 | :tags: model, bug 805 | :tickets: SCRR:111 806 | :changeset: a01fe488 807 | 808 | Fixed parsing stage in ``model.ModelVersion`` constructor 809 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing Guide 2 | ================== 3 | 4 | Welcome! There are many ways to contribute, including submitting bug 5 | reports, improving documentation, submitting feature requests, reviewing 6 | new submissions, or contributing code that can be incorporated into the 7 | project. 8 | 9 | Please, don't use the issue tracker for support questions. Instead use: 10 | `email `__. 11 | 12 | Feature Requests 13 | ---------------- 14 | 15 | Please create a new GitHub issue for any significant changes and 16 | enhancements that you wish to make. Provide the feature you would like 17 | to see, why you need it, and how it will work. Discuss your ideas 18 | transparently and get community feedback before proceeding. 19 | 20 | Significant Changes that you wish to contribute to the project should be 21 | discussed first in a GitHub issue that clearly outlines the changes and 22 | benefits of the feature. 23 | 24 | Small Changes can directly be crafted and submitted to the GitHub 25 | Repository as a Pull Request. 26 | 27 | Pull Request Process 28 | -------------------- 29 | 30 | 1. Update the README.md with details of changes to the interface, 31 | including new environment variables, exposed ports, proper file 32 | locations, and container parameters. 33 | 2. Increase the version numbers in any examples files and the README.md 34 | to the new version that this Pull Request would represent. The 35 | versioning scheme we use is SemVer. 36 | 3. You may merge the Pull Request in once you have the sign-off of two 37 | other developers, or if you do not have permission to do that, you 38 | may request the second reviewer to merge it for you. We expect to 39 | have a minimum of one approval from someone else on the core team. 40 | 41 | Review Process 42 | -------------- 43 | 44 | We keep pull requests open for a few days for multiple people to have 45 | the chance to review/comment. 46 | 47 | After feedback has been given, we expect responses within two weeks. 48 | After two weeks, we may close the pull request if it isn't showing any 49 | activity. 50 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021-2024 MTS (Mobile Telesystems). All rights reserved. 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt README.rst requirements.txt requirements-test.txt requirements-dev.txt mlflow_rest_client/VERSION 2 | recursive-exclude * __pycache__ 3 | recursive-exclude * *.pyc 4 | recursive-exclude * *.pyo 5 | recursive-exclude * *.orig 6 | recursive-exclude docs * 7 | recursive-exclude samples * 8 | recursive-exclude tests * 9 | prune docs* 10 | prune samples* 11 | prune tests* 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. title 2 | 3 | Python Client for MLflow 4 | ========================== 5 | 6 | |status| |PyPI| |PyPI License| |PyPI Python Version| 7 | |ReadTheDocs| |Build| |Coverage| |pre-commit.ci| 8 | 9 | .. |status| image:: https://www.repostatus.org/badges/latest/active.svg 10 | :target: https://www.repostatus.org/#active 11 | .. |PyPI| image:: https://badge.fury.io/py/mlflow-rest-client.svg 12 | :target: https://badge.fury.io/py/mlflow-rest-client 13 | .. |PyPI License| image:: https://img.shields.io/pypi/l/mlflow-rest-client.svg 14 | :target: https://github.com/MobileTeleSystems/mlflow-rest-client/blob/main/LICENSE.txt 15 | .. |PyPI Python Version| image:: https://img.shields.io/pypi/pyversions/mlflow-rest-client.svg 16 | :target: https://badge.fury.io/py/mlflow-rest-client 17 | .. |ReadTheDocs| image:: https://img.shields.io/readthedocs/mlflow-rest-client.svg 18 | :target: https://mlflow-rest-client.readthedocs.io 19 | .. |Build| image:: https://github.com/MobileTeleSystems/mlflow-rest-client/workflows/Tests/badge.svg 20 | :target: https://github.com/MobileTeleSystems/mlflow-rest-client/actions 21 | .. |Coverage| image:: https://codecov.io/gh/MobileTeleSystems/mlflow-rest-client/branch/main/graph/badge.svg 22 | :target: https://codecov.io/gh/MobileTeleSystems/mlflow-rest-client 23 | .. |pre-commit.ci| image:: https://results.pre-commit.ci/badge/github/MobileTeleSystems/mlflow-rest-client/main.svg 24 | :target: https://results.pre-commit.ci/latest/github/MobileTeleSystems/mlflow-rest-client/main 25 | 26 | Python client for `MLflow `_ REST API. 27 | 28 | **Features:** 29 | 30 | - Minimal dependencies 31 | 32 | - Unlike `MLflow Tracking client `__ 33 | all REST API methods and params are exposed to user. 34 | 35 | - MLflow URL is passed via constructor argument instead of env variable, 36 | so multiple client instances could be created in the same Python interpreter. 37 | 38 | - Basic and Bearer auth are supported (via constructor args too). 39 | 40 | - All class fields are validated with `pydantic `_. 41 | 42 | - All methods and classes are documented. 43 | 44 | **Limitations:** 45 | 46 | - There is no integration with ML frameworks and libraries. 47 | You should use official `MLflow client `__ instead. 48 | 49 | - There is no integration with S3 or other artifact storage type. 50 | You should access it directly with `boto3 `_ or other client. 51 | 52 | - Supported MLflow versions: from ``1.17.0`` to ``1.23.0``. 53 | It is possible to use client with older MLflow versions (e.g. ``1.10.0``), but this is not guaranteed. 54 | 55 | - Only Python 3.7+ is supported. Python 3.6 and lower already reached end of life. 56 | 57 | .. documentation 58 | 59 | Documentation 60 | ------------- 61 | 62 | See https://mlflow-rest-client.readthedocs.io/ 63 | 64 | .. contribution 65 | 66 | Contribution guide 67 | ------------------- 68 | 69 | See ``__ 70 | 71 | Security 72 | ------------------- 73 | 74 | See ``__ 75 | 76 | 77 | .. install 78 | 79 | Installation 80 | --------------- 81 | 82 | Stable release 83 | ~~~~~~~~~~~~~~~ 84 | Stable version is released on every tag to ``master`` branch. Please use stable releases on production environment. 85 | Version example: ``2.0.0`` 86 | 87 | .. code:: bash 88 | 89 | pip install mlflow-rest-client==2.0.0 # exact version 90 | 91 | pip install mlflow-rest-client # latest release 92 | 93 | Development release 94 | ~~~~~~~~~~~~~~~~~~~~ 95 | Development version is released on every commit to ``dev`` branch. You can use them to test some new features before official release. 96 | Version example: ``2.0.0.dev5`` 97 | 98 | .. code:: bash 99 | 100 | pip install mlflow-rest-client==2.0.0.dev5 # exact dev version 101 | 102 | pip install --pre mlflow-rest-client # latest dev version 103 | 104 | .. develop 105 | 106 | Development 107 | --------------- 108 | Clone repo: 109 | 110 | .. code:: bash 111 | 112 | git clone git@github.com:MobileTeleSystems/mlflow-rest-client.git 113 | 114 | cd mlflow-rest-client 115 | 116 | Install dependencies for development: 117 | 118 | .. code:: bash 119 | 120 | pip install -r requirements-dev.txt 121 | 122 | Install pre-commit hooks: 123 | 124 | .. code:: bash 125 | 126 | pre-commit install 127 | pre-commit autoupdate 128 | pre-commit install-hooks 129 | 130 | Test pre-commit hooks run: 131 | 132 | .. code:: bash 133 | 134 | pre-commit run --all-files -v 135 | 136 | .. usage 137 | 138 | Usage 139 | ------------ 140 | Make sure you have an `MLflow Tracking Server `_ running. 141 | 142 | .. code:: python 143 | 144 | from mlflow_rest_client import MLflowRESTClient 145 | 146 | client = MLflowRESTClient("https://mlflow.domain", ignore_ssl_check=True) 147 | 148 | experiment = client.get_or_create_experiment("experiment_name") 149 | run = client.create_run(experiment.id) 150 | 151 | See `sample.py `_ for more examples. 152 | -------------------------------------------------------------------------------- /SECURITY.rst: -------------------------------------------------------------------------------- 1 | Security 2 | ============= 3 | 4 | Supported Python versions 5 | ------------------------- 6 | 3.7 or above 7 | 8 | Product development security recommendations 9 | -------------------------------------------- 10 | 11 | 1. Update dependencies to last stable version 12 | 2. Build SBOM for the project 13 | 3. Perform SAST (Static Application Security Testing) where possible 14 | 15 | Product development security requirements 16 | ----------------------------------------- 17 | 18 | 1. No binaries in repository 19 | 2. No passwords, keys, access tokens in source code 20 | 3. No "Critical" and/or "High" vulnerabilities in contributed source code 21 | 22 | Vulnerability reports 23 | --------------------- 24 | 25 | Please, use email ``__ for reporting security issues or anything that can cause any consequences for security. 26 | 27 | Please avoid any public disclosure (including registering issues) at least until it is fixed. 28 | 29 | Thank you in advance for understanding. 30 | -------------------------------------------------------------------------------- /docker/.env.test: -------------------------------------------------------------------------------- 1 | ARTIFACT_ROOT=./ 2 | POSTGRES_DB=mlflow 3 | POSTGRES_HOST=mlflow-db 4 | POSTGRES_PASSWORD=mlflow 5 | POSTGRES_PORT=5432 6 | POSTGRES_USER=mlflow 7 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG MLFLOW_VERSION=1.22.0 2 | FROM adacotechjp/mlflow:${MLFLOW_VERSION} 3 | 4 | RUN apt-get update && \ 5 | apt-get install --no-install-recommends -fy postgresql-client && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | COPY ./mlflow-entrypoint.sh ./wait-for-postgres.sh / 9 | RUN chmod +x /mlflow-entrypoint.sh /wait-for-postgres.sh 10 | 11 | ENTRYPOINT ["/mlflow-entrypoint.sh"] 12 | CMD [] 13 | -------------------------------------------------------------------------------- /docker/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | mlflow: 4 | image: mlflow-test:${MLFLOW_VERSION:-1.22.0} 5 | build: 6 | context: . 7 | args: 8 | MLFLOW_VERSION: ${MLFLOW_VERSION:-1.22.0} 9 | env_file: .env.test 10 | restart: unless-stopped 11 | ports: 12 | - 127.0.0.1:5000:5000 13 | depends_on: 14 | - mlflow-db 15 | networks: 16 | - default 17 | - db 18 | 19 | mlflow-db: 20 | image: postgres:11.8 21 | env_file: .env.test 22 | restart: unless-stopped 23 | networks: 24 | - db 25 | networks: 26 | db: 27 | internal: true 28 | -------------------------------------------------------------------------------- /docker/mlflow-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /wait-for-postgres.sh 4 | 5 | mlflow db upgrade "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB" 6 | 7 | mlflow server --host 0.0.0.0 --backend-store-uri "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB" --default-artifact-root "$ARTIFACT_ROOT" $* 8 | -------------------------------------------------------------------------------- /docker/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | WAITFORIT_cmdname=${0##*/} 5 | 6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 29 | else 30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" 31 | fi 32 | WAITFORIT_start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then 36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT 37 | WAITFORIT_result=$? 38 | else 39 | (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 40 | WAITFORIT_result=$? 41 | fi 42 | if [[ $WAITFORIT_result -eq 0 ]]; then 43 | WAITFORIT_end_ts=$(date +%s) 44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $WAITFORIT_result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then 56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 57 | else 58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 59 | fi 60 | WAITFORIT_PID=$! 61 | trap "kill -INT -$WAITFORIT_PID" INT 62 | wait $WAITFORIT_PID 63 | WAITFORIT_RESULT=$? 64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then 65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 66 | fi 67 | return $WAITFORIT_RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | WAITFORIT_hostport=(${1//:/ }) 76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]} 77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | WAITFORIT_CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | WAITFORIT_QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | WAITFORIT_STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | WAITFORIT_HOST="$2" 94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | WAITFORIT_HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | WAITFORIT_PORT="$2" 103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | WAITFORIT_PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | WAITFORIT_TIMEOUT="$2" 112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | WAITFORIT_TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | WAITFORIT_CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} 140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} 141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} 142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} 143 | 144 | # Check to see if timeout is from busybox? 145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout) 146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) 147 | 148 | WAITFORIT_BUSYTIMEFLAG="" 149 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then 150 | WAITFORIT_ISBUSY=1 151 | # Check if busybox timeout uses -t flag 152 | # (recent Alpine versions don't support -t anymore) 153 | if timeout &>/dev/stdout | grep -q -e '-t '; then 154 | WAITFORIT_BUSYTIMEFLAG="-t" 155 | fi 156 | else 157 | WAITFORIT_ISBUSY=0 158 | fi 159 | 160 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then 161 | wait_for 162 | WAITFORIT_RESULT=$? 163 | exit $WAITFORIT_RESULT 164 | else 165 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 166 | wait_for_wrapper 167 | WAITFORIT_RESULT=$? 168 | else 169 | wait_for 170 | WAITFORIT_RESULT=$? 171 | fi 172 | fi 173 | 174 | if [[ $WAITFORIT_CLI != "" ]]; then 175 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then 176 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" 177 | exit $WAITFORIT_RESULT 178 | fi 179 | exec "${WAITFORIT_CLI[@]}" 180 | else 181 | exit $WAITFORIT_RESULT 182 | fi 183 | -------------------------------------------------------------------------------- /docker/wait-for-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | export PGHOST=$POSTGRES_HOST 5 | export PGDATABASE=$POSTGRES_DB 6 | export PGPORT=$POSTGRES_PORT 7 | export PGUSER=$POSTGRES_USER 8 | export PGPASSWORD=$POSTGRES_PASSWORD 9 | 10 | until psql -c '\q'; do 11 | >&2 echo "Postgres is unavailable - sleeping" 12 | sleep 1 13 | done 14 | 15 | >&2 echo "Postgres is up" 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS = 7 | SPHINXBUILD = sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import subprocess 14 | import sys 15 | from pathlib import Path 16 | 17 | from packaging.version import Version 18 | 19 | sys.path.insert(0, str(Path(__file__).parent.parent.absolute())) 20 | 21 | setup_py = Path(__file__).parent.parent.joinpath("setup.py").absolute() 22 | ver = Version( 23 | subprocess.check_output( # nosec 24 | f"{sys.executable} {setup_py} --version", 25 | shell=True, 26 | cwd=Path(__file__).parent.parent, 27 | ) 28 | .decode("utf-8") 29 | .strip() 30 | ) 31 | 32 | # -- Project information ----------------------------------------------------- 33 | 34 | project = "mlflow-rest-client" 35 | copyright = "2021-2024, MTS (Mobile Telesystems)" 36 | author = "DSX Team" 37 | 38 | # The short X.Y version 39 | version = ver.base_version 40 | # The full version, including alpha/beta/rc tags 41 | release = str(ver) 42 | 43 | 44 | # -- General configuration --------------------------------------------------- 45 | 46 | # Add any Sphinx extension module names here, as strings. They can be 47 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 48 | # ones. 49 | extensions = [ 50 | "sphinx.ext.autodoc", 51 | "sphinx.ext.extlinks", 52 | "sphinx_autodoc_typehints", 53 | "changelog", 54 | "numpydoc", 55 | ] 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = ["_templates"] 59 | 60 | # List of patterns, relative to source directory, that match files and 61 | # directories to ignore when looking for source files. 62 | # This pattern also affects html_static_path and html_extra_path. 63 | exclude_patterns = ["_build", "Thumds.db", ".DS_Store"] 64 | 65 | # -- Options for HTML output ------------------------------------------------- 66 | 67 | # The theme to use for HTML and HTML Help pages. See the documentation for 68 | # a list of builtin themes. 69 | # 70 | html_theme = "furo" 71 | html_title = f"mlflow-rest-client {version}" 72 | 73 | # Add any paths that contain custom static files (such as style sheets) here, 74 | # relative to this directory. They are copied after the builtin static files, 75 | # so a file named 'default.css' will overwrite the builtin 'default.css'. 76 | # html_static_path = ['_static'] 77 | 78 | extlinks = { 79 | "github-user": ("https://github.com/%s", "@%s"), 80 | } 81 | 82 | changelog_sections = [ 83 | "general", 84 | "client", 85 | "artifact", 86 | "experiment", 87 | "model", 88 | "page", 89 | "run", 90 | "tag", 91 | "dependency", 92 | "docs", 93 | "samples", 94 | "ci", 95 | "tests", 96 | "dev", 97 | ] 98 | 99 | changelog_caption_class = "" 100 | 101 | changelog_inner_tag_sort = ["breaking", "deprecated", "feature", "bug", "refactor"] 102 | changelog_hive_sections_from_tags = True 103 | 104 | changelog_render_ticket = { 105 | "default": "https://github.com/MobileTeleSystems/mlflow-rest-client/issues/%s", 106 | "DSX": "https://jira.bd.msk.mts.ru/browse/DSX-%s", 107 | "SCRR": "https://jira.bd.msk.mts.ru/browse/SCRR-%s", 108 | } 109 | changelog_render_pullreq = { 110 | "default": "https://github.com/MobileTeleSystems/mlflow-rest-client/pull/%s", 111 | "gitlab": "https://gitlab.services.mts.ru/bigdata/platform/dsx/mlflow-client/-/merge_requests/%s", 112 | } 113 | changelog_render_changeset = "https://github.com/MobileTeleSystems/mlflow-rest-client/commit/%s" 114 | 115 | language = "en" 116 | 117 | default_role = "any" 118 | todo_include_todos = False 119 | 120 | numpydoc_show_class_members = False 121 | pygments_style = "sphinx" 122 | 123 | autoclass_content = "both" 124 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/develop.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: develop 3 | :end-before: usage 4 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :hide-toc: 2 | 3 | .. include:: ../README.rst 4 | :end-before: documentation 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: How to 9 | :name: howto 10 | :hidden: 11 | 12 | install 13 | usage 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | :caption: Contents 18 | :name: mastertoc 19 | :hidden: 20 | 21 | mlflow_rest_client.client 22 | mlflow_rest_client.artifact 23 | mlflow_rest_client.experiment 24 | mlflow_rest_client.model 25 | mlflow_rest_client.page 26 | mlflow_rest_client.run 27 | mlflow_rest_client.tag 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | :caption: Develop 32 | :name: develop 33 | :hidden: 34 | 35 | changelog 36 | develop 37 | contributing 38 | security 39 | 40 | .. toctree:: 41 | :maxdepth: 1 42 | :caption: Project Links 43 | :hidden: 44 | 45 | Source Code 46 | CI/CD 47 | Code Coverage 48 | Issue Tracker 49 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: install 3 | :end-before: develop 4 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/mlflow_rest_client.artifact.rst: -------------------------------------------------------------------------------- 1 | Artifact 2 | ================================================================= 3 | 4 | .. currentmodule:: mlflow_rest_client.artifact 5 | 6 | .. autosummary:: 7 | :nosignatures: 8 | 9 | Artifact 10 | 11 | .. autoclass:: mlflow_rest_client.artifact.Artifact 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/mlflow_rest_client.client.rst: -------------------------------------------------------------------------------- 1 | MLflow REST API client 2 | ================================================================= 3 | 4 | Summary 5 | -------- 6 | .. currentmodule:: mlflow_rest_client.mlflow_rest_client.MLflowRESTClient 7 | 8 | Main class 9 | ^^^^^^^^^^^ 10 | .. autosummary:: 11 | :nosignatures: 12 | 13 | mlflow_rest_client.mlflow_rest_client.MLflowRESTClient 14 | 15 | Experiment 16 | ^^^^^^^^^^^ 17 | .. autosummary:: 18 | :nosignatures: 19 | 20 | list_experiments 21 | list_experiments_iterator 22 | 23 | get_experiment 24 | get_experiment_id 25 | get_experiment_by_name 26 | 27 | get_or_create_experiment 28 | create_experiment 29 | rename_experiment 30 | delete_experiment 31 | restore_experiment 32 | 33 | set_experiment_tag 34 | 35 | Run 36 | ^^^^^^^^^^^ 37 | .. autosummary:: 38 | :nosignatures: 39 | 40 | list_experiment_runs 41 | list_experiment_runs_iterator 42 | 43 | search_runs 44 | search_runs_iterator 45 | 46 | get_run 47 | 48 | create_run 49 | set_run_status 50 | start_run 51 | schedule_run 52 | finish_run 53 | fail_run 54 | kill_run 55 | delete_run 56 | restore_run 57 | 58 | log_run_parameter 59 | log_run_parameters 60 | 61 | log_run_metric 62 | log_run_metrics 63 | 64 | log_run_batch 65 | log_run_model 66 | 67 | set_run_tag 68 | set_run_tags 69 | delete_run_tag 70 | delete_run_tags 71 | 72 | Run metrics 73 | ^^^^^^^^^^^ 74 | .. autosummary:: 75 | :nosignatures: 76 | 77 | list_run_metric_history 78 | list_run_metric_history_iterator 79 | 80 | 81 | Run artifacts 82 | ^^^^^^^^^^^^^ 83 | .. autosummary:: 84 | :nosignatures: 85 | 86 | list_run_artifacts 87 | list_run_artifacts_iterator 88 | 89 | Model 90 | ^^^^^ 91 | .. autosummary:: 92 | :nosignatures: 93 | 94 | list_models 95 | list_models_iterator 96 | 97 | search_models 98 | search_models_iterator 99 | 100 | get_model 101 | 102 | get_or_create_model 103 | create_model 104 | rename_model 105 | set_model_description 106 | delete_model 107 | 108 | set_model_tag 109 | delete_model_tag 110 | 111 | Model version 112 | ^^^^^^^^^^^^^ 113 | .. autosummary:: 114 | :nosignatures: 115 | 116 | list_model_versions 117 | list_model_versions_iterator 118 | 119 | list_model_all_versions 120 | list_model_all_versions_iterator 121 | 122 | search_model_versions 123 | search_model_versions_iterator 124 | 125 | get_model_version 126 | get_model_version_download_url 127 | 128 | create_model_version 129 | set_model_version_description 130 | delete_model_version 131 | 132 | set_model_version_tag 133 | delete_model_version_tag 134 | 135 | transition_model_version_stage 136 | test_model_version 137 | promote_model_version 138 | archive_model_version 139 | 140 | Documentation 141 | -------------- 142 | .. autoclass:: mlflow_rest_client.mlflow_rest_client.MLflowRESTClient 143 | :members: 144 | -------------------------------------------------------------------------------- /docs/mlflow_rest_client.experiment.rst: -------------------------------------------------------------------------------- 1 | Experiment 2 | ================================================================= 3 | 4 | .. currentmodule:: mlflow_rest_client.experiment 5 | 6 | .. autosummary:: 7 | :nosignatures: 8 | 9 | Experiment 10 | ExperimentStage 11 | ExperimentTag 12 | 13 | .. autoclass:: mlflow_rest_client.experiment.Experiment 14 | :members: 15 | 16 | .. autoclass:: mlflow_rest_client.experiment.ExperimentStage 17 | :members: 18 | 19 | .. autoclass:: mlflow_rest_client.experiment.ExperimentTag 20 | :members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/mlflow_rest_client.model.rst: -------------------------------------------------------------------------------- 1 | Model 2 | ================================================================= 3 | 4 | .. currentmodule:: mlflow_rest_client.model 5 | 6 | .. autosummary:: 7 | :nosignatures: 8 | 9 | Model 10 | ModelTag 11 | 12 | .. autosummary:: 13 | :nosignatures: 14 | 15 | ModelVersion 16 | ModelVersionTag 17 | ModelVersionStage 18 | ModelVersionState 19 | ModelVersionStatus 20 | 21 | .. autoclass:: mlflow_rest_client.model.Model 22 | :members: 23 | 24 | .. autoclass:: mlflow_rest_client.model.ModelTag 25 | :members: 26 | :show-inheritance: 27 | 28 | .. autoclass:: mlflow_rest_client.model.ModelVersion 29 | :members: 30 | 31 | .. autoclass:: mlflow_rest_client.model.ModelVersionTag 32 | :members: 33 | :show-inheritance: 34 | 35 | .. autoclass:: mlflow_rest_client.model.ModelVersionStage 36 | :members: 37 | 38 | .. autoclass:: mlflow_rest_client.model.ModelVersionState 39 | :members: 40 | 41 | .. autoclass:: mlflow_rest_client.model.ModelVersionStatus 42 | :members: 43 | -------------------------------------------------------------------------------- /docs/mlflow_rest_client.page.rst: -------------------------------------------------------------------------------- 1 | Page 2 | ================================================================= 3 | 4 | .. currentmodule:: mlflow_rest_client.page 5 | 6 | .. autosummary:: 7 | :nosignatures: 8 | 9 | Page 10 | 11 | .. autoclass:: mlflow_rest_client.page.Page 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/mlflow_rest_client.run.rst: -------------------------------------------------------------------------------- 1 | Run 2 | ================================================================= 3 | 4 | .. currentmodule:: mlflow_rest_client.run 5 | 6 | .. autosummary:: 7 | :nosignatures: 8 | 9 | Run 10 | RunStage 11 | RunStatus 12 | 13 | .. autosummary:: 14 | :nosignatures: 15 | 16 | RunInfo 17 | RunViewType 18 | 19 | .. autosummary:: 20 | :nosignatures: 21 | 22 | RunData 23 | Param 24 | Metric 25 | RunTag 26 | 27 | .. autoclass:: mlflow_rest_client.run.Run 28 | :members: 29 | 30 | .. autoclass:: mlflow_rest_client.run.RunStage 31 | :members: 32 | 33 | .. autoclass:: mlflow_rest_client.run.RunStatus 34 | :members: 35 | 36 | .. autoclass:: mlflow_rest_client.run.RunInfo 37 | :members: 38 | 39 | .. autoclass:: mlflow_rest_client.run.RunViewType 40 | :members: 41 | 42 | .. autoclass:: mlflow_rest_client.run.RunData 43 | :members: 44 | 45 | .. autoclass:: mlflow_rest_client.run.Param 46 | :members: 47 | :show-inheritance: 48 | 49 | .. autoclass:: mlflow_rest_client.run.Metric 50 | :members: 51 | 52 | .. autoclass:: mlflow_rest_client.run.RunTag 53 | :members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /docs/mlflow_rest_client.tag.rst: -------------------------------------------------------------------------------- 1 | Tag 2 | ================================================================= 3 | 4 | .. currentmodule:: mlflow_rest_client.tag 5 | 6 | .. autosummary:: 7 | :nosignatures: 8 | 9 | Tag 10 | 11 | .. autoclass:: mlflow_rest_client.tag.Tag 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/security.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../SECURITY.rst 2 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: usage 3 | -------------------------------------------------------------------------------- /mlflow_rest_client/VERSION: -------------------------------------------------------------------------------- 1 | 2.0.1 -------------------------------------------------------------------------------- /mlflow_rest_client/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | # pylint: disable= wrong-import-position 4 | 5 | from .mlflow_rest_client import MLflowRESTClient 6 | from .version import get_version 7 | 8 | __version__ = get_version() 9 | -------------------------------------------------------------------------------- /mlflow_rest_client/artifact.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | import os 6 | from pathlib import Path 7 | from typing import Optional 8 | 9 | from pydantic import AnyUrl, BaseModel # pylint: disable=no-name-in-module 10 | 11 | 12 | class Artifact(BaseModel): 13 | """Artifact representation 14 | 15 | Parameters 16 | ---------- 17 | path : str 18 | Artifact path 19 | 20 | root : str 21 | Artifact root 22 | 23 | is_dir : bool 24 | Is artifact a dir 25 | 26 | file_size : int 27 | Artifact file size in bytes 28 | 29 | Examples 30 | -------- 31 | .. code:: python 32 | 33 | artifact = Artifact(path="some/path") 34 | """ 35 | 36 | path: Path 37 | file_size: int 38 | root: Optional[AnyUrl] 39 | 40 | is_dir: bool = False 41 | 42 | class Config: 43 | frozen = True 44 | 45 | @property 46 | def full_path(self): 47 | if self.root: 48 | return os.path.join(self.root, self.path) 49 | return self.path 50 | 51 | def __str__(self): 52 | return self.full_path 53 | -------------------------------------------------------------------------------- /mlflow_rest_client/experiment.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | from enum import Enum 6 | from typing import List 7 | 8 | from pydantic import BaseModel, Field # pylint: disable=no-name-in-module 9 | 10 | from .internal import ListableBase 11 | from .tag import Tag 12 | 13 | 14 | # pylint: disable=invalid-name 15 | class ExperimentStage(Enum): 16 | """Experiment stage""" 17 | 18 | ACTIVE = "active" 19 | """ Experiment is active """ 20 | 21 | DELETED = "deleted" 22 | """ Experiment was deleted""" 23 | 24 | 25 | # pylint: disable=too-many-ancestors 26 | class ExperimentTag(Tag): 27 | """Experiment tag 28 | 29 | Parameters 30 | ---------- 31 | key : str 32 | Tag name 33 | 34 | value : str 35 | Tag value 36 | 37 | Attributes 38 | ---------- 39 | key : str 40 | Tag name 41 | 42 | value : str 43 | Tag value 44 | 45 | Examples 46 | -------- 47 | .. code:: python 48 | 49 | tag = ExperimentTag(key="some.tag", value="some.val") 50 | """ 51 | 52 | 53 | class ListExperimentTags(ListableBase): 54 | __root__: List[ExperimentTag] 55 | 56 | 57 | class Experiment(BaseModel): 58 | """Experiment representation 59 | 60 | Parameters 61 | ---------- 62 | id : int 63 | Experiment ID 64 | 65 | name : str 66 | Experiment name 67 | 68 | artifact_location : str, optional 69 | Experiment artifact location 70 | 71 | stage : :obj:`str` or :obj:`ExperimentStage`, optional 72 | Experiment stage 73 | 74 | tags : :obj:`dict` or :obj:`list` of :obj:`dict`, optional 75 | Experiment tags list 76 | 77 | Attributes 78 | ---------- 79 | id : int 80 | Experiment ID 81 | 82 | name : str 83 | Experiment name 84 | 85 | artifact_location : str 86 | Experiment artifact location 87 | 88 | stage : :obj:`EXPERIMENTSTAGE` 89 | Experiment stage 90 | 91 | tags : :obj:`ExperimentTagList` 92 | Experiment tags list 93 | 94 | Examples 95 | -------- 96 | .. code:: python 97 | 98 | experiment = Experiment(id=123, name="some_name") 99 | """ 100 | 101 | id: int = Field(alias="experiment_id") 102 | name: str 103 | artifact_location: str = "" 104 | stage: ExperimentStage = Field(ExperimentStage.ACTIVE, alias="lifecycle_stage") 105 | tags: ListExperimentTags = Field(default_factory=list) 106 | 107 | class Config: 108 | frozen = True 109 | 110 | def __str__(self): 111 | return self.name 112 | -------------------------------------------------------------------------------- /mlflow_rest_client/internal.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | from typing import List 6 | 7 | from pydantic import BaseModel # pylint: disable=no-name-in-module 8 | 9 | from .tag import Tag 10 | 11 | 12 | class ListableBase(BaseModel): 13 | __root__: list 14 | 15 | class Config: 16 | frozen = True 17 | 18 | @property 19 | def as_dict(self): 20 | return {item.key: item for item in self.__root__} 21 | 22 | def __iter__(self): 23 | return iter(self.__root__) 24 | 25 | def __getitem__(self, item): 26 | if isinstance(item, str) and self.as_dict: 27 | return self.as_dict[item] 28 | 29 | return self.__root__[item] 30 | 31 | def __contains__(self, item): 32 | if isinstance(item, str): 33 | return any(i.key == item for i in self.__root__) 34 | 35 | return item in self.__root__ 36 | 37 | def __len__(self): 38 | return len(self.__root__) 39 | 40 | 41 | class ListableTag(ListableBase): 42 | __root__: List[Tag] 43 | 44 | def __getitem__(self, item): 45 | if isinstance(item, str): 46 | res = {i.key: i for i in self.__root__} 47 | return res[item] 48 | 49 | return super().__getitem__(item) 50 | -------------------------------------------------------------------------------- /mlflow_rest_client/model.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | from datetime import datetime 6 | from enum import Enum 7 | from typing import List, Optional, Union 8 | from uuid import UUID 9 | 10 | from pydantic import ( # pylint: disable=no-name-in-module 11 | BaseModel, 12 | Field, 13 | root_validator, 14 | validator, 15 | ) 16 | 17 | from mlflow_rest_client.internal import ListableBase 18 | 19 | from .tag import Tag 20 | 21 | 22 | # pylint: disable=invalid-name 23 | class ModelVersionStage(Enum): 24 | """Model version stage""" 25 | 26 | UNKNOWN = "None" 27 | 28 | """ Model version has no stage """ 29 | 30 | TEST = "Staging" 31 | """ Is a testing model version""" 32 | 33 | PROD = "Production" 34 | """ Is a production model version """ 35 | 36 | ARCHIVED = "Archived" 37 | """ Model version was archived """ 38 | 39 | 40 | ModelVersionStageOrList = Union[str, ModelVersionStage, List[ModelVersionStage], List[str]] 41 | 42 | 43 | # pylint: disable=invalid-name 44 | class ModelVersionState(Enum): 45 | """Model version state""" 46 | 47 | PENDING = "PENDING_REGISTRATION" 48 | """ Model version registration is pending """ 49 | 50 | FAILED = "FAILED_REGISTRATION" 51 | """ Model version registration was failed """ 52 | 53 | READY = "READY" 54 | """ Model version registration was successful """ 55 | 56 | 57 | class ModelVersionStatus(BaseModel): 58 | """Model version state with message 59 | 60 | Parameters 61 | ---------- 62 | state : :obj:`str` or :obj:`ModelVersionState`, optional 63 | Model version state 64 | 65 | message : str, optional 66 | Model version state message 67 | 68 | Attributes 69 | ---------- 70 | state : :obj:`ModelVersionState` 71 | Model version state 72 | 73 | message : str 74 | Model version state message 75 | 76 | Examples 77 | -------- 78 | .. code:: python 79 | 80 | status = ModelVersionStatus(state="READY") 81 | 82 | status = ModelVersionStatus(state=ModelVersionState.READY) 83 | 84 | status = ModelVersionStatus(state=ModelVersionState.FAILED, message="Reason") 85 | """ 86 | 87 | state: ModelVersionState = ModelVersionState.PENDING 88 | message: str = "" 89 | 90 | class Config: 91 | frozen = True 92 | 93 | @validator("state") 94 | def valid_state(cls, val): # pylint: disable=no-self-argument 95 | return ModelVersionState(val) if isinstance(val, str) else ModelVersionState.PENDING 96 | 97 | def __str__(self): 98 | return str(self.state.name) + (f" because of '{self.message}'" if self.message else "") 99 | 100 | 101 | # pylint: disable=too-many-ancestors 102 | class ModelVersionTag(Tag): 103 | """Model version tag 104 | 105 | Parameters 106 | ---------- 107 | key : str 108 | Tag name 109 | 110 | value : str 111 | Tag value 112 | 113 | Attributes 114 | ---------- 115 | key : str 116 | Tag name 117 | 118 | value : str 119 | Tag value 120 | 121 | Examples 122 | -------- 123 | .. code:: python 124 | 125 | tag = ModelVersionTag(key="some.tag", value="some.val") 126 | """ 127 | 128 | 129 | class ListableModelVersionTag(ListableBase): 130 | __root__: List[ModelVersionTag] = [] 131 | 132 | 133 | # pylint: disable=too-many-ancestors 134 | class ModelTag(Tag): 135 | """Model tag 136 | 137 | Parameters 138 | ---------- 139 | key : str 140 | Tag name 141 | 142 | value : str 143 | Tag value 144 | 145 | Attributes 146 | ---------- 147 | key : str 148 | Tag name 149 | 150 | value : str 151 | Tag value 152 | 153 | Examples 154 | -------- 155 | .. code:: python 156 | 157 | tag = ModelTag(key="some.tag", value="some.val") 158 | """ 159 | 160 | 161 | class ListableModelTag(ListableBase): 162 | __root__: List[ModelTag] 163 | 164 | 165 | # pylint: disable=too-many-instance-attributes, no-self-argument 166 | class ModelVersion(BaseModel): 167 | """Model version representation 168 | 169 | Parameters 170 | ---------- 171 | name : str 172 | Model name 173 | 174 | version : int 175 | Version number 176 | 177 | creation_timestamp : :obj:`int` (UNIX timestamp) or :obj:`datetime.datetime`, optional 178 | Version creation timestamp 179 | 180 | last_updated_timestamp : :obj:`int` (UNIX timestamp) or :obj:`datetime.datetime`, optional 181 | Version last update timestamp 182 | 183 | stage : :obj:`str` or :obj:`ModelVersionStage`, optional 184 | Version stage 185 | 186 | description : str, optional 187 | Version description 188 | 189 | source : str, optional 190 | Version source path 191 | 192 | run_id : str, optional 193 | Run ID used for generating version 194 | 195 | state : :obj:`str` or :obj:`ModelVersionState`, optional 196 | Version stage 197 | 198 | state_message : str, optional 199 | Version state message 200 | 201 | tags : :obj:`dict` or :obj:`list` of :obj:`dict`, optional 202 | Experiment tags list 203 | 204 | Attributes 205 | ---------- 206 | name : str 207 | Model name 208 | 209 | version : int 210 | Version number 211 | 212 | created_time : :obj:`datetime.datetime` 213 | Version creation timestamp 214 | 215 | updated_time : :obj:`datetime.datetime` 216 | Version last update timestamp 217 | 218 | stage : :obj:`ModelVersionStage` 219 | Version stage 220 | 221 | description : str 222 | Version description 223 | 224 | source : str 225 | Version source path 226 | 227 | run_id : str 228 | Run ID used for generating version 229 | 230 | status : :obj:`ModelVersionStatus` 231 | Version status 232 | 233 | tags : :obj:`ModelVersionTagList` 234 | Experiment tags list 235 | 236 | Examples 237 | -------- 238 | .. code:: python 239 | 240 | model_version = ModelVersion(name="some_model", version=1) 241 | """ 242 | 243 | name: str 244 | version: int 245 | created_time: datetime = Field(None, alias="creation_timestamp") 246 | updated_time: datetime = Field(None, alias="last_updated_timestamp") 247 | stage: ModelVersionStage = Field(ModelVersionStage.UNKNOWN, alias="current_stage") 248 | description: str = "" 249 | source: str = "" 250 | run_id: Optional[UUID] = None 251 | status: ModelVersionStatus = Field(ModelVersionStatus(), alias="state") 252 | tags: ListableModelVersionTag = Field(default_factory=list) 253 | 254 | class Config: 255 | frozen = True 256 | allow_population_by_field_name = True 257 | 258 | @validator("run_id", pre=True) 259 | def validation_run_id(cls, val): 260 | if val == "": 261 | return None 262 | return val 263 | 264 | @validator("tags", pre=True) 265 | def validation_tags(cls, val): 266 | if isinstance(val, dict): 267 | return [val] 268 | 269 | return val 270 | 271 | @validator("status", pre=True) 272 | def validator_status(cls, val): 273 | if isinstance(val, ModelVersionState): 274 | return {"status": val} 275 | if isinstance(val, str): 276 | return {"status": ModelVersionState(val)} 277 | 278 | return val 279 | 280 | def __str__(self): 281 | return f"{self.name} v{self.version}" 282 | 283 | @root_validator(pre=True) 284 | def main_validator(cls, values): 285 | if "state_message" in values: 286 | values["state"]["message"] = values["state_message"] 287 | 288 | if "status_message" in values: 289 | values["status"]["message"] = values["status_message"] 290 | 291 | return values 292 | 293 | 294 | class ListableModelVersion(ListableBase): 295 | __root__: List[ModelVersion] 296 | 297 | def __getitem__(self, item): 298 | if isinstance(item, ModelVersionStage): 299 | res = {i.stage: i for i in self.__root__} 300 | return res[item] 301 | 302 | if isinstance(item, str): 303 | res = {i.name: i for i in self.__root__} 304 | return res[item] 305 | 306 | return self.__root__[item] 307 | 308 | def __contains__(self, item): 309 | for itm in self.__root__: 310 | if isinstance(item, ModelVersionStage) and item == itm.stage: 311 | return True 312 | 313 | if (itm.name == item.name) and (itm.version == item.version): 314 | return True 315 | return False 316 | 317 | 318 | class Model(BaseModel): 319 | name: str 320 | 321 | versions: ListableModelVersion = Field(default_factory=list, alias="latest_versions") 322 | created_time: datetime = Field(None, alias="creation_timestamp") 323 | updated_time: datetime = Field(None, alias="last_updated_timestamp") 324 | description: str = "" 325 | tags: ListableModelTag = Field(default_factory=list) 326 | 327 | def __str__(self): 328 | return str(self.name) 329 | 330 | class Config: 331 | frozen = True 332 | allow_population_by_field_name = True 333 | 334 | 335 | class ListableModel(ListableBase): 336 | __root__: List[Model] 337 | 338 | def __getitem__(self, item): 339 | if isinstance(item, str): 340 | res = {i.name: i for i in self.__root__} 341 | return res[item] 342 | 343 | return self.__root__[item] 344 | 345 | def __contains__(self, item): 346 | if isinstance(item, str): 347 | res = [i.name for i in self.__root__] 348 | 349 | if isinstance(item, Model): 350 | res = self.__root__ 351 | 352 | return item in res 353 | -------------------------------------------------------------------------------- /mlflow_rest_client/page.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | 6 | class Page: 7 | """Page representation 8 | 9 | Parameters 10 | ---------- 11 | items : Iterable, optional 12 | Page items 13 | 14 | next_page_token : str, optional 15 | Next page token 16 | 17 | Attributes 18 | ---------- 19 | items : Iterable 20 | Page items 21 | 22 | next_page_token : str 23 | Next page token 24 | 25 | Examples 26 | -------- 27 | .. code:: python 28 | 29 | model = Page(items=[Model(name="some_model")]) 30 | 31 | model = Page(items=[Model(name="some_model")], next_page_token="some_token") 32 | """ 33 | 34 | def __init__(self, items=None, next_page_token=None): 35 | self.items = items or [] 36 | self.next_page_token = str(next_page_token) if next_page_token is not None else next_page_token 37 | self._index = 0 38 | 39 | @classmethod 40 | def make(cls, inp, items_key="items", item_class=None, **kwargs): 41 | """ 42 | Generate objects from REST API response 43 | 44 | Parameters 45 | ---------- 46 | inp : :obj:`list` or :obj:`dict` 47 | Page items 48 | 49 | items_key : str, optional 50 | Key name for fetching items from dict input 51 | 52 | item_class : class, optional 53 | Item class to be called 54 | 55 | Should implement `from_list` or `make` methods, otherwise constructor will be used 56 | 57 | **kwargs : dict, optional 58 | Additional params for item constructor 59 | 60 | Returns 61 | ------- 62 | page : obj:`Page` of `item_class` 63 | Page of items 64 | 65 | Examples 66 | -------- 67 | .. code:: python 68 | 69 | model = Page.make([Model(name="some_model")]) 70 | 71 | model = Page.make([ModelVersion(name="some_model", version=1)], name="another_model") 72 | """ 73 | 74 | items = inp.copy() 75 | next_page_token = None 76 | 77 | if isinstance(inp, dict): 78 | items = inp.get(items_key, []) 79 | next_page_token = inp.get("next_page_token", None) or kwargs.pop("next_page_token", None) 80 | 81 | if item_class: 82 | items = [item_class.parse_obj(item, **kwargs) for item in items] 83 | 84 | return cls(items=items, next_page_token=next_page_token) 85 | 86 | if isinstance(inp, dict): 87 | return cls(items=items, next_page_token=next_page_token) 88 | 89 | try: 90 | return cls(items=vars(inp), next_page_token=next_page_token) 91 | except TypeError: 92 | return None 93 | 94 | @property 95 | def has_next_page(self): 96 | """ 97 | Checks whether this page is last or not 98 | 99 | Returns 100 | ------- 101 | has_next_page: bool 102 | `True` if there is a next page, `False` if page is last one 103 | """ 104 | return bool(self.next_page_token) 105 | 106 | def __repr__(self): 107 | return f"<{self.__class__.__name__} items={len(self.items)} has_next_page={self.has_next_page}>" 108 | 109 | def __eq__(self, other): 110 | if other is not None and not isinstance(other, self.__class__): 111 | if isinstance(other, list): 112 | return self.items == other 113 | 114 | other = self.make(other) 115 | 116 | return repr(self) == repr(other) 117 | 118 | def __getitem__(self, i): 119 | return self.items[i] 120 | 121 | def __getattr__(self, attr): 122 | return getattr(self.items, attr) 123 | 124 | def __add__(self, item): 125 | self.items.append(item) 126 | return self 127 | 128 | def __contains__(self, item): 129 | return item in self.items 130 | 131 | def __delitem__(self, i): 132 | del self.items[i] 133 | 134 | def __len__(self): 135 | return len(self.items) 136 | 137 | def __iter__(self): 138 | self._index = 0 139 | return self 140 | 141 | def __next__(self): 142 | try: 143 | result = self.items[self._index] 144 | except IndexError as e: 145 | raise StopIteration from e 146 | self._index += 1 147 | return result 148 | -------------------------------------------------------------------------------- /mlflow_rest_client/run.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | from datetime import datetime 6 | from enum import Enum 7 | from typing import Any, List, Optional, Union 8 | from uuid import UUID 9 | 10 | from pydantic import ( # pylint: disable=no-name-in-module 11 | BaseModel, 12 | Field, 13 | root_validator, 14 | ) 15 | 16 | from .internal import ListableBase, ListableTag 17 | from .tag import Tag 18 | 19 | RunId = Union[str, UUID] 20 | 21 | 22 | # pylint: disable=invalid-name 23 | class RunStage(Enum): 24 | """Run stage""" 25 | 26 | ACTIVE = "active" 27 | """ Run is active """ 28 | 29 | DELETED = "deleted" 30 | """ Run was deleted """ 31 | 32 | 33 | # pylint: disable=invalid-name 34 | class RunStatus(Enum): 35 | """Run status""" 36 | 37 | STARTED = "RUNNING" 38 | """ Run is running or created """ 39 | 40 | SCHEDULED = "SCHEDULED" 41 | """ Run is scheduled for run """ 42 | 43 | FINISHED = "FINISHED" 44 | """ Run was finished successfully """ 45 | 46 | FAILED = "FAILED" 47 | """ Run is failed """ 48 | 49 | KILLED = "KILLED" 50 | """ Run was killed """ 51 | 52 | 53 | # pylint: disable=invalid-name 54 | class RunViewType(Enum): 55 | """Run view type""" 56 | 57 | ACTIVE = "ACTIVE_ONLY" 58 | """ Show only active runs """ 59 | 60 | DELETED = "DELETED_ONLY" 61 | """ Show only deleted runs """ 62 | 63 | ALL = "ALL" 64 | """ Show all runs """ 65 | 66 | 67 | class RunInfo(BaseModel): 68 | """Run information representation 69 | 70 | Parameters 71 | ---------- 72 | id : str 73 | Run ID 74 | 75 | experiment_id : int, optional 76 | Experiment ID 77 | 78 | status : :obj:`str` or :obj:`RunStatus`, optional 79 | Run status 80 | 81 | stage : :obj:`str` or :obj:`RunStage`, optional 82 | Run stage 83 | 84 | start_time : :obj:`int` (UNIX timestamp) or :obj:`datetime.datetime`, optional 85 | Run start time 86 | 87 | end_time : :obj:`int` (UNIX timestamp) or :obj:`datetime.datetime`, optional 88 | Run end time 89 | 90 | artifact_uri : str, optional 91 | Artifact URL 92 | 93 | Attributes 94 | ---------- 95 | id : :str 96 | Run ID 97 | 98 | experiment_id : int 99 | Experiment ID 100 | 101 | status : :obj:`RunStatus` 102 | Run status 103 | 104 | stage : :obj:`RunStage` 105 | Run stage 106 | 107 | start_time : :obj:`datetime.datetime` 108 | Run start time 109 | 110 | end_time : :obj:`datetime.datetime` 111 | Run end time 112 | 113 | artifact_uri : str 114 | Artifact URL 115 | 116 | Examples 117 | -------- 118 | .. code:: python 119 | 120 | run_info = RunInfo("some_id") 121 | """ 122 | 123 | id: UUID 124 | experiment_id: Optional[int] = None 125 | status: RunStatus = RunStatus.STARTED 126 | stage: RunStage = Field(RunStage.ACTIVE, alias="lifecycle_stage") 127 | start_time: Optional[datetime] = None 128 | end_time: Optional[datetime] = None 129 | artifact_uri: str = "" 130 | 131 | class Config: 132 | frozen = True 133 | allow_population_by_field_name = True 134 | 135 | @root_validator(pre=True) 136 | def validate_date(cls, values): # pylint: disable=no-self-argument 137 | if not values.get("id"): 138 | values["id"] = values.get("run_id") or values.get("run_uuid") 139 | return values 140 | 141 | def __str__(self): 142 | return str(self.id) 143 | 144 | 145 | class ListableRunInfo(ListableBase): 146 | __root__: List[RunInfo] 147 | 148 | def __getitem__(self, item): 149 | if isinstance(item, str): 150 | item = UUID(item) 151 | 152 | if isinstance(item, UUID): 153 | res = {i.id: i for i in self.__root__} 154 | return res[item] 155 | 156 | return self.__root__[item] 157 | 158 | def __contains__(self, item): 159 | res = [i.id for i in self.__root__] 160 | 161 | if isinstance(item, str): 162 | item = UUID(str(item)) 163 | 164 | if isinstance(item, RunInfo): 165 | res = self.__root__ 166 | 167 | return item in res 168 | 169 | 170 | class Param(Tag): 171 | """Run parameter 172 | 173 | Parameters 174 | ---------- 175 | key : str 176 | Param name 177 | 178 | value : str 179 | Param value 180 | 181 | Attributes 182 | ---------- 183 | key : str 184 | Param name 185 | 186 | value : str 187 | Param value 188 | """ 189 | 190 | 191 | class ListableParam(ListableBase): 192 | __root__: List[Param] 193 | 194 | 195 | # pylint: disable=too-many-ancestors 196 | class Metric(BaseModel): 197 | """Run metric representation 198 | 199 | Parameters 200 | ---------- 201 | name : str 202 | Metric name 203 | 204 | value : float, optional 205 | Metric value 206 | 207 | step : int, optional 208 | Metric step 209 | 210 | timestamp : :obj:`int` (UNIX timestamp) or :obj:`datetime.datetime`, optional 211 | Metric timestamp 212 | 213 | Attributes 214 | ---------- 215 | name : str 216 | Metric name 217 | 218 | value : float 219 | Metric value 220 | 221 | step : int 222 | Metric step 223 | 224 | timestamp : :obj:`datetime.datetime` 225 | Metric timestamp 226 | 227 | Examples 228 | -------- 229 | .. code:: python 230 | 231 | metric = Metric(name="some.metric") 232 | metric = Metric(name="some.metric", value=1.23) 233 | metric = Metric(name="some.metric", value=1.23, step=2) 234 | metric = Metric( 235 | name="some.metric", value=1.23, step=2, timestamp=datetime.datetime.now() 236 | ) 237 | """ 238 | 239 | key: str 240 | value: Optional[float] = None 241 | step: int = 0 242 | timestamp: Optional[datetime] = None 243 | 244 | def __str__(self): 245 | return str(f"{self.key}: {self.value} for {self.step} at {self.timestamp}") 246 | 247 | class Config: 248 | frozen = True 249 | 250 | 251 | class ListableMetric(ListableBase): 252 | __root__: List[Metric] 253 | 254 | 255 | # pylint: disable=too-many-ancestors 256 | class RunTag(Tag): 257 | """Run tag 258 | 259 | Parameters 260 | ---------- 261 | key : str 262 | Tag name 263 | 264 | value : str 265 | Tag value 266 | 267 | Attributes 268 | ---------- 269 | key : str 270 | Tag name 271 | 272 | value : str 273 | Tag value 274 | 275 | Examples 276 | -------- 277 | .. code:: python 278 | 279 | tag = RunTag(key="some.tag", value="some.val") 280 | """ 281 | 282 | 283 | class RunData(BaseModel): 284 | """Run data representation 285 | 286 | Parameters 287 | ---------- 288 | params : :obj:`dict` or :obj:`list` of :obj:`dict`, optional 289 | Params list 290 | 291 | metrics : :obj:`dict` or :obj:`list` of :obj:`dict`, optional 292 | Metrics list 293 | 294 | tags : :obj:`dict` or :obj:`list` of :obj:`dict`, optional 295 | Run tags list 296 | 297 | Attributes 298 | ---------- 299 | params : :obj:`ParamList` 300 | Params list 301 | 302 | metrics : :obj:`MetricList` 303 | Metrics list 304 | 305 | tags : :obj:`RunTagList` 306 | Run tags list 307 | 308 | Examples 309 | -------- 310 | .. code:: python 311 | 312 | param = Param(key="some.param", value="some_value") 313 | metric = Metric(name="some.metric", value=1.23) 314 | tag = RunTag(key="some.tag", value="some.val") 315 | 316 | run_data = RunData(params=[param], metrics=[metric], tags=[tag]) 317 | """ 318 | 319 | params: ListableParam = Field(default_factory=list) 320 | metrics: ListableMetric = Field(default_factory=list) 321 | tags: ListableTag = Field(default_factory=list) 322 | 323 | class Config: 324 | frozen = True 325 | 326 | 327 | class Run(BaseModel): 328 | """Run representation 329 | 330 | Parameters 331 | ---------- 332 | info : :obj:`dict` or :obj:`RunInfo` 333 | Run info 334 | 335 | data : :obj:`dict` or :obj:`RunData`, optional 336 | Run data 337 | 338 | Attributes 339 | ---------- 340 | info : :obj:`RunInfo` 341 | Run info 342 | 343 | data : :obj:`RunData` 344 | Run data 345 | 346 | Examples 347 | -------- 348 | .. code:: python 349 | 350 | run_info = RunInfo(id="some_id") 351 | run_data = RunData(params=..., metrics=..., tags=...) 352 | 353 | run = Run(run_info, run_data) 354 | """ 355 | 356 | info: RunInfo 357 | data: RunData = Field(default_factory=RunData) 358 | 359 | class Config: 360 | frozen = True 361 | 362 | def __str__(self) -> str: 363 | return str(self.info) 364 | 365 | def __getattr__(self, attr): 366 | if hasattr(self.info, attr): 367 | return getattr(self.info, attr) 368 | if hasattr(self.data, attr): 369 | return getattr(self.data, attr) 370 | 371 | raise AttributeError(f"{self.__class__.__name__} object has no attribute {attr}") 372 | 373 | def __eq__(self, other: Any) -> bool: 374 | if isinstance(other, Run): 375 | return self.info.dict() == other.info.dict() 376 | 377 | return super().__eq__(other) 378 | -------------------------------------------------------------------------------- /mlflow_rest_client/tag.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | from typing import Dict, List, Union 6 | 7 | from pydantic import BaseModel, root_validator # pylint: disable=no-name-in-module 8 | 9 | 10 | # pylint: disable=too-many-ancestors 11 | class Tag(BaseModel): 12 | """Generic tag class 13 | 14 | Parameters 15 | ---------- 16 | key : str 17 | Tag name 18 | 19 | value : str 20 | Tag value 21 | 22 | Examples 23 | -------- 24 | .. code:: python 25 | 26 | tag = Tag(key="some.tag", value="some.val") 27 | """ 28 | 29 | key: str 30 | value: str = "" 31 | 32 | class Config: 33 | frozen = True 34 | 35 | def __str__(self): 36 | return self.key 37 | 38 | @root_validator(pre=True) 39 | def to_dict(cls, values: dict) -> dict: # pylint: disable=no-self-argument 40 | """Bring to a single format.""" 41 | if isinstance(values, dict) and ("key" not in values and "value" not in values): 42 | result = {} 43 | for key, val in values.items(): 44 | result["key"] = key 45 | result["value"] = val 46 | 47 | return result 48 | 49 | return values 50 | 51 | 52 | # Custom type for type hints with Tag models 53 | TagsListOrDict = Union[Dict[str, str], List[Dict[str, str]], List[Tag]] 54 | -------------------------------------------------------------------------------- /mlflow_rest_client/timestamp.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | import datetime 6 | from enum import Enum 7 | from typing import Union 8 | 9 | AnyTimestamp = Union[int, datetime.datetime, None] 10 | 11 | 12 | class Unit(Enum): 13 | MSEC = 1000 14 | SEC = 1 15 | 16 | 17 | def current_timestamp() -> int: 18 | return int(datetime.datetime.now().timestamp()) 19 | 20 | 21 | def normalize_timestamp(timestamp: int | float) -> int: 22 | timestamp = int(timestamp) 23 | if timestamp >= 1000000000000: 24 | unit = Unit.MSEC 25 | else: 26 | unit = Unit.SEC 27 | return timestamp // unit.value 28 | 29 | 30 | def mlflow_timestamp(timestamp: int) -> int: 31 | return timestamp * 1000 32 | 33 | 34 | def timestamp_2_time(timestamp: AnyTimestamp) -> datetime.datetime | None: 35 | if timestamp: 36 | if isinstance(timestamp, datetime.datetime): 37 | return timestamp 38 | return datetime.datetime.utcfromtimestamp(normalize_timestamp(timestamp)) 39 | return None 40 | 41 | 42 | def format_to_timestamp(data: AnyTimestamp = None) -> int: 43 | """Any object (str, int, datetime formatting to timestamp.""" 44 | 45 | if not data: 46 | result = datetime.datetime.now().timestamp() 47 | elif isinstance(data, int): 48 | result = normalize_timestamp(data) 49 | elif isinstance(data, datetime.datetime): 50 | result = data.timestamp() 51 | else: 52 | result = data 53 | 54 | return normalize_timestamp(int(result)) 55 | -------------------------------------------------------------------------------- /mlflow_rest_client/version.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 MTS (Mobile Telesystems) 2 | # SPDX-License-Identifier: Apache-2.0 3 | from __future__ import annotations 4 | 5 | import os 6 | 7 | VERSION_FILE = os.path.join(os.path.dirname(__file__), "VERSION") 8 | 9 | 10 | def get_version() -> str: 11 | with open(VERSION_FILE) as f: 12 | return f.read().strip() 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | multi_line_output = 3 4 | 5 | [tool.black] 6 | line-length = 120 7 | target-version = ['py37', 'py38', 'py39', 'py310'] 8 | include = '\.pyi?$' 9 | exclude = ''' 10 | 11 | ( 12 | /( 13 | \.eggs # exclude a few common directories in the 14 | | \.git # root of the project 15 | | \.mypy_cache 16 | | \.tox 17 | | \.venv 18 | | _build 19 | | buck-out 20 | | build 21 | | dist 22 | )/ 23 | ) 24 | ''' 25 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit 2 | pylint 3 | setuptools-git-versioning 4 | -------------------------------------------------------------------------------- /requirements-doc.txt: -------------------------------------------------------------------------------- 1 | changelog 2 | furo 3 | numpydoc 4 | sphinx 5 | sphinx-autodoc-typehints 6 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | pytest 3 | pytest-logger 4 | pytest-rerunfailures 5 | pytest-timeout 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic<2 2 | requests 3 | urllib3 4 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobileTeleSystems/mlflow-rest-client/7f6d22b82f6213936ac7389817d7e3354cfb037e/samples/__init__.py -------------------------------------------------------------------------------- /samples/sample.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | from mlflow_rest_client import MLflowRESTClient 5 | from mlflow_rest_client.log import get_logger 6 | from mlflow_rest_client.timestamp import current_timestamp 7 | 8 | logger = get_logger() 9 | 10 | 11 | def process(client): 12 | logger.info("====== list_experiments") 13 | exps = client.list_experiments() 14 | logger.info(f"list_experiments: #experiments: {len(exps)}") 15 | for exp in exps: 16 | logger.info(f" {exp}") 17 | 18 | logger.info("====== get_or_create_experiment") 19 | experiment_name = "py_exp_" + str(time.time()).replace(".", "") 20 | logger.info(f"create experiment with name {experiment_name}") 21 | experiment = client.get_or_create_experiment(experiment_name) 22 | logger.info(f" id: {experiment.id}") 23 | 24 | logger.info("====== create_run") 25 | run_name = "run_for_exp_" + experiment_name 26 | start_time = current_timestamp() 27 | run = client.create_run(experiment_id=experiment.id, name=run_name, start_time=start_time) 28 | logger.info(f" run id {run.id}") 29 | 30 | logger.info("====== log_run_parameter and metrics") 31 | param_key = "max_depth" 32 | param_value = "2" 33 | client.log_run_parameter(run.id, param_key, param_value) 34 | 35 | logger.info("====== log_run_metric") 36 | metric_key = "auc" 37 | metric_value = 0.59 38 | client.log_run_metric(run.id, metric_key, metric_value) 39 | metric_value = 0.69 40 | client.log_run_metric(run.id, metric_key, metric_value, step=1, timestamp=start_time + 10) 41 | metric_value = 0.79 42 | client.log_run_metric(run.id, metric_key, metric_value, step=2, timestamp=start_time + 15) 43 | metric_value = 0.89 44 | client.log_run_metric(run.id, metric_key, metric_value, step=2, timestamp=start_time + 20) 45 | metric_value = 0.99 46 | client.log_run_metric(run.id, metric_key, metric_value, step=3, timestamp=start_time + 30) 47 | 48 | logger.info("====== finish_run") 49 | client.finish_run(run.id, end_time=start_time + 20) 50 | 51 | logger.info("====== get_run") 52 | run = client.get_run(run.id) 53 | logger.info(f" {run}") 54 | 55 | logger.info("====== get_experiment") 56 | experiment = client.get_experiment(experiment.id) 57 | logger.info(f" {experiment}") 58 | 59 | logger.info("====== get_metric_history") 60 | metric_history = client.get_run_metric_history(run.id, metric_key) 61 | logger.info(f" {metric_history}") 62 | 63 | logger.info("====== list_run_artifacts") 64 | artifacts = client.list_run_artifacts(run.id) 65 | logger.info(f" {artifacts}") 66 | 67 | 68 | if __name__ == "__main__": 69 | if len(sys.argv) < 2: 70 | sys.stderr.write("ERROR: Expecting BASE_URL") 71 | sys.exit(1) 72 | client = MLflowRESTClient(sys.argv[1]) 73 | process(client) 74 | -------------------------------------------------------------------------------- /samples/sklearn_sample.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calls API operations on the hard-coded Python scikit-learn run in experiment 0 executed in the docker container. 3 | """ 4 | 5 | import sys 6 | 7 | from mlflow_rest_client import MLflowRESTClient 8 | 9 | 10 | def process(client): 11 | experiment_id = "0" 12 | metric_key = "auc" 13 | param_key = "max_depth" 14 | 15 | print("====== get_experiment") 16 | exp = client.get_experiment(experiment_id) 17 | print("get_experiment rsp:", exp) 18 | 19 | run = exp["runs"][0] 20 | print(">> run", run) 21 | print("run.id:", run.id) 22 | 23 | print("====== get_run") 24 | run = client.get_run(run.id) 25 | print("get_run rsp:", run) 26 | 27 | print("====== get_metric") 28 | rsp = client.get_metric(run.id, metric_key) 29 | print("get_metric rsp:", rsp) 30 | 31 | print("====== get_metric_history") 32 | rsp = client.get_metric_history(run.id, metric_key) 33 | print("get_metric_history rsp:", rsp) 34 | 35 | print("====== list_artifacts") 36 | path = "" 37 | rsp = client.list_artifacts(run.id, path) 38 | print("list_artifacts rsp:", rsp) 39 | 40 | print("====== get_artifact - txt") 41 | path = "confusion_matrix.txt" 42 | rsp = client.get_artifact(run.id, path) 43 | print(f"get_artifacts: path={path} rsp={rsp}") 44 | 45 | print("====== get_artifact - pkl") 46 | path = "model/model.pkl" 47 | rsp = client.get_artifact(run.id, path) 48 | print(f"get_artifacts: path={path} #rsp.bytes={len(rsp)}") 49 | 50 | print("====== Search") 51 | rsp = client.search_runs( 52 | experiment_id, 53 | query=f"parameter.{param_key} = 3 and metric.{metric_key} >= 0.99", 54 | ) 55 | print("search_runs rsp:", rsp) 56 | 57 | 58 | if __name__ == "__main__": 59 | if len(sys.argv) < 2: 60 | sys.stderr.write("ERROR: Expecting BASE_URL") 61 | sys.exit(1) 62 | client = MLflowRESTClient(sys.argv[1]) 63 | process(client) 64 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [mypy] 5 | python_version = 3.7 6 | # TODO: remove later 7 | exclude = ^(?=.*file).* 8 | strict_optional=True 9 | # ignore typing in third-party packages 10 | ignore_missing_imports = True 11 | follow_imports = silent 12 | show_error_codes = True 13 | disable_error_code = name-defined, misc 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | here = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | with open(os.path.join(here, "requirements.txt")) as f: 8 | requirements = f.readlines() 9 | 10 | with open(os.path.join(here, "README.rst")) as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name="mlflow-rest-client", 15 | setuptools_git_versioning={ 16 | "enabled": True, 17 | "dev_template": "{tag}.dev{ccount}", 18 | "version_file": os.path.join(here, "mlflow_rest_client", "VERSION"), 19 | "count_commits_from_version_file": True, 20 | }, 21 | description="Python client for MLflow REST API", 22 | long_description=long_description, 23 | long_description_content_type="text/x-rst", 24 | license="Apache-2.0", 25 | license_files=("LICENSE.txt",), 26 | url="https://github.com/MobileTeleSystems/mlflow-rest-client", 27 | author="DSX Team", 28 | author_email="dsx-team@mts.ru", 29 | classifiers=[ 30 | "Development Status :: 4 - Beta", 31 | "Environment :: Console", 32 | "Intended Audience :: Developers", 33 | "License :: OSI Approved :: Apache Software License", 34 | "Operating System :: OS Independent", 35 | "Programming Language :: Python :: 3 :: Only", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | "Programming Language :: Python :: 3.12", 42 | "Topic :: Software Development :: Libraries :: Python Modules", 43 | "Typing :: Typed", 44 | ], 45 | project_urls={ 46 | "Documentation": "https://mlflow-rest-client.readthedocs.io/en/stable/", 47 | "Source": "https://github.com/MobileTeleSystems/mlflow-rest-client", 48 | "CI/CD": "https://github.com/MobileTeleSystems/mlflow-rest-client/actions", 49 | "Tracker": "https://github.com/MobileTeleSystems/mlflow-rest-client/issues", 50 | }, 51 | keywords="MLflow REST API", 52 | packages=find_packages(exclude=["docs", "docs.*", "tests", "tests.*", "samples", "samples.*"]), 53 | python_requires=">=3.7", 54 | install_requires=requirements, 55 | setup_requires=["setuptools-git-versioning>=1.8.1"], 56 | test_suite="tests", 57 | include_package_data=True, 58 | zip_safe=False, 59 | ) 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobileTeleSystems/mlflow-rest-client/7f6d22b82f6213936ac7389817d7e3354cfb037e/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobileTeleSystems/mlflow-rest-client/7f6d22b82f6213936ac7389817d7e3354cfb037e/tests/test_integration/__init__.py -------------------------------------------------------------------------------- /tests/test_integration/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import random 6 | import string 7 | from datetime import datetime 8 | 9 | import pytest 10 | 11 | from mlflow_rest_client import MLflowRESTClient 12 | from mlflow_rest_client.experiment import Experiment 13 | from mlflow_rest_client.model import Model, ModelVersion 14 | from mlflow_rest_client.run import Run 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | DEFAULT_TIMEOUT = 60 19 | 20 | 21 | def rand_str(length: int = 8) -> str: 22 | letters = string.ascii_lowercase 23 | return "".join(random.sample(letters, length)) 24 | 25 | 26 | def rand_int(a: int = 0, b: int = 100) -> int: 27 | return random.randint(a, b) 28 | 29 | 30 | def rand_float(a=0, b=100): 31 | return random.uniform(a, b) 32 | 33 | 34 | def create_exp_name() -> str: 35 | return "pyTestExp-" + rand_str() 36 | 37 | 38 | def create_model_name() -> str: 39 | return "pyTestModel-" + rand_str() 40 | 41 | 42 | def now() -> datetime: 43 | return datetime.now() 44 | 45 | 46 | @pytest.fixture(scope="session") 47 | def client() -> MLflowRESTClient: 48 | host = os.environ.get("MLFLOW_HOST", "localhost") 49 | port = os.environ.get("MLFLOW_PORT", "5000") 50 | api_url = f"http://{host}:{port}" 51 | with MLflowRESTClient(api_url) as client: 52 | yield client 53 | 54 | 55 | @pytest.fixture(scope="function") 56 | def create_experiment(request, client) -> Experiment: 57 | exp_name = create_exp_name() 58 | exp = client.create_experiment(exp_name) 59 | 60 | def finalizer(): 61 | client.delete_experiment(exp.id) 62 | 63 | request.addfinalizer(finalizer) 64 | 65 | return exp 66 | 67 | 68 | @pytest.fixture(scope="function") 69 | def create_run(request, client, create_experiment) -> Run: 70 | exp = create_experiment 71 | run = client.create_run(experiment_id=exp.id) 72 | 73 | def finalizer(): 74 | client.delete_run(run.id) 75 | 76 | request.addfinalizer(finalizer) 77 | 78 | return run 79 | 80 | 81 | @pytest.fixture(scope="function") 82 | def create_model(request, client) -> Model: 83 | model_name = create_model_name() 84 | model = client.create_model(model_name) 85 | 86 | def finalizer(): 87 | client.delete_model(model.name) 88 | 89 | request.addfinalizer(finalizer) 90 | 91 | return model 92 | 93 | 94 | @pytest.fixture(scope="function") 95 | def create_model_version(request, client, create_model) -> ModelVersion: 96 | model = create_model 97 | 98 | version = client.create_model_version(model.name) 99 | 100 | def finalizer(): 101 | client.delete_model_version(version.name, version.version) 102 | 103 | request.addfinalizer(finalizer) 104 | 105 | return version 106 | 107 | 108 | @pytest.fixture(scope="function") 109 | def create_test_model_version(client, create_model_version) -> ModelVersion: 110 | version = create_model_version 111 | 112 | new_version = client.test_model_version(version.name, version.version) 113 | 114 | return new_version 115 | 116 | 117 | @pytest.fixture(scope="function") 118 | def create_prod_model_version(client, create_model_version) -> ModelVersion: 119 | version = create_model_version 120 | 121 | new_version = client.promote_model_version(version.name, version.version) 122 | 123 | return new_version 124 | 125 | 126 | @pytest.fixture(scope="function") 127 | def create_archived_model_version(client, create_model_version) -> ModelVersion: 128 | version = create_model_version 129 | 130 | new_version = client.archive_model_version(version.name, version.version) 131 | 132 | return new_version 133 | -------------------------------------------------------------------------------- /tests/test_unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobileTeleSystems/mlflow-rest-client/7f6d22b82f6213936ac7389817d7e3354cfb037e/tests/test_unit/__init__.py -------------------------------------------------------------------------------- /tests/test_unit/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import random 5 | import string 6 | from datetime import datetime 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | DEFAULT_TIMEOUT = 60 11 | 12 | 13 | def rand_str(length: int = 8) -> str: 14 | letters = string.ascii_lowercase 15 | return "".join(random.sample(letters, length)) 16 | 17 | 18 | def rand_int(a: int = 0, b: int = 100) -> int: 19 | return random.randint(a, b) 20 | 21 | 22 | def rand_float(a: float = 0, b: int = 100) -> float: 23 | return random.uniform(a, b) 24 | 25 | 26 | def now() -> datetime: 27 | return datetime.now().replace(microsecond=0) 28 | -------------------------------------------------------------------------------- /tests/test_unit/test_model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from pydantic import parse_obj_as 8 | 9 | from mlflow_rest_client.model import ( 10 | ListableModel, 11 | ListableModelVersion, 12 | Model, 13 | ModelTag, 14 | ModelVersion, 15 | ModelVersionStage, 16 | ModelVersionState, 17 | ModelVersionStatus, 18 | ModelVersionTag, 19 | ) 20 | 21 | from .conftest import DEFAULT_TIMEOUT, now, rand_int, rand_str 22 | 23 | log = logging.getLogger(__name__) 24 | 25 | 26 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 27 | def test_model_version(): 28 | name = rand_str() 29 | version = rand_int() 30 | 31 | model_version = ModelVersion(name=name, version=version) 32 | 33 | assert model_version.name == name 34 | assert model_version.version == version 35 | 36 | 37 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 38 | def test_model_version_with_creation_timestamp(): 39 | name = rand_str() 40 | version = rand_int() 41 | created_time = now() 42 | 43 | model_version = ModelVersion(name=name, version=version, creation_timestamp=created_time) 44 | 45 | assert model_version.created_time == created_time 46 | 47 | 48 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 49 | def test_model_version_without_creation_timestamp(): 50 | name = rand_str() 51 | version = rand_int() 52 | 53 | model_version = ModelVersion(name=name, version=version) 54 | 55 | assert model_version.created_time is None 56 | 57 | 58 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 59 | def test_model_version_with_last_updated_timestamp(): 60 | name = rand_str() 61 | version = rand_int() 62 | updated_time = now() 63 | 64 | model_version = ModelVersion(name=name, version=version, last_updated_timestamp=updated_time) 65 | 66 | assert model_version.updated_time == updated_time 67 | 68 | 69 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 70 | def test_model_version_without_last_updated_timestamp(): 71 | name = rand_str() 72 | version = rand_int() 73 | 74 | model_version = ModelVersion(name=name, version=version) 75 | 76 | assert model_version.updated_time is None 77 | 78 | 79 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 80 | @pytest.mark.parametrize( 81 | "stage", [ModelVersionStage.UNKNOWN, ModelVersionStage.PROD, ModelVersionStage.TEST, ModelVersionStage.ARCHIVED] 82 | ) 83 | def test_model_version_with_stage(stage): 84 | name = rand_str() 85 | version = rand_int() 86 | 87 | model_version = ModelVersion(name=name, version=version, stage=stage) 88 | 89 | assert model_version.stage == stage 90 | 91 | 92 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 93 | def test_model_version_without_stage(): 94 | name = rand_str() 95 | version = rand_int() 96 | 97 | model_version = ModelVersion(name=name, version=version) 98 | 99 | assert model_version.stage == ModelVersionStage.UNKNOWN 100 | 101 | 102 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 103 | def test_model_version_with_description(): 104 | name = rand_str() 105 | version = rand_int() 106 | 107 | description = rand_str() 108 | 109 | model_version = ModelVersion(name=name, version=version, description=description) 110 | 111 | assert model_version.description == description 112 | 113 | 114 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 115 | def test_model_version_without_description(): 116 | name = rand_str() 117 | version = rand_int() 118 | 119 | model_version = ModelVersion(name=name, version=version) 120 | 121 | assert model_version.description == "" 122 | 123 | 124 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 125 | def test_model_version_with_source(): 126 | name = rand_str() 127 | version = rand_int() 128 | 129 | source = rand_str() 130 | 131 | model_version = ModelVersion(name=name, version=version, source=source) 132 | 133 | assert model_version.source == source 134 | 135 | 136 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 137 | def test_model_version_without_source(): 138 | name = rand_str() 139 | version = rand_int() 140 | 141 | model_version = ModelVersion(name=name, version=version) 142 | 143 | assert model_version.source == "" 144 | 145 | 146 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 147 | def test_model_version_with_run_id(): 148 | name = rand_str() 149 | version = rand_int() 150 | 151 | run_id = uuid4() 152 | 153 | model_version = ModelVersion(name=name, version=version, run_id=run_id) 154 | 155 | assert model_version.run_id == run_id 156 | 157 | 158 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 159 | def test_model_version_without_run_id(): 160 | name = rand_str() 161 | version = rand_int() 162 | 163 | model_version = ModelVersion(name=name, version=version) 164 | 165 | assert model_version.run_id is None 166 | 167 | 168 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 169 | @pytest.mark.parametrize("state", [ModelVersionState.READY, ModelVersionState.PENDING, ModelVersionState.FAILED]) 170 | def test_model_version_with_state(state): 171 | name = rand_str() 172 | version = rand_int() 173 | 174 | model_version = ModelVersion(name=name, version=version, state={"state": state.value}) 175 | 176 | assert model_version.status == ModelVersionStatus(state=state) 177 | 178 | 179 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 180 | @pytest.mark.parametrize("state", [ModelVersionState.READY, ModelVersionState.PENDING, ModelVersionState.FAILED]) 181 | def test_model_version_with_state_message(state): 182 | name = rand_str() 183 | version = rand_int() 184 | state_message = rand_str() 185 | 186 | model_version = ModelVersion(name=name, version=version, state={"state": state}, state_message=state_message) 187 | 188 | assert model_version.status == ModelVersionStatus(state=state, message=state_message) 189 | 190 | 191 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 192 | def test_model_version_without_state(): 193 | name = rand_str() 194 | version = rand_int() 195 | 196 | model_version = ModelVersion(name=name, version=version) 197 | 198 | assert model_version.status == ModelVersionStatus(state=ModelVersionState.PENDING) 199 | 200 | 201 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 202 | def test_model_version_with_tags(): 203 | name = rand_str() 204 | version = rand_int() 205 | 206 | key = rand_str() 207 | value = rand_str() 208 | tags = {key: value} 209 | 210 | model_version = ModelVersion(name=name, version=version, tags=[tags]) 211 | 212 | assert model_version.tags 213 | assert key in model_version.tags 214 | assert model_version.tags[key] == ModelVersionTag(key=key, value=value) 215 | 216 | 217 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 218 | def test_model_version_without_tags(): 219 | name = rand_str() 220 | version = rand_int() 221 | 222 | model_version = ModelVersion(name=name, version=version) 223 | 224 | assert not model_version.tags 225 | 226 | 227 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 228 | def test_model_version_make_tuple(): 229 | name = rand_str() 230 | version = rand_int() 231 | 232 | model_version = ModelVersion(name=name, version=version) 233 | 234 | assert model_version.name == name 235 | assert model_version.version == version 236 | 237 | 238 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 239 | def test_model_version_make_dict(): 240 | dct = { 241 | "name": rand_str(), 242 | "version": rand_int(), 243 | } 244 | 245 | model_version = parse_obj_as(ModelVersion, dct) 246 | 247 | assert model_version.name == dct["name"] 248 | 249 | 250 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 251 | def test_model_version_make_dict_with_creation_timestamp(): 252 | dct = { 253 | "name": rand_str(), 254 | "version": rand_int(), 255 | "creation_timestamp": now(), 256 | } 257 | 258 | model_version = parse_obj_as(ModelVersion, dct) 259 | 260 | assert model_version.created_time == dct["creation_timestamp"] 261 | 262 | 263 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 264 | def test_model_version_make_dict_with_last_updated_timestamp(): 265 | dct = { 266 | "name": rand_str(), 267 | "version": rand_int(), 268 | "last_updated_timestamp": now(), 269 | } 270 | 271 | model_version = parse_obj_as(ModelVersion, dct) 272 | 273 | assert model_version.updated_time == dct["last_updated_timestamp"] 274 | 275 | 276 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 277 | @pytest.mark.parametrize( 278 | "stage", [ModelVersionStage.UNKNOWN, ModelVersionStage.PROD, ModelVersionStage.TEST, ModelVersionStage.ARCHIVED] 279 | ) 280 | def test_model_version_make_dict_with_stage(stage): 281 | dct = { 282 | "name": rand_str(), 283 | "version": rand_int(), 284 | "current_stage": stage.value, 285 | } 286 | 287 | model_version = parse_obj_as(ModelVersion, dct) 288 | 289 | assert model_version.stage == stage 290 | 291 | dct = { 292 | "name": rand_str(), 293 | "version": rand_int(), 294 | "stage": stage.value, 295 | } 296 | 297 | model_version = parse_obj_as(ModelVersion, dct) 298 | 299 | assert model_version.stage == stage 300 | 301 | 302 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 303 | def test_model_version_make_dict_with_description(): 304 | dct = { 305 | "name": rand_str(), 306 | "version": rand_int(), 307 | "description": rand_str(), 308 | } 309 | 310 | model_version = parse_obj_as(ModelVersion, dct) 311 | 312 | assert model_version.description == dct["description"] 313 | 314 | 315 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 316 | def test_model_version_make_dict_with_source(): 317 | dct = { 318 | "name": rand_str(), 319 | "version": rand_int(), 320 | "source": rand_str(), 321 | } 322 | 323 | model_version = parse_obj_as(ModelVersion, dct) 324 | 325 | assert model_version.source == dct["source"] 326 | 327 | 328 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 329 | def test_model_version_make_dict_with_run_id(): 330 | dct = { 331 | "name": rand_str(), 332 | "version": rand_int(), 333 | "run_id": uuid4(), 334 | } 335 | 336 | model_version = parse_obj_as(ModelVersion, dct) 337 | 338 | assert model_version.run_id == dct["run_id"] 339 | 340 | 341 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 342 | @pytest.mark.parametrize("state", [ModelVersionState.READY, ModelVersionState.PENDING, ModelVersionState.FAILED]) 343 | def test_model_version_make_dict_with_state_message(state): 344 | dct = {"name": rand_str(), "version": rand_int(), "status": {"state": state}, "status_message": rand_str()} 345 | 346 | model_version = parse_obj_as(ModelVersion, dct) 347 | 348 | assert model_version.status == ModelVersionStatus(state=state, message=dct["status_message"]) 349 | 350 | dct = {"name": rand_str(), "version": rand_int(), "state": {"state": state}, "state_message": rand_str()} 351 | 352 | model_version = parse_obj_as(ModelVersion, dct) 353 | 354 | assert model_version.status == ModelVersionStatus(state=state, message=dct["state_message"]) 355 | 356 | 357 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 358 | def test_model_version_make_dict_with_tags(): 359 | key = rand_str() 360 | value = rand_str() 361 | tags = {key: value} 362 | 363 | dct = { 364 | "id": rand_int(), 365 | "version": rand_int(), 366 | "name": rand_str(), 367 | "tags": tags, 368 | } 369 | 370 | model_version = ModelVersion.parse_obj(dct) 371 | 372 | assert model_version.tags 373 | assert key in model_version.tags 374 | assert model_version.tags[key] == ModelVersionTag(key=key, value=value) 375 | 376 | 377 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 378 | def test_model_version_str(): 379 | name = rand_str() 380 | version = rand_int() 381 | 382 | model_version = ModelVersion(name=name, version=version) 383 | 384 | assert str(model_version) 385 | assert str(model_version) == f"{name} v{version}" 386 | 387 | 388 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 389 | def test_model_version_eq(): 390 | name1 = rand_str() 391 | name2 = rand_str() 392 | 393 | version1 = rand_int() 394 | version2 = rand_int() 395 | 396 | assert ModelVersion(name=name1, version=version1) == ModelVersion(name=name1, version=version1) 397 | assert ModelVersion(name=name1, version=version1) != ModelVersion(name=name1, version=version2) 398 | assert ModelVersion(name=name1, version=version1) != ModelVersion(name=name2, version=version1) 399 | assert ModelVersion(name=name1, version=version1) != ModelVersion(name=name2, version=version2) 400 | 401 | 402 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 403 | def test_model_version_in(): 404 | name1 = rand_str() 405 | name2 = rand_str() 406 | 407 | version1 = rand_int() 408 | version2 = rand_int() 409 | 410 | model1 = parse_obj_as(ModelVersion, {"name": name1, "version": version1}) 411 | model2 = parse_obj_as(ModelVersion, {"name": name2, "version": version2}) 412 | 413 | lst = parse_obj_as(ListableModelVersion, [model1]) 414 | 415 | assert model1 in lst 416 | assert model2 not in lst 417 | 418 | 419 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 420 | @pytest.mark.parametrize( 421 | "stage", [ModelVersionStage.UNKNOWN, ModelVersionStage.PROD, ModelVersionStage.TEST, ModelVersionStage.ARCHIVED] 422 | ) 423 | @pytest.mark.parametrize( 424 | "other_stage", 425 | [ModelVersionStage.UNKNOWN, ModelVersionStage.PROD, ModelVersionStage.TEST, ModelVersionStage.ARCHIVED], 426 | ) 427 | def test_model_version_in_by_stage(stage, other_stage): 428 | name1 = rand_str() 429 | 430 | version1 = rand_int() 431 | 432 | model1 = ModelVersion(name=name1, version=version1, stage=stage) 433 | 434 | lst = parse_obj_as(ListableModelVersion, [model1]) 435 | 436 | assert stage in lst 437 | 438 | if other_stage != stage: 439 | assert other_stage not in lst 440 | 441 | 442 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 443 | def test_model(): 444 | name = rand_str() 445 | 446 | model = Model(name=name) 447 | 448 | assert model.name == name 449 | 450 | 451 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 452 | def test_model_with_creation_timestamp(): 453 | name = rand_str() 454 | created_time = now() 455 | 456 | model = Model(name=name, creation_timestamp=created_time) 457 | 458 | assert model.created_time == created_time 459 | 460 | 461 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 462 | def test_model_without_creation_timestamp(): 463 | name = rand_str() 464 | 465 | model = Model(name=name) 466 | 467 | assert model.created_time is None 468 | 469 | 470 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 471 | def test_model_with_last_updated_timestamp(): 472 | name = rand_str() 473 | updated_time = now() 474 | 475 | model = Model(name=name, last_updated_timestamp=updated_time) 476 | 477 | assert model.updated_time == updated_time 478 | 479 | 480 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 481 | def test_model_without_last_updated_timestamp(): 482 | name = rand_str() 483 | 484 | model = Model(name=name) 485 | 486 | assert model.updated_time is None 487 | 488 | 489 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 490 | def test_model_with_description(): 491 | name = rand_str() 492 | description = rand_str() 493 | 494 | model = Model(name=name, description=description) 495 | 496 | assert model.description == description 497 | 498 | 499 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 500 | def test_model_without_description(): 501 | name = rand_str() 502 | 503 | model = Model(name=name) 504 | 505 | assert model.description == "" 506 | 507 | 508 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 509 | def test_model_with_versions(): 510 | name = rand_str() 511 | 512 | version = ModelVersion(name=name, version=rand_int(), stage=ModelVersionStage.PROD) 513 | 514 | model = Model(name=name, versions=[version]) 515 | 516 | assert model.versions 517 | assert ModelVersionStage.PROD in model.versions 518 | assert model.versions[ModelVersionStage.PROD] == version 519 | 520 | 521 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 522 | def test_model_with_tags(): 523 | name = rand_str() 524 | 525 | key = rand_str() 526 | value = rand_str() 527 | tags = {key: value} 528 | 529 | model = Model(name=name, tags=[tags]) 530 | 531 | assert model.tags 532 | assert key in model.tags 533 | assert model.tags[key] == parse_obj_as(ModelTag, {"key": key, "value": value}) 534 | 535 | 536 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 537 | def test_model_with_more_tags(): 538 | name = rand_str() 539 | 540 | key = rand_str() 541 | key1 = rand_str() 542 | key2 = rand_str() 543 | value = rand_str() 544 | value1 = rand_str() 545 | value2 = rand_str() 546 | tags = [{key: value}, {key1: value1}, {key2: value2}] 547 | 548 | model = Model(name=name, tags=tags) 549 | 550 | assert model.tags 551 | assert key in model.tags 552 | assert model.tags[key] == ModelTag(key=key, value=value) 553 | assert model.tags[key1] == ModelTag(key=key1, value=value1) 554 | assert model.tags[key2] == ModelTag(key=key2, value=value2) 555 | 556 | 557 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 558 | def test_model_without_tags(): 559 | name = rand_str() 560 | 561 | model = Model(name=name) 562 | 563 | assert not model.tags 564 | 565 | 566 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 567 | def test_model_without_versions(): 568 | name = rand_str() 569 | 570 | model = Model(name=name) 571 | 572 | assert not model.versions 573 | 574 | 575 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 576 | def test_model_make_str(): 577 | name = rand_str() 578 | 579 | model = Model(name=name) 580 | 581 | assert model.name == name 582 | 583 | 584 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 585 | def test_model_make_dict(): 586 | dct = {"name": rand_str()} 587 | 588 | model = parse_obj_as(Model, dct) 589 | 590 | assert model.name == dct["name"] 591 | 592 | 593 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 594 | def test_model_make_dict_with_creation_timestamp(): 595 | dct = { 596 | "name": rand_str(), 597 | "creation_timestamp": now(), 598 | } 599 | 600 | model = parse_obj_as(Model, dct) 601 | 602 | assert model.created_time == dct["creation_timestamp"] 603 | 604 | 605 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 606 | def test_model_make_dict_with_last_updated_timestamp(): 607 | dct = { 608 | "name": rand_str(), 609 | "last_updated_timestamp": now(), 610 | } 611 | 612 | model = parse_obj_as(Model, dct) 613 | 614 | assert model.updated_time == dct["last_updated_timestamp"] 615 | 616 | 617 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 618 | def test_model_make_dict_with_description(): 619 | dct = { 620 | "name": rand_str(), 621 | "description": rand_str(), 622 | } 623 | 624 | model = parse_obj_as(Model, dct) 625 | 626 | assert model.description == dct["description"] 627 | 628 | 629 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 630 | def test_model_make_dict_with_versions(): 631 | name = rand_str() 632 | version = rand_int() 633 | dct = {"name": name, "latest_versions": [{"name": name, "version": version}]} 634 | 635 | model = parse_obj_as(Model, dct) 636 | 637 | assert model.versions 638 | assert ModelVersionStage.UNKNOWN in model.versions 639 | assert model.versions[ModelVersionStage.UNKNOWN] 640 | assert model.versions[ModelVersionStage.UNKNOWN].version == version 641 | 642 | 643 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 644 | def test_model_make_dict_with_tags(): 645 | key = rand_str() 646 | value = rand_str() 647 | 648 | dct = {"id": rand_int(), "name": rand_str(), "tags": [{key: value}]} 649 | 650 | model = parse_obj_as(Model, dct) 651 | 652 | assert model.tags 653 | assert key in model.tags 654 | assert model.tags[key] == ModelTag(key=key, value=value) 655 | 656 | 657 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 658 | def test_model_str(): 659 | name = rand_str() 660 | 661 | model = Model(name=name) 662 | 663 | assert str(model) 664 | assert str(model) == name 665 | 666 | 667 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 668 | def test_model_eq(): 669 | name1 = rand_str() 670 | name2 = rand_str() 671 | 672 | assert Model(name=name1) == Model(name=name1) 673 | assert Model(name=name1) != Model(name=name2) 674 | 675 | 676 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 677 | def test_model_eq_name(): 678 | name1 = rand_str() 679 | name2 = rand_str() 680 | 681 | assert Model(name=name1).name == name1 682 | assert Model(name=name1).name != name2 683 | 684 | 685 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 686 | def test_model_in(): 687 | name1 = rand_str() 688 | name2 = rand_str() 689 | 690 | model1 = Model(name=name1) 691 | model2 = Model(name=name2) 692 | 693 | lst = parse_obj_as(ListableModel, [model1]) 694 | 695 | assert model1 in lst 696 | assert model2 not in lst 697 | 698 | 699 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 700 | def test_model_in_by_name(): 701 | name1 = rand_str() 702 | name2 = rand_str() 703 | 704 | model1 = Model(name=name1) 705 | 706 | lst = parse_obj_as(ListableModel, [model1]) 707 | 708 | assert name1 in lst 709 | assert name2 not in lst 710 | 711 | 712 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 713 | def test_model_get_item_by_name(): 714 | name1 = rand_str() 715 | name2 = rand_str() 716 | 717 | model1 = Model(name=name1) 718 | 719 | lst = parse_obj_as(ListableModel, [model1]) 720 | 721 | assert lst[name1] == model1 722 | 723 | with pytest.raises(KeyError): 724 | lst[name2] 725 | 726 | 727 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 728 | @pytest.mark.parametrize( 729 | "stage", [ModelVersionStage.UNKNOWN, ModelVersionStage.PROD, ModelVersionStage.TEST, ModelVersionStage.ARCHIVED] 730 | ) 731 | @pytest.mark.parametrize( 732 | "other_stage", 733 | [ModelVersionStage.UNKNOWN, ModelVersionStage.PROD, ModelVersionStage.TEST, ModelVersionStage.ARCHIVED], 734 | ) 735 | def test_model_version_get_item_by_stage(stage, other_stage): 736 | name1 = rand_str() 737 | 738 | version1 = rand_int() 739 | 740 | model1 = ModelVersion(name=name1, version=version1, stage=stage) 741 | 742 | lst = parse_obj_as(ListableModelVersion, [model1]) 743 | 744 | assert lst[stage].version == version1 745 | assert lst[stage].stage == stage 746 | 747 | if other_stage != stage: 748 | assert other_stage not in lst 749 | 750 | with pytest.raises(KeyError): 751 | lst[other_stage] 752 | -------------------------------------------------------------------------------- /tests/test_unit/test_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | import pytest 6 | 7 | from mlflow_rest_client.page import Page 8 | 9 | from .conftest import DEFAULT_TIMEOUT, rand_str 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 15 | def test_page(): 16 | items = [rand_str()] 17 | 18 | page = Page(items) 19 | 20 | assert page.items == items 21 | 22 | 23 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 24 | def test_page_with_next_page_token(): 25 | items = [rand_str()] 26 | next_page_token = rand_str() 27 | 28 | page = Page(items, next_page_token=next_page_token) 29 | 30 | assert page.next_page_token == next_page_token 31 | assert page.has_next_page 32 | 33 | 34 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 35 | def test_page_without_next_page_token(): 36 | items = [rand_str()] 37 | 38 | page = Page(items) 39 | 40 | assert page.next_page_token is None 41 | assert not page.has_next_page 42 | 43 | 44 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 45 | def test_page_make_dict(): 46 | dct = {"items": [rand_str()]} 47 | 48 | page = Page.make(dct) 49 | 50 | assert page.items == dct["items"] 51 | 52 | 53 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 54 | def test_page_make_dict_with_next_page_token(): 55 | dct = {"items": [rand_str()], "next_page_token": rand_str()} 56 | 57 | page = Page.make(dct) 58 | 59 | assert page.items == dct["items"] 60 | assert page.next_page_token == dct["next_page_token"] 61 | 62 | 63 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 64 | def test_page_make_dict_with_items_key(): 65 | dct = {"runs": [rand_str()]} 66 | 67 | page = Page.make(dct, items_key="runs") 68 | 69 | assert page.items == dct["runs"] 70 | 71 | 72 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 73 | def test_page_eq(): 74 | items1 = [rand_str()] 75 | items2 = [rand_str(), rand_str()] 76 | 77 | assert Page(items1) == Page(items1) 78 | assert Page(items1) != Page(items2) 79 | 80 | next_page_token1 = rand_str() 81 | next_page_token2 = rand_str() 82 | 83 | assert Page(items1, next_page_token=next_page_token1) == Page(items1, next_page_token=next_page_token1) 84 | assert Page(items1, next_page_token=next_page_token1) != Page(items1) 85 | 86 | assert Page(items1, next_page_token=next_page_token1) != Page(items2, next_page_token=next_page_token1) 87 | assert Page(items1, next_page_token=next_page_token1) != Page(items2) 88 | 89 | 90 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 91 | def test_page_eq_list(): 92 | items1 = [rand_str()] 93 | items2 = [rand_str()] 94 | 95 | assert Page(items1) == items1 96 | assert Page(items1) != items2 97 | 98 | 99 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 100 | def test_page_in(): 101 | item1 = rand_str() 102 | item2 = rand_str() 103 | 104 | items = [item1] 105 | 106 | page = Page(items) 107 | 108 | assert item1 in page 109 | assert item2 not in page 110 | 111 | 112 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 113 | def test_page_get_item(): 114 | item1 = rand_str() 115 | item2 = rand_str() 116 | 117 | items = [item1] 118 | 119 | page = Page(items) 120 | 121 | assert page[0] == item1 122 | assert page[0] != item2 123 | 124 | 125 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 126 | def test_page_add(): 127 | item1 = rand_str() 128 | item2 = rand_str() 129 | 130 | items = [item1] 131 | 132 | page = Page(items) 133 | assert item2 not in page 134 | 135 | page += item2 136 | assert item2 in page 137 | 138 | 139 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 140 | def test_page_del(): 141 | item1 = rand_str() 142 | item2 = rand_str() 143 | 144 | items = [item1, item2] 145 | 146 | page = Page(items) 147 | assert item2 in page 148 | 149 | del page[1] 150 | assert item2 not in page 151 | 152 | 153 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 154 | def test_page_len(): 155 | item1 = rand_str() 156 | item2 = rand_str() 157 | 158 | assert len(Page([])) == 0 159 | assert len(Page([item1])) == 1 160 | assert len(Page([item1, item2])) == 2 161 | 162 | 163 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 164 | def test_page_iterator(): 165 | item1 = rand_str() 166 | item2 = rand_str() 167 | 168 | page = Page([item1, item2]) 169 | 170 | found_item1 = False 171 | found_item2 = False 172 | for item in page: 173 | if item == item1: 174 | found_item1 = True 175 | if item == item2: 176 | found_item2 = True 177 | 178 | assert found_item1 179 | assert found_item2 180 | -------------------------------------------------------------------------------- /tests/test_unit/test_run.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import List 5 | from uuid import uuid4 6 | 7 | import pytest 8 | from pydantic import parse_obj_as 9 | 10 | from mlflow_rest_client.internal import ListableTag 11 | from mlflow_rest_client.run import ( 12 | ListableMetric, 13 | ListableParam, 14 | ListableRunInfo, 15 | Metric, 16 | Param, 17 | Run, 18 | RunData, 19 | RunInfo, 20 | RunStage, 21 | RunStatus, 22 | RunTag, 23 | ) 24 | 25 | from .conftest import DEFAULT_TIMEOUT, now, rand_float, rand_int, rand_str 26 | 27 | log = logging.getLogger(__name__) 28 | 29 | 30 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 31 | def test_run_info(): 32 | id = uuid4() 33 | 34 | run_info = RunInfo(id=id) 35 | 36 | assert run_info.id == id 37 | 38 | 39 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 40 | def test_run_info_with_experiment_id(): 41 | id = uuid4() 42 | experiment_id = rand_int() 43 | 44 | run_info = RunInfo(id=id, experiment_id=experiment_id) 45 | 46 | assert run_info.experiment_id == experiment_id 47 | 48 | 49 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 50 | def test_run_info_without_experiment_id(): 51 | id = uuid4() 52 | 53 | run_info = RunInfo(id=id) 54 | 55 | assert run_info.experiment_id is None 56 | 57 | 58 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 59 | @pytest.mark.parametrize( 60 | "status", 61 | [ 62 | RunStatus.SCHEDULED, 63 | RunStatus.STARTED, 64 | RunStatus.FINISHED, 65 | RunStatus.FAILED, 66 | RunStatus.KILLED, 67 | ], 68 | ) 69 | def test_run_info_with_status(status): 70 | id = uuid4() 71 | 72 | run_info = RunInfo(id=id, status=status) 73 | 74 | assert run_info.status == status 75 | 76 | 77 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 78 | def test_run_info_without_status(): 79 | id = uuid4() 80 | 81 | run_info = RunInfo(id=id) 82 | 83 | assert run_info.status == RunStatus.STARTED 84 | 85 | 86 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 87 | @pytest.mark.parametrize("stage", [RunStage.ACTIVE, RunStage.DELETED]) 88 | def test_run_info_with_stage(stage): 89 | id = uuid4() 90 | 91 | run_info = RunInfo(id=id, stage=stage) 92 | 93 | assert run_info.stage == stage 94 | 95 | 96 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 97 | def test_run_info_without_stage(): 98 | id = uuid4() 99 | 100 | run_info = RunInfo(id=id) 101 | 102 | assert run_info.stage == RunStage.ACTIVE 103 | 104 | 105 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 106 | def test_run_info_with_start_time(): 107 | id = uuid4() 108 | start_time = now() 109 | 110 | run_info = RunInfo(id=id, start_time=start_time) 111 | 112 | assert run_info.start_time == start_time 113 | 114 | 115 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 116 | def test_run_info_without_start_time(): 117 | id = uuid4() 118 | 119 | run_info = RunInfo(id=id) 120 | 121 | assert run_info.start_time is None 122 | 123 | 124 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 125 | def test_run_info_with_end_time(): 126 | id = uuid4() 127 | end_time = now() 128 | 129 | run_info = RunInfo(id=id, end_time=end_time) 130 | 131 | assert run_info.end_time == end_time 132 | 133 | 134 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 135 | def test_run_info_without_end_time(): 136 | id = uuid4() 137 | 138 | run_info = RunInfo(id=id) 139 | 140 | assert run_info.end_time is None 141 | 142 | 143 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 144 | def test_run_info_with_artifact_uri(): 145 | id = uuid4() 146 | artifact_uri = rand_str() 147 | 148 | run_info = RunInfo(id=id, artifact_uri=artifact_uri) 149 | 150 | assert run_info.artifact_uri == artifact_uri 151 | 152 | 153 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 154 | def test_run_info_without_artifact_uri(): 155 | id = uuid4() 156 | 157 | run_info = RunInfo(id=id) 158 | 159 | assert run_info.artifact_uri == "" 160 | 161 | 162 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 163 | def test_run_info_make_str(): 164 | id = uuid4() 165 | 166 | run_info = RunInfo(id=id) 167 | 168 | assert run_info.id == id 169 | 170 | 171 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 172 | def test_run_info_make_dict(): 173 | dct = {"run_id": uuid4()} 174 | 175 | run_info = parse_obj_as(RunInfo, dct) 176 | 177 | assert run_info.id == dct["run_id"] 178 | 179 | dct = {"run_uuid": uuid4()} 180 | 181 | run_info = parse_obj_as(RunInfo, dct) 182 | 183 | assert run_info.id == dct["run_uuid"] 184 | 185 | dct = {"id": uuid4()} 186 | 187 | run_info = parse_obj_as(RunInfo, dct) 188 | 189 | assert run_info.id == dct["id"] 190 | 191 | 192 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 193 | def test_run_info_make_dict_with_experiment_id(): 194 | dct = {"id": uuid4(), "experiment_id": rand_int()} 195 | 196 | run_info = parse_obj_as(RunInfo, dct) 197 | 198 | assert run_info.experiment_id == dct["experiment_id"] 199 | 200 | 201 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 202 | @pytest.mark.parametrize( 203 | "status", 204 | [ 205 | RunStatus.SCHEDULED, 206 | RunStatus.STARTED, 207 | RunStatus.FINISHED, 208 | RunStatus.FAILED, 209 | RunStatus.KILLED, 210 | ], 211 | ) 212 | def test_run_info_make_dict_with_status(status): 213 | dct = {"id": uuid4(), "status": status.value} 214 | 215 | run_info = parse_obj_as(RunInfo, dct) 216 | 217 | assert run_info.status == status 218 | 219 | 220 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 221 | @pytest.mark.parametrize("stage", [RunStage.ACTIVE, RunStage.DELETED]) 222 | def test_run_info_make_dict_with_stage(stage): 223 | dct = {"id": uuid4(), "lifecycle_stage": stage.value} 224 | 225 | run_info = parse_obj_as(RunInfo, dct) 226 | 227 | assert run_info.stage == stage 228 | 229 | dct = {"id": uuid4(), "stage": stage.value} 230 | 231 | run_info = parse_obj_as(RunInfo, dct) 232 | 233 | assert run_info.stage == stage 234 | 235 | 236 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 237 | def test_run_info_make_dict_with_start_time(): 238 | dct = {"id": uuid4(), "start_time": now()} 239 | 240 | run_info = parse_obj_as(RunInfo, dct) 241 | 242 | assert run_info.start_time == dct["start_time"] 243 | 244 | 245 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 246 | def test_run_info_make_dict_with_end_time(): 247 | dct = {"id": uuid4(), "end_time": now()} 248 | 249 | run_info = parse_obj_as(RunInfo, dct) 250 | 251 | assert run_info.end_time == dct["end_time"] 252 | 253 | 254 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 255 | def test_run_info_make_dict_with_artifact_uri(): 256 | dct = {"id": uuid4(), "artifact_uri": rand_str()} 257 | 258 | run_info = parse_obj_as(RunInfo, dct) 259 | 260 | assert run_info.artifact_uri == dct["artifact_uri"] 261 | 262 | 263 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 264 | def test_run_info_eq(): 265 | id1 = uuid4() 266 | id2 = uuid4() 267 | 268 | assert RunInfo(id=id1) == RunInfo(id=id1) 269 | assert RunInfo(id=id1) != RunInfo(id=id2) 270 | 271 | experiment_id1 = rand_int() 272 | experiment_id2 = rand_int() 273 | 274 | assert RunInfo(id=id1, experiment_id=experiment_id1) == RunInfo(id=id1, experiment_id=experiment_id1) 275 | assert RunInfo(id=id1, experiment_id=experiment_id1) != RunInfo(id=id1, experiment_id=experiment_id2) 276 | 277 | assert RunInfo(id=id1, experiment_id=experiment_id1) != RunInfo(id=id2, experiment_id=experiment_id1) 278 | assert RunInfo(id=id1, experiment_id=experiment_id1) != RunInfo(id=id2, experiment_id=experiment_id2) 279 | 280 | status1 = RunStatus.SCHEDULED 281 | status2 = RunStatus.KILLED 282 | 283 | assert RunInfo(id=id1, status=status1) == RunInfo(id=id1, status=status1) 284 | assert RunInfo(id=id1, status=status1) != RunInfo(id=id1, status=status2) 285 | 286 | assert RunInfo(id=id1, status=status1) != RunInfo(id=id2, status=status1) 287 | assert RunInfo(id=id1, status=status1) != RunInfo(id=id2, status=status2) 288 | 289 | 290 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 291 | def test_run_info_in(): 292 | id1 = uuid4() 293 | id2 = uuid4() 294 | 295 | run1 = RunInfo(id=id1) 296 | run2 = RunInfo(id=id2) 297 | 298 | lst = parse_obj_as(List[RunInfo], [run1]) 299 | 300 | assert run1 in lst 301 | assert run2 not in lst 302 | 303 | 304 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 305 | def test_run_info_in_by_id(): 306 | id1 = uuid4() 307 | id2 = uuid4() 308 | 309 | run1 = RunInfo(id=id1) 310 | 311 | lst = parse_obj_as(ListableRunInfo, [run1]) 312 | 313 | assert id1 in lst 314 | assert str(id1) in lst 315 | assert id1.hex in lst 316 | 317 | assert id2 not in lst 318 | assert str(id2) not in lst 319 | assert id2.hex not in lst 320 | 321 | 322 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 323 | def test_run_info_get_item_by_id(): 324 | id1 = uuid4() 325 | id2 = uuid4() 326 | 327 | run1 = RunInfo(id=id1) 328 | 329 | lst = parse_obj_as(ListableRunInfo, [run1]) 330 | 331 | assert lst[id1] == run1 332 | assert lst[str(id1)] == run1 333 | assert lst[id1.hex] == run1 334 | 335 | with pytest.raises(KeyError): 336 | lst[id2] 337 | 338 | with pytest.raises(KeyError): 339 | lst[str(id2)] 340 | 341 | with pytest.raises(KeyError): 342 | lst[id2.hex] 343 | 344 | 345 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 346 | def test_metric(): 347 | key = rand_str() 348 | 349 | metric = Metric(key=key) 350 | 351 | assert metric.key == key 352 | 353 | 354 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 355 | def test_metric_with_value(): 356 | key = rand_str() 357 | value = rand_float() 358 | 359 | metric = Metric(key=key, value=value) 360 | 361 | assert metric.value == value 362 | 363 | 364 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 365 | def test_metric_without_value(): 366 | key = rand_str() 367 | 368 | metric = Metric(key=key) 369 | 370 | assert metric.value is None 371 | 372 | 373 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 374 | def test_metric_with_step(): 375 | key = rand_str() 376 | step = rand_int() 377 | 378 | metric = Metric(key=key, step=step) 379 | 380 | assert metric.step == step 381 | 382 | 383 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 384 | def test_metric_without_step(): 385 | key = rand_str() 386 | 387 | metric = Metric(key=key) 388 | 389 | assert metric.step == 0 390 | 391 | 392 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 393 | def test_metric_with_timestamp(): 394 | key = rand_str() 395 | timestamp = now() 396 | 397 | metric = Metric(key=key, timestamp=timestamp) 398 | 399 | assert metric.timestamp == timestamp 400 | 401 | 402 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 403 | def test_metric_without_timestamp(): 404 | key = rand_str() 405 | 406 | metric = Metric(key=key) 407 | 408 | assert metric.timestamp is None 409 | 410 | 411 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 412 | def test_metric_make_str(): 413 | key = rand_str() 414 | 415 | metric = Metric(key=key) 416 | 417 | assert metric.key == key 418 | 419 | 420 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 421 | def test_metric_make_tuple(): 422 | key = rand_str() 423 | value = rand_float() 424 | step = rand_int() 425 | timestamp = now() 426 | 427 | metric = Metric(key=key, value=value) 428 | assert metric.value == value 429 | 430 | metric = Metric(key=key, value=value, step=step) 431 | assert metric.value == value 432 | assert metric.step == step 433 | 434 | metric = Metric(key=key, value=value, step=step, timestamp=timestamp) 435 | assert metric.value == value 436 | assert metric.step == step 437 | assert metric.timestamp == timestamp 438 | 439 | 440 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 441 | def test_metric_make_dict(): 442 | dct = {"key": rand_str()} 443 | 444 | metric = parse_obj_as(Metric, dct) 445 | 446 | assert metric.key == dct["key"] 447 | 448 | 449 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 450 | def test_metric_make_dict_with_value(): 451 | dct = {"key": rand_str(), "value": rand_float()} 452 | 453 | metric = parse_obj_as(Metric, dct) 454 | 455 | assert metric.value == dct["value"] 456 | 457 | 458 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 459 | def test_metric_make_dict_with_step(): 460 | dct = {"key": rand_str(), "step": rand_int()} 461 | 462 | metric = parse_obj_as(Metric, dct) 463 | 464 | assert metric.step == dct["step"] 465 | 466 | 467 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 468 | def test_metric_make_dict_with_timestamp(): 469 | dct = {"key": rand_str(), "timestamp": now()} 470 | 471 | metric = parse_obj_as(Metric, dct) 472 | 473 | assert metric.timestamp == dct["timestamp"] 474 | 475 | 476 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 477 | def test_metric_str(): 478 | key = rand_str() 479 | value = rand_float() 480 | step = rand_int() 481 | timestamp = now() 482 | 483 | metric = Metric(key=key, value=value, step=step, timestamp=timestamp) 484 | 485 | assert str(metric) 486 | assert str(metric) == f"{key}: {value} for {step} at {timestamp}" 487 | 488 | 489 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 490 | def test_metric_eq(): 491 | key1 = rand_str() 492 | key2 = rand_str() 493 | 494 | assert Metric(key=key1) == Metric(key=key1) 495 | assert Metric(key=key1) != Metric(key=key2) 496 | 497 | value1 = rand_float() 498 | value2 = rand_float() 499 | 500 | assert Metric(key=key1, value=value1) == Metric(key=key1, value=value1) 501 | assert Metric(key=key1, value=value1) != Metric(key=key1, value=value2) 502 | 503 | assert Metric(key=key1, value=value1) != Metric(key=key2, value=value1) 504 | assert Metric(key=key1, value=value1) != Metric(key=key2, value=value2) 505 | 506 | step1 = rand_int() 507 | step2 = rand_int() 508 | 509 | assert Metric(key=key1, step=step1) == Metric(key=key1, step=step1) 510 | assert Metric(key=key1, step=step1) != Metric(key=key1, step=step2) 511 | 512 | assert Metric(key=key1, step=step1) != Metric(key=key2, step=step1) 513 | assert Metric(key=key1, step=step1) != Metric(key=key2, step=step2) 514 | 515 | timestamp1 = rand_float() 516 | timestamp2 = rand_float() 517 | 518 | assert Metric(key=key1, timestamp=timestamp1) == Metric(key=key1, timestamp=timestamp1) 519 | assert Metric(key=key1, timestamp=timestamp1) != Metric(key=key1, timestamp=timestamp2) 520 | 521 | assert Metric(key=key1, timestamp=timestamp1) != Metric(key=key2, timestamp=timestamp1) 522 | assert Metric(key=key1, timestamp=timestamp1) != Metric(key=key2, timestamp=timestamp2) 523 | 524 | 525 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 526 | def test_metric_in(): 527 | key1 = rand_str() 528 | key2 = rand_str() 529 | 530 | metric1 = Metric(key=key1) 531 | metric2 = Metric(key=key2) 532 | 533 | lst = parse_obj_as(ListableMetric, [metric1]) 534 | 535 | assert metric1 in lst 536 | assert metric2 not in lst 537 | 538 | 539 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 540 | def test_metric_in_by_key(): 541 | key1 = rand_str() 542 | key2 = rand_str() 543 | 544 | metric1 = Metric(key=key1) 545 | 546 | lst = parse_obj_as(ListableMetric, [metric1]) 547 | 548 | assert key1 in lst 549 | assert key2 not in lst 550 | 551 | 552 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 553 | def test_metric_get_item_by_key(): 554 | key1 = rand_str() 555 | key2 = rand_str() 556 | 557 | metric1 = Metric(key=key1) 558 | 559 | lst = parse_obj_as(ListableMetric, [metric1]) 560 | 561 | assert lst[key1] == metric1 562 | 563 | with pytest.raises(KeyError): 564 | lst[key2] 565 | 566 | 567 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 568 | def test_run_data_with_params(): 569 | param = Param(key=rand_str()) 570 | 571 | run_data = RunData(params=[param]) 572 | 573 | assert run_data.params == parse_obj_as(ListableParam, [param]) 574 | 575 | 576 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 577 | def test_run_data_without_params(): 578 | run_data = RunData() 579 | 580 | assert not run_data.params 581 | 582 | 583 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 584 | def test_run_data_with_metrics(): 585 | metric = Metric(key=rand_str()) 586 | 587 | run_data = RunData(metrics=[metric]) 588 | 589 | assert run_data.metrics == parse_obj_as(ListableMetric, [metric]) 590 | 591 | 592 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 593 | def test_run_data_without_metrics(): 594 | run_data = RunData() 595 | 596 | assert not run_data.metrics 597 | 598 | 599 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 600 | def test_run_data_with_tags(): 601 | key = rand_str() 602 | value = rand_str() 603 | tags = {key: value} 604 | 605 | run_data = RunData(tags=[tags]) 606 | 607 | assert run_data.tags 608 | assert key in run_data.tags 609 | assert run_data.tags[key] == RunTag(key=key, value=value) 610 | 611 | 612 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 613 | def test_run_data_without_tags(): 614 | run_data = RunData() 615 | 616 | assert not run_data.tags 617 | 618 | 619 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 620 | def test_run_data_eq(): 621 | param1 = Param(key=rand_str()) 622 | param2 = Param(key=rand_str()) 623 | 624 | assert RunData(params=[param1]) == RunData(params=[param1]) 625 | assert RunData(params=[param1]) != RunData(params=[param2]) 626 | 627 | metric1 = Metric(key=rand_str()) 628 | metric2 = Metric(key=rand_str()) 629 | 630 | assert RunData(metrics=[metric1]) == RunData(metrics=[metric1]) 631 | assert RunData(metrics=[metric1]) != RunData(metrics=[metric2]) 632 | 633 | tag1 = RunTag(key=rand_str()) 634 | tag2 = RunTag(key=rand_str()) 635 | 636 | assert RunData(tags=[tag1]) == RunData(tags=[tag1]) 637 | assert RunData(tags=[tag1]) != RunData(tags=[tag2]) 638 | 639 | 640 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 641 | def test_run_data_param_in(): 642 | param = Param(key=rand_str()) 643 | 644 | run_data = RunData(params=[param]) 645 | 646 | assert param in run_data.params 647 | 648 | 649 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 650 | def test_run_data_param_by_name(): 651 | name = rand_str() 652 | param = Param(key=name) 653 | 654 | run_data = RunData(params=[param]) 655 | 656 | assert name in run_data.params 657 | 658 | 659 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 660 | def test_run_data_param_get_item_by_name(): 661 | name = rand_str() 662 | param = Param(key=name) 663 | 664 | run_data = RunData(params=[param]) 665 | 666 | assert run_data.params[name] == param 667 | 668 | 669 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 670 | def test_run_data_metric_in(): 671 | metric = Metric(key=rand_str()) 672 | 673 | run_data = RunData(metrics=[metric]) 674 | 675 | assert metric in run_data.metrics 676 | 677 | 678 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 679 | def test_run_data_metric_by_name(): 680 | name = rand_str() 681 | metric = Metric(key=name) 682 | 683 | run_data = RunData(metrics=[metric]) 684 | 685 | assert name in run_data.metrics 686 | 687 | 688 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 689 | def test_run_data_metric_get_item_by_name(): 690 | name = rand_str() 691 | metric = Metric(key=name) 692 | 693 | run_data = RunData(metrics=[metric]) 694 | 695 | assert run_data.metrics[name] == metric 696 | 697 | 698 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 699 | def test_run_data_tag_by_name(): 700 | name = rand_str() 701 | tag = RunTag(key=name) 702 | 703 | run_data = RunData(tags=[tag]) 704 | 705 | assert name in run_data.tags 706 | 707 | 708 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 709 | def test_run(): 710 | id = uuid4() 711 | run_info = RunInfo(id=id) 712 | run_data = RunData() 713 | 714 | run = Run(info=run_info, data=run_data) 715 | 716 | assert run.info == run_info 717 | assert run.data == run_data 718 | 719 | 720 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 721 | def test_run_make_dict(): 722 | id = uuid4() 723 | run_info = RunInfo(id=id) 724 | run_data = RunData() 725 | 726 | dct = {"info": run_info, "data": run_data} 727 | 728 | run = parse_obj_as(Run, dct) 729 | 730 | assert run.info == run_info 731 | assert run.data == run_data 732 | 733 | 734 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 735 | def test_run_str(): 736 | id = uuid4() 737 | run_info = RunInfo(id=id) 738 | run = Run(info=run_info) 739 | 740 | assert str(run) 741 | assert str(run) == str(run_info) 742 | 743 | 744 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 745 | def test_run_eq(): 746 | id1 = uuid4() 747 | id2 = uuid4() 748 | 749 | run_info1 = RunInfo(id=id1) 750 | run_info2 = RunInfo(id=id2) 751 | 752 | param1 = Param(key=rand_str()) 753 | param2 = Param(key=rand_str()) 754 | 755 | metric1 = Metric(key=rand_str()) 756 | metric2 = Metric(key=rand_str()) 757 | 758 | run_tag1 = RunTag(key=rand_str()) 759 | run_tag2 = RunTag(key=rand_str()) 760 | 761 | run_data1 = RunData(params=[param1], metrics=[metric1], tags=[run_tag1]) 762 | run_data2 = RunData(params=[param2], metrics=[metric2], tags=[run_tag2]) 763 | 764 | assert Run(info=run_info1, data=run_data1) == Run(info=run_info1, data=run_data1) 765 | assert Run(info=run_info1, data=run_data1) == Run(info=run_info1, data=run_data2) 766 | assert Run(info=run_info1, data=run_data1) != Run(info=run_info2, data=run_data1) 767 | assert Run(info=run_info1, data=run_data1) != Run(info=run_info2, data=run_data2) 768 | 769 | 770 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 771 | def test_run_get_attr(): 772 | id = uuid4() 773 | run_info = RunInfo(id=id) 774 | 775 | param = Param(key=rand_str()) 776 | metric = Metric(key=rand_str()) 777 | run_tag = RunTag(key=rand_str()) 778 | 779 | run_data = RunData(params=[param], metrics=[metric], tags=[run_tag]) 780 | 781 | run = Run(info=run_info, data=run_data) 782 | 783 | assert run.id == id 784 | assert run.experiment_id is None 785 | assert run.status == RunStatus.STARTED 786 | assert run.params == parse_obj_as(ListableParam, [param]) 787 | assert run.metrics == parse_obj_as(ListableMetric, [metric]) 788 | assert run.tags == parse_obj_as(ListableTag, [run_tag]) 789 | 790 | with pytest.raises(AttributeError): 791 | run.unknown 792 | 793 | 794 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 795 | def test_run_in(): 796 | id1 = uuid4() 797 | id2 = uuid4() 798 | 799 | run_info1 = RunInfo(id=id1) 800 | run_info2 = RunInfo(id=id2) 801 | 802 | run1 = Run(info=run_info1) 803 | run2 = Run(info=run_info2) 804 | 805 | lst = parse_obj_as(List[Run], [run1]) 806 | 807 | assert run1 in lst 808 | assert run2 not in lst 809 | -------------------------------------------------------------------------------- /tests/test_unit/test_tag.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | import pytest 6 | 7 | from mlflow_rest_client.tag import Tag 8 | 9 | from .conftest import DEFAULT_TIMEOUT, rand_str 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 15 | def test_tag(): 16 | key = rand_str() 17 | value = rand_str() 18 | 19 | tag = Tag(key=key, value=value) 20 | 21 | assert tag.key == key 22 | assert tag.value == value 23 | 24 | 25 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 26 | def test_tag_without_value(): 27 | key = rand_str() 28 | 29 | tag = Tag(key=key) 30 | 31 | assert tag.key == key 32 | assert tag.value == "" 33 | 34 | 35 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 36 | def test_tag_str(): 37 | key = rand_str() 38 | value = rand_str() 39 | 40 | tag = Tag(key=key, value=value) 41 | 42 | assert str(tag) 43 | assert str(tag) == key 44 | 45 | 46 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 47 | def test_tag_eq(): 48 | key1 = rand_str() 49 | key2 = rand_str() 50 | 51 | value1 = rand_str() 52 | value2 = rand_str() 53 | 54 | assert Tag(key=key1, value=value1) == Tag(key=key1, value=value1) 55 | assert Tag(key=key1, value=value1) != Tag(key=key1, value=value2) 56 | 57 | assert Tag(key=key1, value=value1) != Tag(key=key2, value=value1) 58 | assert Tag(key=key1, value=value1) != Tag(key=key2, value=value2) 59 | 60 | 61 | @pytest.mark.timeout(DEFAULT_TIMEOUT) 62 | def test_tag_eq_str(): 63 | key1 = rand_str() 64 | key2 = rand_str() 65 | 66 | value1 = rand_str() 67 | 68 | assert Tag(key=key1, value=value1).key == key1 69 | assert Tag(key=key1, value=value1).key != key2 70 | --------------------------------------------------------------------------------