[
  {
    "path": ".cruft.json",
    "content": "{\n  \"template\": \"https://github.com/iterative/py-template\",\n  \"commit\": \"e4ec95f4cfd03d4af0a8604d462ee11d07d63b42\",\n  \"checkout\": null,\n  \"context\": {\n    \"cookiecutter\": {\n      \"project_name\": \"dvclive\",\n      \"package_name\": \"dvclive\",\n      \"friendly_name\": \"dvclive\",\n      \"author\": \"Iterative\",\n      \"email\": \"support@dvc.org\",\n      \"github_user\": \"iterative\",\n      \"version\": \"0.0.0\",\n      \"copyright_year\": \"2022\",\n      \"license\": \"Apache-2.0\",\n      \"docs\": \"False\",\n      \"short_description\": \"Metric logger for ML projects.\",\n      \"development_status\": \"Development Status :: 4 - Beta\",\n      \"_template\": \"https://github.com/iterative/py-template\"\n    }\n  },\n  \"directory\": null\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "- [ ] ❗ I have followed the [Contributing to DVCLive](https://github.com/iterative/dvclive/blob/main/CONTRIBUTING.rst) guide.\n\n- [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/iterative/dvc.org) and linked it here.\n\nThank you for the contribution - we'll try to review it as soon as possible. 🙏\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "coverage:\n  status:\n    project:\n      default:\n        # auto compares coverage to the previous base commit\n        target: auto\n        # adjust accordingly based on how flaky your tests are\n        # this allows a 10% drop from the previous base commit coverage\n        threshold: 10%\n        # non-blocking status checks\n        informational: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  - directory: \"/\"\n    package-ecosystem: \"pip\"\n    schedule:\n      interval: \"weekly\"\n    labels:\n      - \"maintenance\"\n\n  - directory: \"/\"\n    package-ecosystem: \"github-actions\"\n    schedule:\n      interval: \"weekly\"\n    labels:\n      - \"maintenance\"\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\nenv:\n  FORCE_COLOR: \"1\"\n\njobs:\n  release:\n    environment: pypi\n    permissions:\n      contents: read\n      id-token: write\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repository\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: Set up Python 3.14\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.14'\n\n      - uses: astral-sh/setup-uv@v7\n      - name: Install nox\n        run: uv pip install --system nox --upgrade\n\n      - name: Build package\n        run: nox -s build\n\n      - name: Upload package\n        if: github.event_name == 'release'\n        uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n  workflow_dispatch:\n\nenv:\n  FORCE_COLOR: \"1\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repository\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.13\"\n\n      - uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: false\n\n      - name: Install nox\n        run: uv pip install --system nox --upgrade\n\n      - uses: actions/cache@v5\n        with:\n          path: ~/.cache/pre-commit/\n          key: pre-commit-4|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}\n\n      - name: Lint code\n        run: nox -s lint\n\n  tests:\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        pyv: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n        type: [core_tests]\n        include:\n          - os: ubuntu-latest\n            pyv: \"3.13\"\n            type: tests\n        exclude:\n          - os: ubuntu-latest\n            pyv: \"3.13\"\n            type: core_tests\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Check out the repository\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n\n      - name: Set up Python ${{ matrix.pyv }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.pyv }}\n\n      - uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: false\n\n      - name: Install nox\n        run: uv pip install --system nox --upgrade\n\n      - name: Run tests\n        run: nox -s ${{ matrix.type }}-${{ matrix.pyv }} -- --cov-report=xml\n\n      - name: Build package\n        run: nox -s build\n\n      - name: Upload coverage report\n        uses: codecov/codecov-action@v5\n\n  check:\n    if: always()\n    needs: [lint, tests]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: re-actors/alls-green@release/v1\n        with:\n          jobs: ${{ toJSON(needs) }}\n"
  },
  {
    "path": ".github/workflows/update-template.yaml",
    "content": "name: Update template\n\non:\n  schedule:\n    - cron: '5 1 * * *'  # every day at 01:05\n  workflow_dispatch:\n\njobs:\n  update:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repository\n        uses: actions/checkout@v5\n\n      - name: Update template\n        uses: iterative/py-template@main\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# Editors\n.idea\n.vscode\n\n.dvc/\n.dvcignore\nsrc/dvclive/_dvclive_version.py\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "default_language_version:\n  python: python3\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-added-large-files\n      - id: check-case-conflict\n      - id: check-docstring-first\n      - id: check-executables-have-shebangs\n      - id: check-json\n      - id: check-merge-conflict\n        args: [\"--assume-in-merge\"]\n      - id: check-toml\n      - id: check-yaml\n      - id: debug-statements\n      - id: end-of-file-fixer\n      - id: mixed-line-ending\n        args: [\"--fix=lf\"]\n      - id: sort-simple-yaml\n      - id: trailing-whitespace\n  - repo: https://github.com/codespell-project/codespell\n    rev: v2.4.1\n    hooks:\n      - id: codespell\n        additional_dependencies: [\"tomli\"]\n        exclude: >\n          (?x)^(\n              .*\\.ipynb\n          )$\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: \"v0.14.3\"\n    hooks:\n      - id: ruff-check\n        args: [--fix, --exit-non-zero-on-fix]\n      - id: ruff-format\n  - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks\n    rev: v2.15.0\n    hooks:\n      - id: pretty-format-toml\n        args: [--autofix, --no-sort]\n      - id: pretty-format-yaml\n        args: [--autofix, --indent, '2', '--offset', '2', --preserve-quotes]\n"
  },
  {
    "path": "CODE_OF_CONDUCT.rst",
    "content": "Contributor Covenant Code of Conduct\n====================================\n\nOur Pledge\n----------\n\nWe as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.\n\n\nOur Standards\n-------------\n\nExamples of behavior that contributes to a positive environment for our community include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\nEnforcement Responsibilities\n----------------------------\n\nCommunity leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.\n\n\nScope\n-----\n\nThis Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.\n\n\nEnforcement\n-----------\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at support@dvc.org. All complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the reporter of any incident.\n\n\nEnforcement Guidelines\n----------------------\n\nCommunity leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:\n\n\n1. Correction\n~~~~~~~~~~~~~\n\n**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.\n\n\n2. Warning\n~~~~~~~~~~\n\n**Community Impact**: A violation through a single incident or series of actions.\n\n**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.\n\n\n3. Temporary Ban\n~~~~~~~~~~~~~~~~\n\n**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.\n\n\n4. Permanent Ban\n~~~~~~~~~~~~~~~~\n\n**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the community.\n\n\nAttribution\n-----------\n\nThis Code of Conduct is adapted from the `Contributor Covenant <homepage_>`__, version 2.0,\navailable at https://www.contributor-covenant.org/version/2/0/code_of_conduct/.\n\nCommunity Impact Guidelines were inspired by `Mozilla’s code of conduct enforcement ladder <https://github.com/mozilla/inclusion>`__.\n\n.. _homepage: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.rst",
    "content": "Contributor Guide\n=================\n\nThank you for your interest in improving this project.\nThis project is open-source under the `Apache 2.0 license`_ and\nwelcomes contributions in the form of bug reports, feature requests, and pull requests.\n\nHere is a list of important resources for contributors:\n\n- `Source Code`_\n- `Issue Tracker`_\n- `Code of Conduct`_\n\n.. _Apache 2.0 license: https://opensource.org/licenses/Apache-2.0\n.. _Source Code: https://github.com/iterative/dvclive\n.. _Issue Tracker: https://github.com/iterative/dvclive/issues\n\nHow to report a bug\n-------------------\n\nReport bugs on the `Issue Tracker`_.\n\nWhen filing an issue, make sure to answer these questions:\n\n- Which operating system and Python version are you using?\n- Which version of this project are you using?\n- What did you do?\n- What did you expect to see?\n- What did you see instead?\n\nThe best way to get your bug fixed is to provide a test case,\nand/or steps to reproduce the issue.\n\n\nHow to request a feature\n------------------------\n\nRequest features on the `Issue Tracker`_.\n\n\nHow to set up your development environment\n------------------------------------------\n\nYou need Python 3.9+.\n\n- Clone the repository:\n\n.. code:: console\n\n   $ git clone https://github.com/iterative/dvclive\n   $ cd dvclive\n\n- Set up a virtual environment:\n\n.. code:: console\n\n   $ python -m venv .venv\n   $ source .venv/bin/activate\n\nInstall in editable mode including development dependencies:\n\n.. code:: console\n\n   $ pip install -e .[tests]\n\nIf you need to test against a specific framework, you can install it separately:\n\n.. code:: console\n\n   $ pip install -e .[tests,tf]\n   $ pip install -e .[tests,optuna]\n\nHow to test the project\n-----------------------\n\nRun the full test suite:\n\n.. code:: console\n\n   $ pytest -v tests\n\nTests are located in the ``tests`` directory,\nand are written using the pytest_ testing framework.\n\n.. _pytest: https://pytest.readthedocs.io/\n\n\nHow to submit changes\n---------------------\n\nOpen a `pull request`_ to submit changes to this project.\n\nYour pull request needs to meet the following guidelines for acceptance:\n\n- The test suite must pass without errors and warnings.\n- Include unit tests.\n- If your changes add functionality, update the documentation accordingly.\n\nFeel free to submit early, though—we can always iterate on this.\n\nTo run linting and code formatting checks, you can use `pre-commit`:\n\n.. code:: console\n\n   $ pre-commit run --all-files\n\nIt is recommended to open an issue before starting work on anything.\nThis will allow a chance to talk it over with the owners and validate your approach.\n\n.. _pull request: https://github.com/iterative/dvclive/pulls\n.. github-only\n.. _Code of Conduct: CODE_OF_CONDUCT.rst\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2022 Iterative.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# DVCLive\n\n[![PyPI](https://img.shields.io/pypi/v/dvclive.svg)](https://pypi.org/project/dvclive/)\n[![Status](https://img.shields.io/pypi/status/dvclive.svg)](https://pypi.org/project/dvclive/)\n[![Python Version](https://img.shields.io/pypi/pyversions/dvclive)](https://pypi.org/project/dvclive)\n[![License](https://img.shields.io/pypi/l/dvclive)](https://opensource.org/licenses/Apache-2.0)\n\n[![Tests](https://github.com/iterative/dvclive/workflows/Tests/badge.svg?branch=main)](https://github.com/iterative/dvclive/actions?workflow=Tests)\n[![Codecov](https://codecov.io/gh/iterative/dvclive/branch/main/graph/badge.svg)](https://app.codecov.io/gh/iterative/dvclive)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)\n[![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\nDVCLive is a Python library for logging machine learning metrics and other\nmetadata in simple file formats, which is fully compatible with DVC.\n\n# [Documentation](https://dvc.org/doc/dvclive)\n\n- [Get Started](https://dvc.org/doc/start/experiments)\n- [How it Works](https://dvc.org/doc/dvclive/how-it-works)\n- [API Reference](https://dvc.org/doc/dvclive/live)\n- [Integrations](https://dvc.org/doc/dvclive/ml-frameworks)\n\n______________________________________________________________________\n\n# Quickstart\n\n| Python API Overview | PyTorch Lightning | Scikit-learn | Ultralytics YOLO v8 |\n|--------|--------|--------|--------|\n| <a href=\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-Quickstart.ipynb\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" /></a> | <a href=\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-PyTorch-Lightning.ipynb\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" /></a> | <a href=\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-scikit-learn.ipynb\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" /></a> | <a href=\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-YOLO.ipynb\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" /></a> |\n\n## Install *dvclive*\n\n```console\n$ pip install dvclive\n```\n\n## Initialize DVC Repository\n\n```console\n$ git init\n$ dvc init\n$ git commit -m \"DVC init\"\n```\n\n## Example code\n\nCopy the snippet below into `train.py` for a basic API usage example:\n\n```python\nimport time\nimport random\n\nfrom dvclive import Live\n\nparams = {\"learning_rate\": 0.002, \"optimizer\": \"Adam\", \"epochs\": 20}\n\nwith Live() as live:\n\n    # log a parameters\n    for param in params:\n        live.log_param(param, params[param])\n\n    # simulate training\n    offset = random.uniform(0.2, 0.1)\n    for epoch in range(1, params[\"epochs\"]):\n        fuzz = random.uniform(0.01, 0.1)\n        accuracy = 1 - (2 ** - epoch) - fuzz - offset\n        loss = (2 ** - epoch) + fuzz + offset\n\n        # log metrics to studio\n        live.log_metric(\"accuracy\", accuracy)\n        live.log_metric(\"loss\", loss)\n        live.next_step()\n        time.sleep(0.2)\n```\n\nSee [Integrations](https://dvc.org/doc/dvclive/ml-frameworks) for examples using\nDVCLive alongside different ML Frameworks.\n\n## Running\n\nRun this a couple of times to simulate multiple experiments:\n\n```console\n$ python train.py\n$ python train.py\n$ python train.py\n...\n```\n\n## Comparing\n\nDVCLive outputs can be rendered in different ways:\n\n### DVC CLI\n\nYou can use [dvc exp show](https://dvc.org/doc/command-reference/exp/show) and\n[dvc plots](https://dvc.org/doc/command-reference/plots) to compare and\nvisualize metrics, parameters and plots across experiments:\n\n```console\n$ dvc exp show\n```\n\n```\n─────────────────────────────────────────────────────────────────────────────────────────────────────────────\nExperiment                 Created    train.accuracy   train.loss   val.accuracy   val.loss   step   epochs\n─────────────────────────────────────────────────────────────────────────────────────────────────────────────\nworkspace                  -                  6.0109      0.23311          6.062    0.24321      6   7\nmaster                     08:50 PM                -            -              -          -      -   -\n├── 4475845 [aulic-chiv]   08:56 PM           6.0109      0.23311          6.062    0.24321      6   7\n├── 7d4cef7 [yarer-tods]   08:56 PM           4.8551      0.82012         4.5555   0.033533      4   5\n└── d503f8e [curst-chad]   08:56 PM           4.9768     0.070585         4.0773    0.46639      4   5\n─────────────────────────────────────────────────────────────────────────────────────────────────────────────\n```\n\n```console\n$ dvc plots diff $(dvc exp list --names-only) --open\n```\n\n![dvc plots diff](./docs/dvc_plots_diff.png)\n\n### DVC Extension for VS Code\n\nInside the\n[DVC Extension for VS Code](https://marketplace.visualstudio.com/items?itemName=Iterative.dvc),\nyou can compare and visualize results using the\n[Experiments](https://github.com/iterative/vscode-dvc/blob/main/extension/resources/walkthrough/experiments-table.md)\nand\n[Plots](https://github.com/iterative/vscode-dvc/blob/main/extension/resources/walkthrough/plots.md)\nviews:\n\n![VSCode Experiments](./docs/vscode_experiments.png)\n\n![VSCode Plots](./docs/vscode_plots.png)\n\nWhile experiments are running, live updates will be displayed in both views.\n\n### DVC Studio\n\nIf you push the results to [DVC Studio](https://dvc.org/doc/studio), you can\ncompare experiments against the entire repo history:\n\n![Studio Compare](./docs/studio_compare.png)\n\nYou can enable\n[Studio Live Experiments](https://dvc.org/doc/studio/user-guide/projects-and-experiments/live-metrics-and-plots)\nto see live updates while experiments are running.\n\n______________________________________________________________________\n\n# Comparison to related technologies\n\n**DVCLive** is an *ML Logger*, similar to:\n\n- [MLFlow](https://mlflow.org/)\n- [Weights & Biases](https://wandb.ai/site)\n- [Neptune](https://neptune.ai/)\n\nThe main differences with those *ML Loggers* are:\n\n- **DVCLive** does not **require** any additional services or servers to run.\n- **DVCLive** metrics, parameters, and plots are\n  [stored as plain text files](https://dvc.org/doc/dvclive/how-it-works#directory-structure)\n  that can be versioned by tools like Git or tracked as pointers to files in DVC\n  storage.\n- **DVCLive** can save experiments or runs as\n  [hidden Git commits](https://dvc.org/doc/dvclive/how-it-works#track-the-results).\n\nYou can then use different [options](#comparing) to visualize the metrics,\nparameters, and plots across experiments.\n\n______________________________________________________________________\n\n# Contributing\n\nContributions are very welcome. To learn more, see the\n[Contributor Guide](CONTRIBUTING.rst).\n\n# License\n\nDistributed under the terms of the\n[Apache 2.0 license](https://opensource.org/licenses/Apache-2.0), *dvclive* is\nfree and open source software.\n"
  },
  {
    "path": "examples/DVCLive-Evidently.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"WpfOFaqHcnAt\"\n   },\n   \"source\": [\n    \"# Install Evidently and DVC with DVCLive\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 2337,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468096427,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"BqWpagFPZ45W\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!pip uninstall -q -y sqlalchemy pyarrow ipython-sql pandas-gbq\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 33615,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468130037,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"DijzqeokW595\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"%%capture\\n\",\n    \"!pip install -q dvc==3.25.0 dvclive==3.0.1 evidently==0.4.5 pandas==1.5.3\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"ZyZ2sX8GcvMU\"\n   },\n   \"source\": [\n    \"# Load the data\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"executionInfo\": {\n     \"elapsed\": 1772,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468131788,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"ZUrB0D59XMDD\",\n    \"outputId\": \"9f6f5a3c-f856-4d56-a8fb-ec4483ec6127\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"--2023-10-16 14:55:29--  https://archive.ics.uci.edu/static/public/275/bike+sharing+dataset.zip\\n\",\n      \"Resolving archive.ics.uci.edu (archive.ics.uci.edu)... 128.195.10.252\\n\",\n      \"Connecting to archive.ics.uci.edu (archive.ics.uci.edu)|128.195.10.252|:443... connected.\\n\",\n      \"HTTP request sent, awaiting response... 200 OK\\n\",\n      \"Length: unspecified\\n\",\n      \"Saving to: ‘bike+sharing+dataset.zip’\\n\",\n      \"\\n\",\n      \"bike+sharing+datase     [   <=>              ] 273.43K   443KB/s    in 0.6s    \\n\",\n      \"\\n\",\n      \"2023-10-16 14:55:30 (443 KB/s) - ‘bike+sharing+dataset.zip’ saved [279992]\\n\",\n      \"\\n\",\n      \"Archive:  bike+sharing+dataset.zip\\n\",\n      \"  inflating: Readme.txt              \\n\",\n      \"  inflating: day.csv                 \\n\",\n      \"  inflating: hour.csv                \\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"!mkdir raw_data && \\\\\\n\",\n    \" cd raw_data && \\\\\\n\",\n    \" wget https://archive.ics.uci.edu/static/public/275/bike+sharing+dataset.zip && \\\\\\n\",\n    \" unzip bike+sharing+dataset.zip\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 357,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468132141,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"P3XXcUrQY1EQ\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas as pd\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\",\n     \"height\": 206\n    },\n    \"executionInfo\": {\n     \"elapsed\": 9,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468132141,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"MDK0xkdbYCWg\",\n    \"outputId\": \"ec8d2605-144d-45ff-b442-70ba858a44a3\"\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"\\n\",\n       \"  <div id=\\\"df-f57eeeda-2494-42a9-8d15-33081c4de93d\\\" class=\\\"colab-df-container\\\">\\n\",\n       \"    <div>\\n\",\n       \"<style scoped>\\n\",\n       \"    .dataframe tbody tr th:only-of-type {\\n\",\n       \"        vertical-align: middle;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe tbody tr th {\\n\",\n       \"        vertical-align: top;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe thead th {\\n\",\n       \"        text-align: right;\\n\",\n       \"    }\\n\",\n       \"</style>\\n\",\n       \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n       \"  <thead>\\n\",\n       \"    <tr style=\\\"text-align: right;\\\">\\n\",\n       \"      <th></th>\\n\",\n       \"      <th>instant</th>\\n\",\n       \"      <th>dteday</th>\\n\",\n       \"      <th>season</th>\\n\",\n       \"      <th>yr</th>\\n\",\n       \"      <th>mnth</th>\\n\",\n       \"      <th>holiday</th>\\n\",\n       \"      <th>weekday</th>\\n\",\n       \"      <th>workingday</th>\\n\",\n       \"      <th>weathersit</th>\\n\",\n       \"      <th>temp</th>\\n\",\n       \"      <th>atemp</th>\\n\",\n       \"      <th>hum</th>\\n\",\n       \"      <th>windspeed</th>\\n\",\n       \"      <th>casual</th>\\n\",\n       \"      <th>registered</th>\\n\",\n       \"      <th>cnt</th>\\n\",\n       \"    </tr>\\n\",\n       \"  </thead>\\n\",\n       \"  <tbody>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>0</th>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>2011-01-01</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>6</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>2</td>\\n\",\n       \"      <td>0.344167</td>\\n\",\n       \"      <td>0.363625</td>\\n\",\n       \"      <td>0.805833</td>\\n\",\n       \"      <td>0.160446</td>\\n\",\n       \"      <td>331</td>\\n\",\n       \"      <td>654</td>\\n\",\n       \"      <td>985</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>1</th>\\n\",\n       \"      <td>2</td>\\n\",\n       \"      <td>2011-01-02</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>2</td>\\n\",\n       \"      <td>0.363478</td>\\n\",\n       \"      <td>0.353739</td>\\n\",\n       \"      <td>0.696087</td>\\n\",\n       \"      <td>0.248539</td>\\n\",\n       \"      <td>131</td>\\n\",\n       \"      <td>670</td>\\n\",\n       \"      <td>801</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>2</th>\\n\",\n       \"      <td>3</td>\\n\",\n       \"      <td>2011-01-03</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0.196364</td>\\n\",\n       \"      <td>0.189405</td>\\n\",\n       \"      <td>0.437273</td>\\n\",\n       \"      <td>0.248309</td>\\n\",\n       \"      <td>120</td>\\n\",\n       \"      <td>1229</td>\\n\",\n       \"      <td>1349</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>3</th>\\n\",\n       \"      <td>4</td>\\n\",\n       \"      <td>2011-01-04</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>2</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0.200000</td>\\n\",\n       \"      <td>0.212122</td>\\n\",\n       \"      <td>0.590435</td>\\n\",\n       \"      <td>0.160296</td>\\n\",\n       \"      <td>108</td>\\n\",\n       \"      <td>1454</td>\\n\",\n       \"      <td>1562</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>4</th>\\n\",\n       \"      <td>5</td>\\n\",\n       \"      <td>2011-01-05</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"      <td>3</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>0.226957</td>\\n\",\n       \"      <td>0.229270</td>\\n\",\n       \"      <td>0.436957</td>\\n\",\n       \"      <td>0.186900</td>\\n\",\n       \"      <td>82</td>\\n\",\n       \"      <td>1518</td>\\n\",\n       \"      <td>1600</td>\\n\",\n       \"    </tr>\\n\",\n       \"  </tbody>\\n\",\n       \"</table>\\n\",\n       \"</div>\\n\",\n       \"    <div class=\\\"colab-df-buttons\\\">\\n\",\n       \"\\n\",\n       \"  <div class=\\\"colab-df-container\\\">\\n\",\n       \"    <button class=\\\"colab-df-convert\\\" onclick=\\\"convertToInteractive('df-f57eeeda-2494-42a9-8d15-33081c4de93d')\\\"\\n\",\n       \"            title=\\\"Convert this dataframe to an interactive table.\\\"\\n\",\n       \"            style=\\\"display:none;\\\">\\n\",\n       \"\\n\",\n       \"  <svg xmlns=\\\"http://www.w3.org/2000/svg\\\" height=\\\"24px\\\" viewBox=\\\"0 -960 960 960\\\">\\n\",\n       \"    <path d=\\\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\\\"/>\\n\",\n       \"  </svg>\\n\",\n       \"    </button>\\n\",\n       \"\\n\",\n       \"  <style>\\n\",\n       \"    .colab-df-container {\\n\",\n       \"      display:flex;\\n\",\n       \"      gap: 12px;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .colab-df-convert {\\n\",\n       \"      background-color: #E8F0FE;\\n\",\n       \"      border: none;\\n\",\n       \"      border-radius: 50%;\\n\",\n       \"      cursor: pointer;\\n\",\n       \"      display: none;\\n\",\n       \"      fill: #1967D2;\\n\",\n       \"      height: 32px;\\n\",\n       \"      padding: 0 0 0 0;\\n\",\n       \"      width: 32px;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .colab-df-convert:hover {\\n\",\n       \"      background-color: #E2EBFA;\\n\",\n       \"      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\\n\",\n       \"      fill: #174EA6;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .colab-df-buttons div {\\n\",\n       \"      margin-bottom: 4px;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    [theme=dark] .colab-df-convert {\\n\",\n       \"      background-color: #3B4455;\\n\",\n       \"      fill: #D2E3FC;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    [theme=dark] .colab-df-convert:hover {\\n\",\n       \"      background-color: #434B5C;\\n\",\n       \"      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n\",\n       \"      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\\n\",\n       \"      fill: #FFFFFF;\\n\",\n       \"    }\\n\",\n       \"  </style>\\n\",\n       \"\\n\",\n       \"    <script>\\n\",\n       \"      const buttonEl =\\n\",\n       \"        document.querySelector('#df-f57eeeda-2494-42a9-8d15-33081c4de93d button.colab-df-convert');\\n\",\n       \"      buttonEl.style.display =\\n\",\n       \"        google.colab.kernel.accessAllowed ? 'block' : 'none';\\n\",\n       \"\\n\",\n       \"      async function convertToInteractive(key) {\\n\",\n       \"        const element = document.querySelector('#df-f57eeeda-2494-42a9-8d15-33081c4de93d');\\n\",\n       \"        const dataTable =\\n\",\n       \"          await google.colab.kernel.invokeFunction('convertToInteractive',\\n\",\n       \"                                                    [key], {});\\n\",\n       \"        if (!dataTable) return;\\n\",\n       \"\\n\",\n       \"        const docLinkHtml = 'Like what you see? Visit the ' +\\n\",\n       \"          '<a target=\\\"_blank\\\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\\n\",\n       \"          + ' to learn more about interactive tables.';\\n\",\n       \"        element.innerHTML = '';\\n\",\n       \"        dataTable['output_type'] = 'display_data';\\n\",\n       \"        await google.colab.output.renderOutput(dataTable, element);\\n\",\n       \"        const docLink = document.createElement('div');\\n\",\n       \"        docLink.innerHTML = docLinkHtml;\\n\",\n       \"        element.appendChild(docLink);\\n\",\n       \"      }\\n\",\n       \"    </script>\\n\",\n       \"  </div>\\n\",\n       \"\\n\",\n       \"\\n\",\n       \"<div id=\\\"df-1a8f4885-5b57-4a9a-bfc7-8d551bb879a7\\\">\\n\",\n       \"  <button class=\\\"colab-df-quickchart\\\" onclick=\\\"quickchart('df-1a8f4885-5b57-4a9a-bfc7-8d551bb879a7')\\\"\\n\",\n       \"            title=\\\"Suggest charts.\\\"\\n\",\n       \"            style=\\\"display:none;\\\">\\n\",\n       \"\\n\",\n       \"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" height=\\\"24px\\\"viewBox=\\\"0 0 24 24\\\"\\n\",\n       \"     width=\\\"24px\\\">\\n\",\n       \"    <g>\\n\",\n       \"        <path d=\\\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\\\"/>\\n\",\n       \"    </g>\\n\",\n       \"</svg>\\n\",\n       \"  </button>\\n\",\n       \"\\n\",\n       \"<style>\\n\",\n       \"  .colab-df-quickchart {\\n\",\n       \"      --bg-color: #E8F0FE;\\n\",\n       \"      --fill-color: #1967D2;\\n\",\n       \"      --hover-bg-color: #E2EBFA;\\n\",\n       \"      --hover-fill-color: #174EA6;\\n\",\n       \"      --disabled-fill-color: #AAA;\\n\",\n       \"      --disabled-bg-color: #DDD;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  [theme=dark] .colab-df-quickchart {\\n\",\n       \"      --bg-color: #3B4455;\\n\",\n       \"      --fill-color: #D2E3FC;\\n\",\n       \"      --hover-bg-color: #434B5C;\\n\",\n       \"      --hover-fill-color: #FFFFFF;\\n\",\n       \"      --disabled-bg-color: #3B4455;\\n\",\n       \"      --disabled-fill-color: #666;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-quickchart {\\n\",\n       \"    background-color: var(--bg-color);\\n\",\n       \"    border: none;\\n\",\n       \"    border-radius: 50%;\\n\",\n       \"    cursor: pointer;\\n\",\n       \"    display: none;\\n\",\n       \"    fill: var(--fill-color);\\n\",\n       \"    height: 32px;\\n\",\n       \"    padding: 0;\\n\",\n       \"    width: 32px;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-quickchart:hover {\\n\",\n       \"    background-color: var(--hover-bg-color);\\n\",\n       \"    box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\\n\",\n       \"    fill: var(--button-hover-fill-color);\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-quickchart-complete:disabled,\\n\",\n       \"  .colab-df-quickchart-complete:disabled:hover {\\n\",\n       \"    background-color: var(--disabled-bg-color);\\n\",\n       \"    fill: var(--disabled-fill-color);\\n\",\n       \"    box-shadow: none;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-spinner {\\n\",\n       \"    border: 2px solid var(--fill-color);\\n\",\n       \"    border-color: transparent;\\n\",\n       \"    border-bottom-color: var(--fill-color);\\n\",\n       \"    animation:\\n\",\n       \"      spin 1s steps(1) infinite;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  @keyframes spin {\\n\",\n       \"    0% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-bottom-color: var(--fill-color);\\n\",\n       \"      border-left-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    20% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-left-color: var(--fill-color);\\n\",\n       \"      border-top-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    30% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-left-color: var(--fill-color);\\n\",\n       \"      border-top-color: var(--fill-color);\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    40% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"      border-top-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    60% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    80% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"      border-bottom-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    90% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-bottom-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"  }\\n\",\n       \"</style>\\n\",\n       \"\\n\",\n       \"  <script>\\n\",\n       \"    async function quickchart(key) {\\n\",\n       \"      const quickchartButtonEl =\\n\",\n       \"        document.querySelector('#' + key + ' button');\\n\",\n       \"      quickchartButtonEl.disabled = true;  // To prevent multiple clicks.\\n\",\n       \"      quickchartButtonEl.classList.add('colab-df-spinner');\\n\",\n       \"      try {\\n\",\n       \"        const charts = await google.colab.kernel.invokeFunction(\\n\",\n       \"            'suggestCharts', [key], {});\\n\",\n       \"      } catch (error) {\\n\",\n       \"        console.error('Error during call to suggestCharts:', error);\\n\",\n       \"      }\\n\",\n       \"      quickchartButtonEl.classList.remove('colab-df-spinner');\\n\",\n       \"      quickchartButtonEl.classList.add('colab-df-quickchart-complete');\\n\",\n       \"    }\\n\",\n       \"    (() => {\\n\",\n       \"      let quickchartButtonEl =\\n\",\n       \"        document.querySelector('#df-1a8f4885-5b57-4a9a-bfc7-8d551bb879a7 button');\\n\",\n       \"      quickchartButtonEl.style.display =\\n\",\n       \"        google.colab.kernel.accessAllowed ? 'block' : 'none';\\n\",\n       \"    })();\\n\",\n       \"  </script>\\n\",\n       \"</div>\\n\",\n       \"    </div>\\n\",\n       \"  </div>\\n\"\n      ],\n      \"text/plain\": [\n       \"   instant     dteday  season  yr  mnth  holiday  weekday  workingday  \\\\\\n\",\n       \"0        1 2011-01-01       1   0     1        0        6           0   \\n\",\n       \"1        2 2011-01-02       1   0     1        0        0           0   \\n\",\n       \"2        3 2011-01-03       1   0     1        0        1           1   \\n\",\n       \"3        4 2011-01-04       1   0     1        0        2           1   \\n\",\n       \"4        5 2011-01-05       1   0     1        0        3           1   \\n\",\n       \"\\n\",\n       \"   weathersit      temp     atemp       hum  windspeed  casual  registered  \\\\\\n\",\n       \"0           2  0.344167  0.363625  0.805833   0.160446     331         654   \\n\",\n       \"1           2  0.363478  0.353739  0.696087   0.248539     131         670   \\n\",\n       \"2           1  0.196364  0.189405  0.437273   0.248309     120        1229   \\n\",\n       \"3           1  0.200000  0.212122  0.590435   0.160296     108        1454   \\n\",\n       \"4           1  0.226957  0.229270  0.436957   0.186900      82        1518   \\n\",\n       \"\\n\",\n       \"    cnt  \\n\",\n       \"0   985  \\n\",\n       \"1   801  \\n\",\n       \"2  1349  \\n\",\n       \"3  1562  \\n\",\n       \"4  1600  \"\n      ]\n     },\n     \"execution_count\": 5,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"df = pd.read_csv(\\\"raw_data/day.csv\\\", header=0, sep=\\\",\\\", parse_dates=[\\\"dteday\\\"])\\n\",\n    \"df.head()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"4a9DrmjyhhEP\"\n   },\n   \"source\": [\n    \"# Define column mapping\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 5,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468132141,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"_bkEZuM8gELe\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from evidently.pipeline.column_mapping import ColumnMapping\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 5,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468132141,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"dLIZqkHAgEuo\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"data_columns = ColumnMapping()\\n\",\n    \"data_columns.numerical_features = [\\\"weathersit\\\", \\\"temp\\\", \\\"atemp\\\", \\\"hum\\\", \\\"windspeed\\\"]\\n\",\n    \"data_columns.categorical_features = [\\\"holiday\\\", \\\"workingday\\\"]\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"yNBKbk51hpyz\"\n   },\n   \"source\": [\n    \"# Define what to log\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 4428,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468136565,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"owblpS3Ahw0o\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from evidently.metric_preset import DataDriftPreset\\n\",\n    \"from evidently.report import Report\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 3,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468136565,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"vRF8PjiYho6z\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def eval_drift(reference, production, column_mapping):\\n\",\n    \"    data_drift_report = Report(metrics=[DataDriftPreset()])\\n\",\n    \"    data_drift_report.run(\\n\",\n    \"        reference_data=reference, current_data=production, column_mapping=column_mapping\\n\",\n    \"    )\\n\",\n    \"    report = data_drift_report.as_dict()\\n\",\n    \"\\n\",\n    \"    drifts = []\\n\",\n    \"\\n\",\n    \"    for feature in (\\n\",\n    \"        column_mapping.numerical_features + column_mapping.categorical_features\\n\",\n    \"    ):\\n\",\n    \"        drifts.append(\\n\",\n    \"            (\\n\",\n    \"                feature,\\n\",\n    \"                report[\\\"metrics\\\"][1][\\\"result\\\"][\\\"drift_by_columns\\\"][feature][\\n\",\n    \"                    \\\"drift_score\\\"\\n\",\n    \"                ],\\n\",\n    \"            )\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    return drifts\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"4Yhet51mh6Xz\"\n   },\n   \"source\": [\n    \"# Define the comparison windows\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 3,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468136565,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"nTq8xUbGh3Ux\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# set reference dates\\n\",\n    \"reference_dates = (\\\"2011-01-01 00:00:00\\\", \\\"2011-01-28 23:00:00\\\")\\n\",\n    \"\\n\",\n    \"# set experiment batches dates\\n\",\n    \"experiment_batches = [\\n\",\n    \"    (\\\"2011-01-01 00:00:00\\\", \\\"2011-01-29 23:00:00\\\"),\\n\",\n    \"    (\\\"2011-01-29 00:00:00\\\", \\\"2011-02-07 23:00:00\\\"),\\n\",\n    \"    (\\\"2011-02-07 00:00:00\\\", \\\"2011-02-14 23:00:00\\\"),\\n\",\n    \"    (\\\"2011-02-15 00:00:00\\\", \\\"2011-02-21 23:00:00\\\"),\\n\",\n    \"]\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"8lNq9OdniDss\"\n   },\n   \"source\": [\n    \"# Run and log experiments with DVCLive\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 11,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 3,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468136565,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"zUt5jrVSRIqD\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!git config --global user.email \\\"you@example.com\\\"\\n\",\n    \"!git config --global user.name \\\"Your Name\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 12,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 1231,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468137794,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"5Hx1jI9PnT3C\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from dvclive import Live\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"jTsrtISaSF7D\"\n   },\n   \"source\": [\n    \"There are two ways to use DVC, put all the drift evaluation steps in one single experiment (corresponding to a git commit), or to save each step as a separate experiment (git commit)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"RGrEbbla30jr\"\n   },\n   \"source\": [\n    \"## In one experiment\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 13,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"executionInfo\": {\n     \"elapsed\": 2844,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468140631,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"ijUf_HhRobl0\",\n    \"outputId\": \"796d7eec-17dc-40b2-a4c9-5bdcf9184c58\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"/content\\n\",\n      \"/content/experiments\\n\",\n      \"hint: Using 'master' as the name for the initial branch. This default branch name\\n\",\n      \"hint: is subject to change. To configure the initial branch name to use in all\\n\",\n      \"hint: of your new repositories, which will suppress this warning, call:\\n\",\n      \"hint: \\n\",\n      \"hint: \\tgit config --global init.defaultBranch <name>\\n\",\n      \"hint: \\n\",\n      \"hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and\\n\",\n      \"hint: 'development'. The just-created branch can be renamed via this command:\\n\",\n      \"hint: \\n\",\n      \"hint: \\tgit branch -m <name>\\n\",\n      \"Initialized empty Git repository in /content/experiments/.git/\\n\",\n      \"fatal: pathspec '.gitignore' did not match any files\\n\",\n      \"On branch master\\n\",\n      \"\\n\",\n      \"Initial commit\\n\",\n      \"\\n\",\n      \"nothing to commit (create/copy files and use \\\"git add\\\" to track)\\n\",\n      \"Initialized DVC repository.\\n\",\n      \"\\n\",\n      \"You can now commit the changes to git.\\n\",\n      \"\\n\",\n      \"+---------------------------------------------------------------------+\\n\",\n      \"|                                                                     |\\n\",\n      \"|        DVC has enabled anonymous aggregate usage analytics.         |\\n\",\n      \"|     Read the analytics documentation (and how to opt-out) here:     |\\n\",\n      \"|             <https://dvc.org/doc/user-guide/analytics>              |\\n\",\n      \"|                                                                     |\\n\",\n      \"+---------------------------------------------------------------------+\\n\",\n      \"\\n\",\n      \"What's next?\\n\",\n      \"------------\\n\",\n      \"- Check out the documentation: <https://dvc.org/doc>\\n\",\n      \"- Get help and share ideas: <https://dvc.org/chat>\\n\",\n      \"- Star us on GitHub: <https://github.com/iterative/dvc>\\n\",\n      \"[master (root-commit) 9220260] Init DVC\\n\",\n      \" 3 files changed, 6 insertions(+)\\n\",\n      \" create mode 100644 .dvc/.gitignore\\n\",\n      \" create mode 100644 .dvc/config\\n\",\n      \" create mode 100644 .dvcignore\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Setup a git repo with dvc\\n\",\n    \"\\n\",\n    \"%cd /content\\n\",\n    \"!rm -rf experiments && mkdir experiments\\n\",\n    \"%cd experiments\\n\",\n    \"\\n\",\n    \"!git init\\n\",\n    \"!git add .gitignore\\n\",\n    \"!git commit -m \\\"Init repo\\\"\\n\",\n    \"!dvc init\\n\",\n    \"!git commit -m \\\"Init DVC\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 14,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\",\n     \"height\": 1000\n    },\n    \"executionInfo\": {\n     \"elapsed\": 16055,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468156663,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"_h-jGJqPiA30\",\n    \"outputId\": \"0b949e24-8c53-4765-a8ee-64d002b3801e\"\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/markdown\": \"# DVC Report\\n\\nparams.yaml\\n\\n| begin               | end                 |\\n|---------------------|---------------------|\\n| 2011-02-15 00:00:00 | 2011-02-21 23:00:00 |\\n\\nmetrics.json\\n\\n|   weathersit |   temp |   atemp |   hum |   windspeed |   holiday |   workingday |   step |\\n|--------------|--------|---------|-------|-------------|-----------|--------------|--------|\\n|        0.231 |      0 |       0 | 0.062 |       0.012 |     0.275 |        0.593 |      3 |\\n\\n![static/holiday](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABTZ0lEQVR4nO3deVxUhf7G8Wdm2GVzY1FRU8tdUEzDFlsoSyttNc0lylLbo7rprbTlFrfNLDMx0/SnleaSdcsso6xM0wRxS80dXABNBQRlmTm/PyySREUdODPM5/16zUs5nDPzzLlzp8czc87XYhiGIQAAAHgMq9kBAAAAUL0ogAAAAB6GAggAAOBhKIAAAAAehgIIAADgYSiAAAAAHoYCCAAA4GEogAAAAB6GAggAAOBhKIAAAAAehgIIAADgYSiAAAAAHoYCCAAA4GEogAAAAB6GAgjAozz33HOyWCzav3+/U+7v8ssv1+WXX172844dO2SxWDR16tTTbnvXXXepadOmTskBAGeCAggAAOBhvMwOAAA1SZMmTXTkyBF5e3ubHQUATooCCABOZLFY5OfnZ3YMADglPgIG4JEOHTqku+66S6GhoQoJCVFCQoIKCwvLfl9aWqoXX3xRzZs3l6+vr5o2bap///vfKioqOuX9nuw7gPPnz1e7du3k5+endu3a6dNPP61w+9dff13dunVT3bp15e/vr9jYWM2ZM6fcOt27d1d0dHSF27ds2VI9evSoxB4A4MkogAA80u233678/HwlJSXp9ttv19SpU/X888+X/X7IkCEaNWqUOnXqpDfffFPdu3dXUlKS7rjjjjN+rG+++Ua33HKLLBaLkpKS1KdPHyUkJGjlypUnrPvWW2+pY8eOeuGFF/Tyyy/Ly8tLt912m7788suydQYOHKg1a9Zo3bp15bb99ddf9fvvv2vAgAFnnBGAhzEAwIOMHj3akGTcfffd5ZbfdNNNRt26dQ3DMIz09HRDkjFkyJBy6zzxxBOGJOO7774rW9a9e3eje/fuZT9v377dkGR88MEHZctiYmKMyMhI49ChQ2XLvvnmG0OS0aRJk3KPUVhYWO7n4uJio127dsaVV15ZtuzQoUOGn5+f8dRTT5Vb9+GHHzZq1aplHD58+PQ7AoBH4wggAI80bNiwcj9feuml+uOPP5SXl6cFCxZIkhITE8ut8/jjj0tSuaNxp7N3716lp6dr8ODBCgkJKVt+9dVXq02bNies7+/vX/b3gwcPKjc3V5deeqnS0tLKloeEhKh37976+OOPZRiGJMlut2vWrFnq06ePatWqVel8ADwTBRCAR2rcuHG5n2vXri3pWOnauXOnrFarWrRoUW6diIgIhYaGaufOnZV+nL/WPf/880/4XcuWLU9Y9sUXX+iiiy6Sn5+f6tSpo/r162vChAnKzc0tt96gQYOUkZGhn376SZL07bffKjs7WwMHDqx0NgCeiwIIwCPZbLYKl/91RE06dkZvdfrpp5904403ys/PT++++64WLFigRYsWqX///uVySVKPHj0UHh6uGTNmSJJmzJihiIgIxcfHV2tmAO6JAggA/9CkSRM5HA5t3ry53PLs7GwdOnRITZo0OaP7knTCfUnSpk2byv08d+5c+fn56euvv9bdd9+t66677qSFzmazqX///pozZ44OHjyo+fPnq1+/ficttgBwPAogAPxDz549JUljx44tt3zMmDGSpF69elX6viIjIxUTE6Np06aV+xh30aJF+u2338qta7PZZLFYZLfby5bt2LFD8+fPr/C+Bw4cqIMHD2ro0KE6fPgwZ/8CqDQuBA0A/xAdHa3Bgwfrvffe06FDh9S9e3etWLFC06ZNU58+fXTFFVec0f0lJSWpV69euuSSS3T33XfrwIEDGjdunNq2bavDhw+XrderVy+NGTNG1157rfr376+cnByNHz9eLVq00Jo1a064344dO6pdu3aaPXu2WrdurU6dOp3zcwfgGTgCCAAVeP/99/X888/r119/1aOPPqrvvvtOI0eO1MyZM8/4vq699lrNnj1bdrtdI0eO1Lx58/TBBx+oc+fO5da78sorNXnyZGVlZenRRx/Vxx9/rFdeeUU33XTTSe970KBBksTJHwDOiMX45zeLAQBu46233tJjjz2mHTt2nHBmMwCcDAUQANyUYRiKjo5W3bp19f3335sdB4Ab4TuAAOBmCgoK9Pnnn+v777/X2rVr9dlnn5kdCYCb4QggALiZHTt26LzzzlNoaKjuv/9+vfTSS2ZHAuBmKIAAAAAehrOAAQAAPAwFEAAAwMNwEogkh8OhPXv2KCgoqNpnfwIAAJwpwzCUn5+vBg0ayGo98+N5FEBJe/bsUVRUlNkxAAAAzkhmZqYaNWp0xttRACUFBQVJOrYTg4OD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qxYtWmjq1KlVntNdnOn+XLx48QmvUYvFoqysrOoJ7OKSkpJ04YUXKigoSGFhYerTp482bdp02u14Hz25s9mnvJee2oQJE9ShQ4eya/zFxcXpq6++OuU2rvgapQA60axZs5SYmKjRo0crLS1N0dHR6tGjh3Jycipcf+nSperXr5/uuecerVq1Sn369FGfPn20bt26ak7ums50f0rHLrq5d+/estvOnTurMbHrKygoUHR0tMaPH1+p9bdv365evXrpiiuuUHp6uh599FENGTJEX3/9dRUndQ9nuj//smnTpnKv07CwsCpK6F5++OEHPfDAA/rll1+0aNEilZSU6JprrlFBQcFJt+F99NTOZp9KvJeeSqNGjfTf//5XqampWrlypa688kr17t1b69evr3B9l32NGnCaLl26GA888EDZz3a73WjQoIGRlJRU4fq333670atXr3LLunbtagwdOrRKc7qLM92fH3zwgRESElJN6dyfJOPTTz895Tr/+te/jLZt25Zb1rdvX6NHjx5VmMw9VWZ/fv/994Yk4+DBg9WSyd3l5OQYkowffvjhpOvwPnpmKrNPeS89c7Vr1zbef//9Cn/nqq9RjgA6SXFxsVJTUxUfH1+2zGq1Kj4+XsuWLatwm2XLlpVbX5J69Ohx0vU9ydnsT0k6fPiwmjRpoqioqFP+iwyVw2u0asTExCgyMlJXX321fv75Z7PjuKzc3FxJUp06dU66Dq/RM1OZfSrxXlpZdrtdM2fOVEFBgeLi4ipcx1VfoxRAJ9m/f7/sdrvCw8PLLQ8PDz/p93uysrLOaH1Pcjb7s2XLlpoyZYo+++wzzZgxQw6HQ926ddOuXbuqI3KNdLLXaF5eno4cOWJSKvcVGRmp5ORkzZ07V3PnzlVUVJQuv/xypaWlmR3N5TgcDj366KO6+OKL1a5du5Oux/to5VV2n/Jeenpr165VYGCgfH19NWzYMH366adq06ZNheu66mvUy9RHB5woLi6u3L/AunXrptatW2vixIl68cUXTUwGHNOyZUu1bNmy7Odu3bpp69atevPNNzV9+nQTk7meBx54QOvWrdOSJUvMjlJjVHaf8l56ei1btlR6erpyc3M1Z84cDR48WD/88MNJS6Ar4gigk9SrV082m03Z2dnllmdnZysiIqLCbSIiIs5ofU9yNvvzn7y9vdWxY0dt2bKlKiJ6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)\\n\\n![static/windspeed](data:image/png;base64,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KukIip5ZtWqVaGJiIi5dulSMj48Xhw4dKtrY2IipqamiKIrioEGDxHHjxpXtf+jQIdHIyEicOXOmmJCQIEZERIjGxsbimTNnpHoKWqWyr+ekSZPEHTt2iJcvXxZjYmLEfv36iUqlUjx37pxUT0Hr5OTkiCdPnhRPnjwpAhBnzZolnjx5Urx27ZooiqI4btw4cdCgQWX7X7lyRTQzMxM//fRTMSEhQZw3b54ol8vF7du3S/UUtEplX8/Zs2eLGzduFC9duiSeOXNGHDNmjCiTycR//vlHqqegVUaMGCFaW1uLe/fuFW/dulV2y8/PL9uH76OV8zyvKd9Ln27cuHHivn37xKSkJPH06dPiuHHjREEQxJ07d4qiqDu/oyyAGvbTTz+JdevWFRUKhdiiRQvxyJEjZf/Wvn178a233iq3/59//il6eHiICoVC9PX1Fbds2VLNibVbZV7PDz/8sGxfR0dHsVu3bmJsbKwEqbXXw2VI/nt7+Dq+9dZbYvv27R+5j7+/v6hQKMT69euLv/76a7Xn1laVfT2//fZb0d3dXVQqlaKtra3YoUMHcffu3dKE10KPey0BlPud4/to5TzPa8r30qd75513RFdXV1GhUIj29vZi586dy8qfKOrO76ggiqJYfZ83EhEREZHUeA4gERERkYFhASQiIiIyMCyARERERAaGBZCIiIjIwLAAEhERERkYFkAiIiIiA8MCSERERGRgWACJiIiIDAwLIBEREZGBYQEkInpBb7/9Nnr27Cl1DCKiCmMBJCIiIjIwLIBERBW0du1aNG7cGKampqhZsyZCQkLw6aefYtmyZfjrr78gCAIEQcDevXsBACkpKejTpw9sbGxga2uLV199FVevXi17vIefHE6aNAn29vawsrLC8OHDUVRUJM0TJCKDYSR1ACIiXXDr1i30798f3333HV577TXk5OTgwIEDCA8PR3JyMrKzs/Hrr78CAGxtbVFcXIzQ0FAEBwfjwIEDMDIywjfffIOXX34Zp0+fhkKhAABERUVBqVRi7969uHr1KgYPHoyaNWti6tSpUj5dItJzLIBERBVw69YtlJSUoFevXnB1dQUANG7cGABgamqKwsJCODk5le2/YsUKqNVqLF68GIIgAAB+/fVX2NjYYO/evejSpQsAQKFQYMmSJTAzM4Ovry8mT56MTz/9FFOmTIFMxi9piKhq8N2FiKgC/Pz80LlzZzRu3Bi9e/fGokWLcPfu3Sfuf+rUKSQmJsLS0hIWFhawsLCAra0tCgoKcPny5XKPa2ZmVvZzcHAwcnNzkZKSUqXPh4gMGz8BJCKqALlcjl27duHw4cPYuXMnfvrpJ3z55Zc4evToY/fPzc1FQEAAfv/990f+zd7evqrjEhE9FQsgEVEFCYKA1q1bo3Xr1pg4cSJcXV2xYcMGKBQKqFSqcvs2a9YMq1evhoODA6ysrJ74mKdOncL9+/dhamoKADhy5AgsLCzg4uJSpc+FiAwbvwImIqqAo0ePYtq0aThx4gSSk5Oxfv16ZGRkwNvbG25ubjh9+jQuXLiAzMxMFBcXY+DAgbCzs8Orr76KAwcOICkpCXv37sXo0aNx/fr1ssctKirCkCFDEB8fj61btyIiIgIffPABz/8joirFTwCJiCrAysoK+/fvx5w5c5CdnQ1XV1d8//336Nq1KwIDA7F3714EBgYiNzcXe/bsQYcOHbB//358/vnn6NWrF3JyclC7dm107ty53CeCnTt3RsOGDdGuXTsUFhaif//++Prrr6V7okRkEARRFEWpQxARGaK3334b9+7dw8aNG6WOQkQGht8xEBERERkYFkAiIiIiA8OvgImIiIgMDD8BJCIiIjIwLIBEREREBoYFkIiIiMjAsAASERERGRgWQCIiIiIDwwJIREREZGBYAImIiIgMDAsgERERkYFhASQiIiIyMCyARERERAaGBZCIiIjIwLAAEhERERkYFkAiIiIiA8MCSERERGRg/h9LylRJ0HLjNwAAAABJRU5ErkJggg==)\\n\\n![static/temp](data:image/png;base64,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)\\n\\n![static/workingday](data:image/png;base64,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)\\n\\n![static/weathersit](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABVHklEQVR4nO3dd3RUdcLG8e9Meg8hPYROqKEIgiAgSgDRRRF3LesqYl+xor6AqICFIKuCLqysunZXXaVZEKmhCMpC6C300BISSiqpc98/kLiRAIHc5E4yz+ecOcfc3PLMdRgebvldm2EYBiIiIiLiMuxWBxARERGRmqUCKCIiIuJiVABFREREXIwKoIiIiIiLUQEUERERcTEqgCIiIiIuRgVQRERExMWoAIqIiIi4GBVAERERERejAigiIiLiYlQARURERFyMCqCIiIiIi1EBFBEREXExKoAiIiIiLkYFUESkGthsNh555BGrY5QZN24cNpvN6hgi4iRUAEVELtHKlSsZN24cJ0+etDrKJZkwYQKzZ8+2OoaIWEAFUETkEq1cuZLx48fXigL43HPPcerUqXLTVABFXJcKoIhILVVQUIDD4ajUvO7u7nh7e1dzIhGpLVQARaTW2bhxIzabjW+++aZs2tq1a7HZbFx22WXl5h04cCDdunUr+/mHH36gV69e+Pn5ERAQwPXXX8+WLVvOWv/dd99N06ZN8fb2JjIyknvuuYdjx46VzTNu3DieeeYZAJo0aYLNZsNms7Fv375y65o9ezbt2rXDy8uLtm3bMm/evLPez6FDh7jnnnuIiIgom+/9998vN09SUhI2m40vvviC5557jpiYGHx9fcnOzqa4uJjx48fTokULvL29qV+/Pj179mTBggXl8v7vNYA2m428vDw++uijsux33333Bfa8iNQV7lYHEBG5WO3atSM4OJhly5Zxww03ALB8+XLsdjsbNmwgOzubwMBAHA4HK1eu5IEHHgDgk08+YejQoQwYMIBXX32V/Px83n77bXr27Mm6deto3LgxAAsWLGDPnj0MGzaMyMhItmzZwjvvvMOWLVv4+eefsdlsDBkyhJSUFD7//HMmT55MaGgoAGFhYWU5V6xYwcyZM3n44YcJCAjgrbfe4uabbyY1NZX69esDkJ6ezhVXXFF200hYWBg//PAD9957L9nZ2TzxxBPl3vtLL72Ep6cnTz/9NIWFhXh6ejJu3DgSExO577776Nq1K9nZ2axZs4bk5GT69etX4T785JNPyuY/s3+aNWtm2v8jEXFyhohILXT99dcbXbt2Lft5yJAhxpAhQww3Nzfjhx9+MAzDMJKTkw3AmDNnjpGTk2MEBwcb999/f7n1pKWlGUFBQeWm5+fnn7W9zz//3ACMZcuWlU3729/+ZgDG3r17z5ofMDw9PY1du3aVTduwYYMBGH//+9/Lpt17771GVFSUkZmZWW752267zQgKCirLsmTJEgMwmjZtela+Dh06GNdff/0595VhGMbYsWON33/l+/n5GUOHDj3vciJSN+kUsIjUSr169SI5OZm8vDzg9NG26667jo4dO7J8+XLg9FFBm81Wdjr05MmT3H777WRmZpa93Nzc6NatG0uWLClbt4+PT9l/FxQUkJmZyRVXXAFAcnJypTMmJCSUO6rWvn17AgMD2bNnDwCGYTBjxgwGDRqEYRjlcg0YMICsrKyztjd06NBy+QCCg4PZsmULO3furHQ2EXFtOgUsIrVSr169KCkpYdWqVcTGxnL06FF69erFli1byhXANm3aEBISUlaOrrnmmgrXFxgYWPbfx48fZ/z48XzxxRccPXq03HxZWVmVztiwYcOzptWrV48TJ04AkJGRwcmTJ3nnnXd45513KlzH77ffpEmTs+Z58cUXufHGG4mLi6Ndu3Zce+213HnnnbRv377SWUXEtagAikit1KVLF7y9vVm2bBkNGzYkPDycuLg4evXqxT/+8Q8KCwtZvnw5N910E0DZ3bKffPIJkZGRZ63P3f23r8NbbrmFlStX8swzz9CxY0f8/f1xOBxce+21lb7rFsDNza3C6YZhlMv0l7/8haFDh1Y47+9L3O+P/gH07t2b3bt3M2fOHObPn897773H5MmTmT59Ovfdd1+l84qI61ABFJFaydPTk65du7J8+XIaNmxIr169gNNHBgsLC/nss89IT0+nd+/ewG83OISHh5OQkHDO9Z44cYJFixYxfvx4XnjhhbLpFZ1ereqTNcLCwggICKC0tPS8mSojJCSEYcOGMWzYMHJzc+nduzfjxo07bwHUk0FEXJeuARSRWqtXr1788ssvLFmypKwAhoaG0rp1a1599dWyeQAGDBhAYGAgEyZMoLi4+Kx1ZWRkAL8dtTtzlO6MKVOmnLWMn58fwCUPBO3m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mzF//nzDMGrPZ1QF0GR///vfjYYNGxqenp5G165djZ9//rnsd1dddZUxdOjQcvP/5z//MeLi4gxPT0+jbdu2xvfff1/DiZ3bxezPJ554omzeiIgI47rrrjOSk5MtSO28zgxD8vvXmf04dOhQ46qrrjprmY4dOxqenp5G06ZNjQ8++KDGczuri92fr776qtGsWTPD29vbCAkJMfr06WMsXrzYmvBOqKJ9CZT7zOl79OJcyj7Vd+n53XPPPUajRo0MT09PIywszOjbt29Z+TOM2vMZtRmGYdTc8UYRERERsZquARQRERFxMSqAIiIiIi5GBVBERETExagAioiIiLgYFUARERERF6MCKCIiIuJiVABFREREXIwKoIiIiIiLUQEUERERcTEqgCIiVXT33XczePBgq2OIiFSaCqCIiIiIi1EBFBGppK+//pr4+Hh8fHyoX78+CQkJPPPMM3z00UfMmTMHm82GzWYjKSkJgAMHDnDLLbcQHBxMSEgIN954I/v27Stb35kjh+PHjycsLIzAwEAeeughioqKrHmDIuIy3K0OICJSGxw5coTbb7+dSZMmcdNNN5GTk8Py5cu56667SE1NJTs7mw8++ACAkJAQiouLGTBgAN27d2f58uW4u7vz8ssvc+2117Jx40Y8PT0BWLRoEd7e3iQlJbFv3z6GDRtG/fr1eeWVV6x8uyJSx6kAiohUwpEjRygpKWHIkCE0atQIgPj4eAB8fHwoLCwkMjKybP5PP/0Uh8PBe++9h81mA+CDDz4gODiYpKQk+vfvD4Cnpyfvv/8+vr6+tG3blhdffJFnnnmGl156CbtdJ2lEpHro20VEpBI6dOhA3759iY+P509/+hPvvvsuJ06cOOf8GzZsYNeuXQQEBODv74+/vz8hISEUFBSwe/fucuv19fUt+7l79+7k5uZy4MCBan0/IuLadARQRKQS3NzcWLBgAStXrmT+/Pn8/e9/Z8yYMfzyyy8Vzp+bm0vnzp357LPPzvpdWFhYdccVETkvFUARkUqy2WxceeWVXHnllbzwwgs0atSIWbNm4enpSWlpabl5L7vsMr788kvCw8MJDAw85zo3bNjAqVOn8PHxAeDnn3/G39+f2NjYan0vIuLadApYRKQSfvnlFyZMmMCaNWtITU1l5syZZGRk0Lp1axo3bszGjRvZsWMHmZmZFBcXc8cddxAaGsqNN97I8uXL2bt3L0lJSTz22GMcPHiwbL1FRUXce++9bN26lblz5zJ27FgeeeQRXf8nItVKRwBFRCohMDCQZcuWMWXKFLKzs2nUqBGvv/46AwcOpEuXLiQlJdGlSxdyc3NZsmQJffr0YdmyZYwcOZIhQ4aQk5NDTEwMffv2LXdEsG/fvrRo0YLevXtTWFjI7bffzrhx46x7oyLiEmyGYRhWhxARcUV33303J0+eZPbs2VZHEREXo3MMIiIiIi5GBVBERETExegUsIiIiIiL0RFAERERERejAigiIiLiYlQARURERFyMCqCIiIiIi1EBFBEREXExKoAiIiIiLkYFUERERMTFqACKiIiIuBgVQBEREREXowIoIiIi4mJUAEVERERcjAqgiIiIiItRARQRERFxMSqAIiIiIi7m/wG0zqNzX7fG2AAAAABJRU5ErkJggg==)\\n\\n![static/hum](data:image/png;base64,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Fh999BEeeughFBUVYd++fXjyySeRlpaGwsJCfP311wAADw8PVFZWYujQoYiIiMC+ffugUqnw3nvv4d5778XJkyehVqsBAFFRUdBqtYiOjsbFixcxYcIENG3aFO+//77Il0tENo4FkIioDjIzM1FVVYWHH34YQUFBAIDOnTsDABwdHVFRUQFfX9/q7b/77jsYjUZ8+eWXkCQJAPD111/D3d0d0dHRuOeeewAAarUaa9asgZOTEzp27Ih33nkHr776Kt59910oFPyShogaBs8uRER1EBoaiiFDhqBz58547LHHsHr1aly7du2G2584cQKJiYlwdXWFi4sLXFxc4OHhgfLyciQlJdV4XCcnp+o/R0REoLi4GOnp6Q36eojIvnEEkIioDpRKJXbu3ImDBw/ijz/+wLJly/Dvf/8bR44cqXX74uJidOvWDWvXrv3H33l5eTV0XCKim2IBJCKqI0mS0KdPH/Tp0wdz585FUFAQtm7dCrVaDYPBUGPbrl27YuPGjfD29oabm9sNH/PEiRMoKyuDo6MjAODw4cNwcXFBYGBgg74WIrJv/AqYiKgOjhw5gg8++ADHjh1DWloatmzZgitXrqB9+/YIDg7GyZMnce7cOeTm5qKyshLjx4+Hp6cnRowYgX379iElJQXR0dGYPn06Ll26VP24er0ezzzzDM6cOYPt27dj3rx5mDp1Kuf/EVGD4gggEVEduLm5Ye/evViyZAkKCwsRFBSERYsWYdiwYejevTuio6PRvXt3FBcXY/fu3Rg4cCD27t2L119/HQ8//DCKiooQEBCAIUOG1BgRHDJkCFq3bo3+/fujoqICY8eOxVtvvSXuhRKRXZBkWZZFhyAiskdPP/008vPzsW3bNtFRiMjO8DsGIiIiIjvDAkhERERkZ/gVMBEREZGd4QggERERkZ1hASQiIiKyMyyARERERHaGBZCIiIjIzrAAEhEREdkZFkAiIiIiO8MCSERERGRnWACJiIiI7AwLIBEREZGdYQEkIiIisjMsgERERER2hgWQiIiIyM6wABIRERHZGRZAIiIiIjvz/wCUWP+3zJ+aTQAAAABJRU5ErkJggg==)\\n\\n![static/atemp](data:image/png;base64,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GZ07dzbWrFlT8XfXXHONMXDgwDPW//LLL42mTZsafn5+RsuWLY0ffvihihO7tos5nk888UTFujExMcYNN9xgbNiwwYTUrus/w5D87+s/x3HgwIHGNddc84dt2rVrZ/j5+RkNGzY0pk+fXuW5XdXFHs/XX3/daNSokREQEGBERkYa3bt3N3766Sdzwrugsx1L4IzPnM6jF+dSjqnOpec3ZMgQo379+oafn59Ro0YN49prr60of4bhPp9Ri2EYRtVdbxQRERERs+keQBEREREvowIoIiIi4mVUAEVERES8jAqgiIiIiJdRARQRERHxMiqAIiIiIl5GBVBERETEy6gAioiIiHgZFUARERERL6MCKCJymQYNGkTfvn3NjiEicsFUAEVERES8jAqgiMgF+vrrr2ndujWBgYFUr16dpKQknn76aWbOnMn333+PxWLBYrGwbNkyANLT07nrrruIiIggMjKSW265hf3791f8vP9cOXzxxRepUaMGYWFhDBs2jJKSEnPeoIh4DR+zA4iIuIOjR4/Sv39/xo0bx6233kpeXh4rV64kOTmZgwcPkpuby/Tp0wGIjIyktLSUXr16kZiYyMqVK/Hx8eEf//gHvXv3ZvPmzfj5+QGwZMkSAgICWLZsGfv372fw4MFUr16dV155xcy3KyIeTgVQROQCHD16lLKyMm677Tbq168PQOvWrQEIDAykuLiYmjVrVqz/6aef4nA4+Oijj7BYLABMnz6diIgIli1bxvXXXw+An58f06ZNIygoiJYtW/LSSy/x9NNP8/LLL2O16ksaEakcOruIiFyAtm3bcu2119K6dWvuvPNOpk6dysmTJ8+5/qZNm9izZw+hoaGEhIQQEhJCZGQkRUVF7N2794yfGxQUVPHnxMRE8vPzSU9Pr9T3IyLeTVcARUQugM1mY9GiRaxatYqFCxfywQcf8Le//Y1ff/31rOvn5+eTkJDAZ5999oe/q1GjRmXHFRE5LxVAEZELZLFY6NatG926deOFF16gfv36fPvtt/j5+WG3289Yt0OHDsyZM4fo6GjCwsLO+TM3bdrE6dOnCQwMBGDNmjWEhIQQGxtbqe9FRLybvgIWEbkAv/76K6+++irr1q3j4MGDzJ07l2PHjtG8eXPi4uLYvHkzO3fuJDs7m9LSUu69916ioqK45ZZbWLlyJWlpaSxbtozHHnuMQ4cOVfzckpIShg4dyrZt25g3bx5jxozhkUce0f1/IlKpdAVQROQChIWFsWLFCt59911yc3OpX78+b731Fn369KFjx44sW7aMjh07kp+fz9KlS+nevTsrVqzg2Wef5bbbbiMvL486depw7bXXnnFF8Nprr6VJkyZcffXVFBcX079/f/7+97+b90ZFxCtYDMMwzA4hIuKNBg0axKlTp/juu+/MjiIiXkbfMYiIiIh4GRVAERERES+jr4BFREREvIyuAIqIiIh4GRVAERERES+jAigiIiLiZVQARURERLyMCqCIiIiIl1EBFBEREfEyKoAiIiIiXkYFUERERMTLqACKiIiIeBkVQBEREREvowIoIiIi4mVUAEVERES8jAqgiIiIiJdRARQRERHxMv8fkZH8wotxHvMAAAAASUVORK5CYII=)\\n\",\n      \"text/plain\": [\n       \"<IPython.core.display.Markdown object>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"with Live(report=\\\"notebook\\\") as live:\\n\",\n    \"    for date in experiment_batches:\\n\",\n    \"        live.log_param(\\\"begin\\\", date[0])\\n\",\n    \"        live.log_param(\\\"end\\\", date[1])\\n\",\n    \"\\n\",\n    \"        metrics = eval_drift(\\n\",\n    \"            df.loc[df.dteday.between(reference_dates[0], reference_dates[1])],\\n\",\n    \"            df.loc[df.dteday.between(date[0], date[1])],\\n\",\n    \"            column_mapping=data_columns,\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        for feature in metrics:\\n\",\n    \"            live.log_metric(feature[0], round(feature[1], 3))\\n\",\n    \"\\n\",\n    \"        live.next_step()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"Pc3jDX1q-y3c\"\n   },\n   \"source\": [\n    \"To explore the results from CLI:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 15,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"executionInfo\": {\n     \"elapsed\": 1434,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468158085,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"6OAsURiL-Ge2\",\n    \"outputId\": \"0fb47be1-f524-41f1-8c74-d4663d721290\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\rReading plot's data from workspace:   0% 0/7 [00:00<?, ?files/s]\\rReading plot's data from workspace:   0% 0/7 [00:00<?, ?files/s{'info': ''}]\\r                                                                            \\rfile:///content/experiments/dvc_plots/index.html\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"!dvc plots show\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 16,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\",\n     \"height\": 802\n    },\n    \"executionInfo\": {\n     \"elapsed\": 4,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468158085,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"pwdjxeEG-I49\",\n    \"outputId\": \"e01c74f9-45b6-4715-b1bd-2dd20875a421\"\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"<!DOCTYPE html>\\n\",\n       \"<html>\\n\",\n       \"<head>\\n\",\n       \"    \\n\",\n       \"    <title>DVC Plot</title>\\n\",\n       \"    \\n\",\n       \"\\n\",\n       \"    <script src=\\\"https://cdn.jsdelivr.net/npm/vega@5.20.2\\\"></script>\\n\",\n       \"    <script src=\\\"https://cdn.jsdelivr.net/npm/vega-lite@5.2.0\\\"></script>\\n\",\n       \"    <script src=\\\"https://cdn.jsdelivr.net/npm/vega-embed@6.18.2\\\"></script>\\n\",\n       \"    \\n\",\n       \"    <style>\\n\",\n       \"        table {\\n\",\n       \"            border-spacing: 15px;\\n\",\n       \"        }\\n\",\n       \"    </style>\\n\",\n       \"</head>\\n\",\n       \"<body>\\n\",\n       \"    \\n\",\n       \"    <div id = \\\"dvclive_plots_metrics_atemp_tsv\\\">\\n\",\n       \"        <script type = \\\"text/javascript\\\">\\n\",\n       \"            var spec = {\\\"$schema\\\": \\\"https://vega.github.io/schema/vega-lite/v5.json\\\", \\\"data\\\": {\\\"values\\\": [{\\\"step\\\": \\\"0\\\", \\\"atemp\\\": \\\"1.0\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"1\\\", \\\"atemp\\\": \\\"0.107\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"2\\\", \\\"atemp\\\": \\\"0.537\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"3\\\", \\\"atemp\\\": \\\"0.0\\\", \\\"rev\\\": \\\"workspace\\\"}]}, \\\"title\\\": \\\"dvclive/plots/metrics/atemp.tsv\\\", \\\"width\\\": 300, \\\"height\\\": 300, \\\"params\\\": [{\\\"name\\\": \\\"smooth\\\", \\\"value\\\": 0.001, \\\"bind\\\": {\\\"input\\\": \\\"range\\\", \\\"min\\\": 0.001, \\\"max\\\": 1, \\\"step\\\": 0.001}}], \\\"layer\\\": [{\\\"mark\\\": \\\"line\\\", \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"atemp\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"atemp\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"atemp\\\", \\\"title\\\": \\\"atemp\\\", \\\"type\\\": \\\"quantitative\\\"}]}, \\\"transform\\\": [{\\\"loess\\\": \\\"atemp\\\", \\\"on\\\": \\\"step\\\", \\\"groupby\\\": [\\\"rev\\\", \\\"filename\\\", \\\"field\\\", \\\"filename::field\\\"], \\\"bandwidth\\\": {\\\"signal\\\": \\\"smooth\\\"}}]}, {\\\"mark\\\": {\\\"type\\\": \\\"line\\\", \\\"opacity\\\": 0.2}, \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"atemp\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"atemp\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"atemp\\\", \\\"title\\\": \\\"atemp\\\", \\\"type\\\": \\\"quantitative\\\"}]}}, {\\\"mark\\\": {\\\"type\\\": \\\"circle\\\", \\\"size\\\": 10, \\\"tooltip\\\": {\\\"content\\\": \\\"encoding\\\"}}, \\\"encoding\\\": {\\\"x\\\": {\\\"aggregate\\\": \\\"max\\\", \\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"aggregate\\\": {\\\"argmax\\\": \\\"step\\\"}, \\\"field\\\": \\\"atemp\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"atemp\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}}}]};\\n\",\n       \"            vegaEmbed('#dvclive_plots_metrics_atemp_tsv', spec);\\n\",\n       \"        </script>\\n\",\n       \"    </div>\\n\",\n       \"    \\n\",\n       \"\\n\",\n       \"    <div id = \\\"dvclive_plots_metrics_holiday_tsv\\\">\\n\",\n       \"        <script type = \\\"text/javascript\\\">\\n\",\n       \"            var spec = {\\\"$schema\\\": \\\"https://vega.github.io/schema/vega-lite/v5.json\\\", \\\"data\\\": {\\\"values\\\": [{\\\"step\\\": \\\"0\\\", \\\"holiday\\\": \\\"0.98\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"1\\\", \\\"holiday\\\": \\\"0.545\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"2\\\", \\\"holiday\\\": \\\"0.588\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"3\\\", \\\"holiday\\\": \\\"0.275\\\", \\\"rev\\\": \\\"workspace\\\"}]}, \\\"title\\\": \\\"dvclive/plots/metrics/holiday.tsv\\\", \\\"width\\\": 300, \\\"height\\\": 300, \\\"params\\\": [{\\\"name\\\": \\\"smooth\\\", \\\"value\\\": 0.001, \\\"bind\\\": {\\\"input\\\": \\\"range\\\", \\\"min\\\": 0.001, \\\"max\\\": 1, \\\"step\\\": 0.001}}], \\\"layer\\\": [{\\\"mark\\\": \\\"line\\\", \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"holiday\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"holiday\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"holiday\\\", \\\"title\\\": \\\"holiday\\\", \\\"type\\\": \\\"quantitative\\\"}]}, \\\"transform\\\": [{\\\"loess\\\": \\\"holiday\\\", \\\"on\\\": \\\"step\\\", \\\"groupby\\\": [\\\"rev\\\", \\\"filename\\\", \\\"field\\\", \\\"filename::field\\\"], \\\"bandwidth\\\": {\\\"signal\\\": \\\"smooth\\\"}}]}, {\\\"mark\\\": {\\\"type\\\": \\\"line\\\", \\\"opacity\\\": 0.2}, \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"holiday\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"holiday\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"holiday\\\", \\\"title\\\": \\\"holiday\\\", \\\"type\\\": \\\"quantitative\\\"}]}}, {\\\"mark\\\": {\\\"type\\\": \\\"circle\\\", \\\"size\\\": 10, \\\"tooltip\\\": {\\\"content\\\": \\\"encoding\\\"}}, \\\"encoding\\\": {\\\"x\\\": {\\\"aggregate\\\": \\\"max\\\", \\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"aggregate\\\": {\\\"argmax\\\": \\\"step\\\"}, \\\"field\\\": \\\"holiday\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"holiday\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}}}]};\\n\",\n       \"            vegaEmbed('#dvclive_plots_metrics_holiday_tsv', spec);\\n\",\n       \"        </script>\\n\",\n       \"    </div>\\n\",\n       \"    \\n\",\n       \"\\n\",\n       \"    <div id = \\\"dvclive_plots_metrics_hum_tsv\\\">\\n\",\n       \"        <script type = \\\"text/javascript\\\">\\n\",\n       \"            var spec = {\\\"$schema\\\": \\\"https://vega.github.io/schema/vega-lite/v5.json\\\", \\\"data\\\": {\\\"values\\\": [{\\\"step\\\": \\\"0\\\", \\\"hum\\\": \\\"1.0\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"1\\\", \\\"hum\\\": \\\"0.03\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"2\\\", \\\"hum\\\": \\\"0.684\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"3\\\", \\\"hum\\\": \\\"0.062\\\", \\\"rev\\\": \\\"workspace\\\"}]}, \\\"title\\\": \\\"dvclive/plots/metrics/hum.tsv\\\", \\\"width\\\": 300, \\\"height\\\": 300, \\\"params\\\": [{\\\"name\\\": \\\"smooth\\\", \\\"value\\\": 0.001, \\\"bind\\\": {\\\"input\\\": \\\"range\\\", \\\"min\\\": 0.001, \\\"max\\\": 1, \\\"step\\\": 0.001}}], \\\"layer\\\": [{\\\"mark\\\": \\\"line\\\", \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"hum\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"hum\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"hum\\\", \\\"title\\\": \\\"hum\\\", \\\"type\\\": \\\"quantitative\\\"}]}, \\\"transform\\\": [{\\\"loess\\\": \\\"hum\\\", \\\"on\\\": \\\"step\\\", \\\"groupby\\\": [\\\"rev\\\", \\\"filename\\\", \\\"field\\\", \\\"filename::field\\\"], \\\"bandwidth\\\": {\\\"signal\\\": \\\"smooth\\\"}}]}, {\\\"mark\\\": {\\\"type\\\": \\\"line\\\", \\\"opacity\\\": 0.2}, \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"hum\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"hum\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"hum\\\", \\\"title\\\": \\\"hum\\\", \\\"type\\\": \\\"quantitative\\\"}]}}, {\\\"mark\\\": {\\\"type\\\": \\\"circle\\\", \\\"size\\\": 10, \\\"tooltip\\\": {\\\"content\\\": \\\"encoding\\\"}}, \\\"encoding\\\": {\\\"x\\\": {\\\"aggregate\\\": \\\"max\\\", \\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"aggregate\\\": {\\\"argmax\\\": \\\"step\\\"}, \\\"field\\\": \\\"hum\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"hum\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}}}]};\\n\",\n       \"            vegaEmbed('#dvclive_plots_metrics_hum_tsv', spec);\\n\",\n       \"        </script>\\n\",\n       \"    </div>\\n\",\n       \"    \\n\",\n       \"\\n\",\n       \"    <div id = \\\"dvclive_plots_metrics_temp_tsv\\\">\\n\",\n       \"        <script type = \\\"text/javascript\\\">\\n\",\n       \"            var spec = {\\\"$schema\\\": \\\"https://vega.github.io/schema/vega-lite/v5.json\\\", \\\"data\\\": {\\\"values\\\": [{\\\"step\\\": \\\"0\\\", \\\"temp\\\": \\\"1.0\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"1\\\", \\\"temp\\\": \\\"0.098\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"2\\\", \\\"temp\\\": \\\"0.399\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"3\\\", \\\"temp\\\": \\\"0.0\\\", \\\"rev\\\": \\\"workspace\\\"}]}, \\\"title\\\": \\\"dvclive/plots/metrics/temp.tsv\\\", \\\"width\\\": 300, \\\"height\\\": 300, \\\"params\\\": [{\\\"name\\\": \\\"smooth\\\", \\\"value\\\": 0.001, \\\"bind\\\": {\\\"input\\\": \\\"range\\\", \\\"min\\\": 0.001, \\\"max\\\": 1, \\\"step\\\": 0.001}}], \\\"layer\\\": [{\\\"mark\\\": \\\"line\\\", \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"temp\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"temp\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"temp\\\", \\\"title\\\": \\\"temp\\\", \\\"type\\\": \\\"quantitative\\\"}]}, \\\"transform\\\": [{\\\"loess\\\": \\\"temp\\\", \\\"on\\\": \\\"step\\\", \\\"groupby\\\": [\\\"rev\\\", \\\"filename\\\", \\\"field\\\", \\\"filename::field\\\"], \\\"bandwidth\\\": {\\\"signal\\\": \\\"smooth\\\"}}]}, {\\\"mark\\\": {\\\"type\\\": \\\"line\\\", \\\"opacity\\\": 0.2}, \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"temp\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"temp\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"temp\\\", \\\"title\\\": \\\"temp\\\", \\\"type\\\": \\\"quantitative\\\"}]}}, {\\\"mark\\\": {\\\"type\\\": \\\"circle\\\", \\\"size\\\": 10, \\\"tooltip\\\": {\\\"content\\\": \\\"encoding\\\"}}, \\\"encoding\\\": {\\\"x\\\": {\\\"aggregate\\\": \\\"max\\\", \\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"aggregate\\\": {\\\"argmax\\\": \\\"step\\\"}, \\\"field\\\": \\\"temp\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"temp\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}}}]};\\n\",\n       \"            vegaEmbed('#dvclive_plots_metrics_temp_tsv', spec);\\n\",\n       \"        </script>\\n\",\n       \"    </div>\\n\",\n       \"    \\n\",\n       \"\\n\",\n       \"    <div id = \\\"dvclive_plots_metrics_weathersit_tsv\\\">\\n\",\n       \"        <script type = \\\"text/javascript\\\">\\n\",\n       \"            var spec = {\\\"$schema\\\": \\\"https://vega.github.io/schema/vega-lite/v5.json\\\", \\\"data\\\": {\\\"values\\\": [{\\\"step\\\": \\\"0\\\", \\\"weathersit\\\": \\\"0.985\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"1\\\", \\\"weathersit\\\": \\\"0.779\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"2\\\", \\\"weathersit\\\": \\\"0.155\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"3\\\", \\\"weathersit\\\": \\\"0.231\\\", \\\"rev\\\": \\\"workspace\\\"}]}, \\\"title\\\": \\\"dvclive/plots/metrics/weathersit.tsv\\\", \\\"width\\\": 300, \\\"height\\\": 300, \\\"params\\\": [{\\\"name\\\": \\\"smooth\\\", \\\"value\\\": 0.001, \\\"bind\\\": {\\\"input\\\": \\\"range\\\", \\\"min\\\": 0.001, \\\"max\\\": 1, \\\"step\\\": 0.001}}], \\\"layer\\\": [{\\\"mark\\\": \\\"line\\\", \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"weathersit\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"weathersit\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"weathersit\\\", \\\"title\\\": \\\"weathersit\\\", \\\"type\\\": \\\"quantitative\\\"}]}, \\\"transform\\\": [{\\\"loess\\\": \\\"weathersit\\\", \\\"on\\\": \\\"step\\\", \\\"groupby\\\": [\\\"rev\\\", \\\"filename\\\", \\\"field\\\", \\\"filename::field\\\"], \\\"bandwidth\\\": {\\\"signal\\\": \\\"smooth\\\"}}]}, {\\\"mark\\\": {\\\"type\\\": \\\"line\\\", \\\"opacity\\\": 0.2}, \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"weathersit\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"weathersit\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"weathersit\\\", \\\"title\\\": \\\"weathersit\\\", \\\"type\\\": \\\"quantitative\\\"}]}}, {\\\"mark\\\": {\\\"type\\\": \\\"circle\\\", \\\"size\\\": 10, \\\"tooltip\\\": {\\\"content\\\": \\\"encoding\\\"}}, \\\"encoding\\\": {\\\"x\\\": {\\\"aggregate\\\": \\\"max\\\", \\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"aggregate\\\": {\\\"argmax\\\": \\\"step\\\"}, \\\"field\\\": \\\"weathersit\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"weathersit\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}}}]};\\n\",\n       \"            vegaEmbed('#dvclive_plots_metrics_weathersit_tsv', spec);\\n\",\n       \"        </script>\\n\",\n       \"    </div>\\n\",\n       \"    \\n\",\n       \"\\n\",\n       \"    <div id = \\\"dvclive_plots_metrics_windspeed_tsv\\\">\\n\",\n       \"        <script type = \\\"text/javascript\\\">\\n\",\n       \"            var spec = {\\\"$schema\\\": \\\"https://vega.github.io/schema/vega-lite/v5.json\\\", \\\"data\\\": {\\\"values\\\": [{\\\"step\\\": \\\"0\\\", \\\"windspeed\\\": \\\"1.0\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"1\\\", \\\"windspeed\\\": \\\"0.171\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"2\\\", \\\"windspeed\\\": \\\"0.611\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"3\\\", \\\"windspeed\\\": \\\"0.012\\\", \\\"rev\\\": \\\"workspace\\\"}]}, \\\"title\\\": \\\"dvclive/plots/metrics/windspeed.tsv\\\", \\\"width\\\": 300, \\\"height\\\": 300, \\\"params\\\": [{\\\"name\\\": \\\"smooth\\\", \\\"value\\\": 0.001, \\\"bind\\\": {\\\"input\\\": \\\"range\\\", \\\"min\\\": 0.001, \\\"max\\\": 1, \\\"step\\\": 0.001}}], \\\"layer\\\": [{\\\"mark\\\": \\\"line\\\", \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"windspeed\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"windspeed\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"windspeed\\\", \\\"title\\\": \\\"windspeed\\\", \\\"type\\\": \\\"quantitative\\\"}]}, \\\"transform\\\": [{\\\"loess\\\": \\\"windspeed\\\", \\\"on\\\": \\\"step\\\", \\\"groupby\\\": [\\\"rev\\\", \\\"filename\\\", \\\"field\\\", \\\"filename::field\\\"], \\\"bandwidth\\\": {\\\"signal\\\": \\\"smooth\\\"}}]}, {\\\"mark\\\": {\\\"type\\\": \\\"line\\\", \\\"opacity\\\": 0.2}, \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"windspeed\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"windspeed\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"windspeed\\\", \\\"title\\\": \\\"windspeed\\\", \\\"type\\\": \\\"quantitative\\\"}]}}, {\\\"mark\\\": {\\\"type\\\": \\\"circle\\\", \\\"size\\\": 10, \\\"tooltip\\\": {\\\"content\\\": \\\"encoding\\\"}}, \\\"encoding\\\": {\\\"x\\\": {\\\"aggregate\\\": \\\"max\\\", \\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"aggregate\\\": {\\\"argmax\\\": \\\"step\\\"}, \\\"field\\\": \\\"windspeed\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"windspeed\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}}}]};\\n\",\n       \"            vegaEmbed('#dvclive_plots_metrics_windspeed_tsv', spec);\\n\",\n       \"        </script>\\n\",\n       \"    </div>\\n\",\n       \"    \\n\",\n       \"\\n\",\n       \"    <div id = \\\"dvclive_plots_metrics_workingday_tsv\\\">\\n\",\n       \"        <script type = \\\"text/javascript\\\">\\n\",\n       \"            var spec = {\\\"$schema\\\": \\\"https://vega.github.io/schema/vega-lite/v5.json\\\", \\\"data\\\": {\\\"values\\\": [{\\\"step\\\": \\\"0\\\", \\\"workingday\\\": \\\"0.851\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"1\\\", \\\"workingday\\\": \\\"0.653\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"2\\\", \\\"workingday\\\": \\\"0.699\\\", \\\"rev\\\": \\\"workspace\\\"}, {\\\"step\\\": \\\"3\\\", \\\"workingday\\\": \\\"0.593\\\", \\\"rev\\\": \\\"workspace\\\"}]}, \\\"title\\\": \\\"dvclive/plots/metrics/workingday.tsv\\\", \\\"width\\\": 300, \\\"height\\\": 300, \\\"params\\\": [{\\\"name\\\": \\\"smooth\\\", \\\"value\\\": 0.001, \\\"bind\\\": {\\\"input\\\": \\\"range\\\", \\\"min\\\": 0.001, \\\"max\\\": 1, \\\"step\\\": 0.001}}], \\\"layer\\\": [{\\\"mark\\\": \\\"line\\\", \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"workingday\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"workingday\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"workingday\\\", \\\"title\\\": \\\"workingday\\\", \\\"type\\\": \\\"quantitative\\\"}]}, \\\"transform\\\": [{\\\"loess\\\": \\\"workingday\\\", \\\"on\\\": \\\"step\\\", \\\"groupby\\\": [\\\"rev\\\", \\\"filename\\\", \\\"field\\\", \\\"filename::field\\\"], \\\"bandwidth\\\": {\\\"signal\\\": \\\"smooth\\\"}}]}, {\\\"mark\\\": {\\\"type\\\": \\\"line\\\", \\\"opacity\\\": 0.2}, \\\"encoding\\\": {\\\"x\\\": {\\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"field\\\": \\\"workingday\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"workingday\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}, \\\"tooltip\\\": [{\\\"field\\\": \\\"step\\\", \\\"title\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\"}, {\\\"field\\\": \\\"workingday\\\", \\\"title\\\": \\\"workingday\\\", \\\"type\\\": \\\"quantitative\\\"}]}}, {\\\"mark\\\": {\\\"type\\\": \\\"circle\\\", \\\"size\\\": 10, \\\"tooltip\\\": {\\\"content\\\": \\\"encoding\\\"}}, \\\"encoding\\\": {\\\"x\\\": {\\\"aggregate\\\": \\\"max\\\", \\\"field\\\": \\\"step\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"step\\\"}, \\\"y\\\": {\\\"aggregate\\\": {\\\"argmax\\\": \\\"step\\\"}, \\\"field\\\": \\\"workingday\\\", \\\"type\\\": \\\"quantitative\\\", \\\"title\\\": \\\"workingday\\\", \\\"scale\\\": {\\\"zero\\\": false}}, \\\"color\\\": {\\\"field\\\": \\\"rev\\\", \\\"type\\\": \\\"nominal\\\"}}}]};\\n\",\n       \"            vegaEmbed('#dvclive_plots_metrics_workingday_tsv', spec);\\n\",\n       \"        </script>\\n\",\n       \"    </div>\\n\",\n       \"    \\n\",\n       \"</body>\\n\",\n       \"</html>\"\n      ],\n      \"text/plain\": [\n       \"<IPython.core.display.HTML object>\"\n      ]\n     },\n     \"execution_count\": 16,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"import IPython\\n\",\n    \"\\n\",\n    \"IPython.display.HTML(filename=\\\"dvc_plots/index.html\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"CCdF_ipAIY7k\"\n   },\n   \"source\": [\n    \"## In multiple experiments (one per step)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 17,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"executionInfo\": {\n     \"elapsed\": 1213,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468159295,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"0x81BAI--2Gm\",\n    \"outputId\": \"7fb22cea-d367-41b0-f27d-a99e9d6081dc\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"/content\\n\",\n      \"/content/experiments\\n\",\n      \"hint: Using 'master' as the name for the initial branch. This default branch name\\n\",\n      \"hint: is subject to change. To configure the initial branch name to use in all\\n\",\n      \"hint: of your new repositories, which will suppress this warning, call:\\n\",\n      \"hint: \\n\",\n      \"hint: \\tgit config --global init.defaultBranch <name>\\n\",\n      \"hint: \\n\",\n      \"hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and\\n\",\n      \"hint: 'development'. The just-created branch can be renamed via this command:\\n\",\n      \"hint: \\n\",\n      \"hint: \\tgit branch -m <name>\\n\",\n      \"Initialized empty Git repository in /content/experiments/.git/\\n\",\n      \"fatal: pathspec '.gitignore' did not match any files\\n\",\n      \"On branch master\\n\",\n      \"\\n\",\n      \"Initial commit\\n\",\n      \"\\n\",\n      \"nothing to commit (create/copy files and use \\\"git add\\\" to track)\\n\",\n      \"Initialized DVC repository.\\n\",\n      \"\\n\",\n      \"You can now commit the changes to git.\\n\",\n      \"\\n\",\n      \"+---------------------------------------------------------------------+\\n\",\n      \"|                                                                     |\\n\",\n      \"|        DVC has enabled anonymous aggregate usage analytics.         |\\n\",\n      \"|     Read the analytics documentation (and how to opt-out) here:     |\\n\",\n      \"|             <https://dvc.org/doc/user-guide/analytics>              |\\n\",\n      \"|                                                                     |\\n\",\n      \"+---------------------------------------------------------------------+\\n\",\n      \"\\n\",\n      \"What's next?\\n\",\n      \"------------\\n\",\n      \"- Check out the documentation: <https://dvc.org/doc>\\n\",\n      \"- Get help and share ideas: <https://dvc.org/chat>\\n\",\n      \"- Star us on GitHub: <https://github.com/iterative/dvc>\\n\",\n      \"[master (root-commit) 469083d] Init DVC\\n\",\n      \" 3 files changed, 6 insertions(+)\\n\",\n      \" create mode 100644 .dvc/.gitignore\\n\",\n      \" create mode 100644 .dvc/config\\n\",\n      \" create mode 100644 .dvcignore\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Setup a git repo with dvc\\n\",\n    \"\\n\",\n    \"%cd /content\\n\",\n    \"!rm -rf experiments && mkdir experiments\\n\",\n    \"%cd experiments\\n\",\n    \"\\n\",\n    \"!git init\\n\",\n    \"!git add .gitignore\\n\",\n    \"!git commit -m \\\"Init repo\\\"\\n\",\n    \"!dvc init\\n\",\n    \"!git commit -m \\\"Init DVC\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 18,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 2355,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468161649,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"VfVLDwfD39qO\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from dvclive import Live\\n\",\n    \"\\n\",\n    \"for step, date in enumerate(experiment_batches):\\n\",\n    \"    with Live() as live:\\n\",\n    \"        live.log_param(\\\"step\\\", step)\\n\",\n    \"        live.log_param(\\\"begin\\\", date[0])\\n\",\n    \"        live.log_param(\\\"end\\\", date[1])\\n\",\n    \"\\n\",\n    \"        metrics = eval_drift(\\n\",\n    \"            df.loc[df.dteday.between(reference_dates[0], reference_dates[1])],\\n\",\n    \"            df.loc[df.dteday.between(date[0], date[1])],\\n\",\n    \"            column_mapping=data_columns,\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        for feature in metrics:\\n\",\n    \"            live.log_metric(feature[0], round(feature[1], 3))\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 19,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\",\n     \"height\": 238\n    },\n    \"executionInfo\": {\n     \"elapsed\": 433,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468162078,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"ijcN3PaZ6fM0\",\n    \"outputId\": \"2d26f834-604f-4e28-8924-f5d97ae92596\"\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"\\n\",\n       \"  <div id=\\\"df-8c0d1ac2-fd6a-4e45-909c-a1c7a643c0ff\\\" class=\\\"colab-df-container\\\">\\n\",\n       \"    <div>\\n\",\n       \"<style scoped>\\n\",\n       \"    .dataframe tbody tr th:only-of-type {\\n\",\n       \"        vertical-align: middle;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe tbody tr th {\\n\",\n       \"        vertical-align: top;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe thead th {\\n\",\n       \"        text-align: right;\\n\",\n       \"    }\\n\",\n       \"</style>\\n\",\n       \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n       \"  <thead>\\n\",\n       \"    <tr style=\\\"text-align: right;\\\">\\n\",\n       \"      <th></th>\\n\",\n       \"      <th>Experiment</th>\\n\",\n       \"      <th>rev</th>\\n\",\n       \"      <th>typ</th>\\n\",\n       \"      <th>Created</th>\\n\",\n       \"      <th>parent</th>\\n\",\n       \"      <th>State</th>\\n\",\n       \"      <th>Executor</th>\\n\",\n       \"      <th>weathersit</th>\\n\",\n       \"      <th>temp</th>\\n\",\n       \"      <th>atemp</th>\\n\",\n       \"      <th>hum</th>\\n\",\n       \"      <th>windspeed</th>\\n\",\n       \"      <th>holiday</th>\\n\",\n       \"      <th>workingday</th>\\n\",\n       \"      <th>step</th>\\n\",\n       \"      <th>begin</th>\\n\",\n       \"      <th>end</th>\\n\",\n       \"    </tr>\\n\",\n       \"  </thead>\\n\",\n       \"  <tbody>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>0</th>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>workspace</td>\\n\",\n       \"      <td>baseline</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>0.231</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>0.062</td>\\n\",\n       \"      <td>0.012</td>\\n\",\n       \"      <td>0.275</td>\\n\",\n       \"      <td>0.593</td>\\n\",\n       \"      <td>3.0</td>\\n\",\n       \"      <td>2011-02-15 00:00:00</td>\\n\",\n       \"      <td>2011-02-21 23:00:00</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>1</th>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>master</td>\\n\",\n       \"      <td>baseline</td>\\n\",\n       \"      <td>02:55 PM</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>2</th>\\n\",\n       \"      <td>elite-mobs</td>\\n\",\n       \"      <td>e4d6acd</td>\\n\",\n       \"      <td>branch_commit</td>\\n\",\n       \"      <td>02:56 PM</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>0.231</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>0.062</td>\\n\",\n       \"      <td>0.012</td>\\n\",\n       \"      <td>0.275</td>\\n\",\n       \"      <td>0.593</td>\\n\",\n       \"      <td>3.0</td>\\n\",\n       \"      <td>2011-02-15 00:00:00</td>\\n\",\n       \"      <td>2011-02-21 23:00:00</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>3</th>\\n\",\n       \"      <td>buxom-shes</td>\\n\",\n       \"      <td>439f6e1</td>\\n\",\n       \"      <td>branch_commit</td>\\n\",\n       \"      <td>02:56 PM</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>0.155</td>\\n\",\n       \"      <td>0.399</td>\\n\",\n       \"      <td>0.537</td>\\n\",\n       \"      <td>0.684</td>\\n\",\n       \"      <td>0.611</td>\\n\",\n       \"      <td>0.588</td>\\n\",\n       \"      <td>0.699</td>\\n\",\n       \"      <td>2.0</td>\\n\",\n       \"      <td>2011-02-07 00:00:00</td>\\n\",\n       \"      <td>2011-02-14 23:00:00</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>4</th>\\n\",\n       \"      <td>hammy-skip</td>\\n\",\n       \"      <td>b5b80b5</td>\\n\",\n       \"      <td>branch_commit</td>\\n\",\n       \"      <td>02:55 PM</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>0.985</td>\\n\",\n       \"      <td>1.000</td>\\n\",\n       \"      <td>1.000</td>\\n\",\n       \"      <td>1.000</td>\\n\",\n       \"      <td>1.000</td>\\n\",\n       \"      <td>0.980</td>\\n\",\n       \"      <td>0.851</td>\\n\",\n       \"      <td>NaN</td>\\n\",\n       \"      <td>2011-01-01 00:00:00</td>\\n\",\n       \"      <td>2011-01-29 23:00:00</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>5</th>\\n\",\n       \"      <td>girly-sere</td>\\n\",\n       \"      <td>2ba9568</td>\\n\",\n       \"      <td>branch_base</td>\\n\",\n       \"      <td>02:55 PM</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>None</td>\\n\",\n       \"      <td>0.779</td>\\n\",\n       \"      <td>0.098</td>\\n\",\n       \"      <td>0.107</td>\\n\",\n       \"      <td>0.030</td>\\n\",\n       \"      <td>0.171</td>\\n\",\n       \"      <td>0.545</td>\\n\",\n       \"      <td>0.653</td>\\n\",\n       \"      <td>1.0</td>\\n\",\n       \"      <td>2011-01-29 00:00:00</td>\\n\",\n       \"      <td>2011-02-07 23:00:00</td>\\n\",\n       \"    </tr>\\n\",\n       \"  </tbody>\\n\",\n       \"</table>\\n\",\n       \"</div>\\n\",\n       \"    <div class=\\\"colab-df-buttons\\\">\\n\",\n       \"\\n\",\n       \"  <div class=\\\"colab-df-container\\\">\\n\",\n       \"    <button class=\\\"colab-df-convert\\\" onclick=\\\"convertToInteractive('df-8c0d1ac2-fd6a-4e45-909c-a1c7a643c0ff')\\\"\\n\",\n       \"            title=\\\"Convert this dataframe to an interactive table.\\\"\\n\",\n       \"            style=\\\"display:none;\\\">\\n\",\n       \"\\n\",\n       \"  <svg xmlns=\\\"http://www.w3.org/2000/svg\\\" height=\\\"24px\\\" viewBox=\\\"0 -960 960 960\\\">\\n\",\n       \"    <path d=\\\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\\\"/>\\n\",\n       \"  </svg>\\n\",\n       \"    </button>\\n\",\n       \"\\n\",\n       \"  <style>\\n\",\n       \"    .colab-df-container {\\n\",\n       \"      display:flex;\\n\",\n       \"      gap: 12px;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .colab-df-convert {\\n\",\n       \"      background-color: #E8F0FE;\\n\",\n       \"      border: none;\\n\",\n       \"      border-radius: 50%;\\n\",\n       \"      cursor: pointer;\\n\",\n       \"      display: none;\\n\",\n       \"      fill: #1967D2;\\n\",\n       \"      height: 32px;\\n\",\n       \"      padding: 0 0 0 0;\\n\",\n       \"      width: 32px;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .colab-df-convert:hover {\\n\",\n       \"      background-color: #E2EBFA;\\n\",\n       \"      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\\n\",\n       \"      fill: #174EA6;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .colab-df-buttons div {\\n\",\n       \"      margin-bottom: 4px;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    [theme=dark] .colab-df-convert {\\n\",\n       \"      background-color: #3B4455;\\n\",\n       \"      fill: #D2E3FC;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    [theme=dark] .colab-df-convert:hover {\\n\",\n       \"      background-color: #434B5C;\\n\",\n       \"      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n\",\n       \"      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\\n\",\n       \"      fill: #FFFFFF;\\n\",\n       \"    }\\n\",\n       \"  </style>\\n\",\n       \"\\n\",\n       \"    <script>\\n\",\n       \"      const buttonEl =\\n\",\n       \"        document.querySelector('#df-8c0d1ac2-fd6a-4e45-909c-a1c7a643c0ff button.colab-df-convert');\\n\",\n       \"      buttonEl.style.display =\\n\",\n       \"        google.colab.kernel.accessAllowed ? 'block' : 'none';\\n\",\n       \"\\n\",\n       \"      async function convertToInteractive(key) {\\n\",\n       \"        const element = document.querySelector('#df-8c0d1ac2-fd6a-4e45-909c-a1c7a643c0ff');\\n\",\n       \"        const dataTable =\\n\",\n       \"          await google.colab.kernel.invokeFunction('convertToInteractive',\\n\",\n       \"                                                    [key], {});\\n\",\n       \"        if (!dataTable) return;\\n\",\n       \"\\n\",\n       \"        const docLinkHtml = 'Like what you see? Visit the ' +\\n\",\n       \"          '<a target=\\\"_blank\\\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\\n\",\n       \"          + ' to learn more about interactive tables.';\\n\",\n       \"        element.innerHTML = '';\\n\",\n       \"        dataTable['output_type'] = 'display_data';\\n\",\n       \"        await google.colab.output.renderOutput(dataTable, element);\\n\",\n       \"        const docLink = document.createElement('div');\\n\",\n       \"        docLink.innerHTML = docLinkHtml;\\n\",\n       \"        element.appendChild(docLink);\\n\",\n       \"      }\\n\",\n       \"    </script>\\n\",\n       \"  </div>\\n\",\n       \"\\n\",\n       \"\\n\",\n       \"<div id=\\\"df-34c6bd2a-95f3-454a-a096-42c68cda98fb\\\">\\n\",\n       \"  <button class=\\\"colab-df-quickchart\\\" onclick=\\\"quickchart('df-34c6bd2a-95f3-454a-a096-42c68cda98fb')\\\"\\n\",\n       \"            title=\\\"Suggest charts.\\\"\\n\",\n       \"            style=\\\"display:none;\\\">\\n\",\n       \"\\n\",\n       \"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" height=\\\"24px\\\"viewBox=\\\"0 0 24 24\\\"\\n\",\n       \"     width=\\\"24px\\\">\\n\",\n       \"    <g>\\n\",\n       \"        <path d=\\\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\\\"/>\\n\",\n       \"    </g>\\n\",\n       \"</svg>\\n\",\n       \"  </button>\\n\",\n       \"\\n\",\n       \"<style>\\n\",\n       \"  .colab-df-quickchart {\\n\",\n       \"      --bg-color: #E8F0FE;\\n\",\n       \"      --fill-color: #1967D2;\\n\",\n       \"      --hover-bg-color: #E2EBFA;\\n\",\n       \"      --hover-fill-color: #174EA6;\\n\",\n       \"      --disabled-fill-color: #AAA;\\n\",\n       \"      --disabled-bg-color: #DDD;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  [theme=dark] .colab-df-quickchart {\\n\",\n       \"      --bg-color: #3B4455;\\n\",\n       \"      --fill-color: #D2E3FC;\\n\",\n       \"      --hover-bg-color: #434B5C;\\n\",\n       \"      --hover-fill-color: #FFFFFF;\\n\",\n       \"      --disabled-bg-color: #3B4455;\\n\",\n       \"      --disabled-fill-color: #666;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-quickchart {\\n\",\n       \"    background-color: var(--bg-color);\\n\",\n       \"    border: none;\\n\",\n       \"    border-radius: 50%;\\n\",\n       \"    cursor: pointer;\\n\",\n       \"    display: none;\\n\",\n       \"    fill: var(--fill-color);\\n\",\n       \"    height: 32px;\\n\",\n       \"    padding: 0;\\n\",\n       \"    width: 32px;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-quickchart:hover {\\n\",\n       \"    background-color: var(--hover-bg-color);\\n\",\n       \"    box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\\n\",\n       \"    fill: var(--button-hover-fill-color);\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-quickchart-complete:disabled,\\n\",\n       \"  .colab-df-quickchart-complete:disabled:hover {\\n\",\n       \"    background-color: var(--disabled-bg-color);\\n\",\n       \"    fill: var(--disabled-fill-color);\\n\",\n       \"    box-shadow: none;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  .colab-df-spinner {\\n\",\n       \"    border: 2px solid var(--fill-color);\\n\",\n       \"    border-color: transparent;\\n\",\n       \"    border-bottom-color: var(--fill-color);\\n\",\n       \"    animation:\\n\",\n       \"      spin 1s steps(1) infinite;\\n\",\n       \"  }\\n\",\n       \"\\n\",\n       \"  @keyframes spin {\\n\",\n       \"    0% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-bottom-color: var(--fill-color);\\n\",\n       \"      border-left-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    20% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-left-color: var(--fill-color);\\n\",\n       \"      border-top-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    30% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-left-color: var(--fill-color);\\n\",\n       \"      border-top-color: var(--fill-color);\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    40% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"      border-top-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    60% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    80% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-right-color: var(--fill-color);\\n\",\n       \"      border-bottom-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"    90% {\\n\",\n       \"      border-color: transparent;\\n\",\n       \"      border-bottom-color: var(--fill-color);\\n\",\n       \"    }\\n\",\n       \"  }\\n\",\n       \"</style>\\n\",\n       \"\\n\",\n       \"  <script>\\n\",\n       \"    async function quickchart(key) {\\n\",\n       \"      const quickchartButtonEl =\\n\",\n       \"        document.querySelector('#' + key + ' button');\\n\",\n       \"      quickchartButtonEl.disabled = true;  // To prevent multiple clicks.\\n\",\n       \"      quickchartButtonEl.classList.add('colab-df-spinner');\\n\",\n       \"      try {\\n\",\n       \"        const charts = await google.colab.kernel.invokeFunction(\\n\",\n       \"            'suggestCharts', [key], {});\\n\",\n       \"      } catch (error) {\\n\",\n       \"        console.error('Error during call to suggestCharts:', error);\\n\",\n       \"      }\\n\",\n       \"      quickchartButtonEl.classList.remove('colab-df-spinner');\\n\",\n       \"      quickchartButtonEl.classList.add('colab-df-quickchart-complete');\\n\",\n       \"    }\\n\",\n       \"    (() => {\\n\",\n       \"      let quickchartButtonEl =\\n\",\n       \"        document.querySelector('#df-34c6bd2a-95f3-454a-a096-42c68cda98fb button');\\n\",\n       \"      quickchartButtonEl.style.display =\\n\",\n       \"        google.colab.kernel.accessAllowed ? 'block' : 'none';\\n\",\n       \"    })();\\n\",\n       \"  </script>\\n\",\n       \"</div>\\n\",\n       \"    </div>\\n\",\n       \"  </div>\\n\"\n      ],\n      \"text/plain\": [\n       \"   Experiment        rev            typ   Created parent State Executor  \\\\\\n\",\n       \"0        None  workspace       baseline      None   None  None     None   \\n\",\n       \"1        None     master       baseline  02:55 PM   None  None     None   \\n\",\n       \"2  elite-mobs    e4d6acd  branch_commit  02:56 PM   None  None     None   \\n\",\n       \"3  buxom-shes    439f6e1  branch_commit  02:56 PM   None  None     None   \\n\",\n       \"4  hammy-skip    b5b80b5  branch_commit  02:55 PM   None  None     None   \\n\",\n       \"5  girly-sere    2ba9568    branch_base  02:55 PM   None  None     None   \\n\",\n       \"\\n\",\n       \"   weathersit   temp  atemp    hum  windspeed  holiday  workingday  step  \\\\\\n\",\n       \"0       0.231    NaN    NaN  0.062      0.012    0.275       0.593   3.0   \\n\",\n       \"1         NaN    NaN    NaN    NaN        NaN      NaN         NaN   NaN   \\n\",\n       \"2       0.231    NaN    NaN  0.062      0.012    0.275       0.593   3.0   \\n\",\n       \"3       0.155  0.399  0.537  0.684      0.611    0.588       0.699   2.0   \\n\",\n       \"4       0.985  1.000  1.000  1.000      1.000    0.980       0.851   NaN   \\n\",\n       \"5       0.779  0.098  0.107  0.030      0.171    0.545       0.653   1.0   \\n\",\n       \"\\n\",\n       \"                 begin                  end  \\n\",\n       \"0  2011-02-15 00:00:00  2011-02-21 23:00:00  \\n\",\n       \"1                 None                 None  \\n\",\n       \"2  2011-02-15 00:00:00  2011-02-21 23:00:00  \\n\",\n       \"3  2011-02-07 00:00:00  2011-02-14 23:00:00  \\n\",\n       \"4  2011-01-01 00:00:00  2011-01-29 23:00:00  \\n\",\n       \"5  2011-01-29 00:00:00  2011-02-07 23:00:00  \"\n      ]\n     },\n     \"execution_count\": 19,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"import dvc.api\\n\",\n    \"\\n\",\n    \"pd.DataFrame(dvc.api.exp_show())\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"TQE5aBWl-sef\"\n   },\n   \"source\": [\n    \"To explore the results from CLI:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"executionInfo\": {\n     \"elapsed\": 1221,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468163295,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"oZtY-97bQj-Q\",\n    \"outputId\": \"14eb8d4c-c9ce-4bb8-caba-42e46d45bb65\"\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── \\n\",\n      \"  Experiment                 Created    weathersit    temp   atemp     hum   windspeed   holiday   workingday   step   begin                 end                  \\n\",\n      \" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── \\n\",\n      \"  workspace                  -               0.231       0       0   0.062       0.012     0.275        0.593   3      2011-02-15 00:00:00   2011-02-21 23:00:00  \\n\",\n      \"  master                     02:55 PM            -       -       -       -           -         -            -   -      -                     -                    \\n\",\n      \"  ├── e4d6acd [elite-mobs]   02:56 PM        0.231       0       0   0.062       0.012     0.275        0.593   3      2011-02-15 00:00:00   2011-02-21 23:00:00  \\n\",\n      \"  ├── 439f6e1 [buxom-shes]   02:56 PM        0.155   0.399   0.537   0.684       0.611     0.588        0.699   2      2011-02-07 00:00:00   2011-02-14 23:00:00  \\n\",\n      \"  ├── b5b80b5 [hammy-skip]   02:55 PM        0.985       1       1       1           1      0.98        0.851   0      2011-01-01 00:00:00   2011-01-29 23:00:00  \\n\",\n      \"  └── 2ba9568 [girly-sere]   02:55 PM        0.779   0.098   0.107    0.03       0.171     0.545        0.653   1      2011-01-29 00:00:00   2011-02-07 23:00:00  \\n\",\n      \" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── \\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"!dvc exp show\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"metadata\": {\n    \"executionInfo\": {\n     \"elapsed\": 464,\n     \"status\": \"ok\",\n     \"timestamp\": 1697468163757,\n     \"user\": {\n      \"displayName\": \"Francesco Motoko\",\n      \"userId\": \"00974636158007469548\"\n     },\n     \"user_tz\": -120\n    },\n    \"id\": \"QoYexufp-qw2\"\n   },\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"authorship_tag\": \"ABX9TyNJAdha/v4n9zLqIfGakg0E\",\n   \"provenance\": [],\n   \"toc_visible\": true\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"name\": \"python\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/DVCLive-Fabric.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"QKSE19fW_Dnj\"\n   },\n   \"source\": [\n    \"# DVCLive and Lightning Fabric\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"q-C_4R_o_QGG\"\n   },\n   \"source\": [\n    \"## Install dvclive\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"-XFbvwq7TSwN\",\n    \"outputId\": \"15d0e3b5-bb4a-4b3e-d37f-21608d1822ed\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install \\\"dvclive[lightning]\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"I6S6Uru1_Y0x\"\n   },\n   \"source\": [\n    \"## Initialize DVC Repository\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\"\n    },\n    \"id\": \"WcbvUl2uTV0y\",\n    \"outputId\": \"aff9740c-26db-483d-ce30-cfef395f3cbb\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!git init -q\\n\",\n    \"!git config --local user.email \\\"you@example.com\\\"\\n\",\n    \"!git config --local user.name \\\"Your Name\\\"\\n\",\n    \"!dvc init -q\\n\",\n    \"!git commit -m \\\"DVC init\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"LmY4PLMh_cUk\"\n   },\n   \"source\": [\n    \"## Imports\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"85qErT5yTEbN\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from types import SimpleNamespace\\n\",\n    \"\\n\",\n    \"import torch\\n\",\n    \"import torch.nn.functional as F  # noqa: N812\\n\",\n    \"import torchvision.transforms as T  # noqa: N812\\n\",\n    \"from lightning.fabric import Fabric, seed_everything\\n\",\n    \"from lightning.fabric.utilities.rank_zero import rank_zero_only\\n\",\n    \"from torch import nn, optim\\n\",\n    \"from torch.optim.lr_scheduler import StepLR\\n\",\n    \"from torchmetrics.classification import Accuracy\\n\",\n    \"from torchvision.datasets import MNIST\\n\",\n    \"\\n\",\n    \"from dvclive.fabric import DVCLiveLogger\\n\",\n    \"\\n\",\n    \"DATASETS_PATH = \\\"Datasets\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"UrmAHbhr_lgs\"\n   },\n   \"source\": [\n    \"## Setup model code\\n\",\n    \"\\n\",\n    \"Adapted from https://github.com/Lightning-AI/pytorch-lightning/blob/master/examples/fabric/image_classifier/train_fabric.py.\\n\",\n    \"\\n\",\n    \"Look for the `logger` statements where DVCLiveLogger calls were added.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"UCzTygUnTHM8\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"class Net(nn.Module):\\n\",\n    \"    def __init__(self) -> None:\\n\",\n    \"        super().__init__()\\n\",\n    \"        self.conv1 = nn.Conv2d(1, 32, 3, 1)\\n\",\n    \"        self.conv2 = nn.Conv2d(32, 64, 3, 1)\\n\",\n    \"        self.dropout1 = nn.Dropout(0.25)\\n\",\n    \"        self.dropout2 = nn.Dropout(0.5)\\n\",\n    \"        self.fc1 = nn.Linear(9216, 128)\\n\",\n    \"        self.fc2 = nn.Linear(128, 10)\\n\",\n    \"\\n\",\n    \"    def forward(self, x):\\n\",\n    \"        x = self.conv1(x)\\n\",\n    \"        x = F.relu(x)\\n\",\n    \"        x = self.conv2(x)\\n\",\n    \"        x = F.relu(x)\\n\",\n    \"        x = F.max_pool2d(x, 2)\\n\",\n    \"        x = self.dropout1(x)\\n\",\n    \"        x = torch.flatten(x, 1)\\n\",\n    \"        x = self.fc1(x)\\n\",\n    \"        x = F.relu(x)\\n\",\n    \"        x = self.dropout2(x)\\n\",\n    \"        x = self.fc2(x)\\n\",\n    \"        return F.log_softmax(x, dim=1)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def run(hparams):\\n\",\n    \"    # Create the DVCLive Logger\\n\",\n    \"    logger = DVCLiveLogger(report=\\\"notebook\\\")\\n\",\n    \"\\n\",\n    \"    # Log dict of hyperparameters\\n\",\n    \"    logger.log_hyperparams(hparams.__dict__)\\n\",\n    \"\\n\",\n    \"    # Create the Lightning Fabric object. The parameters like accelerator, strategy,\\n\",\n    \"    # devices etc. will be proided by the command line. See all options: `lightning\\n\",\n    \"    # run model --help`\\n\",\n    \"    fabric = Fabric()\\n\",\n    \"\\n\",\n    \"    seed_everything(hparams.seed)  # instead of torch.manual_seed(...)\\n\",\n    \"\\n\",\n    \"    transform = T.Compose([T.ToTensor(), T.Normalize((0.1307,), (0.3081,))])\\n\",\n    \"\\n\",\n    \"    # Let rank 0 download the data first, then everyone will load MNIST\\n\",\n    \"    with fabric.rank_zero_first(\\n\",\n    \"        local=False\\n\",\n    \"    ):  # set `local=True` if your filesystem is not shared between machines\\n\",\n    \"        train_dataset = MNIST(\\n\",\n    \"            DATASETS_PATH,\\n\",\n    \"            download=fabric.is_global_zero,\\n\",\n    \"            train=True,\\n\",\n    \"            transform=transform,\\n\",\n    \"        )\\n\",\n    \"        test_dataset = MNIST(\\n\",\n    \"            DATASETS_PATH,\\n\",\n    \"            download=fabric.is_global_zero,\\n\",\n    \"            train=False,\\n\",\n    \"            transform=transform,\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    train_loader = torch.utils.data.DataLoader(\\n\",\n    \"        train_dataset,\\n\",\n    \"        batch_size=hparams.batch_size,\\n\",\n    \"    )\\n\",\n    \"    test_loader = torch.utils.data.DataLoader(\\n\",\n    \"        test_dataset, batch_size=hparams.batch_size\\n\",\n    \"    )\\n\",\n    \"\\n\",\n    \"    # don't forget to call `setup_dataloaders` to prepare for dataloaders for\\n\",\n    \"    # distributed training.\\n\",\n    \"    train_loader, test_loader = fabric.setup_dataloaders(train_loader, test_loader)\\n\",\n    \"\\n\",\n    \"    model = Net()  # remove call to .to(device)\\n\",\n    \"    optimizer = optim.Adadelta(model.parameters(), lr=hparams.lr)\\n\",\n    \"\\n\",\n    \"    # don't forget to call `setup` to prepare for model / optimizer for\\n\",\n    \"    # distributed training. The model is moved automatically to the right device.\\n\",\n    \"    model, optimizer = fabric.setup(model, optimizer)\\n\",\n    \"\\n\",\n    \"    scheduler = StepLR(optimizer, step_size=1, gamma=hparams.gamma)\\n\",\n    \"\\n\",\n    \"    # use torchmetrics instead of manually computing the accuracy\\n\",\n    \"    test_acc = Accuracy(task=\\\"multiclass\\\", num_classes=10).to(fabric.device)\\n\",\n    \"\\n\",\n    \"    # EPOCH LOOP\\n\",\n    \"    for epoch in range(1, hparams.epochs + 1):\\n\",\n    \"        # TRAINING LOOP\\n\",\n    \"        model.train()\\n\",\n    \"        for batch_idx, (data, target) in enumerate(train_loader):\\n\",\n    \"            # NOTE: no need to call `.to(device)` on the data, target\\n\",\n    \"            optimizer.zero_grad()\\n\",\n    \"            output = model(data)\\n\",\n    \"            loss = F.nll_loss(output, target)\\n\",\n    \"            fabric.backward(loss)  # instead of loss.backward()\\n\",\n    \"\\n\",\n    \"            optimizer.step()\\n\",\n    \"            if (batch_idx == 0) or ((batch_idx + 1) % hparams.log_interval == 0):\\n\",\n    \"                done = (batch_idx * len(data)) / len(train_loader.dataset)\\n\",\n    \"                pct = 100.0 * batch_idx / len(train_loader)\\n\",\n    \"                print(  # noqa: T201\\n\",\n    \"                    f\\\"-> Epoch: {epoch} [{done} ({pct:.0f}%)]\\\\tLoss: {loss.item():.6f}\\\"\\n\",\n    \"                )\\n\",\n    \"\\n\",\n    \"                # Log dict of metrics\\n\",\n    \"                logger.log_metrics({\\\"loss\\\": loss.item()})\\n\",\n    \"\\n\",\n    \"                if hparams.dry_run:\\n\",\n    \"                    break\\n\",\n    \"\\n\",\n    \"        scheduler.step()\\n\",\n    \"\\n\",\n    \"        # TESTING LOOP\\n\",\n    \"        model.eval()\\n\",\n    \"        test_loss = 0\\n\",\n    \"        with torch.no_grad():\\n\",\n    \"            for data, target in test_loader:\\n\",\n    \"                # NOTE: no need to call `.to(device)` on the data, target\\n\",\n    \"                output = model(data)\\n\",\n    \"                test_loss += F.nll_loss(output, target, reduction=\\\"sum\\\").item()\\n\",\n    \"\\n\",\n    \"                # WITHOUT TorchMetrics\\n\",\n    \"                # pred = output.argmax(dim=1, keepdim=True)  # get the index of the max\\n\",\n    \"                # log-probability correct += pred.eq(target.view_as(pred)).sum().item()\\n\",\n    \"\\n\",\n    \"                # WITH TorchMetrics\\n\",\n    \"                test_acc(output, target)\\n\",\n    \"\\n\",\n    \"                if hparams.dry_run:\\n\",\n    \"                    break\\n\",\n    \"\\n\",\n    \"        # all_gather is used to aggregated the value across processes\\n\",\n    \"        test_loss = fabric.all_gather(test_loss).sum() / len(test_loader.dataset)\\n\",\n    \"        acc = 100 * test_acc.compute()\\n\",\n    \"\\n\",\n    \"        print(  # noqa: T201\\n\",\n    \"            f\\\"\\\\nTest set: Average loss: {test_loss:.4f}, Accuracy: ({acc:.0f}%)\\\\n\\\"\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        # log additional metrics\\n\",\n    \"        logger.log_metrics(\\n\",\n    \"            {\\\"test_loss\\\": test_loss, \\\"test_acc\\\": 100 * test_acc.compute()}\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        test_acc.reset()\\n\",\n    \"\\n\",\n    \"        if hparams.dry_run:\\n\",\n    \"            break\\n\",\n    \"\\n\",\n    \"    # When using distributed training, use `fabric.save`\\n\",\n    \"    # to ensure the current process is allowed to save a checkpoint\\n\",\n    \"    if hparams.save_model:\\n\",\n    \"        fabric.save(\\\"mnist_cnn.pt\\\", model.state_dict())\\n\",\n    \"\\n\",\n    \"        # `logger.experiment` provides access to the `dvclive.Live` instance where you\\n\",\n    \"        # can use additional logging methods. Check that `rank_zero_only.rank == 0` to\\n\",\n    \"        # avoid logging in other processes.\\n\",\n    \"        if rank_zero_only.rank == 0:\\n\",\n    \"            logger.experiment.log_artifact(\\\"mnist_cnn.pt\\\")\\n\",\n    \"\\n\",\n    \"    # Call finalize to save final results as a DVC experiment\\n\",\n    \"    logger.finalize(\\\"success\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"o5_v9lRDAM7l\"\n   },\n   \"source\": [\n    \"## Train the model\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"colab\": {\n     \"base_uri\": \"https://localhost:8080/\",\n     \"height\": 1000\n    },\n    \"id\": \"BbCXen1PTM4V\",\n    \"outputId\": \"b79c90eb-74cc-474d-c0dd-21245064bca8\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"hparams = SimpleNamespace(\\n\",\n    \"    batch_size=64,\\n\",\n    \"    epochs=5,\\n\",\n    \"    lr=1.0,\\n\",\n    \"    gamma=0.7,\\n\",\n    \"    dry_run=False,\\n\",\n    \"    seed=1,\\n\",\n    \"    log_interval=10,\\n\",\n    \"    save_model=True,\\n\",\n    \")\\n\",\n    \"run(hparams)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"DnqCrlbLAopV\"\n   },\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \".venv\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"name\": \"python\",\n   \"version\": \"3.12.2\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/DVCLive-HuggingFace.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"3SJ8SY6ldmsS\"\n   },\n   \"source\": [\n    \"### How to do Experiment tracking with DVCLive\\n\",\n    \"\\n\",\n    \"What you will learn?\\n\",\n    \"\\n\",\n    \"- Fine-tuning a model on a binary text classification task\\n\",\n    \"- Track machine learning experiments with DVCLive\\n\",\n    \"- Visualize results and create a report\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"nxiSBytidmsU\"\n   },\n   \"source\": [\n    \"#### Setup (Install Dependencies & Setup Git)\\n\",\n    \"\\n\",\n    \"- Install accelerate , Datasets , evaluate , transformers and dvclive\\n\",\n    \"- Start a Git repo. Your experiments will be saved in a commit but hidden in\\n\",\n    \"  order to not clutter your repo.\\n\",\n    \"- Initialize DVC\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"CLRgy2W4dmsU\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!pip install datasets dvclive evaluate pandas 'transformers[torch]' --upgrade\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"fo0sq84UdmsV\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!git init -q\\n\",\n    \"!git config --local user.email \\\"you@example.com\\\"\\n\",\n    \"!git config --local user.name \\\"Your Name\\\"\\n\",\n    \"!dvc init -q\\n\",\n    \"!git commit -m \\\"DVC init\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"T5WYJ31UdmsV\"\n   },\n   \"source\": [\n    \"### Fine-tuning a model on a text classification task\\n\",\n    \"\\n\",\n    \"#### Loading the dataset\\n\",\n    \"\\n\",\n    \"We will use the [imdb](https://huggingface.co/datasets/imdb) Large Movie Review Dataset. This is a dataset for binary\\n\",\n    \"sentiment classification containing a set of 25K movie reviews for training and\\n\",\n    \"25K for testing.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"41fP0WCbdmsV\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from datasets import load_dataset\\n\",\n    \"from transformers import AutoTokenizer\\n\",\n    \"\\n\",\n    \"dataset = load_dataset(\\\"imdb\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"V3gDKbbSdmsV\"\n   },\n   \"source\": [\n    \"#### Preprocessing the data\\n\",\n    \"\\n\",\n    \"We use `transformers.AutoTokenizer` which transforms the inputs and put them in a format\\n\",\n    \"the model expects.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"uVr5lufodmsV\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"tokenizer = AutoTokenizer.from_pretrained(\\\"distilbert-base-cased\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def tokenize_function(examples):\\n\",\n    \"    return tokenizer(examples[\\\"text\\\"], padding=\\\"max_length\\\", truncation=True)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"small_train_dataset = (\\n\",\n    \"    dataset[\\\"train\\\"]\\n\",\n    \"    .shuffle(seed=42)\\n\",\n    \"    .select(range(2000))\\n\",\n    \"    .map(tokenize_function, batched=True)\\n\",\n    \")\\n\",\n    \"small_eval_dataset = (\\n\",\n    \"    dataset[\\\"test\\\"]\\n\",\n    \"    .shuffle(seed=42)\\n\",\n    \"    .select(range(200))\\n\",\n    \"    .map(tokenize_function, batched=True)\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"g9sELYMHdmsV\"\n   },\n   \"source\": [\n    \"#### Define evaluation metrics\\n\",\n    \"\\n\",\n    \"f1 is a metric for combining precision and recall metrics in one unique value, so\\n\",\n    \"we take this criteria for evaluating the models.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"wmJoy5V-dmsW\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import evaluate\\n\",\n    \"import numpy as np\\n\",\n    \"\\n\",\n    \"metric = evaluate.load(\\\"f1\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def compute_metrics(eval_pred):\\n\",\n    \"    logits, labels = eval_pred\\n\",\n    \"    predictions = np.argmax(logits, axis=-1)\\n\",\n    \"    return metric.compute(predictions=predictions, references=labels)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"NwFntrIKdmsW\"\n   },\n   \"source\": [\n    \"### Training and Tracking experiments with DVCLive\\n\",\n    \"\\n\",\n    \"Track experiments in DVC by changing a few lines of your Python code.\\n\",\n    \"Save model artifacts using `HF_DVCLIVE_LOG_MODEL=true`.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"-A1oXCxE4zGi\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"%env HF_DVCLIVE_LOG_MODEL=true\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"gKKSTh0ZdmsW\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments\\n\",\n    \"from transformers.integrations import DVCLiveCallback\\n\",\n    \"\\n\",\n    \"model = AutoModelForSequenceClassification.from_pretrained(\\n\",\n    \"    \\\"distilbert-base-cased\\\", num_labels=2\\n\",\n    \")\\n\",\n    \"for param in model.base_model.parameters():\\n\",\n    \"    param.requires_grad = False\\n\",\n    \"\\n\",\n    \"lr = 3e-4\\n\",\n    \"\\n\",\n    \"training_args = TrainingArguments(\\n\",\n    \"    eval_strategy=\\\"epoch\\\",\\n\",\n    \"    learning_rate=lr,\\n\",\n    \"    logging_strategy=\\\"epoch\\\",\\n\",\n    \"    num_train_epochs=5,\\n\",\n    \"    output_dir=\\\"output\\\",\\n\",\n    \"    overwrite_output_dir=True,\\n\",\n    \"    load_best_model_at_end=True,\\n\",\n    \"    save_strategy=\\\"epoch\\\",\\n\",\n    \"    weight_decay=0.01,\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"trainer = Trainer(\\n\",\n    \"    model=model,\\n\",\n    \"    args=training_args,\\n\",\n    \"    train_dataset=small_train_dataset,\\n\",\n    \"    eval_dataset=small_eval_dataset,\\n\",\n    \"    compute_metrics=compute_metrics,\\n\",\n    \")\\n\",\n    \"trainer.train()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"KKJCw0Vj6UTw\"\n   },\n   \"source\": [\n    \"To customize tracking, include `transformers.integrations.DVCLiveCallback` in the `Trainer` callbacks and pass additional keyword arguments to `dvclive.Live`.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"M4FKUYTi5zYQ\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from dvclive import Live\\n\",\n    \"\\n\",\n    \"lr = 1e-4\\n\",\n    \"\\n\",\n    \"training_args = TrainingArguments(\\n\",\n    \"    eval_strategy=\\\"epoch\\\",\\n\",\n    \"    learning_rate=lr,\\n\",\n    \"    logging_strategy=\\\"epoch\\\",\\n\",\n    \"    num_train_epochs=5,\\n\",\n    \"    output_dir=\\\"output\\\",\\n\",\n    \"    overwrite_output_dir=True,\\n\",\n    \"    load_best_model_at_end=True,\\n\",\n    \"    save_strategy=\\\"epoch\\\",\\n\",\n    \"    weight_decay=0.01,\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"trainer = Trainer(\\n\",\n    \"    model=model,\\n\",\n    \"    args=training_args,\\n\",\n    \"    train_dataset=small_train_dataset,\\n\",\n    \"    eval_dataset=small_eval_dataset,\\n\",\n    \"    compute_metrics=compute_metrics,\\n\",\n    \"    callbacks=[DVCLiveCallback(live=Live(report=\\\"notebook\\\"), log_model=True)],\\n\",\n    \")\\n\",\n    \"trainer.train()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"l29wqAaDdmsW\"\n   },\n   \"source\": [\n    \"### Comparing Experiments\\n\",\n    \"\\n\",\n    \"We create a dataframe with the experiments in order to visualize it.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"wwMwHvVtdmsW\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import dvc.api\\n\",\n    \"import pandas as pd\\n\",\n    \"\\n\",\n    \"columns = [\\\"Experiment\\\", \\\"epoch\\\", \\\"eval.f1\\\"]\\n\",\n    \"\\n\",\n    \"df = pd.DataFrame(dvc.api.exp_show(), columns=columns)\\n\",\n    \"\\n\",\n    \"df.dropna(inplace=True)\\n\",\n    \"df.reset_index(drop=True, inplace=True)\\n\",\n    \"df\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"TNBGUqoCdmsW\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!dvc plots diff $(dvc exp list --names-only)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"sL5pH4X5dmsW\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from IPython.display import HTML\\n\",\n    \"\\n\",\n    \"HTML(filename=\\\"./dvc_plots/index.html\\\")\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"colab\": {\n   \"provenance\": []\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.11.7\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "examples/DVCLive-PyTorch-Lightning.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"A812CVYi_B2b\"\n   },\n   \"source\": [\n    \"<a href=\\\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-PyTorch-Lightning.ipynb\\\" target=\\\"_parent\\\"><img src=\\\"https://colab.research.google.com/assets/colab-badge.svg\\\" alt=\\\"Open In Colab\\\"/></a>\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"gPh2FiPo_B2e\"\n   },\n   \"source\": [\n    \"# DVCLive and PyTorch Lightning\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"m0XW9Ml7_B2e\"\n   },\n   \"source\": [\n    \"## Setup\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"QivH1_cU_B2f\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install \\\"dvclive[lightning]\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"pn_5GW1f_B2g\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!git init -q\\n\",\n    \"!git config --local user.email \\\"you@example.com\\\"\\n\",\n    \"!git config --local user.name \\\"Your Name\\\"\\n\",\n    \"!dvc init -q\\n\",\n    \"!git commit -m \\\"DVC init\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"zC9hk7kibFTX\"\n   },\n   \"source\": [\n    \"### Define LightningModule\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"t5PxdljP_B2h\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import lightning.pytorch as pl\\n\",\n    \"import torch\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"class LitAutoEncoder(pl.LightningModule):\\n\",\n    \"    def __init__(self, encoder_size=64, lr=1e-3):  # noqa: ARG002\\n\",\n    \"        super().__init__()\\n\",\n    \"        self.save_hyperparameters()\\n\",\n    \"        self.encoder = torch.nn.Sequential(\\n\",\n    \"            torch.nn.Linear(28 * 28, encoder_size),\\n\",\n    \"            torch.nn.ReLU(),\\n\",\n    \"            torch.nn.Linear(encoder_size, 3),\\n\",\n    \"        )\\n\",\n    \"        self.decoder = torch.nn.Sequential(\\n\",\n    \"            torch.nn.Linear(3, encoder_size),\\n\",\n    \"            torch.nn.ReLU(),\\n\",\n    \"            torch.nn.Linear(encoder_size, 28 * 28),\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"    def training_step(self, batch, batch_idx):  # noqa: ARG002\\n\",\n    \"        x, _y = batch\\n\",\n    \"        x = x.view(x.size(0), -1)\\n\",\n    \"        z = self.encoder(x)\\n\",\n    \"        x_hat = self.decoder(z)\\n\",\n    \"        train_mse = torch.nn.functional.mse_loss(x_hat, x)\\n\",\n    \"        self.log(\\\"train_mse\\\", train_mse)\\n\",\n    \"        return train_mse\\n\",\n    \"\\n\",\n    \"    def validation_step(self, batch, batch_idx):  # noqa: ARG002\\n\",\n    \"        x, _y = batch\\n\",\n    \"        x = x.view(x.size(0), -1)\\n\",\n    \"        z = self.encoder(x)\\n\",\n    \"        x_hat = self.decoder(z)\\n\",\n    \"        val_mse = torch.nn.functional.mse_loss(x_hat, x)\\n\",\n    \"        self.log(\\\"val_mse\\\", val_mse)\\n\",\n    \"        return val_mse\\n\",\n    \"\\n\",\n    \"    def configure_optimizers(self):\\n\",\n    \"        return torch.optim.Adam(self.parameters(), lr=self.hparams.lr)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"St0ElX9obqRS\"\n   },\n   \"source\": [\n    \"### Dataset and loaders\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"T5s53qgr_B2h\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from torchvision import transforms\\n\",\n    \"from torchvision.datasets import MNIST\\n\",\n    \"\\n\",\n    \"transform = transforms.ToTensor()\\n\",\n    \"train_set = MNIST(root=\\\"MNIST\\\", download=True, train=True, transform=transform)\\n\",\n    \"validation_set = MNIST(root=\\\"MNIST\\\", download=True, train=False, transform=transform)\\n\",\n    \"train_loader = torch.utils.data.DataLoader(train_set)\\n\",\n    \"validation_loader = torch.utils.data.DataLoader(validation_set)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"ttiwwreH_B2i\"\n   },\n   \"source\": [\n    \"# Tracking experiments with DVCLive\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"sE6qj6BMoDkn\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from dvclive.lightning import DVCLiveLogger\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"XDqNY8pL_B2i\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"for encoder_size in (64, 128):\\n\",\n    \"    for lr in (1e-3, 0.1):\\n\",\n    \"        model = LitAutoEncoder(encoder_size=encoder_size, lr=lr)\\n\",\n    \"        trainer = pl.Trainer(\\n\",\n    \"            limit_train_batches=200,\\n\",\n    \"            limit_val_batches=100,\\n\",\n    \"            max_epochs=5,\\n\",\n    \"            logger=DVCLiveLogger(log_model=True, report=\\\"notebook\\\"),\\n\",\n    \"        )\\n\",\n    \"        trainer.fit(model, train_loader, validation_loader)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {\n    \"id\": \"7zEi0BXp_B2i\"\n   },\n   \"source\": [\n    \"## Comparing results\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"1aHmLHmf_B2i\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import dvc.api\\n\",\n    \"import pandas as pd\\n\",\n    \"\\n\",\n    \"columns = [\\\"Experiment\\\", \\\"encoder_size\\\", \\\"lr\\\", \\\"train.mse\\\", \\\"val.mse\\\"]\\n\",\n    \"\\n\",\n    \"df = pd.DataFrame(dvc.api.exp_show(), columns=columns)\\n\",\n    \"\\n\",\n    \"df.dropna(inplace=True)\\n\",\n    \"df.reset_index(drop=True, inplace=True)\\n\",\n    \"df\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"db42qeHEGqTA\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from plotly.express import parallel_coordinates\\n\",\n    \"\\n\",\n    \"fig = parallel_coordinates(df, columns, color=\\\"val.mse\\\")\\n\",\n    \"fig.show()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"3cfvi0Uk_B2j\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!dvc plots diff $(dvc exp list --names-only)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"id\": \"Zx5n2zbn_B2j\"\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from IPython.display import HTML\\n\",\n    \"\\n\",\n    \"HTML(filename=\\\"./dvc_plots/index.html\\\")\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"accelerator\": \"GPU\",\n  \"colab\": {\n   \"gpuType\": \"T4\",\n   \"provenance\": [],\n   \"toc_visible\": true\n  },\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.9.16\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "examples/DVCLive-Quickstart.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"attachments\": {},\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"<a href=\\\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-Quickstart.ipynb\\\" target=\\\"_parent\\\"><img src=\\\"https://colab.research.google.com/assets/colab-badge.svg\\\" alt=\\\"Open In Colab\\\"/></a>\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# DVCLive Quickstart\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Install dvclive\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install dvclive\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Initialize DVC Repository\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!git init -q\\n\",\n    \"!git config --local user.email \\\"you@example.com\\\"\\n\",\n    \"!git config --local user.name \\\"Your Name\\\"\\n\",\n    \"!dvc init -q\\n\",\n    \"!git commit -m \\\"DVC init\\\"\"\n   ]\n  },\n  {\n   \"attachments\": {},\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Setup code\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# @title Training helpers. { display-mode: \\\"form\\\" }\\n\",\n    \"\\n\",\n    \"import numpy as np\\n\",\n    \"import torch\\n\",\n    \"import torchvision\\n\",\n    \"\\n\",\n    \"from dvclive import Live\\n\",\n    \"\\n\",\n    \"device = \\\"cuda:0\\\" if torch.cuda.is_available() else \\\"cpu\\\"\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def transform(dataset):\\n\",\n    \"    \\\"\\\"\\\"Get inputs and targets from dataset.\\\"\\\"\\\"\\n\",\n    \"    x = dataset.data.reshape(len(dataset.data), 1, 28, 28) / 255\\n\",\n    \"    y = dataset.targets\\n\",\n    \"    return x.to(device), y.to(device)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def train_one_epoch(model, criterion, x, y, lr, weight_decay):\\n\",\n    \"    model.train()\\n\",\n    \"    optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)\\n\",\n    \"    y_pred = model(x)\\n\",\n    \"    loss = criterion(y_pred, y)\\n\",\n    \"    optimizer.zero_grad()\\n\",\n    \"    loss.backward()\\n\",\n    \"    optimizer.step()\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def predict(model, x):\\n\",\n    \"    \\\"\\\"\\\"Get model prediction scores.\\\"\\\"\\\"\\n\",\n    \"    model.eval()\\n\",\n    \"    with torch.no_grad():\\n\",\n    \"        return model(x)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_metrics(y, y_pred, y_pred_label):\\n\",\n    \"    \\\"\\\"\\\"Get loss and accuracy metrics.\\\"\\\"\\\"\\n\",\n    \"    metrics = {}\\n\",\n    \"    criterion = torch.nn.CrossEntropyLoss()\\n\",\n    \"    metrics[\\\"loss\\\"] = criterion(y_pred, y).item()\\n\",\n    \"    metrics[\\\"acc\\\"] = (y_pred_label == y).sum().item() / len(y)\\n\",\n    \"    return metrics\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def evaluate(model, x, y):\\n\",\n    \"    \\\"\\\"\\\"Evaluate model and save metrics.\\\"\\\"\\\"\\n\",\n    \"    scores = predict(model, x)\\n\",\n    \"    _, labels = torch.max(scores, 1)\\n\",\n    \"    actual = [int(v) for v in y]\\n\",\n    \"    predicted = [int(v) for v in labels]\\n\",\n    \"\\n\",\n    \"    metrics = get_metrics(y, scores, labels)\\n\",\n    \"\\n\",\n    \"    return metrics, actual, predicted\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"def get_missclassified_image(actual, predicted, dataset):\\n\",\n    \"    confusion = {}\\n\",\n    \"    for n, (a, p) in enumerate(zip(actual, predicted)):\\n\",\n    \"        image = np.array(dataset[n][0]) / 255\\n\",\n    \"        confusion[(a, p)] = image\\n\",\n    \"\\n\",\n    \"    max_i, max_j = 0, 0\\n\",\n    \"    for i, j in confusion:\\n\",\n    \"        max_i = max(i, max_i)\\n\",\n    \"        max_j = max(j, max_j)\\n\",\n    \"\\n\",\n    \"    frame_size = 30\\n\",\n    \"    image_shape = (28, 28)\\n\",\n    \"    incorrect_color = np.array((255, 100, 100), dtype=\\\"uint8\\\")\\n\",\n    \"    label_color = np.array((100, 100, 240), dtype=\\\"uint8\\\")\\n\",\n    \"\\n\",\n    \"    out_matrix = (\\n\",\n    \"        np.ones(\\n\",\n    \"            shape=((max_i + 2) * frame_size, (max_j + 2) * frame_size, 3), dtype=\\\"uint8\\\"\\n\",\n    \"        )\\n\",\n    \"        * 240\\n\",\n    \"    )\\n\",\n    \"\\n\",\n    \"    for i in range(max_i + 1):\\n\",\n    \"        if (i, i) in confusion:\\n\",\n    \"            image = confusion[(i, i)]\\n\",\n    \"            xs = (i + 1) * frame_size + 1\\n\",\n    \"            xe = (i + 2) * frame_size - 1\\n\",\n    \"            ys = 1\\n\",\n    \"            ye = frame_size - 1\\n\",\n    \"            for c in range(3):\\n\",\n    \"                out_matrix[xs:xe, ys:ye, c] = (1 - image) * label_color[c]\\n\",\n    \"                out_matrix[ys:ye, xs:xe, c] = (1 - image) * label_color[c]\\n\",\n    \"\\n\",\n    \"    for i, j in confusion:  # noqa: PLC0206\\n\",\n    \"        image = confusion[(i, j)]\\n\",\n    \"        assert image.shape == image_shape  # noqa: S101\\n\",\n    \"        xs = (i + 1) * frame_size + 1\\n\",\n    \"        xe = (i + 2) * frame_size - 1\\n\",\n    \"        ys = (j + 1) * frame_size + 1\\n\",\n    \"        ye = (j + 2) * frame_size - 1\\n\",\n    \"        assert (xe - xs, ye - ys) == image_shape  # noqa: S101\\n\",\n    \"        if i != j:\\n\",\n    \"            for c in range(3):\\n\",\n    \"                out_matrix[xs:xe, ys:ye, c] = (1 - image) * incorrect_color[c]\\n\",\n    \"\\n\",\n    \"    return out_matrix\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# @title Initialize model and dataset. { display-mode: \\\"form\\\" }\\n\",\n    \"\\n\",\n    \"model = torch.nn.Sequential(\\n\",\n    \"    torch.nn.Flatten(),\\n\",\n    \"    torch.nn.Linear(28 * 28, 128),\\n\",\n    \"    torch.nn.ReLU(),\\n\",\n    \"    torch.nn.Dropout(0.1),\\n\",\n    \"    torch.nn.Linear(128, 64),\\n\",\n    \"    torch.nn.ReLU(),\\n\",\n    \"    torch.nn.Dropout(0.1),\\n\",\n    \"    torch.nn.Linear(64, 10),\\n\",\n    \").to(device)\\n\",\n    \"\\n\",\n    \"criterion = torch.nn.CrossEntropyLoss()\\n\",\n    \"\\n\",\n    \"mnist_train = torchvision.datasets.MNIST(\\\"data\\\", download=True)\\n\",\n    \"x_train, y_train = transform(mnist_train)\\n\",\n    \"mnist_test = torchvision.datasets.MNIST(\\\"data\\\", download=True, train=False)\\n\",\n    \"x_test, y_test = transform(mnist_test)\"\n   ]\n  },\n  {\n   \"attachments\": {},\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Tracking experiments with DVCLive\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# You can modify these parameters to see how they affect the training\\n\",\n    \"# And run the cell several times\\n\",\n    \"params = {\\\"epochs\\\": 5, \\\"lr\\\": 0.003, \\\"weight_decay\\\": 0}\\n\",\n    \"\\n\",\n    \"best_test_acc = 0\\n\",\n    \"\\n\",\n    \"with Live(report=\\\"notebook\\\") as live:\\n\",\n    \"    live.log_params(params)\\n\",\n    \"\\n\",\n    \"    for _ in range(params[\\\"epochs\\\"]):\\n\",\n    \"        train_one_epoch(\\n\",\n    \"            model, criterion, x_train, y_train, params[\\\"lr\\\"], params[\\\"weight_decay\\\"]\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        # Train Evaluation\\n\",\n    \"        metrics_train, acual_train, predicted_train = evaluate(model, x_train, y_train)\\n\",\n    \"\\n\",\n    \"        for k, v in metrics_train.items():\\n\",\n    \"            live.log_metric(f\\\"train/{k}\\\", v)\\n\",\n    \"\\n\",\n    \"        live.log_sklearn_plot(\\n\",\n    \"            \\\"confusion_matrix\\\",\\n\",\n    \"            acual_train,\\n\",\n    \"            predicted_train,\\n\",\n    \"            name=\\\"train/confusion_matrix\\\",\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        # Test Evaluation\\n\",\n    \"        metrics_test, actual, predicted = evaluate(model, x_test, y_test)\\n\",\n    \"\\n\",\n    \"        for k, v in metrics_test.items():\\n\",\n    \"            live.log_metric(f\\\"test/{k}\\\", v)\\n\",\n    \"\\n\",\n    \"        live.log_sklearn_plot(\\n\",\n    \"            \\\"confusion_matrix\\\", actual, predicted, name=\\\"test/confusion_matrix\\\"\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        live.log_image(\\n\",\n    \"            \\\"misclassified.jpg\\\", get_missclassified_image(actual, predicted, mnist_test)\\n\",\n    \"        )\\n\",\n    \"\\n\",\n    \"        # Save best model\\n\",\n    \"        if metrics_test[\\\"acc\\\"] > best_test_acc:\\n\",\n    \"            torch.save(model.state_dict(), \\\"model.pt\\\")\\n\",\n    \"\\n\",\n    \"        live.next_step()\\n\",\n    \"\\n\",\n    \"    live.log_artifact(\\\"model.pt\\\")\"\n   ]\n  },\n  {\n   \"attachments\": {},\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Comparing results\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import dvc.api\\n\",\n    \"import pandas as pd\\n\",\n    \"\\n\",\n    \"columns = [\\\"epochs\\\", \\\"lr\\\", \\\"weight_decay\\\", \\\"test.acc\\\"]\\n\",\n    \"\\n\",\n    \"df = pd.DataFrame(dvc.api.exp_show(), columns=columns)\\n\",\n    \"\\n\",\n    \"df.dropna(inplace=True)\\n\",\n    \"df.reset_index(drop=True, inplace=True)\\n\",\n    \"df\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from plotly.express import parallel_coordinates\\n\",\n    \"\\n\",\n    \"fig = parallel_coordinates(df, columns, color=\\\"test.acc\\\")\\n\",\n    \"fig.show()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!dvc plots diff $(dvc exp list --names-only)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from IPython.display import HTML\\n\",\n    \"\\n\",\n    \"HTML(filename=\\\"./dvc_plots/index.html\\\")\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.9.16\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/DVCLive-YOLO.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"<a href=\\\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-YOLO.ipynb\\\" target=\\\"_parent\\\"><img src=\\\"https://colab.research.google.com/assets/colab-badge.svg\\\" alt=\\\"Open In Colab\\\"/></a>\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# DVCLive and Ultralytics YOLOv8\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Setup\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%pip install dvclive ultralytics\\n\",\n    \"import ultralytics\\n\",\n    \"\\n\",\n    \"ultralytics.checks()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!git init -q\\n\",\n    \"!git config --local user.email \\\"you@example.com\\\"\\n\",\n    \"!git config --local user.name \\\"Your Name\\\"\\n\",\n    \"!dvc init -q\\n\",\n    \"!git commit -m \\\"DVC init\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Tracking experiments with DVCLive\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"If `dvclive` is installed, Ultralytics YOLO v8 will automatically use DVCLive for tracking experiments.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!yolo train model=yolov8n.pt data=coco8.yaml epochs=5 imgsz=512\\n\",\n    \"!yolo train model=yolov8n.pt data=coco8.yaml epochs=5 imgsz=640\\n\",\n    \"!yolo train model=yolov8n.pt data=coco8.yaml epochs=10 imgsz=640\\n\",\n    \"!yolo train model=yolov8s.pt data=coco8.yaml epochs=10 imgsz=640\\n\",\n    \"!yolo train model=yolov8m.pt data=coco8.yaml epochs=10 imgsz=640\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Comparing results\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import dvc.api\\n\",\n    \"import pandas as pd\\n\",\n    \"\\n\",\n    \"columns = [\\\"Experiment\\\", \\\"epochs\\\", \\\"imgsz\\\", \\\"model\\\", \\\"metrics.mAP50-95(B)\\\"]\\n\",\n    \"\\n\",\n    \"df = pd.DataFrame(dvc.api.exp_show(), columns=columns)\\n\",\n    \"\\n\",\n    \"df.dropna(inplace=True)\\n\",\n    \"df.reset_index(drop=True, inplace=True)\\n\",\n    \"df\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from plotly.express import parallel_coordinates\\n\",\n    \"\\n\",\n    \"fig = parallel_coordinates(df, columns, color=\\\"metrics.mAP50-95(B)\\\")\\n\",\n    \"fig.show()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!dvc plots diff $(dvc exp list --names-only)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from IPython.display import HTML\\n\",\n    \"\\n\",\n    \"HTML(filename=\\\"./dvc_plots/index.html\\\")\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"language_info\": {\n   \"name\": \"python\"\n  },\n  \"orig_nbformat\": 4\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/DVCLive-scikit-learn.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"<a href=\\\"https://colab.research.google.com/github/iterative/dvclive/blob/main/examples/DVCLive-scikit-learn.ipynb\\\" target=\\\"_parent\\\"><img src=\\\"https://colab.research.google.com/assets/colab-badge.svg\\\" alt=\\\"Open In Colab\\\"/></a>\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# DVCLive and scikit-learn\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Setup\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"plaintext\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!pip install dvclive scikit-learn\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"plaintext\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!git init -q\\n\",\n    \"!git config --local user.email \\\"you@example.com\\\"\\n\",\n    \"!git config --local user.name \\\"Your Name\\\"\\n\",\n    \"!dvc init -q\\n\",\n    \"!git commit -m \\\"DVC init\\\"\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"plaintext\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from sklearn.datasets import make_circles\\n\",\n    \"from sklearn.model_selection import train_test_split\\n\",\n    \"\\n\",\n    \"X, y = make_circles(noise=0.3, factor=0.5, random_state=42)\\n\",\n    \"\\n\",\n    \"X_train, X_test, y_train, y_test = train_test_split(\\n\",\n    \"    X,\\n\",\n    \"    y,\\n\",\n    \"    random_state=42)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Tracking experiments with DVCLive\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"plaintext\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from dvclive import Live\\n\",\n    \"\\n\",\n    \"from sklearn.ensemble import RandomForestClassifier\\n\",\n    \"from sklearn.metrics import f1_score\\n\",\n    \"\\n\",\n    \"for n_estimators in (10, 50, 100):\\n\",\n    \"\\n\",\n    \"  with Live() as live:\\n\",\n    \"\\n\",\n    \"    live.log_param(\\\"n_estimators\\\", n_estimators)\\n\",\n    \"\\n\",\n    \"    clf = RandomForestClassifier(n_estimators=n_estimators)\\n\",\n    \"    clf.fit(X_train, y_train)\\n\",\n    \"\\n\",\n    \"    y_train_pred = clf.predict(X_train)\\n\",\n    \"\\n\",\n    \"    live.log_metric(\\\"train/f1\\\", f1_score(y_train, y_train_pred, average=\\\"weighted\\\"), plot=False)\\n\",\n    \"    live.log_sklearn_plot(\\n\",\n    \"      \\\"confusion_matrix\\\", y_train, y_train_pred, name=\\\"train/confusion_matrix\\\",\\n\",\n    \"      title=\\\"Train Confusion Matrix\\\")\\n\",\n    \"\\n\",\n    \"    y_test_pred = clf.predict(X_test)\\n\",\n    \"\\n\",\n    \"    live.log_metric(\\\"test/f1\\\", f1_score(y_test, y_test_pred, average=\\\"weighted\\\"), plot=False)\\n\",\n    \"    live.log_sklearn_plot(\\n\",\n    \"      \\\"confusion_matrix\\\", y_test, y_test_pred, name=\\\"test/confusion_matrix\\\",\\n\",\n    \"      title=\\\"Test Confusion Matrix\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Comparing results\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"plaintext\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import dvc.api\\n\",\n    \"import pandas as pd\\n\",\n    \"\\n\",\n    \"columns = [\\\"Experiment\\\", \\\"train.f1\\\", \\\"test.f1\\\", \\\"n_estimators\\\"]\\n\",\n    \"df = pd.DataFrame(dvc.api.exp_show(), columns=columns)\\n\",\n    \"\\n\",\n    \"df.dropna(inplace=True)\\n\",\n    \"df.reset_index(drop=True, inplace=True)\\n\",\n    \"df\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"plaintext\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"!dvc plots diff $(dvc exp list --names-only)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"vscode\": {\n     \"languageId\": \"plaintext\"\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from IPython.display import HTML\\n\",\n    \"HTML(filename='./dvc_plots/index.html')\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"language_info\": {\n   \"name\": \"python\"\n  },\n  \"orig_nbformat\": 4\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "noxfile.py",
    "content": "\"\"\"Automation using nox.\"\"\"\n\nimport glob\nimport os\n\nimport nox\n\nnox.options.default_venv_backend = \"uv|virtualenv\"\nnox.options.reuse_existing_virtualenvs = True\nnox.options.sessions = \"lint\", \"tests\"\n\nproject = nox.project.load_toml()\npython_versions = nox.project.python_versions(project)\n\n\n@nox.session(python=python_versions)\ndef tests(session: nox.Session) -> None:\n    session.install(\".[dev]\")\n    session.run(\n        \"pytest\",\n        \"--cov\",\n        \"--cov-config=pyproject.toml\",\n        *session.posargs,\n        env={\"COVERAGE_FILE\": f\".coverage.{session.python}\"},\n    )\n\n\n@nox.session(python=python_versions)\ndef core_tests(session: nox.Session) -> None:\n    session.install(\".[tests]\")\n    session.run(\n        \"pytest\",\n        \"--ignore=tests/frameworks\",\n        \"--cov\",\n        \"--cov-config=pyproject.toml\",\n        *session.posargs,\n        env={\"COVERAGE_FILE\": f\".coverage.{session.python}\"},\n    )\n\n\n@nox.session\ndef lint(session: nox.Session) -> None:\n    session.install(\"pre-commit\")\n    session.install(\"-e\", \".[dev]\")\n\n    args = *(session.posargs or (\"--show-diff-on-failure\",)), \"--all-files\"\n    session.run(\"pre-commit\", \"run\", *args)\n    session.run(\"python\", \"-m\", \"mypy\")\n\n\n@nox.session\ndef safety(session: nox.Session) -> None:\n    \"\"\"Scan dependencies for insecure packages.\"\"\"\n    session.install(\".[dev]\")\n    session.install(\"safety\")\n    session.run(\"safety\", \"check\", \"--full-report\")\n\n\n@nox.session\ndef build(session: nox.Session) -> None:\n    session.install(\"twine\", \"uv\")\n    session.run(\"uv\", \"build\")\n    dists = glob.glob(\"dist/*\")\n    session.run(\"twine\", \"check\", *dists, silent=True)\n\n\n@nox.session\ndef dev(session: nox.Session) -> None:\n    \"\"\"Sets up a python development environment for the project.\"\"\"\n    args = session.posargs or (\"venv\",)\n    venv_dir = os.fsdecode(os.path.abspath(args[0]))\n\n    session.log(f\"Setting up virtual environment in {venv_dir}\")\n    session.install(\"virtualenv\")\n    session.run(\"virtualenv\", venv_dir, silent=True)\n\n    python = os.path.join(venv_dir, \"bin/python\")\n    session.run(python, \"-m\", \"pip\", \"install\", \"-e\", \".[dev]\", external=True)\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=77\", \"setuptools_scm[toml]>=8\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"dvclive\"\ndescription = \"Experiments logger for ML projects.\"\nreadme = \"README.md\"\nkeywords = [\n  \"ai\",\n  \"metrics\",\n  \"collaboration\",\n  \"data-science\",\n  \"data-version-control\",\n  \"developer-tools\",\n  \"git\",\n  \"machine-learning\",\n  \"reproducibility\"\n]\nlicense = \"Apache-2.0\"\nlicense-files = [\"LICENSE\"]\nmaintainers = [{name = \"Iterative\", email = \"support@dvc.org\"}]\nauthors = [{name = \"Iterative\", email = \"support@dvc.org\"}]\nrequires-python = \">=3.9\"\nclassifiers = [\n  \"Development Status :: 4 - Beta\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3.9\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\"\n]\ndynamic = [\"version\"]\ndependencies = [\n  \"dvc>=3.48.4\",\n  \"dvc-render>=1.0.0,<2\",\n  \"dvc-studio-client>=0.20,<1\",\n  \"funcy\",\n  \"gto\",\n  \"ruamel.yaml\",\n  \"scmrepo>=3,<4\",\n  \"psutil\",\n  \"pynvml\"\n]\n\n[project.optional-dependencies]\nimage = [\"numpy\", \"pillow\"]\nsklearn = [\"scikit-learn>=1.5.0\"]\nplots = [\"scikit-learn\", \"pandas\", \"numpy\"]\nmarkdown = [\"matplotlib\"]\ntests = [\n  \"pytest>=7.2.0,<9.0\",\n  \"pytest-sugar>=0.9.6,<2.0\",\n  \"pytest-cov>=3.0.0,<8.0\",\n  \"pytest-mock>=3.8.2,<4.0\",\n  \"dvclive[image,plots,markdown]\",\n  \"ipython\",\n  \"pytest_voluptuous\",\n  \"dpath\",\n  \"transformers[torch]\",\n  \"tf-keras\"\n]\nmmcv = [\"mmcv\"]\ntf = [\"tensorflow\"]\nxgb = [\"xgboost\"]\nlgbm = [\"lightgbm\"]\nhuggingface = [\"transformers\", \"datasets\"]\nfastai = [\"fastai\"]\nlightning = [\"lightning>=2.0\", \"torch\", \"jsonargparse[signatures]>=4.26.1\"]\noptuna = [\"optuna\"]\nall = [\n  \"dvclive[image,mmcv,tf,xgb,lgbm,huggingface,fastai,lightning,optuna,plots,markdown]\"\n]\ndev = [\n  \"dvclive[image,tf,xgb,lgbm,huggingface,fastai,lightning,optuna,plots,markdown,tests]\",\n  \"mypy==1.18.2\",\n  \"types-PyYAML\"\n]\n\n[project.urls]\nHomepage = \"https://github.com/iterative/dvclive\"\nDocumentation = \"https://dvc.org/doc/dvclive\"\nRepository = \"https://github.com/iterative/dvclive\"\nChangelog = \"https://github.com/iterative/dvclive/releases\"\nIssues = \"https://github.com/iterative/dvclive/issues\"\n\n[tool.setuptools.packages.find]\nexclude = [\"tests\", \"tests.*\"]\nwhere = [\"src\"]\nnamespaces = false\n\n[tool.setuptools_scm]\nwrite_to = \"src/dvclive/_dvclive_version.py\"\n\n[tool.pytest.ini_options]\naddopts = \"-ra\"\nmarkers = \"\"\"\n    vscode: mark a test that verifies behavior that VS Code relies on\n    studio: mark a test that verifies behavior that Studio relies on\n\"\"\"\n\n[tool.coverage.run]\nbranch = true\nsource = [\"dvclive\", \"tests\"]\n\n[tool.coverage.paths]\nsource = [\"src\", \"*/site-packages\"]\n\n[tool.coverage.report]\nshow_missing = true\nexclude_lines = [\n  \"pragma: no cover\",\n  \"if __name__ == .__main__.:\",\n  \"if typing.TYPE_CHECKING:\",\n  \"if TYPE_CHECKING:\",\n  \"raise NotImplementedError\",\n  \"raise AssertionError\",\n  \"@overload\"\n]\n\n[tool.mypy]\n# Error output\nshow_column_numbers = true\nshow_error_codes = true\nshow_error_context = true\nshow_traceback = true\npretty = true\ncheck_untyped_defs = false\n# Warnings\nwarn_no_return = true\nwarn_redundant_casts = true\nwarn_unreachable = true\nignore_missing_imports = true\nfiles = [\"src\", \"tests\"]\n\n[tool.codespell]\nignore-words-list = \"fpr\"\n\n[tool.ruff.lint]\nignore = [\"PLC0415\", \"G004\"]\nselect = [\n  \"F\",\n  \"E\",\n  \"W\",\n  \"C90\",\n  \"I\",\n  \"N\",\n  \"UP\",\n  \"YTT\",\n  \"ASYNC\",\n  \"S\",\n  \"BLE\",\n  \"B\",\n  \"A\",\n  \"C4\",\n  \"DTZ\",\n  \"T10\",\n  \"EXE\",\n  \"ISC\",\n  \"ICN\",\n  \"LOG\",\n  \"G\",\n  \"INP\",\n  \"PIE\",\n  \"T20\",\n  \"PYI\",\n  \"PT\",\n  \"Q\",\n  \"RSE\",\n  \"RET\",\n  \"SLF\",\n  \"SLOT\",\n  \"SIM\",\n  \"TID\",\n  \"TC\",\n  \"TCH\",\n  \"INT\",\n  \"ARG\",\n  \"PGH\",\n  \"PLC\",\n  \"PLE\",\n  \"PLR\",\n  \"PLW\",\n  \"TRY\",\n  \"NPY\",\n  \"FLY\",\n  \"PERF\",\n  \"FURB\",\n  \"RUF\"\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"noxfile.py\" = [\"D\", \"PTH\"]\n\"tests/*\" = [\"S101\", \"INP001\", \"SLF001\", \"ARG001\", \"ARG002\", \"ARG005\", \"PLR2004\", \"NPY002\"]\n\"examples/*.ipynb\" = [\"PERF401\"]\n\n[tool.ruff.lint.pylint]\nmax-args = 10\n"
  },
  {
    "path": "src/dvclive/__init__.py",
    "content": "from .live import Live  # noqa: F401\n"
  },
  {
    "path": "src/dvclive/dvc.py",
    "content": "# ruff: noqa: SLF001\nimport copy\nimport logging\nimport os\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, Optional\n\nfrom dvclive import env\nfrom dvclive.plots import Image, Metric\nfrom dvclive.serialize import dump_yaml\nfrom dvclive.utils import StrPath, rel_path\n\nif TYPE_CHECKING:\n    from dvc.repo import Repo\n    from dvc.stage import Stage\n\nlogger = logging.getLogger(\"dvclive\")\n\n\ndef _dvc_dir(dirname: StrPath) -> str:\n    return os.path.join(dirname, \".dvc\")\n\n\ndef _find_dvc_root(root: Optional[StrPath] = None) -> Optional[str]:\n    if not root:\n        root = os.getcwd()\n\n    root = os.path.realpath(root)\n\n    if not os.path.isdir(root):\n        raise NotADirectoryError(f\"'{root}'\")\n\n    while True:\n        if os.path.exists(_dvc_dir(root)):\n            return root\n        if os.path.ismount(root):\n            break\n        root = os.path.dirname(root)\n\n    return None\n\n\ndef get_dvc_repo() -> Optional[\"Repo\"]:\n    from dvc.exceptions import NotDvcRepoError\n    from dvc.repo import Repo\n    from dvc.scm import Git, SCMError\n    from scmrepo.exceptions import SCMError as GitSCMError\n\n    try:\n        return Repo()\n    except (NotDvcRepoError, SCMError):\n        try:\n            return Repo.init(Git().root_dir)\n        except GitSCMError:\n            return None\n\n\ndef make_dvcyaml(live) -> None:  # noqa: C901\n    dvcyaml_dir = Path(live.dvc_file).parent.absolute().as_posix()\n\n    dvcyaml = {}\n    if live._params:\n        dvcyaml[\"params\"] = [rel_path(live.params_file, dvcyaml_dir)]\n    if live._metrics or live.summary:\n        dvcyaml[\"metrics\"] = [rel_path(live.metrics_file, dvcyaml_dir)]\n    plots: list[Any] = []\n    plots_path = Path(live.plots_dir)\n    plots_metrics_path = plots_path / Metric.subfolder\n    if plots_metrics_path.exists():\n        metrics_config = {rel_path(plots_metrics_path, dvcyaml_dir): {\"x\": \"step\"}}\n        plots.append(metrics_config)\n    if live._images:\n        images_path = rel_path(plots_path / Image.subfolder, dvcyaml_dir)\n        plots.append(images_path)\n    if live._plots:\n        for plot in live._plots.values():\n            plot_path = rel_path(plot.output_path, dvcyaml_dir)\n            plots.append({plot_path: plot.plot_config})\n    if plots:\n        dvcyaml[\"plots\"] = plots\n\n    if live._artifacts:\n        dvcyaml[\"artifacts\"] = copy.deepcopy(live._artifacts)\n        for artifact in dvcyaml[\"artifacts\"].values():  # type: ignore[attr-defined]\n            artifact[\"path\"] = rel_path(artifact[\"path\"], dvcyaml_dir)\n\n    if not os.path.exists(live.dvc_file):\n        dump_yaml(dvcyaml, live.dvc_file)\n    else:\n        update_dvcyaml(live, dvcyaml)\n\n\ndef update_dvcyaml(live, updates):\n    from dvc.utils.serialize import modify_yaml\n\n    dvcyaml_dir = os.path.abspath(os.path.dirname(live.dvc_file))\n    dvclive_dir = os.path.relpath(live.dir, dvcyaml_dir) + \"/\"\n\n    def _drop_stale_dvclive_entries(entries):\n        non_dvclive = []\n        for e in entries:\n            if isinstance(e, str):\n                if dvclive_dir not in e:\n                    non_dvclive.append(e)\n            elif isinstance(e, dict) and len(e) == 1:\n                if dvclive_dir not in next(iter(e.keys())):\n                    non_dvclive.append(e)\n            else:\n                non_dvclive.append(e)\n        return non_dvclive\n\n    def _update_entries(old, new, key):\n        keepers = _drop_stale_dvclive_entries(old.get(key, []))\n        old[key] = keepers + new.get(key, [])\n        if not old[key]:\n            del old[key]\n        return old\n\n    with modify_yaml(live.dvc_file) as orig:\n        orig = _update_entries(orig, updates, \"params\")  # noqa: PLW2901\n        orig = _update_entries(orig, updates, \"metrics\")  # noqa: PLW2901\n        orig = _update_entries(orig, updates, \"plots\")  # noqa: PLW2901\n        old_artifacts = {\n            name: meta\n            for name, meta in orig.get(\"artifacts\", {}).items()\n            if dvclive_dir not in meta.get(\"path\", dvclive_dir)\n        }\n        orig[\"artifacts\"] = {**old_artifacts, **updates.get(\"artifacts\", {})}\n        if not orig[\"artifacts\"]:\n            del orig[\"artifacts\"]\n\n\ndef get_exp_name(name, scm, baseline_rev) -> str:\n    from dvc.exceptions import InvalidArgumentError\n    from dvc.repo.experiments.refs import ExpRefInfo\n    from dvc.repo.experiments.utils import (\n        check_ref_format,\n        gen_random_name,\n        get_random_exp_name,\n    )\n\n    name = name or os.getenv(env.DVC_EXP_NAME)\n    if name and scm and baseline_rev:\n        ref = ExpRefInfo(baseline_sha=baseline_rev, name=name)\n        if scm.get_ref(str(ref)):\n            logger.warning(f\"Experiment conflicts with existing experiment '{name}'.\")\n        else:\n            try:\n                check_ref_format(scm, ref)\n            except InvalidArgumentError as e:\n                logger.warning(e)\n            else:\n                return name\n    if scm and baseline_rev:\n        return get_random_exp_name(scm, baseline_rev)\n    if name:\n        return name\n    return gen_random_name()\n\n\ndef find_overlapping_stage(dvc_repo: \"Repo\", path: StrPath) -> Optional[\"Stage\"]:\n    abs_path = str(Path(path).absolute())\n    for stage in dvc_repo.index.stages:\n        for out in stage.outs:\n            if str(out.fs_path) in abs_path:\n                return stage\n    return None\n\n\ndef ensure_dir_is_tracked(directory: str, dvc_repo: \"Repo\") -> None:\n    from pathspec import PathSpec\n\n    dir_spec = PathSpec.from_lines(\"gitwildmatch\", [directory])\n    outs_spec = PathSpec.from_lines(\n        \"gitwildmatch\", [str(o) for o in dvc_repo.index.outs]\n    )\n    paths_to_track = [\n        f\n        for f in dvc_repo.scm.untracked_files()\n        if (dir_spec.match_file(f) and not outs_spec.match_file(f))\n    ]\n    if paths_to_track:\n        dvc_repo.scm.add(paths_to_track)\n"
  },
  {
    "path": "src/dvclive/env.py",
    "content": "DVCLIVE_LOGLEVEL = \"DVCLIVE_LOGLEVEL\"\nDVCLIVE_OPEN = \"DVCLIVE_OPEN\"\nDVCLIVE_RESUME = \"DVCLIVE_RESUME\"\nDVCLIVE_TEST = \"DVCLIVE_TEST\"\nDVC_EXP_BASELINE_REV = \"DVC_EXP_BASELINE_REV\"\nDVC_EXP_NAME = \"DVC_EXP_NAME\"\nDVC_ROOT = \"DVC_ROOT\"\n"
  },
  {
    "path": "src/dvclive/error.py",
    "content": "from typing import Any\n\n\nclass DvcLiveError(Exception):\n    pass\n\n\nclass InvalidDataTypeError(DvcLiveError):\n    def __init__(self, name, val):\n        self.name = name\n        self.val = val\n        super().__init__(f\"Data '{name}' has not supported type {val}\")\n\n\nclass InvalidDvcyamlError(DvcLiveError):\n    def __init__(self):\n        super().__init__(\"`dvcyaml` path must have filename 'dvc.yaml'\")\n\n\nclass InvalidImageNameError(DvcLiveError):\n    def __init__(self, name):\n        self.name = name\n        super().__init__(f\"Cannot log image with name '{name}'\")\n\n\nclass InvalidPlotTypeError(DvcLiveError):\n    def __init__(self, name):\n        from .plots import SKLEARN_PLOTS\n\n        self.name = name\n        super().__init__(\n            f\"Plot type '{name}' is not supported.\"\n            f\"\\nSupported types are: {list(SKLEARN_PLOTS)}\"\n        )\n\n\nclass InvalidParameterTypeError(DvcLiveError):\n    def __init__(self, msg: Any):\n        super().__init__(msg)\n\n\nclass InvalidReportModeError(DvcLiveError):\n    def __init__(self, val):\n        super().__init__(\n            f\"`report` can only be `None`, `auto`, `html`, `notebook` or `md`. \"\n            f\"Got {val} instead.\"\n        )\n"
  },
  {
    "path": "src/dvclive/fabric.py",
    "content": "# mypy: disable-error-code=\"no-redef\"\nfrom argparse import Namespace\nfrom collections.abc import Mapping\nfrom typing import TYPE_CHECKING, Any, Optional, Union\n\ntry:\n    from lightning.fabric.loggers.logger import Logger, rank_zero_experiment\n    from lightning.fabric.utilities.logger import (\n        _add_prefix,\n        _convert_params,\n        _sanitize_callable_params,\n    )\n    from lightning.fabric.utilities.rank_zero import rank_zero_only\nexcept ImportError:\n    from lightning_fabric.loggers.logger import (  # type: ignore[assignment]\n        Logger,\n        rank_zero_experiment,\n    )\n    from lightning_fabric.utilities.logger import (\n        _add_prefix,\n        _convert_params,\n        _sanitize_callable_params,\n    )\n    from lightning_fabric.utilities.rank_zero import rank_zero_only\n\nfrom torch import is_tensor\n\nfrom dvclive.plots import Metric\nfrom dvclive.utils import standardize_metric_name\n\nif TYPE_CHECKING:\n    from dvclive import Live\n\n\nclass DVCLiveLogger(Logger):\n    LOGGER_JOIN_CHAR = \"/\"\n\n    def __init__(\n        self,\n        run_name: Optional[str] = None,\n        prefix: str = \"\",\n        experiment: Optional[\"Live\"] = None,\n        **kwargs: Any,\n    ):\n        super().__init__()\n        self._version = run_name\n        self._prefix = prefix\n        self._experiment = experiment\n        self._kwargs = kwargs\n\n    @property\n    def name(self) -> str:\n        return \"DvcLiveLogger\"\n\n    @property\n    def version(self) -> Union[int, str]:\n        if self._version is None:\n            self._version = \"\"\n        return self._version\n\n    @property\n    @rank_zero_experiment\n    def experiment(self) -> \"Live\":\n        if self._experiment is not None:\n            return self._experiment\n\n        assert (  # noqa: S101\n            rank_zero_only.rank == 0  # type: ignore[attr-defined]\n        ), \"tried to init DVCLive in non global_rank=0\"  # type: ignore[attr-defined]\n\n        from dvclive import Live\n\n        self._experiment = Live(**self._kwargs)\n\n        return self._experiment\n\n    @rank_zero_only\n    def log_metrics(\n        self,\n        metrics: Mapping[str, Union[int, float, str]],\n        step: Optional[int] = None,\n        sync: Optional[bool] = True,\n    ) -> None:\n        assert (  # noqa: S101\n            rank_zero_only.rank == 0  # type: ignore[attr-defined]\n        ), \"experiment tried to log from global_rank != 0\"\n\n        if step:\n            self.experiment.step = step\n        else:\n            self.experiment.step = self.experiment.step + 1\n\n        metrics = _add_prefix(metrics, self._prefix, self.LOGGER_JOIN_CHAR)  # type: ignore[assignment,arg-type]\n\n        for metric_name, metric_val in metrics.items():\n            val = metric_val\n            if is_tensor(val):  # type: ignore[unreachable]\n                val = val.cpu().detach().item()  # type: ignore[union-attr,unreachable]\n            name = standardize_metric_name(metric_name, __name__)\n            if Metric.could_log(val):\n                self.experiment.log_metric(name=name, val=val)\n            else:\n                raise ValueError(  # noqa: TRY003\n                    f\"\\n you tried to log {val} which is currently not supported.\"\n                    \"Try a scalar/tensor.\"\n                )\n\n        if sync:\n            self.experiment.sync()\n\n    @rank_zero_only\n    def log_hyperparams(self, params: Union[dict[str, Any], Namespace]) -> None:\n        \"\"\"Record hyperparameters.\n\n        Args:\n            params: a dictionary-like container with the hyperparameters\n        \"\"\"\n        params = _convert_params(params)\n        params = _sanitize_callable_params(params)\n        params = self._sanitize_params(params)\n        self.experiment.log_params(params)\n\n    @rank_zero_only\n    def finalize(self, status: str) -> None:  # noqa: ARG002\n        if self._experiment is not None:\n            self.experiment.end()\n\n    @staticmethod\n    def _sanitize_params(params: Union[dict[str, Any], Namespace]) -> dict[str, Any]:\n        from argparse import Namespace\n\n        # logging of arrays with dimension > 1 is not supported, sanitize as string\n        params = {\n            k: str(v) if hasattr(v, \"ndim\") and v.ndim > 1 else v\n            for k, v in params.items()\n        }\n\n        # logging of argparse.Namespace is not supported, sanitize as string\n        params = {\n            k: str(v) if isinstance(v, Namespace) else v for k, v in params.items()\n        }\n\n        return params  # noqa: RET504\n\n    def __getstate__(self) -> dict[str, Any]:\n        state = self.__dict__.copy()\n        state[\"_experiment\"] = None\n        return state\n"
  },
  {
    "path": "src/dvclive/fastai.py",
    "content": "import inspect\nfrom typing import Optional\n\nfrom fastai.callback.core import Callback\n\nfrom dvclive import Live\nfrom dvclive.utils import standardize_metric_name\n\n\ndef _inside_fine_tune():\n    \"\"\"\n    Hack to find out if fastai is calling `after_fit` at the end of the\n    \"freeze\" stage part of `learn.fine_tune` .\n    \"\"\"\n    fine_tune = False\n    fit_one_cycle = False\n    for frame in inspect.stack():\n        if frame.function == \"fine_tune\":\n            fine_tune = True\n        if frame.function == \"fit_one_cycle\":\n            fit_one_cycle = True\n        if fine_tune and fit_one_cycle:\n            return True\n    return False\n\n\nclass DVCLiveCallback(Callback):\n    def __init__(\n        self,\n        with_opt: bool = False,\n        live: Optional[Live] = None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.with_opt = with_opt\n        self.live = live if live is not None else Live(**kwargs)\n        self.freeze_stage_ended = False\n\n    def before_fit(self):\n        if hasattr(self, \"lr_finder\") or hasattr(self, \"gather_preds\"):\n            return\n        params = {\n            \"model\": type(self.learn.model).__qualname__,\n            \"batch_size\": getattr(self.dls, \"bs\", None),\n            \"batch_per_epoch\": len(getattr(self.dls, \"train\", [])),\n            \"frozen\": bool(getattr(self.opt, \"frozen_idx\", -1)),\n            \"frozen_idx\": getattr(self.opt, \"frozen_idx\", -1),\n            \"transforms\": f\"{getattr(self.dls, 'tfms', None)}\",\n        }\n        self.live.log_params(params)\n\n    def after_epoch(self):\n        if hasattr(self, \"lr_finder\") or hasattr(self, \"gather_preds\"):\n            return\n        logged_metrics = False\n        for key, value in zip(\n            self.learn.recorder.metric_names, self.learn.recorder.log\n        ):\n            if key == \"epoch\":\n                continue\n            self.live.log_metric(standardize_metric_name(key, __name__), float(value))\n            logged_metrics = True\n\n        # When resuming (i.e. passing `start_epoch` to learner)\n        # fast.ai calls after_epoch but we don't want to increase the step.\n        if logged_metrics:\n            self.live.next_step()\n\n    def after_fit(self):\n        if hasattr(self, \"lr_finder\") or hasattr(self, \"gather_preds\"):\n            return\n        if _inside_fine_tune() and not self.freeze_stage_ended:\n            self.freeze_stage_ended = True\n        else:\n            if hasattr(self, \"save_model\") and self.save_model.last_saved_path:\n                self.live.log_artifact(str(self.save_model.last_saved_path))\n            self.live.end()\n"
  },
  {
    "path": "src/dvclive/huggingface.py",
    "content": "# ruff: noqa: ARG002\nimport logging\nimport os\nfrom typing import Literal, Optional, Union\n\nfrom transformers import (\n    TrainerCallback,\n    TrainerControl,\n    TrainerState,\n    TrainingArguments,\n)\nfrom transformers.trainer import Trainer\n\nfrom dvclive import Live\nfrom dvclive.utils import standardize_metric_name\n\nlogger = logging.getLogger(\"dvclive\")\n\n\nclass DVCLiveCallback(TrainerCallback):\n    def __init__(\n        self,\n        live: Optional[Live] = None,\n        log_model: Optional[Union[Literal[\"all\"], bool]] = None,\n        **kwargs,\n    ):\n        logger.warning(\n            \"This callback is deprecated and will be removed in DVCLive 4.0\"\n            \" in favor of `transformers.integrations.DVCLiveCallback`\"\n            \" https://dvc.org/doc/dvclive/ml-frameworks/huggingface.\"\n        )\n        super().__init__()\n        self._log_model = log_model\n        self.live = live if live is not None else Live(**kwargs)\n\n    def on_train_begin(\n        self,\n        args: TrainingArguments,\n        state: TrainerState,\n        control: TrainerControl,\n        **kwargs,\n    ):\n        self.live.log_params(args.to_dict())\n\n    def on_log(\n        self,\n        args: TrainingArguments,\n        state: TrainerState,\n        control: TrainerControl,\n        **kwargs,\n    ):\n        logs = kwargs[\"logs\"]\n        for key, value in logs.items():\n            self.live.log_metric(standardize_metric_name(key, __name__), value)\n        self.live.next_step()\n\n    def on_save(\n        self,\n        args: TrainingArguments,\n        state: TrainerState,\n        control: TrainerControl,\n        **kwargs,\n    ):\n        if self._log_model == \"all\" and state.is_world_process_zero:\n            assert args.output_dir is not None  # noqa: S101\n            self.live.log_artifact(args.output_dir)\n\n    def on_train_end(\n        self,\n        args: TrainingArguments,\n        state: TrainerState,\n        control: TrainerControl,\n        **kwargs,\n    ):\n        if self._log_model is True and state.is_world_process_zero:\n            fake_trainer = Trainer(\n                args=args,\n                model=kwargs.get(\"model\"),\n                tokenizer=kwargs.get(\"tokenizer\"),\n                eval_dataset=[\"fake\"],\n            )\n            name = \"best\" if args.load_best_model_at_end else \"last\"\n            assert args.output_dir is not None  # noqa: S101\n            output_dir = os.path.join(args.output_dir, name)\n            fake_trainer.save_model(output_dir)\n            self.live.log_artifact(output_dir, name=name, type=\"model\", copy=True)\n        self.live.end()\n"
  },
  {
    "path": "src/dvclive/keras.py",
    "content": "# ruff: noqa: ARG002\nfrom typing import Optional\n\nimport tensorflow as tf\n\nfrom dvclive import Live\nfrom dvclive.utils import standardize_metric_name\n\n\nclass DVCLiveCallback(tf.keras.callbacks.Callback):\n    def __init__(\n        self,\n        save_weights_only: bool = False,\n        live: Optional[Live] = None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.save_weights_only = save_weights_only\n        self.live = live if live is not None else Live(**kwargs)\n\n    def on_epoch_end(self, epoch: int, logs: Optional[dict] = None):\n        logs = logs or {}\n        for metric, value in logs.items():\n            self.live.log_metric(standardize_metric_name(metric, __name__), value)\n        self.live.next_step()\n\n    def on_train_end(self, logs: Optional[dict] = None):\n        self.live.end()\n"
  },
  {
    "path": "src/dvclive/lgbm.py",
    "content": "from typing import Optional\n\nfrom dvclive import Live\n\n\nclass DVCLiveCallback:\n    def __init__(self, live: Optional[Live] = None, **kwargs):\n        super().__init__()\n        self.live = live if live is not None else Live(**kwargs)\n\n    def __call__(self, env):\n        multi_eval = len(env.evaluation_result_list) > 1\n        for eval_result in env.evaluation_result_list:\n            data_name, eval_name, result = eval_result[:3]\n            self.live.log_metric(\n                f\"{data_name}/{eval_name}\" if multi_eval else eval_name, result\n            )\n        self.live.next_step()\n"
  },
  {
    "path": "src/dvclive/lightning.py",
    "content": "# mypy: disable-error-code=\"no-redef\"\nimport inspect\nfrom collections.abc import Mapping\nfrom pathlib import Path\nfrom typing import Optional, Union\n\nfrom typing_extensions import override\n\ntry:\n    from lightning.pytorch.callbacks.model_checkpoint import ModelCheckpoint\n    from lightning.pytorch.loggers.logger import Logger\n    from lightning.pytorch.loggers.utilities import _scan_checkpoints\n    from lightning.pytorch.utilities import rank_zero_only\nexcept ImportError:\n    from pytorch_lightning.callbacks.model_checkpoint import (  # type: ignore[assignment]\n        ModelCheckpoint,\n    )\n    from pytorch_lightning.loggers.logger import Logger  # type: ignore[assignment]\n    from pytorch_lightning.utilities import rank_zero_only\n\n    try:\n        from pytorch_lightning.utilities.logger import _scan_checkpoints\n    except ImportError:\n        from pytorch_lightning.loggers.utilities import (  # type: ignore[assignment]\n            _scan_checkpoints,\n        )\n\n\nfrom dvclive.fabric import DVCLiveLogger as FabricDVCLiveLogger\n\n\ndef _should_sync():\n    \"\"\"\n    Find out if pytorch_lightning is calling `log_metrics` from the functions\n    where we actually want to sync.\n    For example, prevents calling sync when external callbacks call\n    `log_metrics` or during the multiple `update_eval_step_metrics`.\n    \"\"\"\n    return any(\n        frame.function\n        in (\n            \"update_train_step_metrics\",\n            \"update_train_epoch_metrics\",\n            \"log_eval_end_metrics\",\n        )\n        for frame in inspect.stack()\n    )\n\n\nclass DVCLiveLogger(Logger, FabricDVCLiveLogger):\n    def __init__(\n        self,\n        run_name: Optional[str] = \"dvclive_run\",\n        prefix=\"\",\n        log_model: Union[str, bool] = False,\n        experiment=None,\n        **kwargs,\n    ):\n        super().__init__(\n            run_name=run_name,\n            prefix=prefix,\n            experiment=experiment,\n            **kwargs,\n        )\n        self._log_model = log_model\n        self._logged_model_time: dict[str, float] = {}\n        self._checkpoint_callback: Optional[ModelCheckpoint] = None\n        self._all_checkpoint_paths: list[str] = []\n\n    @rank_zero_only\n    def log_metrics(\n        self,\n        metrics: Mapping[str, Union[int, float, str]],\n        step: Optional[int] = None,\n        sync: Optional[bool] = False,\n    ) -> None:\n        if not sync and _should_sync():\n            sync = True\n        super().log_metrics(metrics, step, sync)\n\n    def after_save_checkpoint(self, checkpoint_callback: ModelCheckpoint) -> None:\n        if self._log_model in [True, \"all\"]:\n            self._checkpoint_callback = checkpoint_callback\n            self._scan_checkpoints(checkpoint_callback)\n        if self._log_model == \"all\" or (\n            self._log_model is True and checkpoint_callback.save_top_k == -1\n        ):\n            self._save_checkpoints(checkpoint_callback)\n\n    @override\n    @rank_zero_only\n    def finalize(self, status: str) -> None:\n        # Log best model.\n        if self._checkpoint_callback:\n            self._scan_checkpoints(self._checkpoint_callback)\n            self._save_checkpoints(self._checkpoint_callback)\n            best_model_path = self._checkpoint_callback.best_model_path\n            self.experiment.log_artifact(\n                best_model_path, name=\"best\", type=\"model\", copy=True\n            )\n        super().finalize(status)\n\n    def _scan_checkpoints(self, checkpoint_callback: ModelCheckpoint) -> None:\n        # get checkpoints to be saved with associated score\n        checkpoints = _scan_checkpoints(checkpoint_callback, self._logged_model_time)\n\n        # update model time and append path to list of all checkpoints\n        for t, p, _, _ in checkpoints:\n            self._logged_model_time[p] = t\n            self._all_checkpoint_paths.append(p)\n\n    def _save_checkpoints(self, checkpoint_callback: ModelCheckpoint) -> None:\n        # drop unused checkpoints\n        if not self.experiment._resume and checkpoint_callback.dirpath:  # noqa: SLF001\n            for p in Path(checkpoint_callback.dirpath).iterdir():\n                if str(p) not in self._all_checkpoint_paths:\n                    p.unlink(missing_ok=True)\n\n        # save directory\n        self.experiment.log_artifact(checkpoint_callback.dirpath)\n"
  },
  {
    "path": "src/dvclive/live.py",
    "content": "import builtins\nimport glob\nimport json\nimport logging\nimport math\nimport os\nimport queue\nimport shutil\nimport tempfile\nimport threading\nfrom pathlib import Path, PurePath\nfrom typing import TYPE_CHECKING, Any, Literal, Optional, Union\n\nif TYPE_CHECKING:\n    import matplotlib as mpl\n    import numpy as np\n    import pandas as pd\n    import PIL\n    from dvc.repo import Repo\n    from IPython.display import DisplayHandle\n\nfrom dvc.exceptions import DvcException\nfrom dvc.utils.studio import get_repo_url, get_subrepo_relpath\nfrom funcy import set_in\nfrom ruamel.yaml.representer import RepresenterError\n\nfrom . import env\nfrom .dvc import (\n    ensure_dir_is_tracked,\n    find_overlapping_stage,\n    get_dvc_repo,\n    get_exp_name,\n    make_dvcyaml,\n)\nfrom .error import (\n    InvalidDataTypeError,\n    InvalidDvcyamlError,\n    InvalidImageNameError,\n    InvalidParameterTypeError,\n    InvalidPlotTypeError,\n    InvalidReportModeError,\n)\nfrom .monitor_system import _SystemMonitor\nfrom .plots import PLOT_TYPES, SKLEARN_PLOTS, CustomPlot, Image, Metric, NumpyEncoder\nfrom .report import BLANK_NOTEBOOK_REPORT, make_report\nfrom .serialize import dump_json, dump_yaml, load_yaml\nfrom .studio import get_dvc_studio_config, post_to_studio\nfrom .utils import (\n    StrPath,\n    catch_and_warn,\n    clean_and_copy_into,\n    convert_datapoints_to_list_of_dicts,\n    env2bool,\n    inside_notebook,\n    matplotlib_installed,\n    open_file_in_browser,\n    parse_metrics,\n)\nfrom .vscode import (\n    cleanup_dvclive_step_completed,\n    mark_dvclive_only_ended,\n    mark_dvclive_only_started,\n    mark_dvclive_step_completed,\n)\n\nlogger = logging.getLogger(\"dvclive\")\nlogger.setLevel(os.getenv(env.DVCLIVE_LOGLEVEL, \"WARNING\").upper())\nhandler = logging.StreamHandler()\nformatter = logging.Formatter(\"%(levelname)s:%(name)s:%(message)s\")\nhandler.setFormatter(formatter)\nlogger.addHandler(handler)\n\nParamLike = Union[int, float, str, bool, list[\"ParamLike\"], dict[str, \"ParamLike\"]]\n\nNULL_SHA: str = \"0\" * 40\n\n\nclass Live:\n    def __init__(\n        self,\n        dir: str = \"dvclive\",  # noqa: A002\n        resume: bool = False,\n        report: Optional[Literal[\"md\", \"notebook\", \"html\"]] = None,\n        save_dvc_exp: bool = True,\n        dvcyaml: Union[str, os.PathLike, bool, None] = \"dvc.yaml\",\n        cache_images: bool = False,\n        exp_name: Optional[str] = None,\n        exp_message: Optional[str] = None,\n        monitor_system: bool = False,\n    ):\n        \"\"\"\n        Initializes a DVCLive logger. A `Live()` instance is required in order to log\n        machine learning parameters, metrics and other metadata.\n        Warning: `Live()` will remove all existing DVCLive related files under dir\n        unless `resume=True`.\n\n        Args:\n            dir (str | Path): where to save DVCLive's outputs. Defaults to `\"dvclive\"`.\n            resume (bool): if `True`, DVCLive will try to read the previous step from\n                the metrics_file and start from that point. Defaults to `False`.\n            report (\"html\", \"md\", \"notebook\", None): any of `\"html\"`, `\"notebook\"`,\n                `\"md\"` or `None`. See `Live.make_report()`. Defaults to None.\n            save_dvc_exp (bool): if `True`, DVCLive will create a new DVC experiment as\n                part of `Live.end()`. Defaults to `True`. If you are using DVCLive\n                inside a DVC Pipeline and running with `dvc exp run`, the option will be\n                ignored.\n            dvcyaml (str | Path | None): where to write dvc.yaml file, which adds DVC\n                configuration for metrics, plots, and parameters as part of\n                `Live.next_step()` and `Live.end()`. If `None`, no dvc.yaml file is\n                written. Defaults to `\"dvc.yaml\"`. See `Live.make_dvcyaml()`.\n                If a string or Path like `\"subdir/dvc.yaml\"`, DVCLive will write the\n                configuration to that path (file must be named \"dvc.yaml\").\n                If `False`, DVCLive will not write to \"dvc.yaml\" (useful if you are\n                tracking DVCLive metrics, plots, and parameters independently and\n                want to avoid duplication).\n            cache_images (bool): if `True`, DVCLive will cache any images logged with\n                `Live.log_image()` as part of `Live.end()`. Defaults to `False`.\n                If running a DVC pipeline, `cache_images` will be ignored, and you\n                should instead cache images as pipeline outputs.\n            exp_name (str | None): if not `None`, and `save_dvc_exp` is `True`, the\n                provided string will be passed to `dvc exp save --name`.\n                If DVCLive is used inside `dvc exp run`, the option will be ignored, use\n                `dvc exp run --name` instead.\n            exp_message (str | None): if not `None`, and `save_dvc_exp` is `True`, the\n                provided string will be passed to `dvc exp save --message`.\n                If DVCLive is used inside `dvc exp run`, the option will be ignored, use\n                `dvc exp run --message` instead.\n            monitor_system (bool): if `True`, DVCLive will monitor GPU, CPU, ram, and\n                disk usage. Defaults to `False`.\n        \"\"\"\n        self.summary: dict[str, Any] = {}\n\n        self._dir: str = dir\n        self._resume: bool = resume or env2bool(env.DVCLIVE_RESUME)\n        self._save_dvc_exp: bool = save_dvc_exp\n        self._step: Optional[int] = None\n        self._metrics: dict[str, Any] = {}\n        self._images: dict[str, Image] = {}\n        self._params: dict[str, Any] = {}\n        self._plots: dict[str, Any] = {}\n        self._artifacts: dict[str, dict] = {}\n        self._inside_with = False\n        self._dvcyaml = dvcyaml\n        self._cache_images = cache_images\n\n        self._report_mode: Optional[str] = report\n        self._report_notebook: Optional[DisplayHandle] = None\n        self._init_report()\n\n        self._baseline_rev: str = os.getenv(env.DVC_EXP_BASELINE_REV, NULL_SHA)\n        self._exp_name: Optional[str] = exp_name or os.getenv(env.DVC_EXP_NAME)\n        self._exp_message: Optional[str] = exp_message\n        self._subdir: Optional[str] = None\n        self._repo_url: Optional[str] = None\n        self._experiment_rev: Optional[str] = None\n        self._inside_dvc_exp: bool = False\n        self._inside_dvc_pipeline: bool = False\n        self._dvc_repo: Optional[Repo] = None\n        self._include_untracked: list[str] = []\n        if env2bool(env.DVCLIVE_TEST):\n            self._init_test()\n        else:\n            self._init_dvc()\n\n        os.makedirs(self.dir, exist_ok=True)\n\n        if self._resume:\n            self._init_resume()\n        else:\n            self._init_cleanup()\n\n        self._latest_studio_step: int = self.step if resume else -1\n        self._studio_events_to_skip: set[str] = set()\n        self._dvc_studio_config: dict[str, Any] = {}\n        self._num_points_sent_to_studio: dict[str, int] = {}\n        self._studio_queue = None\n        self._init_studio()\n\n        self._system_monitor: Optional[_SystemMonitor] = None  # Monitoring thread\n        if monitor_system:\n            self.monitor_system()\n\n    def _init_resume(self):\n        self._read_params()\n        self.summary = self.read_latest()\n        self._step = self.read_step()\n        if self._step != 0:\n            logger.info(f\"Resuming from step {self._step}\")\n            self._step += 1\n        logger.debug(f\"{self._step=}\")\n\n    def _init_cleanup(self):\n        for plot_type in PLOT_TYPES:\n            shutil.rmtree(\n                Path(self.plots_dir) / plot_type.subfolder, ignore_errors=True\n            )\n\n        for f in (\n            self.metrics_file,\n            self.params_file,\n            os.path.join(self.dir, \"report.html\"),\n            os.path.join(self.dir, \"report.md\"),\n        ):\n            if f and os.path.exists(f):\n                os.remove(f)\n\n        for dvc_file in glob.glob(os.path.join(self.dir, \"**dvc.yaml\")):\n            os.remove(dvc_file)\n\n    @catch_and_warn(DvcException, logger)\n    def _init_dvc(self):  # noqa: C901\n        from dvc.scm import NoSCM\n\n        if os.getenv(env.DVC_ROOT, None):\n            self._inside_dvc_pipeline = True\n            self._init_dvc_pipeline()\n        self._dvc_repo = get_dvc_repo()\n\n        scm = self._dvc_repo.scm if self._dvc_repo else None\n        if isinstance(scm, NoSCM):\n            scm = None\n        if scm:\n            self._baseline_rev = scm.get_rev()\n        self._exp_name = get_exp_name(self._exp_name, scm, self._baseline_rev)\n        logger.info(f\"Logging to experiment '{self._exp_name}'\")\n\n        dvc_logger = logging.getLogger(\"dvc\")\n        dvc_logger.setLevel(os.getenv(env.DVCLIVE_LOGLEVEL, \"WARNING\").upper())\n\n        self._dvc_file = self._init_dvc_file()\n\n        if not scm:\n            if self._save_dvc_exp:\n                logger.warning(\n                    \"Can't save experiment without a Git Repo.\"\n                    \"\\nCreate a Git repo (`git init`) and commit (`git commit`).\"\n                )\n                self._save_dvc_exp = False\n            return\n        if scm.no_commits:\n            if self._save_dvc_exp:\n                logger.warning(\n                    \"Can't save experiment to an empty Git Repo.\"\n                    \"\\nAdd a commit (`git commit`) to save experiments.\"\n                )\n                self._save_dvc_exp = False\n            return\n\n        if self._dvcyaml and (\n            stage := find_overlapping_stage(self._dvc_repo, self.dvc_file)\n        ):\n            logger.warning(\n                f\"'{self.dvc_file}' is in outputs of stage '{stage.addressing}'.\"\n                \"\\nRemove it from outputs to make DVCLive work as expected.\"\n            )\n\n        if self._inside_dvc_pipeline:\n            return\n\n        self._subdir = get_subrepo_relpath(self._dvc_repo)\n        self._repo_url = get_repo_url(self._dvc_repo)\n\n        if self._save_dvc_exp:\n            mark_dvclive_only_started(self._exp_name)\n            self._include_untracked.append(self.dir)\n\n    def _init_dvc_file(self) -> str:\n        if self._dvcyaml is None:\n            return \"dvc.yaml\"\n        if isinstance(self._dvcyaml, bool):\n            return \"dvc.yaml\"\n\n        self._dvcyaml = os.fspath(self._dvcyaml)\n        if (\n            isinstance(self._dvcyaml, str)\n            and os.path.basename(self._dvcyaml) == \"dvc.yaml\"\n        ):\n            return self._dvcyaml\n\n        raise InvalidDvcyamlError\n\n    def _init_dvc_pipeline(self):\n        if os.getenv(env.DVC_EXP_BASELINE_REV, None):\n            # `dvc exp` execution\n            self._inside_dvc_exp = True\n            if self._save_dvc_exp:\n                logger.info(\"Ignoring `save_dvc_exp` because `dvc exp run` is running\")\n        # `dvc repro` execution\n        elif self._save_dvc_exp:\n            logger.warning(\n                \"Ignoring `save_dvc_exp` because `dvc repro` is running.\"\n                \"\\nUse `dvc exp run` to save experiment.\"\n            )\n        self._save_dvc_exp = False\n\n    def _init_studio(self):\n        self._dvc_studio_config = get_dvc_studio_config(self)\n        if not self._dvc_studio_config:\n            logger.debug(\"Skipping `studio` report.\")\n            self._studio_events_to_skip.add(\"start\")\n            self._studio_events_to_skip.add(\"data\")\n            self._studio_events_to_skip.add(\"done\")\n        elif self._inside_dvc_exp:\n            logger.debug(\"Skipping `studio` report `start` and `done` events.\")\n            self._studio_events_to_skip.add(\"start\")\n            self._studio_events_to_skip.add(\"done\")\n        else:\n            post_to_studio(self, \"start\")\n\n    def _init_report(self):\n        if self._report_mode not in {None, \"html\", \"notebook\", \"md\"}:\n            raise InvalidReportModeError(self._report_mode)\n        if self._report_mode == \"notebook\":\n            if inside_notebook():\n                from IPython.display import Markdown, display\n\n                self._report_mode = \"notebook\"\n                self._report_notebook = display(\n                    Markdown(BLANK_NOTEBOOK_REPORT), display_id=True\n                )\n            else:\n                logger.warning(\n                    \"Report mode 'notebook' requires to be\"\n                    \" inside a notebook. Disabling report.\"\n                )\n                self._report_mode = None\n        if self._report_mode in (\"notebook\", \"md\") and not matplotlib_installed():\n            logger.warning(\n                f\"Report mode '{self._report_mode}' requires 'matplotlib'\"\n                \" to be installed. Disabling report.\"\n            )\n            self._report_mode = None\n        logger.debug(f\"{self._report_mode=}\")\n\n    def _init_test(self):\n        \"\"\"\n        Enables a test mode that writes to temporary paths and doesn't depend on the\n        repository.\n\n        This is needed to run integration tests in external libraries, such as\n        HuggingFace Accelerate.\n        \"\"\"\n        with tempfile.TemporaryDirectory() as dirpath:\n            self._dir = os.path.join(dirpath, self._dir)\n            self._dvcyaml = os.fspath(self._dvcyaml)\n            if isinstance(self._dvcyaml, str):\n                self._dvc_file = os.path.join(dirpath, self._dvcyaml)\n            self._save_dvc_exp = False\n            logger.warning(\n                \"DVCLive testing mode enabled.\"\n                f\"Repo will be ignored and output will be written to {dirpath}.\"\n            )\n\n    @property\n    def dir(self) -> str:\n        \"\"\"Location of the directory to store outputs.\"\"\"\n        return self._dir\n\n    @property\n    def params_file(self) -> str:\n        return os.path.join(self.dir, \"params.yaml\")\n\n    @property\n    def metrics_file(self) -> str:\n        return os.path.join(self.dir, \"metrics.json\")\n\n    @property\n    def dvc_file(self) -> str:\n        \"\"\"Path for dvc.yaml file.\"\"\"\n        return self._dvc_file\n\n    @property\n    def plots_dir(self) -> str:\n        return os.path.join(self.dir, \"plots\")\n\n    @property\n    def artifacts_dir(self) -> str:\n        return os.path.join(self.dir, \"artifacts\")\n\n    @property\n    def report_file(self) -> Optional[str]:\n        if self._report_mode in (\"html\", \"md\"):\n            suffix = self._report_mode\n            return os.path.join(self.dir, f\"report.{suffix}\")\n        return None\n\n    @property\n    def step(self) -> int:\n        return self._step or 0\n\n    @step.setter\n    def step(self, value: int) -> None:\n        self._step = value\n        logger.debug(f\"Step: {self.step}\")\n\n    def monitor_system(\n        self,\n        interval: float = 0.05,  # seconds\n        num_samples: int = 20,\n        directories_to_monitor: Optional[dict[str, str]] = None,\n    ) -> None:\n        \"\"\"Monitor GPU, CPU, ram, and disk resources and log them to DVC Live.\n\n        Args:\n            interval (float): the time interval between samples in seconds. To keep the\n                sampling interval small, the maximum value allowed is 0.1 seconds.\n                Default to 0.05.\n            num_samples (int): the number of samples to collect before the aggregation.\n                The value should be between 1 and 30 samples. Default to 20.\n            directories_to_monitor (Optional[Dict[str, str]]): a dictionary with the\n                information about which directories to monitor. The `key` would be the\n                name of the metric and the `value` is the path to the directory.\n                The metric tracked concerns the partition that contains the directory.\n                Default to `{\"main\": \"/\"}`.\n\n        Raises:\n            ValueError: if the keys in `directories_to_monitor` contains invalid\n                characters as defined by `os.path.normpath`.\n        \"\"\"\n        if directories_to_monitor is None:\n            directories_to_monitor = {\"main\": \"/\"}\n\n        if self._system_monitor is not None:\n            self._system_monitor.end()\n\n        self._system_monitor = _SystemMonitor(\n            live=self,\n            interval=interval,\n            num_samples=num_samples,\n            directories_to_monitor=directories_to_monitor,\n        )\n\n    def sync(self):\n        self.make_summary()\n\n        if self._dvcyaml:\n            self.make_dvcyaml()\n\n        self.make_report()\n\n        self.post_data_to_studio()\n\n    def next_step(self):\n        \"\"\"\n        Signals that the current iteration has ended and increases step value by one.\n        DVCLive uses `step` to track the history of the metrics logged with\n        `Live.log_metric()`.\n        You can use `Live.next_step()` to increase the step by one. In addition to\n        increasing the `step` number, it will call `Live.make_report()`,\n        `Live.make_dvcyaml()`, and `Live.make_summary()` by default.\n        \"\"\"\n        if self._step is None:\n            self._step = 0\n\n        self.sync()\n        mark_dvclive_step_completed(self.step)\n        self.step += 1\n\n    def log_metric(\n        self,\n        name: str,\n        val: Union[float, str],\n        timestamp: bool = False,\n        plot: bool = True,\n    ):\n        \"\"\"\n        On each `Live.log_metric(name, val)` call `DVCLive` will create a metrics\n        history file in `{Live.plots_dir}/metrics/{name}.tsv`. Each subsequent call to\n        `Live.log_metric(name, val)` will add a new row to\n        `{Live.plots_dir}/metrics/{name}.tsv`. In addition, `DVCLive` will store the\n        latest value logged in `Live.summary`, so it can be serialized with calls to\n        `live.make_summary()`, `live.next_step()` or when exiting the `Live` context\n        block.\n\n        Args:\n            name (str): name of the metric being logged.\n            val (int | float | str): the value to be logged.\n            timestamp (bool): whether to automatically log timestamp in the metrics\n                history file.\n            plot (bool): whether to add the metric value to the metrics history file for\n                plotting. If `False`, the metric will only be saved to the metrics\n                summary.\n\n        Raises:\n            `InvalidDataTypeError`: thrown if the provided `val` does not have a\n                supported type.\n        \"\"\"\n        if not Metric.could_log(val):\n            raise InvalidDataTypeError(name, type(val))\n\n        if not isinstance(val, str) and (math.isnan(val) or math.isinf(val)):\n            val = str(val)\n\n        if name in self._metrics:\n            metric = self._metrics[name]\n        else:\n            metric = Metric(name, self.plots_dir)\n            self._metrics[name] = metric\n\n        metric.step = self.step\n        if plot:\n            metric.dump(val, timestamp=timestamp)\n\n        self.summary = set_in(self.summary, metric.summary_keys, val)\n        logger.debug(f\"Logged {name}: {val}\")\n\n    def log_image(\n        self,\n        name: str,\n        val: \"Union[np.ndarray, mpl.figure.Figure, PIL.Image.Image, StrPath]\",\n    ):\n        \"\"\"\n        Saves the given image `val` to the output file `name`.\n\n        Supported values for val are:\n        - A valid NumPy array (convertible to an image via `PIL.Image.fromarray`)\n        - A `matplotlib.figure.Figure` instance\n        - A `PIL.Image` instance\n        - A path to an image file (`str` or `Path`). It should be in a format that is\n        readable by `PIL.Image.open()`\n\n        The images will be saved in `{Live.plots_dir}/images/{name}`. When using\n        `Live(cache_images=True)`, the images directory will also be cached as part of\n        `Live.end()`. In that case, a `.dvc` file will be saved to track it, and the\n        directory will be added to a `.gitignore` file to prevent Git tracking.\n\n        By default the images will be overwritten on each step. However, you can log\n        images using the following pattern\n        `live.log_image(f\"folder/{live.step}.png\", img)`.\n        In `DVC Studio` and the `DVC Extension for VSCode`, folders following this\n        pattern will be rendered using an image slider.\n\n        Args:\n            name (str): name of the image file that this command will output\n            val (np.ndarray | matplotlib.figure.Figure | PIL.Image | StrPath):\n                image to be saved. See the list of supported values in the description.\n\n        Raises:\n            `InvalidDataTypeError`: thrown if the provided `val` does not have a\n                supported type.\n        \"\"\"\n        if not Image.could_log(val):\n            raise InvalidDataTypeError(name, type(val))\n\n        # If we're given a path, try loading the image first. This might error out.\n        if isinstance(val, (str, PurePath)):\n            from PIL import Image as ImagePIL\n\n            suffix = Path(val).suffix\n            if not Path(name).suffix and suffix in Image.suffixes:\n                name = f\"{name}{suffix}\"\n\n            val = ImagePIL.open(val)\n\n        # See if the image name is valid\n        if Path(name).suffix not in Image.suffixes:\n            raise InvalidImageNameError(name)\n\n        if name in self._images:\n            image = self._images[name]\n        else:\n            image = Image(name, self.plots_dir)\n            self._images[name] = image\n\n        image.step = self.step\n        image.dump(val)\n        logger.debug(f\"Logged {name}: {val}\")\n\n    def log_plot(\n        self,\n        name: str,\n        datapoints: \"Union[pd.DataFrame, np.ndarray, list[dict]]\",\n        x: str,\n        y: Union[str, list[str]],\n        template: Optional[str] = \"linear\",\n        title: Optional[str] = None,\n        x_label: Optional[str] = None,\n        y_label: Optional[str] = None,\n    ):\n        \"\"\"\n        The method will dump the provided datapoints to\n        `{Live.dir}/plots/custom/{name}.json`and store the provided properties to be\n        included in the plots section written by `Live.make_dvcyaml()`. The plot can be\n        rendered with `DVC CLI`, `VSCode Extension` or `DVC Studio`.\n\n        Args:\n            name (StrPath): name of the output file.\n            datapoints (pd.DataFrame | np.ndarray | List[Dict]): Pandas DataFrame, Numpy\n                Array or List of dictionaries containing the data for the plot.\n            x (str): name of the key (present in the dictionaries) to use as the x axis.\n            y (str | list[str]): name of the key or keys (present in the\n                dictionaries) to use the y axis.\n            template (str): name of the `DVC plots template` to use. Defaults to\n                `\"linear\"`.\n            title (str): title to be displayed. Defaults to\n                `\"{Live.dir}/plots/custom/{name}.json\"`.\n            x_label (str): label for the x axis. Defaults to the name passed as `x`.\n            y_label (str): label for the y axis. Defaults to the name passed as `y`.\n\n        Raises:\n            `InvalidDataTypeError`: thrown if the provided `datapoints` does not have a\n                supported type.\n        \"\"\"\n        # Convert the given datapoints to List[Dict]\n        datapoints = convert_datapoints_to_list_of_dicts(datapoints=datapoints)\n\n        if not CustomPlot.could_log(datapoints):\n            raise InvalidDataTypeError(name, type(datapoints))\n\n        if name in self._plots:\n            plot = self._plots[name]\n        else:\n            plot = CustomPlot(\n                name,\n                self.plots_dir,\n                x=x,\n                y=y,\n                template=template,\n                title=title,\n                x_label=x_label,\n                y_label=y_label,\n            )\n            self._plots[name] = plot\n\n        plot.step = self.step\n        plot.dump(datapoints)\n        logger.debug(f\"Logged {name}\")\n\n    def log_sklearn_plot(\n        self,\n        kind: str,\n        labels: \"Union[list, np.ndarray]\",\n        predictions: \"Union[list, tuple, np.ndarray]\",\n        name: Optional[str] = None,\n        title: Optional[str] = None,\n        x_label: Optional[str] = None,\n        y_label: Optional[str] = None,\n        normalized: Optional[bool] = None,\n        **kwargs,\n    ):\n        \"\"\"\n        Generates a scikit learn plot and saves the data in\n        `{Live.dir}/plots/sklearn/{name}.json`. The method will compute and dump the\n        `kind` plot to `{Live.dir}/plots/sklearn/{name}` in a format compatible with\n        dvc plots. It will also store the provided properties to be included in the\n        plots section written by `Live.make_dvcyaml()`.\n\n        Args:\n            kind (\"calibration\" | \"confusion_matrix\" | \"det\" | \"precision_recall\" |\n                \"roc\"): a supported plot type.\n            labels (List | np.ndarray): array of ground truth labels.\n            predictions (List | np.ndarray): array of predicted labels (for\n                `\"confusion_matrix\"`) or predicted probabilities (for other plots).\n            name (str): optional name of the output file. If not provided, `kind` will\n                be used as name.\n            title (str): optional title to be displayed.\n            x_label (str): optional label for the x axis.\n            y_label (str): optional label for the y axis.\n            normalized (bool): optional, `confusion_matrix` with values normalized to\n                `<0, 1>` range.\n            kwargs: additional arguments to tune the result. Arguments are passed to the\n                scikit-learn function (e.g. `drop_intermediate=True` for the `\"roc\"`\n                type).\n        Raises:\n            InvalidPlotTypeError: thrown if the provided `kind` does not correspond to\n                any of the supported plots.\n        \"\"\"\n        val = (labels, predictions)\n\n        plot_config = {\n            k: v\n            for k, v in {\n                \"title\": title,\n                \"x_label\": x_label,\n                \"y_label\": y_label,\n                \"normalized\": normalized,\n            }.items()\n            if v is not None\n        }\n\n        name = name or kind\n        if name in self._plots:\n            plot = self._plots[name]\n        elif kind in SKLEARN_PLOTS and SKLEARN_PLOTS[kind].could_log(val):\n            plot = SKLEARN_PLOTS[kind](name, self.plots_dir, **plot_config)\n            self._plots[plot.name] = plot\n        else:\n            raise InvalidPlotTypeError(name)\n\n        plot.step = self.step\n        plot.dump(val, **kwargs)\n        logger.debug(f\"Logged {name}\")\n\n    def _read_params(self):\n        if os.path.isfile(self.params_file):\n            params = load_yaml(self.params_file)\n            self._params.update(params)\n\n    def _dump_params(self):\n        try:\n            dump_yaml(self._params, self.params_file)\n        except RepresenterError as exc:\n            raise InvalidParameterTypeError(exc.args[0]) from exc\n\n    def log_params(self, params: dict[str, ParamLike]):\n        \"\"\"\n        On each `Live.log_params(params)` call, DVCLive will write keys/values pairs in\n        the params dict to `{Live.dir}/params.yaml`:\n\n        Also see `Live.log_param()`.\n\n        Args:\n            params (Dict[str, ParamLike]): dictionary with name/value pairs of\n                parameters to be logged.\n\n        Raises:\n            `InvalidParameterTypeError`: thrown if the parameter value is not among\n                supported types.\n        \"\"\"\n        self._params.update(params)\n        self._dump_params()\n        logger.debug(f\"Logged {params} parameters to {self.params_file}\")\n\n    def log_param(self, name: str, val: ParamLike):\n        \"\"\"\n        On each `Live.log_param(name, val)` call, DVCLive will write the name parameter\n        to `{Live.dir}/params.yaml` with the corresponding `val`.\n\n        Also see `Live.log_params()`.\n\n        Args:\n            name (str): name of the parameter being logged.\n            val (ParamLike): the value to be logged.\n\n        Raises:\n            `InvalidParameterTypeError`: thrown if the parameter value is not among\n                supported types.\n        \"\"\"\n        self.log_params({name: val})\n\n    def log_artifact(\n        self,\n        path: StrPath,\n        type: Optional[str] = None,  # noqa: A002\n        name: Optional[str] = None,\n        desc: Optional[str] = None,\n        labels: Optional[list[str]] = None,\n        meta: Optional[dict[str, Any]] = None,\n        copy: bool = False,\n        cache: bool = True,\n    ):\n        \"\"\"\n        Tracks an existing directory or file with DVC.\n\n        Log path, saving its contents to DVC storage. Also annotate with any included\n        metadata fields (for example, to be consumed in the model registry or automation\n        scenarios).\n        If `cache=True` (which is the default), uses `dvc add` to track path with DVC,\n        saving it to the DVC cache and generating a `{path}.dvc` file that acts as a\n        pointer to the cached data.\n        If you include any of the optional metadata fields (type, name, desc, labels,\n        meta), it will add an artifact and all the metadata passed as arguments to the\n        corresponding `dvc.yaml` (unless `dvcyaml=None`). Passing `type=\"model\"` will\n        include it in the model registry.\n\n        Args:\n            path (StrPath): an existing directory or file.\n            type (Optional[str]): an optional type of the artifact. Common types are\n                `\"model\"` or `\"dataset\"`.\n            name (Optional[str]): an optional custom name of an artifact.\n                If not provided the `path` stem (last part of the path without the\n                file extension) will be used as the artifact name.\n            desc (Optional[str]): an optional description of an artifact.\n            labels (Optional[List[str]]): optional labels describing the artifact.\n            meta (Optional[Dict[str, Any]]): optional metainformation in `key: value`\n                format.\n            copy (bool): copy a directory or file at path into the `dvclive/artifacts`\n                location (default) before tracking it. The new path is used instead of\n                the original one to track the artifact. Useful if you don't want to\n                track the original path in your repo (for example, it is outside the\n                repo or in a Git-ignored directory).\n            cache (bool): cache the files with DVC to track them outside of Git.\n                Defaults to `True`, but set to `False` if you want to annotate metadata\n                about the artifact without storing a copy in the DVC cache.\n                If running a DVC pipeline, `cache` will be ignored, and you should\n                instead cache artifacts as pipeline outputs.\n\n        Raises:\n            `InvalidDataTypeError`: thrown if the provided `path` does not have a\n                supported type.\n        \"\"\"\n        if not isinstance(path, (str, PurePath)):\n            raise InvalidDataTypeError(path, builtins.type(path))\n\n        if self._dvc_repo is not None:\n            from gto.constants import assert_name_is_valid\n            from gto.exceptions import ValidationError\n\n            if copy:\n                path = clean_and_copy_into(path, self.artifacts_dir)\n\n            if cache:\n                self.cache(path)\n\n            if any((type, name, desc, labels, meta)):\n                name = name or Path(path).stem\n                try:\n                    assert_name_is_valid(name)\n                    self._artifacts[name] = {\n                        k: v\n                        for k, v in locals().items()\n                        if k in (\"path\", \"type\", \"desc\", \"labels\", \"meta\")\n                        and v is not None\n                    }\n                except ValidationError:\n                    logger.warning(\n                        \"Can't use '%s' as artifact name (ID).\"\n                        \" It will not be included in the `artifacts` section.\",\n                        name,\n                    )\n        else:\n            logger.warning(\n                \"A DVC repo is required to log artifacts. \"\n                f\"Skipping `log_artifact({path})`.\"\n            )\n\n    @catch_and_warn(DvcException, logger)\n    def cache(self, path):\n        if self._inside_dvc_pipeline:\n            existing_stage = find_overlapping_stage(self._dvc_repo, path)\n\n            if existing_stage:\n                if existing_stage.cmd:\n                    logger.info(\n                        f\"Skipping `dvc add {path}` because it is already being\"\n                        \" tracked automatically as an output of the DVC pipeline.\"\n                    )\n                    return  # skip caching\n                logger.warning(\n                    f\"To track '{path}' automatically in the DVC pipeline:\"\n                    f\"\\n1. Run `dvc remove {existing_stage.addressing}` \"\n                    \"to stop tracking it outside the pipeline.\"\n                    \"\\n2. Add it as an output of the pipeline stage.\"\n                )\n            else:\n                logger.warning(\n                    f\"To track '{path}' automatically in the DVC pipeline, \"\n                    \"add it as an output of the pipeline stage.\"\n                )\n\n        stage = self._dvc_repo.add(str(path))\n\n        dvc_file = stage[0].addressing\n\n        if self._save_dvc_exp:\n            self._include_untracked.append(dvc_file)\n            self._include_untracked.append(str(Path(dvc_file).parent / \".gitignore\"))\n\n    def make_summary(self):\n        \"\"\"\n        Serializes a summary of the logged metrics (`Live.summary`) to\n        `Live.metrics_file`.\n\n        The `Live.summary` object will contain the latest value of each metric logged\n        with `Live.log_metric()`. It can be also modified manually.\n\n        `Live.next_step()` and `Live.end()` will call `Live.make_summary()` internally,\n        so you don't need to call both.\n\n        The summary is usable by `dvc metrics`.\n        \"\"\"\n        if self._step is not None:\n            self.summary[\"step\"] = self.step\n        dump_json(self.summary, self.metrics_file, cls=NumpyEncoder)\n\n    def make_report(self):\n        \"\"\"\n        Generates a report from the logged data.\n\n        `Live.next_step()` and `Live.end()` will call `Live.make_report()` internally,\n        so you don't need to call both.\n\n        On each call, DVCLive will collect all the data logged in `{Live.dir}`, generate\n        a report and save it in `{Live.dir}/report.{format}`. The format can be HTML\n        or Markdown depending on the value of the `report` argument passed to `Live()`.\n        \"\"\"\n        if self._report_mode is not None:\n            make_report(self)\n            if self._report_mode == \"html\" and env2bool(env.DVCLIVE_OPEN):\n                open_file_in_browser(self.report_file)\n\n    @catch_and_warn(DvcException, logger)\n    def make_dvcyaml(self):\n        \"\"\"\n        Writes DVC configuration for metrics, plots, and parameters to `Live.dvc_file`.\n\n        Creates `dvc.yaml`, which describes and configures metrics, plots, and\n        parameters. DVC tools use this file to show reports and experiments tables.\n        `Live.next_step()` and `Live.end()` will call `Live.make_dvcyaml()` internally,\n        so you don't need to call both (unless `dvcyaml=None`).\n        \"\"\"\n        make_dvcyaml(self)\n\n    def _get_live_data(self) -> Optional[dict[str, Any]]:\n        params = load_yaml(self.params_file) if os.path.isfile(self.params_file) else {}\n        plots, metrics = parse_metrics(self)\n\n        # Plots can grow large, we don't want to keep in memory data\n        # that we 100% sent already\n        plots_to_send = {}\n        plots_start_idx = {}\n        for name, plot in plots.items():\n            num_points_sent = self._num_points_sent_to_studio.get(name, 0)\n            plots_to_send[name] = plot[num_points_sent:]\n            plots_start_idx[name] = num_points_sent\n\n        return {\n            \"params\": params,\n            \"plots\": plots_to_send,\n            \"plots_start_idx\": plots_start_idx,\n            \"metrics\": metrics,\n            \"images\": list(self._images.values()),\n            \"step\": self.step,\n        }\n\n    def post_data_to_studio(self):\n        if not self._studio_queue:\n            self._studio_queue = queue.Queue()\n\n            def worker():\n                error_occurred = False\n                while True:\n                    item, data = self._studio_queue.get()\n                    try:\n                        if not error_occurred:\n                            post_to_studio(item, \"data\", data)\n                    except Exception:\n                        logger.exception(\"Failed to post data to studio\")\n                        error_occurred = True\n                    finally:\n                        self._studio_queue.task_done()\n\n            threading.Thread(target=worker, daemon=True).start()\n\n        self._studio_queue.put((self, self._get_live_data()))\n\n    def _wait_for_studio_updates_posted(self):\n        if self._studio_queue:\n            logger.debug(\"Waiting for studio updates to be posted\")\n            self._studio_queue.join()\n\n    def end(self):\n        \"\"\"\n        Signals that the current experiment has ended.\n        `Live.end()` gets automatically called when exiting the context manager. It is\n        also called when the training ends for each of the supported ML Frameworks\n\n        By default, `Live.end()` will call `Live.make_summary()`, `Live.make_dvcyaml()`,\n        and `Live.make_report()`.\n\n        If `save_dvc_exp=True`, it will save a new DVC experiment and write a `dvc.yaml`\n        file configuring what DVC will show for logged plots, metrics, and parameters.\n        \"\"\"\n        if self._inside_with:\n            # Prevent `live.end` calls inside context manager\n            return\n\n        if self._images and self._cache_images:\n            images_path = Path(self.plots_dir) / Image.subfolder\n            self.cache(images_path)\n\n        # If next_step called before end, don't want to update step number\n        if \"step\" in self.summary:\n            self.step = self.summary[\"step\"]\n\n        # Kill threads that monitor the system metrics\n        if self._system_monitor is not None:\n            self._system_monitor.end()\n\n        self.sync()\n\n        if self._inside_dvc_exp and self._dvc_repo:\n            catch_and_warn(DvcException, logger)(ensure_dir_is_tracked)(\n                self.dir, self._dvc_repo\n            )\n            if self._dvcyaml:\n                catch_and_warn(DvcException, logger)(self._dvc_repo.scm.add)(\n                    self.dvc_file\n                )\n\n        self.save_dvc_exp()\n\n        self._wait_for_studio_updates_posted()\n\n        # Mark experiment as done\n        post_to_studio(self, \"done\")\n\n        cleanup_dvclive_step_completed()\n\n    def read_step(self):\n        latest = self.read_latest()\n        return latest.get(\"step\", 0)\n\n    def read_latest(self):\n        if Path(self.metrics_file).exists():\n            with open(self.metrics_file, encoding=\"utf-8\") as fobj:\n                return json.load(fobj)\n        return {}\n\n    def __enter__(self):\n        self._inside_with = True\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self._inside_with = False\n        self.end()\n\n    @catch_and_warn(DvcException, logger, mark_dvclive_only_ended)\n    def save_dvc_exp(self):\n        if self._save_dvc_exp:\n            if self._dvcyaml:\n                self._include_untracked.append(self.dvc_file)\n            self._experiment_rev = self._dvc_repo.experiments.save(\n                name=self._exp_name,\n                include_untracked=self._include_untracked,\n                force=True,\n                message=self._exp_message,\n            )\n"
  },
  {
    "path": "src/dvclive/monitor_system.py",
    "content": "import logging\nimport os\nfrom statistics import mean\nfrom threading import Event, Thread\nfrom typing import Union\n\nimport psutil\nfrom funcy import merge_with\n\ntry:\n    from pynvml import (\n        NVMLError,\n        nvmlDeviceGetCount,\n        nvmlDeviceGetHandleByIndex,\n        nvmlDeviceGetMemoryInfo,\n        nvmlDeviceGetUtilizationRates,\n        nvmlInit,\n        nvmlShutdown,\n    )\n\n    GPU_AVAILABLE = True\nexcept ImportError:\n    GPU_AVAILABLE = False\n\nlogger = logging.getLogger(\"dvclive\")\nGIGABYTES_DIVIDER = 1024.0**3\n\nMINIMUM_CPU_USAGE_TO_BE_ACTIVE = 20\n\nMETRIC_CPU_COUNT = \"system/cpu/count\"\nMETRIC_CPU_USAGE_PERCENT = \"system/cpu/usage (%)\"\nMETRIC_CPU_PARALLELIZATION_PERCENT = \"system/cpu/parallelization (%)\"\n\nMETRIC_RAM_USAGE_PERCENT = \"system/ram/usage (%)\"\nMETRIC_RAM_USAGE_GB = \"system/ram/usage (GB)\"\nMETRIC_RAM_TOTAL_GB = \"system/ram/total (GB)\"\n\nMETRIC_DISK_USAGE_PERCENT = \"system/disk/usage (%)\"\nMETRIC_DISK_USAGE_GB = \"system/disk/usage (GB)\"\nMETRIC_DISK_TOTAL_GB = \"system/disk/total (GB)\"\n\nMETRIC_GPU_COUNT = \"system/gpu/count\"\nMETRIC_GPU_USAGE_PERCENT = \"system/gpu/usage (%)\"\nMETRIC_VRAM_USAGE_PERCENT = \"system/vram/usage (%)\"\nMETRIC_VRAM_USAGE_GB = \"system/vram/usage (GB)\"\nMETRIC_VRAM_TOTAL_GB = \"system/vram/total (GB)\"\n\n\nclass _SystemMonitor:\n    _plot_blacklist_prefix: tuple = (\n        METRIC_CPU_COUNT,\n        METRIC_RAM_TOTAL_GB,\n        METRIC_DISK_TOTAL_GB,\n        METRIC_GPU_COUNT,\n        METRIC_VRAM_TOTAL_GB,\n    )\n\n    def __init__(\n        self,\n        live,\n        interval: float,  # seconds\n        num_samples: int,\n        directories_to_monitor: dict[str, str],\n    ):\n        self._live = live\n        self._interval = self._check_interval(interval, max_interval=0.1)\n        self._num_samples = self._check_num_samples(\n            num_samples, min_num_samples=1, max_num_samples=30\n        )\n        self._disks_to_monitor = self._check_directories_to_monitor(\n            directories_to_monitor\n        )\n        self._warn_cpu_problem = True\n        self._warn_gpu_problem = True\n        self._warn_disk_doesnt_exist: dict[str, bool] = {}\n\n        self._shutdown_event = Event()\n        Thread(\n            target=self._monitoring_loop,\n        ).start()\n\n    def _check_interval(self, interval: float, max_interval: float) -> float:\n        if interval > max_interval:\n            logger.warning(\n                f\"System monitoring `interval` should be less than {max_interval} \"\n                f\"seconds. Setting `interval` to {max_interval} seconds.\"\n            )\n            return max_interval\n        return interval\n\n    def _check_num_samples(\n        self, num_samples: int, min_num_samples: int, max_num_samples: int\n    ) -> int:\n        min_num_samples = 1\n        max_num_samples = 30\n        if not min_num_samples < num_samples < max_num_samples:\n            num_samples = max(min(num_samples, max_num_samples), min_num_samples)\n            logger.warning(\n                f\"System monitoring `num_samples` should be between {min_num_samples} \"\n                f\"and {max_num_samples}. Setting `num_samples` to {num_samples}.\"\n            )\n        return num_samples\n\n    def _check_directories_to_monitor(\n        self, directories_to_monitor: dict[str, str]\n    ) -> dict[str, str]:\n        disks_to_monitor = {}\n        for disk_name, disk_path in directories_to_monitor.items():\n            if disk_name != os.path.normpath(disk_name):\n                raise ValueError(  # noqa: TRY003\n                    \"Keys for `directories_to_monitor` should be a valid name\"\n                    f\", but got '{disk_name}'.\"\n                )\n            disks_to_monitor[disk_name] = disk_path\n        return disks_to_monitor\n\n    def _monitoring_loop(self):\n        while not self._shutdown_event.is_set():\n            self._metrics = {}\n            last_metrics = {}\n            for _ in range(self._num_samples):\n                try:\n                    last_metrics = self._get_metrics()\n                except psutil.Error:\n                    if self._warn_cpu_problem:\n                        logger.exception(\"Failed to monitor CPU metrics\")\n                        self._warn_cpu_problem = False\n                except NVMLError:\n                    if self._warn_gpu_problem:\n                        logger.exception(\"Failed to monitor GPU metrics\")\n                        self._warn_gpu_problem = False\n\n                self._metrics = merge_with(sum, self._metrics, last_metrics)\n                self._shutdown_event.wait(self._interval)\n                if self._shutdown_event.is_set():\n                    break\n            for name, values in self._metrics.items():\n                blacklisted = any(\n                    name.startswith(prefix) for prefix in self._plot_blacklist_prefix\n                )\n                self._live.log_metric(\n                    name,\n                    values / self._num_samples,\n                    timestamp=True,\n                    plot=None if blacklisted else True,\n                )\n\n    def _get_metrics(self) -> dict[str, Union[float, int]]:\n        return {\n            **self._get_gpu_info(),\n            **self._get_cpu_info(),\n            **self._get_ram_info(),\n            **self._get_disk_info(),\n        }\n\n    def _get_ram_info(self) -> dict[str, Union[float, int]]:\n        ram_info = psutil.virtual_memory()\n        return {\n            METRIC_RAM_USAGE_PERCENT: ram_info.percent,\n            METRIC_RAM_USAGE_GB: ram_info.used / GIGABYTES_DIVIDER,\n            METRIC_RAM_TOTAL_GB: ram_info.total / GIGABYTES_DIVIDER,\n        }\n\n    def _get_cpu_info(self) -> dict[str, Union[float, int]]:\n        num_cpus = psutil.cpu_count()\n        cpus_percent = psutil.cpu_percent(percpu=True)\n        return {\n            METRIC_CPU_COUNT: num_cpus,\n            METRIC_CPU_USAGE_PERCENT: mean(cpus_percent),\n            METRIC_CPU_PARALLELIZATION_PERCENT: len(\n                [\n                    percent\n                    for percent in cpus_percent\n                    if percent >= MINIMUM_CPU_USAGE_TO_BE_ACTIVE\n                ]\n            )\n            * 100\n            / num_cpus,\n        }\n\n    def _get_disk_info(self) -> dict[str, Union[float, int]]:\n        result = {}\n        for disk_name, disk_path in self._disks_to_monitor.items():\n            try:\n                disk_info = psutil.disk_usage(disk_path)\n            except OSError:\n                if self._warn_disk_doesnt_exist.get(disk_name, True):\n                    logger.warning(\n                        f\"Couldn't find directory '{disk_path}', ignoring it.\"\n                    )\n                    self._warn_disk_doesnt_exist[disk_name] = False\n                continue\n            disk_metrics = {\n                f\"{METRIC_DISK_USAGE_PERCENT}/{disk_name}\": disk_info.percent,\n                f\"{METRIC_DISK_USAGE_GB}/{disk_name}\": disk_info.used\n                / GIGABYTES_DIVIDER,\n                f\"{METRIC_DISK_TOTAL_GB}/{disk_name}\": disk_info.total\n                / GIGABYTES_DIVIDER,\n            }\n            disk_metrics = {k.rstrip(\"/\"): v for k, v in disk_metrics.items()}\n            result.update(disk_metrics)\n        return result\n\n    def _get_gpu_info(self) -> dict[str, Union[float, int]]:\n        if not GPU_AVAILABLE:\n            return {}\n\n        nvmlInit()\n        num_gpus = nvmlDeviceGetCount()\n        gpu_metrics = {\n            \"system/gpu/count\": num_gpus,\n        }\n\n        for gpu_idx in range(num_gpus):\n            gpu_handle = nvmlDeviceGetHandleByIndex(gpu_idx)\n            memory_info = nvmlDeviceGetMemoryInfo(gpu_handle)\n            usage_info = nvmlDeviceGetUtilizationRates(gpu_handle)\n\n            gpu_metrics.update(\n                {\n                    f\"{METRIC_GPU_USAGE_PERCENT}/{gpu_idx}\": (\n                        100 * usage_info.memory / usage_info.gpu\n                        if usage_info.gpu\n                        else 0\n                    ),\n                    f\"{METRIC_VRAM_USAGE_PERCENT}/{gpu_idx}\": (\n                        100 * memory_info.used / memory_info.total\n                    ),\n                    f\"{METRIC_VRAM_USAGE_GB}/{gpu_idx}\": (\n                        memory_info.used / GIGABYTES_DIVIDER\n                    ),\n                    f\"{METRIC_VRAM_TOTAL_GB}/{gpu_idx}\": (\n                        memory_info.total / GIGABYTES_DIVIDER\n                    ),\n                }\n            )\n        nvmlShutdown()\n        return gpu_metrics\n\n    def end(self):\n        self._shutdown_event.set()\n"
  },
  {
    "path": "src/dvclive/optuna.py",
    "content": "# ruff: noqa: ARG002\nfrom dvclive import Live\n\n\nclass DVCLiveCallback:\n    def __init__(self, metric_name=\"metric\", **kwargs) -> None:\n        kwargs[\"dir\"] = kwargs.get(\"dir\", \"dvclive-optuna\")\n        kwargs.pop(\"save_dvc_exp\", None)\n        self.metric_name = metric_name\n        self.live_kwargs = kwargs\n\n    def __call__(self, study, trial) -> None:\n        with Live(**self.live_kwargs) as live:\n            self._log_metrics(trial.values, live)\n            live.log_params(trial.params)\n\n    def _log_metrics(self, values, live):\n        if values is None:\n            return\n\n        if isinstance(self.metric_name, str):\n            if len(values) > 1:\n                # Broadcast default name for multi-objective optimization.\n                names = [f\"{self.metric_name}_{i}\" for i in range(len(values))]\n\n            else:\n                names = [self.metric_name]\n\n        elif len(self.metric_name) != len(values):\n            msg = (\n                \"Running multi-objective optimization \"\n                f\"with {len(values)} objective values, \"\n                f\"but {len(self.metric_name)} names specified. \"\n                \"Match objective values and names,\"\n                \"or use default broadcasting.\"\n            )\n            raise ValueError(msg)\n\n        else:\n            names = [*self.metric_name]\n\n        metrics = dict(zip(names, values))\n        for k, v in metrics.items():\n            live.summary[k] = v\n"
  },
  {
    "path": "src/dvclive/plots/__init__.py",
    "content": "from .custom import CustomPlot\nfrom .image import Image\nfrom .metric import Metric\nfrom .sklearn import Calibration, ConfusionMatrix, Det, PrecisionRecall, Roc\nfrom .utils import NumpyEncoder  # noqa: F401\n\nSKLEARN_PLOTS = {\n    \"calibration\": Calibration,\n    \"confusion_matrix\": ConfusionMatrix,\n    \"det\": Det,\n    \"precision_recall\": PrecisionRecall,\n    \"roc\": Roc,\n}\nPLOT_TYPES = (*SKLEARN_PLOTS.values(), Metric, Image, CustomPlot)\n"
  },
  {
    "path": "src/dvclive/plots/base.py",
    "content": "import abc\nfrom pathlib import Path\n\n\nclass Data(abc.ABC):\n    def __init__(self, name: str, output_folder: str) -> None:\n        self.name = name\n        self.output_folder: Path = Path(output_folder) / self.subfolder\n        self._step: int = -1\n\n    @property\n    def step(self) -> int:\n        return self._step\n\n    @step.setter\n    def step(self, val: int) -> None:\n        self._step = val\n\n    @property\n    @abc.abstractmethod\n    def output_path(self) -> Path:\n        pass\n\n    @property\n    @abc.abstractmethod\n    def subfolder(self):\n        pass\n\n    @staticmethod\n    @abc.abstractmethod\n    def could_log(val) -> bool:\n        pass\n\n    @abc.abstractmethod\n    def dump(self, val, **kwargs):\n        pass\n"
  },
  {
    "path": "src/dvclive/plots/custom.py",
    "content": "from pathlib import Path\nfrom typing import Optional, Union\n\nfrom dvclive.serialize import dump_json\n\nfrom .base import Data\n\n\nclass CustomPlot(Data):\n    suffixes = (\".json\",)\n    subfolder = \"custom\"\n\n    def __init__(\n        self,\n        name: str,\n        output_folder: str,\n        x: str,\n        y: Union[str, list[str]],\n        template: Optional[str],\n        title: Optional[str] = None,\n        x_label: Optional[str] = None,\n        y_label: Optional[str] = None,\n    ) -> None:\n        super().__init__(name, output_folder)\n        self.name = self.name.replace(\".json\", \"\")\n        if not template:\n            template = None\n\n        config = {\n            \"template\": template,\n            \"x\": x,\n            \"y\": y,\n            \"title\": title,\n            \"x_label\": x_label,\n            \"y_label\": y_label,\n        }\n        self._plot_config = {k: v for k, v in config.items() if v is not None}\n\n    @property\n    def output_path(self) -> Path:\n        _path = Path(f\"{self.output_folder / self.name}.json\")\n        _path.parent.mkdir(exist_ok=True, parents=True)\n        return _path\n\n    @staticmethod\n    def could_log(val: object) -> bool:\n        return isinstance(val, list) and all(isinstance(x, dict) for x in val)\n\n    @property\n    def plot_config(self):\n        return self._plot_config\n\n    def dump(self, val, **kwargs) -> None:  # noqa: ARG002\n        dump_json(val, self.output_path)\n"
  },
  {
    "path": "src/dvclive/plots/image.py",
    "content": "from pathlib import Path, PurePath\n\nfrom dvclive.utils import isinstance_without_import\n\nfrom .base import Data\n\n\nclass Image(Data):\n    suffixes = (\".jpg\", \".jpeg\", \".gif\", \".png\")\n    subfolder = \"images\"\n\n    @property\n    def output_path(self) -> Path:\n        _path = self.output_folder / self.name\n        _path.parent.mkdir(exist_ok=True, parents=True)\n        return _path\n\n    @staticmethod\n    def could_log(val: object) -> bool:\n        acceptable = {\n            (\"numpy\", \"ndarray\"),\n            (\"matplotlib.figure\", \"Figure\"),\n            (\"PIL.Image\", \"Image\"),\n        }\n        for cls in type(val).mro():\n            if any(isinstance_without_import(val, *cls) for cls in acceptable):\n                return True\n        return isinstance(val, (PurePath, str))\n\n    def dump(self, val, **kwargs) -> None:  # noqa: ARG002\n        if isinstance_without_import(val, \"numpy\", \"ndarray\"):\n            from PIL import Image as ImagePIL\n\n            ImagePIL.fromarray(val).save(self.output_path)\n        elif isinstance_without_import(val, \"matplotlib.figure\", \"Figure\"):\n            import matplotlib.pyplot as plt\n\n            val.savefig(self.output_path)\n            plt.close(val)\n        elif isinstance_without_import(val, \"PIL.Image\", \"Image\"):\n            val.save(self.output_path)\n"
  },
  {
    "path": "src/dvclive/plots/metric.py",
    "content": "import csv\nimport os\nimport time\nfrom pathlib import Path\n\nfrom .base import Data\nfrom .utils import NUMPY_SCALARS\n\n\nclass Metric(Data):\n    suffixes = (\".csv\", \".tsv\")\n    subfolder = \"metrics\"\n\n    @staticmethod\n    def could_log(val: object) -> bool:\n        if isinstance(val, (int, float, str)):\n            return True\n        return (\n            val.__class__.__module__ == \"numpy\"\n            and val.__class__.__name__ in NUMPY_SCALARS\n        )\n\n    @property\n    def output_path(self) -> Path:\n        _path = Path(f\"{self.output_folder / self.name}.tsv\")\n        _path.parent.mkdir(exist_ok=True, parents=True)\n        return _path\n\n    def dump(self, val, **kwargs) -> None:\n        row = {}\n        if kwargs.get(\"timestamp\", False):\n            row[\"timestamp\"] = int(time.time() * 1000)\n        row[\"step\"] = self.step\n        row[os.path.basename(self.name)] = val\n\n        existed = self.output_path.exists()\n        with open(self.output_path, \"a\", encoding=\"utf-8\", newline=\"\") as fobj:\n            writer = csv.DictWriter(\n                fobj, row.keys(), delimiter=\"\\t\", lineterminator=os.linesep\n            )\n            if not existed:\n                writer.writeheader()\n            writer.writerow(row)\n\n    @property\n    def summary_keys(self) -> list[str]:\n        return os.path.normpath(self.name).split(os.path.sep)\n"
  },
  {
    "path": "src/dvclive/plots/sklearn.py",
    "content": "from dvclive.serialize import dump_json\n\nfrom .custom import CustomPlot\n\n\nclass SKLearnPlot(CustomPlot):\n    subfolder = \"sklearn\"\n\n    @staticmethod\n    def could_log(val: object) -> bool:\n        return isinstance(val, tuple) and len(val) == 2  # noqa: PLR2004\n\n\nclass Roc(SKLearnPlot):\n    def __init__(self, name: str, output_folder: str, **plot_config) -> None:\n        plot_config[\"template\"] = plot_config.get(\"template\", \"simple\")\n        plot_config[\"title\"] = plot_config.get(\n            \"title\", \"Receiver operating characteristic (ROC)\"\n        )\n        plot_config[\"x_label\"] = plot_config.get(\"x_label\", \"False Positive Rate\")\n        plot_config[\"y_label\"] = plot_config.get(\"y_label\", \"True Positive Rate\")\n        plot_config[\"x\"] = \"fpr\"\n        plot_config[\"y\"] = \"tpr\"\n        super().__init__(name, output_folder, **plot_config)\n\n    def dump(self, val, **kwargs) -> None:\n        from sklearn import metrics\n\n        fpr, tpr, roc_thresholds = metrics.roc_curve(\n            y_true=val[0], y_score=val[1], **kwargs\n        )\n        roc = {\n            \"roc\": [\n                {\"fpr\": fp, \"tpr\": tp, \"threshold\": t}\n                for fp, tp, t in zip(fpr, tpr, roc_thresholds)\n            ]\n        }\n        dump_json(roc, self.output_path)\n\n\nclass PrecisionRecall(SKLearnPlot):\n    def __init__(self, name: str, output_folder: str, **plot_config) -> None:\n        plot_config[\"template\"] = plot_config.get(\"template\", \"simple\")\n        plot_config[\"title\"] = plot_config.get(\"title\", \"Precision-Recall Curve\")\n        plot_config[\"x_label\"] = plot_config.get(\"x_label\", \"Recall\")\n        plot_config[\"y_label\"] = plot_config.get(\"y_label\", \"Precision\")\n        plot_config[\"x\"] = \"recall\"\n        plot_config[\"y\"] = \"precision\"\n        super().__init__(name, output_folder, **plot_config)\n\n    def dump(self, val, **kwargs) -> None:\n        from sklearn import metrics\n\n        precision, recall, prc_thresholds = metrics.precision_recall_curve(\n            y_true=val[0], y_score=val[1], **kwargs\n        )\n\n        prc = {\n            \"precision_recall\": [\n                {\"precision\": p, \"recall\": r, \"threshold\": t}\n                for p, r, t in zip(precision, recall, prc_thresholds)\n            ]\n        }\n        dump_json(prc, self.output_path)\n\n\nclass Det(SKLearnPlot):\n    def __init__(self, name: str, output_folder: str, **plot_config) -> None:\n        plot_config[\"template\"] = plot_config.get(\"template\", \"simple\")\n        plot_config[\"title\"] = plot_config.get(\n            \"title\", \"Detection error tradeoff (DET)\"\n        )\n        plot_config[\"x_label\"] = plot_config.get(\"x_label\", \"False Positive Rate\")\n        plot_config[\"y_label\"] = plot_config.get(\"y_label\", \"False Negative Rate\")\n        plot_config[\"x\"] = \"fpr\"\n        plot_config[\"y\"] = \"fnr\"\n        super().__init__(name, output_folder, **plot_config)\n\n    def dump(self, val, **kwargs) -> None:\n        from sklearn import metrics\n\n        fpr, fnr, roc_thresholds = metrics.det_curve(\n            y_true=val[0], y_score=val[1], **kwargs\n        )\n\n        det = {\n            \"det\": [\n                {\"fpr\": fp, \"fnr\": fn, \"threshold\": t}\n                for fp, fn, t in zip(fpr, fnr, roc_thresholds)\n            ]\n        }\n        dump_json(det, self.output_path)\n\n\nclass ConfusionMatrix(SKLearnPlot):\n    def __init__(self, name: str, output_folder: str, **plot_config) -> None:\n        plot_config[\"template\"] = (\n            \"confusion_normalized\"\n            if plot_config.pop(\"normalized\", None)\n            else plot_config.get(\"template\", \"confusion\")\n        )\n        plot_config[\"title\"] = plot_config.get(\"title\", \"Confusion Matrix\")\n        plot_config[\"x_label\"] = plot_config.get(\"x_label\", \"True Label\")\n        plot_config[\"y_label\"] = plot_config.get(\"y_label\", \"Predicted Label\")\n        plot_config[\"x\"] = \"actual\"\n        plot_config[\"y\"] = \"predicted\"\n        super().__init__(name, output_folder, **plot_config)\n\n    def dump(self, val, **kwargs) -> None:  # noqa: ARG002\n        cm = [\n            {\"actual\": str(actual), \"predicted\": str(predicted)}\n            for actual, predicted in zip(val[0], val[1])\n        ]\n        dump_json(cm, self.output_path)\n\n\nclass Calibration(SKLearnPlot):\n    def __init__(self, name: str, output_folder: str, **plot_config) -> None:\n        plot_config[\"template\"] = plot_config.get(\"template\", \"simple\")\n        plot_config[\"title\"] = plot_config.get(\"title\", \"Calibration Curve\")\n        plot_config[\"x_label\"] = plot_config.get(\n            \"x_label\", \"Mean Predicted Probability\"\n        )\n        plot_config[\"y_label\"] = plot_config.get(\"y_label\", \"Fraction of Positives\")\n        plot_config[\"x\"] = \"prob_pred\"\n        plot_config[\"y\"] = \"prob_true\"\n        super().__init__(name, output_folder, **plot_config)\n\n    def dump(self, val, **kwargs) -> None:\n        from sklearn import calibration\n\n        prob_true, prob_pred = calibration.calibration_curve(\n            y_true=val[0], y_prob=val[1], **kwargs\n        )\n\n        _calibration = {\n            \"calibration\": [\n                {\"prob_true\": pt, \"prob_pred\": pp}\n                for pt, pp in zip(prob_true, prob_pred)\n            ]\n        }\n        dump_json(_calibration, self.output_path)\n"
  },
  {
    "path": "src/dvclive/plots/utils.py",
    "content": "import json\n\nNUMPY_INTS = [\n    \"intc\",\n    \"intp\",\n    \"int8\",\n    \"int16\",\n    \"int32\",\n    \"int64\",\n    \"uint8\",\n    \"uint16\",\n    \"uint32\",\n    \"uint64\",\n]\nNUMPY_FLOATS = [\"float16\", \"float32\", \"float64\"]\nNUMPY_SCALARS = NUMPY_INTS + NUMPY_FLOATS\n\n\nclass NumpyEncoder(json.JSONEncoder):\n    def default(self, o):\n        if o.__class__.__module__ == \"numpy\":\n            if o.__class__.__name__ in NUMPY_INTS:\n                return int(o)\n            if o.__class__.__name__ in NUMPY_FLOATS:\n                return float(o)\n        return super().default(o)\n"
  },
  {
    "path": "src/dvclive/py.typed",
    "content": ""
  },
  {
    "path": "src/dvclive/report.py",
    "content": "# ruff: noqa: SLF001\nimport base64\nimport json\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom dvc_render.html import render_html\nfrom dvc_render.image import ImageRenderer\nfrom dvc_render.markdown import render_markdown\nfrom dvc_render.table import TableRenderer\nfrom dvc_render.vega import VegaRenderer\n\nfrom dvclive.error import InvalidReportModeError\nfrom dvclive.plots import SKLEARN_PLOTS, CustomPlot, Image, Metric\nfrom dvclive.plots.sklearn import SKLearnPlot\nfrom dvclive.serialize import load_yaml\nfrom dvclive.utils import parse_tsv\n\nif TYPE_CHECKING:\n    from dvclive import Live\n\n\nBLANK_NOTEBOOK_REPORT = \"\"\"\n<div style=\"width: 100%;height: 700px;text-align: center\">\nDVCLive Report\n</div>\n\"\"\"\n\n\ndef get_scalar_renderers(metrics_path):\n    renderers = []\n    for suffix in Metric.suffixes:\n        for file in metrics_path.rglob(f\"*{suffix}\"):\n            data = parse_tsv(file)\n            for row in data:\n                row[\"rev\"] = \"workspace\"\n\n            name = file.relative_to(metrics_path.parent).with_suffix(\"\")\n            name = name.as_posix()\n            title = name.replace(metrics_path.name, \"\").strip(\"/\")\n            name = name.replace(metrics_path.name, \"static\")\n\n            properties = {\"x\": \"step\", \"y\": file.stem, \"title\": title}\n            renderers.append(VegaRenderer(data, name, **properties))\n    return renderers\n\n\ndef get_image_renderers(images_folder):\n    renderers = []\n    for suffix in Image.suffixes:\n        all_images = Path(images_folder).rglob(f\"*{suffix}\")\n        for file in sorted(all_images):\n            base64_str = base64.b64encode(file.read_bytes()).decode()\n            src = f\"data:image;base64,{base64_str}\"\n            name = str(file.relative_to(images_folder))\n            data = [\n                {\n                    ImageRenderer.SRC_FIELD: src,\n                    ImageRenderer.TITLE_FIELD: name,\n                }\n            ]\n            renderers.append(ImageRenderer(data, name))\n    return renderers\n\n\ndef get_custom_plot_renderers(plots_folder, live):\n    renderers = []\n    for suffix in CustomPlot.suffixes:\n        for file in Path(plots_folder).rglob(f\"*{suffix}\"):\n            name = file.relative_to(plots_folder).with_suffix(\"\").as_posix()\n\n            logged_plot = live._plots[name]\n            properties = logged_plot.plot_config\n\n            data = json.loads(file.read_text())\n\n            for row in data:\n                row[\"rev\"] = \"workspace\"\n\n            renderers.append(VegaRenderer(data, name, **properties))\n    return renderers\n\n\ndef get_sklearn_plot_renderers(plots_folder, live):\n    renderers = []\n    for suffix in SKLearnPlot.suffixes:\n        for file in Path(plots_folder).rglob(f\"*{suffix}\"):\n            name = file.relative_to(plots_folder).with_suffix(\"\").as_posix()\n            properties = {}\n\n            logged_plot = live._plots[name]\n            for default_name, plot_class in SKLEARN_PLOTS.items():\n                if isinstance(logged_plot, plot_class):\n                    properties = logged_plot.plot_config\n                    data_field = default_name\n                    break\n\n            data = json.loads(file.read_text())\n\n            if data_field in data:\n                data = data[data_field]\n\n            for row in data:\n                row[\"rev\"] = \"workspace\"\n\n            renderers.append(VegaRenderer(data, name, **properties))\n    return renderers\n\n\ndef get_metrics_renderers(dvclive_summary):\n    metrics_path = Path(dvclive_summary)\n    if metrics_path.exists():\n        return [\n            TableRenderer(\n                [json.loads(metrics_path.read_text(encoding=\"utf-8\"))],\n                metrics_path.name,\n            )\n        ]\n    return []\n\n\ndef get_params_renderers(dvclive_params):\n    params_path = Path(dvclive_params)\n    if params_path.exists():\n        return [\n            TableRenderer(\n                [load_yaml(params_path)],\n                params_path.name,\n            )\n        ]\n    return []\n\n\ndef make_report(live: \"Live\"):\n    plots_path = Path(live.plots_dir)\n\n    renderers = []\n    renderers.extend(get_params_renderers(live.params_file))\n    renderers.extend(get_metrics_renderers(live.metrics_file))\n    renderers.extend(get_scalar_renderers(plots_path / Metric.subfolder))\n    renderers.extend(get_image_renderers(plots_path / Image.subfolder))\n    renderers.extend(\n        get_sklearn_plot_renderers(plots_path / SKLearnPlot.subfolder, live)\n    )\n    renderers.extend(get_custom_plot_renderers(plots_path / CustomPlot.subfolder, live))\n\n    if live._report_mode == \"html\":\n        render_html(renderers, live.report_file, refresh_seconds=5)\n    elif live._report_mode == \"notebook\":\n        from IPython.display import Markdown\n\n        md = render_markdown(renderers)\n        if live._report_notebook is not None:\n            new_report = Markdown(md)  # type: ignore [assignment]\n            live._report_notebook.update(new_report)\n    elif live._report_mode == \"md\":\n        render_markdown(renderers, live.report_file)\n    else:\n        raise InvalidReportModeError(live._report_mode)\n"
  },
  {
    "path": "src/dvclive/serialize.py",
    "content": "import json\nimport os\nfrom collections import OrderedDict\n\nfrom dvclive.error import DvcLiveError\n\n\nclass YAMLError(DvcLiveError):\n    pass\n\n\nclass YAMLFileCorruptedError(YAMLError):\n    def __init__(self, path):\n        super().__init__(path, \"YAML file structure is corrupted\")\n\n\ndef load_yaml(path, typ=\"safe\"):\n    from ruamel.yaml import YAML\n    from ruamel.yaml import YAMLError as _YAMLError\n\n    yaml = YAML(typ=typ)\n    with open(path, encoding=\"utf-8\") as fd:\n        try:\n            return yaml.load(fd.read())\n        except _YAMLError:\n            raise YAMLFileCorruptedError(path) from _YAMLError\n\n\ndef get_yaml():\n    from ruamel.yaml import YAML\n\n    yaml = YAML()\n    yaml.default_flow_style = False\n\n    # tell Dumper to represent OrderedDict as normal dict\n    yaml_repr_cls = yaml.Representer\n    yaml_repr_cls.add_representer(OrderedDict, yaml_repr_cls.represent_dict)\n    return yaml\n\n\ndef dump_yaml(content, output_file):\n    yaml = get_yaml()\n    make_dir(output_file)\n    with open(output_file, \"w\", encoding=\"utf-8\") as fd:\n        yaml.dump(content, fd)\n\n\ndef dump_json(content, output_file, indent=4, **kwargs):\n    make_dir(output_file)\n    with open(output_file, \"w\", encoding=\"utf-8\") as f:\n        json.dump(content, f, indent=indent, **kwargs)\n        f.write(\"\\n\")\n\n\ndef make_dir(output_file):\n    output_dir = os.path.dirname(output_file)\n    if output_dir:\n        os.makedirs(output_dir, exist_ok=True)\n"
  },
  {
    "path": "src/dvclive/studio.py",
    "content": "# ruff: noqa: SLF001\nimport base64\nimport logging\nimport math\nimport os\nfrom collections.abc import Mapping\nfrom pathlib import PureWindowsPath\nfrom typing import TYPE_CHECKING, Any, Literal, Optional\n\nfrom dvc.exceptions import DvcException\nfrom dvc_studio_client.config import get_studio_config\nfrom dvc_studio_client.post_live_metrics import post_live_metrics\n\nfrom dvclive.utils import StrPath, rel_path\n\nfrom .utils import catch_and_warn\n\nif TYPE_CHECKING:\n    from dvclive.live import Live\n    from dvclive.plots.image import Image\n\nlogger = logging.getLogger(\"dvclive\")\n\n\ndef _cast_to_numbers(datapoints: Mapping):\n    for datapoint in datapoints:\n        for k, v in datapoint.items():\n            if k == \"step\":\n                datapoint[k] = int(v)\n            elif k == \"timestamp\":\n                continue\n            else:\n                float_v = float(v)\n                if math.isnan(float_v) or math.isinf(float_v):\n                    datapoint[k] = str(v)\n                else:\n                    datapoint[k] = float_v\n    return datapoints\n\n\ndef _adapt_path(live: \"Live\", name: StrPath):\n    if live._dvc_repo is not None:\n        name = rel_path(name, live._dvc_repo.root_dir)\n    if os.name == \"nt\":\n        name = str(PureWindowsPath(name).as_posix())\n    return name\n\n\ndef _adapt_image(image_path: StrPath):\n    with open(image_path, \"rb\") as fobj:\n        return base64.b64encode(fobj.read()).decode(\"utf-8\")\n\n\ndef _adapt_images(live: \"Live\", images: \"list[Image]\"):\n    return {\n        _adapt_path(live, image.output_path): {\"image\": _adapt_image(image.output_path)}\n        for image in images\n        if image.step > live._latest_studio_step\n    }\n\n\ndef _get_studio_updates(live: \"Live\", data: dict[str, Any]):\n    params = data[\"params\"]\n    plots = data[\"plots\"]\n    plots_start_idx = data[\"plots_start_idx\"]\n    metrics = data[\"metrics\"]\n    images = data[\"images\"]\n\n    params_file = live.params_file\n    params_file = _adapt_path(live, params_file)\n    params = {params_file: params}\n\n    metrics_file = live.metrics_file\n    metrics_file = _adapt_path(live, metrics_file)\n    metrics = {metrics_file: {\"data\": metrics}}\n\n    plots_to_send = {}\n    for name, plot in plots.items():\n        path = _adapt_path(live, name)\n        start_idx = plots_start_idx.get(name, 0)\n        num_points_sent = live._num_points_sent_to_studio.get(name, 0)\n        plots_to_send[path] = _cast_to_numbers(plot[num_points_sent - start_idx :])\n\n    plots_to_send = {k: {\"data\": v} for k, v in plots_to_send.items()}\n    plots_to_send.update(_adapt_images(live, images))\n\n    return metrics, params, plots_to_send\n\n\ndef get_dvc_studio_config(live: \"Live\"):\n    config = {}\n    if live._dvc_repo:\n        config = live._dvc_repo.config.get(\"studio\")\n    return get_studio_config(dvc_studio_config=config)\n\n\ndef increment_num_points_sent_to_studio(live, plots_sent, data):\n    for name in data[\"plots\"]:\n        path = _adapt_path(live, name)\n        plot = plots_sent.get(path, {})\n        if \"data\" in plot:\n            num_points_sent = live._num_points_sent_to_studio.get(name, 0)\n            live._num_points_sent_to_studio[name] = num_points_sent + len(plot[\"data\"])\n    return live\n\n\n@catch_and_warn(DvcException, logger)\ndef post_to_studio(  # noqa: C901\n    live: \"Live\",\n    event: Literal[\"start\", \"data\", \"done\"],\n    data: Optional[dict[str, Any]] = None,\n):\n    if event in live._studio_events_to_skip:\n        return\n\n    kwargs = {}\n    if event == \"start\":\n        if message := live._exp_message:\n            kwargs[\"message\"] = message\n        if subdir := live._subdir:\n            kwargs[\"subdir\"] = subdir\n    elif event == \"data\":\n        assert data is not None  # noqa: S101\n        metrics, params, plots = _get_studio_updates(live, data)\n        kwargs[\"step\"] = data[\"step\"]\n        kwargs[\"metrics\"] = metrics\n        kwargs[\"params\"] = params\n        kwargs[\"plots\"] = plots\n    elif event == \"done\" and live._experiment_rev:\n        kwargs[\"experiment_rev\"] = live._experiment_rev\n\n    response = post_live_metrics(\n        event,\n        live._baseline_rev,\n        live._exp_name,  # type: ignore[arg-type]\n        \"dvclive\",\n        dvc_studio_config=live._dvc_studio_config,\n        studio_repo_url=live._repo_url,\n        **kwargs,  # type: ignore[arg-type]\n    )\n\n    if not response:\n        logger.warning(f\"`post_to_studio` `{event}` failed.\")\n        if event == \"start\":\n            live._studio_events_to_skip.add(\"start\")\n            live._studio_events_to_skip.add(\"data\")\n            live._studio_events_to_skip.add(\"done\")\n    elif event == \"data\":\n        assert data is not None  # noqa: S101\n        live = increment_num_points_sent_to_studio(live, plots, data)\n        live._latest_studio_step = data[\"step\"]\n\n    if event == \"done\":\n        live._studio_events_to_skip.add(\"done\")\n        live._studio_events_to_skip.add(\"data\")\n"
  },
  {
    "path": "src/dvclive/utils.py",
    "content": "import csv\nimport json\nimport os\nimport re\nimport shutil\nimport webbrowser\nfrom pathlib import Path, PurePath\nfrom platform import uname\nfrom typing import TYPE_CHECKING, Union\n\nfrom .error import InvalidDataTypeError\n\nif TYPE_CHECKING:\n    import numpy as np\n    import pandas as pd\nelse:\n    try:\n        import pandas as pd\n    except ImportError:\n        pd = None\n\n    try:\n        import numpy as np\n    except ImportError:\n        np = None\n\n\nStrPath = Union[str, PurePath]\n\n\ndef run_once(f):\n    def wrapper(*args, **kwargs):\n        if not wrapper.has_run:\n            wrapper.has_run = True\n            return f(*args, **kwargs)\n        return None\n\n    wrapper.has_run = False\n    return wrapper\n\n\n@run_once\ndef open_file_in_browser(file) -> bool:\n    path = Path(file)\n    url = str(path) if \"Microsoft\" in uname().release else path.resolve().as_uri()\n\n    return webbrowser.open(url)\n\n\ndef env2bool(var, undefined=False):\n    \"\"\"\n    undefined: return value if env var is unset\n    \"\"\"\n    var = os.getenv(var, None)\n    if var is None:\n        return undefined\n    return bool(re.search(\"1|y|yes|true\", var, flags=re.IGNORECASE))\n\n\ndef standardize_metric_name(metric_name: str, framework: str) -> str:\n    \"\"\"Map framework-specific format to DVCLive standard.\n\n    Use `{split}/` as prefix in order to separate by subfolders.\n    Use `{train|eval}` as split name.\n    \"\"\"\n    if framework == \"dvclive.fastai\":\n        metric_name = metric_name.replace(\"train_\", \"train/\")\n        metric_name = metric_name.replace(\"valid_\", \"eval/\")\n\n    elif framework == \"dvclive.huggingface\":\n        for split in (\"train\", \"eval\"):\n            metric_name = metric_name.replace(f\"{split}_\", f\"{split}/\")\n\n    elif framework == \"dvclive.keras\":\n        if \"val_\" in metric_name:\n            metric_name = metric_name.replace(\"val_\", \"eval/\")\n        else:\n            metric_name = f\"train/{metric_name}\"\n\n    elif framework in [\"dvclive.lightning\", \"dvclive.fabric\"]:\n        parts = metric_name.split(\"_\")\n        split, freq, rest = None, None, None\n        if any(parts[0].endswith(split) for split in [\"train\", \"val\", \"test\"]):\n            split = parts.pop(0)\n            # Only set freq if split was also found.\n            # Otherwise we end up conflicting with out internal `step` property.\n            if parts[-1] in [\"step\", \"epoch\"]:\n                freq = parts.pop()\n        rest = \"_\".join(parts)\n        parts = [part for part in (split, freq, rest) if part]\n        metric_name = \"/\".join(parts)\n\n    return metric_name\n\n\ndef parse_tsv(path):\n    with open(path, encoding=\"utf-8\", newline=\"\") as fd:\n        reader = csv.DictReader(fd, delimiter=\"\\t\")\n        return list(reader)\n\n\ndef parse_json(path):\n    with open(path, encoding=\"utf-8\") as fd:\n        return json.load(fd)\n\n\ndef parse_metrics(live):\n    from .plots import Metric\n\n    metrics_path = Path(live.plots_dir) / Metric.subfolder\n    history = {}\n    for suffix in Metric.suffixes:\n        for scalar_file in metrics_path.rglob(f\"*{suffix}\"):\n            history[str(scalar_file)] = parse_tsv(scalar_file)\n    latest = parse_json(live.metrics_file)\n    return history, latest\n\n\ndef matplotlib_installed() -> bool:\n    try:\n        import matplotlib as mpl  # noqa: F401\n    except ImportError:\n        return False\n    return True\n\n\ndef inside_colab() -> bool:\n    try:\n        from google import colab  # noqa: F401\n    except ImportError:\n        return False\n    return True\n\n\ndef inside_notebook() -> bool:\n    if inside_colab():\n        return True\n\n    try:\n        shell = get_ipython().__class__.__name__  # type: ignore[name-defined]\n    except NameError:\n        return False\n\n    if shell == \"ZMQInteractiveShell\":\n        import IPython\n\n        return IPython.__version__ >= \"6.0.0\"\n    return False\n\n\ndef clean_and_copy_into(src: StrPath, dst: StrPath) -> str:\n    Path(dst).mkdir(exist_ok=True)\n\n    basename = os.path.basename(os.path.normpath(src))\n    dst_path = Path(os.path.join(dst, basename))\n\n    if dst_path.is_file() or dst_path.is_symlink():\n        dst_path.unlink()\n    elif dst_path.is_dir():\n        shutil.rmtree(dst_path)\n\n    if os.path.isdir(src):\n        shutil.copytree(src, dst_path)\n    else:\n        shutil.copy2(src, dst_path)\n\n    return str(dst_path)\n\n\ndef isinstance_without_import(val, module, name):\n    for cls in type(val).mro():\n        if (cls.__module__, cls.__name__) == (module, name):\n            return True\n    return False\n\n\ndef catch_and_warn(exception, logger, on_finally=None):\n    def decorator(func):\n        def wrapper(*args, **kwargs):\n            try:\n                return func(*args, **kwargs)\n            except exception as e:\n                logger.warning(f\"Error in {func.__name__}: {e}\")\n            finally:\n                if on_finally is not None:\n                    on_finally()\n\n        return wrapper\n\n    return decorator\n\n\ndef rel_path(path, dvc_root_path):\n    absolute_path = Path(path).absolute()\n    return str(Path(os.path.relpath(absolute_path, dvc_root_path)).as_posix())\n\n\ndef read_history(live, metric):\n    from dvclive.plots.metric import Metric\n\n    history, _ = parse_metrics(live)\n    steps = []\n    values = []\n    name = os.path.join(live.plots_dir, Metric.subfolder, f\"{metric}.tsv\")\n    for e in history[name]:\n        steps.append(int(e[\"step\"]))\n        values.append(float(e[metric]))\n    return steps, values\n\n\ndef read_latest(live, metric_name):\n    _, latest = parse_metrics(live)\n    return latest[\"step\"], latest[metric_name]\n\n\ndef convert_datapoints_to_list_of_dicts(\n    datapoints: Union[list[dict], \"pd.DataFrame\", \"np.ndarray\"],\n) -> list[dict]:\n    \"\"\"\n    Convert the given datapoints to a list of dictionaries.\n\n    Args:\n        datapoints: The input datapoints to be converted.\n\n    Returns:\n        A list of dictionaries representing the datapoints.\n\n    Raises:\n        TypeError: `datapoints` must be pd.DataFrame, np.ndarray, or List[Dict]\n    \"\"\"\n    if isinstance(datapoints, list):\n        return datapoints\n\n    if pd and isinstance(datapoints, pd.DataFrame):\n        return datapoints.to_dict(orient=\"records\")\n\n    if np and isinstance(datapoints, np.ndarray):\n        # This is a structured array\n        if datapoints.dtype.names is not None:\n            return [dict(zip(datapoints.dtype.names, row)) for row in datapoints]\n\n        # This is a regular array\n        return [dict(enumerate(row)) for row in datapoints]\n\n    # Raise an error if the input is not a supported type\n    raise InvalidDataTypeError(\"datapoints\", type(datapoints))\n"
  },
  {
    "path": "src/dvclive/vscode.py",
    "content": "import json\nimport os\nfrom typing import Optional, Union\n\nfrom dvclive.dvc import _find_dvc_root\nfrom dvclive.utils import StrPath\n\nfrom . import env\n\n\ndef _dvc_exps_run_dir(dirname: StrPath) -> str:\n    return os.path.join(dirname, \".dvc\", \"tmp\", \"exps\", \"run\")\n\n\ndef _dvclive_only_signal_file(root_dir: StrPath) -> str:\n    dvc_exps_run_dir = _dvc_exps_run_dir(root_dir)\n    return os.path.join(dvc_exps_run_dir, \"DVCLIVE_ONLY\")\n\n\ndef _dvclive_step_completed_signal_file(root_dir: StrPath) -> str:\n    dvc_exps_run_dir = _dvc_exps_run_dir(root_dir)\n    return os.path.join(dvc_exps_run_dir, \"DVCLIVE_STEP_COMPLETED\")\n\n\ndef _find_non_queue_root() -> Optional[str]:\n    return os.getenv(env.DVC_ROOT) or _find_dvc_root()\n\n\ndef _write_file(file: str, contents: dict[str, Union[str, int]]):\n    import builtins\n\n    with builtins.open(file, \"w\", encoding=\"utf-8\") as fobj:\n        # NOTE: force flushing/writing empty file to disk, otherwise when\n        # run in certain contexts (pytest) file may not actually be written\n        fobj.write(json.dumps(contents, sort_keys=True, ensure_ascii=False))\n        fobj.flush()\n        os.fsync(fobj.fileno())\n\n\ndef mark_dvclive_step_completed(step: int) -> None:\n    \"\"\"\n    https://github.com/iterative/vscode-dvc/issues/4528\n    Signal DVC VS Code extension that\n    a step has been completed for an experiment running in the queue\n    \"\"\"\n    non_queue_root_dir = _find_non_queue_root()\n\n    if not non_queue_root_dir:\n        return\n\n    exp_run_dir = _dvc_exps_run_dir(non_queue_root_dir)\n    os.makedirs(exp_run_dir, exist_ok=True)\n\n    signal_file = _dvclive_step_completed_signal_file(non_queue_root_dir)\n\n    _write_file(signal_file, {\"pid\": os.getpid(), \"step\": step})\n\n\ndef cleanup_dvclive_step_completed() -> None:\n    non_queue_root_dir = _find_non_queue_root()\n\n    if not non_queue_root_dir:\n        return\n\n    signal_file = _dvclive_step_completed_signal_file(non_queue_root_dir)\n\n    if not os.path.exists(signal_file):\n        return\n\n    os.remove(signal_file)\n\n\ndef mark_dvclive_only_started(exp_name: str) -> None:\n    \"\"\"\n    Signal DVC VS Code extension that\n    an experiment is running in the workspace.\n    \"\"\"\n    root_dir = _find_dvc_root()\n    if not root_dir:\n        return\n\n    exp_run_dir = _dvc_exps_run_dir(root_dir)\n    os.makedirs(exp_run_dir, exist_ok=True)\n\n    signal_file = _dvclive_only_signal_file(root_dir)\n\n    _write_file(signal_file, {\"pid\": os.getpid(), \"exp_name\": exp_name})\n\n\ndef mark_dvclive_only_ended() -> None:\n    root_dir = _find_dvc_root()\n    if not root_dir:\n        return\n\n    signal_file = _dvclive_only_signal_file(root_dir)\n\n    if not os.path.exists(signal_file):\n        return\n\n    os.remove(signal_file)\n"
  },
  {
    "path": "src/dvclive/xgb.py",
    "content": "# ruff: noqa: ARG002\nfrom typing import Optional\nfrom warnings import warn\n\nfrom xgboost.callback import TrainingCallback\n\nfrom dvclive import Live\n\n\nclass DVCLiveCallback(TrainingCallback):\n    def __init__(\n        self,\n        metric_data: Optional[str] = None,\n        live: Optional[Live] = None,\n        **kwargs,\n    ):\n        super().__init__()\n        if metric_data is not None:\n            warn(\n                \"`metric_data` is deprecated and will be removed\",\n                category=DeprecationWarning,\n                stacklevel=2,\n            )\n        self._metric_data = metric_data\n        self.live = live if live is not None else Live(**kwargs)\n\n    def after_iteration(self, model, epoch, evals_log):\n        if self._metric_data:\n            evals_log = {\"\": evals_log[self._metric_data]}\n        for subdir, data in evals_log.items():\n            for key, values in data.items():\n                self.live.log_metric(f\"{subdir}/{key}\" if subdir else key, values[-1])\n        self.live.next_step()\n\n    def after_training(self, model):\n        self.live.end()\n        return model\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "import sys\n\nimport pytest\nfrom dvc_studio_client.env import DVC_STUDIO_TOKEN, DVC_STUDIO_URL, STUDIO_REPO_URL\n\nfrom dvclive.utils import rel_path\n\n\n@pytest.fixture\ndef tmp_dir(tmp_path, monkeypatch):\n    monkeypatch.chdir(tmp_path)\n    return tmp_path\n\n\n@pytest.fixture\ndef mocked_dvc_repo(tmp_dir, mocker):\n    _dvc_repo = mocker.MagicMock()\n    _dvc_repo.index.stages = []\n    _dvc_repo.scm.get_rev.return_value = \"f\" * 40\n    _dvc_repo.scm.get_ref.return_value = None\n    _dvc_repo.scm.no_commits = False\n    _dvc_repo.experiments.save.return_value = \"e\" * 40\n    _dvc_repo.root_dir = _dvc_repo.scm.root_dir = tmp_dir\n    _dvc_repo.fs.relpath = rel_path\n    _dvc_repo.config = {}\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=_dvc_repo)\n    return _dvc_repo\n\n\n@pytest.fixture\ndef mocked_dvc_subrepo(tmp_dir, mocker, mocked_dvc_repo):\n    mocked_dvc_repo.root_dir = tmp_dir / \"subdir\"\n    return mocked_dvc_repo\n\n\n@pytest.fixture\ndef dvc_repo(tmp_dir):\n    from dvc.repo import Repo\n    from scmrepo.git import Git\n\n    Git.init(tmp_dir)\n    repo = Repo.init(tmp_dir)\n    repo.scm.add_commit(\".\", \"init\")\n    return repo\n\n\n@pytest.fixture(autouse=True)\ndef _capture_wrap():\n    # https://github.com/pytest-dev/pytest/issues/5502#issuecomment-678368525\n    sys.stderr.close = lambda *args: None\n    sys.stdout.close = lambda *args: None\n\n\n@pytest.fixture(autouse=True)\ndef _mocked_webbrowser_open(mocker):\n    mocker.patch(\"webbrowser.open\")\n\n\n@pytest.fixture(autouse=True)\ndef _mocked_ci(monkeypatch):\n    monkeypatch.setenv(\"CI\", \"false\")\n\n\n@pytest.fixture\ndef mocked_studio_post(mocker, monkeypatch):\n    valid_response = mocker.MagicMock()\n    valid_response.status_code = 200\n    mocked_post = mocker.patch(\"requests.post\", return_value=valid_response)\n    monkeypatch.setenv(DVC_STUDIO_URL, \"https://0.0.0.0\")\n    monkeypatch.setenv(STUDIO_REPO_URL, \"STUDIO_REPO_URL\")\n    monkeypatch.setenv(DVC_STUDIO_TOKEN, \"STUDIO_TOKEN\")\n    return mocked_post, valid_response\n"
  },
  {
    "path": "tests/frameworks/test_fabric.py",
    "content": "from argparse import Namespace\nfrom unittest.mock import Mock\n\nimport numpy as np\nimport pytest\n\ntry:\n    import torch\n\n    from dvclive.fabric import DVCLiveLogger\nexcept ImportError:\n    pytest.skip(\"skipping lightning tests\", allow_module_level=True)\n\n\nclass BoringModel(torch.nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.layer = torch.nn.Linear(32, 2, bias=False)\n\n    def forward(self, x):\n        x = self.layer(x)\n        return torch.nn.functional.mse_loss(x, torch.ones_like(x))\n\n\n@pytest.mark.parametrize(\"step_idx\", [10, None])\ndef test_dvclive_log_metrics(tmp_path, mocked_dvc_repo, step_idx):\n    logger = DVCLiveLogger(dir=tmp_path)\n    metrics = {\n        \"float\": 0.3,\n        \"int\": 1,\n        \"FloatTensor\": torch.tensor(0.1),\n        \"IntTensor\": torch.tensor(1),\n    }\n    logger.log_metrics(metrics, step_idx)\n\n\ndef test_dvclive_log_hyperparams(tmp_path, mocked_dvc_repo):\n    logger = DVCLiveLogger(dir=tmp_path)\n    hparams = {\n        \"float\": 0.3,\n        \"int\": 1,\n        \"string\": \"abc\",\n        \"bool\": True,\n        \"dict\": {\"a\": {\"b\": \"c\"}},\n        \"list\": [1, 2, 3],\n        \"namespace\": Namespace(foo=Namespace(bar=\"buzz\")),\n        \"layer\": torch.nn.BatchNorm1d,\n        \"tensor\": torch.empty(2, 2, 2),\n        \"array\": np.empty([2, 2, 2]),\n    }\n    logger.log_hyperparams(hparams)\n\n\ndef test_dvclive_finalize(monkeypatch, tmp_path, mocked_dvc_repo):\n    \"\"\"Test that the SummaryWriter closes in finalize.\"\"\"\n    import dvclive\n\n    monkeypatch.setattr(dvclive, \"Live\", Mock())\n    logger = DVCLiveLogger(dir=tmp_path)\n    assert logger._experiment is None\n    logger.finalize(\"any\")\n\n    # no log calls, no experiment created -> nothing to flush\n    logger.experiment.assert_not_called()\n\n    logger = DVCLiveLogger(dir=tmp_path)\n    logger.log_hyperparams({\"flush_me\": 11.1})  # trigger creation of an experiment\n    logger.finalize(\"any\")\n\n    # finalize flushes to experiment directory\n    logger.experiment.end.assert_called()\n"
  },
  {
    "path": "tests/frameworks/test_fastai.py",
    "content": "import os\n\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.plots.metric import Metric\n\ntry:\n    from fastai.callback.tracker import SaveModelCallback\n    from fastai.tabular.all import (\n        Categorify,\n        Normalize,\n        ProgressCallback,\n        TabularDataLoaders,\n        accuracy,\n        tabular_learner,\n    )\n\n    from dvclive.fastai import DVCLiveCallback\nexcept ImportError:\n    pytest.skip(\"skipping fastai tests\", allow_module_level=True)\n\n\n@pytest.fixture\ndef data_loader():\n    from pandas import DataFrame\n\n    d = {\n        \"x1\": [1, 1, 0, 0, 1, 1, 0, 0],\n        \"x2\": [1, 0, 1, 0, 1, 0, 1, 0],\n        \"y\": [1, 0, 0, 1, 1, 0, 0, 1],\n    }\n    df = DataFrame(d)\n    return TabularDataLoaders.from_df(\n        df,\n        valid_idx=[4, 5, 6, 7],\n        batch_size=2,\n        cont_names=[\"x1\", \"x2\"],\n        procs=[Categorify, Normalize],\n        y_names=\"y\",\n    )\n\n\ndef test_fastai_callback(tmp_dir, data_loader, mocker):\n    learn = tabular_learner(data_loader, metrics=accuracy)\n    learn.remove_cb(ProgressCallback)\n    callback = DVCLiveCallback()\n    live = callback.live\n\n    spy = mocker.spy(live, \"end\")\n    learn.fit_one_cycle(2, cbs=[callback])\n    spy.assert_called_once()\n\n    assert (tmp_dir / live.dir).exists()\n    assert (tmp_dir / live.params_file).exists()\n    assert (tmp_dir / live.params_file).read_text() == (\n        \"model: TabularModel\\nbatch_size: 2\\nbatch_per_epoch: 2\\nfrozen: false\"\n        \"\\nfrozen_idx: 0\\ntransforms: None\\n\"\n    )\n\n    metrics_path = tmp_dir / live.plots_dir / Metric.subfolder\n    train_path = metrics_path / \"train\"\n    valid_path = metrics_path / \"eval\"\n\n    assert train_path.is_dir()\n    assert valid_path.is_dir()\n    assert (metrics_path / \"accuracy.tsv\").exists()\n    assert not (metrics_path / \"epoch.tsv\").exists()\n\n\ndef test_fastai_pass_logger():\n    logger = Live(\"train_logs\")\n\n    assert DVCLiveCallback().live is not logger\n    assert DVCLiveCallback(live=logger).live is logger\n\n\ndef test_fast_ai_resume(tmp_dir, data_loader, mocker):\n    learn = tabular_learner(data_loader, metrics=accuracy)\n    learn.remove_cb(ProgressCallback)\n    callback = DVCLiveCallback()\n    live = callback.live\n\n    spy = mocker.spy(live, \"next_step\")\n    end = mocker.spy(live, \"end\")\n    learn.fit_one_cycle(2, cbs=[callback])\n    assert spy.call_count == 2\n    assert end.call_count == 1\n\n    callback = DVCLiveCallback(resume=True)\n    live = callback.live\n    spy = mocker.spy(live, \"next_step\")\n    learn.fit_one_cycle(3, cbs=[callback], start_epoch=live.step)\n    assert spy.call_count == 1\n\n\ndef test_fast_ai_avoid_unnecessary_end_calls(tmp_dir, data_loader, mocker):\n    \"\"\"\n    `after_fit` might be called from different points and not all mean that the\n    training has ended.\n    \"\"\"\n    learn = tabular_learner(data_loader, metrics=accuracy)\n    learn.remove_cb(ProgressCallback)\n    callback = DVCLiveCallback()\n    live = callback.live\n\n    end = mocker.spy(live, \"end\")\n    after_fit = mocker.spy(callback, \"after_fit\")\n    learn.fine_tune(2, cbs=[callback])\n    assert end.call_count == 1\n    assert after_fit.call_count == 2\n\n\ndef test_fastai_save_model_callback(tmp_dir, data_loader, mocker):\n    learn = tabular_learner(data_loader, metrics=accuracy)\n    learn.remove_cb(ProgressCallback)\n    learn.model_dir = os.path.abspath(\"./\")\n\n    save_callback = SaveModelCallback()\n    live_callback = DVCLiveCallback()\n    log_artifact = mocker.patch.object(live_callback.live, \"log_artifact\")\n    learn.fit_one_cycle(2, cbs=[save_callback, live_callback])\n    assert (tmp_dir / \"model.pth\").is_file()\n    log_artifact.assert_called_with(str(save_callback.last_saved_path))\n"
  },
  {
    "path": "tests/frameworks/test_huggingface.py",
    "content": "import os\n\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.plots.metric import Metric\nfrom dvclive.serialize import load_yaml\nfrom dvclive.utils import parse_metrics\n\ntry:\n    import numpy as np\n    import torch\n    from torch import nn\n    from transformers import (\n        PretrainedConfig,\n        PreTrainedModel,\n        Trainer,\n        TrainingArguments,\n    )\n    from transformers.integrations import DVCLiveCallback as ExternalCallback\n\n    from dvclive.huggingface import DVCLiveCallback as InternalCallback\nexcept ImportError:\n    pytest.skip(\"skipping huggingface tests\", allow_module_level=True)\n\n\ndef compute_metrics(eval_preds):\n    \"\"\"https://github.com/iterative/dvclive/pull/321#issuecomment-1266916039\"\"\"\n    import time\n\n    time.sleep(time.get_clock_info(\"time\").resolution)\n    return {\"foo\": 1}\n\n\n# From transformers/tests/trainer\n\n\nclass RegressionDataset:\n    def __init__(self, a=2, b=3, length=64, seed=42, label_names=None):\n        np.random.seed(seed)\n        self.label_names = [\"labels\"] if label_names is None else label_names\n        self.length = length\n        self.x = np.random.normal(size=(length,)).astype(np.float32)\n        self.ys = [\n            a * self.x + b + np.random.normal(scale=0.1, size=(length,))\n            for _ in self.label_names\n        ]\n        self.ys = [y.astype(np.float32) for y in self.ys]\n\n    def __len__(self):\n        return self.length\n\n    def __getitem__(self, i):\n        result = {name: y[i] for name, y in zip(self.label_names, self.ys)}\n        result[\"input_x\"] = self.x[i]\n        return result\n\n\nclass RegressionModelConfig(PretrainedConfig):\n    def __init__(self, a=0, b=0, double_output=False, random_torch=True, **kwargs):\n        super().__init__(**kwargs)\n        self.a = a\n        self.b = b\n        self.double_output = double_output\n        self.random_torch = random_torch\n        self.hidden_size = 1\n\n\nclass RegressionPreTrainedModel(PreTrainedModel):\n    config_class = RegressionModelConfig  # type: ignore[assignment]\n    base_model_prefix = \"regression\"\n\n    def __init__(self, config):\n        super().__init__(config)\n        self.a = nn.Parameter(torch.tensor(config.a).float())\n        self.b = nn.Parameter(torch.tensor(config.b).float())\n        self.double_output = config.double_output\n\n    def forward(self, input_x, labels=None, **kwargs):\n        y = input_x * self.a + self.b\n        if labels is None:\n            return (y, y) if self.double_output else (y,)\n        loss = nn.functional.mse_loss(y, labels)\n        return (loss, y, y) if self.double_output else (loss, y)\n\n\n@pytest.fixture\ndef data():\n    return RegressionDataset(), RegressionDataset()\n\n\n@pytest.fixture\ndef model():\n    config = RegressionModelConfig()\n    return RegressionPreTrainedModel(config)\n\n\n@pytest.fixture\ndef args():\n    return TrainingArguments(\n        \"foo\",\n        eval_strategy=\"epoch\",\n        num_train_epochs=2,\n        save_strategy=\"epoch\",\n        report_to=\"none\",  # Disable auto-reporting to avoid duplication\n        use_cpu=True,\n    )\n\n\n@pytest.mark.parametrize(\"callback\", [ExternalCallback, InternalCallback])\ndef test_huggingface_integration(tmp_dir, model, args, data, mocker, callback):\n    trainer = Trainer(\n        model,\n        args,\n        train_dataset=data[0],\n        eval_dataset=data[1],\n        compute_metrics=compute_metrics,\n    )\n    callback = callback()\n    spy = mocker.spy(Live, \"end\")\n    trainer.add_callback(callback)\n    trainer.train()\n    spy.assert_called_once()\n\n    live = callback.live\n    assert os.path.exists(live.dir)\n\n    logs, _ = parse_metrics(live)\n\n    scalars = os.path.join(live.plots_dir, Metric.subfolder)\n    assert os.path.join(scalars, \"eval\", \"foo.tsv\") in logs\n    assert os.path.join(scalars, \"eval\", \"loss.tsv\") in logs\n    assert os.path.join(scalars, \"train\", \"loss.tsv\") in logs\n    assert len(logs[os.path.join(scalars, \"epoch.tsv\")]) == 3\n    assert len(logs[os.path.join(scalars, \"eval\", \"loss.tsv\")]) == 2\n\n    params = load_yaml(live.params_file)\n    assert params[\"num_train_epochs\"] == 2\n\n\n@pytest.mark.parametrize(\"log_model\", [\"all\", True, False, None])\n@pytest.mark.parametrize(\"best\", [True, False])\n@pytest.mark.parametrize(\"callback\", [ExternalCallback, InternalCallback])\ndef test_huggingface_log_model(\n    tmp_dir,\n    mocked_dvc_repo,\n    model,\n    data,\n    args,\n    monkeypatch,\n    mocker,\n    log_model,\n    best,\n    callback,\n):\n    live = Live()\n    log_artifact = mocker.patch.object(live, \"log_artifact\")\n    if callback == ExternalCallback:\n        monkeypatch.setenv(\"HF_DVCLIVE_LOG_MODEL\", str(log_model))\n        live_callback = callback(live=live)\n    else:\n        live_callback = callback(live=live, log_model=log_model)\n\n    args.load_best_model_at_end = best\n    args.metric_for_best_model = \"loss\"\n\n    trainer = Trainer(\n        model,\n        args,\n        train_dataset=data[0],\n        eval_dataset=data[1],\n        compute_metrics=compute_metrics,\n    )\n    trainer.add_callback(live_callback)\n    trainer.train()\n\n    expected_call_count = {\n        \"all\": 2,\n        True: 1,\n        False: 0,\n        None: 0,\n    }\n    assert log_artifact.call_count == expected_call_count[log_model]\n\n    if log_model is True:\n        name = \"best\" if best else \"last\"\n        log_artifact.assert_called_with(\n            os.path.join(args.output_dir, name),\n            name=name,\n            type=\"model\",\n            copy=True,\n        )\n\n\n@pytest.mark.parametrize(\"callback\", [ExternalCallback, InternalCallback])\ndef test_huggingface_pass_logger(callback):\n    logger = Live(\"train_logs\")\n\n    assert callback().live is not logger\n    assert callback(live=logger).live is logger\n\n\n@pytest.mark.parametrize(\"report_to\", [\"all\", \"dvclive\", \"none\"])\ndef test_huggingface_report_to(model, report_to):\n    args = TrainingArguments(\"foo\", report_to=report_to)\n    trainer = Trainer(\n        model,\n        args,\n    )\n    live_cbs = [\n        cb\n        for cb in trainer.callback_handler.callbacks\n        if isinstance(cb, ExternalCallback)\n    ]\n    if report_to == \"none\":\n        assert not any(live_cbs)\n    else:\n        assert any(live_cbs)\n"
  },
  {
    "path": "tests/frameworks/test_keras.py",
    "content": "import os\n\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.plots.metric import Metric\nfrom dvclive.utils import parse_metrics\n\ntry:\n    from dvclive.keras import DVCLiveCallback\nexcept ImportError:\n    pytest.skip(\"skipping keras tests\", allow_module_level=True)\n\n\n@pytest.fixture\ndef xor_model():\n    import numpy as np\n    import tensorflow as tf\n\n    def make():\n        x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])\n        y = np.array([[0], [1], [1], [0]])\n\n        model = tf.keras.Sequential()\n        model.add(tf.keras.layers.Dense(8, input_dim=2))\n        model.add(tf.keras.layers.Activation(\"relu\"))\n        model.add(tf.keras.layers.Dense(1))\n        model.add(tf.keras.layers.Activation(\"sigmoid\"))\n\n        model.compile(loss=\"binary_crossentropy\", optimizer=\"sgd\", metrics=[\"accuracy\"])\n\n        return model, x, y\n\n    return make\n\n\ndef test_keras_callback(tmp_dir, xor_model, mocker):\n    model, x, y = xor_model()\n\n    callback = DVCLiveCallback()\n    live = callback.live\n    spy = mocker.spy(live, \"end\")\n    model.fit(\n        x,\n        y,\n        epochs=1,\n        batch_size=1,\n        validation_split=0.2,\n        callbacks=[callback],\n    )\n    spy.assert_called_once()\n\n    assert os.path.exists(\"dvclive\")\n    logs, _ = parse_metrics(callback.live)\n\n    scalars = os.path.join(callback.live.plots_dir, Metric.subfolder)\n    assert os.path.join(scalars, \"train\", \"accuracy.tsv\") in logs\n    assert os.path.join(scalars, \"eval\", \"accuracy.tsv\") in logs\n\n\ndef test_keras_callback_pass_logger():\n    logger = Live(\"train_logs\")\n\n    assert DVCLiveCallback().live is not logger\n    assert DVCLiveCallback(live=logger).live is logger\n"
  },
  {
    "path": "tests/frameworks/test_lgbm.py",
    "content": "import os\nfrom sys import platform\n\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.utils import parse_metrics\n\ntry:\n    import lightgbm as lgbm\n    import pandas as pd\n    from sklearn import datasets\n    from sklearn.model_selection import train_test_split\n\n    from dvclive.lgbm import DVCLiveCallback\nexcept ImportError:\n    pytest.skip(\"skipping lightgbm tests\", allow_module_level=True)\n\n\n@pytest.fixture\ndef model_params():\n    return {\"objective\": \"multiclass\", \"n_estimators\": 5, \"seed\": 0}\n\n\n@pytest.fixture\ndef iris_data():\n    iris = datasets.load_iris()\n    x = pd.DataFrame(iris[\"data\"], columns=iris[\"feature_names\"])\n    y = iris[\"target\"]\n    x_train, x_test, y_train, y_test = train_test_split(\n        x, y, test_size=0.33, random_state=42\n    )\n    return (x_train, y_train), (x_test, y_test)\n\n\n@pytest.mark.skipif(platform == \"darwin\", reason=\"LIBOMP Segmentation fault on MacOS\")\ndef test_lgbm_integration(tmp_dir, model_params, iris_data):\n    model = lgbm.LGBMClassifier()\n    model.set_params(**model_params)\n\n    callback = DVCLiveCallback()\n    model.fit(\n        iris_data[0][0],\n        iris_data[0][1],\n        eval_set=(iris_data[1][0], iris_data[1][1]),\n        eval_metric=[\"multi_logloss\"],\n        callbacks=[callback],\n    )\n\n    assert os.path.exists(\"dvclive\")\n\n    logs, _ = parse_metrics(callback.live)\n    assert \"dvclive/plots/metrics/multi_logloss.tsv\" in logs\n    assert len(logs) == 1\n    assert len(next(iter(logs.values()))) == 5\n\n\n@pytest.mark.skipif(platform == \"darwin\", reason=\"LIBOMP Segmentation fault on MacOS\")\ndef test_lgbm_integration_multi_eval(tmp_dir, model_params, iris_data):\n    model = lgbm.LGBMClassifier()\n    model.set_params(**model_params)\n\n    callback = DVCLiveCallback()\n    model.fit(\n        iris_data[0][0],\n        iris_data[0][1],\n        eval_set=[\n            (iris_data[0][0], iris_data[0][1]),\n            (iris_data[1][0], iris_data[1][1]),\n        ],\n        eval_metric=[\"multi_logloss\"],\n        callbacks=[callback],\n    )\n\n    assert os.path.exists(\"dvclive\")\n\n    logs, _ = parse_metrics(callback.live)\n    assert \"dvclive/plots/metrics/training/multi_logloss.tsv\" in logs\n    assert \"dvclive/plots/metrics/valid_1/multi_logloss.tsv\" in logs\n    assert len(logs) == 2\n    assert len(next(iter(logs.values()))) == 5\n\n\ndef test_lgbm_pass_logger():\n    logger = Live(\"train_logs\")\n\n    assert DVCLiveCallback().live is not logger\n    assert DVCLiveCallback(live=logger).live is logger\n"
  },
  {
    "path": "tests/frameworks/test_lightning.py",
    "content": "import os\nfrom contextlib import redirect_stdout\nfrom io import StringIO\nfrom unittest import mock\n\nimport pytest\nimport yaml\n\nfrom dvclive.plots.metric import Metric\nfrom dvclive.serialize import load_yaml\nfrom dvclive.utils import parse_metrics\n\ntry:\n    import torch\n    from lightning import LightningModule\n    from lightning.pytorch import Trainer\n    from lightning.pytorch.callbacks import ModelCheckpoint\n    from lightning.pytorch.cli import LightningCLI\n    from lightning.pytorch.demos.boring_classes import BoringModel\n    from torch import nn\n    from torch.nn import functional as F  # noqa: N812\n    from torch.optim import SGD, Adam\n    from torch.utils.data import DataLoader, Dataset\n\n    from dvclive import Live\n    from dvclive.lightning import DVCLiveLogger\nexcept ImportError:\n    pytest.skip(\"skipping lightning tests\", allow_module_level=True)\n\n\nclass XORDataset(Dataset):\n    def __init__(self, *args, **kwargs):\n        self.ins = [[0, 0], [0, 1], [1, 0], [1, 1]]\n        self.outs = [1, 0, 0, 1]\n\n    def __getitem__(self, index):\n        return torch.Tensor(self.ins[index]), torch.tensor(\n            self.outs[index], dtype=torch.long\n        )\n\n    def __len__(self):\n        return len(self.ins)\n\n\nclass LitXOR(LightningModule):\n    def __init__(\n        self,\n        latent_dims=4,\n        optim=SGD,\n        optim_params={\"lr\": 0.01},  # noqa: B006\n        input_size=[256, 256, 256],  # noqa: B006\n    ):\n        super().__init__()\n\n        self.save_hyperparameters()\n\n        self.layer_1 = nn.Linear(2, latent_dims)\n        self.layer_2 = nn.Linear(latent_dims, 2)\n\n    def forward(self, *args, **kwargs):\n        x = args[0]\n        batch_size, _ = x.size()\n        x = x.view(batch_size, -1)\n        x = self.layer_1(x)\n        x = F.relu(x)\n        x = self.layer_2(x)\n        return F.log_softmax(x, dim=1)\n\n    def train_loader(self):\n        dataset = XORDataset()\n        return DataLoader(dataset, batch_size=1)\n\n    def train_dataloader(self):\n        return self.train_loader()\n\n    def training_step(self, *args, **kwargs):\n        batch = args[0]\n        x, y = batch\n        logits = self(x)\n        loss = F.nll_loss(logits, y)\n        self.log(\n            \"train_loss\",\n            loss,\n            prog_bar=True,\n            logger=True,\n            on_step=True,\n            on_epoch=True,\n        )\n        return loss\n\n    def configure_optimizers(self):\n        return self.hparams.optim(self.parameters(), **self.hparams.optim_params)\n\n    def predict_dataloader(self):\n        pass\n\n    def test_dataloader(self):\n        pass\n\n    def val_dataloader(self):\n        pass\n\n\ndef test_lightning_integration(tmp_dir, mocker):\n    # init model\n    model = LitXOR(\n        latent_dims=8, optim=Adam, optim_params={\"lr\": 0.02}, input_size=[128, 128, 128]\n    )\n    # init logger\n    dvclive_logger = DVCLiveLogger(\"test_run\", dir=\"logs\")\n    live = dvclive_logger.experiment\n    spy = mocker.spy(live, \"end\")\n    trainer = Trainer(\n        logger=dvclive_logger,\n        max_epochs=2,\n        enable_checkpointing=False,\n        log_every_n_steps=1,\n    )\n    trainer.fit(model)\n    spy.assert_called_once()\n\n    assert os.path.exists(\"logs\")\n    assert not os.path.exists(\"DvcLiveLogger\")\n\n    scalars = os.path.join(dvclive_logger.experiment.plots_dir, Metric.subfolder)\n    logs, _ = parse_metrics(dvclive_logger.experiment)\n\n    assert len(logs) == 3\n    assert os.path.join(scalars, \"train\", \"epoch\", \"loss.tsv\") in logs\n    assert os.path.join(scalars, \"train\", \"step\", \"loss.tsv\") in logs\n    assert os.path.join(scalars, \"epoch.tsv\") in logs\n\n    params_file = dvclive_logger.experiment.params_file\n    assert os.path.exists(params_file)\n    assert load_yaml(params_file) == {\n        \"latent_dims\": 8,\n        \"optim\": \"Adam\",\n        \"optim_params\": {\"lr\": 0.02},\n        \"input_size\": [128, 128, 128],\n    }\n\n\ndef test_lightning_default_dir(tmp_dir):\n    model = LitXOR()\n    # If `dir` is not provided handle it properly, use default value\n    dvclive_logger = DVCLiveLogger(\"test_run\")\n    trainer = Trainer(\n        logger=dvclive_logger,\n        max_epochs=2,\n        enable_checkpointing=False,\n        log_every_n_steps=1,\n    )\n    trainer.fit(model)\n\n    assert os.path.exists(\"dvclive\")\n\n\ndef test_lightning_kwargs(tmp_dir):\n    model = LitXOR()\n    # Handle kwargs passed to Live.\n    dvclive_logger = DVCLiveLogger(\n        dir=\"dir\", report=\"md\", dvcyaml=False, cache_images=True\n    )\n    trainer = Trainer(\n        logger=dvclive_logger,\n        max_epochs=2,\n        enable_checkpointing=False,\n        log_every_n_steps=1,\n    )\n    trainer.fit(model)\n\n    assert os.path.exists(\"dir\")\n    assert os.path.exists(\"dir/report.md\")\n    assert not os.path.exists(\"dir/dvc.yaml\")\n    assert dvclive_logger.experiment._cache_images is True\n\n\n@pytest.mark.parametrize(\"log_model\", [False, True, \"all\"])\n@pytest.mark.parametrize(\"save_top_k\", [1, -1])\ndef test_lightning_log_model(tmp_dir, mocker, log_model, save_top_k):\n    model = LitXOR()\n    dvclive_logger = DVCLiveLogger(dir=\"dir\", log_model=log_model)\n    checkpoint = ModelCheckpoint(dirpath=\"model\", save_top_k=save_top_k)\n    trainer = Trainer(\n        logger=dvclive_logger,\n        max_epochs=2,\n        log_every_n_steps=1,\n        callbacks=[checkpoint],\n    )\n    log_artifact = mocker.patch.object(dvclive_logger.experiment, \"log_artifact\")\n    trainer.fit(model)\n\n    # Check that log_artifact is called.\n    if log_model is False:\n        log_artifact.assert_not_called()\n    elif (log_model is True) and (save_top_k != -1):\n        # called once to cache, then again to log best artifact\n        assert log_artifact.call_count == 2\n    else:\n        # once per epoch plus two calls at the end (see above)\n        assert log_artifact.call_count == 4\n\n    # Check that checkpoint files does not grow with each run.\n    num_checkpoints = len(os.listdir(tmp_dir / \"model\"))\n    if log_model in [True, \"all\"]:\n        trainer.fit(model)\n        assert len(os.listdir(tmp_dir / \"model\")) == num_checkpoints\n        log_artifact.assert_any_call(\n            checkpoint.best_model_path, name=\"best\", type=\"model\", copy=True\n        )\n\n\ndef test_lightning_steps(tmp_dir, mocker):\n    model = LitXOR()\n    # Handle kwargs passed to Live.\n    dvclive_logger = DVCLiveLogger(dir=\"logs\")\n    live = dvclive_logger.experiment\n    spy = mocker.spy(live, \"sync\")\n    trainer = Trainer(\n        logger=dvclive_logger,\n        max_epochs=2,\n        enable_checkpointing=False,\n        # Log one time in the middle of the epoch\n        log_every_n_steps=3,\n    )\n    trainer.fit(model)\n\n    history, latest = parse_metrics(dvclive_logger.experiment)\n    assert latest[\"step\"] == 7\n    assert latest[\"epoch\"] == 1\n\n    scalars = os.path.join(dvclive_logger.experiment.plots_dir, Metric.subfolder)\n    epoch_loss = history[os.path.join(scalars, \"train\", \"epoch\", \"loss.tsv\")]\n    step_loss = history[os.path.join(scalars, \"train\", \"step\", \"loss.tsv\")]\n    assert len(epoch_loss) == 2\n    assert len(step_loss) == 2\n\n    # call sync:\n    # - 2x epoch end\n    # - 2x log_every_n_steps\n    # - 1x experiment end\n    assert spy.call_count == 5\n\n\nclass ValLitXOR(LitXOR):\n    def val_loader(self):\n        dataset = XORDataset()\n        return DataLoader(dataset, batch_size=1)\n\n    def val_dataloader(self):\n        return self.val_loader()\n\n    def training_step(self, *args, **kwargs):\n        batch = args[0]\n        x, y = batch\n        logits = self(x)\n        loss = F.nll_loss(logits, y)\n        self.log(\"train_loss\", loss, on_step=True)\n        return loss\n\n    def validation_step(self, *args, **kwargs):\n        batch = args[0]\n        x, y = batch\n        logits = self(x)\n        loss = F.nll_loss(logits, y)\n        self.log(\"val_loss\", loss, on_step=False, on_epoch=True)\n        return loss\n\n\ndef test_lightning_force_init(tmp_dir, mocker):\n    \"\"\"Related to https://github.com/iterative/dvclive/issues/594\n    Don't call Live.__init__ on rank-nonzero processes.\n    \"\"\"\n    init = mocker.spy(Live, \"__init__\")\n    DVCLiveLogger()\n    init.assert_not_called()\n\n\n# LightningCLI tests\n# Copied from https://github.com/Lightning-AI/lightning/blob/e7afe04ee86b64c76a5446088b3b75d9c275e5bf/tests/tests_pytorch/test_cli.py\nclass TestModel(BoringModel):\n    def __init__(self, foo, bar=5):\n        super().__init__()\n        self.foo = foo\n        self.bar = bar\n\n\ndef _test_logger_init_args(logger_name, init, unresolved={}):  # noqa: B006\n    cli_args = [f\"--trainer.logger={logger_name}\"]\n    cli_args += [f\"--trainer.logger.{k}={v}\" for k, v in init.items()]\n    cli_args += [f\"--trainer.logger.dict_kwargs.{k}={v}\" for k, v in unresolved.items()]\n    cli_args.append(\"--print_config\")\n\n    out = StringIO()\n    with (\n        mock.patch(\n            \"sys.argv\",\n            [\"any.py\"] + cli_args,  # noqa: RUF005\n        ),\n        redirect_stdout(  # noqa: RUF100\n            out\n        ),\n        pytest.raises(SystemExit),\n    ):\n        LightningCLI(TestModel, run=False)\n\n    data = yaml.safe_load(out.getvalue())[\"trainer\"][\"logger\"]\n    assert {k: data[\"init_args\"][k] for k in init} == init\n    if unresolved:\n        assert data[\"dict_kwargs\"] == unresolved\n\n\ndef test_dvclive_logger_init_args():\n    _test_logger_init_args(\n        \"dvclive.lightning.DVCLiveLogger\",\n        {\n            \"run_name\": \"test_run\",  # Resolve from DVCLiveLogger.__init__\n            \"dir\": \"results\",  # Resolve from Live.__init__\n        },\n    )\n"
  },
  {
    "path": "tests/frameworks/test_optuna.py",
    "content": "import pytest\n\nfrom dvclive.serialize import load_yaml\nfrom dvclive.utils import parse_json\n\ntry:\n    import optuna\n\n    from dvclive.optuna import DVCLiveCallback\nexcept ImportError:\n    pytest.skip(\"skipping optuna tests\", allow_module_level=True)\n\n\ndef objective(trial):\n    x = trial.suggest_float(\"x\", -10, 10)\n    return (x - 2) ** 2\n\n\ndef test_optuna_(tmp_dir, mocked_dvc_repo):\n    n_trials = 5\n    metric_name = \"custom_name\"\n    callback = DVCLiveCallback(metric_name=metric_name)\n    study = optuna.create_study()\n\n    study.optimize(objective, n_trials=n_trials, callbacks=[callback])\n\n    assert mocked_dvc_repo.experiments.save.call_count == n_trials\n\n    metrics = parse_json(\"dvclive-optuna/metrics.json\")\n    assert metric_name in metrics\n    params = load_yaml(\"dvclive-optuna/params.yaml\")\n    assert \"x\" in params\n\n    assert not (tmp_dir / \"dvclive-optuna\" / \"plots\").exists()\n"
  },
  {
    "path": "tests/frameworks/test_xgboost.py",
    "content": "import os\nfrom contextlib import nullcontext\n\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.plots.metric import Metric\nfrom dvclive.utils import parse_metrics\n\ntry:\n    import pandas as pd\n    import xgboost as xgb\n    from sklearn import datasets\n    from sklearn.model_selection import train_test_split\n\n    from dvclive.xgb import DVCLiveCallback\nexcept ImportError:\n    pytest.skip(\"skipping xgboost tests\", allow_module_level=True)\n\n\n@pytest.fixture\ndef train_params():\n    return {\"objective\": \"multi:softmax\", \"num_class\": 3, \"seed\": 0}\n\n\n@pytest.fixture\ndef iris_data():\n    iris = datasets.load_iris()\n    x = pd.DataFrame(iris[\"data\"], columns=iris[\"feature_names\"])\n    y = iris[\"target\"]\n    return xgb.DMatrix(x, y)\n\n\n@pytest.fixture\ndef iris_train_eval_data():\n    iris = datasets.load_iris()\n    x_train, x_eval, y_train, y_eval = train_test_split(\n        iris.data, iris.target, random_state=0\n    )\n    return (xgb.DMatrix(x_train, y_train), xgb.DMatrix(x_eval, y_eval))\n\n\n@pytest.mark.parametrize(\n    (\"metric_data\", \"subdirs\", \"context\"),\n    [\n        (\n            \"eval\",\n            (\"\",),\n            pytest.warns(DeprecationWarning, match=\"`metric_data`.+deprecated\"),\n        ),\n        (None, (\"train\", \"eval\"), nullcontext()),\n    ],\n)\ndef test_xgb_integration(\n    tmp_dir, train_params, iris_train_eval_data, metric_data, subdirs, context, mocker\n):\n    with context:\n        callback = DVCLiveCallback(metric_data)\n    live = callback.live\n    spy = mocker.spy(live, \"end\")\n    data_train, data_eval = iris_train_eval_data\n    xgb.train(\n        train_params,\n        data_train,\n        callbacks=[callback],\n        num_boost_round=5,\n        evals=[(data_train, \"train\"), (data_eval, \"eval\")],\n    )\n    spy.assert_called_once()\n\n    assert os.path.exists(\"dvclive\")\n\n    logs, _ = parse_metrics(callback.live)\n    assert len(logs) == len(subdirs)\n    assert list(map(len, logs.values())) == [5] * len(logs)\n    scalars = os.path.join(callback.live.plots_dir, Metric.subfolder)\n    assert all(\n        os.path.join(scalars, subdir, \"mlogloss.tsv\") in logs for subdir in subdirs\n    )\n\n\ndef test_xgb_pass_logger():\n    logger = Live(\"train_logs\")\n\n    assert DVCLiveCallback(\"eval_data\").live is not logger\n    assert DVCLiveCallback(\"eval_data\", live=logger).live is logger\n"
  },
  {
    "path": "tests/plots/test_custom.py",
    "content": "import json\n\nfrom dvclive import Live\nfrom dvclive.plots.custom import CustomPlot\n\n\ndef test_log_custom_plot(tmp_dir):\n    live = Live()\n    out = tmp_dir / live.plots_dir / CustomPlot.subfolder\n\n    datapoints = [{\"x\": 1, \"y\": 2}, {\"x\": 3, \"y\": 4}]\n    live.log_plot(\n        \"custom_linear\",\n        datapoints,\n        x=\"x\",\n        y=\"y\",\n        template=\"linear\",\n        title=\"custom_title\",\n        x_label=\"x_label\",\n        y_label=\"y_label\",\n    )\n\n    assert json.loads((out / \"custom_linear.json\").read_text()) == datapoints\n    assert live._plots[\"custom_linear\"].plot_config == {\n        \"template\": \"linear\",\n        \"title\": \"custom_title\",\n        \"x\": \"x\",\n        \"y\": \"y\",\n        \"x_label\": \"x_label\",\n        \"y_label\": \"y_label\",\n    }\n\n\ndef test_log_custom_plot_multi_y(tmp_dir):\n    live = Live()\n    out = tmp_dir / live.plots_dir / CustomPlot.subfolder\n\n    datapoints = [{\"x\": 1, \"y1\": 2, \"y2\": 3}, {\"x\": 4, \"y1\": 5, \"y2\": 6}]\n    live.log_plot(\n        \"custom_linear\",\n        datapoints,\n        x=\"x\",\n        y=[\"y1\", \"y2\"],\n        template=\"linear\",\n        title=\"custom_title\",\n        x_label=\"x_label\",\n        y_label=\"y_label\",\n    )\n\n    assert json.loads((out / \"custom_linear.json\").read_text()) == datapoints\n    assert live._plots[\"custom_linear\"].plot_config == {\n        \"template\": \"linear\",\n        \"title\": \"custom_title\",\n        \"x\": \"x\",\n        \"y\": [\"y1\", \"y2\"],\n        \"x_label\": \"x_label\",\n        \"y_label\": \"y_label\",\n    }\n\n\ndef test_log_custom_plot_with_template_as_empty_string(tmp_dir):\n    live = Live()\n    out = tmp_dir / live.plots_dir / CustomPlot.subfolder\n\n    datapoints = [{\"x\": 1, \"y\": 2}, {\"x\": 3, \"y\": 4}]\n    live.log_plot(\n        \"custom_linear\",\n        datapoints,\n        x=\"x\",\n        y=\"y\",\n        template=\"\",\n        title=\"custom_title\",\n        x_label=\"x_label\",\n        y_label=\"y_label\",\n    )\n\n    assert json.loads((out / \"custom_linear.json\").read_text()) == datapoints\n    # 'template' should not be in plot_config. Default template will be assigned later.\n    assert live._plots[\"custom_linear\"].plot_config == {\n        \"title\": \"custom_title\",\n        \"x\": \"x\",\n        \"y\": \"y\",\n        \"x_label\": \"x_label\",\n        \"y_label\": \"y_label\",\n    }\n"
  },
  {
    "path": "tests/plots/test_image.py",
    "content": "import matplotlib.pyplot as plt\nimport numpy as np\nimport pytest\nfrom PIL import Image\n\nfrom dvclive import Live\nfrom dvclive.error import InvalidImageNameError\nfrom dvclive.plots import Image as LiveImage\n\n\n# From https://stackoverflow.com/questions/5165317/how-can-i-extend-image-class\nclass ExtendedImage(Image.Image):\n    def __init__(self, img):\n        self._img = img\n\n    def __getattr__(self, key):\n        return getattr(self._img, key)\n\n\ndef test_pil(tmp_dir):\n    live = Live()\n    img = Image.new(\"RGB\", (10, 10), (250, 250, 250))\n    live.log_image(\"image.png\", img)\n\n    assert (tmp_dir / live.plots_dir / LiveImage.subfolder / \"image.png\").exists()\n\n\ndef test_pil_omitting_extension_doesnt_save_without_valid_format(tmp_dir):\n    live = Live()\n    img = Image.new(\"RGB\", (10, 10), (250, 250, 250))\n    with pytest.raises(\n        InvalidImageNameError, match=\"Cannot log image with name 'whoops'\"\n    ):\n        live.log_image(\"whoops\", img)\n\n\ndef test_pil_omitting_extension_sets_the_format_if_path_given(tmp_dir):\n    live = Live()\n    img = Image.new(\"RGB\", (10, 10), (250, 250, 250))\n\n    # Save it first, we'll reload it and pass it's path to log_image again\n    live.log_image(\"saved_with_format.png\", img)\n\n    # Now try saving without explicit format and check if the format is set correctly.\n    live.log_image(\n        \"whoops\",\n        (tmp_dir / live.plots_dir / LiveImage.subfolder / \"saved_with_format.png\"),\n    )\n\n    assert (tmp_dir / live.plots_dir / LiveImage.subfolder / \"whoops.png\").exists()\n\n\ndef test_invalid_extension(tmp_dir):\n    live = Live()\n    img = Image.new(\"RGB\", (10, 10), (250, 250, 250))\n    with pytest.raises(\n        InvalidImageNameError, match=\"Cannot log image with name 'image\\\\.foo'\"\n    ):\n        live.log_image(\"image.foo\", img)\n\n\n@pytest.mark.parametrize(\"shape\", [(10, 10), (10, 10, 3), (10, 10, 4)])\ndef test_numpy(tmp_dir, shape):\n    from PIL import Image as ImagePIL\n\n    live = Live()\n    img = np.ones(shape, np.uint8) * 255\n    live.log_image(\"image.png\", img)\n\n    img_path = tmp_dir / live.plots_dir / LiveImage.subfolder / \"image.png\"\n    assert img_path.exists()\n\n    val = np.asarray(ImagePIL.open(img_path))\n    assert np.array_equal(val, img)\n\n\ndef test_path(tmp_dir):\n    import numpy as np\n    from PIL import Image as ImagePIL\n\n    live = Live()\n    image_data = np.random.randint(0, 255, (100, 100, 3)).astype(np.uint8)\n    pil_image = ImagePIL.fromarray(image_data)\n    image_path = tmp_dir / \"temp.png\"\n    pil_image.save(image_path)\n\n    live = Live()\n    live.log_image(\"foo.png\", image_path)\n    live.end()\n\n    plot_file = tmp_dir / live.plots_dir / \"images\" / \"foo.png\"\n    assert plot_file.exists()\n\n    val = np.asarray(ImagePIL.open(plot_file))\n    assert np.array_equal(val, image_data)\n\n\ndef test_override_on_step(tmp_dir):\n    live = Live()\n\n    zeros = np.zeros((2, 2, 3), np.uint8)\n    live.log_image(\"image.png\", zeros)\n\n    live.next_step()\n\n    ones = np.ones((2, 2, 3), np.uint8)\n    live.log_image(\"image.png\", ones)\n\n    img_path = tmp_dir / live.plots_dir / LiveImage.subfolder / \"image.png\"\n    assert np.array_equal(np.array(Image.open(img_path)), ones)\n\n\ndef test_cleanup(tmp_dir):\n    live = Live()\n    img = np.ones((10, 10, 3), np.uint8)\n    live.log_image(\"image.png\", img)\n\n    assert (tmp_dir / live.plots_dir / LiveImage.subfolder / \"image.png\").exists()\n\n    Live()\n\n    assert not (tmp_dir / live.plots_dir / LiveImage.subfolder).exists()\n\n\ndef test_custom_class(tmp_dir):\n    live = Live()\n    img = Image.new(\"RGB\", (10, 10), (250, 250, 250))\n    extended_img = ExtendedImage(img)\n    live.log_image(\"image.png\", extended_img)\n\n    assert (tmp_dir / live.plots_dir / LiveImage.subfolder / \"image.png\").exists()\n\n\ndef test_matplotlib(tmp_dir):\n    live = Live()\n    fig, ax = plt.subplots()\n    ax.plot([1, 2, 3, 4])\n\n    assert plt.fignum_exists(fig.number)\n\n    live.log_image(\"image.png\", fig)\n\n    assert not plt.fignum_exists(fig.number)\n\n    assert (tmp_dir / live.plots_dir / LiveImage.subfolder / \"image.png\").exists()\n\n\n@pytest.mark.parametrize(\"cache\", [False, True])\ndef test_cache_images(tmp_dir, dvc_repo, cache):\n    live = Live(save_dvc_exp=False, cache_images=cache)\n    img = Image.new(\"RGB\", (10, 10), (250, 250, 250))\n    live.log_image(\"image.png\", img)\n    live.end()\n    assert (tmp_dir / \"dvclive\" / \"plots\" / \"images.dvc\").exists() == cache\n"
  },
  {
    "path": "tests/plots/test_metric.py",
    "content": "import json\n\nimport numpy as np\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.plots.metric import Metric\nfrom dvclive.plots.utils import NUMPY_INTS, NUMPY_SCALARS\nfrom dvclive.utils import parse_tsv\n\n\n@pytest.mark.parametrize(\"dtype\", NUMPY_SCALARS)\ndef test_numpy(tmp_dir, dtype):\n    scalar = np.random.rand(1).astype(dtype)[0]\n    live = Live()\n\n    live.log_metric(\"scalar\", scalar)\n    live.next_step()\n\n    parsed = json.loads((tmp_dir / live.metrics_file).read_text())\n    assert isinstance(parsed[\"scalar\"], int if dtype in NUMPY_INTS else float)\n    tsv_file = tmp_dir / live.plots_dir / Metric.subfolder / \"scalar.tsv\"\n    tsv_val = parse_tsv(tsv_file)[0][\"scalar\"]\n    assert tsv_val == str(scalar)\n\n\ndef test_name_with_dot(tmp_dir):\n    \"\"\"Regression test for #284\"\"\"\n    live = Live()\n\n    live.log_metric(\"scalar.foo.bar\", 1.0)\n    live.next_step()\n\n    tsv_file = tmp_dir / live.plots_dir / Metric.subfolder / \"scalar.foo.bar.tsv\"\n    assert tsv_file.exists()\n    tsv_val = parse_tsv(tsv_file)[0][\"scalar.foo.bar\"]\n    assert tsv_val == \"1.0\"\n"
  },
  {
    "path": "tests/plots/test_sklearn.py",
    "content": "# ruff: noqa: N806\nimport json\n\nimport pytest\nfrom sklearn import calibration, metrics\n\nfrom dvclive import Live\nfrom dvclive.plots.sklearn import SKLearnPlot\n\n\n@pytest.fixture\ndef y_true_y_pred_y_score():\n    from sklearn.datasets import make_classification\n    from sklearn.ensemble import RandomForestClassifier\n    from sklearn.model_selection import train_test_split\n\n    X, y = make_classification(random_state=0)\n    X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)\n    clf = RandomForestClassifier(random_state=0)\n    clf.fit(X_train, y_train)\n\n    y_pred = clf.predict(X_test)\n    y_score = clf.predict_proba(X_test)[:, 1]\n\n    return y_test, y_pred, y_score\n\n\ndef test_log_calibration_curve(tmp_dir, y_true_y_pred_y_score, mocker):\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, _, y_score = y_true_y_pred_y_score\n\n    spy = mocker.spy(calibration, \"calibration_curve\")\n\n    live.log_sklearn_plot(\"calibration\", y_true, y_score)\n\n    spy.assert_called_once_with(y_true, y_score)\n\n    assert (out / \"calibration.json\").exists()\n\n\ndef test_log_det_curve(tmp_dir, y_true_y_pred_y_score, mocker):\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, _, y_score = y_true_y_pred_y_score\n\n    spy = mocker.spy(metrics, \"det_curve\")\n\n    live.log_sklearn_plot(\"det\", y_true, y_score)\n\n    spy.assert_called_once_with(y_true, y_score)\n    assert (out / \"det.json\").exists()\n\n\ndef test_log_roc_curve(tmp_dir, y_true_y_pred_y_score, mocker):\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, _, y_score = y_true_y_pred_y_score\n\n    spy = mocker.spy(metrics, \"roc_curve\")\n\n    live.log_sklearn_plot(\"roc\", y_true, y_score)\n\n    spy.assert_called_once_with(y_true, y_score)\n    assert (out / \"roc.json\").exists()\n\n\ndef test_log_prc_curve(tmp_dir, y_true_y_pred_y_score, mocker):\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, _, y_score = y_true_y_pred_y_score\n\n    spy = mocker.spy(metrics, \"precision_recall_curve\")\n\n    live.log_sklearn_plot(\"precision_recall\", y_true, y_score)\n\n    spy.assert_called_once_with(y_true=y_true, y_score=y_score)\n    assert (out / \"precision_recall.json\").exists()\n\n\ndef test_log_confusion_matrix(tmp_dir, y_true_y_pred_y_score, mocker):\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, y_pred, _ = y_true_y_pred_y_score\n\n    live.log_sklearn_plot(\"confusion_matrix\", y_true, y_pred)\n\n    cm = json.loads((out / \"confusion_matrix.json\").read_text())\n\n    assert isinstance(cm, list)\n    assert isinstance(cm[0], dict)\n    assert cm[0][\"actual\"] == str(y_true[0])\n    assert cm[0][\"predicted\"] == str(y_pred[0])\n\n\ndef test_dump_kwargs(tmp_dir, y_true_y_pred_y_score, mocker):\n    live = Live()\n\n    y_true, _, y_score = y_true_y_pred_y_score\n\n    spy = mocker.spy(metrics, \"roc_curve\")\n\n    live.log_sklearn_plot(\"roc\", y_true, y_score, drop_intermediate=True)\n\n    spy.assert_called_once_with(y_true, y_score, drop_intermediate=True)\n\n\ndef test_override_on_step(tmp_dir):\n    live = Live()\n\n    live.log_sklearn_plot(\"confusion_matrix\", [0, 0], [0, 0])\n    live.next_step()\n    live.log_sklearn_plot(\"confusion_matrix\", [0, 0], [1, 1])\n\n    plot_path = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n    plot_path = plot_path / \"confusion_matrix.json\"\n\n    assert json.loads(plot_path.read_text()) == [\n        {\"actual\": \"0\", \"predicted\": \"1\"},\n        {\"actual\": \"0\", \"predicted\": \"1\"},\n    ]\n\n\ndef test_cleanup(tmp_dir, y_true_y_pred_y_score):\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, y_pred, _ = y_true_y_pred_y_score\n\n    live.log_sklearn_plot(\"confusion_matrix\", y_true, y_pred)\n\n    assert (out / \"confusion_matrix.json\").exists()\n\n    Live()\n\n    assert not (tmp_dir / live.plots_dir / SKLearnPlot.subfolder).exists()\n\n\ndef test_custom_name(tmp_dir, y_true_y_pred_y_score):\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, y_pred, _ = y_true_y_pred_y_score\n\n    live.log_sklearn_plot(\"confusion_matrix\", y_true, y_pred, name=\"train/cm\")\n    live.log_sklearn_plot(\"confusion_matrix\", y_true, y_pred, name=\"val/cm\")\n    # \".json\" should be stripped from the name\n    live.log_sklearn_plot(\"confusion_matrix\", y_true, y_pred, name=\"cm.json\")\n\n    assert (out / \"train\" / \"cm.json\").exists()\n    assert (out / \"val\" / \"cm.json\").exists()\n    assert (out / \"cm.json\").exists()\n\n\ndef test_custom_title(tmp_dir, y_true_y_pred_y_score):\n    \"\"\"https://github.com/iterative/dvclive/issues/453\"\"\"\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, y_pred, y_score = y_true_y_pred_y_score\n\n    live.log_sklearn_plot(\n        \"confusion_matrix\",\n        y_true,\n        y_pred,\n        name=\"train/cm\",\n        title=\"Train Confusion Matrix\",\n    )\n    live.log_sklearn_plot(\n        \"confusion_matrix\", y_true, y_pred, name=\"val/cm\", title=\"Val Confusion Matrix\"\n    )\n    live.log_sklearn_plot(\n        \"precision_recall\",\n        y_true,\n        y_score,\n        name=\"val/prc\",\n        title=\"Val Precision Recall\",\n    )\n    assert (out / \"train\" / \"cm.json\").exists()\n    assert (out / \"val\" / \"cm.json\").exists()\n    assert (out / \"val\" / \"prc.json\").exists()\n\n    assert live._plots[\"train/cm\"].plot_config[\"title\"] == \"Train Confusion Matrix\"\n    assert live._plots[\"val/cm\"].plot_config[\"title\"] == \"Val Confusion Matrix\"\n    assert live._plots[\"val/prc\"].plot_config[\"title\"] == \"Val Precision Recall\"\n\n\ndef test_custom_labels(tmp_dir, y_true_y_pred_y_score):\n    \"\"\"https://github.com/iterative/dvclive/issues/453\"\"\"\n    live = Live()\n    out = tmp_dir / live.plots_dir / SKLearnPlot.subfolder\n\n    y_true, _, y_score = y_true_y_pred_y_score\n\n    live.log_sklearn_plot(\n        \"precision_recall\",\n        y_true,\n        y_score,\n        name=\"val/prc\",\n        x_label=\"x_test\",\n        y_label=\"y_test\",\n    )\n    assert (out / \"val\" / \"prc.json\").exists()\n\n    assert live._plots[\"val/prc\"].plot_config[\"x_label\"] == \"x_test\"\n    assert live._plots[\"val/prc\"].plot_config[\"y_label\"] == \"y_test\"\n"
  },
  {
    "path": "tests/test_cleanup.py",
    "content": "import pytest\n\nfrom dvclive import Live\nfrom dvclive.plots import Metric\n\n\n@pytest.mark.parametrize(\n    \"html\",\n    [True, False],\n)\n@pytest.mark.parametrize(\n    \"dvcyaml\",\n    [\"dvc.yaml\", \"logs/dvc.yaml\"],\n)\ndef test_cleanup(tmp_dir, html, dvcyaml):\n    dvclive = Live(\"logs\", report=\"html\" if html else None, dvcyaml=dvcyaml)\n    dvclive.log_metric(\"m1\", 1)\n    dvclive.next_step()\n\n    html_path = tmp_dir / dvclive.dir / \"report.html\"\n    if html:\n        html_path.touch()\n\n    (tmp_dir / \"logs\" / \"some_user_file.txt\").touch()\n    (tmp_dir / \"dvc.yaml\").touch()\n\n    assert (tmp_dir / dvclive.plots_dir / Metric.subfolder / \"m1.tsv\").is_file()\n    assert (tmp_dir / dvclive.metrics_file).is_file()\n    assert (tmp_dir / dvclive.dvc_file).is_file()\n    assert html_path.is_file() == html\n\n    dvclive = Live(\"logs\")\n\n    assert (tmp_dir / \"logs\" / \"some_user_file.txt\").is_file()\n    assert not (tmp_dir / dvclive.plots_dir / Metric.subfolder).exists()\n    assert not (tmp_dir / dvclive.metrics_file).is_file()\n    if dvcyaml == \"dvc.yaml\":\n        assert (tmp_dir / dvcyaml).is_file()\n    if dvcyaml == \"logs/dvc.yaml\":\n        assert not (tmp_dir / dvcyaml).is_file()\n    assert not (html_path).is_file()\n"
  },
  {
    "path": "tests/test_context_manager.py",
    "content": "import json\n\nfrom dvclive import Live\nfrom dvclive.plots import Metric\n\n\ndef test_context_manager(tmp_dir):\n    with Live(report=\"html\") as live:\n        live.summary[\"foo\"] = 1.0\n\n    assert json.loads((tmp_dir / live.metrics_file).read_text()) == {\n        # no `step`\n        \"foo\": 1.0\n    }\n    log_file = tmp_dir / live.plots_dir / Metric.subfolder / \"foo.tsv\"\n    assert not log_file.exists()\n    report_file = tmp_dir / live.report_file\n    assert report_file.exists()\n\n\ndef test_context_manager_skips_end_calls(tmp_dir):\n    with Live() as live:\n        live.summary[\"foo\"] = 1.0\n        live.end()\n        assert not (tmp_dir / live.metrics_file).exists()\n    assert (tmp_dir / live.metrics_file).exists()\n"
  },
  {
    "path": "tests/test_dvc.py",
    "content": "import os\n\nimport pytest\nfrom dvc.exceptions import DvcException\nfrom dvc.repo import Repo\nfrom dvc.scm import NoSCM\nfrom scmrepo.git import Git\n\nfrom dvclive import Live\nfrom dvclive.dvc import get_dvc_repo\nfrom dvclive.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME, DVC_ROOT, DVCLIVE_TEST\n\n\ndef test_get_dvc_repo(tmp_dir):\n    assert get_dvc_repo() is None\n    Git.init(tmp_dir)\n    assert isinstance(get_dvc_repo(), Repo)\n\n\ndef test_get_dvc_repo_subdir(tmp_dir):\n    Git.init(tmp_dir)\n    subdir = tmp_dir / \"sub\"\n    subdir.mkdir()\n    os.chdir(subdir)\n    assert get_dvc_repo().root_dir == str(tmp_dir)\n\n\n@pytest.mark.parametrize(\"save\", [True, False])\ndef test_exp_save_on_end(tmp_dir, save, mocked_dvc_repo):\n    live = Live(save_dvc_exp=save)\n    live.end()\n    assert live._baseline_rev is not None\n    assert live._exp_name is not None\n    if save:\n        mocked_dvc_repo.experiments.save.assert_called_with(\n            name=live._exp_name,\n            include_untracked=[live.dir, \"dvc.yaml\"],\n            force=True,\n            message=None,\n        )\n    else:\n        mocked_dvc_repo.experiments.save.assert_not_called()\n\n\ndef test_exp_save_skip_on_env_vars(tmp_dir, monkeypatch):\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"foo\")\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n    monkeypatch.setenv(DVC_ROOT, tmp_dir)\n\n    live = Live()\n    live.end()\n\n    assert live._dvc_repo is None\n    assert live._baseline_rev == \"foo\"\n    assert live._exp_name == \"bar\"\n    assert live._inside_dvc_exp\n    assert live._inside_dvc_pipeline\n\n\ndef test_exp_save_with_dvc_files(tmp_dir, mocker):\n    dvc_repo = mocker.MagicMock()\n    dvc_file = mocker.MagicMock()\n    dvc_file.is_data_source = True\n    dvc_repo.index.stages = [dvc_file]\n    dvc_repo.scm.get_rev.return_value = \"current_rev\"\n    dvc_repo.scm.get_ref.return_value = None\n    dvc_repo.scm.no_commits = False\n    dvc_repo.root_dir = tmp_dir\n    dvc_repo.config = {}\n\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=dvc_repo)\n    live = Live()\n    live.end()\n\n    dvc_repo.experiments.save.assert_called_with(\n        name=live._exp_name,\n        include_untracked=[live.dir, \"dvc.yaml\"],\n        force=True,\n        message=None,\n    )\n\n\ndef test_exp_save_dvcexception_is_ignored(tmp_dir, mocker):\n    from dvc.exceptions import DvcException\n\n    dvc_repo = mocker.MagicMock()\n    dvc_repo.index.stages = []\n    dvc_repo.scm.get_rev.return_value = \"current_rev\"\n    dvc_repo.scm.get_ref.return_value = None\n    dvc_repo.config = {}\n    dvc_repo.experiments.save.side_effect = DvcException(\"foo\")\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=dvc_repo)\n\n    with Live():\n        pass\n\n\ndef test_untracked_dvclive_files_inside_dvc_exp_run_are_added(\n    tmp_dir, mocked_dvc_repo, monkeypatch\n):\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"foo\")\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n    monkeypatch.setenv(DVC_ROOT, tmp_dir)\n    plot_file = os.path.join(\"dvclive\", \"plots\", \"metrics\", \"foo.tsv\")\n    mocked_dvc_repo.scm.untracked_files.return_value = [\n        \"dvclive/metrics.json\",\n        plot_file,\n    ]\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.next_step()\n    live._dvc_repo.scm.add.assert_any_call([\"dvclive/metrics.json\", plot_file])\n    live._dvc_repo.scm.add.assert_any_call(live.dvc_file)\n\n\ndef test_dvc_outs_are_not_added(tmp_dir, mocked_dvc_repo, monkeypatch):\n    \"\"\"Regression test for https://github.com/iterative/dvclive/issues/516\"\"\"\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"foo\")\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n    monkeypatch.setenv(DVC_ROOT, tmp_dir)\n    mocked_dvc_repo.index.outs = [\"dvclive/plots\"]\n    plot_file = os.path.join(\"dvclive\", \"plots\", \"metrics\", \"foo.tsv\")\n    mocked_dvc_repo.scm.untracked_files.return_value = [\n        \"dvclive/metrics.json\",\n        plot_file,\n    ]\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.next_step()\n\n    live._dvc_repo.scm.add.assert_any_call([\"dvclive/metrics.json\"])\n\n\ndef test_errors_on_git_add_are_catched(tmp_dir, mocked_dvc_repo, monkeypatch):\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"foo\")\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n    mocked_dvc_repo.scm.untracked_files.return_value = [\"dvclive/metrics.json\"]\n    mocked_dvc_repo.scm.add.side_effect = DvcException(\"foo\")\n\n    with Live() as live:\n        live.summary[\"foo\"] = 1\n\n\ndef test_exp_save_message(tmp_dir, mocked_dvc_repo):\n    live = Live(exp_message=\"Custom message\")\n    live.end()\n    mocked_dvc_repo.experiments.save.assert_called_with(\n        name=live._exp_name,\n        include_untracked=[live.dir, \"dvc.yaml\"],\n        force=True,\n        message=\"Custom message\",\n    )\n\n\ndef test_exp_save_name(tmp_dir, mocked_dvc_repo):\n    live = Live(exp_name=\"custom-name\")\n    live.end()\n    mocked_dvc_repo.experiments.save.assert_called_with(\n        name=\"custom-name\",\n        include_untracked=[live.dir, \"dvc.yaml\"],\n        force=True,\n        message=None,\n    )\n\n\ndef test_no_scm_repo(tmp_dir, mocker):\n    dvc_repo = mocker.MagicMock()\n    dvc_repo.scm = NoSCM()\n\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=dvc_repo)\n    live = Live()\n    assert live._dvc_repo == dvc_repo\n\n    live = Live()\n    assert live._save_dvc_exp is False\n\n\ndef test_dvc_repro(tmp_dir, monkeypatch, mocked_dvc_repo, mocked_studio_post):\n    monkeypatch.setenv(DVC_ROOT, \"root\")\n    live = Live(save_dvc_exp=True)\n    assert live._baseline_rev is not None\n    assert live._exp_name is not None\n    assert not live._studio_events_to_skip\n    assert not live._save_dvc_exp\n\n\ndef test_get_exp_name_valid(tmp_dir, mocked_dvc_repo):\n    live = Live(exp_name=\"name\")\n    assert live._exp_name == \"name\"\n\n\ndef test_get_exp_name_random(tmp_dir, mocked_dvc_repo, mocker):\n    mocker.patch(\n        \"dvc.repo.experiments.utils.get_random_exp_name\", return_value=\"random\"\n    )\n    live = Live()\n    assert live._exp_name == \"random\"\n\n\ndef test_get_exp_name_invalid(tmp_dir, mocked_dvc_repo, mocker, caplog):\n    mocker.patch(\n        \"dvc.repo.experiments.utils.get_random_exp_name\", return_value=\"random\"\n    )\n    with caplog.at_level(\"WARNING\"):\n        live = Live(exp_name=\"invalid//name\")\n    assert live._exp_name == \"random\"\n    assert caplog.text\n\n\ndef test_get_exp_name_duplicate(tmp_dir, mocked_dvc_repo, mocker, caplog):\n    mocker.patch(\n        \"dvc.repo.experiments.utils.get_random_exp_name\", return_value=\"random\"\n    )\n    mocked_dvc_repo.scm.get_ref.return_value = \"duplicate\"\n    with caplog.at_level(\"WARNING\"):\n        live = Live(exp_name=\"duplicate\")\n    assert live._exp_name == \"random\"\n    msg = \"Experiment conflicts with existing experiment 'duplicate'.\"\n    assert msg in caplog.text\n\n\ndef test_test_mode(tmp_dir, monkeypatch, mocked_dvc_repo):\n    monkeypatch.setenv(DVCLIVE_TEST, \"true\")\n    live = Live(\"dir\", dvcyaml=\"dvc.yaml\")\n    live.make_dvcyaml()\n    assert live._dir != \"dir\"\n    assert live._dvc_file != \"dvc.yaml\"\n    assert live._save_dvc_exp is False\n    assert not os.path.exists(\"dir\")\n    assert not os.path.exists(\"dvc.yaml\")\n"
  },
  {
    "path": "tests/test_log_artifact.py",
    "content": "import shutil\nfrom pathlib import Path\n\nimport pytest\nfrom dvc.exceptions import DvcException\n\nfrom dvclive import Live\nfrom dvclive.error import InvalidDataTypeError\nfrom dvclive.serialize import load_yaml\n\ndvcyaml = \"\"\"\nstages:\n  train:\n    cmd: python train.py\n    outs:\n    - data\n\"\"\"\n\n\n@pytest.mark.parametrize(\"cache\", [True, False])\ndef test_log_artifact(tmp_dir, dvc_repo, cache):\n    data = tmp_dir / \"data\"\n    data.touch()\n    with Live(save_dvc_exp=False) as live:\n        live.log_artifact(\"data\", cache=cache)\n    assert data.with_suffix(\".dvc\").exists() is cache\n    assert load_yaml(live.dvc_file) == {}\n\n\ndef test_log_artifact_on_existing_dvc_file(tmp_dir, dvc_repo):\n    data = tmp_dir / \"data\"\n    data.write_text(\"foo\")\n    with Live(save_dvc_exp=False) as live:\n        live.log_artifact(\"data\")\n\n    prev_content = data.with_suffix(\".dvc\").read_text()\n\n    with Live(save_dvc_exp=False) as live:\n        data.write_text(\"bar\")\n        live.log_artifact(\"data\")\n\n    assert data.with_suffix(\".dvc\").read_text() != prev_content\n\n\ndef test_log_artifact_twice(tmp_dir, dvc_repo):\n    data = tmp_dir / \"data\"\n    with Live(save_dvc_exp=False) as live:\n        for i in range(2):\n            data.write_text(str(i))\n            live.log_artifact(\"data\")\n    assert data.with_suffix(\".dvc\").exists()\n\n\ndef test_log_artifact_with_save_dvc_exp(tmp_dir, mocker, mocked_dvc_repo):\n    stage = mocker.MagicMock()\n    stage.addressing = \"data\"\n    mocked_dvc_repo.add.return_value = [stage]\n    with Live() as live:\n        live.log_artifact(\"data\")\n    mocked_dvc_repo.experiments.save.assert_called_with(\n        name=live._exp_name,\n        include_untracked=[live.dir, \"data\", \".gitignore\", \"dvc.yaml\"],\n        force=True,\n        message=None,\n    )\n\n\ndef test_log_artifact_type_model(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live() as live:\n        live.log_artifact(\"model.pth\", type=\"model\")\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\"model\": {\"path\": \"model.pth\", \"type\": \"model\"}}\n    }\n\n\ndef test_log_artifact_dvc_symlink(tmp_dir, dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live(save_dvc_exp=False, dvcyaml=\"dvc.yaml\") as live:\n        live._dvc_repo.cache.local.cache_types = [\"symlink\"]\n        live.log_artifact(\"model.pth\", type=\"model\")\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\"model\": {\"path\": \"model.pth\", \"type\": \"model\"}}\n    }\n\n\ndef test_log_artifact_copy(tmp_dir, dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live(save_dvc_exp=False, dvcyaml=\"dvc.yaml\") as live:\n        live.log_artifact(\"model.pth\", type=\"model\", copy=True)\n\n    artifacts_dir = Path(live.artifacts_dir)\n    assert (artifacts_dir / \"model.pth\").exists()\n    assert (artifacts_dir / \"model.pth.dvc\").exists()\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\"model\": {\"path\": \"dvclive/artifacts/model.pth\", \"type\": \"model\"}}\n    }\n\n\ndef test_log_artifact_copy_overwrite(tmp_dir, dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live(save_dvc_exp=False, dvcyaml=\"dvc.yaml\") as live:\n        artifacts_dir = Path(live.artifacts_dir)\n        # testing with symlink cache to make sure that DVC protected mode\n        # does not prevent the overwrite\n        live._dvc_repo.cache.local.cache_types = [\"symlink\"]\n        live.log_artifact(\"model.pth\", type=\"model\", copy=True)\n        assert (artifacts_dir / \"model.pth\").is_symlink()\n        live.log_artifact(\"model.pth\", type=\"model\", copy=True)\n\n    assert (artifacts_dir / \"model.pth\").exists()\n    assert (artifacts_dir / \"model.pth.dvc\").exists()\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\"model\": {\"path\": \"dvclive/artifacts/model.pth\", \"type\": \"model\"}}\n    }\n\n\ndef test_log_artifact_copy_directory_overwrite(tmp_dir, dvc_repo):\n    model_path = Path(tmp_dir / \"weights\")\n    model_path.mkdir()\n    (tmp_dir / \"weights\" / \"model-epoch-1.pth\").touch()\n\n    with Live(save_dvc_exp=False, dvcyaml=\"dvc.yaml\") as live:\n        artifacts_dir = Path(live.artifacts_dir)\n        # testing with symlink cache to make sure that DVC protected mode\n        # does not prevent the overwrite\n        live._dvc_repo.cache.local.cache_types = [\"symlink\"]\n        live.log_artifact(model_path, type=\"model\", copy=True)\n        assert (artifacts_dir / \"weights\" / \"model-epoch-1.pth\").is_symlink()\n\n        shutil.rmtree(model_path)\n        model_path.mkdir()\n        (tmp_dir / \"weights\" / \"model-epoch-10.pth\").write_text(\"Model weights\")\n        (tmp_dir / \"weights\" / \"best.pth\").write_text(\"Best model weights\")\n        live.log_artifact(model_path, type=\"model\", copy=True)\n\n    assert (artifacts_dir / \"weights\").exists()\n    assert (artifacts_dir / \"weights\" / \"best.pth\").is_symlink()\n    assert (artifacts_dir / \"weights\" / \"best.pth\").read_text() == \"Best model weights\"\n    assert (artifacts_dir / \"weights\" / \"model-epoch-10.pth\").is_symlink()\n    assert len(list((artifacts_dir / \"weights\").iterdir())) == 2\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\"weights\": {\"path\": \"dvclive/artifacts/weights\", \"type\": \"model\"}}\n    }\n\n\ndef test_log_artifact_type_model_provided_name(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live(dvcyaml=\"dvc.yaml\") as live:\n        live.log_artifact(\"model.pth\", type=\"model\", name=\"custom\")\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\"custom\": {\"path\": \"model.pth\", \"type\": \"model\"}}\n    }\n\n\ndef test_log_artifact_type_model_on_step_and_final(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live(dvcyaml=\"dvc.yaml\") as live:\n        for _ in range(3):\n            live.log_artifact(\"model.pth\", type=\"model\")\n            live.next_step()\n        live.log_artifact(\"model.pth\", type=\"model\", labels=[\"final\"])\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\n            \"model\": {\"path\": \"model.pth\", \"type\": \"model\", \"labels\": [\"final\"]},\n        },\n        \"metrics\": [\"dvclive/metrics.json\"],\n    }\n\n\ndef test_log_artifact_type_model_on_step(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live(dvcyaml=\"dvc.yaml\") as live:\n        for _ in range(3):\n            live.log_artifact(\"model.pth\", type=\"model\")\n            live.next_step()\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\n            \"model\": {\"path\": \"model.pth\", \"type\": \"model\"},\n        },\n        \"metrics\": [\"dvclive/metrics.json\"],\n    }\n\n\ndef test_log_artifact_attrs(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    attrs = {\n        \"type\": \"model\",\n        \"name\": \"foo\",\n        \"desc\": \"bar\",\n        \"labels\": [\"foo\"],\n        \"meta\": {\"foo\": \"bar\"},\n    }\n    with Live(dvcyaml=\"dvc.yaml\") as live:\n        live.log_artifact(\"model.pth\", **attrs)\n    attrs.pop(\"name\")\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\n            \"foo\": {\"path\": \"model.pth\", **attrs},\n        }\n    }\n\n\ndef test_log_artifact_type_model_when_dvc_add_fails(tmp_dir, mocker, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n    mocked_dvc_repo.add.side_effect = DvcException(\"foo\")\n    with Live(save_dvc_exp=True, dvcyaml=\"dvc.yaml\") as live:\n        live.log_artifact(\"model.pth\", type=\"model\")\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\"model\": {\"path\": \"model.pth\", \"type\": \"model\"}}\n    }\n\n\n@pytest.mark.parametrize(\"tracked\", [\"data_source\", \"stage\", None])\ndef test_log_artifact_inside_pipeline(tmp_dir, mocker, dvc_repo, tracked):\n    logger = mocker.patch(\"dvclive.live.logger\")\n    data = tmp_dir / \"data\"\n    data.touch()\n    if tracked == \"data_source\":\n        dvc_repo.add(data)\n    elif tracked == \"stage\":\n        dvcyaml_path = tmp_dir / \"dvc.yaml\"\n        with open(dvcyaml_path, \"w\") as f:\n            f.write(dvcyaml)\n    live = Live(save_dvc_exp=False)\n    spy = mocker.spy(live._dvc_repo, \"add\")\n    live._inside_dvc_pipeline = True\n    live.log_artifact(\"data\")\n    if tracked == \"stage\":\n        msg = (\n            \"Skipping `dvc add data` because it is already being tracked\"\n            \" automatically as an output of the DVC pipeline.\"\n        )\n        logger.info.assert_called_with(msg)\n        spy.assert_not_called()\n    elif tracked == \"data_source\":\n        msg = (\n            \"To track 'data' automatically in the DVC pipeline:\"\n            \"\\n1. Run `dvc remove data.dvc` \"\n            \"to stop tracking it outside the pipeline.\"\n            \"\\n2. Add it as an output of the pipeline stage.\"\n        )\n        logger.warning.assert_called_with(msg)\n        spy.assert_called_once()\n    else:\n        msg = (\n            \"To track 'data' automatically in the DVC pipeline, \"\n            \"add it as an output of the pipeline stage.\"\n        )\n        logger.warning.assert_called_with(msg)\n        spy.assert_called_once()\n\n\ndef test_log_artifact_inside_pipeline_subdir(tmp_dir, mocker, dvc_repo):\n    logger = mocker.patch(\"dvclive.live.logger\")\n    subdir = tmp_dir / \"subdir\"\n    subdir.mkdir()\n    data = subdir / \"data\"\n    data.touch()\n    dvc_repo.add(subdir)\n    live = Live()\n    spy = mocker.spy(live._dvc_repo, \"add\")\n    live._inside_dvc_pipeline = True\n    live.log_artifact(\"subdir/data\")\n    msg = (\n        \"To track 'subdir/data' automatically in the DVC pipeline:\"\n        \"\\n1. Run `dvc remove subdir.dvc` \"\n        \"to stop tracking it outside the pipeline.\"\n        \"\\n2. Add it as an output of the pipeline stage.\"\n    )\n    logger.warning.assert_called_with(msg)\n    spy.assert_called_once()\n\n\ndef test_log_artifact_no_repo(tmp_dir, mocker):\n    logger = mocker.patch(\"dvclive.live.logger\")\n    (tmp_dir / \"data\").touch()\n    live = Live()\n    live.log_artifact(\"data\")\n    logger.warning.assert_called_with(\n        \"A DVC repo is required to log artifacts. Skipping `log_artifact(data)`.\"\n    )\n\n\n@pytest.mark.parametrize(\"invalid_path\", [None, 1.0, True, [], {}], ids=type)\ndef test_log_artifact_invalid_path_type(invalid_path, tmp_dir):\n    live = Live(save_dvc_exp=False)\n    expected_error_msg = f\"not supported type {type(invalid_path)}\"\n    with pytest.raises(InvalidDataTypeError, match=expected_error_msg):\n        live.log_artifact(path=invalid_path)\n"
  },
  {
    "path": "tests/test_log_metric.py",
    "content": "import math\nimport os\n\nimport numpy as np\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.error import InvalidDataTypeError\nfrom dvclive.plots import Metric\nfrom dvclive.serialize import load_yaml\nfrom dvclive.utils import parse_metrics, parse_tsv\n\n\ndef test_logging_no_step(tmp_dir):\n    dvclive = Live(\"logs\")\n\n    dvclive.log_metric(\"m1\", 1, plot=False)\n    dvclive.make_summary()\n\n    assert not (tmp_dir / \"logs\" / \"plots\" / \"metrics\" / \"m1.tsv\").is_file()\n    assert (tmp_dir / dvclive.metrics_file).is_file()\n\n    s = load_yaml(dvclive.metrics_file)\n    assert s[\"m1\"] == 1\n    assert \"step\" not in s\n\n\n@pytest.mark.parametrize(\"path\", [\"logs\", os.path.join(\"subdir\", \"logs\")])\ndef test_logging_step(tmp_dir, path):\n    dvclive = Live(path)\n    dvclive.log_metric(\"m1\", 1)\n    dvclive.next_step()\n    assert (tmp_dir / dvclive.dir).is_dir()\n    assert (tmp_dir / dvclive.plots_dir / Metric.subfolder / \"m1.tsv\").is_file()\n    assert (tmp_dir / dvclive.metrics_file).is_file()\n\n    s = load_yaml(dvclive.metrics_file)\n    assert s[\"m1\"] == 1\n    assert s[\"step\"] == 0\n\n\ndef test_nested_logging(tmp_dir):\n    dvclive = Live(\"logs\")\n\n    out = tmp_dir / dvclive.plots_dir / Metric.subfolder\n\n    dvclive.log_metric(\"train/m1\", 1)\n    dvclive.log_metric(\"val/val_1/m1\", 1)\n    dvclive.log_metric(\"val/val_1/m2\", 1)\n\n    dvclive.next_step()\n\n    assert (out / \"val\" / \"val_1\").is_dir()\n    assert (out / \"train\" / \"m1.tsv\").is_file()\n    assert (out / \"val\" / \"val_1\" / \"m1.tsv\").is_file()\n    assert (out / \"val\" / \"val_1\" / \"m2.tsv\").is_file()\n\n    assert \"m1\" in parse_tsv(out / \"train\" / \"m1.tsv\")[0]\n    assert \"m1\" in parse_tsv(out / \"val\" / \"val_1\" / \"m1.tsv\")[0]\n    assert \"m2\" in parse_tsv(out / \"val\" / \"val_1\" / \"m2.tsv\")[0]\n\n    summary = load_yaml(dvclive.metrics_file)\n\n    assert summary[\"train\"][\"m1\"] == 1\n    assert summary[\"val\"][\"val_1\"][\"m1\"] == 1\n    assert summary[\"val\"][\"val_1\"][\"m2\"] == 1\n\n\n@pytest.mark.parametrize(\"timestamp\", [True, False])\ndef test_log_metric_timestamp(tmp_dir, timestamp):\n    live = Live()\n    live.log_metric(\"foo\", 1.0, timestamp=timestamp)\n    live.next_step()\n\n    history, _ = parse_metrics(live)\n    logged = next(iter(history.values()))\n    assert (\"timestamp\" in logged[0]) == timestamp\n\n\n@pytest.mark.parametrize(\"invalid_type\", [{0: 1}, [0, 1], (0, 1)])\ndef test_invalid_metric_type(tmp_dir, invalid_type):\n    dvclive = Live()\n\n    with pytest.raises(\n        InvalidDataTypeError,\n        match=f\"Data 'm' has not supported type {type(invalid_type)}\",\n    ):\n        dvclive.log_metric(\"m\", invalid_type)\n\n\n@pytest.mark.parametrize(\n    (\"val\"),\n    [math.inf, math.nan, np.nan, np.inf],\n)\ndef test_log_metric_inf_nan(tmp_dir, val):\n    with Live() as live:\n        live.log_metric(\"metric\", val)\n    assert live.summary[\"metric\"] == str(val)\n\n\ndef test_log_metic_str(tmp_dir):\n    with Live() as live:\n        live.log_metric(\"metric\", \"foo\")\n    assert live.summary[\"metric\"] == \"foo\"\n"
  },
  {
    "path": "tests/test_log_param.py",
    "content": "import os\n\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.error import InvalidParameterTypeError\nfrom dvclive.serialize import load_yaml\n\n\ndef test_cleanup_params(tmp_dir):\n    dvclive = Live(\"logs\")\n    dvclive.log_param(\"param\", 42)\n\n    assert os.path.isfile(dvclive.params_file)\n\n    dvclive = Live(\"logs\")\n    assert not os.path.exists(dvclive.params_file)\n\n\n@pytest.mark.parametrize(\n    (\"param_name\", \"param_value\"),\n    [\n        (\"param_string\", \"value\"),\n        (\"param_int\", 42),\n        (\"param_float\", 42.0),\n        (\"param_bool_true\", True),\n        (\"param_bool_false\", False),\n        (\"param_list\", [1, 2, 3]),\n        (\n            \"param_dict_simple\",\n            {\"str\": \"value\", \"int\": 42, \"bool\": True, \"list\": [1, 2, 3]},\n        ),\n        (\n            \"param_dict_nested\",\n            {\n                \"str\": \"value\",\n                \"int\": 42,\n                \"bool\": True,\n                \"list\": [1, 2, 3],\n                \"dict\": {\"nested-str\": \"value\", \"nested-int\": 42},\n            },\n        ),\n    ],\n)\ndef test_log_param(tmp_dir, param_name, param_value):\n    dvclive = Live()\n\n    dvclive.log_param(param_name, param_value)\n\n    s = load_yaml(dvclive.params_file)\n    assert s[param_name] == param_value\n\n\ndef test_log_params(tmp_dir):\n    dvclive = Live()\n    params = {\n        \"param_string\": \"value\",\n        \"param_int\": 42,\n        \"param_float\": 42.0,\n        \"param_bool_true\": True,\n        \"param_bool_false\": False,\n    }\n\n    dvclive.log_params(params)\n\n    s = load_yaml(dvclive.params_file)\n    assert s == params\n\n\n@pytest.mark.parametrize(\"resume\", [False, True])\ndef test_log_params_resume(tmp_dir, resume):\n    dvclive = Live(resume=resume)\n    dvclive.log_param(\"param\", 42)\n\n    dvclive = Live(resume=resume)\n    assert (\"param\" in dvclive._params) == resume\n\n\ndef test_log_param_custom_obj(tmp_dir):\n    dvclive = Live(\"logs\")\n\n    class Dummy:\n        val = 42\n\n    param_value = Dummy()\n\n    with pytest.raises(InvalidParameterTypeError) as excinfo:\n        dvclive.log_param(\"param_complex\", param_value)\n    assert \"Dummy\" in excinfo.value.args[0]\n"
  },
  {
    "path": "tests/test_logging.py",
    "content": "import logging\n\nfrom dvclive import Live\n\n\ndef test_logger(tmp_dir, mocker):\n    logger = mocker.patch(\"dvclive.live.logger\")\n\n    live = Live()\n    live.log_metric(\"foo\", 0)\n    logger.debug.assert_called_with(\"Logged foo: 0\")\n    live.next_step()\n    logger.debug.assert_called_with(\"Step: 1\")\n    live.log_metric(\"foo\", 1)\n    live.next_step()\n\n    live = Live(resume=True)\n    logger.info.assert_called_with(\"Resuming from step 1\")\n\n\ndef test_suppress_dvc_logs(tmp_dir, mocked_dvc_repo):\n    Live()\n    assert logging.getLogger(\"dvc\").level == 30\n"
  },
  {
    "path": "tests/test_make_dvcyaml.py",
    "content": "import os\nfrom pathlib import Path\n\nimport pytest\nfrom PIL import Image\n\nfrom dvclive import Live\nfrom dvclive.dvc import make_dvcyaml\nfrom dvclive.error import InvalidDvcyamlError\nfrom dvclive.serialize import dump_yaml, load_yaml\n\n\ndef test_make_dvcyaml_empty(tmp_dir):\n    live = Live(dvcyaml=\"dvc.yaml\")\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {}\n\n\ndef test_make_dvcyaml_param(tmp_dir):\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.log_param(\"foo\", 1)\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {\n        \"params\": [\"dvclive/params.yaml\"],\n    }\n\n\ndef test_make_dvcyaml_metrics(tmp_dir):\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.log_metric(\"bar\", 2)\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {\n        \"metrics\": [\"dvclive/metrics.json\"],\n        \"plots\": [{\"dvclive/plots/metrics\": {\"x\": \"step\"}}],\n    }\n\n\ndef test_make_dvcyaml_metrics_no_plots(tmp_dir):\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.log_metric(\"bar\", 2, plot=False)\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {\n        \"metrics\": [\"dvclive/metrics.json\"],\n    }\n\n\ndef test_make_dvcyaml_summary(tmp_dir):\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.summary[\"bar\"] = 2\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {\n        \"metrics\": [\"dvclive/metrics.json\"],\n    }\n\n\ndef test_make_dvcyaml_all_plots(tmp_dir):\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.log_param(\"foo\", 1)\n    live.log_metric(\"bar\", 2)\n    live.log_image(\"img.png\", Image.new(\"RGB\", (10, 10), (250, 250, 250)))\n    live.log_sklearn_plot(\"confusion_matrix\", [0, 0, 1, 1], [0, 1, 1, 0])\n    live.log_sklearn_plot(\n        \"confusion_matrix\",\n        [0, 0, 1, 1],\n        [0, 1, 1, 0],\n        name=\"confusion_matrix_normalized\",\n        normalized=True,\n    )\n    live.log_sklearn_plot(\"roc\", [0, 0, 1, 1], [0.0, 0.5, 0.5, 0.0], \"custom_name_roc\")\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {\n        \"metrics\": [\"dvclive/metrics.json\"],\n        \"params\": [\"dvclive/params.yaml\"],\n        \"plots\": [\n            {\"dvclive/plots/metrics\": {\"x\": \"step\"}},\n            \"dvclive/plots/images\",\n            {\n                \"dvclive/plots/sklearn/confusion_matrix.json\": {\n                    \"template\": \"confusion\",\n                    \"x\": \"actual\",\n                    \"y\": \"predicted\",\n                    \"title\": \"Confusion Matrix\",\n                    \"x_label\": \"True Label\",\n                    \"y_label\": \"Predicted Label\",\n                },\n            },\n            {\n                \"dvclive/plots/sklearn/confusion_matrix_normalized.json\": {\n                    \"template\": \"confusion_normalized\",\n                    \"title\": \"Confusion Matrix\",\n                    \"x\": \"actual\",\n                    \"x_label\": \"True Label\",\n                    \"y\": \"predicted\",\n                    \"y_label\": \"Predicted Label\",\n                }\n            },\n            {\n                \"dvclive/plots/sklearn/custom_name_roc.json\": {\n                    \"template\": \"simple\",\n                    \"x\": \"fpr\",\n                    \"y\": \"tpr\",\n                    \"title\": \"Receiver operating characteristic (ROC)\",\n                    \"x_label\": \"False Positive Rate\",\n                    \"y_label\": \"True Positive Rate\",\n                }\n            },\n        ],\n    }\n\n\ndef test_make_dvcyaml_relpath(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n    live = Live(dvcyaml=\"dir/dvc.yaml\")\n    live.log_metric(\"foo\", 1)\n    live.log_artifact(\"model.pth\", type=\"model\")\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {\n        \"metrics\": [\"../dvclive/metrics.json\"],\n        \"plots\": [{\"../dvclive/plots/metrics\": {\"x\": \"step\"}}],\n        \"artifacts\": {\n            \"model\": {\"path\": \"../model.pth\", \"type\": \"model\"},\n        },\n    }\n\n\n@pytest.mark.parametrize(\n    (\"orig_yaml\", \"updated_yaml\"),\n    [\n        pytest.param(\n            {\"stages\": {\"train\": {\"cmd\": \"train.py\"}}},\n            {\n                \"stages\": {\"train\": {\"cmd\": \"train.py\"}},\n                \"metrics\": [\"dvclive/metrics.json\"],\n                \"plots\": [\n                    {\"dvclive/plots/metrics\": {\"x\": \"step\"}},\n                ],\n            },\n            id=\"stages\",\n        ),\n        pytest.param(\n            {\"params\": [\"dvclive/params.yaml\"]},\n            {\n                \"metrics\": [\"dvclive/metrics.json\"],\n                \"plots\": [{\"dvclive/plots/metrics\": {\"x\": \"step\"}}],\n            },\n            id=\"drop_extra_sections\",\n        ),\n        pytest.param(\n            {\"plots\": [\"dvclive/plots/images\"]},\n            {\n                \"metrics\": [\"dvclive/metrics.json\"],\n                \"plots\": [{\"dvclive/plots/metrics\": {\"x\": \"step\"}}],\n            },\n            id=\"drop_unlogged_plots\",\n        ),\n        pytest.param(\n            {\"plots\": [{\"dvclive/plots/metrics\": {\"x\": \"step\", \"y\": \"foo\"}}]},\n            {\n                \"metrics\": [\"dvclive/metrics.json\"],\n                \"plots\": [{\"dvclive/plots/metrics\": {\"x\": \"step\"}}],\n            },\n            id=\"plot_props\",\n        ),\n        pytest.param(\n            {\n                \"plots\": [\n                    {\n                        \"custom\": {\n                            \"x\": \"step\",\n                            \"y\": {\"dvclive/plots/metrics\": \"foo\"},\n                            \"title\": \"custom\",\n                        }\n                    },\n                ],\n            },\n            {\n                \"metrics\": [\"dvclive/metrics.json\"],\n                \"plots\": [\n                    {\n                        \"custom\": {\n                            \"x\": \"step\",\n                            \"y\": {\"dvclive/plots/metrics\": \"foo\"},\n                            \"title\": \"custom\",\n                        }\n                    },\n                    {\"dvclive/plots/metrics\": {\"x\": \"step\"}},\n                ],\n            },\n            id=\"keep_custom_plots\",\n        ),\n    ],\n)\ndef test_make_dvcyaml_update(tmp_dir, orig_yaml, updated_yaml):\n    dump_yaml(orig_yaml, \"dvc.yaml\")\n\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.log_metric(\"foo\", 2)\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == updated_yaml\n\n\n@pytest.mark.parametrize(\n    (\"orig_yaml\", \"updated_yaml\"),\n    [\n        pytest.param(\n            {\n                \"artifacts\": {\n                    \"model\": {\n                        \"path\": \"model.pth\",\n                        \"type\": \"model\",\n                        \"desc\": \"best model\",\n                    },\n                },\n            },\n            {\n                \"artifacts\": {\n                    \"model\": {\"path\": \"dvclive/artifacts/model.pth\", \"type\": \"model\"},\n                },\n            },\n            id=\"props\",\n        ),\n        pytest.param(\n            {\n                \"artifacts\": {\n                    \"duplicate\": {\"path\": \"dvclive/artifacts/model.pth\"},\n                },\n            },\n            {\n                \"artifacts\": {\n                    \"model\": {\"path\": \"dvclive/artifacts/model.pth\", \"type\": \"model\"},\n                },\n            },\n            id=\"duplicate\",\n        ),\n        pytest.param(\n            {\n                \"artifacts\": {\n                    \"data\": {\"path\": \"data.csv\", \"desc\": \"source data\"},\n                },\n            },\n            {\n                \"artifacts\": {\n                    \"model\": {\"path\": \"dvclive/artifacts/model.pth\", \"type\": \"model\"},\n                    \"data\": {\"path\": \"data.csv\", \"desc\": \"source data\"},\n                },\n            },\n            id=\"keep_extra\",\n        ),\n    ],\n)\ndef test_make_dvcyaml_update_artifact(\n    tmp_dir, mocked_dvc_repo, orig_yaml, updated_yaml\n):\n    dump_yaml(orig_yaml, \"dvc.yaml\")\n    (tmp_dir / \"model.pth\").touch()\n\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.log_artifact(\"model.pth\", type=\"model\", copy=True)\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == updated_yaml\n\n\ndef test_make_dvcyaml_update_all(tmp_dir, mocked_dvc_repo):\n    orig_yaml = {\n        \"stages\": {\"train\": {\"cmd\": \"train.py\"}},\n        \"metrics\": [\n            \"dvclive/metrics.json\",\n            \"dvclive/metrics.yaml\",\n            \"other/metrics.json\",\n        ],\n        \"params\": [\"dvclive/params.yaml\"],\n        \"plots\": [\n            {\"dvclive/plots/metrics\": {\"x\": \"step\", \"y\": \"foo\"}},\n            \"dvclive/plots/images\",\n            \"other/plots\",\n            {\n                \"custom\": {\n                    \"x\": \"step\",\n                    \"y\": {\"dvclive/plots/metrics\": \"foo\"},\n                    \"title\": \"custom\",\n                }\n            },\n            {\n                \"dvclive/plots/sklearn/confusion_matrix.json\": {\n                    \"template\": \"confusion\",\n                    \"x\": \"actual\",\n                    \"y\": \"predicted\",\n                    \"title\": \"Confusion Matrix\",\n                    \"x_label\": \"True Label\",\n                    \"y_label\": \"Predicted Label\",\n                },\n            },\n        ],\n        \"artifacts\": {\n            \"model\": {\"path\": \"dvclive/artifacts/model.pth\", \"type\": \"model\"},\n            \"duplicate\": {\"path\": \"dvclive/artifacts/model.pth\"},\n            \"data\": {\"path\": \"data.csv\", \"desc\": \"source data\"},\n            \"other\": {\"path\": \"other.pth\"},\n        },\n    }\n\n    updated_yaml = {\n        \"stages\": {\"train\": {\"cmd\": \"train.py\"}},\n        \"metrics\": [\"other/metrics.json\", \"dvclive/metrics.json\"],\n        \"plots\": [\n            \"other/plots\",\n            {\n                \"custom\": {\n                    \"x\": \"step\",\n                    \"y\": {\"dvclive/plots/metrics\": \"foo\"},\n                    \"title\": \"custom\",\n                }\n            },\n            {\"dvclive/plots/metrics\": {\"x\": \"step\"}},\n            \"dvclive/plots/images\",\n        ],\n        \"artifacts\": {\n            \"model\": {\"path\": \"dvclive/artifacts/model.pth\", \"type\": \"model\"},\n            \"data\": {\"path\": \"data.csv\", \"desc\": \"source data\"},\n            \"other\": {\"path\": \"other.pth\"},\n        },\n    }\n\n    dump_yaml(orig_yaml, \"dvc.yaml\")\n    (tmp_dir / \"model.pth\").touch()\n    (tmp_dir / \"data.csv\").touch()\n\n    live = Live(dvcyaml=\"dvc.yaml\")\n    live.log_metric(\"foo\", 2)\n    live.log_image(\"img.png\", Image.new(\"RGB\", (10, 10), (250, 250, 250)))\n    live.log_artifact(\"model.pth\", type=\"model\", copy=True)\n    live.log_artifact(\"data.csv\", desc=\"source data\")\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == updated_yaml\n\n\ndef test_make_dvcyaml_update_multiple(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    live = Live(\"train\", dvcyaml=\"dvc.yaml\")\n    live.log_metric(\"foo\", 2)\n    live.log_artifact(\"model.pth\", type=\"model\", copy=True)\n    make_dvcyaml(live)\n\n    live = Live(\"eval\", dvcyaml=\"dvc.yaml\")\n    live.log_metric(\"bar\", 3)\n    make_dvcyaml(live)\n\n    assert load_yaml(live.dvc_file) == {\n        \"metrics\": [\"train/metrics.json\", \"eval/metrics.json\"],\n        \"plots\": [\n            {\"train/plots/metrics\": {\"x\": \"step\"}},\n            {\"eval/plots/metrics\": {\"x\": \"step\"}},\n        ],\n        \"artifacts\": {\n            \"model\": {\"path\": \"train/artifacts/model.pth\", \"type\": \"model\"},\n        },\n    }\n\n\n@pytest.mark.parametrize(\"dvcyaml\", [True, False])\ndef test_dvcyaml_on_next_step(tmp_dir, dvcyaml, mocked_dvc_repo):\n    live = Live(dvcyaml=dvcyaml)\n    live.next_step()\n    if dvcyaml:\n        assert (tmp_dir / live.dvc_file).exists()\n    else:\n        assert not (tmp_dir / live.dvc_file).exists()\n\n\n@pytest.mark.parametrize(\"dvcyaml\", [True, False])\ndef test_dvcyaml_on_end(tmp_dir, dvcyaml, mocked_dvc_repo):\n    live = Live(dvcyaml=dvcyaml)\n    live.end()\n    if dvcyaml:\n        assert (tmp_dir / live.dvc_file).exists()\n    else:\n        assert not (tmp_dir / live.dvc_file).exists()\n\n\ndef test_make_dvcyaml_idempotent(tmp_dir, mocked_dvc_repo):\n    (tmp_dir / \"model.pth\").touch()\n\n    with Live() as live:\n        live.log_artifact(\"model.pth\", type=\"model\")\n\n    live.make_dvcyaml()\n\n    assert load_yaml(live.dvc_file) == {\n        \"artifacts\": {\n            \"model\": {\"path\": \"model.pth\", \"type\": \"model\"},\n        }\n    }\n\n\n@pytest.mark.parametrize(\"dvcyaml\", [True, False, \"dvclive/dvc.yaml\"])\ndef test_warn_on_dvcyaml_output_overlap(tmp_dir, mocker, mocked_dvc_repo, dvcyaml):\n    logger = mocker.patch(\"dvclive.live.logger\")\n    dvc_stage = mocker.MagicMock()\n    dvc_stage.addressing = \"train\"\n    dvc_out = mocker.MagicMock()\n    dvc_out.fs_path = tmp_dir / \"dvclive\"\n    dvc_stage.outs = [dvc_out]\n    mocked_dvc_repo.index.stages = [dvc_stage]\n    live = Live(dvcyaml=dvcyaml)\n\n    if dvcyaml == \"dvclive/dvc.yaml\":\n        msg = f\"'{live.dvc_file}' is in outputs of stage 'train'.\\n\"\n        msg += \"Remove it from outputs to make DVCLive work as expected.\"\n        logger.warning.assert_called_with(msg)\n    else:\n        logger.warning.assert_not_called()\n\n\n@pytest.mark.parametrize(\n    \"dvcyaml\",\n    [True, False, \"dvc.yaml\", Path(\"dvc.yaml\")],\n)\ndef test_make_dvcyaml(tmp_dir, mocked_dvc_repo, dvcyaml):\n    dvclive = Live(\"logs\", dvcyaml=dvcyaml)\n    dvclive.log_metric(\"m1\", 1)\n    dvclive.next_step()\n\n    if dvcyaml:\n        assert \"metrics\" in load_yaml(dvclive.dvc_file)\n    else:\n        assert not os.path.exists(dvclive.dvc_file)\n\n    dvclive.make_dvcyaml()\n    assert \"metrics\" in load_yaml(dvclive.dvc_file)\n\n\ndef test_make_dvcyaml_no_repo(tmp_dir, mocker):\n    dvclive = Live(\"logs\")\n    dvclive.make_dvcyaml()\n\n    assert os.path.exists(\"dvc.yaml\")\n\n\ndef test_make_dvcyaml_invalid(tmp_dir, mocker):\n    with pytest.raises(InvalidDvcyamlError):\n        Live(\"logs\", dvcyaml=\"invalid\")\n\n\ndef test_make_dvcyaml_on_end(tmp_dir, mocker):\n    dvclive = Live(\"logs\")\n    dvclive.end()\n\n    assert os.path.exists(\"dvc.yaml\")\n\n\ndef test_make_dvcyaml_false(tmp_dir, mocker):\n    dvclive = Live(\"logs\", dvcyaml=False)\n    dvclive.end()\n\n    assert not os.path.exists(\"dvc.yaml\")\n\n\ndef test_make_dvcyaml_none(tmp_dir, mocker):\n    dvclive = Live(\"logs\", dvcyaml=None)\n    dvclive.end()\n\n    assert not os.path.exists(\"dvc.yaml\")\n"
  },
  {
    "path": "tests/test_make_report.py",
    "content": "import numpy as np\nimport pytest\nfrom PIL import Image\n\nfrom dvclive import Live\nfrom dvclive.env import DVCLIVE_OPEN\nfrom dvclive.error import InvalidReportModeError\nfrom dvclive.plots import CustomPlot, Metric\nfrom dvclive.plots import Image as LiveImage\nfrom dvclive.plots.sklearn import SKLearnPlot\nfrom dvclive.report import (\n    get_custom_plot_renderers,\n    get_image_renderers,\n    get_metrics_renderers,\n    get_params_renderers,\n    get_scalar_renderers,\n    get_sklearn_plot_renderers,\n)\n\n\n@pytest.mark.parametrize(\"mode\", [\"html\", \"md\", \"notebook\"])\ndef test_get_image_renderers(tmp_dir, mode):\n    with Live() as live:\n        img = Image.new(\"RGB\", (10, 10), (255, 0, 0))\n        live.log_image(\"image.png\", img)\n\n    image_renderers = get_image_renderers(\n        tmp_dir / live.plots_dir / LiveImage.subfolder\n    )\n    assert len(image_renderers) == 1\n    img = image_renderers[0].datapoints[0]\n    assert img[\"src\"].startswith(\"data:image;base64,\")\n    assert img[\"rev\"] == \"image.png\"\n\n\ndef test_get_renderers(tmp_dir, mocker):\n    live = Live()\n\n    live.log_param(\"string\", \"goo\")\n    live.log_param(\"number\", 2)\n\n    for i in range(2):\n        live.log_metric(\"foo/bar\", i)\n        live.next_step()\n\n    scalar_renderers = get_scalar_renderers(tmp_dir / live.plots_dir / Metric.subfolder)\n    assert len(scalar_renderers) == 1\n    assert scalar_renderers[0].datapoints == [\n        {\n            \"bar\": \"0\",\n            \"rev\": \"workspace\",\n            \"step\": \"0\",\n        },\n        {\n            \"bar\": \"1\",\n            \"rev\": \"workspace\",\n            \"step\": \"1\",\n        },\n    ]\n    assert scalar_renderers[0].properties[\"y\"] == \"bar\"\n    assert scalar_renderers[0].properties[\"title\"] == \"foo/bar\"\n    assert scalar_renderers[0].name == \"static/foo/bar\"\n\n    metrics_renderer = get_metrics_renderers(live.metrics_file)[0]\n    assert metrics_renderer.datapoints == [{\"step\": 1, \"foo\": {\"bar\": 1}}]\n\n    params_renderer = get_params_renderers(live.params_file)[0]\n    assert params_renderer.datapoints == [{\"string\": \"goo\", \"number\": 2}]\n\n\ndef test_report_init(monkeypatch, mocker):\n    mocker.patch(\"dvclive.live.inside_notebook\", return_value=False)\n    live = Live(report=\"notebook\")\n    assert live._report_mode is None\n\n    mocker.patch(\"dvclive.live.matplotlib_installed\", return_value=False)\n    live = Live(report=\"md\")\n    assert live._report_mode is None\n\n    mocker.patch(\"dvclive.live.matplotlib_installed\", return_value=True)\n    live = Live(report=\"md\")\n    assert live._report_mode == \"md\"\n\n    live = Live(report=\"html\")\n    assert live._report_mode == \"html\"\n\n    with pytest.raises(InvalidReportModeError, match=\"Got foo instead\\\\.\"):\n        Live(report=\"foo\")\n\n\n@pytest.mark.parametrize(\"mode\", [\"html\", \"md\"])\ndef test_make_report(tmp_dir, mode):\n    last_report = \"\"\n    live = Live(report=mode)\n    for i in range(3):\n        live.log_metric(\"foobar\", i)\n        live.log_metric(\"foo/bar\", i)\n        live.make_report()\n        live.next_step()\n        assert (tmp_dir / live.report_file).exists()\n        current_report = (tmp_dir / live.report_file).read_text()\n        assert last_report != current_report\n        last_report = current_report\n\n\n@pytest.mark.vscode\ndef test_make_report_open(tmp_dir, mocker, monkeypatch):\n    mocked_open = mocker.patch(\"webbrowser.open\")\n    live = Live()\n    live.log_sklearn_plot(\"confusion_matrix\", [0, 0, 1, 1], [1, 0, 0, 1])\n    live.make_report()\n    live.make_report()\n\n    assert not mocked_open.called\n\n    live = Live(report=\"html\")\n    live.log_metric(\"foo\", 1)\n    live.next_step()\n\n    assert not mocked_open.called\n\n    monkeypatch.setenv(DVCLIVE_OPEN, \"true\")\n\n    live = Live(report=\"html\")\n    live.log_sklearn_plot(\"confusion_matrix\", [0, 0, 1, 1], [1, 0, 0, 1])\n    live.make_report()\n\n    mocked_open.assert_called_once()\n\n\ndef test_get_plot_renderers_sklearn(tmp_dir):\n    live = Live()\n\n    for _ in range(2):\n        live.log_sklearn_plot(\"confusion_matrix\", [0, 0, 1, 1], [1, 0, 0, 1])\n        live.log_sklearn_plot(\n            \"confusion_matrix\", [0, 0, 1, 1], [1, 0, 0, 1], name=\"train/cm\"\n        )\n        live.log_sklearn_plot(\"roc\", [0, 0, 1, 1], [1, 0.1, 0, 1], name=\"roc_curve\")\n        live.log_sklearn_plot(\n            \"roc\", [0, 0, 1, 1], [1, 0.1, 0, 1], name=\"other_roc.json\"\n        )\n        live.next_step()\n\n    plot_renderers = get_sklearn_plot_renderers(\n        tmp_dir / live.plots_dir / SKLearnPlot.subfolder, live\n    )\n    assert len(plot_renderers) == 4\n    plot_renderers_dict = {\n        plot_renderer.name: plot_renderer for plot_renderer in plot_renderers\n    }\n    for name in (\"roc_curve\", \"other_roc\"):\n        plot_renderer = plot_renderers_dict[name]\n        assert plot_renderer.datapoints == [\n            {\"fpr\": 0.0, \"rev\": \"workspace\", \"threshold\": np.inf, \"tpr\": 0.0},\n            {\"fpr\": 0.5, \"rev\": \"workspace\", \"threshold\": 1.0, \"tpr\": 0.5},\n            {\"fpr\": 1.0, \"rev\": \"workspace\", \"threshold\": 0.1, \"tpr\": 0.5},\n            {\"fpr\": 1.0, \"rev\": \"workspace\", \"threshold\": 0.0, \"tpr\": 1.0},\n        ]\n        assert plot_renderer.properties == live._plots[name].plot_config\n\n    for name in (\"confusion_matrix\", \"train/cm\"):\n        plot_renderer = plot_renderers_dict[name]\n        assert plot_renderer.datapoints == [\n            {\"actual\": \"0\", \"rev\": \"workspace\", \"predicted\": \"1\"},\n            {\"actual\": \"0\", \"rev\": \"workspace\", \"predicted\": \"0\"},\n            {\"actual\": \"1\", \"rev\": \"workspace\", \"predicted\": \"0\"},\n            {\"actual\": \"1\", \"rev\": \"workspace\", \"predicted\": \"1\"},\n        ]\n        assert plot_renderer.properties == live._plots[name].plot_config\n\n\ndef test_get_plot_renderers_custom(tmp_dir):\n    live = Live()\n\n    datapoints = [{\"x\": 1, \"y\": 2}, {\"x\": 3, \"y\": 4}]\n    for _ in range(2):\n        live.log_plot(\"foo_default\", datapoints, x=\"x\", y=\"y\")\n        live.log_plot(\n            \"foo_scatter\",\n            datapoints,\n            x=\"x\",\n            y=\"y\",\n            template=\"scatter\",\n        )\n        live.next_step()\n    plot_renderers = get_custom_plot_renderers(\n        tmp_dir / live.plots_dir / CustomPlot.subfolder, live\n    )\n\n    assert len(plot_renderers) == 2\n    plot_renderers_dict = {\n        plot_renderer.name: plot_renderer for plot_renderer in plot_renderers\n    }\n    for name in (\"foo_default\", \"foo_scatter\"):\n        plot_renderer = plot_renderers_dict[name]\n        assert plot_renderer.datapoints == [\n            {\"rev\": \"workspace\", \"x\": 1, \"y\": 2},\n            {\"rev\": \"workspace\", \"x\": 3, \"y\": 4},\n        ]\n        assert plot_renderer.properties == live._plots[name].plot_config\n\n\ndef test_report_notebook(tmp_dir, mocker):\n    mocker.patch(\"dvclive.live.inside_notebook\", return_value=True)\n    mocked_display = mocker.MagicMock()\n    mocker.patch(\"IPython.display.display\", return_value=mocked_display)\n    live = Live(report=\"notebook\")\n    assert live._report_mode == \"notebook\"\n    live.make_report()\n    assert mocked_display.update.called\n"
  },
  {
    "path": "tests/test_make_summary.py",
    "content": "import json\n\nfrom dvclive import Live\nfrom dvclive.plots import Metric\n\n\ndef test_make_summary_without_calling_log(tmp_dir):\n    dvclive = Live()\n\n    dvclive.summary[\"foo\"] = 1.0\n    dvclive.make_summary()\n\n    assert json.loads((tmp_dir / dvclive.metrics_file).read_text()) == {\n        # no `step`\n        \"foo\": 1.0\n    }\n    log_file = tmp_dir / dvclive.plots_dir / Metric.subfolder / \"foo.tsv\"\n    assert not log_file.exists()\n\n\ndef test_make_summary_is_called_on_end(tmp_dir):\n    live = Live()\n\n    live.summary[\"foo\"] = 1.0\n    live.end()\n\n    assert json.loads((tmp_dir / live.metrics_file).read_text()) == {\n        # no `step`\n        \"foo\": 1.0\n    }\n    log_file = tmp_dir / live.plots_dir / Metric.subfolder / \"foo.tsv\"\n    assert not log_file.exists()\n\n\ndef test_make_summary_on_end_dont_increment_step(tmp_dir):\n    with Live() as live:\n        for i in range(2):\n            live.log_metric(\"foo\", i)\n            live.next_step()\n\n    assert json.loads((tmp_dir / live.metrics_file).read_text()) == {\n        \"foo\": 1.0,\n        \"step\": 1,\n    }\n"
  },
  {
    "path": "tests/test_monitor_system.py",
    "content": "import time\nfrom pathlib import Path\n\nimport dpath\nimport pytest\nfrom pytest_voluptuous import S\n\nfrom dvclive import Live\nfrom dvclive.monitor_system import (\n    GIGABYTES_DIVIDER,\n    METRIC_CPU_COUNT,\n    METRIC_CPU_PARALLELIZATION_PERCENT,\n    METRIC_CPU_USAGE_PERCENT,\n    METRIC_DISK_TOTAL_GB,\n    METRIC_DISK_USAGE_GB,\n    METRIC_DISK_USAGE_PERCENT,\n    METRIC_GPU_COUNT,\n    METRIC_GPU_USAGE_PERCENT,\n    METRIC_RAM_TOTAL_GB,\n    METRIC_RAM_USAGE_GB,\n    METRIC_RAM_USAGE_PERCENT,\n    METRIC_VRAM_TOTAL_GB,\n    METRIC_VRAM_USAGE_GB,\n    METRIC_VRAM_USAGE_PERCENT,\n    _SystemMonitor,\n)\nfrom dvclive.utils import parse_metrics\n\n\ndef mock_psutil_cpu(mocker):\n    mocker.patch(\n        \"dvclive.monitor_system.psutil.cpu_percent\",\n        return_value=[10, 10, 10, 40, 50, 60],\n    )\n    mocker.patch(\"dvclive.monitor_system.psutil.cpu_count\", return_value=6)\n\n\ndef mock_psutil_ram(mocker):\n    mocked_ram = mocker.MagicMock()\n    mocked_ram.percent = 50\n    mocked_ram.used = 2 * GIGABYTES_DIVIDER\n    mocked_ram.total = 4 * GIGABYTES_DIVIDER\n    mocker.patch(\n        \"dvclive.monitor_system.psutil.virtual_memory\", return_value=mocked_ram\n    )\n\n\ndef mock_psutil_disk(mocker):\n    mocked_disk = mocker.MagicMock()\n    mocked_disk.percent = 50\n    mocked_disk.used = 16 * GIGABYTES_DIVIDER\n    mocked_disk.total = 32 * GIGABYTES_DIVIDER\n    mocker.patch(\"dvclive.monitor_system.psutil.disk_usage\", return_value=mocked_disk)\n\n\ndef mock_psutil_disk_with_oserror(mocker):\n    mocked_disk = mocker.MagicMock()\n    mocked_disk.percent = 50\n    mocked_disk.used = 16 * GIGABYTES_DIVIDER\n    mocked_disk.total = 32 * GIGABYTES_DIVIDER\n    mocker.patch(\n        \"dvclive.monitor_system.psutil.disk_usage\",\n        side_effect=[\n            mocked_disk,\n            OSError,\n            mocked_disk,\n            OSError,\n        ],\n    )\n\n\ndef mock_pynvml(mocker, num_gpus=2):\n    prefix = \"dvclive.monitor_system\"\n    mocker.patch(f\"{prefix}.GPU_AVAILABLE\", bool(num_gpus))\n    mocker.patch(f\"{prefix}.nvmlDeviceGetCount\", return_value=num_gpus)\n    mocker.patch(f\"{prefix}.nvmlInit\", return_value=None)\n    mocker.patch(f\"{prefix}.nvmlShutdown\", return_value=None)\n    mocker.patch(f\"{prefix}.nvmlDeviceGetHandleByIndex\", return_value=None)\n\n    vram_info = mocker.MagicMock()\n    vram_info.used = 3 * 1024**3\n    vram_info.total = 6 * 1024**3\n\n    gpu_usage = mocker.MagicMock()\n    gpu_usage.memory = 5\n    gpu_usage.gpu = 10\n\n    mocker.patch(f\"{prefix}.nvmlDeviceGetMemoryInfo\", return_value=vram_info)\n    mocker.patch(f\"{prefix}.nvmlDeviceGetUtilizationRates\", return_value=gpu_usage)\n\n\n@pytest.fixture\ndef cpu_metrics():\n    content = {\n        METRIC_CPU_COUNT: 6,\n        METRIC_CPU_USAGE_PERCENT: 30.0,\n        METRIC_CPU_PARALLELIZATION_PERCENT: 50.0,\n        METRIC_RAM_USAGE_PERCENT: 50.0,\n        METRIC_RAM_USAGE_GB: 2.0,\n        METRIC_RAM_TOTAL_GB: 4.0,\n        f\"{METRIC_DISK_USAGE_PERCENT}/main\": 50.0,\n        f\"{METRIC_DISK_USAGE_GB}/main\": 16.0,\n        f\"{METRIC_DISK_TOTAL_GB}/main\": 32.0,\n    }\n    result = {}\n    for name, value in content.items():\n        dpath.new(result, name, value)\n    return result\n\n\ndef _timeserie_schema(name, value):\n    return [{name: str(value), \"timestamp\": str, \"step\": \"0\"}]\n\n\n@pytest.fixture\ndef cpu_timeseries():\n    return {\n        f\"{METRIC_CPU_USAGE_PERCENT}.tsv\": _timeserie_schema(\n            METRIC_CPU_USAGE_PERCENT.split(\"/\")[-1], 30.0\n        ),\n        f\"{METRIC_CPU_PARALLELIZATION_PERCENT}.tsv\": _timeserie_schema(\n            METRIC_CPU_PARALLELIZATION_PERCENT.split(\"/\")[-1], 50.0\n        ),\n        f\"{METRIC_RAM_USAGE_PERCENT}.tsv\": _timeserie_schema(\n            METRIC_RAM_USAGE_PERCENT.split(\"/\")[-1], 50.0\n        ),\n        f\"{METRIC_RAM_USAGE_GB}.tsv\": _timeserie_schema(\n            METRIC_RAM_USAGE_GB.split(\"/\")[-1], 2.0\n        ),\n        f\"{METRIC_DISK_USAGE_PERCENT}/main.tsv\": _timeserie_schema(\"main\", 50.0),\n        f\"{METRIC_DISK_USAGE_GB}/main.tsv\": _timeserie_schema(\"main\", 16.0),\n    }\n\n\n@pytest.fixture\ndef gpu_timeseries():\n    return {\n        f\"{METRIC_GPU_USAGE_PERCENT}/0.tsv\": _timeserie_schema(\"0\", 50.0),\n        f\"{METRIC_GPU_USAGE_PERCENT}/1.tsv\": _timeserie_schema(\"1\", 50.0),\n        f\"{METRIC_VRAM_USAGE_PERCENT}/0.tsv\": _timeserie_schema(\"0\", 50.0),\n        f\"{METRIC_VRAM_USAGE_PERCENT}/1.tsv\": _timeserie_schema(\"1\", 50.0),\n        f\"{METRIC_VRAM_USAGE_GB}/0.tsv\": _timeserie_schema(\"0\", 3.0),\n        f\"{METRIC_VRAM_USAGE_GB}/1.tsv\": _timeserie_schema(\"1\", 3.0),\n    }\n\n\ndef test_monitor_system_is_false(tmp_dir, mocker):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk(mocker)\n    mock_pynvml(mocker, num_gpus=0)\n    system_monitor_mock = mocker.patch(\n        \"dvclive.live._SystemMonitor\", spec=_SystemMonitor\n    )\n    Live(tmp_dir, save_dvc_exp=False, monitor_system=False)\n    system_monitor_mock.assert_not_called()\n\n\ndef test_monitor_system_is_true(tmp_dir, mocker):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk(mocker)\n    mock_pynvml(mocker, num_gpus=0)\n    system_monitor_mock = mocker.patch(\n        \"dvclive.live._SystemMonitor\", spec=_SystemMonitor\n    )\n\n    Live(tmp_dir, save_dvc_exp=False, monitor_system=True)\n    system_monitor_mock.assert_called_once()\n\n\ndef test_all_threads_close(tmp_dir, mocker):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk(mocker)\n    mock_pynvml(mocker, num_gpus=0)\n\n    with Live(\n        tmp_dir,\n        save_dvc_exp=False,\n        monitor_system=True,\n    ) as live:\n        first_end_spy = mocker.spy(live._system_monitor, \"end\")\n        first_end_spy.assert_not_called()\n\n        live.monitor_system(interval=0.01)\n        first_end_spy.assert_called_once()\n\n        second_end_spy = mocker.spy(live._system_monitor, \"end\")\n\n    # check the monitoring thread is stopped\n    second_end_spy.assert_called_once()\n\n\ndef test_ignore_non_existent_directories(tmp_dir, mocker):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk_with_oserror(mocker)\n    mock_pynvml(mocker, num_gpus=0)\n    with Live(\n        tmp_dir,\n        save_dvc_exp=False,\n        monitor_system=False,\n    ) as live:\n        non_existent_disk = \"/non-existent\"\n        system_monitor = _SystemMonitor(\n            live=live,\n            interval=0.1,\n            num_samples=4,\n            directories_to_monitor={\"main\": \"/\", \"non-existent\": non_existent_disk},\n        )\n        metrics = system_monitor._get_metrics()\n        system_monitor.end()\n\n    assert not Path(non_existent_disk).exists()\n\n    assert f\"{METRIC_DISK_USAGE_PERCENT}/non-existent\" not in metrics\n    assert f\"{METRIC_DISK_USAGE_GB}/non-existent\" not in metrics\n    assert f\"{METRIC_DISK_TOTAL_GB}/non-existent\" not in metrics\n\n\n@pytest.mark.timeout(2)\ndef test_monitor_system_metrics(tmp_dir, cpu_metrics, mocker):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk(mocker)\n    mock_pynvml(mocker, num_gpus=0)\n    with Live(\n        tmp_dir,\n        save_dvc_exp=False,\n        monitor_system=False,\n    ) as live:\n        live.monitor_system(interval=0.05, num_samples=4)\n        # wait for the metrics to be logged.\n        # METRIC_DISK_TOTAL_GB is the last metric to be logged.\n        while len(dpath.search(live.summary, METRIC_DISK_TOTAL_GB)) == 0:\n            time.sleep(0.001)\n        live.next_step()\n\n        _, latest = parse_metrics(live)\n\n    schema = {\"step\": 0, **cpu_metrics}\n    assert latest == S(schema)\n\n\n@pytest.mark.timeout(2)\ndef test_monitor_system_timeseries(tmp_dir, cpu_timeseries, mocker):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk(mocker)\n    mock_pynvml(mocker, num_gpus=0)\n    with Live(\n        tmp_dir,\n        save_dvc_exp=False,\n        monitor_system=False,\n    ) as live:\n        live.monitor_system(interval=0.05, num_samples=4)\n\n        # wait for the metrics to be logged.\n        # METRIC_DISK_TOTAL_GB is the last metric to be logged.\n        while len(dpath.search(live.summary, METRIC_DISK_TOTAL_GB)) == 0:\n            time.sleep(0.001)\n\n        live.next_step()\n\n        timeseries, _ = parse_metrics(live)\n\n    prefix = Path(tmp_dir) / \"plots/metrics\"\n    schema = {str(prefix / name): value for name, value in cpu_timeseries.items()}\n    assert timeseries == S(schema)\n\n\n@pytest.mark.timeout(2)\ndef test_monitor_system_metrics_with_gpu(tmp_dir, cpu_metrics, mocker):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk(mocker)\n    mock_pynvml(mocker, num_gpus=2)\n    with Live(\n        tmp_dir,\n        save_dvc_exp=False,\n        monitor_system=False,\n    ) as live:\n        live.monitor_system(interval=0.05, num_samples=4)\n        # wait for the metrics to be logged.\n        # METRIC_DISK_TOTAL_GB is the last metric to be logged.\n        while len(dpath.search(live.summary, METRIC_DISK_TOTAL_GB)) == 0:\n            time.sleep(0.001)\n        live.next_step()\n\n        _, latest = parse_metrics(live)\n\n    schema = {\"step\": 0, **cpu_metrics}\n    gpu_content = {\n        METRIC_GPU_COUNT: 2,\n        f\"{METRIC_GPU_USAGE_PERCENT}\": {\"0\": 50.0, \"1\": 50.0},\n        f\"{METRIC_VRAM_USAGE_PERCENT}\": {\"0\": 50.0, \"1\": 50.0},\n        f\"{METRIC_VRAM_USAGE_GB}\": {\"0\": 3.0, \"1\": 3.0},\n        f\"{METRIC_VRAM_TOTAL_GB}\": {\"0\": 6.0, \"1\": 6.0},\n    }\n    for name, value in gpu_content.items():\n        dpath.new(schema, name, value)\n    assert latest == S(schema)\n\n\n@pytest.mark.timeout(2)\ndef test_monitor_system_timeseries_with_gpu(\n    tmp_dir, cpu_timeseries, gpu_timeseries, mocker\n):\n    mock_psutil_cpu(mocker)\n    mock_psutil_ram(mocker)\n    mock_psutil_disk(mocker)\n    mock_pynvml(mocker, num_gpus=2)\n    with Live(\n        tmp_dir,\n        save_dvc_exp=False,\n        monitor_system=False,\n    ) as live:\n        live.monitor_system(interval=0.05, num_samples=4)\n\n        # wait for the metrics to be logged.\n        # METRIC_DISK_TOTAL_GB is the last metric to be logged.\n        while len(dpath.search(live.summary, METRIC_DISK_TOTAL_GB)) == 0:\n            time.sleep(0.001)\n\n        live.next_step()\n\n        timeseries, _ = parse_metrics(live)\n\n    prefix = Path(tmp_dir) / \"plots/metrics\"\n    schema = {str(prefix / name): value for name, value in cpu_timeseries.items()}\n    schema.update({str(prefix / name): value for name, value in gpu_timeseries.items()})\n    assert timeseries == S(schema)\n"
  },
  {
    "path": "tests/test_post_to_studio.py",
    "content": "import time\nimport unittest\nfrom collections import defaultdict\nfrom copy import deepcopy\nfrom pathlib import Path\n\nimport pytest\nfrom dvc.env import DVC_EXP_GIT_REMOTE\nfrom dvc_studio_client import DEFAULT_STUDIO_URL\nfrom dvc_studio_client.env import DVC_STUDIO_REPO_URL, DVC_STUDIO_TOKEN\nfrom PIL import Image as ImagePIL\n\nfrom dvclive import Live\nfrom dvclive.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME, DVC_ROOT\nfrom dvclive.plots import Image, Metric\nfrom dvclive.studio import _adapt_image, get_dvc_studio_config, post_to_studio\n\n\ndef get_studio_call(event_type, exp_name, **kwargs):\n    data = {\n        \"type\": event_type,\n        \"name\": exp_name,\n        \"repo_url\": \"STUDIO_REPO_URL\",\n        \"baseline_sha\": kwargs.pop(\"baseline_sha\", None) or \"f\" * 40,\n        \"client\": \"dvclive\",\n    } | kwargs\n\n    return {\n        \"json\": data,\n        \"headers\": {\n            \"Authorization\": \"token STUDIO_TOKEN\",\n            \"Content-type\": \"application/json\",\n        },\n        \"timeout\": (30, 5),\n    }\n\n\ndef test_post_to_studio(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    live = Live()\n    live.log_param(\"fooparam\", 1)\n\n    foo_path = (Path(live.plots_dir) / Metric.subfolder / \"foo.tsv\").as_posix()\n\n    mocked_post, _ = mocked_studio_post\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\", **get_studio_call(\"start\", exp_name=live._exp_name)\n    )\n\n    live.log_metric(\"foo\", 1)\n    live.step = 0\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            exp_name=live._exp_name,\n            step=0,\n            plots={f\"{foo_path}\": {\"data\": [{\"step\": 0, \"foo\": 1.0}]}},\n        ),\n    )\n\n    live.step += 1\n    live.log_metric(\"foo\", 2)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            exp_name=live._exp_name,\n            step=1,\n            plots={f\"{foo_path}\": {\"data\": [{\"step\": 1, \"foo\": 2.0}]}},\n        ),\n    )\n\n    mocked_post.reset_mock()\n    live.save_dvc_exp()\n    data = live._get_live_data()\n    post_to_studio(live, \"done\", data)\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"done\", exp_name=live._exp_name, experiment_rev=live._experiment_rev\n        ),\n    )\n\n\ndef test_post_to_studio_subrepo(tmp_dir, mocked_dvc_subrepo, mocked_studio_post):\n    live = Live()\n    live.log_param(\"fooparam\", 1)\n\n    mocked_post, _ = mocked_studio_post\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\"start\", exp_name=live._exp_name, subdir=\"subdir\"),\n    )\n\n\ndef test_post_to_studio_repo_url(tmp_dir, dvc_repo, mocked_studio_post, monkeypatch):\n    monkeypatch.setenv(DVC_EXP_GIT_REMOTE, \"dvc_exp_git_remote\")\n\n    live = Live()\n    live.log_param(\"fooparam\", 1)\n\n    mocked_post, _ = mocked_studio_post\n\n    assert mocked_post.call_args.kwargs[\"json\"][\"repo_url\"] == \"dvc_exp_git_remote\"\n\n\ndef test_post_to_studio_failed_data_request(\n    tmp_dir, mocker, mocked_dvc_repo, mocked_studio_post\n):\n    mocked_post, valid_response = mocked_studio_post\n\n    live = Live()\n\n    foo_path = (Path(live.plots_dir) / Metric.subfolder / \"foo.tsv\").as_posix()\n\n    error_response = mocker.MagicMock()\n    error_response.status_code = 400\n    mocker.patch(\"requests.post\", return_value=error_response)\n    live.log_metric(\"foo\", 1)\n    live.step = 0\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    mocked_post = mocker.patch(\"requests.post\", return_value=valid_response)\n    live.step += 1\n    live.log_metric(\"foo\", 2)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            exp_name=live._exp_name,\n            step=1,\n            plots={\n                f\"{foo_path}\": {\n                    \"data\": [{\"step\": 0, \"foo\": 1.0}, {\"step\": 1, \"foo\": 2.0}]\n                }\n            },\n        ),\n    )\n\n\ndef test_post_to_studio_failed_start_request(\n    tmp_dir, mocker, mocked_dvc_repo, mocked_studio_post\n):\n    mocked_response = mocker.MagicMock()\n    mocked_response.status_code = 400\n    mocked_post = mocker.patch(\"requests.post\", return_value=mocked_response)\n\n    live = Live()\n\n    live.log_metric(\"foo\", 1)\n    live.next_step()\n\n    live.log_metric(\"foo\", 2)\n    live.next_step()\n\n    assert mocked_post.call_count == 1\n    assert live._studio_events_to_skip == {\"start\", \"data\", \"done\"}\n\n\ndef test_post_to_studio_done_only_once(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    mocked_post, _ = mocked_studio_post\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.next_step()\n\n    expected_done_calls = [\n        call\n        for call in mocked_post.call_args_list\n        if call.kwargs[\"json\"][\"type\"] == \"done\"\n    ]\n    live.end()\n    actual_done_calls = [\n        call\n        for call in mocked_post.call_args_list\n        if call.kwargs[\"json\"][\"type\"] == \"done\"\n    ]\n    assert expected_done_calls == actual_done_calls\n\n\ndef test_post_to_studio_snapshots_data_to_send(\n    tmp_dir, mocked_dvc_repo, mocked_studio_post\n):\n    # Tests race condition between main app thread and Studio post thread\n    # where the main thread can be faster in producing metrics than the\n    # Studio post thread in sending them.\n    mocked_post, _ = mocked_studio_post\n\n    calls = defaultdict(dict)\n\n    def _long_post(*_, **kwargs):\n        if kwargs[\"json\"][\"type\"] == \"data\":\n            # Mock by default doesn't copy lists, dict, we share \"body\" var in\n            # some calls, thus we can't rely on `mocked_post.call_args_list`\n            json = deepcopy(kwargs)[\"json\"]\n            step = json[\"step\"]\n            for key in [\"metrics\", \"params\", \"plots\"]:\n                if key in json:\n                    calls[step][key] = json[key]\n            time.sleep(0.1)\n        return unittest.mock.DEFAULT\n\n    mocked_post.side_effect = lambda *args, **kwargs: _long_post(*args, **kwargs)\n\n    live = Live()\n    for i in range(10):\n        live.log_metric(\"foo\", i)\n        live.log_param(f\"fooparam-{i}\", i)\n        live.log_image(f\"foo.{i}.png\", ImagePIL.new(\"RGB\", (i + 1, i + 1), (0, 0, 0)))\n        live.next_step()\n\n    live._wait_for_studio_updates_posted()\n\n    assert len(calls) == 10\n    for i in range(10):\n        call = calls[i]\n        assert call[\"metrics\"] == {\n            \"dvclive/metrics.json\": {\"data\": {\"foo\": i, \"step\": i}}\n        }\n        assert call[\"params\"] == {\n            \"dvclive/params.yaml\": {f\"fooparam-{k}\": k for k in range(i + 1)}\n        }\n        # Check below that `plots`` has the following shape\n        # {\n        #    'dvclive/plots/metrics/foo.tsv': {'data': [{'step': i, 'foo': float(i)}]},\n        #    f\"dvclive/plots/images/foo.{i}.png\": {'image': '...'}\n        # }\n        assert len(call[\"plots\"]) == 2\n        foo_data = call[\"plots\"][\"dvclive/plots/metrics/foo.tsv\"][\"data\"]\n        assert len(foo_data) == 1\n        assert foo_data[0][\"step\"] == i\n        assert foo_data[0][\"foo\"] == pytest.approx(float(i))\n        assert call[\"plots\"][f\"dvclive/plots/images/foo.{i}.png\"][\"image\"]\n\n\ndef test_studio_updates_posted_on_end(tmp_path, mocked_dvc_repo, mocked_studio_post):\n    mocked_post, valid_response = mocked_studio_post\n    metrics_file = tmp_path / \"metrics.json\"\n    metrics_content = \"metrics\"\n\n    def long_post(*args, **kwargs):\n        # in case of `data` `long_post` should be called from a separate thread,\n        # meanwhile main thread go forward without slowing down, so if there is no\n        # some kind of wait in the Live main thread, then it will complete before\n        # we even can have a chance to write the file below\n        if kwargs[\"json\"][\"type\"] == \"data\":\n            time.sleep(1)\n            metrics_file.write_text(metrics_content)\n\n        return valid_response\n\n    mocked_post.side_effect = long_post\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n\n    assert metrics_file.read_text() == metrics_content\n\n\ndef test_studio_update_raises_exception(tmp_path, mocked_dvc_repo, mocked_studio_post):\n    # Test that if a studio update raises an exception, main process doesn't hang on\n    # queue join in the Live main thread.\n    # https://github.com/iterative/dvclive/pull/864\n    mocked_post, valid_response = mocked_studio_post\n\n    def post_raises_exception(*args, **kwargs):\n        if kwargs[\"json\"][\"type\"] == \"data\":\n            # We'll hit this sleep only once, other calls are ignored\n            # after the exception is raised\n            time.sleep(1)\n            raise Exception(\"test exception\")  # noqa: TRY002, TRY003\n        return valid_response\n\n    mocked_post.side_effect = post_raises_exception\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.log_metric(\"foo\", 2)\n        live.log_metric(\"foo\", 3)\n\n    # Only 1 data call is made, other calls are ignored after the exception is raised\n    assert mocked_post.call_count == 3\n    assert [e.kwargs[\"json\"][\"type\"] for e in mocked_post.call_args_list] == [\n        \"start\",\n        \"data\",\n        \"done\",\n    ]\n\n\n@pytest.mark.studio\ndef test_post_to_studio_skip_start_and_done_on_env_var(\n    tmp_dir, mocked_dvc_repo, mocked_studio_post, monkeypatch\n):\n    mocked_post, _ = mocked_studio_post\n\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"f\" * 40)\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n    monkeypatch.setenv(DVC_ROOT, tmp_dir)\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.next_step()\n\n    call_types = [call.kwargs[\"json\"][\"type\"] for call in mocked_post.call_args_list]\n    assert \"start\" not in call_types\n    assert \"done\" not in call_types\n\n\n@pytest.mark.studio\ndef test_post_to_studio_dvc_studio_config(\n    tmp_dir, mocker, mocked_dvc_repo, mocked_studio_post, monkeypatch\n):\n    mocked_post, _ = mocked_studio_post\n\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"f\" * 40)\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n    monkeypatch.setenv(DVC_ROOT, tmp_dir)\n    monkeypatch.delenv(DVC_STUDIO_TOKEN)\n\n    mocked_dvc_repo.config = {\"studio\": {\"token\": \"token\"}}\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.step = 0\n        live.make_summary()\n        data = live._get_live_data()\n        post_to_studio(live, \"data\", data)\n\n    assert mocked_post.call_args.kwargs[\"headers\"][\"Authorization\"] == \"token token\"\n\n\n@pytest.mark.studio\ndef test_post_to_studio_skip_if_no_token(\n    tmp_dir,\n    mocker,\n    monkeypatch,\n    mocked_dvc_repo,\n):\n    mocked_post = mocker.patch(\"dvclive.studio.post_live_metrics\", return_value=None)\n\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"f\" * 40)\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n\n    mocked_dvc_repo.config = {}\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.step = 0\n        live.make_summary()\n        data = live._get_live_data()\n        post_to_studio(live, \"data\", data)\n\n    assert mocked_post.call_count == 0\n\n\ndef test_post_to_studio_shorten_names(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    mocked_post, _ = mocked_studio_post\n\n    live = Live()\n    live.log_metric(\"eval/loss\", 1)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    plots_path = Path(live.plots_dir)\n    loss_path = (plots_path / Metric.subfolder / \"eval/loss.tsv\").as_posix()\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            exp_name=live._exp_name,\n            step=0,\n            plots={f\"{loss_path}\": {\"data\": [{\"step\": 0, \"loss\": 1.0}]}},\n        ),\n    )\n\n\n@pytest.mark.studio\ndef test_post_to_studio_inside_dvc_exp(\n    tmp_dir, mocker, monkeypatch, mocked_studio_post, mocked_dvc_repo\n):\n    mocked_post, _ = mocked_studio_post\n\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"f\" * 40)\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n    monkeypatch.setenv(DVC_ROOT, tmp_dir)\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.step = 0\n        live.make_summary()\n        data = live._get_live_data()\n        post_to_studio(live, \"data\", data)\n\n    call_types = [call.kwargs[\"json\"][\"type\"] for call in mocked_post.call_args_list]\n    assert \"start\" not in call_types\n    assert \"done\" not in call_types\n\n\n@pytest.mark.studio\ndef test_post_to_studio_inside_subdir(\n    tmp_dir, dvc_repo, mocker, monkeypatch, mocked_studio_post, mocked_dvc_repo\n):\n    mocked_post, _ = mocked_studio_post\n    subdir = tmp_dir / \"subdir\"\n    subdir.mkdir()\n    monkeypatch.chdir(subdir)\n\n    live = Live()\n    live.log_metric(\"foo\", 1)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    foo_path = (Path(live.plots_dir) / Metric.subfolder / \"foo.tsv\").as_posix()\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            baseline_sha=live._baseline_rev,\n            exp_name=live._exp_name,\n            step=0,\n            plots={f\"subdir/{foo_path}\": {\"data\": [{\"step\": 0, \"foo\": 1.0}]}},\n        ),\n    )\n\n\n@pytest.mark.studio\ndef test_post_to_studio_inside_subdir_dvc_exp(\n    tmp_dir, dvc_repo, monkeypatch, mocked_studio_post, mocked_dvc_repo\n):\n    mocked_post, _ = mocked_studio_post\n    subdir = tmp_dir / \"subdir\"\n    subdir.mkdir()\n    monkeypatch.chdir(subdir)\n\n    monkeypatch.setenv(DVC_EXP_BASELINE_REV, \"f\" * 40)\n    monkeypatch.setenv(DVC_EXP_NAME, \"bar\")\n\n    live = Live()\n    live.log_metric(\"foo\", 1)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    foo_path = (Path(live.plots_dir) / Metric.subfolder / \"foo.tsv\").as_posix()\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            baseline_sha=live._baseline_rev,\n            exp_name=live._exp_name,\n            step=0,\n            plots={f\"subdir/{foo_path}\": {\"data\": [{\"step\": 0, \"foo\": 1.0}]}},\n        ),\n    )\n\n\ndef test_post_to_studio_without_exp(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    assert not Live(save_dvc_exp=False)._studio_events_to_skip\n\n\ndef test_get_dvc_studio_config_none(mocker):\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=None)\n    live = Live()\n    assert get_dvc_studio_config(live) == {}\n\n\ndef test_get_dvc_studio_config_env_var(monkeypatch, mocker):\n    monkeypatch.setenv(DVC_STUDIO_TOKEN, \"token\")\n    monkeypatch.setenv(DVC_STUDIO_REPO_URL, \"repo_url\")\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=None)\n    live = Live()\n    assert get_dvc_studio_config(live) == {\n        \"token\": \"token\",\n        \"repo_url\": \"repo_url\",\n        \"url\": DEFAULT_STUDIO_URL,\n    }\n\n\ndef test_get_dvc_studio_config_dvc_repo(mocked_dvc_repo):\n    mocked_dvc_repo.config = {\"studio\": {\"token\": \"token\", \"repo_url\": \"repo_url\"}}\n    live = Live()\n    assert get_dvc_studio_config(live) == {\n        \"token\": \"token\",\n        \"repo_url\": \"repo_url\",\n        \"url\": DEFAULT_STUDIO_URL,\n    }\n\n\ndef test_post_to_studio_images(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    mocked_post, _ = mocked_studio_post\n\n    live = Live()\n    live.log_image(\"foo.png\", ImagePIL.new(\"RGB\", (10, 10), (0, 0, 0)))\n    live.step = 0\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    foo_path = (Path(live.plots_dir) / Image.subfolder / \"foo.png\").as_posix()\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            baseline_sha=live._baseline_rev,\n            exp_name=live._exp_name,\n            step=0,\n            plots={f\"{foo_path}\": {\"image\": _adapt_image(foo_path)}},\n        ),\n    )\n\n\ndef test_post_to_studio_message(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    live = Live(exp_message=\"Custom message\")\n\n    mocked_post, _ = mocked_studio_post\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\"start\", exp_name=live._exp_name, message=\"Custom message\"),\n    )\n\n\ndef test_post_to_studio_name(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    Live(exp_name=\"custom-name\")\n\n    mocked_post, _ = mocked_studio_post\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\"start\", exp_name=\"custom-name\"),\n    )\n\n\ndef test_post_to_studio_if_done_skipped(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    with Live() as live:\n        live._studio_events_to_skip.add(\"start\")\n        live._studio_events_to_skip.add(\"done\")\n        live.log_metric(\"foo\", 1)\n        live.step = 0\n        live.make_summary()\n        data = live._get_live_data()\n        post_to_studio(live, \"data\", data)\n\n    mocked_post, _ = mocked_studio_post\n    call_types = [call.kwargs[\"json\"][\"type\"] for call in mocked_post.call_args_list]\n    assert \"data\" in call_types\n\n\n@pytest.mark.studio\ndef test_post_to_studio_no_repo(tmp_dir, monkeypatch, mocked_studio_post):\n    monkeypatch.setenv(DVC_STUDIO_TOKEN, \"STUDIO_TOKEN\")\n    monkeypatch.setenv(DVC_STUDIO_REPO_URL, \"STUDIO_REPO_URL\")\n\n    live = Live(save_dvc_exp=True)\n    live.log_param(\"fooparam\", 1)\n\n    foo_path = (Path(live.plots_dir) / Metric.subfolder / \"foo.tsv\").as_posix()\n\n    mocked_post, _ = mocked_studio_post\n\n    mocked_post.assert_called()\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\"start\", baseline_sha=\"0\" * 40, exp_name=live._exp_name),\n    )\n\n    live.log_metric(\"foo\", 1)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            baseline_sha=\"0\" * 40,\n            exp_name=live._exp_name,\n            step=0,\n            plots={f\"{foo_path}\": {\"data\": [{\"step\": 0, \"foo\": 1.0}]}},\n        ),\n    )\n\n    live.step += 1\n    live.log_metric(\"foo\", 2)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            baseline_sha=\"0\" * 40,\n            exp_name=live._exp_name,\n            step=1,\n            plots={f\"{foo_path}\": {\"data\": [{\"step\": 1, \"foo\": 2.0}]}},\n        ),\n    )\n\n    post_to_studio(live, \"done\")\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\"done\", baseline_sha=\"0\" * 40, exp_name=live._exp_name),\n    )\n\n\n@pytest.mark.studio\ndef test_post_to_studio_skip_if_no_repo_url(\n    tmp_dir,\n    mocker,\n    monkeypatch,\n):\n    mocked_post = mocker.patch(\"dvclive.studio.post_live_metrics\", return_value=None)\n\n    monkeypatch.setenv(DVC_STUDIO_TOKEN, \"token\")\n\n    with Live() as live:\n        live.log_metric(\"foo\", 1)\n        live.step = 0\n        live.make_summary()\n        data = live._get_live_data()\n        post_to_studio(live, \"data\", data)\n\n    assert mocked_post.call_count == 0\n\n\ndef test_post_to_studio_repeat_step(tmp_dir, mocked_dvc_repo, mocked_studio_post):\n    # for more context see the PR https://github.com/iterative/dvclive/pull/788\n    live = Live()\n\n    prefix = Path(live.plots_dir) / Metric.subfolder\n    foo_path = (prefix / \"foo.tsv\").as_posix()\n    bar_path = (prefix / \"bar.tsv\").as_posix()\n\n    mocked_post, _ = mocked_studio_post\n\n    live.step = 0\n    live.log_metric(\"foo\", 1)\n    live.log_metric(\"bar\", 0.1)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            exp_name=live._exp_name,\n            step=0,\n            plots={\n                f\"{foo_path}\": {\"data\": [{\"step\": 0, \"foo\": 1.0}]},\n                f\"{bar_path}\": {\"data\": [{\"step\": 0, \"bar\": 0.1}]},\n            },\n        ),\n    )\n\n    live.log_metric(\"foo\", 2)\n    live.log_metric(\"foo\", 3)\n    live.log_metric(\"bar\", 0.2)\n    live.make_summary()\n    data = live._get_live_data()\n    post_to_studio(live, \"data\", data)\n\n    mocked_post.assert_called_with(\n        \"https://0.0.0.0/api/live\",\n        **get_studio_call(\n            \"data\",\n            exp_name=live._exp_name,\n            step=0,\n            plots={\n                f\"{foo_path}\": {\n                    \"data\": [{\"step\": 0, \"foo\": 2.0}, {\"step\": 0, \"foo\": 3.0}]\n                },\n                f\"{bar_path}\": {\"data\": [{\"step\": 0, \"bar\": 0.2}]},\n            },\n        ),\n    )\n"
  },
  {
    "path": "tests/test_resume.py",
    "content": "import pytest\n\nfrom dvclive import Live\nfrom dvclive.env import DVCLIVE_RESUME\nfrom dvclive.utils import read_history, read_latest\n\n\n@pytest.mark.parametrize(\n    (\"resume\", \"steps\", \"metrics\"),\n    [(True, [0, 1, 2, 3], [0.9, 0.8, 0.7, 0.6]), (False, [0, 1], [0.7, 0.6])],\n)\ndef test_resume(tmp_dir, resume, steps, metrics):\n    dvclive = Live(\"logs\")\n\n    for metric in [0.9, 0.8]:\n        dvclive.log_metric(\"metric\", metric)\n        dvclive.next_step()\n    dvclive.log_metric(\"summary\", 1)\n    dvclive.end()\n\n    assert read_history(dvclive, \"metric\") == ([0, 1], [0.9, 0.8])\n    assert read_latest(dvclive, \"metric\") == (1, 0.8)\n\n    dvclive = Live(\"logs\", resume=resume)\n\n    for new_metric in [0.7, 0.6]:\n        dvclive.log_metric(\"metric\", new_metric)\n        dvclive.next_step()\n    dvclive.end()\n\n    assert read_history(dvclive, \"metric\") == (steps, metrics)\n    assert read_latest(dvclive, \"metric\") == (steps[-1], metrics[-1])\n    if resume:\n        assert dvclive.read_latest()[\"summary\"] == 1\n    else:\n        assert \"summary\" not in dvclive.read_latest()\n\n\ndef test_resume_on_first_init(tmp_dir):\n    dvclive = Live(resume=True)\n\n    assert dvclive._step == 0\n\n\ndef test_resume_env_var(tmp_dir, monkeypatch):\n    assert not Live()._resume\n\n    monkeypatch.setenv(DVCLIVE_RESUME, \"true\")\n    assert Live()._resume\n"
  },
  {
    "path": "tests/test_step.py",
    "content": "import os\n\nimport pytest\n\nfrom dvclive import Live\nfrom dvclive.utils import read_history, read_latest\n\n\n@pytest.mark.parametrize(\"metric\", [\"m1\", os.path.join(\"train\", \"m1\")])\ndef test_allow_step_override(tmp_dir, metric):\n    dvclive = Live(\"logs\")\n\n    dvclive.log_metric(metric, 1.0)\n    dvclive.log_metric(metric, 2.0)\n\n\ndef test_custom_steps(tmp_dir):\n    dvclive = Live(\"logs\")\n\n    steps = [0, 62, 1000]\n    metrics = [0.9, 0.8, 0.7]\n\n    for step, metric in zip(steps, metrics):\n        dvclive.step = step\n        dvclive.log_metric(\"m\", metric)\n        dvclive.make_summary()\n\n    assert read_history(dvclive, \"m\") == (steps, metrics)\n    assert read_latest(dvclive, \"m\") == (steps[-1], metrics[-1])\n\n\ndef test_log_reset_with_set_step(tmp_dir):\n    dvclive = Live()\n\n    for i in range(3):\n        dvclive.step = i\n        dvclive.log_metric(\"train_m\", 1)\n        dvclive.make_summary()\n\n    for i in range(3):\n        dvclive.step = i\n        dvclive.log_metric(\"val_m\", 1)\n        dvclive.make_summary()\n\n    assert read_history(dvclive, \"train_m\") == ([0, 1, 2], [1, 1, 1])\n    assert read_history(dvclive, \"val_m\") == ([0, 1, 2], [1, 1, 1])\n    assert read_latest(dvclive, \"train_m\") == (2, 1)\n    assert read_latest(dvclive, \"val_m\") == (2, 1)\n\n\ndef test_get_step_resume(tmp_dir):\n    dvclive = Live()\n\n    for metric in [0.9, 0.8]:\n        dvclive.log_metric(\"metric\", metric)\n        dvclive.next_step()\n\n    assert dvclive.step == 2\n\n    dvclive = Live(resume=True)\n    assert dvclive.step == 2\n\n    dvclive = Live(resume=False)\n    assert dvclive.step == 0\n\n\ndef test_get_step_custom_steps(tmp_dir):\n    dvclive = Live()\n\n    steps = [0, 62, 1000]\n    metrics = [0.9, 0.8, 0.7]\n\n    for step, metric in zip(steps, metrics):\n        dvclive.step = step\n        dvclive.log_metric(\"x\", metric)\n        assert dvclive.step == step\n\n\ndef test_get_step_control_flow(tmp_dir):\n    dvclive = Live()\n\n    while dvclive.step < 10:\n        dvclive.log_metric(\"i\", dvclive.step)\n        dvclive.next_step()\n\n    steps, values = read_history(dvclive, \"i\")\n    assert steps == list(range(10))\n    assert values == [float(x) for x in range(10)]\n\n\ndef test_set_step_only(tmp_dir):\n    dvclive = Live()\n    dvclive.step = 1\n    dvclive.end()\n\n    assert dvclive.read_latest() == {\"step\": 1}\n    assert not os.path.exists(os.path.join(tmp_dir, \"dvclive\", \"plots\"))\n\n\ndef test_step_on_end(tmp_dir):\n    dvclive = Live()\n    for metric in range(3):\n        dvclive.log_metric(\"m\", metric)\n        dvclive.next_step()\n    dvclive.end()\n    assert dvclive.step == metric\n\n    assert dvclive.read_latest() == {\"step\": metric, \"m\": metric}\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import numpy as np\nimport pandas as pd\nimport pytest\n\nfrom dvclive.error import InvalidDataTypeError\nfrom dvclive.utils import convert_datapoints_to_list_of_dicts, standardize_metric_name\n\n\n@pytest.mark.parametrize(\n    (\"framework\", \"logged\", \"standardized\"),\n    [\n        (\"dvclive.lightning\", \"epoch\", \"epoch\"),\n        (\"dvclive.lightning\", \"train_loss\", \"train/loss\"),\n        (\"dvclive.lightning\", \"train_loss_epoch\", \"train/epoch/loss\"),\n        (\"dvclive.lightning\", \"train_model_error\", \"train/model_error\"),\n        (\"dvclive.lightning\", \"grad_step\", \"grad_step\"),\n    ],\n)\ndef test_standardize_metric_name(framework, logged, standardized):\n    assert standardize_metric_name(logged, framework) == standardized\n\n\n# Tests for convert_datapoints_to_list_of_dicts()\n@pytest.mark.parametrize(\n    (\"input_data\", \"expected_output\"),\n    [\n        (\n            pd.DataFrame({\"A\": [1, 2], \"B\": [3, 4]}),\n            [{\"A\": 1, \"B\": 3}, {\"A\": 2, \"B\": 4}],\n        ),\n        (np.array([[1, 3], [2, 4]]), [{0: 1, 1: 3}, {0: 2, 1: 4}]),\n        (\n            np.array([(1, 3), (2, 4)], dtype=[(\"A\", \"i4\"), (\"B\", \"i4\")]),\n            [{\"A\": 1, \"B\": 3}, {\"A\": 2, \"B\": 4}],\n        ),\n        ([{\"A\": 1, \"B\": 3}, {\"A\": 2, \"B\": 4}], [{\"A\": 1, \"B\": 3}, {\"A\": 2, \"B\": 4}]),\n    ],\n)\ndef test_convert_datapoints_to_list_of_dicts(input_data, expected_output):\n    assert convert_datapoints_to_list_of_dicts(input_data) == expected_output\n\n\ndef test_unsupported_format():\n    with pytest.raises(InvalidDataTypeError) as exc_info:\n        convert_datapoints_to_list_of_dicts(\"unsupported data format\")\n\n    assert \"not supported type\" in str(exc_info.value)\n"
  },
  {
    "path": "tests/test_vscode.py",
    "content": "import json\nimport os\n\nimport pytest\n\nfrom dvclive import Live, env\n\n\n@pytest.mark.vscode\n@pytest.mark.parametrize(\"dvc_root\", [True, False])\ndef test_vscode_dvclive_step_completed_signal_file(\n    tmp_dir, dvc_root, mocker, monkeypatch\n):\n    signal_file = os.path.join(\n        tmp_dir, \".dvc\", \"tmp\", \"exps\", \"run\", \"DVCLIVE_STEP_COMPLETED\"\n    )\n    cwd = tmp_dir\n    test_pid = 12345\n\n    if dvc_root:\n        cwd = tmp_dir / \".dvc\" / \"tmp\" / \"exps\" / \"asdasasf\"\n        monkeypatch.setenv(env.DVC_ROOT, tmp_dir.as_posix())\n        (cwd / \".dvc\").mkdir(parents=True)\n\n    assert not os.path.exists(signal_file)\n\n    dvc_repo = mocker.MagicMock()\n    dvc_repo.index.stages = []\n    dvc_repo.config = {}\n    dvc_repo.scm.get_rev.return_value = \"current_rev\"\n    dvc_repo.scm.get_ref.return_value = None\n    dvc_repo.scm.no_commits = False\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=dvc_repo)\n    mocker.patch(\"dvclive.live.os.getpid\", return_value=test_pid)\n\n    dvclive = Live(save_dvc_exp=True)\n    assert not os.path.exists(signal_file)\n    dvclive.next_step()\n    assert dvclive.step == 1\n\n    if dvc_root:\n        assert os.path.exists(signal_file)\n        with open(signal_file, encoding=\"utf-8\") as f:\n            assert json.load(f) == {\"pid\": test_pid, \"step\": 0}\n\n    else:\n        assert not os.path.exists(signal_file)\n\n    dvclive.next_step()\n    assert dvclive.step == 2\n\n    if dvc_root:\n        with open(signal_file, encoding=\"utf-8\") as f:\n            assert json.load(f) == {\"pid\": test_pid, \"step\": 1}\n\n    dvclive.end()\n\n    assert not os.path.exists(signal_file)\n\n\n@pytest.mark.vscode\n@pytest.mark.parametrize(\"dvc_root\", [True, False])\ndef test_vscode_dvclive_only_signal_file(tmp_dir, dvc_root, mocker):\n    signal_file = os.path.join(tmp_dir, \".dvc\", \"tmp\", \"exps\", \"run\", \"DVCLIVE_ONLY\")\n    test_pid = 12345\n\n    if dvc_root:\n        (tmp_dir / \".dvc\").mkdir(parents=True)\n\n    assert not os.path.exists(signal_file)\n\n    dvc_repo = mocker.MagicMock()\n    dvc_repo.index.stages = []\n    dvc_repo.config = {}\n    dvc_repo.scm.get_rev.return_value = \"current_rev\"\n    dvc_repo.scm.get_ref.return_value = None\n    dvc_repo.scm.no_commits = False\n    mocker.patch(\"dvclive.live.get_dvc_repo\", return_value=dvc_repo)\n    mocker.patch(\"dvclive.live.os.getpid\", return_value=test_pid)\n\n    dvclive = Live(save_dvc_exp=True)\n\n    if dvc_root:\n        assert os.path.exists(signal_file)\n        with open(signal_file, encoding=\"utf-8\") as f:\n            assert json.load(f) == {\"pid\": test_pid, \"exp_name\": dvclive._exp_name}\n\n    else:\n        assert not os.path.exists(signal_file)\n\n    dvclive.end()\n\n    assert not os.path.exists(signal_file)\n"
  }
]