[
  {
    "path": ".github/issue_template.md",
    "content": "# Overview\n\nPlease replace this line with full information about your idea or problem. If it's a bug share as much as possible to reproduce it\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "- fixes #<issue-number>\n\n---\n\nPlease make sure that all the checks pass. Please add here any additional information regarding this pull request. It's highly recommended that you link this PR to an issue (please create one if it doesn't exist for this PR)\n"
  },
  {
    "path": ".github/stale.yaml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 90\n\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 30\n\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - feature\n  - enhancement\n  - bug\n\n# Label to use when marking an issue as stale\nstaleLabel: wontfix\n\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/general.yaml",
    "content": "name: general\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - v*.*.*\n  pull_request:\n    branches:\n      - main\nenv:\n  # Mandatory when using uv pip workflow.\n  UV_SYSTEM_PYTHON: 1\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Install uv\n      uses: astral-sh/setup-uv@v5\n      with:\n        version: \"0.9.9\"\n    - name: Set up Python 3.13\n      uses: actions/setup-python@v3\n      with:\n        python-version: \"3.13\"\n    - name: Check pep8\n      run: |\n          uvx ruff check\n  tests:\n    needs: lint\n    runs-on: ubuntu-latest\n    env:\n      QT_QPA_PLATFORM: 'offscreen'\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.9.9\"\n      - name: Set up Python 3.13\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.13\"\n      - name: Install QT Libs and Dependencies\n        uses: tlambert03/setup-qt-libs@v1\n      - name: Install python requirements\n        run: |\n          uv sync\n      - name: Check pep8\n        run: |\n          uvx ruff check\n      - name: Run tests\n        run: |\n          uv run pytest\n  macos-packaging:\n    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')\n    needs: tests\n    runs-on: macos-13\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.9.9\"\n      - name: Set up Python 3.13\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.13\"\n      - name: Install Dependencies\n        run: |\n          uv sync\n          brew install create-dmg\n      - name: Build and notarize the dmg file\n        env:\n          CSC_LINK: ${{ secrets.CSC_LINK }}\n          CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\n        run: |\n          chmod +x create-dmg.sh\n          ./create-dmg.sh\n      - name: Archive build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: distribution-files-macos\n          path: |\n            *.dmg\n          retention-days: 14\n  linux-packaging:\n    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')\n    needs: tests\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.9.9\"\n      - name: Set up Python 3.13\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.13\"\n      - name: Install OS Dependencies\n        run: |\n          sudo apt-get update\n          sudo gem install fpm\n          fpm --version\n      - name: Install Qt Dependencies\n        run: |\n          # https://forum.qt.io/post/769050\n          # Fix PyInstaller warnings of Qt Dependencies missing\n          sudo apt-get install synaptic\n          sudo apt-get install libxcb-icccm4 libxcb-image0-dev libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxcb-xinerama0 libxkbcommon-x11-0 libxcb-cursor0 libxcb-shape0-dev\n      - name: Install Dependencies\n        run: |\n          uv sync\n      - name: Build the deb package\n        run: |\n          chmod +x create-deb.sh\n          ./create-deb.sh\n      - name: Archive build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: distribution-files-deb\n          path: |\n            dist/*.deb\n          retention-days: 14\n  windows-packaging:\n    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')\n    needs: tests\n    runs-on: windows-2022\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.9.9\"\n      - name: Set up Python 3.13\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.13\"\n      - name: Install Dependencies\n        run: |\n          uv sync\n      - name: Install NSIS\n        run: choco install nsis\n      - name: Build with PyInstaller\n        run: uv run build.py build\n      - name: Compile installer\n        shell: bash  # Force bash shell for VERSION command\n        run: |\n          VERSION=$(uv run python -c \"from importlib.metadata import version; print(version('opendataeditor'))\")\n          makensis -DAPP_VERSION=\"$VERSION\" ./packaging/windows/installer.nsi\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: distribution-files-win\n          path: |\n            .\\packaging\\windows\\*.exe\n          retention-days: 14\n  linux-app-image:\n    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')\n    needs: tests\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install uv\n        uses: astral-sh/setup-uv@v5\n        with:\n          version: \"0.9.9\"\n      - name: Set up Python 3.13\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.13\"\n      - name: Install Qt Dependencies\n        run: |\n          # https://forum.qt.io/post/769050\n          # Fix PyInstaller warnings of Qt Dependencies missing\n          sudo apt-get update\n          sudo apt-get install synaptic\n          sudo apt-get install libxcb-icccm4 libxcb-image0-dev libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxcb-xinerama0 libxkbcommon-x11-0\n      - name: Install Dependencies\n        run: |\n          uv sync\n      - name: Build the deb package\n        run: |\n          VERSION=$(uv run python -c \"from importlib.metadata import version; print(version('opendataeditor'))\")\n          uv run build.py build --onefile --name \"opendataeditor-$VERSION.AppImage\"\n      - name: Archive build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: distribution-files-app-image\n          path: |\n            dist/*.AppImage\n          retention-days: 14\n\n"
  },
  {
    "path": ".gitignore",
    "content": "venv\ndata/\n__pycache__\nbuild/\ndist/\ntmp/\n\n# Astro \nnode_modules/\n.astro\npackage-lock.json\n/.venv\n.DS_Store\n"
  },
  {
    "path": ".python-version",
    "content": "3.13\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2022 Open Knowledge Foundation\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n[![Build](https://img.shields.io/github/actions/workflow/status/frictionlessdata/application/general.yaml?branch=main)](https://github.com/frictionlessdata/application/actions)\n[![Codebase](https://img.shields.io/badge/codebase-github-brightgreen)](https://github.com/frictionlessdata/application)\n\n![ODE-landscape-full-rgb@3x](https://github.com/okfn/opendataeditor/assets/20649846/01ae62e8-87f6-4e44-9487-790b8111e321)\n\n\n# Open Data Editor (beta)\n\n### Welcome to our Readme!\n\nThe Open Data Editor (ODE) is a **no-code application** to **explore, validate and publish data** in a simple way. Forever free and **open source project** powered by the **Frictionless Framework**.\n\n\n 📩 [Send us feedback/Report a problem (email)](mailto:info@okfn.org)\n 🪲 [Create an issue on GitHub](https://github.com/okfn/opendataeditor/issues)\n 🤔 [Suggest a new feature](https://github.com/okfn/opendataeditor/issues)\n\n\n\n# Useful links\n🔵 [Development guide](https://opendataeditor.okfn.org/contributing/development/)\n\n🔵 [Open Data Editor User Guide and Project Documentation](https://opendataeditor.okfn.org/)\n\n🔵 [Frictionless Framework](https://framework.frictionlessdata.io/)\n\n🔵 [Frictionless Data](https://frictionlessdata.io/)\n\n🔵 [Contributing Guidelines](https://opendataeditor.okfn.org/contributing/contribution-guidelines)\n\n🔵 [Open Data Editor Concept Note](https://opendataeditor.okfn.org/ode-concept-note.pdf)\n\n🔵 For all contributions: [Code of conduct](https://frictionlessdata.io/code-of-conduct/)\n\n# How to download the ODE\n\nYou can download the latest version from the [ODE website](https://okfn.org/en/projects/open-data-editor/)\n\nFor previous releases, you can download them from Github [RELEASE](https://github.com/okfn/opendataeditor/releases)\n* For **Windows**:Download the most recent **EXE** file.\n* For **MacOS**:Download the most recent **DMG** file.\n* For **Linux**:Download the most recent **AppImage** or **DEB** file.\n\n"
  },
  {
    "path": "build.py",
    "content": "import os\nimport platform\nimport PyInstaller.__main__\nimport subprocess\nimport sys\n\n\ndef run(cmd: list[str], cwd: str = \".\"):\n    \"\"\"Run a subprocess command.\"\"\"\n    subprocess.run(cmd, check=True, cwd=cwd)\n\n\ndef docs():\n    \"\"\"Build the documentation and run a local server.\"\"\"\n    run([\"make\", \"html\"], cwd=\"docs\")\n    run([\"python\", \"-m\", \"http.server\", \"-d\", \"build/html\"], cwd=\"docs\")\n\n\ndef update_translations():\n    \"\"\"Generate/update .ts files from the source code.\"\"\"\n    run([\"pyside6-lupdate\", \"-extensions\", \"py\", \"-recursive\", \"ode\", \"-ts\", \"ode/assets/translations/de.ts\", \"-target-language\", \"de_DE\"])\n    run([\"pyside6-lupdate\", \"-extensions\", \"py\", \"-recursive\", \"ode\", \"-ts\", \"ode/assets/translations/es.ts\", \"-target-language\", \"es_ES\"])\n    run([\"pyside6-lupdate\", \"-extensions\", \"py\", \"-recursive\", \"ode\", \"-ts\", \"ode/assets/translations/fr.ts\", \"-target-language\", \"fr_FR\"])\n    run([\"pyside6-lupdate\", \"-extensions\", \"py\", \"-recursive\", \"ode\", \"-ts\", \"ode/assets/translations/pt.ts\", \"-target-language\", \"pt_PT\"])\n    run([\"pyside6-lupdate\", \"-extensions\", \"py\", \"-recursive\", \"ode\", \"-ts\", \"ode/assets/translations/it.ts\", \"-target-language\", \"it_IT\"])\n\n\ndef compile_translations():\n    \"\"\"Compile .ts to .qm files.\"\"\"\n    run([\"pyside6-lrelease\", \"ode/assets/translations/de.ts\", \"-qm\", \"ode/assets/translations/de.qm\"])\n    run([\"pyside6-lrelease\", \"ode/assets/translations/es.ts\", \"-qm\", \"ode/assets/translations/es.qm\"])\n    run([\"pyside6-lrelease\", \"ode/assets/translations/fr.ts\", \"-qm\", \"ode/assets/translations/fr.qm\"])\n    run([\"pyside6-lrelease\", \"ode/assets/translations/pt.ts\", \"-qm\", \"ode/assets/translations/pt.qm\"])\n    run([\"pyside6-lrelease\", \"ode/assets/translations/it.ts\", \"-qm\", \"ode/assets/translations/it.qm\"])\n\n\ndef build_application():\n    \"\"\"Build an executable file for the Application.\"\"\"\n    system = platform.system()\n\n    # Linux Defaults\n    icon_path = \"packaging/linux/icon.svg\"\n    app_name = \"opendataeditor\"\n\n    if system == \"Darwin\":  # macOS\n        icon_path = \"packaging/macos/icon.icns\"\n        app_name = \"OpenDataEditor\"\n    elif system == \"Windows\":\n        icon_path = \"packaging/windows/icon.ico\"\n        app_name = \"opendataeditor\"\n\n    print(\"Creating executable file for Open Data Editor\")\n\n    params = [\n        \"src/ode/main.py\",\n        \"--windowed\",  # Required for Windows install to not open a console.\n        \"--collect-all\",\n        \"frictionless\",  # Frictionless depends on data files\n        \"--collect-all\",\n        \"ode\",  # Collect all assets from Open Data Editor\n        \"--collect-all\",\n        \"llama_cpp\",  # Collect all assets from llama_cpp\n        \"--collect-all\",\n        \"numpy\",  # Collect all assets from numpy (llama_cpp dependency)\n        \"--log-level\",\n        \"WARN\",\n        \"--name\",\n        app_name,\n        \"--noconfirm\",\n        \"--icon\",\n        icon_path,\n    ]\n\n    if system == \"Darwin\":\n        params.extend([\"--osx-bundle-identifier\", \"org.okfn.opendataeditor\"])\n\n    if system == \"Windows\":\n        # llama_cpp depends on vcomp140.dll and it is not properly collected by PyInstaller as it\n        # is a dependency of shiboken6 as well. This library is only present in Windows if C++ Redistributable\n        # is installed which might not be the case for all of our users.\n        params.extend([\"--add-binary\", \"C:\\\\Windows\\\\system32\\\\vcomp140.dll:.\"])\n\n    # Allow calling `python build.py build` with extra arguments\n    # e.g. when building AppImage `python build.py build --onefile --name \"opendataeditor-X.Y.Z.AppImage\"`\n    cli_args =  sys.argv[2:]\n    if cli_args:\n        params.extend(cli_args)\n\n    PyInstaller.__main__.run(params)\n\n    # Clean the spec file generated by PyInstaller\n    if os.path.exists(f\"{app_name}.spec\"):\n        os.remove(f\"{app_name}.spec\")\n\n\ndef main():\n    if len(sys.argv) < 2:\n        print(\"Usage:\")\n        print(\"  python build.py update-translations\")\n        print(\"  python build.py compile-translations\")\n        print(\"  python build.py build\")\n        print(\"  python build.py docs\")\n        sys.exit(1)\n\n    command = sys.argv[1].lower()\n\n    if command == \"update-translations\":\n        update_translations()\n    elif command == \"compile-translations\":\n        compile_translations()\n    elif command == \"build\":\n        build_application()\n    elif command == \"docs\":\n        docs()\n    else:\n        print(f\"Unknown command: {command}\")\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "create-deb.sh",
    "content": "#!/bin/sh\n# Script to create a PyQT deb package using fpm\n#\n# This script will create a folder structure that will be used as input\n# for the fpm command and copy into it all the distributable files generated by\n# pyinstaller.\n# We will create a folder with the same structure Linux systems expects:\n#  - /opt for our executable and associated files (a.k.a our dist/ folder)\n#  - /usr/share/applications (for the .desktop file)\n#  - /usr/share/icons/hicolor/scalable/apps for our svg icons (only svg in scalable folder)\n#\n# More info:\n#  - https://www.pythonguis.com/tutorials/packaging-pyqt5-applications-linux-pyinstaller/\n#  - https://fpm.readthedocs.io/en/latest/packages/dir.html#dir-local-files\n#\n# Create folders\n[ -e tmp ] && rm -r tmp\nmkdir -p tmp/opt\nmkdir -p tmp/usr/share/applications\nmkdir -p tmp/usr/share/icons/hicolor/scalable/apps\n\n# Build the project\n[ -e build ] && rm -r build\n[ -e dist ] && rm -r dist\nuv run build.py build\n\n# Copy files\ncp -r dist/opendataeditor tmp/opt/opendataeditor\ncp ./packaging/linux/icon.svg tmp/usr/share/icons/hicolor/scalable/apps/org.okfn.opendataeditor.svg\ncp ./packaging/linux/opendataeditor.desktop tmp/usr/share/applications\n\n# Change permissions\n# Packages retain the permissions of installed files from when they were packaged,\n# but will be installed by root. In order for ordinary users to be able to run the\n# application, we need to change the permissions.\nfind tmp/opt/opendataeditor -type f -exec chmod 644 -- {} +\nfind tmp/opt/opendataeditor -type d -exec chmod 755 -- {} +\nfind tmp/usr/share -type f -exec chmod 644 -- {} +\nchmod +x tmp/opt/opendataeditor/opendataeditor\n\n# Create the deb package\nVERSION=$(uv run python -c \"from importlib.metadata import version; print(version('opendataeditor'))\")\nFILENAME=opendataeditor-linux-$VERSION.deb\n[ -e dist/$FILENAME ] && rm dist/$FILENAME\nfpm -C tmp -s dir -t deb -n \"opendataeditor\" -v $VERSION  -p dist/$FILENAME\n"
  },
  {
    "path": "create-dmg.sh",
    "content": "#!/bin/sh\n# File to create the DMG file using create-dmg tool and notarize it.\n#\n# It is intended to work on Github Actions and so it might not be the most optimized\n# workflow for executing it locally (because of the secrets required for code sign\n# and notarizing)\n#\n# This script expects 6 secrets:\n#  - CSC_LINK: A base64 encoded p12 certificate.\n#  - CSC_KEY_PASSWORD: The password used to encrypt the p12 certificate\n#  - APPLE_TEAM_ID: This is the ID of the team of your Apple Developer Account (Something like S1235Q75WSA)\n#  - APPLE_APPLE_ID: This is the ID of your Apple Developer Account (usually your email)\n#  - APPLE_APP_SPECIFIC_PASSWORD: The Application Specific Password created in your Developer Account.\n#\n# How to generate an APPLE_APP_SPECIFIC_PASSWORD\n# 1) Visit the Apple ID website: https://appleid.apple.com/\n# 2) Sign in with your Apple ID credentials\n# 3) Navigate to the \"Security\" section\n# 4) Look for \"App-Specific Passwords\"\n# 5) Click \"Generate Password...\"\n# 6) Enter a descriptive label (e.g., \"macOS App Notarization\")\n# 7) Apple will generate a 16-character app-specific password\n# 8) Copy this password and use it as the value for the $APPLE_APP_SPECIFIC_PASSWORD environment variable in your notarization workflow\n#\n# Context and materials that inspired this script:\n#  - https://www.pythonguis.com/tutorials/packaging-pyqt6-applications-pyinstaller-macos-dmg/\n#  - https://medium.com/flutter-community/build-sign-and-deliver-flutter-macos-desktop-applications-on-github-actions-5d9b69b0469c\n#  - https://defn.io/2023/09/22/distributing-mac-apps-with-github-actions/\n#  - https://gist.github.com/txoof/0636835d3cc65245c6288b2374799c43\n# \n# Issues we had with the notarization process:\n# - https://github.com/pyinstaller/pyinstaller/issues/8927\n\n# Build the project\n[ -e build ] && rm -r build\n[ -e dist ] && rm -r dist\nuv run build.py build\n\nmv \"dist/OpenDataEditor.app\" \"dist/Open Data Editor.app\"\n\n# # Codesign the executable created by pyinstaller\necho \"Codesigning the executable created by PyInstaller\"\necho $CSC_LINK | base64 --decode > certificate.p12\nsecurity create-keychain -p thisisatemporarypass build.keychain\nsecurity default-keychain -s build.keychain\nsecurity unlock-keychain -p thisisatemporarypass build.keychain\nsecurity import certificate.p12 -k build.keychain -P $CSC_KEY_PASSWORD -T /usr/bin/codesign\nsecurity set-key-partition-list -S apple-tool:,apple:,codedign: -s -k thisisatemporarypass build.keychain\n\necho \"Signing complete application bundle...\"\n/usr/bin/codesign --force --deep --options=runtime --entitlements ./packaging/macos/entitlements.mac.plist -s $APPLE_TEAM_ID --timestamp \"dist/Open Data Editor.app\"\n\n# Verify signature\necho \"Verifying signature...\"\ncodesign -vvv --deep --strict \"dist/Open Data Editor.app\"\n\necho \"Signing process completed.\"\n\n# Create dmg folder and copy our signed executable\nmkdir -p dist/dmg\n\n# We need to use -R to copy the app bundle recursively instead of -r because it doesn't preserver the symlinks otherwise\n# https://github.com/pyinstaller/pyinstaller/issues/8927\n# https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html#requirements-imposed-by-symbolic-links-in-frozen-application\ncp -R \"dist/Open Data Editor.app\" \"dist/dmg\" \n\n# We need to detach the volume if it is already mounted\n# and remove the dmg file if it exists\necho \"Unmounting any existing volume...\"\nhdiutil detach /Volumes/\"Open Data Editor\" &>/dev/null || true\nsleep 5\nrm -f *.dmg\n\n# Create the dmg file\nVERSION=$(uv run python -c \"from importlib.metadata import version; print(version('opendataeditor'))\")\nFILENAME=opendataeditor-macos-$VERSION.dmg\n[ -e $FILENAME ] && rm $FILENAME\n\nMAX_RETRIES=3\nRETRY_COUNT=0\n\nwhile [ $RETRY_COUNT -lt $MAX_RETRIES ]; do\n  echo \"Creating the DMG file\"\n  if create-dmg \\\n    --volname \"Open Data Editor\" \\\n    --volicon \"./packaging/macos/icon.icns\" \\\n    --window-pos 200 120 \\\n    --window-size 800 400 \\\n    --icon-size 100 \\\n    --icon \"Open Data Editor.app\" 200 190 \\\n    --hide-extension \"Open Data Editor.app\" \\\n    --app-drop-link 600 185 \\\n    $FILENAME \\\n    \"dist/dmg/\";then\n\n    echo \"DMG created: $FILENAME\"\n    break\n  else\n      RETRY_COUNT=$((RETRY_COUNT + 1))\n      echo \"Failed to create DMG. Retrying... ($RETRY_COUNT/$MAX_RETRIES)\"\n      hdiutil detach \"/Volumes/Open Data Editor\" -force &>/dev/null || true\n      killall Finder &>/dev/null || true\n      sleep 20\n  fi\ndone\n\nif [ $RETRY_COUNT -eq $MAX_RETRIES ]; then\n    echo \"Failed to create DMG after $MAX_RETRIES attempts. Exiting.\"\n    exit 1\nfi\n\nif [ ! -f \"$FILENAME\" ]; then\n    echo \"DMG file not found. Exiting.\"\n    exit 1\nfi\n\n# Notarize the DMG File\n# If an error occurs, we can check the logs using\n# xcrun notarytool log $REPLACE-WITH-RUNNING-HASH --team-id $APPLE_TEAM_ID --apple-id $APPLE_ID --password $APPLE_APP_SPECIFIC_PASSWORD notarization_log.json\necho \"Notarizing the DMG file\"\nxcrun notarytool submit --verbose --team-id $APPLE_TEAM_ID --apple-id $APPLE_ID --password $APPLE_APP_SPECIFIC_PASSWORD --wait $FILENAME > notarization_output.txt\n\n# Staple the file\n# We check if the notarization was successful\nif grep -q \"status: Accepted\" notarization_output.txt; then\n  echo \"Notarization successful!\"\n  \n  # We wait for 30 seconds to make sure the notarization ticket is available\n  echo \"Waiting 30 seconds for notarization ticket to be available...\"\n  sleep 30\n  \n  echo \"Stapling the file\"\n  xcrun stapler staple $FILENAME\nelse\n  echo \"Notarization failed. Check notarization_output.txt for details.\"\n  cat notarization_output.txt\n  exit 1\nfi\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=source\r\nset BUILDDIR=build\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/public/.gitkeep",
    "content": ""
  },
  {
    "path": "docs/requirements.txt",
    "content": "sphinx\nmyst-parser\nsphinx_rtd_theme\n"
  },
  {
    "path": "docs/source/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nproject = 'Open Data Editor'\ncopyright = '2025, Open Knowledge Foundation'\nauthor = 'Open Knowledge Foundation'\nrelease = '1.5.1'\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n    'myst_parser',\n    'sphinx_rtd_theme',\n]\n\nmyst_enable_extensions = [\n    \"colon_fence\", # Admonitions\n]\n\ntemplates_path = ['_templates']\nexclude_patterns = []\n\n\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\nhtml_theme = 'sphinx_rtd_theme'\nhtml_static_path = ['_static']\n"
  },
  {
    "path": "docs/source/contributing/contribution-guidelines.md",
    "content": "# Contributing\n\n## Contribution Guidelines\n\nYou don’t need to know how to code to contribute to Open Data Editor, you just need time and attitude. There are many possible ways to contribute to the project:\n\n* Use the Open Data Editor and give us feedback  \n* Spread the word about it\\!  \n* Improve the documentation  \n* Add a translation  \n* Report issues  \n* Contribute to the code\n\nPlease read this guide for more details on the contribution process.\n\n### How can you help?\n\n#### Giving us feedback\n\nUse the Open Data Editor and give us feedback. You can try out all the features and report any issues you might encounter. You can also suggest improvements to the Open Data Editor’s look and layout. For example, *if something feels confusing or hard to find in the tool’s layout, you can suggest changes to make it easier to use.*\n\nFeedback from a beginner’s perspective is particularly welcome as it helps us improve usability.\n\nTo give us feedback, you can either [open an issue in the GitHub repository](https://github.com/okfn/opendataeditor) or reach out by email at info@okfn.org.\n\n#### Spread the word\n\nIf you like the application, tell others about it\\! Spreading the word about the Open Data Editor is also a valuable contribution to the project. Whether you share it on social media, write a blog post, create a tutorial video, or simply tell a friend, you’re helping more people discover and use the tool. Every bit of awareness and engagement makes a difference\\!\n\n#### Documentation\n\n##### Improving documentation\n\nDocumentation is always the best place to start contributing. If you spot a typo or you’ve ever thought, “I wish the documentation explained this more clearly,” you can help make it easier to understand. You can either flag that, or propose a modification. You can also add missing steps or explanations.\n\n##### Adding examples \n\nYou can also contribute by adding examples\\! If you notice something is not covered in the documentation, try creating a helpful example and sharing it. You can also provide real-world use cases to show how Open Data Editor can be used. Every improvement makes Open Data Editor easier for everyone to use\\!\n\nAll documentation files are contained [in this folder](https://github.com/okfn/opendataeditor/tree/main/portal/content/docs/documentation). You can just propose a change or add examples by opening a PR. Please **always include a short description of what you changed and why**, so it is easier to review. If you don’t feel comfortable using GitHub, you can also send us an email with the proposed changes at info\\[at\\]okfn.org.\n\n##### Use cases\n\nIf you have started using Open Data Editor in your daily work and would like to share with the world how the tool is helping you, you can always write a blog about it. You can find a few examples [here](https://blog.okfn.org/tag/ode-use-cases/).\n\nIf possible, try to include photos and screenshots. If you need guidance, you can always reach out to us at info@okfn.org. You can send us your blog at that same email address.\n\n#### Reporting a bug\n\nWe use GitHub as a code and issues hosting platform. To report a bug or propose a new feature, please open an issue [in the repository](https://github.com/okfn/opendataeditor/issues). Please provide a detailed description of the bug and include screenshots. There are a few predefined issue templates to help you get started.\n\nIf you don’t feel comfortable using GitHub, you can also send us an email at info@okfn.org.\n\n#### Code contributions\n\n##### Pull requests \n\nFirst, please check the [issue tracker](https://github.com/okfn/opendataeditor/issues). Look for issues with “help wanted” or “good first issue.” **If you want to submit a PR, there needs to be a corresponding issue in the issue tracker.** Please link the issue in your PR, and provide a brief explanation of the changes you are pushing. \n\n### What is the review process for your contribution?\n\nYour contribution will be **reviewed by the Open Data Editor core team at Open Knowledge Foundation**.\n\nIn case of documentation and code contributions, a member of the core team will check if it is relevant, helpful, and easy to understand. If small changes are needed, they suggest edits. Once everything looks good, the contribution is approved and added to the project, and the contributor is acknowledged (because every contribution matters\\!).\n\nIn case of bugs and feedback contributions, the core team will discuss if and how they can be implemented. We can include you in the discussion if you wish. Once we have a clear pathway, we will notify you.\n\n### Do you need help?\n\nIf at any point you need help, feel free to contact us via email at [info@okfn.org](mailto:info@okfn.org)."
  },
  {
    "path": "docs/source/contributing/translations.md",
    "content": "# Translations\n\nODE supports several languages following the [Qt Framework practices for](https://doc.qt.io/qt-6/internationalization.html) internationalisation.\n\n## Internationalisation workflow \n\n:::{note}\nODE provides a `python build.py update-translations` and `python build.py compile-translations` command for all the supported languages. Check the project’s `build.py` file for reference. \n:::\n\n1. Create or update translation files by running `python build.py update-translations` (or the `pyside6-lupdate` command directly if you are on macOS)  \n2. Update the translation files:  \n   1. Complete `unfinished` translations (this is the actual addition of translated text).  \n   2. Clean `vanished` translations.  \n3. Compile the translation files by running `python build.py compile-translations` (or the `pyside6-lrelease` command directly if you are on macOS)  \n4. Commit both the `.ts` and `.qm` files.  \n5. Create a PR with the changes.\n\nAll translation files are located in the `ode/assets/translations/` folder.\n\n## Translation Tools \n\nFor updating translations, you can use either:\n\n1. A text editor to directly update the translation files (\\*.ts)  \n2. Install Qt and use [Qt Linguist](https://doc.qt.io/qt-6/qtlinguist-index.html) application to do the translation using a UI.\n\n## Adding new languages\n\nWhen adding a new language, two extra changes are required:\n\n1. Add the new language to both `python build.py update-translations` and `python build.py compile-translations` commands of the `build.py` file. (after doing this, you can run the `python build.py update-translations` and it will create the `.ts` file for you.)  \n2. Update the `language` QComboBox of the `main.py` file so the new language appears as an option to the user.\n\nHere is a reference Pull Request of what is expected when adding a new language: [https://github.com/okfn/opendataeditor/pull/750](https://github.com/okfn/opendataeditor/pull/750)"
  },
  {
    "path": "docs/source/index.rst",
    "content": ".. Open Data Editor documentation master file, created by\n   sphinx-quickstart on Fri Jul  4 13:22:06 2025.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nOpen Data Editor Docs\n========================================\n\n.. image:: /assets/ODE-logo.png\n   :alt: Open Data Editor logo\n   :width: 200px\n\n**Your no-code app for error-free spreadsheets, and guaranteed privacy and FAIR data**\n\nThis website contains user guides and technical documentation for the Open Data Editor (ODE), as well as information on contributing code and use cases. \n\nFor more information, please visit the official project page at the Open Knowledge Foundation (OKFN) website: `https://okfn.org/en/projects/open-data-editor/ <https://okfn.org/en/projects/open-data-editor/>`_\n\n.. toctree::\n   :maxdepth: 3\n   :caption: Introduction\n\n   introduction/what-is-open-data-editor.md\n   introduction/fair-data.md\n   introduction/responsible-ai-integration.md\n   introduction/free-data-literacy-course.md\n   introduction/similar-tools-and-differentiators.md\n   introduction/acknowledgements.md\n   introduction/latest-updates.md\n\n.. toctree::\n   :maxdepth: 3\n   :caption: Use Cases\n\n   use-cases/context.md\n   use-cases/agricultural-data-ghana.md\n   use-cases/climate-data-kenya.md\n   use-cases/data-journalism-mexico.md\n   use-cases/defence-data-france.md\n   use-cases/financial-data-south-africa.md\n   use-cases/government-data-croatia.md\n   use-cases/heritage-data-cambodia.md\n   use-cases/library-data-india.md\n\n.. toctree::\n   :maxdepth: 3\n   :caption: User Guide\n\n   user-guide/downloading-ode.md\n   user-guide/installing-ode.md\n   user-guide/uploading-data.md\n   user-guide/how-to-explore-table-errors.md\n   user-guide/editing-errors-in-tables.md\n   user-guide/deleting-files-or-folders.md\n   user-guide/full-list-of-table-errors-detected.md\n   user-guide/how-to-use-the-ai-component.md\n   user-guide/how-to-explore-and-edit-metadata.md\n   user-guide/exporting-your-data.md\n\n.. toctree::\n   :maxdepth: 3\n   :caption: Technical Documentation\n\n   technical-documentation/prerequisites.md\n   technical-documentation/environment.md\n   technical-documentation/start-the-application.md\n   technical-documentation/running-tests.md\n   technical-documentation/building-the-application.md\n   technical-documentation/documentation.md\n   technical-documentation/making-a-release.md\n\n.. toctree::\n   :maxdepth: 3\n   :caption: Contributing\n\n   contributing/contribution-guidelines.md\n   contributing/translations.md\n"
  },
  {
    "path": "docs/source/introduction/acknowledgements.md",
    "content": "## Acknowledgements\n\nWe are grateful for the support and partnership of the [Patrick J. McGovern Foundation (PJMF)](https://www.mcgovern.org/), without which the development of the Open Data Editor would not have been possible. Learn more about its funding programmes [here](https://www.mcgovern.org/grants/)."
  },
  {
    "path": "docs/source/introduction/fair-data.md",
    "content": "## FAIR data \n\nThe Open Data Editor (ODE) improves data quality based on the [FAIR principles](https://www.go-fair.org/fair-principles/).\n\nAs described by the [GO FAIR initiative](https://www.go-fair.org/), FAIR data refers to data that adheres to the **Findable**, **Accessible**, **Interoperable**, and **Reusable** principles, designed to make research data more discoverable and usable by people and machines, enhancing its value and reuse.\n\n**Findable**  \nThe first step in (re)using data is to find it. Metadata and data should be easy to find for both humans and computers.\n\n**Accessible**  \nOnce the user finds the required data, they need to know how it can be accessed, possibly including authentication and authorisation.\n\n**Interoperable**  \nThe data usually need to be integrated with other data. In addition, the data needs to interoperate with applications or workflows for analysis, storage, and processing.\n\n**Reusable**  \nThe ultimate goal of FAIR is to optimise the reuse of data. To achieve this, metadata and data should be well-described so that they can be replicated and/or combined in different settings.\n\nLearn all about the FAIR principles in [Module 4](https://schoolofdata.org/courses/quality-and-consistent-data-with-open-data-editor/lessons/introduction-6/) of the ‘Quality and Consistent Data with ODE’ course, available on [School of Data](https://schoolofdata.org/)."
  },
  {
    "path": "docs/source/introduction/free-data-literacy-course.md",
    "content": "## Free data literacy course \n\nOpen Data Editor's focus on improving digital literacy and preparing non-technical users to work with data, led us to develop a free course available on School of Data. [‘Quality and Consistent Data with the Open Data Editor’](https://schoolofdata.org/courses/quality-and-consistent-data-with-open-data-editor/) is an essential open educational resource for anyone who wants to generate knowledge from data. The course is now available in English and Portuguese, and will soon be translated into Spanish and French.\n\nIt is specially designed for non-technical users working with tabular data (Excel, Google Sheets, CSV) but without advanced technical knowledge. It's 100% online, free, and gamified.\n\nYou can check the contents and enrol here: [https://schoolofdata.org/courses/quality-and-consistent-data-with-open-data-editor/](https://schoolofdata.org/courses/quality-and-consistent-data-with-open-data-editor/) "
  },
  {
    "path": "docs/source/introduction/latest-updates.md",
    "content": "## Latest updates\n\nOpen Data Editor is being built in the open. Follow the progress, from feature updates to community stories on the Open Knowledge Blog: [https://blog.okfn.org/category/open-data-editor/](https://blog.okfn.org/category/open-data-editor/) "
  },
  {
    "path": "docs/source/introduction/responsible-ai-integration.md",
    "content": "## Responsible AI integration\n\nThe Open Data Editor (ODE) has an AI component to help users better understand their data. The AI component is powered by local models. **No data is sent to the cloud, and all operations take place on the user’s computer.**\n\nThe introduction of these AI-assisted data quality features significantly expands the tool’s capabilities, enabling faster identification of anomalies, smarter suggestions for corrections, and improved handling of large, dense datasets while retaining a privacy-preserving, local-first design.\n\nThis deepens Open Knowledge Foundation's commitment to developing simple, sustainable and long-lasting technologies that solve people’s real problems, in line with [The Tech We Want](https://okfn.org/en/projects/the-tech-we-want/) initiative.\n\nIn order to use the AI feature, ODE will guide users to download the model onto their machine first.\n\n:::{note} \nAs the model is local and all computing will be happening in the user’s computer, performance will be affected by the machine’s hardware, and the response quality sometimes will be minor than typical cloud LLMs like ChatGPT, Deepseek or Claude. \n:::\n\nODE is trying to balance user experience for non-technical audiences and performance and privacy."
  },
  {
    "path": "docs/source/introduction/similar-tools-and-differentiators.md",
    "content": "## Similar tools and differentiators\n\nThe tools currently available with functions similar to those of the Open Data Editor were created for a specific purpose and have a more technical profile. This makes them difficult for people unfamiliar with code, standards or programming languages to access.\n\nThe main differences in relation to ODE are listed in each subsection below:\n\n### Data Check\n\nAvailable at: [https://data.humdata.org/tools/datacheck/import](https://data.humdata.org/tools/datacheck/import)\n\nMain differences:\n\n* Maximum file size: 20 MB.  \n* Works only with the HXL standard.  \n* The table view after the error check is limited, and the user needs to navigate through several tabs if the file has many lines.  \n* Does not include a publication feature.\n\n### IATI Validator\n\nAvailable at: [https://validator.iatistandard.org/](https://validator.iatistandard.org/)\n\nMain differences:\n\n* Works only with the IATI standard.  \n* Targets a specific sector, the international aid community.  \n* The tool offers five levels of qualification regarding data quality (Success, Success with Advisories, Warning, Error, and Critical), rather than a list of all errors and how to correct them.\n\n### CSV Lint.io\n\nAvailable at: [https://csvlint.io/](https://csvlint.io/)\n\nMain differences:\n\n* Works only with CSV files, informing the user if the file “is readable” or not.  \n* Agnostic tool; the schema can also be ingested.\n\n### 360Giving Data Quality Checker\n\nAvailable at: [https://dataquality.threesixtygiving.org/](https://dataquality.threesixtygiving.org/) \n\nMain differences:\n\n* Works only with the 360Giving standard."
  },
  {
    "path": "docs/source/introduction/what-is-open-data-editor.md",
    "content": "## What is Open Data Editor\n\n[Open Data Editor (ODE)](https://okfn.org/opendataeditor), developed by the [Open Knowledge Foundation (OKFN)](https://okfn.org/en/), is a free, open-source tool designed to help nonprofits, data journalists, activists, and public servants detect errors in their datasets. It is designed for people working with tabular data (Excel, Google Sheets, CSV) who don't know how to code or don't have the programming skills to automatise the data exploration process.\n\nThe technical mission of ODE is to provide a free, open-source, no-code, cross-platform desktop application that empowers non-technical users working with tabular data to quickly detect and correct errors, enforce data-validation and metadata standards (notably the [FAIR principles](https://www.go-fair.org/fair-principles/)), and output clean, interoperable datasets ready for publication – while preserving privacy (local-first architecture), remaining lightweight (suitable for low-resource or offline contexts), and employing open standards (e.g., the [Frictionless framework](https://framework.frictionlessdata.io/)) for maximum reuse and integration.\n\nSince 2025, the Digital Public Goods Alliance (DPGA) has recognised Open Data Editor as a [digital public good](https://blog.okfn.org/2025/10/22/open-data-editor-recognised-as-a-digital-public-good/) (DPG), which means it meets high standards of openness and supports sustainable development globally."
  },
  {
    "path": "docs/source/technical-documentation/building-the-application.md",
    "content": "## Building the application\n\n```bash\nuv run build.py build\n```\n\nor\n\n```bash\n# With the virtual environment activated\npython build.py build\n```\n\nThis will create a distributable file for the application in the ‘dist/’ folder.\n"
  },
  {
    "path": "docs/source/technical-documentation/documentation.md",
    "content": "## Documentation\n\nDocumentation is written with [Sphinx](https://www.sphinx-doc.org/en/master/) (in the `docs` directory). The source files are in the `docs/source/` directory. To locally build the documentation, you can execute:\n\n```bash\nuv run build.py docs\n```\n\nor\n\n```bash\n# With the virtual environment activated\npython build.py docs\n```\n\nIt will be automatically published on CloudFlare when merged to the `main`branch, with previews available for pull requests.\n"
  },
  {
    "path": "docs/source/technical-documentation/environment.md",
    "content": "## Environment\n\nUse `uv` to create a virtualenv and activate it:\n\n```bash\nuv sync\nsource venv/bin/activate\n```\n"
  },
  {
    "path": "docs/source/technical-documentation/making-a-release.md",
    "content": "## Making a release\n\nTo make a release, follow the following checklist:\n\n* Check with the Product Owner that the `main` branch is code complete.  \n* Check that the distributables built on `main` are working by installing them on your machine.  \n* Sometimes PyInstaller cannot compile new dependencies, and the application will fail at runtime.  \n* Create a new PR bumping the version of the application in the `pyproject.toml` file and merge it to main.  \n* Create a New Github Release with a new tag matching the new version number of the application.  \n* Fill in the Release notes.  \n* Create the Release.  \n* Wait until the GitHub Action for the new tag finishes, and then upload the distributable files to the new Release.  \n* Notify the Communications Team to make the announcement and changes to the [OKFN’s Website](https://okfn.org/opendataeditor/)."
  },
  {
    "path": "docs/source/technical-documentation/prerequisites.md",
    "content": "## Prerequisites\n\nWe are using 3.13. To start working on the project, you need the following dependencies on your machine:\n\n* Python 3.13  \n* python3.13-dev (For PyInstaller)\n\nWe are using [uv](https://docs.astral.sh/uv/) as a package manager, so make sure you have it installed."
  },
  {
    "path": "docs/source/technical-documentation/running-tests.md",
    "content": "## Running tests\n\n```bash\nuv run pytest tests/\n```\n\nor\n\n```bash\n# With the virtual environment activated\npytest tests/\n```\n"
  },
  {
    "path": "docs/source/technical-documentation/start-the-application.md",
    "content": "## Start the application\n\n```bash\nuv run ode\n```\n\nor\n\n```bash\n# With the virtual environment activated\npython src/ode/main.py\n```\n"
  },
  {
    "path": "docs/source/use-cases/agricultural-data-ghana.md",
    "content": "## Agricultural data (Ghana)\n\nOpen Science Community Ghana (OSCG) used ODE to streamline the work with manually-collected data and reduce the time to identify and correct errors. \n\nThe ODE interface enabled the team to easily define and enforce a standard data structure, ensuring that every dataset adheres to a consistent format. This directly solved their core problem of harmonising the data that they receive from different sources.\n\n![Ghana](./assets/use-cases/ghana.png)\n\nAn example of errors detected by ODE in a research dataset: blank cells and wrong formats.\n\nLearn more: [https://blog.okfn.org/2025/11/10/open-data-editor-in-action-bridging-the-gap-between-field-data-and-research-insights-in-ghana/](https://blog.okfn.org/2025/11/10/open-data-editor-in-action-bridging-the-gap-between-field-data-and-research-insights-in-ghana/) "
  },
  {
    "path": "docs/source/use-cases/climate-data-kenya.md",
    "content": "## Climate data (Kenya)\n\nThe Demography Project used ODE to check and correct errors in a giant spreadsheet of air quality data, enabling accurate analysis.\n\nThese tables, which compile data from different sources (including two portable air quality monitors, GPS devices, smartphones), had many inconsistencies that were detected in seconds. Once the errors have been identified, they could then correct the missing or incorrect data, increasing the dataset’s quality.\n\n![Kenya](./assets/use-cases/kenya.png)\n\nIn this spreadsheet, ODE identified 29 inconsistencies and errors in the dataset.\n\nLearn more: [https://blog.okfn.org/2025/04/28/open-data-editor-use-case-a-giant-spreadsheet-of-environmental-data-now-accurate-for-analysis/](https://blog.okfn.org/2025/04/28/open-data-editor-use-case-a-giant-spreadsheet-of-environmental-data-now-accurate-for-analysis/)"
  },
  {
    "path": "docs/source/use-cases/context.md",
    "content": "# Context\n\nThe Open Data Editor was developed in collaboration with user communities worldwide. Between 2024 and 2025, three formal testing and feedback phases were conducted with civil society organisations working in different fields of knowledge. This work has given rise to several use cases, all of which are compiled here: [https://blog.okfn.org/tag/ode-use-cases/](https://blog.okfn.org/tag/ode-use-cases/) \n\nBelow, we highlight some of them to give you an idea of the types of tasks that ODE helps to perform to improve data workflow and data quality.\n\nYou can also contribute by documenting a use case. Learn how at **Documentation \\> Use cases** in this guide."
  },
  {
    "path": "docs/source/use-cases/data-journalism-mexico.md",
    "content": "## Data journalism (Mexico)\n\nData Crítica used ODE to identify reliable variables for journalistic investigations and ensure stories are built on a solid foundation.\n\nThey used it as a central part of its “data interrogation” methodology in workshops and its own research. ODE instantly flagged missing values, providing a clear, visual overview of a dataset’s completeness by highlighting empty cells.\n\n![Mexico](./assets/use-cases/mexico.png)\n\nIn the same dataset as the picture above, ODE now identifies errors in columns with the same name and unexpected data types\n\nLearn more: [https://blog.okfn.org/2025/11/28/open-data-editor-in-action-interrogating-data-for-investigative-journalism-in-mexico/](https://blog.okfn.org/2025/11/28/open-data-editor-in-action-interrogating-data-for-investigative-journalism-in-mexico/)"
  },
  {
    "path": "docs/source/use-cases/defence-data-france.md",
    "content": "## Defence data (France)\n\nThe Observatoire des armements used ODE to turn multiple public spending data sources (arms purchases and sales) into a single quality spreadsheet.\n\nThey reduced error resolution time from days to seconds, eliminated 95% of manual review work, and enabled the team to focus on what matters. \n\n![France](./assets/use-cases/france.jpg)\n\nIn this spreadsheet, ODE flagged 48 inconsistencies in seconds.\n\nLearn more: [https://blog.okfn.org/2025/04/14/open-data-editor-use-case-multiple-defence-spending-data-sources-in-a-single-quality-spreadsheet/](https://blog.okfn.org/2025/04/14/open-data-editor-use-case-multiple-defence-spending-data-sources-in-a-single-quality-spreadsheet/)"
  },
  {
    "path": "docs/source/use-cases/financial-data-south-africa.md",
    "content": "## Financial data (South Africa)\n\nThe Public Affairs Research Institute (PARI) used ODE to restructure messy public financial datasets into a clean, consistent format, making them ready for reliable analysis.\n\nODE automatically profiled the data, instantly highlighting empty cells, type mismatches, and structural inconsistencies that would take days to find manually. This shifted their role from manual detectives to efficient data supervisors.\n\n![South Africa](./assets/use-cases/south-africa.png)\n\nExamples of errors detected by ODE in a municipal dataset: blank cells and wrong formats.\n\nLearn more: [https://blog.okfn.org/2025/11/10/open-data-editor-in-action-enhancing-fiscal-governance-and-transparency-in-south-african-municipalities/](https://blog.okfn.org/2025/11/10/open-data-editor-in-action-enhancing-fiscal-governance-and-transparency-in-south-african-municipalities/)\n"
  },
  {
    "path": "docs/source/use-cases/government-data-croatia.md",
    "content": "## Government data (Croatia)\n\nThe City of Zagreb used ODE to comply with open data standards and foster a culture of data literacy across different city offices in Zagreb.\n\nODE flagged inconsistencies (e.g., missing values, formatting errors) in seconds, such as a public buildings dataset with 11 columns of unlabeled energy metrics. The team was able to add critical context (e.g., data owners, sourcing methods) directly in ODE’s metadata panel, aligning with FAIR principles.\n\n![Croatia](./assets/use-cases/croatia.jpg)\n\nODE’s metadata panel was central to understanding the importance of interoperability and creating a culture of data literacy in the public administration.\n\nLearn more: [https://blog.okfn.org/2025/05/20/open-data-editor-in-action-streamlining-data-governance-and-unlocking-the-potential-value-of-urban-data-in-croatia/](https://blog.okfn.org/2025/05/20/open-data-editor-in-action-streamlining-data-governance-and-unlocking-the-potential-value-of-urban-data-in-croatia/) "
  },
  {
    "path": "docs/source/use-cases/heritage-data-cambodia.md",
    "content": "## Heritage data (Cambodia)\n\nAn AI of Our Own (AAOO) used ODE to create AI models that are built on respectful and ethically sourced data from the Global South.\n\nODE allowed the team to identify and rectify formatting inconsistencies. They could standardise date formats and other variables, ensuring that data from different collection methods could be seamlessly unified. A critical feature for AAOO was the ability to add detailed descriptions to each column. This process of adding context and meaning to each data point is fundamental to building a high-quality, culturally nuanced AI dataset.\n\n![Cambodia](./assets/use-cases/cambodia.jpg)\n\nErrors flagged showing data inconsistency from the converted unstructured data into a structured format without considering the standard format and time stamps (Data on Indigenous Knowledge Systems on Plant use)\n\nLearn more: [https://blog.okfn.org/2025/11/05/open-data-editor-in-action-building-culturally-accurate-and-global-south-sensitive-ai-datasets/](https://blog.okfn.org/2025/11/05/open-data-editor-in-action-building-culturally-accurate-and-global-south-sensitive-ai-datasets/) "
  },
  {
    "path": "docs/source/use-cases/library-data-india.md",
    "content": "## Library data (India)\n\nThe Indian Institute of Technology (IIT) Delhi used ODE to check errors in large spreadsheets of publications catalogue and educate colleagues about data quality.\n\nODE automatically flagged a range of issues in complex datasets, including bibliographic information from major indexes like Scopus and Web of Science. By identifying formatting inconsistencies, ODE provided a foundation for the team to clean and standardise fields, creating more reliable datasets for their reports and website.\n\n![India](./assets/use-cases/india.png)\n\nOpen Data Editor helps automate error detection; above, problems with blank cells and the data types expected for a given column.\n\nLearn more: [https://blog.okfn.org/2025/12/09/open-data-editor-in-action-advancing-data-quality-in-academic-library-services-in-india/](https://blog.okfn.org/2025/12/09/open-data-editor-in-action-advancing-data-quality-in-academic-library-services-in-india/) "
  },
  {
    "path": "docs/source/user-guide/assets/table-error-list/column-name-missing.csv",
    "content": "col1,\n1,2\n3,4\n"
  },
  {
    "path": "docs/source/user-guide/assets/table-error-list/duplicate-column-name.csv",
    "content": "col1,col1\n1,2\n3,4\n"
  },
  {
    "path": "docs/source/user-guide/assets/table-error-list/empty-row.csv",
    "content": "col1,col2\n1,2\n\n3,4\n"
  },
  {
    "path": "docs/source/user-guide/assets/table-error-list/extra-cell.csv",
    "content": "col1,col2\n1,2\n3,4\n5,6,7\n"
  },
  {
    "path": "docs/source/user-guide/assets/table-error-list/header-missing.csv",
    "content": ",\n1,2\n3,4\n"
  },
  {
    "path": "docs/source/user-guide/assets/table-error-list/wrong-data-type.csv",
    "content": "col1,col2\n1,2\n3,4\n5,6\n7,8\n9,10\n11,12\n13,14\n15,16\n17,18\n19,20\n21,bad\n"
  },
  {
    "path": "docs/source/user-guide/deleting-files-or-folders.md",
    "content": "## Deleting files or folders\n\nTo delete a file or folder, click on the three dots next to the file/folder name and select **Delete**.\n\n![Delete button in the file navigator](./assets/deleting-files-folder/delete-option.png)"
  },
  {
    "path": "docs/source/user-guide/downloading-ode.md",
    "content": "## Downloading ODE\n\nOpen Data Editor is available on all major platforms:\n\n* **For Windows:** Download the most recent **EXE file**.  \n* **For MacOS:** Download the most recent **DMG file**.  \n* **For Debian-based Linux:** Download the most recent **AppImage or DEB file**.  \n* **For other Linux:** Download the most recent **AppImage**.\n\n### From our website \n\nYou can download ODE from our website: [https://okfn.org/opendataeditor/](https://okfn.org/opendataeditor/) \n\n### From GitHub Releases\n\nYou can download ODE from the repository: [https://github.com/okfn/opendataeditor/releases](https://github.com/okfn/opendataeditor/releases) "
  },
  {
    "path": "docs/source/user-guide/editing-errors-in-tables.md",
    "content": "## Editing errors in tables\n\nTo fix cell errors, you can directly edit the data cells in the viewer/editor.\n\n**Step 1:** Locate the cell with the error. For example:\n\n![Error cell](./assets/editing-errors-in-table/cell-with-errors-edit.png)\n\n**Step 2:** Double-click on the cell to start editing content:\n\n![Edit cell with errors](./assets/editing-errors-in-table/edit-cell-with-error.png)\n\n**Step 3:** To save changes, click on another part of the table to accept the change in the cell, and when the **Save changes** button is activated, click on it. The button will get activated if there are unsaved changes.\n\n![Save changes button](./assets/editing-errors-in-table/save-changes-button.png)\n\nAfter clicking the **Save changes** button, ODE will update the Errors Report."
  },
  {
    "path": "docs/source/user-guide/exporting-your-data.md",
    "content": "## Exporting your data\n\nYou can export your data using the **Export** feature located at the top right of the datagrid:\n\n![Publish button](./assets/exporting-data/export-button.png)\n\nOnce you click the **Export** button, ODE will display the following dialogue:\n\n![Publish form](./assets/exporting-data/export-dialog.png)\n\n### Download file\n\nThis option will download the file in CSV format.\n\n### Download file with errors\n\nThis option will export an Excel file with three sheets:\n\n* **Data:** This sheet contains the original table with all the errors painted in red.  \n* **Errors Description:** This sheet contains the description of the errors detected by ODE with the corresponding cell (row and column).  \n* **Blank Rows:** This sheet contains the rows on the original table that did not contain any values."
  },
  {
    "path": "docs/source/user-guide/full-list-of-table-errors-detected.md",
    "content": "## Full list of table errors detected\n\nHere we describe the list of errors that ODE can detect after users upload tables to the app. All the examples are based on CSV files.\n\n:::{note} \nIt is possible to reproduce a subset of these errors using other formats like Excel, but some errors might not be applicable to other formats. \n:::\n\nTo explain and understand errors, we need to illustrate some key elements that are part of tables:\n\n* A regular table contains one **header row** (where the names of columns are listed), **rows** and **cells**.  \n* **Cells describing names of columns** are also called **labels**.  \n* Rows contain **cells** called **values**.\n\nThe table example is represented as follows:\n\n```\n[1] [header row] label 1  | label 2\n[2] [data row]   value 1  | value 2\n[3] [data row]   value 3  | value 4\n```\n\n### Errors detected automatically\n\n**This type of error occurs when the structure of the data is not as expected.** For example, the number of columns in a row is different from the number of columns in the header.\n\n#### Header missing (Blank Label)\n\nThis error occurs when the **header row is empty**. The header row should contain the names of the columns:\n\n```\n,\n1,2\n3,4\n```\n\n* [Reproduce the error using this file](https://opendataeditor.okfn.org/_downloads/bb8d52a4fb7cb7ce135cda4ac8156173/header-missing.csv)\n\nThis is how ODE will show the error:\n\n![Header missing error](./assets/table-error-list/header-missing.png)\n\n#### Column name missing\n\nThis error occurs when **one or more column names are missing**:\n\n```\ncol1,\n1,2\n3,4\n```\n\n* [Reproduce the error using this file](https://opendataeditor.okfn.org/_downloads/45ba64a900f79509f76ef9865da818fe/column-name-missing.csv)\n\nThis is how ODE will show the error:\n\n![Column name missing error](./assets/table-error-list/column-name-missing.png)\n\n#### Duplicate column name\n\nThis error occurs when there are **two or more columns with the same name**. Each column should have a unique name.\n\n```\ncol1,col1\n1,2\n3,4\n```\n\n* [Reproduce the error using this file](https://opendataeditor.okfn.org/_downloads/c352eb003acbc2caf2f45df0d37631c9/duplicate-column-name.csv)\n\nThis is how ODE will show the error:\n\n![Duplicated column name error](./assets/table-error-list/duplicate-column-name.png)\n\n#### Empty row\n\nThis error occurs when an **empty row is present in the data**.\n\n```\ncol1,col2\n1,2\n\n3,4\n```\n\n* [Reproduce the error using this file](https://opendataeditor.okfn.org/_downloads/4cd0ff415190ada70a6cf765842f7aff/empty-row.csv)\n\nThis is how ODE will show the error:\n\n![Empty row error](./assets/table-error-list/empty-row.png)\n\n#### Missing cell\n\nThis error occurs when **a row has fewer cells than the header**.\n\n```\ncol1,col2\n1,2\n3,4\n5\n```\n\nThis is how ODE will show the error:\n\n![Missing cell error](./assets/table-error-list/missing-cell.png)\n\n#### Extra cell\n\nThis error occurs when a row has more cells than the header. Each row should have the same number of cells as the header.\n\n```\ncol1,col2\n1,2\n3,4\n5,6,7\n```\n\n* [Reproduce the error using this file](https://opendataeditor.okfn.org/_downloads/1c7d6743ad084bf1dbe1de05b961e158/extra-cell.csv)\n\nThis is how ODE will show the error:\n\n![Extra cell error](./assets/table-error-list/extra-cell.png)\n\n#### Wrong data type\n\nThis error occurs when **a cell contains a value that is not of the expected type**. For example, a cell in a column that should contain numbers contains a string.\n\n```\ncol1,col2\n1,2\n3,4\n5,6\n7,8\n9,10\n11,12\n13,14\n15,16\n17,18\n19,20\n21,bad\n```\n\n* [Reproduce the error using this file](https://opendataeditor.okfn.org/_downloads/508223106a8c55b55430211419875f01/wrong-data-type.csv)\n\nThis is how ODE will show the error:\n\n![Wrong data type](./assets/table-error-list/wrong-data-type.png)\n\n:::{note}\nThis error can be identified without providing a Table Schema, but only if the data has enough cells of the correct type in the column to infer the intended type. :::\n\n### Errors Requiring Metadata\n\nThese errors can only be identified if a Table Dialect or Table Schema is provided by editing the table’s metadata. The Table Schema defines the structure of the data, including the type of each column. Table Schema adds additional constraints to the data, which are used to validate the data.\n\n#### Extra column name\n\nThis error occurs when **a header label in the data is not defined in the Table Schema**. The Table Schema should define all the columns in the data.\n\n```\nfields:\n  - name: col1\n  - name: col2\n\ncol1,col2,col3\n1,2\n3,4\n\nCell col3 is an extra label\n```\n\n#### Missing column name\n\nThis error occurs when **a column defined in the Table Schema is not present in the header row**. The data should contain all the columns defined in the Table Schema.\n\n```\nfields:\n  - name: col1\n  - name: col2\n  - name: col3\n\ncol1,col2\n1,2,3\n4,5,6\n\nMissing cell col3 is a missing label.\n```\n\n#### Incorrect column name\n\nThis error occurs when **the header label in the data does not match the label defined in the Table Schema**. The header row should contain the same labels as defined in the Table Schema.\n\n```\nfields:\n  - name: col1\n  - name: col2\n\ncol1,col3\n1,2\n3,4\n\nCell col3 is an incorrect label.\n```\n\n#### Primary Key Error\n\nThis error occurs when the **primary key constraint defined in the Table Schema is not satisfied**. The primary key constraint ensures that the values in the specified columns are unique.\n\n```\nfields:\n  - name: col1\n  - name: col2\n\nprimaryKey: col1\ncol1,col2\n1,2\n1,4\n\nCell 1 in the second data row is not unique.\n```\n\n#### Foreign Key Error\n\nThis error occurs when the **foreign key constraint defined in the Table Schema is not satisfied**. The foreign key constraint ensures that the values in the specified columns are present in another table or satisfies self-referencing constraint.\n\n```\nfields:\n  - name: col1\n  - name: col2\nforeignKeys:\n  - fields: col2\n    reference:\n      fields: col1\n\ncol1,col2\n1,2\n2,4\n\nCell 4 in the second data row is not present in the col1 column.\n```\n\n#### Unique constraint error\n\nThis error occurs when the **unique constraint defined in the Table Schema is not satisfied**. The unique constraint ensures that the values in the specified columns are unique.\n\n```\nfields:\n  - name: col1\n  - name: col2\n    unique: true\n\ncol1,col2\n1,2\n3,2\n\nCell 2 in the second data row is not unique.\n```\n\n#### Constraint Error\n\nThis error occurs when **a field constraint defined in the Table Schema is not satisfied**.\n\nRead more about Table Schema constraints: [https://datapackage.org/standard/table-schema/\\#field-constraints](https://datapackage.org/standard/table-schema/#field-constraints) \n\n```\nfields:\n  - name: col1\n  - name: col2\n    constraints:\n      - required: true\n\ncol1,col2\n1,2\n3\n\nMissing cell 4 in the second data row is required.\n```\n\nThe following constraints can be defined in the Table Schema and are currently supported by Open Data Editor (please read the section above about unique constraints):\n\n- required\n- enum\n- minimum\n- maximum\n- minLength\n- maxLength\n- pattern"
  },
  {
    "path": "docs/source/user-guide/how-to-explore-and-edit-metadata.md",
    "content": "## How to explore and edit metadata\n\nTo explore or edit the metadata, select a file from the menu on the left and click on any cell of the header row (first row).\n\n![Metadata Button](./assets/explore-edit-metadata/metadata-button.png)\n\nODE will then display the **Metadata** window:\n\n![Metadata panel](./assets/explore-edit-metadata/metadata-panel.png)\n\nYou can click on any of the options to start editing the metadata linked to your file.\n\nOnce you have finished editing the metadata, click on the **Save changes** button to save the changes.\n\n:::{note} \nSaving changes will trigger a validation of the file. \n:::"
  },
  {
    "path": "docs/source/user-guide/how-to-explore-table-errors.md",
    "content": "## How to explore table errors\n\nAs mentioned in the **Uploading data** section in this guide, if a file has errors, ODE will show a red dot next to the file name on the sidebar. However, if you want to review errors in the table, you can use the datagrid to explore problematic data.\n\nODE will highlight the cell in red if it has a problem. For instance, if it contains text instead of a number.\n\nThis is how a cell with an error is shown on ODE:\n\n![Cell with errors](./assets/explore-table-errors/cell-with-error-edit.png)\n\nYou can also explore errors by clicking on the **Errors Report** button, located at the top left of the datagrid:\n\n![Errors panel button](./assets/explore-table-errors/errors-panel-button.png)\n\nAfter clicking the button, ODE will display a panel with the full list of errors:\n\n![Errors panel](./assets/explore-table-errors/errors-panel.png)"
  },
  {
    "path": "docs/source/user-guide/how-to-use-the-ai-component.md",
    "content": "## How to use the AI component\n\nTo use the AI assistant, select a file from the sidebar, and then click on the **AI** button located in the top right corner of the app:\n\n![AI Integration button is located in the top panel](./assets/ai-integration/ai-integration-1.png)\n\nODE will show a dialogue to assist the user in downloading the model:\n\n![Downloading model dialog](./assets/ai-integration/ai-integration-2.png)\n\nOnce the model is downloaded, users will be able to click on the **Next** button to continue.\n\n### AI Use Cases\n\nODE has two use cases for the AI component:\n\n1. Assist users in understanding the columns of a table.  \n2. Suggest analysis and questions that users can use to query the data.\n\nTo choose the use case, select it from the dropdown menu.\n\n![AI Use Cases dropdown menu](./assets/ai-integration/ai-integration-3.png)\n\nOnce selected, click on the **Execute** button to get a response from the AI model.\n\n:::{note}\nDepending on the hardware of the users’ machines, the response time can vary. Usually, it will take around 10 seconds to get a response. :::\n\n#### Assist users in understanding the columns of a table\n\nThe AI model will analyse the columns’ names, types and some sample data and will generate a description of each column. This is useful to understand the data better, clarify technical or complex names, expand acronyms, etc.\n\n![AI assistance in understanding the columns of a table](./assets/ai-integration/ai-integration-4.png)\n\n#### Suggest analysis and questions that users can use to query the data\n\nThe AI model will analyse the columns’ names, types and some sample data and will generate a list of questions that the user can use to query the data.\n\n![AI assistance in understanding the columns of a table](./assets/ai-integration/ai-integration-5.png)"
  },
  {
    "path": "docs/source/user-guide/installing-ode.md",
    "content": "## Installing ODE\n\n### Windows\n\nDownload the most recent **EXE** file as per the above instructions.\n\n1\\. If you receive the following message, click ‘Continue download’.\n\n![DOWNLOAD SECURITY](./assets/getting-started/gs-windows-download.png)\n\n2\\. After downloading, double-click to run the app. You may encounter the security message window, click ‘More info’ and proceed.\n\n![SECURITY MESSAGE](./assets/getting-started/gs-protection-screen.png)\n\n3\\. Click ‘Run anyway’ to run the application.\n\n![SECURITY MESSAGE STEP 2](./assets/getting-started/gs-protection-screen-2.png)\n\n### MacOS\n\nDownload the most recent **DMG** file as per the above instructions.\n\n1\\. If you encounter a security message, click on the question mark and then click the link in the first section.\n\n![DOWNLOAD SECURITY](./assets/getting-started/gs-macos-download.png)\n\n2\\. Change settings to allow the app to execute.\n\n![DOWNLOAD SETTINGS](./assets/getting-started/gs-macos-download-step2.png)\n\n### Linux\n\nFor Linux, there are two options available:\n\n* AppImage (for any distributions)  \n* deb (for Ubuntu/Debian)\n\n#### Any Distribution\n\nDownload the most recent **AppImage** file as per the above instructions.\n\nAfter downloading, you have to make it executable:\n\n![MAKE EXECUTABLE](./assets/getting-started/gs-linux-executable.png)\n\nThen double-click on the file to start the application.\n\n#### Ubuntu/Debian\n\nDownload the most recent **DEB** file as per the above instructions.\n\nDouble-click on the file, and it will initiate the installation process.\n\n![MAKE INSTALLATION](./assets/getting-started/gs-ode-installation.png)\n\nAfter installation, you can use it.\n\n![INSTALLED APP](./assets/getting-started/gs-ode-app.png)\n\nOptionally, in Debian, you can install it by running the following command:\n\n*\\# Replace \\<version\\> with the version you downloaded*  \nsudo dpkg \\-i opendataeditor-linux-\\<version\\>.deb"
  },
  {
    "path": "docs/source/user-guide/uploading-data.md",
    "content": "## Uploading data\n\nThis section explains how to upload tabular files, folders with tables and online data to ODE. The tool also ingests other types of format files like PDF, JPEG, etc. However, please note, ODE's main objective is to detect errors on tabular files, the application will **ONLY** show previews for tables.\n\n**Uploading data to ODE is easy\\!** After installing the app and once you open the application on your laptop, you will see this screen:\n\n![Uploading data](./assets/uploading-data/uploading-data.png)\n\nYou can click on the **Upload your data** button, located in the centre of the screen or at the top left of the sidebar, to start adding files/folders to the app.\n\n:::{note} \nEach time you upload a file or folder to ODE, the application will not ingest the original file/folder from your computer. Instead, it will make a copy of it and add it to the application folder on your laptop. After the ingestion process ends, you can right-click on the file/folder and select the **Open Location** option. By doing so, ODE will redirect you to the exact location where the copy was saved. \n:::\n\n![Open file location](./assets/uploading-data/open-location.png)\n\n### Excel, CSV files and folders\n\nWhen clicking on the **Upload your data** button, ODE will display the following dialogue. If you want to upload files or folders, you can do so from the **From Your Computer** section. If you want to add tables that are online, you can do it from **Add External Data**:\n\n![Upload files from your computer](./assets/uploading-data/uploading-data-1.png)\n\nYou will see there are two options available in the **From your computer** section. To add file/s, click **Select** on the **Add one or more Excel or csv files** feature. If you want to ingest one or many folders, click Select on the **Add one or more folders** box.\n\nOnce the ingestion process concludes, ODE will add your data to the sidebar of the app:\n\n![Uploading data sidebar](./assets/uploading-data/uploading-data-sidebar.png)\n\nNote that while uploading your data, the tool checks your files or folders to find errors according to the validation rules provided by [Frictionless](https://framework.frictionlessdata.io/). Please, check the **Full list of table errors detected** in this guide to learn more.\n\nPlease keep in mind that since the preview section (datagrid) can only show one tabular file at a time, when uploading folders, and after the ingestion process is done, you will need to click on the folder and select the file you want to visualise on the screen. As soon as you click on a specific file, ODE will start validating your data (looking for possible errors), and the table will be shown on the app.\n\n#### Tables published online\n\nODE also allows users to upload online tables. You can upload files from open data portals, Google Sheets or tables from your GitHub repository.\n\nTo upload online tables, first click the **Upload your data** button and then select the **Add External Data** section:\n\n![Upload online tables](./assets/uploading-data/tables-published-online.png)\n\nNow, write or paste the URL to the table and click the **Add** button:\n\n![Upload online table URL input](./assets/uploading-data/tables-published-online-2.png)\n\nAfter that, ODE will start reviewing your file in the background to detect possible errors, and data will be displayed on the main screen.\n\n:::{note}\nBefore you upload your online table…\n\n👉🏼 If you are uploading a Google Sheets file, check that the file is published online. If you don’t know how to do it, please visit [this page](https://support.google.com/docs/answer/183965?hl=en&amp;co=GENIE.Platform%3DDesktop) and follow the steps listed there.\n\n👉🏼 For Google Sheets, please make sure you are adding the public version of your file without the HTML term at the end. For example:\n\n✅ https://docs.google.com/spreadsheets/d/1dFVoF6f9VU5pjaGhyyvQaBN0n6ae-iLCtlvsO1N2jhA/edit?gid=0\\#gid=0\n\n❌ https://docs.google.com/spreadsheets/d/e/2PACX-1vQ8w9yb7D-iYEbImb0WD4Kh53\\_Yp7H1VOi1bIMcicphWbkrrH9PobXCJhXt9frqyQ/pubhtml\n\n👉🏼 When exporting a file from Google Sheets in CSV and you have columns with numbers, please make sure to use “.” for decimals, instead of commas. Otherwise, Frictionless, the code working behind ODE will interpret the content of your cells with numbers as text.\n\nFor all tables…\n\n👉🏼 Make sure your file is well-organised (well-formatted: each column must contain a name, there should be no extra rows before the table, or additional elements (like borders or lines) next to the space where the tabular data is located.\n\n👉🏼 Check that the tabular data does not contain cells that are merged. Data producers from Governments, international organizations and internal reports usually add tiles, descriptions, and graphs within sheets, like in this case. If there are extra elements in your file, ODE will ingest your file and show you multiple errors. \n:::"
  },
  {
    "path": "packaging/linux/opendataeditor.desktop",
    "content": "[Desktop Entry]\n\n# The type of the thing this desktop file refers to\nType=Application\n\n# The Application Name\nName=Open Data Editor\n\n# Tooltip comment to show in menus\nComment=Data management for humans.\n\n# The path (folder) in which the executable is run\nPath=/opt/opendataeditor\n\n# The executable (can include arguments)\nExec=/opt/opendataeditor/opendataeditor\n\n# The icon we install for the application, use the target filesystem path\nIcon=org.okfn.opendataeditor\n"
  },
  {
    "path": "packaging/macos/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "packaging/windows/installer.nsi",
    "content": "; NSIS Script to create a Windows Installer.\n;\n; To create a windows installer:\n;   1. Install NSIS from https://nsis.sourceforge.io/Download\n;   2. Build your application with PyInstaller first: python build.py\n;   3. Create the installer: 'C:\\Program Files (x86)\\NSIS\\makensis.exe' .\\packaging\\windows\\installer.nsi\n;\n; The APP_ID was originaly set by electron-builder in the first versions of the application and we are maintaining it.\n; https://www.electron.build/nsis.html#guid-vs-application-name\n\n!define APP_ID \"42c092cd-67f7-566d-b9a4-980d3103f082\"\n!define APP_NAME \"Open Data Editor\"\n!define PUBLISHER \"Open Knowledge Foundation\"\n!define INSTALL_DIR \"$LOCALAPPDATA\\Programs\\opendataeditor\"\n\n; Required version parameter\n!ifndef APP_VERSION\n    !error \"APP_VERSION must be defined via -DAPP_VERSION=x.y.z command parameter.\"\n!endif\n\n; Modern UI setup\n!include \"MUI2.nsh\"\n!include \"LogicLib.nsh\"\n!include \"FileFunc.nsh\" ; To calculate Estimated Size\n\nName \"${APP_NAME}\"\nOutFile \"opendataeditor-win-${APP_VERSION}.exe\"\nInstallDir \"${INSTALL_DIR}\"\nRequestExecutionLevel user ; No admin privileges needed for user-level install\n\n; Registry key for uninstaller\n!define UNINST_KEY \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}\"\n!define MUI_ICON \"icon.ico\"\n!define MUI_UNICON \"icon.ico\"\n\n!insertmacro MUI_PAGE_WELCOME\n!insertmacro MUI_PAGE_INSTFILES\n!insertmacro MUI_UNPAGE_CONFIRM\n!insertmacro MUI_UNPAGE_INSTFILES\n!insertmacro MUI_LANGUAGE \"English\"\n\nSection \"Install\"\n    ; Remove previous installation\n    RMDir /r \"$INSTDIR\"\n\n    ; Create installation directory\n    SetOutPath \"$INSTDIR\"\n    \n    ; Copy application files from PyInstaller output\n    File /r \"..\\..\\dist\\opendataeditor\\*.*\"\n\n    ; Calculate installed size\n    ${GetSize} \"$INSTDIR\" \"/S=0K\" $0 $1 $2\n    IntFmt $0 \"0x%08X\" $0\n    \n    ; Create shortcuts\n    CreateDirectory \"$SMPROGRAMS\\${APP_NAME}\"\n    CreateShortcut \"$SMPROGRAMS\\${APP_NAME}\\${APP_NAME}.lnk\" \"$INSTDIR\\opendataeditor.exe\"\n    CreateShortcut \"$SMPROGRAMS\\${APP_NAME}\\Uninstall.lnk\" \"$INSTDIR\\Uninstall opendataeditor.exe\"\n\n    ; Write uninstaller\n    WriteUninstaller \"$INSTDIR\\Uninstall opendataeditor.exe\"\n\n    ; Write registry entries\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"DisplayName\" \"${APP_NAME}\"\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"DisplayVersion\" \"${APP_VERSION}\"\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"Publisher\" \"${PUBLISHER}\"\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"UninstallString\" '\"$INSTDIR\\Uninstall opendataeditor.exe\" /currentuser'\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"DisplayIcon\" \"$INSTDIR\\opendataeditor.exe\"\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"InstallLocation\" \"$INSTDIR\"\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"NoModify\" 1\n    WriteRegStr HKCU \"${UNINST_KEY}\" \"NoRepair\" 1\n    WriteRegDWORD HKCU \"${UNINST_KEY}\" \"EstimatedSize\" \"$0\"\nSectionEnd\n\nSection \"Uninstall\"\n    ; Remove application files\n    RMDir /r \"$INSTDIR\"\n\n    ; Remove shortcuts\n    RMDir /r \"$SMPROGRAMS\\${APP_NAME}\"\n\n    ; Remove registry entries\n    DeleteRegKey HKCU \"${UNINST_KEY}\"\nSectionEnd"
  },
  {
    "path": "pyproject.sublime-workspace",
    "content": "{}"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"opendataeditor\"\nversion = \"1.7.1\" # Update this version number also in ode/__init__.py\ndescription = \"A no-code application to explore and validate tabular data in a simple way. \"\nreadme = \"README.md\"\nrequires-python = \">=3.13\"\ndependencies = [\n    \"frictionless[excel,github]>=5.18.1\",\n    \"llama-cpp-python>=0.3.16\",\n    \"openpyxl>=3.1.5\",\n    \"pyside6>=6.10.0\",\n    \"xlrd>=2.0.2\",\n    \"xlutils>=2.0.0\",\n    \"xlwt>=1.3.0\",\n]\n\n[project.scripts]\node = \"ode.main:main\"\n\n[build-system]\nrequires = [\"uv_build>=0.9.10,<0.10.0\"]\nbuild-backend = \"uv_build\"\n\n[tool.uv.build-backend]\nmodule-name = \"ode\"\n\n[tool.ruff]\nline-length = 140\n\n[dependency-groups]\ndev = [\n    \"ipdb>=0.13.13\",\n    \"mypy>=1.18.2\",\n    \"myst-parser>=4.0.1\",\n    \"pyinstaller==6.11.1\",\n    \"pytest>=9.0.0\",\n    \"pytest-qt>=4.5.0\",\n    \"ruff>=0.14.4\",\n    \"sphinx>=8.2.3\",\n    \"sphinx-rtd-theme>=3.0.2\",\n]\n"
  },
  {
    "path": "src/ode/__init__.py",
    "content": ""
  },
  {
    "path": "src/ode/assets/__init__.py",
    "content": "# Required for PyInstaller to collect all assets\n"
  },
  {
    "path": "src/ode/assets/licenses.json",
    "content": "[\n  {\n    \"name\": \"AAL\",\n    \"path\": \"https://opensource.org/licenses/AAL\",\n    \"title\": \"Attribution Assurance Licenses\"\n  },\n  {\n    \"name\": \"AFL-3.0\",\n    \"path\": \"https://opensource.org/licenses/AFL-3.0\",\n    \"title\": \"Academic Free License 3.0\"\n  },\n  {\n    \"name\": \"AGPL-3.0\",\n    \"path\": \"https://opensource.org/licenses/AGPL-3.0\",\n    \"title\": \"GNU Affero General Public License v3\"\n  },\n  {\n    \"name\": \"APL-1.0\",\n    \"path\": \"https://opensource.org/licenses/APL-1.0\",\n    \"title\": \"Adaptive Public License 1.0\"\n  },\n  {\n    \"name\": \"APSL-2.0\",\n    \"path\": \"https://opensource.org/licenses/APSL-2.0\",\n    \"title\": \"Apple Public Source License 2.0\"\n  },\n  {\n    \"name\": \"Against-DRM\",\n    \"path\": \"https://opendefinition.org/licenses/against-drm\",\n    \"title\": \"Against DRM\"\n  },\n  {\n    \"name\": \"Apache-1.1\",\n    \"path\": \"https://opensource.org/licenses/Apache-1.1\",\n    \"title\": \"Apache Software License 1.1\"\n  },\n  {\n    \"name\": \"Apache-2.0\",\n    \"path\": \"https://opensource.org/licenses/Apache-2.0\",\n    \"title\": \"Apache Software License 2.0\"\n  },\n  {\n    \"name\": \"Artistic-2.0\",\n    \"path\": \"https://opensource.org/licenses/Artistic-2.0\",\n    \"title\": \"Artistic License 2.0\"\n  },\n  {\n    \"name\": \"BSD-2-Clause\",\n    \"path\": \"https://opensource.org/licenses/BSD-2-Clause\",\n    \"title\": \"BSD 2-Clause \\\"Simplified\\\" or \\\"FreeBSD\\\" License (BSD-2-Clause)\"\n  },\n  {\n    \"name\": \"BSD-3-Clause\",\n    \"path\": \"https://opensource.org/licenses/BSD-3-Clause\",\n    \"title\": \"BSD 3-Clause \\\"New\\\" or \\\"Revised\\\" License (BSD-3-Clause)\"\n  },\n  {\n    \"name\": \"BSL-1.0\",\n    \"path\": \"https://opensource.org/licenses/BSL-1.0\",\n    \"title\": \"Boost Software License 1.0\"\n  },\n  {\n    \"name\": \"BitTorrent-1.1\",\n    \"path\": \"https://spdx.org/licenses/BitTorrent-1.1\",\n    \"title\": \"BitTorrent Open Source License 1.1\"\n  },\n  {\n    \"name\": \"CATOSL-1.1\",\n    \"path\": \"https://opensource.org/licenses/CATOSL-1.1\",\n    \"title\": \"Computer Associates Trusted Open Source License 1.1 (CATOSL-1.1)\"\n  },\n  {\n    \"name\": \"CC-BY-4.0\",\n    \"path\": \"https://creativecommons.org/licenses/by/4.0/\",\n    \"title\": \"Creative Commons Attribution 4.0\"\n  },\n  {\n    \"name\": \"CC-BY-NC-4.0\",\n    \"path\": \"https://creativecommons.org/licenses/by-nc/4.0/\",\n    \"title\": \"Creative Commons Attribution-NonCommercial 4.0\"\n  },\n  {\n    \"name\": \"CC-BY-NC-ND-4.0\",\n    \"path\": \"https://creativecommons.org/licenses/by-nc-nd/4.0/\",\n    \"title\": \"Attribution-NonCommercial-NoDerivatives 4.0\"\n  },\n  {\n    \"name\": \"CC-BY-NC-SA-4.0\",\n    \"path\": \"https://creativecommons.org/licenses/by-nc-sa/4.0/\",\n    \"title\": \"Attribution-NonCommercial-ShareAlike 4.0\"\n  },\n  {\n    \"name\": \"CC-BY-ND-4.0\",\n    \"path\": \"https://creativecommons.org/licenses/by-nd/4.0/\",\n    \"title\": \"Attribution-NoDerivatives 4.0\"\n  },\n  {\n    \"name\": \"CC-BY-SA-4.0\",\n    \"path\": \"https://creativecommons.org/licenses/by-sa/4.0/\",\n    \"title\": \"Creative Commons Attribution Share-Alike 4.0\"\n  },\n  {\n    \"name\": \"CC0-1.0\",\n    \"path\": \"https://creativecommons.org/publicdomain/zero/1.0/\",\n    \"title\": \"CC0 1.0\"\n  },\n  {\n    \"name\": \"CDDL-1.0\",\n    \"path\": \"https://opensource.org/licenses/CDDL-1.0\",\n    \"title\": \"Common Development and Distribution License 1.0\"\n  },\n  {\n    \"name\": \"CECILL-2.1\",\n    \"path\": \"https://opensource.org/licenses/CECILL-2.1\",\n    \"title\": \"CeCILL License 2.1\"\n  },\n  {\n    \"name\": \"CNRI-Python\",\n    \"path\": \"https://opensource.org/licenses/CNRI-Python\",\n    \"title\": \"CNRI Python License\"\n  },\n  {\n    \"name\": \"CPAL-1.0\",\n    \"path\": \"https://opensource.org/licenses/CPAL-1.0\",\n    \"title\": \"Common Public Attribution License 1.0\"\n  },\n  {\n    \"name\": \"CUA-OPL-1.0\",\n    \"path\": \"https://opensource.org/licenses/CUA-OPL-1.0\",\n    \"title\": \"CUA Office Public License 1.0\"\n  },\n  {\n    \"name\": \"DSL\",\n    \"path\": \"https://opendefinition.org/licenses/dsl\",\n    \"title\": \"Design Science License\"\n  },\n  {\n    \"name\": \"ECL-2.0\",\n    \"path\": \"https://opensource.org/licenses/ECL-2.0\",\n    \"title\": \"Educational Community License 2.0\"\n  },\n  {\n    \"name\": \"EFL-2.0\",\n    \"path\": \"https://opensource.org/licenses/EFL-2.0\",\n    \"title\": \"Eiffel Forum License 2.0\"\n  },\n  {\n    \"name\": \"EPL-1.0\",\n    \"path\": \"https://opensource.org/licenses/EPL-1.0\",\n    \"title\": \"Eclipse Public License 1.0\"\n  },\n  {\n    \"name\": \"EPL-2.0\",\n    \"path\": \"https://opensource.org/licenses/EPL-2.0\",\n    \"title\": \"Eclipse Public License 2.0\"\n  },\n  {\n    \"name\": \"EUDatagrid\",\n    \"path\": \"https://opensource.org/licenses/EUDatagrid\",\n    \"title\": \"EU DataGrid Software License\"\n  },\n  {\n    \"name\": \"EUPL-1.1\",\n    \"path\": \"https://opensource.org/licenses/EUPL-1.1\",\n    \"title\": \"European Union Public License 1.1\"\n  },\n  {\n    \"name\": \"Entessa\",\n    \"path\": \"https://opensource.org/licenses/Entessa\",\n    \"title\": \"Entessa Public License\"\n  },\n  {\n    \"name\": \"FAL-1.3\",\n    \"path\": \"https://opendefinition.org/licenses/fal\",\n    \"title\": \"Free Art License 1.3\"\n  },\n  {\n    \"name\": \"Fair\",\n    \"path\": \"https://opensource.org/licenses/Fair\",\n    \"title\": \"Fair License\"\n  },\n  {\n    \"name\": \"Frameworx-1.0\",\n    \"path\": \"https://opensource.org/licenses/Frameworx-1.0\",\n    \"title\": \"Frameworx License 1.0\"\n  },\n  {\n    \"name\": \"GFDL-1.3-no-cover-texts-no-invariant-sections\",\n    \"path\": \"https://opendefinition.org/licenses/gfdl\",\n    \"title\": \"GNU Free Documentation License 1.3 with no cover texts and no invariant sections\"\n  },\n  {\n    \"name\": \"GPL-2.0\",\n    \"path\": \"https://opensource.org/licenses/GPL-2.0\",\n    \"title\": \"GNU General Public License 2.0\"\n  },\n  {\n    \"name\": \"GPL-3.0\",\n    \"path\": \"https://opensource.org/licenses/GPL-3.0\",\n    \"title\": \"GNU General Public License 3.0\"\n  },\n  {\n    \"name\": \"HPND\",\n    \"path\": \"https://opensource.org/licenses/HPND\",\n    \"title\": \"Historical Permission Notice and Disclaimer\"\n  },\n  {\n    \"name\": \"IPA\",\n    \"path\": \"https://opensource.org/licenses/IPA\",\n    \"title\": \"IPA Font License\"\n  },\n  {\n    \"name\": \"IPL-1.0\",\n    \"path\": \"https://opensource.org/licenses/IPL-1.0\",\n    \"title\": \"IBM Public License 1.0\"\n  },\n  {\n    \"name\": \"ISC\",\n    \"path\": \"https://opensource.org/licenses/ISC\",\n    \"title\": \"ISC License\"\n  },\n  {\n    \"name\": \"Intel\",\n    \"path\": \"https://opensource.org/licenses/Intel\",\n    \"title\": \"Intel Open Source License\"\n  },\n  {\n    \"name\": \"LGPL-2.1\",\n    \"path\": \"https://opensource.org/licenses/LGPL-2.1\",\n    \"title\": \"GNU Lesser General Public License 2.1\"\n  },\n  {\n    \"name\": \"LGPL-3.0\",\n    \"path\": \"https://opensource.org/licenses/LGPL-3.0\",\n    \"title\": \"GNU Lesser General Public License 3.0\"\n  },\n  {\n    \"name\": \"LO-FR-2.0\",\n    \"path\": \"https://www.etalab.gouv.fr/licence-ouverte-open-licence\",\n    \"title\": \"Open License 2.0\"\n  },\n  {\n    \"name\": \"LPL-1.0\",\n    \"path\": \"https://opensource.org/licenses/LPL-1.0\",\n    \"title\": \"Lucent Public License (\\\"Plan9\\\") 1.0\"\n  },\n  {\n    \"name\": \"LPL-1.02\",\n    \"path\": \"https://opensource.org/licenses/LPL-1.02\",\n    \"title\": \"Lucent Public License 1.02\"\n  },\n  {\n    \"name\": \"LPPL-1.3c\",\n    \"path\": \"https://opensource.org/licenses/LPPL-1.3c\",\n    \"title\": \"LaTeX Project Public License 1.3c\"\n  },\n  {\n    \"name\": \"MIT\",\n    \"path\": \"https://opensource.org/licenses/MIT\",\n    \"title\": \"MIT License\"\n  },\n  {\n    \"name\": \"MPL-1.0\",\n    \"path\": \"https://opensource.org/licenses/MPL-1.0\",\n    \"title\": \"Mozilla Public License 1.0\"\n  },\n  {\n    \"name\": \"MPL-1.1\",\n    \"path\": \"https://opensource.org/licenses/MPL-1.1\",\n    \"title\": \"Mozilla Public License 1.1\"\n  },\n  {\n    \"name\": \"MPL-2.0\",\n    \"path\": \"https://opensource.org/licenses/MPL-2.0\",\n    \"title\": \"Mozilla Public License 2.0\"\n  },\n  {\n    \"name\": \"MS-PL\",\n    \"path\": \"https://opensource.org/licenses/MS-PL\",\n    \"title\": \"Microsoft Public License\"\n  },\n  {\n    \"name\": \"MS-RL\",\n    \"path\": \"https://opensource.org/licenses/MS-RL\",\n    \"title\": \"Microsoft Reciprocal License\"\n  },\n  {\n    \"name\": \"MirOS\",\n    \"path\": \"https://opensource.org/licenses/MirOS\",\n    \"title\": \"MirOS Licence\"\n  },\n  {\n    \"name\": \"Motosoto\",\n    \"path\": \"https://opensource.org/licenses/Motosoto\",\n    \"title\": \"Motosoto License\"\n  },\n  {\n    \"name\": \"Multics\",\n    \"path\": \"https://opensource.org/licenses/Multics\",\n    \"title\": \"Multics License\"\n  },\n  {\n    \"name\": \"NASA-1.3\",\n    \"path\": \"https://opensource.org/licenses/NASA-1.3\",\n    \"title\": \"NASA Open Source Agreement 1.3\"\n  },\n  {\n    \"name\": \"NCSA\",\n    \"path\": \"https://opensource.org/licenses/NCSA\",\n    \"title\": \"University of Illinois/NCSA Open Source License\"\n  },\n  {\n    \"name\": \"NGPL\",\n    \"path\": \"https://opensource.org/licenses/NGPL\",\n    \"title\": \"Nethack General Public License\"\n  },\n  {\n    \"name\": \"NPOSL-3.0\",\n    \"path\": \"https://opensource.org/licenses/NPOSL-3.0\",\n    \"title\": \"Non-Profit Open Software License 3.0\"\n  },\n  {\n    \"name\": \"NTP\",\n    \"path\": \"https://opensource.org/licenses/NTP\",\n    \"title\": \"NTP License\"\n  },\n  {\n    \"name\": \"Naumen\",\n    \"path\": \"https://opensource.org/licenses/Naumen\",\n    \"title\": \"Naumen Public License\"\n  },\n  {\n    \"name\": \"Nokia\",\n    \"path\": \"https://opensource.org/licenses/Nokia\",\n    \"title\": \"Nokia Open Source License\"\n  },\n  {\n    \"name\": \"OCLC-2.0\",\n    \"path\": \"https://opensource.org/licenses/OCLC-2.0\",\n    \"title\": \"OCLC Research Public License 2.0\"\n  },\n  {\n    \"name\": \"ODC-BY-1.0\",\n    \"path\": \"https://opendefinition.org/licenses/odc-by\",\n    \"title\": \"Open Data Commons Attribution License 1.0\"\n  },\n  {\n    \"name\": \"ODbL-1.0\",\n    \"path\": \"https://opendefinition.org/licenses/odc-odbl\",\n    \"title\": \"Open Data Commons Open Database License 1.0\"\n  },\n  {\n    \"name\": \"OFL-1.1\",\n    \"path\": \"https://opensource.org/licenses/OFL-1.1\",\n    \"title\": \"Open Font License 1.1\"\n  },\n  {\n    \"name\": \"OGL-Canada-2.0\",\n    \"path\": \"https://open.canada.ca/en/open-government-licence-canada\",\n    \"title\": \"Open Government License 2.0 (Canada)\"\n  },\n  {\n    \"name\": \"OGL-UK-1.0\",\n    \"path\": \"https://www.nationalarchives.gov.uk/doc/open-government-licence/version/1/\",\n    \"title\": \"Open Government Licence 1.0 (United Kingdom)\"\n  },\n  {\n    \"name\": \"OGL-UK-2.0\",\n    \"path\": \"https://www.nationalarchives.gov.uk/doc/open-government-licence/version/2/\",\n    \"title\": \"Open Government Licence 2.0 (United Kingdom)\"\n  },\n  {\n    \"name\": \"OGL-UK-3.0\",\n    \"path\": \"https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/\",\n    \"title\": \"Open Government Licence 3.0 (United Kingdom)\"\n  },\n  {\n    \"name\": \"OGTSL\",\n    \"path\": \"https://opensource.org/licenses/OGTSL\",\n    \"title\": \"Open Group Test Suite License\"\n  },\n  {\n    \"name\": \"OSL-3.0\",\n    \"path\": \"https://opensource.org/licenses/OSL-3.0\",\n    \"title\": \"Open Software License 3.0\"\n  },\n  {\n    \"name\": \"PDDL-1.0\",\n    \"path\": \"https://opendefinition.org/licenses/odc-pddl\",\n    \"title\": \"Open Data Commons Public Domain Dedication and Licence 1.0\"\n  },\n  {\n    \"name\": \"PHP-3.0\",\n    \"path\": \"https://opensource.org/licenses/PHP-3.0\",\n    \"title\": \"PHP License 3.0\"\n  },\n  {\n    \"name\": \"PostgreSQL\",\n    \"path\": \"https://opensource.org/licenses/PostgreSQL\",\n    \"title\": \"PostgreSQL License\"\n  },\n  {\n    \"name\": \"Python-2.0\",\n    \"path\": \"https://opensource.org/licenses/Python-2.0\",\n    \"title\": \"Python License 2.0\"\n  },\n  {\n    \"name\": \"QPL-1.0\",\n    \"path\": \"https://opensource.org/licenses/QPL-1.0\",\n    \"title\": \"Q Public License 1.0\"\n  },\n  {\n    \"name\": \"RPL-1.5\",\n    \"path\": \"https://opensource.org/licenses/RPL-1.5\",\n    \"title\": \"Reciprocal Public License 1.5\"\n  },\n  {\n    \"name\": \"RPSL-1.0\",\n    \"path\": \"https://opensource.org/licenses/RPSL-1.0\",\n    \"title\": \"RealNetworks Public Source License 1.0\"\n  },\n  {\n    \"name\": \"RSCPL\",\n    \"path\": \"https://opensource.org/licenses/RSCPL\",\n    \"title\": \"Ricoh Source Code Public License\"\n  },\n  {\n    \"name\": \"SISSL\",\n    \"path\": \"https://opensource.org/licenses/SISSL\",\n    \"title\": \"Sun Industry Standards Source License 1.1\"\n  },\n  {\n    \"name\": \"SPL-1.0\",\n    \"path\": \"https://opensource.org/licenses/SPL-1.0\",\n    \"title\": \"Sun Public License 1.0\"\n  },\n  {\n    \"name\": \"SimPL-2.0\",\n    \"path\": \"https://opensource.org/licenses/SimPL-2.0\",\n    \"title\": \"Simple Public License 2.0\"\n  },\n  {\n    \"name\": \"Sleepycat\",\n    \"path\": \"https://opensource.org/licenses/Sleepycat\",\n    \"title\": \"Sleepycat License\"\n  },\n  {\n    \"name\": \"Talis\",\n    \"path\": \"https://opendefinition.org/licenses/tcl\",\n    \"title\": \"Talis Community License\"\n  },\n  {\n    \"name\": \"Unlicense\",\n    \"path\": \"https://unlicense.org/\",\n    \"title\": \"Unlicense\"\n  },\n  {\n    \"name\": \"VSL-1.0\",\n    \"path\": \"https://opensource.org/licenses/VSL-1.0\",\n    \"title\": \"Vovida Software License 1.0\"\n  },\n  {\n    \"name\": \"W3C\",\n    \"path\": \"https://opensource.org/licenses/W3C\",\n    \"title\": \"W3C License\"\n  },\n  {\n    \"name\": \"WXwindows\",\n    \"path\": \"https://opensource.org/licenses/WXwindows\",\n    \"title\": \"wxWindows Library License\"\n  },\n  {\n    \"name\": \"Watcom-1.0\",\n    \"path\": \"https://opensource.org/licenses/Watcom-1.0\",\n    \"title\": \"Sybase Open Watcom Public License 1.0\"\n  },\n  {\n    \"name\": \"Xnet\",\n    \"path\": \"https://opensource.org/licenses/Xnet\",\n    \"title\": \"X.Net License\"\n  },\n  {\n    \"name\": \"ZPL-2.0\",\n    \"path\": \"https://opensource.org/licenses/ZPL-2.0\",\n    \"title\": \"Zope Public License 2.0\"\n  },\n  {\n    \"name\": \"Zlib\",\n    \"path\": \"https://opensource.org/licenses/Zlib\",\n    \"title\": \"zlib/libpng license\"\n  },\n  {\n    \"name\": \"dli-model-use\",\n    \"path\": \"http://data.library.ubc.ca/datalib/geographic/DMTI/license.html\",\n    \"title\": \"Statistics Canada: Data Liberation Initiative (DLI) - Model Data Use Licence\"\n  },\n  {\n    \"name\": \"geogratis\",\n    \"path\": \"http://geogratis.gc.ca/geogratis/licenceGG\",\n    \"title\": \"Geogratis\"\n  },\n  {\n    \"name\": \"hesa-withrights\",\n    \"path\": \"https://web.archive.org/web/20131009082944/http://www.hesa.ac.uk/index.php?option=com_content&task=view&id=2619&Itemid=209\",\n    \"title\": \"Higher Education Statistics Agency Copyright with data.gov.uk rights\"\n  },\n  {\n    \"name\": \"localauth-withrights\",\n    \"path\": \"\",\n    \"title\": \"Local Authority Copyright with data.gov.uk rights\"\n  },\n  {\n    \"name\": \"met-office-cp\",\n    \"path\": \"https://www.metoffice.gov.uk/climatechange/science/monitoring/ukcp09/UKCIP08_license_agreement_130709.pdf\",\n    \"title\": \"Met Office UK Climate Projections Licence Agreement\"\n  },\n  {\n    \"name\": \"mitre\",\n    \"path\": \"https://opensource.org/licenses/CVW\",\n    \"title\": \"MITRE Collaborative Virtual Workspace License (CVW License)\"\n  },\n  {\n    \"name\": \"notspecified\",\n    \"path\": \"\",\n    \"title\": \"License Not Specified\"\n  },\n  {\n    \"name\": \"other-at\",\n    \"path\": \"\",\n    \"title\": \"Other (Attribution)\"\n  },\n  {\n    \"name\": \"other-closed\",\n    \"path\": \"\",\n    \"title\": \"Other (Not Open)\"\n  },\n  {\n    \"name\": \"other-nc\",\n    \"path\": \"\",\n    \"title\": \"Other (Non-Commercial)\"\n  },\n  {\n    \"name\": \"other-open\",\n    \"path\": \"\",\n    \"title\": \"Other (Open)\"\n  },\n  {\n    \"name\": \"other-pd\",\n    \"path\": \"\",\n    \"title\": \"Other (Public Domain)\"\n  },\n  {\n    \"name\": \"ukclickusepsi\",\n    \"path\": \"\",\n    \"title\": \"UK Click Use PSI\"\n  },\n  {\n    \"name\": \"ukcrown\",\n    \"path\": \"\",\n    \"title\": \"UK Crown Copyright\"\n  },\n  {\n    \"name\": \"ukcrown-withrights\",\n    \"path\": \"\",\n    \"title\": \"UK Crown Copyright with data.gov.uk rights\"\n  },\n  {\n    \"name\": \"ukpsi\",\n    \"path\": \"https://opendefinition.org/licenses/ukpsi\",\n    \"title\": \"UK PSI Public Sector Information\"\n  }\n]\n"
  },
  {
    "path": "src/ode/assets/style.qss",
    "content": "/* Open Data Editor main Style Sheet.\n\nThe application uses a fusion style set when creating the QMainWindow object. This stylesheets\ncomplements the fusion style with colors and branding. Open Data Editor is design for light-mode\nso we are enforcing it by explicitly setting the main widgets and components background to white.\n*/\n\n/* Enforcing Light Mode of main elements. */\nContent, FrictionlessResourceMetadataWidget, SelectWidget, Toolbar,\nFieldsForm, SingleFieldForm, SchemaForm, QWidget#fields_form_container,\nQTableView, QTableView QHeaderView, QTableView QTableCornerButton,\nQTreeView, QDialog, QPushButton, QComboBox, QComboBox QAbstractItemView, QLineEdit,\nQLabel, QTabWidget, QTabBar, QScrollBar, QSpinBox, QListWidget, QTextEdit, QSrollArea,\nQPlainTextEdit, QWidget#central_widget, QGroupBox {\n    background: #FFF;\n    color: #404040;\n    font-size: 19px;  /* multi-os support */\n}\n\n* { gridline-color: lightgrey; } /* multi-os support */\n\n\n.QComboBox:disabled, QLineEdit:disabled {\n  background: #f0f0f0;\n}\n\n\nErrorsReportButton {\n  border: 0;\n}\nErrorsReportButton:hover {\n  background-color: #F0F0F0;\n}\nErrorsReportButton QLabel {\n  font-size: 16px;\n  font-weight: 600;\n  color: #4C5564;\n  background: transparent;\n}\nErrorsReportButton QLabel:disabled {\n  color: grey;\n}\nErrorsReportButton QLabel[error=\"true\"] {\n  border: 1px solid #FECBCA;\n  color: #D32F2F;\n  padding: 0px;\n}\n\nToolbar QPushButton {\n  font-size: 16px;\n  font-weight: 600;\n  border: 0px solid;\n  color: #4C5564;\n  background: #FFFFFF;\n  padding: 6px 8px;\n}\nToolbar QPushButton:hover {\n  background-color: #F0F0F0;\n}\nToolbar QPushButton:pressed {\n  background-color: #F0F0F0;\n}\n\nToolbar QPushButton[active=\"true\"]{\n  border: 2px solid #0078D7;\n  background-color: #F0F0F0;\n  outline: none;\n}\n\nToolbar QComboBox#excelSheetCombo,\nToolbar QLabel#excelSheetLabel {\n  font-size: 16px;\n  font-weight: 600;\n  color: #4C5564;\n}\n\nQPushButton#button_save, QPushButton#button_export, QPushButton#button_ai {\n  font-size: 14px;\n  font-weight: 500;\n  color: #FFF;\n  background-color: #000;\n  border-style: outset;\n  border-width: 1px;\n  border-radius: 4px;\n  border-color: #000;\n  padding: 6px 8px;\n}\nQPushButton#button_save:hover, QPushButton#button_export:hover, QPushButton#button_ai:hover {\n  color: #FFF;\n  background: #0288D1;\n  border-color: #0288d1;\n}\nQPushButton#button_save:pressed, QPushButton#button_export:pressed, QPushButton#button_ai:pressed {\n  color: #FFF;\n  background-color: #000;\n  border-color: #FFF;\n}\nQPushButton#button_save:disabled, QPushButton#button_export:disabled, QPushButton#button_ai:disabled {\n  background-color: #F0F0F0;\n  border-color: #F0F0F0;\n  color: #4C5564;\n}\n\n\nSidebar {\n  border-right: 1px solid #000;\n}\nSidebar QPushButton#button_upload {\n  font-size: 14px;\n  font-weight: 500;\n  text-align: center;\n  color: #0288D1;\n  border-style: outset;\n  border-width: 1px;\n  border-radius: 4px;\n  border-color: #0288d1;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  padding-left: 15px;\n  padding-right: 15px;\n  margin-top: 22px;\n  margin-bottom: 22px;\n}\nSidebar QPushButton#button_upload:hover {\n  color: #FFF;\n  background: #0288D1;\n}\nSidebar QPushButton#button_upload:pressed {\n  color: #0288D1;\n  background: #FFF;\n}\nSidebar QTreeView {\n    border: 1px solid #d0d0d0;\n}\nSidebar QTreeView::item:hover {\n  color: #FFF;\n  background: black;\n}\nSidebar QTreeView::item:selected {\n  color: #FFF;\n  background: gray;\n}\nSidebar QPushButton {\n  border: 0px;\n  padding: 3px;\n  text-align: left;\n}\nSidebar QPushButton:hover {\n  background: #D0D0D0;\n}\nSidebar QPushButton:pressed {\n  background: #FFFFFF;\n}\n\n\nWelcome QPushButton {\n  font-size: 14px;\n  font-weight: 500;\n  color: #FFFFFF;\n  background: #000000;\n  border-style: outset;\n  border-width: 1px;\n  border-radius: 4px;\n  padding: 10px 15px;\n}\nWelcome QPushButton:hover {\n  color: #FFFFFF;\n  background: #0288D1;\n  border-color: #0288D1;\n}\nWelcome QPushButton:pressed {\n  color: #FFFFFF;\n  background: #000000;\n  border-color: #000000;\n}\n\nFieldsForm QScrollArea {\n  border: none;\n}\n\n\nQHeaderView::section {\n  background-color: rgb(240, 240, 240);\n  border: 1px solid rgb(200, 200, 200);\n}\nQTableCornerButton::section {\n  background-color: rgb(240, 240, 240);\n  border: 1px solid rgb(200, 200, 200);\n}\n\nDataUploadDialog,\nDeleteDialog,\nRenameDialog,\nDownloadDialog,\nLlamaDialog,\nLLMWarningDialog,\nColumnMetadataDialog,\nLlamaDownloadingDialog {\n  border: 2px solid #000000;\n}\n\nDataUploadDialog QPushButton,\nDeleteDialog QPushButton,\nRenameDialog QPushButton,\nDownloadDialog QPushButton,\nLLMWarningDialog QPushButton,\nColumnMetadataDialog QPushButton,\nLlamaDialog QPushButton,\nLlamaDownloadDialog QPushButton {\n  border: 2px solid #000000;\n  background-color: #F0F0F0;\n  font-size: 16px;\n  font-weight: 500;\n  color: #FFFFFF;\n  background: #000000;\n  border-style: outset;\n  border-width: 1px;\n  border-radius: 4px;\n  padding: 10px 15px;\n  text-align: center;\n}\n\nDeleteDialog QPushButton,\nRenameDialog QPushButton,\nDownloadDialog QPushButton,\nLLMWarningDialog QPushButton,\nColumnMetadataDialog QPushButton,\nLlamaDialog QPushButton,\nLlamaDownloadDialog QPushButton {\n  padding: 5px;\n}\n\nDataUploadDialog QPushButton:hover,\nDeleteDialog QPushButton:hover,\nRenameDialog QPushButton:hover,\nDownloadDialog QPushButton:hover,\nColumnMetadataDialog QPushButton:hover,\nLLMWarningDialog QPushButton:hover,\nLlamaDialog QPushButton:hover,\nLlamaDownloadDialog QPushButton:hover {\n  background: #0288D1;\n  border-color: #0288d1;\n}\n\nDataUploadDialog QPushButton:pressed,\nDeleteDialog QPushButton:pressed,\nRenameDialog QPushButton:pressed,\nDownloadDialog QPushButton:pressed,\nColumnMetadataDialog QPushButton:pressed,\nLLMWarningDialog QPushButton:pressed,\nLlamaDialog QPushButton:pressed,\nLlamaDownloadDialog QPushButton:pressed {\n  color: #FFFFFF;\n  background: #000000;\n  border-color: #000000;\n}\n\nDataUploadDialog QPushButton:disabled,\nDeleteDialog QPushButton:disabled,\nRenameDialog QPushButton:disabled,\nDownloadDialog QPushButton:disabled,\nColumnMetadataDialog QPushButton:disabled,\nLLMWarningDialog QPushButton:disabled,\nLlamaDialog QPushButton:disabled,\nLlamaDownloadDialog QPushButton:disabled {\n  background-color: #F0F0F0;\n  border-color: #F0F0F0;\n  color: #4C5564;\n}\n\nQTableView::item:selected {\n  background: white;\n  color: black;\n  border: 2px solid #646464;\n}\n\n\n/* Keyboard Focus Styles for Accessibility */\n/* General focus styles for primary UI components */\n\nQPushButton:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\nQLineEdit:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\nQComboBox:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\nQTreeView:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\nQTableView:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\n/* Focus styles for save/publish buttons */\nQPushButton#button_save:focus, QPushButton#button_export:focus {\n  border: 3px solid #0078D7;\n  outline: none;\n}\n\n/* Focus styles for sidebar elements */\nSidebar QPushButton#button_upload:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\nSidebar QPushButton:focus {\n  border: 2px solid #0078D7;\n  background: #D0D0D0;\n  outline: none;\n}\n\nSidebar QTreeView:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\n/* Focus styles for welcome screen buttons */\nWelcome QPushButton:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\n/* Focus styles for dialog buttons */\nDataUploadDialog QPushButton:focus,\nDeleteDialog QPushButton:focus,\nRenameDialog QPushButton:focus,\nDownloadDialog QPushButton:focus,\nColumnMetadataDialog QPushButton:focus,\nLLMWarningDialog QPushButton:focus,\nLlamaDialog QPushButton:focus,\nLlamaDownloadDialog QPushButton:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\n/* Focus styles for table items */\nQTableView::item:focus {\n  border: 2px solid #0078D7;\n  outline: none;\n}\n\nLLMWarningDialog QCheckBox {\n    color: #404040;\n    font-size: 18px; \n    background-color: white;\n}\n\nQPushButton#deleteButton {\n    background-color: black;\n    color: white;\n}\n\nQPushButton#deleteButton:hover {\n    background-color: #0078D7;\n}\n\nQPushButton#deleteButton:disabled {\n    color: gray;\n    background-color: #f0f0f0;\n}\n\n/* Download Button */\nQPushButton#downloadButton {\n    background-color: #black;\n    color: white;\n}\n\nQPushButton#downloadButton:hover {\n    background-color: #0078D7;\n}\n\nQPushButton#downloadButton:disabled {\n    color: gray;\n    background-color: #f0f0f0;\n}\n\nQWidget#llamaDownloadModelRow {\n    border: 1px solid #ccc;\n    border-radius: 4px;\n    background-color: #f9f9f9;\n    margin: 2px;\n}"
  },
  {
    "path": "src/ode/assets/translations/de.ts",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>\n<TS version=\"2.1\" language=\"de_DE\">\n<context>\n    <name>ColumnMetadataDialog</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"276\"/>\n        <source>Column name cannot be empty</source>\n        <translation>Spaltenname darf nicht leer sein</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"281\"/>\n        <source>There is another column in the table with the same name. Please choose a different one</source>\n        <translation>Es gibt bereits eine Spalte in der Tabelle mit demselben Namen. Bitte wählen Sie einen anderen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"306\"/>\n        <source>Save</source>\n        <translation>Speichern</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"307\"/>\n        <source>Cancel</source>\n        <translation>Abbrechen</translation>\n    </message>\n</context>\n<context>\n    <name>DataUploadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"163\"/>\n        <source>Please paste a valid URL.</source>\n        <translation>Bitte fügen Sie eine gültige URL ein.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"166\"/>\n        <source>Please paste a valid URL starting with http:// or https://.</source>\n        <translation>Bitte fügen Sie eine gültige URL ein, die mit http:// oder https:// beginnt.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"176\"/>\n        <source>Error: The Google Sheets URL is not valid or the table is not publicly available.</source>\n        <translation>Fehler: Die Google Sheets URL ist nicht gültig oder die Tabelle ist nicht öffentlich verfügbar.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"187\"/>\n        <source>Error: The URL is not associated with a table</source>\n        <translation>Fehler: Die URL ist nicht mit einer Tabelle verknüpft</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"216\"/>\n        <source>Upload your data</source>\n        <translation>Laden Sie Ihre Daten hoch</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"217\"/>\n        <source>Add one or more Excel or csv files</source>\n        <translation>Fügen Sie eine oder mehrere Excel- oder CSV-Dateien hinzu</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"218\"/>\n        <source>Add a folder</source>\n        <translation>Einen Ordner hinzufügen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"226\"/>\n        <source>Paste your Google Sheet or csv link to create a local copy in the Open Data Editor</source>\n        <translation>Fügen Sie Ihren Google Sheet oder CSV-Link ein, um eine lokale Kopie im Open Data Editor zu erstellen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"228\"/>\n        <source>Add Local Files</source>\n        <translation>Lokale Dateien hinzufügen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"219\"/>\n        <location filename=\"../../dialogs/upload.py\" line=\"220\"/>\n        <source>Select</source>\n        <translation>Auswählen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"221\"/>\n        <source>Link to the external table: </source>\n        <translation>Link zur externen Tabelle: </translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"222\"/>\n        <source>Enter or paste URL</source>\n        <translation>URL eingeben oder einfügen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"227\"/>\n        <source>Add</source>\n        <translation>Hinzufügen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"229\"/>\n        <source>Add External Data</source>\n        <translation>Externe Daten hinzufügen</translation>\n    </message>\n</context>\n<context>\n    <name>DataViewer</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"557\"/>\n        <source>Preview not available for this item.</source>\n        <translation>Vorschau für dieses Element nicht verfügbar.</translation>\n    </message>\n</context>\n<context>\n    <name>DataWorker</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"67\"/>\n        <source>Reading file...</source>\n        <translation>Datei wird gelesen...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"69\"/>\n        <source>Checking errors...</source>\n        <translation>Fehler werden überprüft...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"72\"/>\n        <source>Drawing table...</source>\n        <translation>Tabelle wird gezeichnet...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"82\"/>\n        <source>Read and error checking finished.</source>\n        <translation>Lesen und Fehlerüberprüfung abgeschlossen.</translation>\n    </message>\n</context>\n<context>\n    <name>DeleteDialog</name>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"43\"/>\n        <source>Cancel</source>\n        <translation>Abbrechen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"44\"/>\n        <source>Ok</source>\n        <translation>OK</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"45\"/>\n        <source>Are you sure you want to delete this item?</source>\n        <translation>Sind Sie sicher, dass Sie dieses Element löschen möchten?</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"46\"/>\n        <source>Delete file</source>\n        <translation>Datei löschen</translation>\n    </message>\n</context>\n<context>\n    <name>DownloadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"48\"/>\n        <source>Download</source>\n        <translation>Herunterladen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"49\"/>\n        <source>Please, select one of the following options:</source>\n        <translation>Bitte wählen Sie eine der folgenden Optionen:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"50\"/>\n        <source>Download file</source>\n        <translation>Datei herunterladen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"51\"/>\n        <source>Download file with errors</source>\n        <translation>Datei mit Fehlern herunterladen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"63\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Datei erfolgreich heruntergeladen nach:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"64\"/>\n        <source>Success</source>\n        <translation>Erfolg</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"67\"/>\n        <source>Error downloading file:\n{}</source>\n        <translation>Fehler beim Herunterladen der Datei:\n{}</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsMessages</name>\n    <message>\n        <location filename=\"../../utils.py\" line=\"121\"/>\n        <location filename=\"../../utils.py\" line=\"127\"/>\n        <source>Missing header</source>\n        <translation>Fehlende Kopfzeile</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"122\"/>\n        <source>Duplicated header</source>\n        <translation>Doppelte Kopfzeile</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"123\"/>\n        <source>Empty row</source>\n        <translation>Leere Zeile</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"124\"/>\n        <source>Type mismatch</source>\n        <translation>Typkonflikt</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"125\"/>\n        <source>Missing value</source>\n        <translation>Fehlender Wert</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"126\"/>\n        <source>Extra cell</source>\n        <translation>Zusätzliche Zelle</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"128\"/>\n        <source>Blank Label</source>\n        <translation>Leeres Label</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"137\"/>\n        <location filename=\"../../utils.py\" line=\"143\"/>\n        <source>A column in the header row has no name. Every column should have a unique, non-empty header.</source>\n        <translation>Eine Spalte in der Kopfzeile hat keinen Namen. Jede Spalte sollte einen eindeutigen, nicht-leeren Kopfzeilennamen haben.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"138\"/>\n        <source>Two or more columns share the same name. Column names must be unique.</source>\n        <translation>Zwei oder mehr Spalten teilen sich denselben Namen. Spaltennamen müssen eindeutig sein.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"139\"/>\n        <source>This row has no data. Rows should contain at least one cell with data.</source>\n        <translation>Diese Zeile enthält keine Daten. Zeilen sollten mindestens eine Zelle mit Daten enthalten.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"140\"/>\n        <source>A cell value doesn&apos;t match the expected data type or format for the column.</source>\n        <translation>Ein Zellenwert entspricht nicht dem erwarteten Datentyp oder Format für die Spalte.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"141\"/>\n        <source>This cell is missing data</source>\n        <translation>Diese Zelle enthält keine Daten</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"142\"/>\n        <source>This row has more values compared to the header row.</source>\n        <translation>Diese Zeile hat mehr Werte im Vergleich zur Kopfzeile.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"144\"/>\n        <source>A label in the header row is missing a value. Label should be provided and not be blank.</source>\n        <translation>Ein Label in der Kopfzeile hat keinen Wert. Das Label sollte angegeben werden und nicht leer sein.</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsWidget</name>\n    <message>\n        <location filename=\"../../panels/errors.py\" line=\"220\"/>\n        <source>Please note that the ODE currently detects errors in tables, with a maximum of </source>\n        <translation>Bitte beachten Sie, dass der ODE derzeit Fehler in Tabellen erkennt, mit einem Maximum von </translation>\n    </message>\n</context>\n<context>\n    <name>FrictionlessTableModel</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"249\"/>\n        <source>Data</source>\n        <translation>Daten</translation>\n    </message>\n</context>\n<context>\n    <name>LLMWarningDialog</name>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"13\"/>\n        <source>AI assistant</source>\n        <translation>KI-Assistent</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"25\"/>\n        <source>Welcome to the ODE&apos;s AI assistant! This feature will help you generating better descriptions for the columns of your table and also questions for data analysis. \n\nTo get started, you will need to install the AI file in your computer. Once installed, everything will run locally, meaning your data always stays private and secure. Learn more</source>\n        <translation>Willkommen beim KI-Assistenten des ODE! Diese Funktion hilft Ihnen dabei, bessere Beschreibungen für die Spalten Ihrer Tabelle zu generieren und auch Fragen zur Datenanalyse.\n\nUm zu beginnen, müssen Sie die KI-Datei auf Ihrem Computer installieren. Nach der Installation läuft alles lokal, was bedeutet, dass Ihre Daten immer privat und sicher bleiben. Mehr erfahren</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"28\"/>\n        <source>Don&apos;t show again</source>\n        <translation>Nicht mehr anzeigen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"33\"/>\n        <source>Cancel</source>\n        <translation>Abbrechen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"40\"/>\n        <source>Ok</source>\n        <translation>OK</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"282\"/>\n        <source>Generating response...</source>\n        <translation>Antwort wird generiert...</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"288\"/>\n        <location filename=\"../../llama.py\" line=\"298\"/>\n        <location filename=\"../../llama.py\" line=\"314\"/>\n        <source>Execute</source>\n        <translation>Ausführen</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"299\"/>\n        <source>Error</source>\n        <translation>Fehler</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"313\"/>\n        <source>AI assistant</source>\n        <translation>KI-Assistent</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"315\"/>\n        <source>Stop execution</source>\n        <translation>Ausführung stoppen</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"316\"/>\n        <source>Results will be displayed here...</source>\n        <translation>Ergebnisse werden hier angezeigt...</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDownloadDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"348\"/>\n        <source>AI assistant</source>\n        <translation>KI-Assistent</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"352\"/>\n        <source>To start using the AI assistant, please download the following model.</source>\n        <translation>Um den KI-Assistenten zu verwenden, laden Sie bitte das folgende Modell herunter.</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"359\"/>\n        <source>The ODE will save the file in this location: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</source>\n        <translation>Der ODE wird die Datei an diesem Speicherort speichern: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"373\"/>\n        <source>Next</source>\n        <translation>Weiter</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"398\"/>\n        <source>Delete</source>\n        <translation>Löschen</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"404\"/>\n        <source>Download</source>\n        <translation>Herunterladen</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"457\"/>\n        <source>File exists</source>\n        <translation>Datei existiert</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"458\"/>\n        <source>Do you want to delete it and download it again?</source>\n        <translation>Möchten Sie sie löschen und erneut herunterladen?</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Downloading model</source>\n        <translation>Modell wird heruntergeladen</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Cancel</source>\n        <translation>Abbrechen</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"475\"/>\n        <source>LLM Model Download Progress</source>\n        <translation>LLM-Modell Download-Fortschritt</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"486\"/>\n        <location filename=\"../../llama.py\" line=\"553\"/>\n        <source>Error Occurred</source>\n        <translation>Fehler ist aufgetreten</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"494\"/>\n        <source>Confirm Deletion</source>\n        <translation>Löschung bestätigen</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"495\"/>\n        <source>Are you sure you want to delete {AI_MODEL.name}?</source>\n        <translation>Sind Sie sicher, dass Sie {AI_MODEL.name} löschen möchten?</translation>\n    </message>\n</context>\n<context>\n    <name>LoadingDialog</name>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"30\"/>\n        <source>Loading</source>\n        <translation>Laden</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"31\"/>\n        <source>Loading...</source>\n        <translation>Laden...</translation>\n    </message>\n</context>\n<context>\n    <name>MainWindow</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error</source>\n        <translation>Fehler</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"864\"/>\n        <source>File</source>\n        <translation>Datei</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"684\"/>\n        <source>Ready.</source>\n        <translation>Bereit.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error initializing the LLM:\n</source>\n        <translation>Fehler beim Initialisieren des LLM:\n</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"865\"/>\n        <source>Add</source>\n        <translation>Hinzufügen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"866\"/>\n        <source>File/Folder</source>\n        <translation>Datei/Ordner</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"867\"/>\n        <source>External URL</source>\n        <translation>Externe URL</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"870\"/>\n        <source>View</source>\n        <translation>Ansicht</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"871\"/>\n        <source>Errors panel</source>\n        <translation>Fehlerpanel</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"872\"/>\n        <source>Source panel</source>\n        <translation>Quellpanel</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"875\"/>\n        <source>Help</source>\n        <translation>Hilfe</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"876\"/>\n        <source>User Guide</source>\n        <translation>Benutzerhandbuch</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"877\"/>\n        <source>Report an Issue</source>\n        <translation>Ein Problem melden</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"878\"/>\n        <source>View logs</source>\n        <translation>Logs anzeigen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"879\"/>\n        <source>About</source>\n        <translation>Über</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"915\"/>\n        <source>Language changed.</source>\n        <translation>Sprache geändert.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"942\"/>\n        <source>File and Metadata changes saved.</source>\n        <translation>Datei- und Metadatenänderungen gespeichert.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1130\"/>\n        <source>Last 100 Lines</source>\n        <translation>Letzte 100 Zeilen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1150\"/>\n        <source>Close</source>\n        <translation>Schließen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1155\"/>\n        <source>Copy to Clipboard</source>\n        <translation>In Zwischenablage kopieren</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1173\"/>\n        <source>Downloading data with errors...</source>\n        <translation>Daten mit Fehlern werden heruntergeladen...</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1191\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Datei erfolgreich heruntergeladen nach:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1192\"/>\n        <source>Success</source>\n        <translation>Erfolg</translation>\n    </message>\n</context>\n<context>\n    <name>MetadataForm</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"189\"/>\n        <source>Column Name:</source>\n        <translation>Spaltenname:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"190\"/>\n        <source>Data Type:</source>\n        <translation>Datentyp:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"191\"/>\n        <source>Description:</source>\n        <translation>Beschreibung:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"192\"/>\n        <source>Flag empty cells as errors?:</source>\n        <translation>Leere Zellen als Fehler markieren?:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"193\"/>\n        <source>Min. Characters in cells:</source>\n        <translation>Min. Zeichen in Zellen:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"194\"/>\n        <source>Max. Characters in cell</source>\n        <translation>Max. Zeichen in Zelle</translation>\n    </message>\n</context>\n<context>\n    <name>RenameDialog</name>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"50\"/>\n        <source>Rename file</source>\n        <translation>Datei umbenennen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"51\"/>\n        <source>Rename item to:</source>\n        <translation>Element umbenennen zu:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"52\"/>\n        <source>Cancel</source>\n        <translation>Abbrechen</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"53\"/>\n        <source>OK</source>\n        <translation>OK</translation>\n    </message>\n</context>\n<context>\n    <name>Sidebar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"257\"/>\n        <source>Upload your data</source>\n        <translation>Laden Sie Ihre Daten hoch</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"258\"/>\n        <source>User guide</source>\n        <translation>Benutzerhandbuch</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"259\"/>\n        <source>Report an issue</source>\n        <translation>Ein Problem melden</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"260\"/>\n        <source>Rename</source>\n        <translation>Umbenennen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"261\"/>\n        <source>Open File in Location</source>\n        <translation>Datei im Speicherort öffnen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"262\"/>\n        <source>Delete</source>\n        <translation>Löschen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <source>Operation not permitted.</source>\n        <translation>Vorgang nicht zulässig.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <source>File with this name already exists.</source>\n        <translation>Datei mit diesem Namen existiert bereits.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"314\"/>\n        <source>Item renamed successfuly.</source>\n        <translation>Element erfolgreich umbenannt.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"344\"/>\n        <source>Item deleted successfuly.</source>\n        <translation>Element erfolgreich gelöscht.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"302\"/>\n        <location filename=\"../../main.py\" line=\"306\"/>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <location filename=\"../../main.py\" line=\"340\"/>\n        <source>Error</source>\n        <translation>Fehler</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"303\"/>\n        <source>Source is a file but destination a directory.</source>\n        <translation>Die Quelle ist eine Datei, das Ziel jedoch ein Verzeichnis.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"307\"/>\n        <source>Source is a directory but destination a file.</source>\n        <translation>Die Quelle ist ein Verzeichnis, das Ziel jedoch eine Datei.</translation>\n    </message>\n</context>\n<context>\n    <name>SourceViewer</name>\n    <message>\n        <location filename=\"../../panels/source.py\" line=\"82\"/>\n        <source>This view is only available for CSV files.</source>\n        <translation>Diese Ansicht ist nur für CSV-Dateien verfügbar.</translation>\n    </message>\n</context>\n<context>\n    <name>Toolbar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"502\"/>\n        <source>Data</source>\n        <translation>Daten</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"503\"/>\n        <source>Errors Report</source>\n        <translation>Fehlerbericht</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"504\"/>\n        <source>Source code</source>\n        <translation>Quellcode</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"505\"/>\n        <source>Export</source>\n        <translation>Exportieren</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"506\"/>\n        <source>Save changes</source>\n        <translation>Änderungen speichern</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"507\"/>\n        <source>AI</source>\n        <translation>KI</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"508\"/>\n        <source>Sheet:</source>\n        <translation>Blatt:</translation>\n    </message>\n</context>\n<context>\n    <name>Welcome</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"573\"/>\n        <source>The ODE supports Excel &amp; csv files</source>\n        <translation>Der ODE unterstützt Excel- &amp; CSV-Dateien</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"574\"/>\n        <source>You can also add links to online tables</source>\n        <translation>Sie können auch Links zu Online-Tabellen hinzufügen</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"575\"/>\n        <source>Upload your data</source>\n        <translation>Laden Sie Ihre Daten hoch</translation>\n    </message>\n</context>\n</TS>\n"
  },
  {
    "path": "src/ode/assets/translations/es.ts",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>\n<TS version=\"2.1\" language=\"es_ES\">\n<context>\n    <name>ColumnMetadataDialog</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"276\"/>\n        <source>Column name cannot be empty</source>\n        <translation>El nombre de la columna no puede estar vacío</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"281\"/>\n        <source>There is another column in the table with the same name. Please choose a different one</source>\n        <translation>Hay otra columna en la tabla con el mismo nombre. Por favor elige otro</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"306\"/>\n        <source>Save</source>\n        <translation>Guardar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"307\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n</context>\n<context>\n    <name>DataUploadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"163\"/>\n        <source>Please paste a valid URL.</source>\n        <translation>Por favor ingresa una URL válida.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"166\"/>\n        <source>Please paste a valid URL starting with http:// or https://.</source>\n        <translation>Por favor ingresa una URL válida que empiece con http:// o https://.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"176\"/>\n        <source>Error: The Google Sheets URL is not valid or the table is not publicly available.</source>\n        <translation>Error: La URL de la Planilla de Google no es válida o su contenido no es público.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"187\"/>\n        <source>Error: The URL is not associated with a table</source>\n        <translation>Error: La URL no está asociada a una tabla</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"216\"/>\n        <source>Upload your data</source>\n        <translation>Carga tus archivos</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"217\"/>\n        <source>Add one or more Excel or csv files</source>\n        <translation>Agrega uno o más archivos Excel of CSV</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"218\"/>\n        <source>Add a folder</source>\n        <translation>Agrega una carpeta</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"219\"/>\n        <location filename=\"../../dialogs/upload.py\" line=\"220\"/>\n        <source>Select</source>\n        <translation>Seleccionar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"221\"/>\n        <source>Link to the external table: </source>\n        <translation>Enlace a la tabla externa: </translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"222\"/>\n        <source>Enter or paste URL</source>\n        <translation>Introduce o pega la URL</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"226\"/>\n        <source>Paste your Google Sheet or csv link to create a local copy in the Open Data Editor</source>\n        <translation>Pega el enlace de tu hoja de cálculo de Google o CSV para crear una copia local en el Editor de Datos Abiertos</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"227\"/>\n        <source>Add</source>\n        <translation>Agregar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"228\"/>\n        <source>Add Local Files</source>\n        <translation>Agregar archivos locales</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"229\"/>\n        <source>Add External Data</source>\n        <translation>Agregar Datos Externos</translation>\n    </message>\n</context>\n<context>\n    <name>DataViewer</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"557\"/>\n        <source>Preview not available for this item.</source>\n        <translation>Vista previa no disponible para este ítem.</translation>\n    </message>\n</context>\n<context>\n    <name>DataWorker</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"67\"/>\n        <source>Reading file...</source>\n        <translation>Leyendo el archivo...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"69\"/>\n        <source>Checking errors...</source>\n        <translation>Comprobando errores...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"82\"/>\n        <source>Read and error checking finished.</source>\n        <translation>Lectura y comprobación de errores finalizada.</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"72\"/>\n        <source>Drawing table...</source>\n        <translation>Renderizando la tabla...</translation>\n    </message>\n</context>\n<context>\n    <name>DeleteDialog</name>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"43\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"44\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"45\"/>\n        <source>Are you sure you want to delete this item?</source>\n        <translation>¿Está seguro de que desea eliminar este elemento?</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"46\"/>\n        <source>Delete file</source>\n        <translation>Eliminar archivo</translation>\n    </message>\n</context>\n<context>\n    <name>DownloadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"48\"/>\n        <source>Download</source>\n        <translation>Descargar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"49\"/>\n        <source>Please, select one of the following options:</source>\n        <translation>Por favor, selecciona una de las siguientes opciones:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"50\"/>\n        <source>Download file</source>\n        <translation>Descargar archivo</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"51\"/>\n        <source>Download file with errors</source>\n        <translation>Descargar archivo con errores</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"63\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Archivo descargado exitosamente en:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"67\"/>\n        <source>Error downloading file:\n{}</source>\n        <translation>Error descargando el archivo:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"64\"/>\n        <source>Success</source>\n        <translation>Éxito</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsMessages</name>\n    <message>\n        <location filename=\"../../utils.py\" line=\"121\"/>\n        <location filename=\"../../utils.py\" line=\"127\"/>\n        <source>Missing header</source>\n        <translation>Falta el encabezado</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"122\"/>\n        <source>Duplicated header</source>\n        <translation>Encabezado duplicado</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"123\"/>\n        <source>Empty row</source>\n        <translation>Fila vacía</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"124\"/>\n        <source>Type mismatch</source>\n        <translation>Error de tipo</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"125\"/>\n        <source>Missing value</source>\n        <translation>Valor faltante</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"126\"/>\n        <source>Extra cell</source>\n        <translation>Celda adicional</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"128\"/>\n        <source>Blank Label</source>\n        <translation>Etiqueta en blanco</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"137\"/>\n        <location filename=\"../../utils.py\" line=\"143\"/>\n        <source>A column in the header row has no name. Every column should have a unique, non-empty header.</source>\n        <translation>Una columna en la fila de encabezado no tiene nombre. Cada columna debe tener un encabezado único y no vacío.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"138\"/>\n        <source>Two or more columns share the same name. Column names must be unique.</source>\n        <translation>Dos o más columnas comparten el mismo nombre. Los nombres de columna deben ser únicos.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"139\"/>\n        <source>This row has no data. Rows should contain at least one cell with data.</source>\n        <translation>Esta fila no tiene datos. Las filas deben contener al menos una celda con datos.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"140\"/>\n        <source>A cell value doesn&apos;t match the expected data type or format for the column.</source>\n        <translation>Un valor de celda no coincide con el tipo de dato o formato esperado para la columna.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"141\"/>\n        <source>This cell is missing data</source>\n        <translation>Esta celda carece de datos</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"142\"/>\n        <source>This row has more values compared to the header row.</source>\n        <translation>Esta fila tiene más valores en comparación con la fila de encabezado.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"144\"/>\n        <source>A label in the header row is missing a value. Label should be provided and not be blank.</source>\n        <translation>Falta un valor en una etiqueta de la fila de encabezado. La etiqueta debe proporcionarse y no estar en blanco.</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsWidget</name>\n    <message>\n        <location filename=\"../../panels/errors.py\" line=\"220\"/>\n        <source>Please note that the ODE currently detects errors in tables, with a maximum of </source>\n        <translation>Por favor, ten en cuenta que la ODE detecta actualmente errores en las tablas, con un máximo de </translation>\n    </message>\n</context>\n<context>\n    <name>FrictionlessTableModel</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"249\"/>\n        <source>Data</source>\n        <translation>Datos</translation>\n    </message>\n</context>\n<context>\n    <name>LLMWarningDialog</name>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"13\"/>\n        <source>AI assistant</source>\n        <translation>Asistente de IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"25\"/>\n        <source>Welcome to the ODE&apos;s AI assistant! This feature will help you generating better descriptions for the columns of your table and also questions for data analysis. \n\nTo get started, you will need to install the AI file in your computer. Once installed, everything will run locally, meaning your data always stays private and secure. Learn more</source>\n        <translation>¡Bienvenido al asistente de IA de ODE! Esta función te ayudará a generar mejores descripciones para las columnas de tu tabla y también preguntas para el análisis de datos.\n\nPara comenzar, necesitarás instalar el archivo de IA en tu computadora. Una vez instalado, todo se ejecutará localmente, lo que significa que tus datos siempre permanecerán privados y seguros. Más información</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"28\"/>\n        <source>Don&apos;t show again</source>\n        <translation>No mostrar de nuevo</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"33\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"40\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"288\"/>\n        <location filename=\"../../llama.py\" line=\"298\"/>\n        <location filename=\"../../llama.py\" line=\"314\"/>\n        <source>Execute</source>\n        <translation>Ejecutar</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"282\"/>\n        <source>Generating response...</source>\n        <translation>Generando respuesta...</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"299\"/>\n        <source>Error</source>\n        <translation>Error</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"313\"/>\n        <source>AI assistant</source>\n        <translation>Asistente de IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"315\"/>\n        <source>Stop execution</source>\n        <translation>Detener ejecución</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"316\"/>\n        <source>Results will be displayed here...</source>\n        <translation>Los resultados se mostrarán aquí...</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDownloadDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"398\"/>\n        <source>Delete</source>\n        <translation>Eliminar</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"404\"/>\n        <source>Download</source>\n        <translation>Descargar</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"348\"/>\n        <source>AI assistant</source>\n        <translation>Asistente de IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"352\"/>\n        <source>To start using the AI assistant, please download the following model.</source>\n        <translation>Para comenzar a usar el asistente de IA, por favor descarga el siguiente modelo.</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"359\"/>\n        <source>The ODE will save the file in this location: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</source>\n        <translation>El ODE guardará el archivo en esta carpeta: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"373\"/>\n        <source>Next</source>\n        <translation>Siguiente</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"457\"/>\n        <source>File exists</source>\n        <translation>El archivo ya existe</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"458\"/>\n        <source>Do you want to delete it and download it again?</source>\n        <translation>¿Quieres eliminarlo y volver a descargarlo?</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Downloading model</source>\n        <translation>Descargando modelo</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"475\"/>\n        <source>LLM Model Download Progress</source>\n        <translation>Progreso de descarga del modelo LLM</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"486\"/>\n        <location filename=\"../../llama.py\" line=\"553\"/>\n        <source>Error Occurred</source>\n        <translation>Ocurrió un error</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"494\"/>\n        <source>Confirm Deletion</source>\n        <translation>Confirmar eliminación</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"495\"/>\n        <source>Are you sure you want to delete {AI_MODEL.name}?</source>\n        <translation>¿Estás seguro de que quieres eliminar {AI_MODEL.name}?</translation>\n    </message>\n</context>\n<context>\n    <name>LoadingDialog</name>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"30\"/>\n        <source>Loading</source>\n        <translation>Cargando</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"31\"/>\n        <source>Loading...</source>\n        <translation>Cargando...</translation>\n    </message>\n</context>\n<context>\n    <name>MainWindow</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"684\"/>\n        <source>Ready.</source>\n        <translation>Listo.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error</source>\n        <translation>Error</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error initializing the LLM:\n</source>\n        <translation>Error al inicializar el LLM:\n</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"864\"/>\n        <source>File</source>\n        <translation>Archivo</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"865\"/>\n        <source>Add</source>\n        <translation>Agregar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"866\"/>\n        <source>File/Folder</source>\n        <translation>Archivo/Carpeta</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"867\"/>\n        <source>External URL</source>\n        <translation>URL Externa</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"870\"/>\n        <source>View</source>\n        <translation>Ver</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1173\"/>\n        <source>Downloading data with errors...</source>\n        <translation>Descargando datos con errores...</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1191\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Archivo descargado exitosamente en:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1192\"/>\n        <source>Success</source>\n        <translation>Éxito</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"871\"/>\n        <source>Errors panel</source>\n        <translation>Panel de Errores</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"872\"/>\n        <source>Source panel</source>\n        <translation>Panel de Fuente</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"875\"/>\n        <source>Help</source>\n        <translation>Ayuda</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"876\"/>\n        <source>User Guide</source>\n        <translation>Guía de Usuario</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"877\"/>\n        <source>Report an Issue</source>\n        <translation>Reportar un problema</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"879\"/>\n        <source>About</source>\n        <translation>Acerca de</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"878\"/>\n        <source>View logs</source>\n        <translation>Ver logs</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"915\"/>\n        <source>Language changed.</source>\n        <translation>Lenguaje cambiado.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"942\"/>\n        <source>File and Metadata changes saved.</source>\n        <translation>Archivo y Metadatos guardados.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1130\"/>\n        <source>Last 100 Lines</source>\n        <translation>Últimas 100 líneas</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1150\"/>\n        <source>Close</source>\n        <translation>Cerrar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1155\"/>\n        <source>Copy to Clipboard</source>\n        <translation>Copiar al portapapeles</translation>\n    </message>\n</context>\n<context>\n    <name>MetadataForm</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"189\"/>\n        <source>Column Name:</source>\n        <translation>Nombre de la columna:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"190\"/>\n        <source>Data Type:</source>\n        <translation>Tipo de dato:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"191\"/>\n        <source>Description:</source>\n        <translation>Descripción:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"192\"/>\n        <source>Flag empty cells as errors?:</source>\n        <translation>¿Marcar celdas vacías como errores?:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"193\"/>\n        <source>Min. Characters in cells:</source>\n        <translation>Caracteres mínimos en las celdas:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"194\"/>\n        <source>Max. Characters in cell</source>\n        <translation>Caracteres máximos en la celda</translation>\n    </message>\n</context>\n<context>\n    <name>RenameDialog</name>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"50\"/>\n        <source>Rename file</source>\n        <translation>Renombrar archivo</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"51\"/>\n        <source>Rename item to:</source>\n        <translation>Renombrar archivo a:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"52\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"53\"/>\n        <source>OK</source>\n        <translation>OK</translation>\n    </message>\n</context>\n<context>\n    <name>Sidebar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"257\"/>\n        <source>Upload your data</source>\n        <translation>Carga tus archivos</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"258\"/>\n        <source>User guide</source>\n        <translation>Guía de Usuario</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"259\"/>\n        <source>Report an issue</source>\n        <translation>Reportar un problema</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"260\"/>\n        <source>Rename</source>\n        <translation>Renombrar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"261\"/>\n        <source>Open File in Location</source>\n        <translation>Abrir la ubicacion de archivo</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"262\"/>\n        <source>Delete</source>\n        <translation>Eliminar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"302\"/>\n        <location filename=\"../../main.py\" line=\"306\"/>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <location filename=\"../../main.py\" line=\"340\"/>\n        <source>Error</source>\n        <translation>Error</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"303\"/>\n        <source>Source is a file but destination a directory.</source>\n        <translation>El origen es un archivo pero el destino es un directorio.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"307\"/>\n        <source>Source is a directory but destination a file.</source>\n        <translation>El origen es un directorio pero el destino es un archivo.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <source>Operation not permitted.</source>\n        <translation>Operación no permitida.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <source>File with this name already exists.</source>\n        <translation>Ya existe un archivo con este nombre.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"314\"/>\n        <source>Item renamed successfuly.</source>\n        <translation>Item renombrado exitosamente.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"344\"/>\n        <source>Item deleted successfuly.</source>\n        <translation>Item eliminado exitosamente.</translation>\n    </message>\n</context>\n<context>\n    <name>SourceViewer</name>\n    <message>\n        <location filename=\"../../panels/source.py\" line=\"82\"/>\n        <source>This view is only available for CSV files.</source>\n        <translation>Esta vista sólo está disponible para archivos CSV.</translation>\n    </message>\n</context>\n<context>\n    <name>Toolbar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"508\"/>\n        <source>Sheet:</source>\n        <translation>Hoja:</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"502\"/>\n        <source>Data</source>\n        <translation>Datos</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"503\"/>\n        <source>Errors Report</source>\n        <translation>Reporte de Errores</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"504\"/>\n        <source>Source code</source>\n        <translation>Codigo fuente</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"505\"/>\n        <source>Export</source>\n        <translation>Exportar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"506\"/>\n        <source>Save changes</source>\n        <translation>Guardar cambios</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"507\"/>\n        <source>AI</source>\n        <translation>IA</translation>\n    </message>\n</context>\n<context>\n    <name>Welcome</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"573\"/>\n        <source>The ODE supports Excel &amp; csv files</source>\n        <translation>El ODE admite archivos Excel y CSV</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"574\"/>\n        <source>You can also add links to online tables</source>\n        <translation>También puedes agregar enlaces a tablas en línea</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"575\"/>\n        <source>Upload your data</source>\n        <translation>Carga tus archivos</translation>\n    </message>\n</context>\n</TS>\n"
  },
  {
    "path": "src/ode/assets/translations/fr.ts",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>\n<TS version=\"2.1\" language=\"fr_FR\">\n<context>\n    <name>ColumnMetadataDialog</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"276\"/>\n        <source>Column name cannot be empty</source>\n        <translation>Le nom de la colonne ne peut pas être vide</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"281\"/>\n        <source>There is another column in the table with the same name. Please choose a different one</source>\n        <translation>Il y a une autre colonne dans le tableau avec le même nom. Veuillez en choisir un autre</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"306\"/>\n        <source>Save</source>\n        <translation>Enregistrer</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"307\"/>\n        <source>Cancel</source>\n        <translation>Annuler</translation>\n    </message>\n</context>\n<context>\n    <name>DataUploadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"163\"/>\n        <source>Please paste a valid URL.</source>\n        <translation>Veuillez coller une URL valide.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"166\"/>\n        <source>Please paste a valid URL starting with http:// or https://.</source>\n        <translation>Veuillez coller une URL valide commençant par http:// ou https://.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"176\"/>\n        <source>Error: The Google Sheets URL is not valid or the table is not publicly available.</source>\n        <translation>Erreur: L&apos;URL Google Sheets n&apos;est pas valide ou le tableau n&apos;est pas accessible publiquement.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"187\"/>\n        <source>Error: The URL is not associated with a table</source>\n        <translation>Erreur: L&apos;URL n&apos;est pas associée à un tableau</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"216\"/>\n        <source>Upload your data</source>\n        <translation>Télécharger vos données</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"217\"/>\n        <source>Add one or more Excel or csv files</source>\n        <translation>Ajouter un ou plusieurs fichiers Excel ou csv</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"218\"/>\n        <source>Add a folder</source>\n        <translation>Ajouter un dossier</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"219\"/>\n        <location filename=\"../../dialogs/upload.py\" line=\"220\"/>\n        <source>Select</source>\n        <translation>Sélectionner</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"221\"/>\n        <source>Link to the external table: </source>\n        <translation>Lien vers le tableau externe: </translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"222\"/>\n        <source>Enter or paste URL</source>\n        <translation>Entrez ou collez l&apos;URL</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"226\"/>\n        <source>Paste your Google Sheet or csv link to create a local copy in the Open Data Editor</source>\n        <translation>Collez le lien de votre feuille de calcul Google ou fichier CSV pour créer une copie locale dans l&apos;Éditeur de Données Ouvertes.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"227\"/>\n        <source>Add</source>\n        <translation>Ajouter</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"228\"/>\n        <source>Add Local Files</source>\n        <translation>Ajouter fichiers</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"229\"/>\n        <source>Add External Data</source>\n        <translation>Ajouter des données externes</translation>\n    </message>\n</context>\n<context>\n    <name>DataViewer</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"557\"/>\n        <source>Preview not available for this item.</source>\n        <translation>Aperçu non disponible pour cet article.</translation>\n    </message>\n</context>\n<context>\n    <name>DataWorker</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"67\"/>\n        <source>Reading file...</source>\n        <translation>Lecture du fichier...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"69\"/>\n        <source>Checking errors...</source>\n        <translation>Vérification des erreurs...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"82\"/>\n        <source>Read and error checking finished.</source>\n        <translation>Lecture et vérification des erreurs terminées.</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"72\"/>\n        <source>Drawing table...</source>\n        <translation>Rendu du tableau...</translation>\n    </message>\n</context>\n<context>\n    <name>DeleteDialog</name>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"43\"/>\n        <source>Cancel</source>\n        <translation>Annuler</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"44\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"45\"/>\n        <source>Are you sure you want to delete this item?</source>\n        <translation>Êtes-vous sûr de vouloir supprimer cet élément?</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"46\"/>\n        <source>Delete file</source>\n        <translation>Supprimer le fichier</translation>\n    </message>\n</context>\n<context>\n    <name>DownloadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"48\"/>\n        <source>Download</source>\n        <translation>Télécharger</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"49\"/>\n        <source>Please, select one of the following options:</source>\n        <translation>Veuillez sélectionner l&apos;une des options suivantes :</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"50\"/>\n        <source>Download file</source>\n        <translation>Télécharger le fichier</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"51\"/>\n        <source>Download file with errors</source>\n        <translation>Télécharger le fichier avec erreurs</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"63\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Fichier téléchargé avec succès dans :\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"67\"/>\n        <source>Error downloading file:\n{}</source>\n        <translation>Erreur lors du téléchargement du fichier :\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"64\"/>\n        <source>Success</source>\n        <translation>Succès</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsMessages</name>\n    <message>\n        <location filename=\"../../utils.py\" line=\"121\"/>\n        <location filename=\"../../utils.py\" line=\"127\"/>\n        <source>Missing header</source>\n        <translation>En-tête manquant</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"122\"/>\n        <source>Duplicated header</source>\n        <translation>En-tête dupliqué</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"123\"/>\n        <source>Empty row</source>\n        <translation>Ligne vide</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"124\"/>\n        <source>Type mismatch</source>\n        <translation>Type incompatible</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"125\"/>\n        <source>Missing value</source>\n        <translation>Valeur manquante</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"126\"/>\n        <source>Extra cell</source>\n        <translation>Cellule supplémentaire</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"128\"/>\n        <source>Blank Label</source>\n        <translation>Étiquette vide</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"137\"/>\n        <location filename=\"../../utils.py\" line=\"143\"/>\n        <source>A column in the header row has no name. Every column should have a unique, non-empty header.</source>\n        <translation>Une colonne dans la ligne d&apos;en-tête n&apos;a pas de nom. Chaque colonne doit avoir un en-tête unique et non vide.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"138\"/>\n        <source>Two or more columns share the same name. Column names must be unique.</source>\n        <translation>Deux colonnes ou plus partagent le même nom. Les noms de colonnes doivent être uniques.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"139\"/>\n        <source>This row has no data. Rows should contain at least one cell with data.</source>\n        <translation>Cette ligne ne contient aucune donnée. Les lignes doivent contenir au moins une cellule avec des données.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"140\"/>\n        <source>A cell value doesn&apos;t match the expected data type or format for the column.</source>\n        <translation>Une valeur de cellule ne correspond pas au type de données ou au format attendu pour la colonne.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"141\"/>\n        <source>This cell is missing data</source>\n        <translation>Cette cellule ne contient pas de données</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"142\"/>\n        <source>This row has more values compared to the header row.</source>\n        <translation>Cette ligne a plus de valeurs que la ligne d&apos;en-tête.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"144\"/>\n        <source>A label in the header row is missing a value. Label should be provided and not be blank.</source>\n        <translation>Une étiquette dans la ligne d&apos;en-tête n&apos;a pas de valeur. L&apos;étiquette doit être fournie et ne pas être vide.</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsWidget</name>\n    <message>\n        <location filename=\"../../panels/errors.py\" line=\"220\"/>\n        <source>Please note that the ODE currently detects errors in tables, with a maximum of </source>\n        <translation>Veuillez noter que l&apos;ODE détecte actuellement des erreurs dans les tableaux, avec un maximum de </translation>\n    </message>\n</context>\n<context>\n    <name>FrictionlessTableModel</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"249\"/>\n        <source>Data</source>\n        <translation>Données</translation>\n    </message>\n</context>\n<context>\n    <name>LLMWarningDialog</name>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"13\"/>\n        <source>AI assistant</source>\n        <translation>Assistant IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"25\"/>\n        <source>Welcome to the ODE&apos;s AI assistant! This feature will help you generating better descriptions for the columns of your table and also questions for data analysis. \n\nTo get started, you will need to install the AI file in your computer. Once installed, everything will run locally, meaning your data always stays private and secure. Learn more</source>\n        <translation>Bienvenue dans l&apos;assistant IA de l&apos;ODE ! Cette fonctionnalité vous aidera à générer de meilleures descriptions pour les colonnes de votre tableau ainsi que des questions pour l&apos;analyse des données.\n\nPour commencer, vous devrez installer le fichier IA sur votre ordinateur. Une fois installé, tout fonctionnera localement, ce qui signifie que vos données restent toujours privées et sécurisées. En savoir plus</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"28\"/>\n        <source>Don&apos;t show again</source>\n        <translation>Ne plus afficher</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"33\"/>\n        <source>Cancel</source>\n        <translation>Annuler</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"40\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"288\"/>\n        <location filename=\"../../llama.py\" line=\"298\"/>\n        <location filename=\"../../llama.py\" line=\"314\"/>\n        <source>Execute</source>\n        <translation>Exécuter</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"282\"/>\n        <source>Generating response...</source>\n        <translation>Génération de la réponse...</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"299\"/>\n        <source>Error</source>\n        <translation>Erreur</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"313\"/>\n        <source>AI assistant</source>\n        <translation>Assistant IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"315\"/>\n        <source>Stop execution</source>\n        <translation>Arrêter l'exécution</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"316\"/>\n        <source>Results will be displayed here...</source>\n        <translation>Les résultats seront affichés ici...</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDownloadDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"348\"/>\n        <source>AI assistant</source>\n        <translation>Assistant IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"352\"/>\n        <source>To start using the AI assistant, please download the following model.</source>\n        <translation>Pour commencer à utiliser l'assistant IA, veuillez télécharger le modèle suivant.</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"359\"/>\n        <source>The ODE will save the file in this location: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</source>\n        <translation>L&apos;ODE enregistrera le fichier à cet emplacement : &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"373\"/>\n        <source>Next</source>\n        <translation>Suivant</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"398\"/>\n        <source>Delete</source>\n        <translation>Supprimer</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"404\"/>\n        <source>Download</source>\n        <translation>Télécharger</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"457\"/>\n        <source>File exists</source>\n        <translation>Fichier existant</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"458\"/>\n        <source>Do you want to delete it and download it again?</source>\n        <translation>Voulez-vous le supprimer et le télécharger à nouveau ?</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Downloading model</source>\n        <translation>Téléchargement du modèle</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Cancel</source>\n        <translation>Annuler</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"475\"/>\n        <source>LLM Model Download Progress</source>\n        <translation>Progression du téléchargement du modèle LLM</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"486\"/>\n        <location filename=\"../../llama.py\" line=\"553\"/>\n        <source>Error Occurred</source>\n        <translation>Une erreur s&apos;est produite</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"494\"/>\n        <source>Confirm Deletion</source>\n        <translation>Confirmer la suppression</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"495\"/>\n        <source>Are you sure you want to delete {AI_MODEL.name}?</source>\n        <translation>Êtes-vous sûr de vouloir supprimer {AI_MODEL.name} ?</translation>\n    </message>\n</context>\n<context>\n    <name>LoadingDialog</name>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"30\"/>\n        <source>Loading</source>\n        <translation>Chargement...</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"31\"/>\n        <source>Loading...</source>\n        <translation>Chargement...</translation>\n    </message>\n</context>\n<context>\n    <name>MainWindow</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"684\"/>\n        <source>Ready.</source>\n        <translation>Prêt.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error</source>\n        <translation>Erreur</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error initializing the LLM:\n</source>\n        <translation>Erreur lors de l'initialisation du LLM :\n</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"864\"/>\n        <source>File</source>\n        <translation>Fichier</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"865\"/>\n        <source>Add</source>\n        <translation>Ajouter</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"866\"/>\n        <source>File/Folder</source>\n        <translation>Fichier/Dossier</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"867\"/>\n        <source>External URL</source>\n        <translation>URL externe</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"870\"/>\n        <source>View</source>\n        <translation>Affichage</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1173\"/>\n        <source>Downloading data with errors...</source>\n        <translation>Téléchargement des données avec erreurs...</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1191\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Fichier téléchargé avec succès dans :\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1192\"/>\n        <source>Success</source>\n        <translation>Succès</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"871\"/>\n        <source>Errors panel</source>\n        <translation>Panneau d&apos;erreurs</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"872\"/>\n        <source>Source panel</source>\n        <translation>Panneau source</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"875\"/>\n        <source>Help</source>\n        <translation>Aide</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"876\"/>\n        <source>User Guide</source>\n        <translation>Guide d&apos;utilisateur</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"877\"/>\n        <source>Report an Issue</source>\n        <translation>Signaler un problème</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"879\"/>\n        <source>About</source>\n        <translation>À propos</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"878\"/>\n        <source>View logs</source>\n        <translation>Voir les logs</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"915\"/>\n        <source>Language changed.</source>\n        <translation>Langue modifiée.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"942\"/>\n        <source>File and Metadata changes saved.</source>\n        <translation>Modifications du fichier et des métadonnées enregistrées.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1130\"/>\n        <source>Last 100 Lines</source>\n        <translation>100 dernières lignes</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1150\"/>\n        <source>Close</source>\n        <translation>Fermer</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1155\"/>\n        <source>Copy to Clipboard</source>\n        <translation>Copier dans le presse-papiers</translation>\n    </message>\n</context>\n<context>\n    <name>MetadataForm</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"189\"/>\n        <source>Column Name:</source>\n        <translation>Nom de la colonne:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"190\"/>\n        <source>Data Type:</source>\n        <translation>Type de données:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"191\"/>\n        <source>Description:</source>\n        <translation>Description:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"192\"/>\n        <source>Flag empty cells as errors?:</source>\n        <translation>Marquer les cellules vides comme des erreurs ?</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"193\"/>\n        <source>Min. Characters in cells:</source>\n        <translation>Caractères min. dans les cellules:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"194\"/>\n        <source>Max. Characters in cell</source>\n        <translation>Caractères max. dans la cellule:</translation>\n    </message>\n</context>\n<context>\n    <name>RenameDialog</name>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"50\"/>\n        <source>Rename file</source>\n        <translation>Renommer le fichier</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"51\"/>\n        <source>Rename item to:</source>\n        <translation>Renommer l&apos;élément en:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"52\"/>\n        <source>Cancel</source>\n        <translation>Annuler</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"53\"/>\n        <source>OK</source>\n        <translation>OK</translation>\n    </message>\n</context>\n<context>\n    <name>Sidebar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"257\"/>\n        <source>Upload your data</source>\n        <translation>Téléchargez vos données</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"258\"/>\n        <source>User guide</source>\n        <translation>Guide d&apos;utilisateur</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"259\"/>\n        <source>Report an issue</source>\n        <translation>Signaler un problème</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"260\"/>\n        <source>Rename</source>\n        <translation>Renommer</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"261\"/>\n        <source>Open File in Location</source>\n        <translation>Ouvrir le fichier dans l&apos;emplacement</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"262\"/>\n        <source>Delete</source>\n        <translation>Supprimer</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"302\"/>\n        <location filename=\"../../main.py\" line=\"306\"/>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <location filename=\"../../main.py\" line=\"340\"/>\n        <source>Error</source>\n        <translation>Erreur</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"303\"/>\n        <source>Source is a file but destination a directory.</source>\n        <translation>La source est un fichier mais la destination est un répertoire.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"307\"/>\n        <source>Source is a directory but destination a file.</source>\n        <translation>La source est un répertoire mais la destination est un fichier.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <source>Operation not permitted.</source>\n        <translation>Opération non autorisée.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <source>File with this name already exists.</source>\n        <translation>Un fichier portant ce nom existe déjà.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"314\"/>\n        <source>Item renamed successfuly.</source>\n        <translation>Élément renommé avec succès.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"344\"/>\n        <source>Item deleted successfuly.</source>\n        <translation>Élément supprimé avec succès.</translation>\n    </message>\n</context>\n<context>\n    <name>SourceViewer</name>\n    <message>\n        <location filename=\"../../panels/source.py\" line=\"82\"/>\n        <source>This view is only available for CSV files.</source>\n        <translation>Cette vue est uniquement disponible pour les fichiers CSV.</translation>\n    </message>\n</context>\n<context>\n    <name>Toolbar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"502\"/>\n        <source>Data</source>\n        <translation>Données</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"508\"/>\n        <source>Sheet:</source>\n        <translation>Feuille :</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"503\"/>\n        <source>Errors Report</source>\n        <translation>Rapport d&apos;erreurs</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"504\"/>\n        <source>Source code</source>\n        <translation>Code source</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"505\"/>\n        <source>Export</source>\n        <translation>Exporter</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"506\"/>\n        <source>Save changes</source>\n        <translation>Enregistrer les modifications</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"507\"/>\n        <source>AI</source>\n        <translation>IA</translation>\n    </message>\n</context>\n<context>\n    <name>Welcome</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"573\"/>\n        <source>The ODE supports Excel &amp; csv files</source>\n        <translation>L&apos;ODE prend en charge les fichiers Excel et csv</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"574\"/>\n        <source>You can also add links to online tables</source>\n        <translation>Vous pouvez également ajouter des liens vers des tableaux en ligne</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"575\"/>\n        <source>Upload your data</source>\n        <translation>Téléchargez vos données</translation>\n    </message>\n</context>\n</TS>\n"
  },
  {
    "path": "src/ode/assets/translations/it.ts",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>\n<TS version=\"2.1\" language=\"it_IT\">\n<context>\n    <name>ColumnMetadataDialog</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"276\"/>\n        <source>Column name cannot be empty</source>\n        <translation>Il nome della colonna non può essere vuoto</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"281\"/>\n        <source>There is another column in the table with the same name. Please choose a different one</source>\n        <translation>C&apos;è un altra colonna nella tabella con lo stesso nome. Scegli un nome differente</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"306\"/>\n        <source>Save</source>\n        <translation>Salva</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"307\"/>\n        <source>Cancel</source>\n        <translation>Annulla</translation>\n    </message>\n</context>\n<context>\n    <name>DataUploadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"163\"/>\n        <source>Please paste a valid URL.</source>\n        <translation>Incolla una URL valida.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"166\"/>\n        <source>Please paste a valid URL starting with http:// or https://.</source>\n        <translation>Incollare una URL valida che cominci con http:// o https://.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"176\"/>\n        <source>Error: The Google Sheets URL is not valid or the table is not publicly available.</source>\n        <translation>Errore: l&apos;URL del Google Fogli non è valida o la tabella non è stata resa pubblica.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"187\"/>\n        <source>Error: The URL is not associated with a table</source>\n        <translation>Errore: l&apos;URL non è associata ad una tabella</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"216\"/>\n        <source>Upload your data</source>\n        <translation>Carica i tuoi dati</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"217\"/>\n        <source>Add one or more Excel or csv files</source>\n        <translation>Aggiungi uno o più file Excel o csv</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"218\"/>\n        <source>Add a folder</source>\n        <translation>Aggiungi una cartella</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"219\"/>\n        <location filename=\"../../dialogs/upload.py\" line=\"220\"/>\n        <source>Select</source>\n        <translation>Seleziona</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"221\"/>\n        <source>Link to the external table: </source>\n        <translation>Link alla tabella esterna: </translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"222\"/>\n        <source>Enter or paste URL</source>\n        <translation>Inserisci o incolla URL</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"226\"/>\n        <source>Paste your Google Sheet or csv link to create a local copy in the Open Data Editor</source>\n        <translation>Incolla il link di Google Fogli o csv per creare una copia locale nell&apos;Open Data Editor</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"227\"/>\n        <source>Add</source>\n        <translation>Aggiungi</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"228\"/>\n        <source>Add Local Files</source>\n        <translation>Add file locali</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"229\"/>\n        <source>Add External Data</source>\n        <translation>Aggiungi dati esterni</translation>\n    </message>\n</context>\n<context>\n    <name>DataViewer</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"557\"/>\n        <source>Preview not available for this item.</source>\n        <translation>L&apos;anteprima non è disponibile per questa voce.</translation>\n    </message>\n</context>\n<context>\n    <name>DataWorker</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"67\"/>\n        <source>Reading file...</source>\n        <translation>Lettura del file...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"69\"/>\n        <source>Checking errors...</source>\n        <translation>Verifica errori...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"82\"/>\n        <source>Read and error checking finished.</source>\n        <translation>Lettura e verifica degli errori completata.</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"72\"/>\n        <source>Drawing table...</source>\n        <translation>Sto disegnando la tabella...</translation>\n    </message>\n</context>\n<context>\n    <name>DeleteDialog</name>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"43\"/>\n        <source>Cancel</source>\n        <translation>Annulla</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"44\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"45\"/>\n        <source>Are you sure you want to delete this item?</source>\n        <translation>Vuoi davvero cancellare questa voce?</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"46\"/>\n        <source>Delete file</source>\n        <translation>Cancella file</translation>\n    </message>\n</context>\n<context>\n    <name>DownloadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"48\"/>\n        <source>Download</source>\n        <translation>Download</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"49\"/>\n        <source>Please, select one of the following options:</source>\n        <translation>Seleziona una di queste opzioni:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"50\"/>\n        <source>Download file</source>\n        <translation>Download file</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"51\"/>\n        <source>Download file with errors</source>\n        <translation>Download file con errori</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"63\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>File scaricati con successo in:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"67\"/>\n        <source>Error downloading file:\n{}</source>\n        <translation>Errore scaricando file:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"64\"/>\n        <source>Success</source>\n        <translation>Successo</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsMessages</name>\n    <message>\n        <location filename=\"../../utils.py\" line=\"121\"/>\n        <location filename=\"../../utils.py\" line=\"127\"/>\n        <source>Missing header</source>\n        <translation>Intestazione mancante</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"122\"/>\n        <source>Duplicated header</source>\n        <translation>Intestazione duplicata</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"123\"/>\n        <source>Empty row</source>\n        <translation>Riga vuota</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"124\"/>\n        <source>Type mismatch</source>\n        <translation>Tipo non corrispondente</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"125\"/>\n        <source>Missing value</source>\n        <translation>Valore mancante</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"126\"/>\n        <source>Extra cell</source>\n        <translation>Cella extra</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"128\"/>\n        <source>Blank Label</source>\n        <translation>Etichetta vuota</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"137\"/>\n        <location filename=\"../../utils.py\" line=\"143\"/>\n        <source>A column in the header row has no name. Every column should have a unique, non-empty header.</source>\n        <translation>Una colonna nella riga di intestazione non ha nome. Ogni colonna deve avere un&apos;intestazione unica e non vuota.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"138\"/>\n        <source>Two or more columns share the same name. Column names must be unique.</source>\n        <translation>Due o più colonne condividono lo stesso nome. I nomi delle colonne devono essere univoci.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"139\"/>\n        <source>This row has no data. Rows should contain at least one cell with data.</source>\n        <translation>Questa riga non contiene dati. Le righe devono contenere almeno una cella con dati.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"140\"/>\n        <source>A cell value doesn&apos;t match the expected data type or format for the column.</source>\n        <translation>Il valore di una cella non corrisponde al tipo di dato o formato previsto per la colonna.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"141\"/>\n        <source>This cell is missing data</source>\n        <translation>Questa cella manca di dati</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"142\"/>\n        <source>This row has more values compared to the header row.</source>\n        <translation>Questa riga ha più valori rispetto alla riga di intestazione.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"144\"/>\n        <source>A label in the header row is missing a value. Label should be provided and not be blank.</source>\n        <translation>Un&apos;etichetta nella riga di intestazione manca di un valore. L&apos;etichetta deve essere fornita e non essere vuota.</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsWidget</name>\n    <message>\n        <location filename=\"../../panels/errors.py\" line=\"220\"/>\n        <source>Please note that the ODE currently detects errors in tables, with a maximum of </source>\n        <translation>Attenzione: ODE attualmente individua errori nelle tabelle con un massimo di </translation>\n    </message>\n</context>\n<context>\n    <name>FrictionlessTableModel</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"249\"/>\n        <source>Data</source>\n        <translation>Dati</translation>\n    </message>\n</context>\n<context>\n    <name>LLMWarningDialog</name>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"13\"/>\n        <source>AI assistant</source>\n        <translation>Assistente AI</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"25\"/>\n        <source>Welcome to the ODE&apos;s AI assistant! This feature will help you generating better descriptions for the columns of your table and also questions for data analysis. \n\nTo get started, you will need to install the AI file in your computer. Once installed, everything will run locally, meaning your data always stays private and secure. Learn more</source>\n        <translation>Benvenuto nell&apos;assistente AI di ODE! Questa funzione ti aiuterà a generare descrizioni migliori per le colonne della tua tabella e anche domande per l&apos;analisi dei dati.\n\nPer iniziare, dovrai installare il file AI sul tuo computer. Una volta installato, tutto funzionerà localmente, il che significa che i tuoi dati rimarranno sempre privati e sicuri. Scopri di più</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"28\"/>\n        <source>Don&apos;t show again</source>\n        <translation>Non mostrare più</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"33\"/>\n        <source>Cancel</source>\n        <translation>Annulla</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"40\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"282\"/>\n        <source>Generating response...</source>\n        <translation>Generazione della risposta...</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"288\"/>\n        <location filename=\"../../llama.py\" line=\"298\"/>\n        <location filename=\"../../llama.py\" line=\"314\"/>\n        <source>Execute</source>\n        <translation>Esegui</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"299\"/>\n        <source>Error</source>\n        <translation>Errore</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"313\"/>\n        <source>AI assistant</source>\n        <translation>Assistente AI</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"315\"/>\n        <source>Stop execution</source>\n        <translation>Ferma esecuzione</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"316\"/>\n        <source>Results will be displayed here...</source>\n        <translation>I risultati saranno mostrati qui...</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDownloadDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"398\"/>\n        <source>Delete</source>\n        <translation>Elimina</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"404\"/>\n        <source>Download</source>\n        <translation>Download</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"348\"/>\n        <source>AI assistant</source>\n        <translation>Assistente AI</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"352\"/>\n        <source>To start using the AI assistant, please download the following model.</source>\n        <translation>Per iniziare ad utilizzare l&apos;assistente AI, scarica il seguente modello.</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"359\"/>\n        <source>The ODE will save the file in this location: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</source>\n        <translation>ODE salverà il file in questa posizione: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"373\"/>\n        <source>Next</source>\n        <translation>Avanti</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"457\"/>\n        <source>File exists</source>\n        <translation>Il file esiste</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"458\"/>\n        <source>Do you want to delete it and download it again?</source>\n        <translation>Vuoi eliminarlo e scaricarlo nuovamente?</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Downloading model</source>\n        <translation>Donwload modello</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Cancel</source>\n        <translation>Annulla</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"475\"/>\n        <source>LLM Model Download Progress</source>\n        <translation>Avanzamento del download del modello LLM</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"486\"/>\n        <location filename=\"../../llama.py\" line=\"553\"/>\n        <source>Error Occurred</source>\n        <translation>Si è verificato un errore</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"494\"/>\n        <source>Confirm Deletion</source>\n        <translation>Conferma eliminazione</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"495\"/>\n        <source>Are you sure you want to delete {AI_MODEL.name}?</source>\n        <translation>Sei sicuro di voler eliminare {AI_MODEL.name}?</translation>\n    </message>\n</context>\n<context>\n    <name>LoadingDialog</name>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"30\"/>\n        <source>Loading</source>\n        <translation>Caricamento</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"31\"/>\n        <source>Loading...</source>\n        <translation>Caricamento...</translation>\n    </message>\n</context>\n<context>\n    <name>MainWindow</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"684\"/>\n        <source>Ready.</source>\n        <translation>Pronto.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error</source>\n        <translation>Errore</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error initializing the LLM:\n</source>\n        <translation>Errore nell&apos;inizializzazione del LLM:\n</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"864\"/>\n        <source>File</source>\n        <translation>File</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"865\"/>\n        <source>Add</source>\n        <translation>Aggiungi</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"866\"/>\n        <source>File/Folder</source>\n        <translation>File/Cartella</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"867\"/>\n        <source>External URL</source>\n        <translation>URL Esterna</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"870\"/>\n        <source>View</source>\n        <translation>Vedi</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1173\"/>\n        <source>Downloading data with errors...</source>\n        <translation>Download di dati con errori...</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1191\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>File scaricato correttamente in:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1192\"/>\n        <source>Success</source>\n        <translation>Successo</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"871\"/>\n        <source>Errors panel</source>\n        <translation>Pannello degli errori</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"872\"/>\n        <source>Source panel</source>\n        <translation>Pannello Sorgente</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"875\"/>\n        <source>Help</source>\n        <translation>Aiuto</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"876\"/>\n        <source>User Guide</source>\n        <translation>Guida utente</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"877\"/>\n        <source>Report an Issue</source>\n        <translation>Segnala un problema</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"879\"/>\n        <source>About</source>\n        <translation>Info</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"878\"/>\n        <source>View logs</source>\n        <translation>Vedi i log</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"915\"/>\n        <source>Language changed.</source>\n        <translation>Lingua cambiata.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"942\"/>\n        <source>File and Metadata changes saved.</source>\n        <translation>Cambiamenti nel file e nei metadati salvati.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1130\"/>\n        <source>Last 100 Lines</source>\n        <translation>Ultime 100 line</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1150\"/>\n        <source>Close</source>\n        <translation>Chiudi</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1155\"/>\n        <source>Copy to Clipboard</source>\n        <translation>Copia nel clipboard</translation>\n    </message>\n</context>\n<context>\n    <name>MetadataForm</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"189\"/>\n        <source>Column Name:</source>\n        <translation>Nome della colonna:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"190\"/>\n        <source>Data Type:</source>\n        <translation>Tipo di dato:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"191\"/>\n        <source>Description:</source>\n        <translation>Descrizione:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"192\"/>\n        <source>Flag empty cells as errors?:</source>\n        <translation>Contrassegnare le celle vuote come errori?:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"193\"/>\n        <source>Min. Characters in cells:</source>\n        <translation>numero minimo caratteri nella cella:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"194\"/>\n        <source>Max. Characters in cell</source>\n        <translation>numero massimo caratteri nella cella</translation>\n    </message>\n</context>\n<context>\n    <name>RenameDialog</name>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"50\"/>\n        <source>Rename file</source>\n        <translation>Rinomina file</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"51\"/>\n        <source>Rename item to:</source>\n        <translation>Rinomina la voce in:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"52\"/>\n        <source>Cancel</source>\n        <translation>Annulla</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"53\"/>\n        <source>OK</source>\n        <translation>OK</translation>\n    </message>\n</context>\n<context>\n    <name>Sidebar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"257\"/>\n        <source>Upload your data</source>\n        <translation>Carica i tuoi dati</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"258\"/>\n        <source>User guide</source>\n        <translation>Guida utente</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"259\"/>\n        <source>Report an issue</source>\n        <translation>Segnala un problema</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"260\"/>\n        <source>Rename</source>\n        <translation>Rinominare</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"261\"/>\n        <source>Open File in Location</source>\n        <translation>Apri file nella posizione</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"262\"/>\n        <source>Delete</source>\n        <translation>Elimina</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"302\"/>\n        <location filename=\"../../main.py\" line=\"306\"/>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <location filename=\"../../main.py\" line=\"340\"/>\n        <source>Error</source>\n        <translation>Errore</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"303\"/>\n        <source>Source is a file but destination a directory.</source>\n        <translation>La sorgente è un file ma la destinazione è una cartella.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"307\"/>\n        <source>Source is a directory but destination a file.</source>\n        <translation>La sorgente è una cartella ma la destinazione è un file.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <source>Operation not permitted.</source>\n        <translation>Operazione non permessa.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <source>File with this name already exists.</source>\n        <translation>Esiste già un file con questo nome.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"314\"/>\n        <source>Item renamed successfuly.</source>\n        <translation>Voce rinominata.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"344\"/>\n        <source>Item deleted successfuly.</source>\n        <translation>Voce eliminata.</translation>\n    </message>\n</context>\n<context>\n    <name>SourceViewer</name>\n    <message>\n        <location filename=\"../../panels/source.py\" line=\"82\"/>\n        <source>This view is only available for CSV files.</source>\n        <translation>Questa vista è disponibile solo per i file CSV.</translation>\n    </message>\n</context>\n<context>\n    <name>Toolbar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"502\"/>\n        <source>Data</source>\n        <translation>Dati</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"503\"/>\n        <source>Errors Report</source>\n        <translation>Report Errori</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"504\"/>\n        <source>Source code</source>\n        <translation>Codice Sorgente</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"505\"/>\n        <source>Export</source>\n        <translation>Esporta</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"506\"/>\n        <source>Save changes</source>\n        <translation>Salva modifiche</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"507\"/>\n        <source>AI</source>\n        <translation>AI</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"508\"/>\n        <source>Sheet:</source>\n        <translation>Foglio:</translation>\n    </message>\n</context>\n<context>\n    <name>Welcome</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"573\"/>\n        <source>The ODE supports Excel &amp; csv files</source>\n        <translation>ODE supporta file Excel &amp; csv</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"574\"/>\n        <source>You can also add links to online tables</source>\n        <translation>Puoi anche aggiungere link a tabelle online</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"575\"/>\n        <source>Upload your data</source>\n        <translation>Carica i tuoi dati</translation>\n    </message>\n</context>\n</TS>\n"
  },
  {
    "path": "src/ode/assets/translations/pt.ts",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>\n<TS version=\"2.1\" language=\"pt_PT\">\n<context>\n    <name>ColumnMetadataDialog</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"276\"/>\n        <source>Column name cannot be empty</source>\n        <translation>O nome da coluna não pode estar vazio</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"281\"/>\n        <source>There is another column in the table with the same name. Please choose a different one</source>\n        <translation>Há outra coluna na tabela com o mesmo nome. Por favor, escolha outro</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"306\"/>\n        <source>Save</source>\n        <translation>Salvar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"307\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n</context>\n<context>\n    <name>DataUploadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"163\"/>\n        <source>Please paste a valid URL.</source>\n        <translation>Por favor, cole uma URL válida.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"166\"/>\n        <source>Please paste a valid URL starting with http:// or https://.</source>\n        <translation>Por favor, cole uma URL válida começando com http:// ou https://.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"176\"/>\n        <source>Error: The Google Sheets URL is not valid or the table is not publicly available.</source>\n        <translation>Erro: A URL do Google Sheets não é válida ou a tabela não está publicamente disponível.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"187\"/>\n        <source>Error: The URL is not associated with a table</source>\n        <translation>Erro: A URL não está associada a uma tabela</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"216\"/>\n        <source>Upload your data</source>\n        <translation>Carregue seus dados</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"217\"/>\n        <source>Add one or more Excel or csv files</source>\n        <translation>Adicione um ou mais arquivos Excel ou CSV</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"218\"/>\n        <source>Add a folder</source>\n        <translation>Adicione uma pasta</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"219\"/>\n        <location filename=\"../../dialogs/upload.py\" line=\"220\"/>\n        <source>Select</source>\n        <translation>Selecionar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"221\"/>\n        <source>Link to the external table: </source>\n        <translation>Link para a tabela externa: </translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"222\"/>\n        <source>Enter or paste URL</source>\n        <translation>Digite ou cole a URL</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"226\"/>\n        <source>Paste your Google Sheet or csv link to create a local copy in the Open Data Editor</source>\n        <translation>Cole o link da sua planilha do Google ou CSV para criar uma cópia local no Editor de Dados Abertos.</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"227\"/>\n        <source>Add</source>\n        <translation>Adicionar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"228\"/>\n        <source>Add Local Files</source>\n        <translation>Adicionar arquivos locais</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/upload.py\" line=\"229\"/>\n        <source>Add External Data</source>\n        <translation>Adicionar Dados Externos</translation>\n    </message>\n</context>\n<context>\n    <name>DataViewer</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"557\"/>\n        <source>Preview not available for this item.</source>\n        <translation>Visualização não disponível para este item.</translation>\n    </message>\n</context>\n<context>\n    <name>DataWorker</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"67\"/>\n        <source>Reading file...</source>\n        <translation>Lendo arquivo...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"69\"/>\n        <source>Checking errors...</source>\n        <translation>Verificando erros...</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"82\"/>\n        <source>Read and error checking finished.</source>\n        <translation>Leitura e verificação de erros concluída.</translation>\n    </message>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"72\"/>\n        <source>Drawing table...</source>\n        <translation>Renderizando a tabela...</translation>\n    </message>\n</context>\n<context>\n    <name>DeleteDialog</name>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"43\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"44\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"45\"/>\n        <source>Are you sure you want to delete this item?</source>\n        <translation>Tem certeza que deseja excluir este item?</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/delete.py\" line=\"46\"/>\n        <source>Delete file</source>\n        <translation>Excluir arquivo</translation>\n    </message>\n</context>\n<context>\n    <name>DownloadDialog</name>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"48\"/>\n        <source>Download</source>\n        <translation>Baixar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"49\"/>\n        <source>Please, select one of the following options:</source>\n        <translation>Por favor, selecione uma das seguintes opções:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"50\"/>\n        <source>Download file</source>\n        <translation>Baixar arquivo</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"51\"/>\n        <source>Download file with errors</source>\n        <translation>Baixar arquivo com erros</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"63\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Arquivo baixado com sucesso em:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"67\"/>\n        <source>Error downloading file:\n{}</source>\n        <translation>Erro ao baixar o arquivo:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/download.py\" line=\"64\"/>\n        <source>Success</source>\n        <translation>Sucesso</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsMessages</name>\n    <message>\n        <location filename=\"../../utils.py\" line=\"121\"/>\n        <location filename=\"../../utils.py\" line=\"127\"/>\n        <source>Missing header</source>\n        <translation>Cabeçalho faltante</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"122\"/>\n        <source>Duplicated header</source>\n        <translation>Cabeçalho duplicado</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"123\"/>\n        <source>Empty row</source>\n        <translation>Linha vazia</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"124\"/>\n        <source>Type mismatch</source>\n        <translation>Incompatibilidade de tipo</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"125\"/>\n        <source>Missing value</source>\n        <translation>Valor faltante</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"126\"/>\n        <source>Extra cell</source>\n        <translation>Célula extra</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"128\"/>\n        <source>Blank Label</source>\n        <translation>Rótulo em branco</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"137\"/>\n        <location filename=\"../../utils.py\" line=\"143\"/>\n        <source>A column in the header row has no name. Every column should have a unique, non-empty header.</source>\n        <translation>Uma coluna na linha do cabeçalho não tem nome. Cada coluna deve ter um cabeçalho único e não vazio.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"138\"/>\n        <source>Two or more columns share the same name. Column names must be unique.</source>\n        <translation>Duas ou mais colunas compartilham o mesmo nome. Os nomes das colunas devem ser únicos.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"139\"/>\n        <source>This row has no data. Rows should contain at least one cell with data.</source>\n        <translation>Esta linha não tem dados. As linhas devem conter pelo menos uma célula com dados.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"140\"/>\n        <source>A cell value doesn&apos;t match the expected data type or format for the column.</source>\n        <translation>Um valor de célula não corresponde ao tipo de dado ou formato esperado para a coluna.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"141\"/>\n        <source>This cell is missing data</source>\n        <translation>Esta célula está sem dados</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"142\"/>\n        <source>This row has more values compared to the header row.</source>\n        <translation>Esta linha tem mais valores em comparação com a linha do cabeçalho.</translation>\n    </message>\n    <message>\n        <location filename=\"../../utils.py\" line=\"144\"/>\n        <source>A label in the header row is missing a value. Label should be provided and not be blank.</source>\n        <translation>Falta um valor em um rótulo da linha do cabeçalho. O rótulo deve ser fornecido e não pode estar em branco.</translation>\n    </message>\n</context>\n<context>\n    <name>ErrorsWidget</name>\n    <message>\n        <location filename=\"../../panels/errors.py\" line=\"220\"/>\n        <source>Please note that the ODE currently detects errors in tables, with a maximum of </source>\n        <translation>Por favor, note que a ODE atualmente detecta erros nas tabelas, com um máximo de </translation>\n    </message>\n</context>\n<context>\n    <name>FrictionlessTableModel</name>\n    <message>\n        <location filename=\"../../panels/data.py\" line=\"249\"/>\n        <source>Data</source>\n        <translation>Dados</translation>\n    </message>\n</context>\n<context>\n    <name>LLMWarningDialog</name>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"13\"/>\n        <source>AI assistant</source>\n        <translation>Assistente de IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"25\"/>\n        <source>Welcome to the ODE&apos;s AI assistant! This feature will help you generating better descriptions for the columns of your table and also questions for data analysis. \n\nTo get started, you will need to install the AI file in your computer. Once installed, everything will run locally, meaning your data always stays private and secure. Learn more</source>\n        <translation>Bem-vindo ao assistente de IA do ODE! Este recurso irá ajudá-lo a gerar melhores descrições para as colunas da sua tabela e também perguntas para análise de dados.\n\nPara começar, você precisará instalar o arquivo de IA no seu computador. Uma vez instalado, tudo será executado localmente, o que significa que seus dados sempre permanecem privados e seguros. Saiba mais</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"28\"/>\n        <source>Don&apos;t show again</source>\n        <translation>Não mostrar novamente</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"33\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/llm_dialog_warning.py\" line=\"40\"/>\n        <source>Ok</source>\n        <translation>Ok</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"288\"/>\n        <location filename=\"../../llama.py\" line=\"298\"/>\n        <location filename=\"../../llama.py\" line=\"314\"/>\n        <source>Execute</source>\n        <translation>Executar</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"282\"/>\n        <source>Generating response...</source>\n        <translation>Gerando resposta...</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"299\"/>\n        <source>Error</source>\n        <translation>Erro</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"313\"/>\n        <source>AI assistant</source>\n        <translation>Assistente de IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"315\"/>\n        <source>Stop execution</source>\n        <translation>Parar execução</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"316\"/>\n        <source>Results will be displayed here...</source>\n        <translation>Os resultados serão exibidos aqui...</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaDownloadDialog</name>\n    <message>\n        <location filename=\"../../llama.py\" line=\"398\"/>\n        <source>Delete</source>\n        <translation>Excluir</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"404\"/>\n        <source>Download</source>\n        <translation>Baixar</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"348\"/>\n        <source>AI assistant</source>\n        <translation>Assistente de IA</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"352\"/>\n        <source>To start using the AI assistant, please download the following model.</source>\n        <translation>Para começar a usar o assistente de IA, por favor baixe o seguinte modelo.</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"359\"/>\n        <source>The ODE will save the file in this location: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</source>\n        <translation>O ODE salvará o arquivo neste local: &lt;i&gt;&lt;a href=&quot;file://{AI_MODELS_PATH}&quot;&gt;{AI_MODELS_PATH}&lt;/a&gt;&lt;/i&gt;</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"373\"/>\n        <source>Next</source>\n        <translation>Próximo</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"457\"/>\n        <source>File exists</source>\n        <translation>Arquivo existente</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"458\"/>\n        <source>Do you want to delete it and download it again?</source>\n        <translation>Deseja excluí-lo e baixá-lo novamente?</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Downloading model</source>\n        <translation>Baixando modelo</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"474\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"475\"/>\n        <source>LLM Model Download Progress</source>\n        <translation>Progresso do Download do Modelo LLM</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"486\"/>\n        <location filename=\"../../llama.py\" line=\"553\"/>\n        <source>Error Occurred</source>\n        <translation>Ocorreu um erro</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"494\"/>\n        <source>Confirm Deletion</source>\n        <translation>Confirmar exclusão</translation>\n    </message>\n    <message>\n        <location filename=\"../../llama.py\" line=\"495\"/>\n        <source>Are you sure you want to delete {AI_MODEL.name}?</source>\n        <translation>Tem certeza de que deseja excluir {AI_MODEL.name}?</translation>\n    </message>\n</context>\n<context>\n    <name>LlamaWorker</name>\n    <message>\n        <source>Preparing data for analysis...</source>\n        <translation type=\"obsolete\">Preparando os dados para análise...</translation>\n    </message>\n</context>\n<context>\n    <name>LoadingDialog</name>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"30\"/>\n        <source>Loading</source>\n        <translation>Carregando...</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/loading.py\" line=\"31\"/>\n        <source>Loading...</source>\n        <translation>Carregando...</translation>\n    </message>\n</context>\n<context>\n    <name>MainWindow</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"684\"/>\n        <source>Ready.</source>\n        <translation>Pronto.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error</source>\n        <translation>Erro</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"817\"/>\n        <source>Error initializing the LLM:\n</source>\n        <translation>Erro ao inicializar o LLM:\n</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"864\"/>\n        <source>File</source>\n        <translation>Arquivo</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"865\"/>\n        <source>Add</source>\n        <translation>Adicionar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"866\"/>\n        <source>File/Folder</source>\n        <translation>Arquivo/Pasta</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"867\"/>\n        <source>External URL</source>\n        <translation>URL Externa</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"870\"/>\n        <source>View</source>\n        <translation>Visualizar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1173\"/>\n        <source>Downloading data with errors...</source>\n        <translation>Baixando dados com erros...</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1191\"/>\n        <source>File downloaded successfully to:\n{}</source>\n        <translation>Arquivo baixado com sucesso em:\n{}</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1192\"/>\n        <source>Success</source>\n        <translation>Sucesso</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"871\"/>\n        <source>Errors panel</source>\n        <translation>Painel de Erros</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"872\"/>\n        <source>Source panel</source>\n        <translation>Painel de Origem</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"875\"/>\n        <source>Help</source>\n        <translation>Ajuda</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"876\"/>\n        <source>User Guide</source>\n        <translation>Guia do Usuário</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"877\"/>\n        <source>Report an Issue</source>\n        <translation>Reportar um Problema</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"879\"/>\n        <source>About</source>\n        <translation>Sobre</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"878\"/>\n        <source>View logs</source>\n        <translation>Ver logs</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"915\"/>\n        <source>Language changed.</source>\n        <translation>Idioma alterado.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"942\"/>\n        <source>File and Metadata changes saved.</source>\n        <translation>Alterações no arquivo e metadados salvas.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1130\"/>\n        <source>Last 100 Lines</source>\n        <translation>Últimas 100 linhas</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1150\"/>\n        <source>Close</source>\n        <translation>Fechar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"1155\"/>\n        <source>Copy to Clipboard</source>\n        <translation>Copiar para área de transferência</translation>\n    </message>\n</context>\n<context>\n    <name>MetadataForm</name>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"189\"/>\n        <source>Column Name:</source>\n        <translation>Nome da coluna:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"190\"/>\n        <source>Data Type:</source>\n        <translation>Tipo de dado:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"191\"/>\n        <source>Description:</source>\n        <translation>Descrição:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"192\"/>\n        <source>Flag empty cells as errors?:</source>\n        <translation>Marcar células vazias como erros?</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"193\"/>\n        <source>Min. Characters in cells:</source>\n        <translation>Caracteres mínimos nas células:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/metadata.py\" line=\"194\"/>\n        <source>Max. Characters in cell</source>\n        <translation>Caracteres máximos na célula:</translation>\n    </message>\n</context>\n<context>\n    <name>RenameDialog</name>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"50\"/>\n        <source>Rename file</source>\n        <translation>Renomear arquivo</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"51\"/>\n        <source>Rename item to:</source>\n        <translation>Renomear item para:</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"52\"/>\n        <source>Cancel</source>\n        <translation>Cancelar</translation>\n    </message>\n    <message>\n        <location filename=\"../../dialogs/rename.py\" line=\"53\"/>\n        <source>OK</source>\n        <translation>OK</translation>\n    </message>\n</context>\n<context>\n    <name>Sidebar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"257\"/>\n        <source>Upload your data</source>\n        <translation>Carregue seus dados</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"258\"/>\n        <source>User guide</source>\n        <translation>Guia do Usuário</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"259\"/>\n        <source>Report an issue</source>\n        <translation>Reportar um Problema</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"260\"/>\n        <source>Rename</source>\n        <translation>Renomear</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"261\"/>\n        <source>Open File in Location</source>\n        <translation>Abrir Arquivo no Local</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"262\"/>\n        <source>Delete</source>\n        <translation>Excluir</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"302\"/>\n        <location filename=\"../../main.py\" line=\"306\"/>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <location filename=\"../../main.py\" line=\"340\"/>\n        <source>Error</source>\n        <translation>Erro</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"303\"/>\n        <source>Source is a file but destination a directory.</source>\n        <translation>A origem é um arquivo, mas o destino é um diretório.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"307\"/>\n        <source>Source is a directory but destination a file.</source>\n        <translation>A origem é um diretório, mas o destino é um arquivo.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"310\"/>\n        <source>Operation not permitted.</source>\n        <translation>Operação não permitida.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"312\"/>\n        <source>File with this name already exists.</source>\n        <translation>Um arquivo com este nome já existe.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"314\"/>\n        <source>Item renamed successfuly.</source>\n        <translation>Item renomeado com sucesso.</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"344\"/>\n        <source>Item deleted successfuly.</source>\n        <translation>Item excluído com sucesso.</translation>\n    </message>\n</context>\n<context>\n    <name>SourceViewer</name>\n    <message>\n        <location filename=\"../../panels/source.py\" line=\"82\"/>\n        <source>This view is only available for CSV files.</source>\n        <translation>Esta visualização está disponível apenas para arquivos CSV.</translation>\n    </message>\n</context>\n<context>\n    <name>Toolbar</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"502\"/>\n        <source>Data</source>\n        <translation>Dados</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"508\"/>\n        <source>Sheet:</source>\n        <translation>Planilha:</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"503\"/>\n        <source>Errors Report</source>\n        <translation>Relatório de Erros</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"504\"/>\n        <source>Source code</source>\n        <translation>Código-fonte</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"505\"/>\n        <source>Export</source>\n        <translation>Exportar</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"506\"/>\n        <source>Save changes</source>\n        <translation>Salvar alterações</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"507\"/>\n        <source>AI</source>\n        <translation>IA</translation>\n    </message>\n</context>\n<context>\n    <name>Welcome</name>\n    <message>\n        <location filename=\"../../main.py\" line=\"573\"/>\n        <source>The ODE supports Excel &amp; csv files</source>\n        <translation>O ODE suporta arquivos Excel e CSV</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"574\"/>\n        <source>You can also add links to online tables</source>\n        <translation>Você também pode adicionar links para tabelas online</translation>\n    </message>\n    <message>\n        <location filename=\"../../main.py\" line=\"575\"/>\n        <source>Upload your data</source>\n        <translation>Carregue seus dados</translation>\n    </message>\n</context>\n</TS>\n"
  },
  {
    "path": "src/ode/dialogs/__init__.py",
    "content": ""
  },
  {
    "path": "src/ode/dialogs/delete.py",
    "content": "from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QWidget\n\n\nclass DeleteDialog(QDialog):\n    \"\"\"\n    Dialog to delete a file.\n    \"\"\"\n\n    def __init__(self, parent: QWidget, filename: str):\n        super().__init__(parent)\n\n        self.result_text = None\n        layout = QVBoxLayout()\n\n        self.delete_dialog_label = QLabel()\n        layout.addWidget(self.delete_dialog_label)\n\n        button_layout = QHBoxLayout()\n        self.cancel_button = QPushButton()\n        # We set the default button to Cancel when clicking Enter\n        self.cancel_button.setDefault(True)\n        self.ok_button = QPushButton()\n\n        self.cancel_button.clicked.connect(self.reject)\n        self.ok_button.clicked.connect(self.accept)\n\n        button_layout.addWidget(self.cancel_button)\n        button_layout.addWidget(self.ok_button)\n\n        layout.addLayout(button_layout)\n        self.setLayout(layout)\n        self.retranslateUI()\n\n    @staticmethod\n    def confirm(parent, filename):\n        \"\"\"Static method that returns a boolean value indicating if the user\n        confirmed the deletion of the file.\"\"\"\n        dialog = DeleteDialog(parent, filename)\n        return dialog.exec() == QDialog.DialogCode.Accepted\n\n    def retranslateUI(self):\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.cancel_button.setText(self.tr(\"Cancel\"))\n        self.ok_button.setText(self.tr(\"Ok\"))\n        self.delete_dialog_label.setText(self.tr(\"Are you sure you want to delete this item?\"))\n        self.setWindowTitle(self.tr(\"Delete file\"))\n"
  },
  {
    "path": "src/ode/dialogs/download.py",
    "content": "import os\nimport shutil\n\nfrom PySide6.QtWidgets import QVBoxLayout, QPushButton, QDialog, QMessageBox, QLabel, QHBoxLayout\nfrom PySide6.QtCore import Qt, Signal, QStandardPaths\nfrom pathlib import Path\n\n\nclass DownloadDialog(QDialog):\n    \"\"\"Dialog to export the file and the errors.\"\"\"\n\n    download_data_with_errors = Signal()\n    finished = Signal()\n\n    def __init__(self, parent, filepath: Path, has_errors:bool) -> None:\n        super().__init__(parent)\n\n        self.filepath = filepath\n        self.setFixedHeight(200)\n\n        layout = QVBoxLayout()\n\n        self.label = QLabel()\n        layout.addWidget(self.label)\n\n        # Block the main window until the dialog is closed\n        self.setWindowModality(Qt.WindowModality.ApplicationModal)\n\n        button_layout = QHBoxLayout()\n\n        self.download_button = QPushButton()\n        self.download_button.clicked.connect(self.download_file)\n        button_layout.addWidget(self.download_button)\n\n        self.download_error_button = QPushButton()\n        self.download_error_button.clicked.connect(self.download_error_file)\n        if not has_errors:\n            self.download_error_button.setDisabled(True)\n        button_layout.addWidget(self.download_error_button)\n\n        layout.addLayout(button_layout)\n\n        self.setLayout(layout)\n        self.retranslateUI()\n\n    def retranslateUI(self) -> None:\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.setWindowTitle(self.tr(\"Download\"))\n        self.label.setText(self.tr(\"Please, select one of the following options:\"))\n        self.download_button.setText(self.tr(\"Download file\"))\n        self.download_error_button.setText(self.tr(\"Download file with errors\"))\n\n    def download_file(self):\n        \"\"\"\n        Opens a dialog to select the destination directory and copies the file\n        \"\"\"\n        downloads_path = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation)\n        filename = os.path.basename(self.filepath)\n        filepath = Path(downloads_path, filename)\n\n        try:\n            shutil.copy2(self.filepath, filepath)\n            success_text = self.tr(\"File downloaded successfully to:\\n{}\").format(filepath)\n            QMessageBox.information(self, self.tr(\"Success\"), success_text)\n\n        except Exception as e:\n            error_text = self.tr(\"Error downloading file:\\n{}\").format(str(e))\n            QMessageBox.critical(self, \"Error\", error_text)\n\n    def download_error_file(self):\n        self.download_data_with_errors.emit()\n"
  },
  {
    "path": "src/ode/dialogs/llm_dialog_warning.py",
    "content": "from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QWidget, QCheckBox\nfrom PySide6.QtCore import QSettings\n\n\nclass LLMWarningDialog(QDialog):\n    \"\"\"\n    This dialog informs users that the AI assistant operates entirely on their laptop,\n    ensuring that data from their table is never sent or shared outside the device.\n    \"\"\"\n\n    def __init__(self, parent: QWidget):\n        super().__init__(parent)\n        self.setWindowTitle(self.tr(\"AI assistant\"))\n        self.setFixedSize(600, 270)\n\n        layout = QVBoxLayout()\n\n        self.warning_text = QLabel()\n        self.warning_text.setWordWrap(True)\n        self.warning_text.setText(\n            self.tr(\n                \"Welcome to the ODE's AI assistant! This feature will help you generating better descriptions for the columns of your table and also questions for data analysis. \\n\\n\"\n                \"To get started, you will need to install the AI file in your computer. Once installed, everything will run locally, meaning your data always stays private and secure. Learn more\"\n            )\n        )\n\n        self.dont_show_again_checkbox = QCheckBox()\n        self.dont_show_again_checkbox.setText(self.tr(\"Don't show again\"))\n\n        button_layout = QHBoxLayout()\n        self.cancel_button = QPushButton()\n        self.cancel_button.clicked.connect(self.reject)\n        self.cancel_button.setText(self.tr(\"Cancel\"))\n        button_layout.addWidget(self.cancel_button)\n\n        self.ok_button = QPushButton()\n        self.ok_button.setDefault(True)\n        button_layout.addWidget(self.ok_button)\n        self.ok_button.clicked.connect(self.accept)\n        self.ok_button.setText(self.tr(\"Ok\"))\n\n        layout.addWidget(self.warning_text)\n        layout.addWidget(self.dont_show_again_checkbox)\n        layout.addLayout(button_layout)\n        self.setLayout(layout)\n\n    def accept(self):\n        \"\"\"Override accept to save the checkbox state\"\"\"\n        if self.dont_show_again_checkbox.isChecked():\n            settings = QSettings()\n            settings.setValue(\"llm_warning_dialog/dont_show_again\", True)\n\n        super().accept()\n\n    @staticmethod\n    def confirm(parent):\n        settings = QSettings()\n        # Check if the user has previously chosen to not show the dialog again\n        if settings.value(\"llm_warning_dialog/dont_show_again\", False, type=bool):\n            return True\n\n        dialog = LLMWarningDialog(parent)\n        return dialog.exec() == QDialog.DialogCode.Accepted\n"
  },
  {
    "path": "src/ode/dialogs/loading.py",
    "content": "from PySide6.QtWidgets import QDialog, QProgressBar, QVBoxLayout, QLabel\nfrom PySide6.QtCore import QTimer, Slot\n\n\nclass LoadingDialog(QDialog):\n    \"\"\"\n    Dialog to show a loading message while the application is doing some work.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.setMinimumSize(350, 80)\n\n        self.label = QLabel()\n        self.label.setMargin(0)\n        self.progressBar = QProgressBar()\n        self.progressBar.setRange(0, 0)\n\n        mainLayout = QVBoxLayout(self)\n        mainLayout.addWidget(self.label)\n        mainLayout.addWidget(self.progressBar)\n\n        self.timer = QTimer()\n        self.timer.setSingleShot(True)\n        self.timer.timeout.connect(self.exec)\n\n        self.retranslateUI()\n\n    def retranslateUI(self):\n        self.setWindowTitle(self.tr(\"Loading\"))\n        self.label.setText(self.tr(\"Loading...\"))\n\n    def cancel_loading_timer(self):\n        if self.timer.isActive():\n            self.timer.stop()\n\n    def show(self, millis: int = 300):\n        self.timer.start(millis)\n\n    def show_immediately(self):\n        self.show()\n\n    @Slot(str)\n    def show_message(self, message):\n        self.label.setText(message)\n"
  },
  {
    "path": "src/ode/dialogs/metadata.py",
    "content": "from typing import NamedTuple\nfrom PySide6.QtWidgets import (\n    QDialog,\n    QVBoxLayout,\n    QHBoxLayout,\n    QPushButton,\n    QWidget,\n    QGridLayout,\n    QLabel,\n    QLineEdit,\n    QTextEdit,\n    QComboBox,\n    QSpinBox,\n)\nfrom PySide6.QtCore import Qt, Signal\n\n\nclass ColumnMetadataField(NamedTuple):\n    \"\"\"\n    Represents a field in the metadata with its name, type, description, and constraints.\n    \"\"\"\n\n    name: str\n    type: str\n    description: str\n    constraints: dict\n\n\nclass DataTypeMapper:\n    \"\"\"Class to handle bidirectional mapping between internal types and user-displayed types\"\"\"\n\n    DATA_TYPES = [\n        \"number\",\n        \"date\",\n        \"string\",\n        \"any\",\n        \"array\",\n        \"boolean\",\n        \"datetime\",\n        \"duration\",\n        \"geojson\",\n        \"geopoint\",\n        \"integer\",\n        \"object\",\n        \"time\",\n        \"year\",\n        \"yearmonth\",\n    ]\n\n    # Mapping from internal types to user-displayed types\n    DISPLAY_MAPPING = {\n        \"string\": \"Text\",\n        \"number\": \"Number\",\n        \"date\": \"Date\",\n        \"any\": \"Any\",\n        \"array\": \"Array\",\n        \"boolean\": \"Boolean\",\n        \"datetime\": \"Date Time\",\n        \"duration\": \"Duration\",\n        \"geojson\": \"GeoJSON\",\n        \"geopoint\": \"Geo Point\",\n        \"integer\": \"Integer\",\n        \"object\": \"Object\",\n        \"time\": \"Time\",\n        \"year\": \"Year\",\n        \"yearmonth\": \"Year Month\",\n    }\n\n    def __init__(self):\n        # Reverse mapping: from displayed types to internal types\n        self.internal_mapping = {v: k for k, v in self.DISPLAY_MAPPING.items()}\n\n    def get_display_type(self, internal_type):\n        \"\"\"Converts an internal type to its user-displayed representation\"\"\"\n        return self.DISPLAY_MAPPING.get(internal_type, internal_type)\n\n    def get_internal_type(self, display_type):\n        \"\"\"Converts a user-displayed type to its internal representation\"\"\"\n        return self.internal_mapping.get(display_type, display_type)\n\n    def get_all_display_types(self):\n        \"\"\"Returns all types in user-display format\"\"\"\n        return [self.get_display_type(t) for t in self.DATA_TYPES]\n\n    def get_all_internal_types(self):\n        \"\"\"Returns all internal types\"\"\"\n        return self.DATA_TYPES.copy()\n\n\nclass NoWheelComboBox(QComboBox):\n    \"\"\"QComboBox that disables the mouse wheel event.\n\n    The current UX when scrolling through FieldsForms is not ideal since as soon as\n    the mouse points a QComboBox the form stops scrolling and it starts changing the\n    value of the QComboBox instead.\n    \"\"\"\n\n    def wheelEvent(self, event):\n        event.ignore()\n\n\nclass MetadataForm(QWidget):\n    \"\"\"\n    Widget to show the Metadata Fields that will be displayed inside a dialog.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        layout = QGridLayout()\n\n        # Name\n        self.nameLabel = QLabel()\n        layout.addWidget(self.nameLabel, 0, 0)\n        self.name = QLineEdit()\n        self.name.setMinimumWidth(200)\n        layout.addWidget(self.name, 0, 1, 1, 3)\n\n        # Type\n        self.typeLabel = QLabel()\n        layout.addWidget(self.typeLabel, 1, 0)\n        self.type = NoWheelComboBox()\n        layout.addWidget(self.type, 1, 1, 1, 3)\n        self.type.currentTextChanged.connect(self.on_type_changed)\n\n        # Description\n        self.description_label = QLabel()\n        layout.addWidget(self.description_label, 2, 0)\n        self.description = QTextEdit()\n        self.description.setMaximumHeight(60)\n        layout.addWidget(self.description, 2, 1, 1, 3)\n\n        # Required\n        self.required_label = QLabel()\n        layout.addWidget(self.required_label, 3, 0)\n        self.required = QComboBox()\n        self.required.addItems([\"Yes\", \"No\"])\n        layout.addWidget(self.required, 3, 1, 1, 3)\n\n        # Min and Max Length\n        self.min_length_label = QLabel()\n        layout.addWidget(self.min_length_label, 4, 0)\n        self.min_length = QSpinBox()\n        layout.addWidget(self.min_length, 4, 1)\n\n        self.max_length_label = QLabel()\n        layout.addWidget(self.max_length_label, 4, 2)\n        self.max_length = QSpinBox()\n        layout.addWidget(self.max_length, 4, 3)\n\n        # Error label\n        self.error_label = QLabel()\n        self.error_label.setStyleSheet(\"color: red;\")\n        layout.addWidget(self.error_label, 6, 0, 1, 4)\n        self.error_label.setHidden(True)\n\n        # Set layout properties\n        layout.setColumnMinimumWidth(1, 150)\n        layout.setColumnMinimumWidth(3, 150)\n        layout.setHorizontalSpacing(20)\n\n        self.setLayout(layout)\n        self.retranslateUI()\n\n    def on_type_changed(self, text):\n        \"\"\"\n        Updates the min and max length fields based on the selected type.\n        \"\"\"\n        if text == \"Text\":\n            self.min_length.setEnabled(True)\n            self.min_length.setStyleSheet(\"\")\n            self.min_length_label.setStyleSheet(\"\")\n\n            self.max_length.setEnabled(True)\n            self.max_length.setStyleSheet(\"\")\n            self.max_length_label.setStyleSheet(\"\")\n        else:\n            self.min_length.setEnabled(False)\n            self.min_length.setStyleSheet(\"color: lightgray;\")\n            self.min_length_label.setStyleSheet(\"color: lightgray;\")\n\n            self.max_length.setEnabled(False)\n            self.max_length.setStyleSheet(\"color: lightgray;\")\n            self.max_length_label.setStyleSheet(\"color: lightgray;\")\n\n    def retranslateUI(self):\n        \"\"\"\n        Applies the translations to the labels.\n        \"\"\"\n        self.nameLabel.setText(self.tr(\"Column Name:\"))\n        self.typeLabel.setText(self.tr(\"Data Type:\"))\n        self.description_label.setText(self.tr(\"Description:\"))\n        self.required_label.setText(self.tr(\"Flag empty cells as errors?:\"))\n        self.min_length_label.setText(self.tr(\"Min. Characters in cells:\"))\n        self.max_length_label.setText(self.tr(\"Max. Characters in cell\"))\n\n\nclass ColumnMetadataDialog(QDialog):\n    \"\"\"\n    Dialog for editing the column's metadata.\n    \"\"\"\n\n    save_clicked = Signal(object)\n\n    def __init__(self, parent: QWidget, field: ColumnMetadataField, field_index: int, field_names: list):\n        \"\"\"\n        Initialize the dialog.\n\n        Args:\n            parent: The parent widget\n        \"\"\"\n        super().__init__(parent)\n        self.parent = parent\n\n        self.dataTypeMapper = DataTypeMapper()\n\n        self.field_index = field_index\n        self.field_names = field_names\n        self.field = field\n\n        # Set up the form\n        self.form = MetadataForm()\n        self.form.name.setText(field.name)\n        self.form.type.addItems(self.dataTypeMapper.get_all_display_types())\n        self.form.type.setCurrentText(self.dataTypeMapper.get_display_type(field.type))\n        self.form.description.setText(field.description)\n        self.form.required.setCurrentText(\"Yes\" if field.constraints.get(\"required\") else \"No\")\n\n        self.form.min_length.setMinimum(0)\n        self.form.min_length.setMaximum(999999)\n        self.form.min_length.setValue(field.constraints.get(\"minLength\", 0))\n\n        self.form.max_length.setMinimum(0)\n        self.form.max_length.setMaximum(999999)\n        self.form.max_length.setValue(field.constraints.get(\"maxLength\", 999))\n\n        # Set window modality\n        self.setWindowModality(Qt.WindowModality.WindowModal)\n\n        # Create buttons\n        self.save_button = QPushButton()\n        self.cancel_button = QPushButton()\n\n        # Set up the layout\n        self.setup_layout()\n\n        # Connect signals\n        self.save_button.clicked.connect(self.save_and_close)\n        self.cancel_button.clicked.connect(self.reject)\n\n    def setup_layout(self):\n        \"\"\"\n        Set up the dialog layout.\n        \"\"\"\n        layout = QVBoxLayout()\n        layout.addWidget(self.form)\n\n        # Buttons layout\n        buttons_layout = QHBoxLayout()\n        buttons_layout.addWidget(self.cancel_button)\n        buttons_layout.addWidget(self.save_button)\n        layout.addLayout(buttons_layout)\n\n        self.setLayout(layout)\n        self.retranslateUI()\n\n    def save_and_close(self):\n        \"\"\"\n        Emits the save_clicked signal with the form data and closes the dialog.\n        \"\"\"\n        # Validate the field name\n        field_name_error = None\n        field_name = self.form.name.text()\n\n        if field_name.strip() == \"\":\n            field_name_error = self.tr(\"Column name cannot be empty\")\n        elif field_name != self.field.name and field_name in self.field_names:\n            field_name_error = self.tr(\n                \"There is another column in the table with the same name. Please choose a different one\"\n            )\n\n        if field_name_error:\n            self.form.name.setStyleSheet(\"border: 1px solid red;\")\n            self.form.error_label.setText(field_name_error)\n            self.form.error_label.setHidden(False)\n            return\n\n        self.save_clicked.emit(\n            {\n                \"index\": self.field_index,\n                \"name\": field_name,\n                \"type\": self.dataTypeMapper.get_internal_type(self.form.type.currentText()),\n                \"description\": self.form.description.toPlainText(),\n                \"constraints\": {\n                    \"required\": self.form.required.currentText() == \"Yes\",\n                    \"minLength\": self.form.min_length.value(),\n                    \"maxLength\": self.form.max_length.value(),\n                },\n            }\n        )\n        self.accept()\n\n    def retranslateUI(self):\n        \"\"\"\n        Applies the translations to the labels.\n        \"\"\"\n        self.save_button.setText(self.tr(\"Save\"))\n        self.cancel_button.setText(self.tr(\"Cancel\"))\n"
  },
  {
    "path": "src/ode/dialogs/rename.py",
    "content": "from PySide6.QtWidgets import QDialog, QVBoxLayout, QLineEdit, QPushButton, QHBoxLayout, QLabel, QWidget\n\n\nclass RenameDialog(QDialog):\n    \"\"\"\n    Dialog to rename a file.\n    The dialog will show a text box with the current filename and allow the user\n    to change it. The dialog will save the new filaneme in the result_text\n    attribute when the user clicks OK.\n    \"\"\"\n\n    def __init__(self, parent: QWidget, filename: str):\n        super().__init__(parent)\n\n        self.result_text = None\n\n        layout = QVBoxLayout()\n        self.rename_label = QLabel()\n        layout.addWidget(self.rename_label)\n\n        self.text_edit = QLineEdit(filename)\n        self.text_edit.setMinimumWidth(300)\n        self.text_edit.setMinimumHeight(30)\n        layout.addWidget(self.text_edit)\n\n        button_layout = QHBoxLayout()\n        self.cancel_button = QPushButton()\n        self.ok_button = QPushButton()\n        # We set the default button to OK when clicking Enter\n        self.ok_button.setDefault(True)\n\n        self.cancel_button.clicked.connect(self.reject)\n        self.ok_button.clicked.connect(self.accept)\n\n        button_layout.addWidget(self.cancel_button)\n        button_layout.addWidget(self.ok_button)\n\n        layout.addLayout(button_layout)\n        self.setLayout(layout)\n        self.retranslateUI()\n\n    def accept(self):\n        \"\"\"\n        Accept the dialog and save the new filename in result_text.\"\"\"\n        self.result_text = self.text_edit.text()\n        super().accept()\n\n    def retranslateUI(self):\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.setWindowTitle(self.tr(\"Rename file\"))\n        self.rename_label.setText(self.tr(\"Rename item to:\"))\n        self.cancel_button.setText(self.tr(\"Cancel\"))\n        self.ok_button.setText(self.tr(\"OK\"))\n"
  },
  {
    "path": "src/ode/dialogs/upload.py",
    "content": "import re\nimport shutil\n\nfrom frictionless.resources import FileResource, TableResource, FrictionlessException\nfrom pathlib import Path\nfrom PySide6.QtWidgets import (\n    QWidget,\n    QVBoxLayout,\n    QHBoxLayout,\n    QPushButton,\n    QLabel,\n    QFileDialog,\n    QDialog,\n    QTabWidget,\n    QLineEdit,\n)\nfrom PySide6.QtGui import QPixmap\nfrom PySide6.QtCore import Qt\n\nfrom ode import paths\nfrom ode.paths import Paths\n\n\nclass SelectWidget(QWidget):\n    \"\"\"Widget to render the File/Folder upload buttons.\"\"\"\n\n    def __init__(self, icon_path, parent=None):\n        super().__init__(parent)\n        layout = QVBoxLayout()\n\n        icon_label = QLabel(self)\n        pixmap = QPixmap(icon_path)\n        icon_label.setPixmap(pixmap)\n        icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        layout.addWidget(icon_label)\n\n        self.text_label = QLabel()\n        self.text_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)\n        self.text_label.setWordWrap(True)\n        layout.addWidget(self.text_label)\n\n        self.select_button = QPushButton()\n        layout.addWidget(self.select_button)\n\n        self.setLayout(layout)\n\n    def connect_select_action(self, action):\n        self.select_button.clicked.connect(action)\n\n\nclass DataUploadDialog(QDialog):\n    \"\"\"Dialog to Upload File, Folders or URLs.\n\n    The goal of this Dialog is to have an intuitive UX for people to add\n    files, folders or URLs. For external URLs we rely on frictionless's\n    TableResource to read and write tables hosted in the web or Google\n    Spreadsheets.\n\n    How to use:\n      dialog = DataUploadDialog(self)\n      ok, path = dialog.upload_dialog()\n    \"\"\"\n\n    def __init__(self, parent, external_first=False):\n        super().__init__(parent)\n        self.setFixedHeight(400)\n        self.setFixedWidth(600)\n\n        self.target_path = Path()\n\n        main_layout = QVBoxLayout()\n        main_layout.setContentsMargins(10, 100, 10, 10)\n\n        # Block the main window until the dialog is closed\n        self.setWindowModality(Qt.WindowModality.ApplicationModal)\n\n        # Tab Widget\n        self.tab_widget = QTabWidget()\n        main_layout.addWidget(self.tab_widget)\n\n        # From Your Computer Tab\n        from_computer_tab = QWidget()\n        from_computer_layout = QHBoxLayout()\n        from_computer_tab.setLayout(from_computer_layout)\n\n        self.file_select_widget = SelectWidget(Paths.asset(\"icons/upload-file.png\"))\n        self.file_select_widget.connect_select_action(self.add_files)\n        self.folder_select_widget = SelectWidget(Paths.asset(\"icons/upload-folder.png\"))\n        self.folder_select_widget.connect_select_action(self.add_folders)\n\n        from_computer_layout.addWidget(self.file_select_widget)\n        from_computer_layout.addWidget(self.folder_select_widget)\n\n        # Add External Data Tab\n        external_data_tab = QWidget()\n        external_data_layout = QVBoxLayout()\n        external_data_tab.setLayout(external_data_layout)\n\n        self.url_label = QLabel()\n        self.url_input = QLineEdit()\n        self.help_text = QLabel()\n        self.help_text.setWordWrap(True)\n        self.help_text.setStyleSheet(\"font-style:italic; font-size: 15px;\")\n        self.paste_button = QPushButton()\n        self.paste_button.clicked.connect(self.load_table_from_url)\n        self.error_text = QLabel()\n        self.error_text.setWordWrap(True)\n        self.error_text.setStyleSheet(\"color: red; font-style: italic; font-size: 15px;\")\n\n        external_data_layout.addWidget(self.url_label)\n        external_data_layout.addWidget(self.url_input)\n        external_data_layout.addWidget(self.help_text)\n        external_data_layout.addWidget(self.paste_button)\n        external_data_layout.addWidget(self.error_text)\n\n        # Add Tabs to Tab Widget\n        self.tab_widget.addTab(from_computer_tab, \"\")\n        self.tab_widget.addTab(external_data_tab, \"\")\n\n        if external_first:\n            self.tab_widget.setCurrentIndex(1)\n\n        self.setLayout(main_layout)\n\n        self.retranslateUI()\n\n    def add_files(self):\n        \"\"\"Copy the selected file to the project path.\"\"\"\n        filters = [\n            \"All supported files (*.csv *.xlsx *.xls)\",\n            \"Comma Separated Values (*.csv)\",\n            \"Excel 2007-365 (*.xlsx)\",\n            \"Excel 97-2003 (*.xls)\",\n        ]\n        filename, _ = QFileDialog.getOpenFileName(self, filter=\";;\".join(filters))\n\n        if not filename:\n            return\n\n        self.target_path = Paths.get_unique_destination_filepath(filename)\n        shutil.copy(filename, self.target_path)\n        self.accept()\n\n    def add_folders(self) -> None:\n        \"\"\"Copy the selected folder and all its content to the project path.\"\"\"\n        source_folder = QFileDialog.getExistingDirectory(self)\n        if not source_folder:\n            self.reject()\n            return\n        folder = Path(source_folder)\n        self.target_path = paths.PROJECT_PATH / folder.name\n        shutil.copytree(folder, self.target_path, dirs_exist_ok=True)\n        self.accept()\n\n    def load_table_from_url(self):\n        \"\"\"Load a tabular file from a public URL.\n\n        This method uses frictionless to read a remote URL. Currently we support\n        Google Spreadsheets and any other URL pointing to a csv file.\n        \"\"\"\n        url = self.url_input.text()\n        if not url:\n            self.error_text.setText(self.tr(\"Please paste a valid URL.\"))\n            return\n        if not url.startswith((\"http://\", \"https://\")):\n            self.error_text.setText(self.tr(\"Please paste a valid URL starting with http:// or https://.\"))\n            return\n\n        table = TableResource(path=url)\n        filename = table.name\n        if table.format == \"gsheets\":\n            try:\n                filename = self._read_url_html_title(url)\n            except FrictionlessException:\n                error = self.tr(\"Error: The Google Sheets URL is not valid or the table is not publicly available.\")\n                self.error_text.setText(error)\n                return\n\n        self.target_path = Paths.get_unique_destination_filepath(filename + \".csv\")\n\n        try:\n            with open(self.target_path, mode=\"w\") as file:\n                table.write(file.name)\n            self.accept()\n        except Exception:\n            error = self.tr(\"Error: The URL is not associated with a table\")\n            self.error_text.setText(error)\n\n    def upload_dialog(self) -> tuple[int, Path]:\n        \"\"\"Shows the dialog and then returns the result code and the path to the uploaded file.\n\n        This method is inspired in QFileDIalog.getOpenFileName(...) and\n        QInputDialog.getText(..). When called, this method will display the dialog\n        and return the result code + the path where the file/folder has been copied to.\n        \"\"\"\n        result = self.exec()\n        return result, self.target_path\n\n    def _read_url_html_title(self, url):\n        \"\"\"Return the title of HTML document.\n\n        We use the `title` attribute of the Google Spreadshet's HTML as name of the file.\n        This attribute is the same as the name of the spreadsheet.\n        \"\"\"\n        file = FileResource(path=url)\n        text = file.read_text(size=10000)\n        match = re.search(r\"<title>(.*?)</title>\", text)\n        if match:\n            title = match.group(1)\n            title = title.rsplit(\"- Google\", 1)[0].strip()\n            return f\"{title}\"\n        return \"google-sheets\"\n\n    def retranslateUI(self):\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.setWindowTitle(self.tr(\"Upload your data\"))\n        self.file_select_widget.text_label.setText(self.tr(\"Add one or more Excel or csv files\"))\n        self.folder_select_widget.text_label.setText(self.tr(\"Add a folder\"))\n        self.file_select_widget.select_button.setText(self.tr(\"Select\"))\n        self.folder_select_widget.select_button.setText(self.tr(\"Select\"))\n        self.url_label.setText(self.tr(\"Link to the external table: \"))\n        self.url_input.setPlaceholderText(self.tr(\"Enter or paste URL\"))\n\n        self.help_text.setText(\n            self.tr(\"Paste your Google Sheet or csv link to create a local copy in the Open Data Editor\")\n        )\n        self.paste_button.setText(self.tr(\"Add\"))\n        self.tab_widget.setTabText(0, self.tr(\"Add Local Files\"))\n        self.tab_widget.setTabText(1, self.tr(\"Add External Data\"))\n"
  },
  {
    "path": "src/ode/file.py",
    "content": "import json\nimport shutil\nimport logging\nimport xlrd\nimport openpyxl\n\nfrom frictionless import system\nfrom frictionless.resources import TableResource\nfrom frictionless.formats.excel import ExcelControl\nfrom pathlib import Path\n\nfrom ode import paths\n\nlogger = logging.getLogger(__name__)\n\n\nclass File:\n    \"\"\"Class to interact with a File and it's associated Metadata.\n\n    In ODE, every file has a corresponding metadata file that stores Fricionless Metadata\n    and any other metadata required by ODE. All metadata files are going to be stored in a\n    `.metadata` folder mimicing the file name and the structure of the project folder.\n\n    Everytime the name or the location of the file changes, we need to update it's metadata file\n    as well, so this class will implement all methods required to keep synchronized all the information.\n\n    Metadata file example:\n        {\n          \"resource\": \"{...frictionless descriptor...}\"\n          \"custom_ode_metadata\": \"custom_ode_metadata_value\"\n        }\n    \"\"\"\n\n    def __init__(self, path: str | Path, sheet_name: str | None = None) -> None:\n        self.path: Path = path if isinstance(path, Path) else Path(path)\n        self.metadata_path: Path = self._get_path_to_metadata_file(self.path, sheet_name)\n\n    def get_metadata_dict(self, metadata_path) -> dict:\n        \"\"\"Returns the ODE metadata dictionary for the current file.\n\n        The difference with get_or_create_metadata is that this method will not return\n        a Frictionless TableResource object in the record key, just a JSON object.\n        \"\"\"\n        with open(metadata_path) as file:\n            metadata = json.load(file)\n        return metadata\n\n    def set_metadata_dict(self, metadata_path: Path, metadata: dict) -> None:\n        with open(metadata_path, mode=\"w\") as file:\n            json.dump(metadata, file)\n\n    def _get_path_to_metadata_file(self, path: Path, sheet_name: str | None = None) -> Path:\n        \"\"\"Returns the path to the metadata file of the given file.\n\n        Example 1:\n          - File: Paths.PROJECT_FOLDER / 'myfile.csv'\n          - Metadata: Paths.PROJECT_FOLDER / '.metadata/myfile.json'\n\n        Example 2 (subfolder):\n          - File: Paths.PROJECT_FOLDER / 'subfolder/invalid-file.csv'\n          - Metadata: Paths.PROJECT_FOLDER / '.metadata/subfolder/invalid-file.json'\n\n        Example 3 (input is folder):\n          - Folder: Paths.PROJECT_FOLDER / 'subfolder'\n          - Metadata: Paths.PROJECT_FOLDER / '.metadata/subfolder'\n        \"\"\"\n        relative_path = path.parent.relative_to(paths.PROJECT_PATH)\n        metadata_path = paths.METADATA_PATH / relative_path\n\n        if path.is_dir():\n            return metadata_path / path.stem\n\n        if sheet_name:\n            # If the file is an Excel file and a sheet name is provided, we include the sheet name\n            # in the metadata filename to differentiate between different sheets of the same file.\n            safe_sheet_name = \"\".join(c if c.isalnum() else \"_\" for c in sheet_name)\n            return metadata_path / (path.stem + f\"_sheet_{safe_sheet_name}\" + \".json\")\n        else:\n            return metadata_path / (path.stem + \".json\")\n\n    def get_or_create_metadata(self, sheet_name: str | None = None):\n        \"\"\"Get or create a metadata object for the Resource.\n\n        Sheet name is used to specify which sheet of the Excel file we want to use.\n        \"\"\"\n        if self.metadata_path.exists():\n            metadata = dict()\n            with open(self.metadata_path) as file:\n                metadata = json.load(file)\n\n            with system.use_context(trusted=True):\n                if sheet_name:\n                    logger.info(\"Using sheet %s for resource %s\", sheet_name, self.path)\n                    resource = TableResource(metadata[\"resource\"], control=ExcelControl(sheet=sheet_name))\n                else:\n                    logger.info(\"Using resource %s\", self.path)\n                    resource = TableResource(metadata[\"resource\"])\n\n                resource.infer()\n                metadata[\"resource\"] = resource\n        else:\n            # If the metadata file does not exist, we create it.\n            metadata = self._setup_metadata_first_time(sheet_name)\n\n        return metadata\n\n    def _setup_metadata_first_time(self, sheet_name: str | None = None):\n        \"\"\"\n        Set up the metadata for the first time when the file is opened.\n        \"\"\"\n        metadata = dict()\n        self.metadata_path.parent.mkdir(parents=True, exist_ok=True)\n        with system.use_context(trusted=True):\n            if sheet_name:\n                resource = TableResource(self.path, control=ExcelControl(sheet=sheet_name))\n            else:\n                resource = TableResource(self.path)\n\n            resource.infer()\n\n        with open(self.metadata_path, \"w\") as f:\n            # Resource is not serializable, converting to dict before writing.\n            metadata[\"resource\"] = resource.to_descriptor()\n            json.dump(metadata, f)\n\n        # We want to return a Frictionless object, so we are plugging it back.\n        metadata[\"resource\"] = resource\n        return metadata\n\n    def rename(self, new_name, sheet_names: list[str] | None = None):\n        \"\"\"Rename a file and the corresponding metadata file.\n\n        Whenever we rename files we need to update a) the name of the metadata file and\n        b) the Frictionless path attribute. When renaming a folder, we need to ensure that\n        every metadata file of children files are updated as well.\n        \"\"\"\n        new_path = self.path.with_stem(new_name)\n        old_path = self.path\n\n        if new_path.exists():\n            raise OSError\n\n        self.path.rename(new_path)\n        self.path = new_path\n\n        if sheet_names:\n            first = True\n            # If we are renaming an Excel file, we need to create a metadata file for each sheet.\n            for sheet_name in sheet_names:\n                old_metadata_path_with_sheet_name = self._get_path_to_metadata_file(old_path, sheet_name)\n                new_metadata_path_with_sheet_name = self._get_path_to_metadata_file(new_path, sheet_name)\n                self.rename_metadata_file(\n                    old_metadata_path_with_sheet_name, new_metadata_path_with_sheet_name, old_path, new_path\n                )\n\n                if first:\n                    self.metadata_path = new_metadata_path_with_sheet_name\n                    first = False\n        else:\n            new_metadata_path = self.metadata_path.with_stem(new_name)\n            self.rename_metadata_file(self.metadata_path, new_metadata_path, old_path, new_path)\n            self.metadata_path = new_metadata_path\n\n    def rename_metadata_file(self, old_metadata_path: Path, new_metadata_path: str, old_path: Path, new_path: Path):\n        \"\"\"Rename the metadata file and update the path attribute in the metadata.\"\"\"\n        # First we update metadata's path attribute to point to the renamed file/folder\n        if old_metadata_path.is_file():\n            metadata = self.get_metadata_dict(old_metadata_path)\n            metadata[\"resource\"][\"path\"] = str(new_path)\n            self.set_metadata_dict(new_metadata_path, metadata)\n            old_metadata_path.unlink()\n        elif old_metadata_path.is_dir():\n            # If we are renaming a directory, we need to update all existing metadata files.\n            for file in old_metadata_path.rglob(\"*.json\"):\n                metadata = self.get_metadata_dict(file)\n                # When renaming a directory the filename remains but we need to replace its\n                # parent directory.\n                current = metadata[\"resource\"][\"path\"]\n                metadata[\"resource\"][\"path\"] = current.replace(str(old_path), str(new_path))\n                self.set_metadata_dict(file, metadata)\n            old_metadata_path.rename(new_metadata_path)\n\n    def remove(self, sheet_names: list[str] | None = None):\n        \"\"\"Remove a file from disk.\n\n        When uploading a folder, the metadata folder does not exist until the first children file\n        is open and validated. If the user uploads a folder and do not open any file,\n        we will not have a metadata folder. We check if it exist before deleting to ignore errors.\n        \"\"\"\n        if self.path.is_file():\n            self.path.unlink()\n            if sheet_names:\n                for sheet_name in sheet_names:\n                    metadata_path_with_sheet_name = self._get_path_to_metadata_file(self.path, sheet_name)\n                    if metadata_path_with_sheet_name.exists():\n                        metadata_path_with_sheet_name.unlink()\n            elif self.metadata_path.exists():\n                self.metadata_path.unlink()\n        elif self.path.is_dir():\n            shutil.rmtree(self.path)\n            if self.metadata_path.exists():\n                shutil.rmtree(self.metadata_path)\n\n    @staticmethod\n    def get_sheets_names(filepath: Path) -> list[str]:\n        \"\"\"Get the names of the sheets in an Excel file.\"\"\"\n        sheet_names = []\n        if filepath.suffix == \".xls\":\n            workbook = xlrd.open_workbook(str(filepath))\n            sheet_names = workbook.sheet_names()\n        elif filepath.suffix in [\".xlsx\"]:\n            workbook = openpyxl.load_workbook(filepath, read_only=True)\n            sheet_names = workbook.sheetnames\n\n        return sheet_names\n"
  },
  {
    "path": "src/ode/llama.py",
    "content": "import sys\nimport os\nimport logging\nfrom pathlib import Path\nfrom typing import NamedTuple\nfrom enum import Enum\n\nfrom llama_cpp import Llama\nfrom PySide6.QtWidgets import (\n    QDialog,\n    QVBoxLayout,\n    QTextEdit,\n    QPushButton,\n    QLabel,\n    QHBoxLayout,\n    QMessageBox,\n    QProgressDialog,\n    QComboBox,\n    QWidget,\n)\nfrom PySide6.QtCore import QThread, Signal, QObject, QSaveFile, QIODevice, Slot, Qt\nfrom PySide6.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkAccessManager\n\nfrom ode.paths import AI_MODELS_PATH\n\nif not os.path.exists(AI_MODELS_PATH):\n    os.makedirs(AI_MODELS_PATH)\n\nlogger = logging.getLogger(__name__)\n\n\nclass PromptKeys(Enum):\n    SELECT = \"select\"\n    COLUMNS = \"columns\"\n    ANALYSIS = \"analysis\"\n\n\nclass AIModel(NamedTuple):\n    \"\"\"Data structure to hold AI model information.\"\"\"\n\n    name: str\n    url: str\n    filename: str\n\n\nAI_MODEL = AIModel(\n    name=\"Llama 3.2 3B (2.0GB)\",\n    url=\"https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-Q4_K_M.gguf?download=true\",\n    filename=\"Llama-3.2-3B-Instruct-Q4_K_M.gguf\",\n)\n\n\nclass LlamaWorkerSignals(QObject):\n    \"\"\"Define the signals for the LlamaWorker.\"\"\"\n\n    finished = Signal()\n    error = Signal(str)\n    stream_token = Signal(str)\n    stream_token_first_received = Signal()\n    started = Signal()\n\n\nclass LlamaWorker(QThread):\n    \"\"\"\n    LlamaWorker is a QThread that processes a prompt using the LLM with streaming.\n    \"\"\"\n\n    def __init__(self, llm, prompt):\n        super().__init__()\n        self.llm = llm\n        self.prompt = prompt\n        self.signals = LlamaWorkerSignals()\n\n    def run(self):\n        try:\n            self.signals.started.emit()\n            messages = [\n                {\n                    \"role\": \"system\",\n                    \"content\": \"You are an English-speaking Data Analyst in charge of reviewing and improving the quality of tabular files. You are concise and only reply with the answer, nothing else. You always respond in English, regardless of the input language.\",\n                },\n                {\n                    \"role\": \"user\",\n                    \"content\": self.prompt,\n                },\n            ]\n            first_response = True\n            for output in self.llm.create_chat_completion(messages, max_tokens=-1, stream=True, temperature=0.2):\n                token = output[\"choices\"][0][\"delta\"].get(\"content\", \"\")\n\n                if first_response:\n                    self.signals.stream_token_first_received.emit()\n                    first_response = False\n\n                self.signals.stream_token.emit(token)\n\n            self.signals.finished.emit()\n        except Exception as e:\n            logger.error(\"Error during LLM processing\", exc_info=True)\n            self.signals.error.emit(str(e))\n\n\nclass LlamaDialog(QDialog):\n    \"\"\"\n    Dialog for interacting with the LLM.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.llm = None\n        self.worker = None\n        self.init_ui()\n        self.data = None\n        self.prompt = None\n\n        # Block the AI Assistant window until the dialog is closed\n        self.setWindowModality(Qt.WindowModality.ApplicationModal)\n\n    def closeEvent(self, event):\n        \"\"\"Handle the close event to clear the output text.\"\"\"\n        if self.worker and self.worker.isRunning():\n            self.worker.terminate()\n            self.worker.wait()\n        self.prompt_selector.setCurrentIndex(0)\n        self.output_text.clear()\n        self.on_execution_finished()\n        event.accept()\n        super().closeEvent(event)\n\n    def init_ui(self):\n        \"\"\"Initialize the UI for the Llama dialog.\"\"\"\n        layout = QVBoxLayout(self)\n\n        self.prompt_label = QLabel(\n            \"The AI assistant currently support two use cases. Please, select one of the following options:\"\n        )\n        layout.addWidget(self.prompt_label)\n\n        self.prompt_selector = QComboBox()\n        options = [\n            (\"Please select a use case\", PromptKeys.SELECT.value),\n            (\"Generate descriptions for columns\", PromptKeys.COLUMNS.value),\n            (\"Suggest questions for data analysis\", PromptKeys.ANALYSIS.value),\n        ]\n        for i, (text, key) in enumerate(options):\n            self.prompt_selector.addItem(text)\n            self.prompt_selector.setItemData(i, key)\n\n        layout.addWidget(self.prompt_selector)\n\n        self.btn_is_running = False\n        self.btn_run = QPushButton()\n        self.btn_run.setEnabled(False)\n        self.btn_run.clicked.connect(self.run)\n        layout.addWidget(self.btn_run)\n\n        self.btn_stop = QPushButton()\n        self.btn_stop.clicked.connect(self.stop)\n        self.btn_stop.setVisible(False)\n        layout.addWidget(self.btn_stop)\n\n        self.output_text = QTextEdit()\n        self.output_text.setReadOnly(True)\n        self.output_text.setMinimumHeight(500)\n        self.output_text.setMinimumWidth(700)\n        layout.addWidget(self.output_text)\n\n        self.prompt_text_label = QLabel(\n            \"This answer is AI generated. We recommend users to check responses to make sure they are in accordance with the table's data\"\n        )\n        layout.addWidget(self.prompt_text_label)\n\n        self.prompt_selector.activated.connect(self.set_prompt)\n\n        self.retranslateUI()\n\n    def set_data(self, data):\n        \"\"\"Set the data for analysis.\"\"\"\n        self.data = data\n\n    def _get_columns_prompt(self):\n        headers = [str(h) for h in self.data[0] if h is not None and h != \"\"]\n        prompt = f\"\"\"# Explain the column names\nBelow is the metadata and sample data of a dataset that you will suggest columns descriptions.\n\n## Describe what each columns means in the context of the dataset\n 1. Please provide a description for each column name.\n 2. Assume the user does not know anything about the topic.\n 3. Use plain language and be verbose when explaining what data the column contains.\n 4. If there are technical terms, expand the description to explain what that term means in the context of the dataset.\n 5. Use the content of the First 5 rows to gain context about the columns so you answer is more accurate.\n\n# Metadata\nCurrent column names: {\" | \".join(headers)}\nFirst 5 rows: {self.data[1:6]}\n\"\"\"\n\n        return prompt\n\n    def _get_analysis_prompt(self):\n        headers = [str(h) for h in self.data[0] if h is not None and h != \"\"]\n        prompt = f\"\"\"# Understand the Data:\nBelow is the metadata and sample data of a dataset that you will suggest analysis for.\n\n## Create questions that cover a wide range of analytical techniques:\n  1. Descriptive Statistics: (e.g., counts, averages, distributions, min/max)\n  2. Trend Analysis: (e.g., over time, across categories)\n  3. Relationship & Correlation: (e.g., between two numeric columns)\n  4. Segmentation & Comparison: (e.g., comparing groups based on a category)\n  5. Aggregation & Binning: (e.g., using groups and summary functions)\n  6. Data Quality & Anomalies: (e.g., missing values, outliers)\n\n## Tailor the Questions:\nThe questions must be specific to the provided column names and inferred context. Do not generate generic questions that could apply to any dataset.\n\n## Value of the Question:\nFor each question you would add a sentence providing what useful information could be get out of the answer and why you consider the question important.\n\n# Metadata:\n 1. Column names: {\" | \".join(headers)}\n 2. First 5 rows: {self.data[1:6]}\n\"\"\"\n        return prompt\n\n    def set_prompt(self, index):\n        \"\"\"Set the prompt\"\"\"\n        key = self.prompt_selector.itemData(index)\n        self.prompt = \"No prompt selected.\"\n        self.output_text.clear()\n        if key == PromptKeys.SELECT.value:\n            self.prompt = None\n            self.btn_run.setEnabled(False)\n            return\n\n        if key == PromptKeys.COLUMNS.value:\n            self.prompt = self._get_columns_prompt()\n        elif key == PromptKeys.ANALYSIS.value:\n            self.prompt = self._get_analysis_prompt()\n\n        if not self.btn_is_running:\n            self.btn_run.setEnabled(True)\n\n    def init_llm(self, model_path):\n        \"\"\"Initialize the LLM with the given model path.\"\"\"\n        cores = self._calculate_half_cpu_count()\n        self.llm = Llama(\n            model_path=model_path,\n            n_ctx=4096,\n            # chat_format=\"llama-3\",  # TODO: Understand if this is being inferred correctly from the model metadata.\n            verbose=False,  # Change to True for verbose output when running the model in development.\n            seed=4294967295,  # Copied from llama.cpp server.\n            n_threads=cores,\n            n_threads_batch=cores,\n        )\n\n    def run(self):\n        \"\"\"Run the LLM with the selected prompt.\"\"\"\n        if self.llm is None or self.data is None:\n            return\n\n        self.output_text.clear()\n\n        self.btn_run.setEnabled(False)\n\n        self.worker = LlamaWorker(self.llm, self.prompt)\n        self.worker.signals.started.connect(self.on_execution_started)\n        self.worker.signals.finished.connect(self.on_execution_finished)\n        self.worker.signals.error.connect(self.on_execution_error)\n        self.worker.signals.stream_token.connect(self.on_stream_token)\n        self.worker.signals.stream_token_first_received.connect(self.on_stream_token_first_received)\n        self.worker.start()\n\n    def stop(self):\n        \"\"\"Stop the current execution.\"\"\"\n        if self.worker and self.worker.isRunning():\n            self.worker.terminate()\n            self.worker.wait()\n        self.on_execution_finished()\n\n    def on_execution_started(self):\n        \"\"\"Handle the start of execution.\"\"\"\n        self.btn_run.setText(self.tr(\"Generating response...\"))\n        self.btn_is_running = True\n        self.prompt_selector.setEnabled(False)\n\n    def on_execution_finished(self):\n        \"\"\"Handle the completion of execution.\"\"\"\n        self.btn_run.setText(self.tr(\"Execute\"))\n        self.btn_is_running = False\n        self.btn_run.setEnabled(True)\n        self.btn_stop.setVisible(False)\n        self.btn_run.setVisible(True)\n        self.prompt_selector.setEnabled(True)\n\n    def on_execution_error(self, error_msg):\n        \"\"\"Handle execution errors.\"\"\"\n        self.btn_run.setEnabled(True)\n        self.btn_run.setText(self.tr(\"Execute\"))\n        QMessageBox.critical(self, self.tr(\"Error\"), error_msg)\n\n    def on_stream_token(self, token):\n        \"\"\"Inserts token and scrolls down to ensure stream is allways visible.\"\"\"\n        self.output_text.insertPlainText(token)\n        self.output_text.ensureCursorVisible()\n\n    def on_stream_token_first_received(self):\n        \"\"\"Handle the first token received event by allowing the user to stop the execution.\"\"\"\n        self.btn_run.setVisible(False)\n        self.btn_stop.setVisible(True)\n\n    def retranslateUI(self):\n        \"\"\"Retranslate the UI elements.\"\"\"\n        self.setWindowTitle(self.tr(\"AI assistant\"))\n        self.btn_run.setText(self.tr(\"Execute\"))\n        self.btn_stop.setText(self.tr(\"Stop execution\"))\n        self.output_text.setPlaceholderText(self.tr(\"Results will be displayed here...\"))\n\n    def _calculate_half_cpu_count(self) -> int:\n        \"\"\"Returns half of the core number of the current machine.\n\n        By default LLMs use all of the available cores in the machine causing the computer\n        to freeze as it is using all the resources availables. We are limiting to half.\n        \"\"\"\n        cores = os.cpu_count()\n        if cores and isinstance(cores, int):\n            return int(cores / 2)\n        return 4\n\n\nclass LlamaDownloadDialog(QDialog):\n    \"\"\"Dialog for downloading and selecting LLama models.\n\n    Based on: https://doc.qt.io/qtforpython-6/examples/example_network_downloader.html\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.manager = QNetworkAccessManager(self)\n        self.selected_model_path = None\n        self.download_file_path = None\n        self.file = None\n        self.reply = None\n        self.progress_dialog = None\n        self.init_ui()\n\n    def init_ui(self):\n        \"\"\"Initialize the UI for the Llama download dialog.\"\"\"\n        self.setWindowTitle(self.tr(\"AI assistant\"))\n\n        layout = QVBoxLayout(self)\n\n        label_models = QLabel(self.tr(\"To start using the AI assistant, please download the following model.\"))\n        layout.addWidget(label_models)\n\n        label_download_location = QLabel(\n            self.tr(\n                f'The ODE will save the file in this location: <i><a href=\"file://{AI_MODELS_PATH}\">{AI_MODELS_PATH}</a></i>'\n            )\n        )\n        label_download_location.setTextFormat(Qt.TextFormat.RichText)\n        label_download_location.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)\n        label_download_location.linkActivated.connect(self.open_download_directory)\n\n        layout.addWidget(label_download_location)\n\n        # Single model row\n        self.create_model_row(layout)\n\n        # Next button section\n        next_layout = QHBoxLayout()\n        next_layout.addStretch()  # Push button to the right\n\n        self.btn_next = QPushButton(self.tr(\"Next\"))\n        self.btn_next.clicked.connect(self.on_next)\n        self.btn_next.setDefault(True)\n        next_layout.addWidget(self.btn_next)\n\n        layout.addLayout(next_layout)\n\n        # Update button states on startup\n        self.update_ui_state()\n\n    def create_model_row(self, parent_layout):\n        \"\"\"Create the single model row with buttons.\"\"\"\n        # Model row widget\n        model_row = QWidget()\n        model_row.setObjectName(\"llamaDownloadModelRow\")\n        model_layout = QHBoxLayout(model_row)\n        model_layout.setContentsMargins(10, 8, 10, 8)\n\n        # Model name label\n        self.model_label = QLabel()\n        model_layout.addWidget(self.model_label)\n\n        model_layout.addStretch()  # Push buttons to the right\n\n        # Delete button\n        self.btn_delete = QPushButton(self.tr(\"Delete\"))\n        self.btn_delete.setObjectName(\"deleteButton\")\n        self.btn_delete.clicked.connect(self.on_delete_model)\n        model_layout.addWidget(self.btn_delete)\n\n        # Download button\n        self.btn_download = QPushButton(self.tr(\"Download\"))\n        self.btn_download.setObjectName(\"downloadButton\")  # Class name for QSS\n        self.btn_download.clicked.connect(self.on_download_model)\n        model_layout.addWidget(self.btn_download)\n\n        model_layout.setSpacing(10)\n\n        parent_layout.addWidget(model_row)\n\n    def update_ui_state(self):\n        \"\"\"Update UI elements based on download status.\"\"\"\n        is_downloaded = self.is_model_downloaded()\n\n        # Update label text and style\n        status_text = \"Downloaded\" if is_downloaded else \"Not downloaded\"\n        self.model_label.setText(f\"{AI_MODEL.name} ({status_text})\")\n\n        # Update button states and styles\n        self.btn_delete.setEnabled(is_downloaded)\n        self.btn_download.setEnabled(not is_downloaded)\n        self.btn_next.setEnabled(is_downloaded)\n\n    def is_model_downloaded(self):\n        \"\"\"Check if the model is already downloaded.\"\"\"\n        model_path = AI_MODELS_PATH / AI_MODEL.filename\n        return model_path.exists()\n\n    def on_next(self):\n        \"\"\"Continue to next step - use the downloaded model.\"\"\"\n        model_path = AI_MODELS_PATH / AI_MODEL.filename\n        self.selected_model_path = str(model_path)\n        self.accept()\n\n    def open_download_directory(self):\n        \"\"\"Open the directory where models are downloaded.\"\"\"\n        path = str(AI_MODELS_PATH)\n        if sys.platform == \"win32\":\n            os.system(f'explorer.exe /select,\"{Path(path)}\"')\n        elif sys.platform == \"darwin\":\n            os.system(f'osascript -e \\'tell application \"Finder\" to reveal (POSIX file \"{path}\")\\'')\n            os.system(\"osascript -e 'tell application \\\"Finder\\\" to activate'\")\n        else:\n            cmd_run = f'dbus-send --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"{path}\" string:\"\"'\n            os.system(cmd_run)\n\n    @Slot()\n    def on_download_model(self):\n        \"\"\"Download the model.\"\"\"\n        self.download_file_path = AI_MODELS_PATH / AI_MODEL.filename\n\n        if self.download_file_path.exists():\n            ret = QMessageBox.question(\n                self,\n                self.tr(\"File exists\"),\n                self.tr(\"Do you want to delete it and download it again?\"),\n                QMessageBox.Yes | QMessageBox.No,\n            )\n            if ret == QMessageBox.No:\n                return\n            self.download_file_path.unlink()\n\n        # Disable download button during download\n        self.btn_download.setEnabled(False)\n\n        # Create the file in write mode to append bytes\n        self.file = QSaveFile(str(self.download_file_path))\n\n        if self.file.open(QIODevice.OpenModeFlag.WriteOnly):\n            self.reply = self.manager.get(QNetworkRequest(AI_MODEL.url))\n\n            self.progress_dialog = QProgressDialog(self.tr(\"Downloading model\"), self.tr(\"Cancel\"), 0, 0, self)\n            self.progress_dialog.setWindowTitle(self.tr(\"LLM Model Download Progress\"))\n            self.progress_dialog.setAutoClose(True)\n            self.progress_dialog.setWindowModality(Qt.WindowModality.WindowModal)\n            self.progress_dialog.canceled.connect(self.on_download_abort)\n\n            self.reply.downloadProgress.connect(self.on_download_progress)\n            self.reply.finished.connect(self.on_download_finished)\n            self.reply.readyRead.connect(self.on_download_ready_read)\n            self.reply.errorOccurred.connect(self.on_download_error)\n        else:\n            error = self.file.errorString()\n            QMessageBox.warning(self, self.tr(\"Error Occurred\"), error)\n\n    @Slot()\n    def on_delete_model(self):\n        \"\"\"Delete the model file.\"\"\"\n        # Confirm deletion\n        ret = QMessageBox.question(\n            self,\n            self.tr(\"Confirm Deletion\"),\n            self.tr(f\"Are you sure you want to delete {AI_MODEL.name}?\"),\n            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,\n        )\n\n        if ret == QMessageBox.StandardButton.No:\n            return\n\n        model_path = AI_MODELS_PATH / AI_MODEL.filename\n        model_path.unlink(missing_ok=True)\n        self.update_ui_state()\n\n    @Slot()\n    def on_download_abort(self):\n        \"\"\"When user press abort button\"\"\"\n        if self.reply:\n            self.reply.abort()\n        if self.file:\n            self.file.cancelWriting()\n            # cancelWriting should delete the file but it doesn't seems to be happening.\n            if self.download_file_path.exists():\n                self.download_file_path.unlink()\n\n        self.btn_download.setEnabled(True)\n\n    @Slot()\n    def on_download_finished(self):\n        \"\"\"Handle the completion of the download.\"\"\"\n        if self.reply:\n            self.reply.deleteLater()\n\n        if self.file:\n            self.file.commit()\n\n        self.update_ui_state()  # Refresh UI state\n\n    @Slot()\n    def on_download_ready_read(self):\n        \"\"\"Get available bytes and store them into the file\"\"\"\n        if self.reply:\n            if self.reply.error() == QNetworkReply.NetworkError.NoError:\n                self.file.write(self.reply.readAll())\n\n    @Slot(int, int)\n    def on_download_progress(self, bytesReceived: int, bytesTotal: int):\n        \"\"\"Update progress bar\"\"\"\n\n        if self.reply.isOpen() and bytesTotal > 0:\n            # Percentage progress\n            self.progress_dialog.setMaximum(100)\n            percentage = int((bytesReceived / bytesTotal) * 100)\n            self.progress_dialog.setValue(percentage)\n            text = f\"{self.format_size(bytesReceived)} / {self.format_size(bytesTotal)}\"\n            self.progress_dialog.setLabelText(text)\n\n    @Slot(QNetworkReply.NetworkError)\n    def on_download_error(self, code: QNetworkReply.NetworkError):\n        \"\"\"Show a message if an error happen\"\"\"\n        if self.reply:\n            QMessageBox.warning(self, self.tr(\"Error Occurred\"), self.reply.errorString())\n        self.progress_dialog = None\n\n    @staticmethod\n    def format_size(bytes_size: int) -> str:\n        # Convert bytes to human-readable format\n        for unit in [\"B\", \"KB\", \"MB\", \"GB\"]:\n            if bytes_size < 1024.0:\n                return f\"{bytes_size:.2f} {unit}\"\n            bytes_size /= 1024.0\n        return f\"{bytes_size:.2f} TB\"\n\n\nclass LlamaInitWorker(QObject):\n    \"\"\"\n    Worker to initialize the LLM in a separate thread to avoid blocking the UI and show a loading dialog.\n    \"\"\"\n\n    finished = Signal()\n    error = Signal(str)\n    progress = Signal(str)\n\n    def __init__(self, ai_llama, model_path):\n        super().__init__()\n        self.ai_llama = ai_llama\n        self.model_path = model_path\n\n    @Slot()\n    def init_llm(self):\n        try:\n            self.progress.emit(\"Initializing model...\")\n            self.ai_llama.init_llm(self.model_path)\n            self.finished.emit()\n        except Exception as e:\n            self.error.emit(str(e))\n"
  },
  {
    "path": "src/ode/log_setup.py",
    "content": "import logging\nimport sys\nfrom logging.handlers import RotatingFileHandler\n\nfrom PySide6.QtCore import QtMsgType, qInstallMessageHandler\n\nfrom ode import utils\nfrom ode.paths import LOGS_PATH\n\n\ndef configure_logging():\n    \"\"\"Configure logging for the application.\"\"\"\n    # Create the logs directory if it doesn't exist\n    LOGS_PATH.mkdir(parents=True, exist_ok=True)\n\n    root_logger = logging.getLogger()\n    root_logger.setLevel(logging.INFO)\n    formatter = logging.Formatter(\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\")\n\n    # File handler for logging errors\n    file_handler = RotatingFileHandler(\n        LOGS_PATH / \"info.log\",\n        maxBytes=5 * 1024 * 1024,  # 5MB (5 * 1024 * 1024 bytes)\n        backupCount=3,  # 5MB,  Keep 3 backup files\n    )\n    file_handler.setLevel(logging.INFO)\n    file_handler.setFormatter(formatter)\n    root_logger.addHandler(file_handler)\n\n    # Configure the handler for non handled exceptions\n    configure_exception_handling(root_logger)\n\n    return root_logger\n\n\ndef configure_exception_handling(logger):\n    \"\"\"Configure exception handling to log uncaught exceptions.\"\"\"\n\n    # This will always be called when an exception is raised and not handled by the application\n    # The only downside is that it will be executed even if the exception is handled in another thread.\n    def exception_hook(exctype, value, traceback):\n        logger.error(f\"{exctype.__name__}: {value}\", exc_info=(exctype, value, traceback))\n        utils.show_error_dialog(\n            message=f\"An unexpected error occurred: {exctype.__name__}: {value}\",\n            title=\"Error\",\n        )\n        sys._excepthook(exctype, value, traceback)\n\n    # Replace the default exception hook with our custom one\n    # This is necessary to ensure that the original exception hook is called\n    sys._excepthook = sys.excepthook\n    sys.excepthook = exception_hook\n\n    # Set up logging for PyQt/PySide\n    def qt_message_handler(msg_type, context, message):\n        if msg_type == QtMsgType.QtFatalMsg or msg_type == QtMsgType.QtCriticalMsg:\n            logger.error(f\"Qt Error: {message}\")\n\n    qInstallMessageHandler(qt_message_handler)\n\n\ndef get_module_logger(module_name):\n    \"\"\"Get a logger for a specific module.\"\"\"\n    return logging.getLogger(module_name)\n"
  },
  {
    "path": "src/ode/main.py",
    "content": "import logging\nimport os\nimport sys\n\nfrom enum import IntEnum\nfrom importlib.metadata import version\nfrom pathlib import Path\nfrom typing import Callable\n\nfrom PySide6.QtWidgets import (\n    QApplication,\n    QMainWindow,\n    QWidget,\n    QVBoxLayout,\n    QHBoxLayout,\n    QTreeView,\n    QPushButton,\n    QLabel,\n    QStackedLayout,\n    QComboBox,\n    QMenu,\n    QMessageBox,\n    QToolTip,\n    QTextEdit,\n    QSplitter,\n)\n\nfrom PySide6.QtGui import (\n    QPixmap,\n    QIcon,\n    QDesktopServices,\n    QAction,\n    QFont,\n    QPalette,\n    QColor,\n    QShortcut,\n    QKeySequence,\n    QKeyEvent,\n)\nfrom PySide6.QtCore import (\n    Qt,\n    QSize,\n    QFileInfo,\n    QTranslator,\n    QFile,\n    QTextStream,\n    QThreadPool,\n    Slot,\n    Signal,\n    QItemSelectionModel,\n    QEvent,\n    QModelIndex,\n    QStandardPaths,\n    QTimer,\n    QThread,\n)\n\n# https://bugreports.qt.io/browse/PYSIDE-1914\nfrom PySide6.QtWidgets import QFileSystemModel, QDialog\n\nfrom ode import paths\nfrom ode.dialogs.delete import DeleteDialog\nfrom ode.dialogs.llm_dialog_warning import LLMWarningDialog\nfrom ode.dialogs.loading import LoadingDialog\nfrom ode.file import File\nfrom ode.llama import LlamaDialog, LlamaDownloadDialog, LlamaInitWorker\nfrom ode.paths import Paths\nfrom ode.panels.errors import ErrorsWidget\nfrom ode.panels.data import FrictionlessTableModel, DataWorker, DataViewer\nfrom ode.panels.source import SourceViewer\nfrom ode.dialogs.upload import DataUploadDialog\nfrom ode.dialogs.rename import RenameDialog\nfrom ode.dialogs.download import DownloadDialog\nfrom ode.utils import migrate_metadata_store, setup_ode_internal_folders\n\nfrom ode.log_setup import LOGS_PATH, configure_logging\n\nconfigure_logging()\n\nlogger = logging.getLogger(__name__)\nlogger.info(\"Starting Open Data Editor\")\n\n_VERSION = version(\"opendataeditor\")\n\nclass ContentIndex(IntEnum):\n    \"\"\"Enum to represent the index of the content panels.\n    They need to be added in this same order to match the stacked layout indices.\n    \"\"\"\n\n    DATA = 0\n    ERRORS = 1\n    SOURCE = 2\n\n\nclass CustomTreeView(QTreeView):\n    \"\"\"An extended QTreeView to handle custom features for ODE.\n\n    Currently we want the application to show a Welcome widget with an Upload Button\n    whenever the user clicks on the empty space of the QTreeView.\n    \"\"\"\n\n    empty_area_click = Signal()\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n        self.clicked.connect(self.item_clicked)\n\n    def keyPressEvent(self, event: QKeyEvent):\n        \"\"\"Override keyPressEvent to handle expande/collapse folders.\"\"\"\n        if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:\n            index = self.currentIndex()\n            model = self.model()\n            if model and model.hasChildren(index):\n                if self.isExpanded(index):\n                    self.collapse(index)\n                else:\n                    self.expand(index)\n\n        super().keyPressEvent(event)\n\n    def mousePressEvent(self, event):\n        \"\"\"Emits an event if the user clicks on an empty space.\"\"\"\n        index = self.indexAt(event.position().toPoint())\n        if not index.isValid():\n            self.empty_area_click.emit()\n        super().mousePressEvent(event)\n\n    def viewportEvent(self, event):\n        \"\"\"\n        Show a tooltip with the filename when hovering over a file in the file navigator.\n        \"\"\"\n        if event.type() == QEvent.Type.ToolTip:\n            index = self.indexAt(event.pos())\n            if index.isValid():\n                global_pos = event.globalPos()\n                file_path = self.model().filePath(index)\n                filename = Path(file_path).name\n\n                # We cannot change the QToolTip styles through the style.qss file\n                font = QFont()\n                font.setPointSize(14)\n                QToolTip.setFont(font)\n\n                palette = QToolTip.palette()\n                palette.setColor(QPalette.ToolTipBase, QColor(\"#D6D6D6\"))\n                palette.setColor(QPalette.ToolTipText, QColor(\"#333333\"))\n\n                QToolTip.setPalette(palette)\n\n                QToolTip.showText(global_pos, filename)\n\n                # We return True to indicate that we handled the event and stop the propagation\n                return True\n        else:\n            return super().viewportEvent(event)\n\n    def item_clicked(self, index: QModelIndex):\n        \"\"\"\n        Handle the click event of the QTreeView.\n        If the item has children, we want to expand/collapse it when clicked.\n        \"\"\"\n        model = self.model()\n        if model and model.hasChildren(index):\n            if self.isExpanded(index):\n                self.collapse(index)\n            else:\n                self.expand(index)\n\n\nclass ClickableLabel(QLabel):\n    \"\"\"Add a click event to a QLabel.\n\n    We want an interaction when the user clicks on the ODE logo of the sidebar.\n    \"\"\"\n\n    clicked = Signal()\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)\n\n    def mousePressEvent(self, event):\n        self.clicked.emit()\n        super().mousePressEvent(event)\n\n\nclass Sidebar(QWidget):\n    \"\"\"Widget containing the left sidebar of ODE.\n\n    This class is responsible for:\n     - Rendering all the components of the Sidebar.\n     - All the logic of the context menu of the File Navigator.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        layout = QVBoxLayout()\n\n        self.icon_label = ClickableLabel()\n        pixmap = QPixmap(Paths.asset(\"logo.svg\"))\n        self.icon_label.setPixmap(pixmap)\n        self.icon_label.setAlignment(Qt.AlignmentFlag.AlignLeft)\n\n        self.button_upload = QPushButton(objectName=\"button_upload\")\n\n        self.file_navigator = CustomTreeView()\n\n        self.file_model = QFileSystemModel()\n        self.file_navigator.setModel(self.file_model)\n        self.file_navigator.setRootIndex(self.file_model.setRootPath(str(paths.PROJECT_PATH)))\n        self._show_only_name_column_in_file_navigator(self.file_model, self.file_navigator)\n        self.file_navigator.setHeaderHidden(True)\n        self.file_navigator.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)\n        self.file_navigator.customContextMenuRequested.connect(self._show_context_menu)\n        self._setup_file_navigator_context_menu()\n\n        self.user_guide = QPushButton()\n        self.user_guide.setIcon(QIcon(Paths.asset(\"icons/24/open-in-new.svg\")))\n        self.user_guide.setIconSize(QSize(20, 20))\n\n        self.report_issue = QPushButton()\n        self.report_issue.setIcon(QIcon(Paths.asset(\"icons/24/open-in-new.svg\")))\n        self.report_issue.setIconSize(QSize(20, 20))\n\n        self.language = QComboBox()\n        # We are changing since default SizeAdjustPolicy has a buggy behaviour with the Splitter.\n        self.language.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)\n        options = [\n            (\"English\", \"\"),\n            (\"Français\", \"fr\"),\n            (\"Deutsch\", \"de\"),\n            (\"Español\", \"es\"),\n            (\"Português\", \"pt\"),\n            (\"Italiano\", \"it\"),\n        ]\n        language_icon = QIcon(Paths.asset(\"icons/24/language.svg\"))\n        for i, (text, locale) in enumerate(options):\n            self.language.addItem(text)\n            self.language.setItemData(i, locale)\n            self.language.setItemIcon(i, language_icon)\n        self.language.setStyleSheet(\"text-align: left;\")\n\n        layout.addWidget(self.icon_label)\n        layout.addWidget(self.button_upload)\n        layout.addWidget(self.file_navigator)\n        layout.addWidget(self.user_guide)\n        layout.addWidget(self.report_issue)\n        layout.addWidget(self.language)\n\n        self.setLayout(layout)\n\n    def retranslateUI(self):\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.button_upload.setText(self.tr(\"Upload your data\"))\n        self.user_guide.setText(self.tr(\"User guide\"))\n        self.report_issue.setText(self.tr(\"Report an issue\"))\n        self.rename_action.setText(self.tr(\"Rename\"))\n        self.open_location_action.setText(self.tr(\"Open File in Location\"))\n        self.delete_action.setText(self.tr(\"Delete\"))\n\n    def _setup_file_navigator_context_menu(self):\n        \"\"\"Create the context menu for the file navigator.\"\"\"\n        self.context_menu = QMenu(self)\n\n        self.rename_action = QAction()\n        self.open_location_action = QAction()\n        self.delete_action = QAction()\n\n        self.rename_action.triggered.connect(self._rename_file_navigator_item)\n        self.open_location_action.triggered.connect(self._open_file_navigator_location)\n        self.delete_action.triggered.connect(self._delete_file_navitagor_item)\n\n        self.context_menu.addAction(self.rename_action)\n        self.context_menu.addAction(self.open_location_action)\n        self.context_menu.addAction(self.delete_action)\n\n    def _show_context_menu(self, position):\n        \"\"\"Show the context menu for a specific index at the specific position.\"\"\"\n        index = self.file_navigator.indexAt(position)\n        if index.isValid():\n            global_pos = self.file_navigator.viewport().mapToGlobal(position)\n            self.context_menu.exec(global_pos)\n\n    def _rename_file_navigator_item(self):\n        \"\"\"Ask user for the new name for the selected file/folder.\"\"\"\n        index = self.file_navigator.currentIndex()\n        if index.isValid():\n            file = File(self.file_model.filePath(index))\n            name = file.path.stem\n            dialog = RenameDialog(self, name)\n            dialog.exec()\n\n            new_name = dialog.result_text\n            if new_name and new_name != name:\n                try:\n                    sheets_names = None\n                    if file.path.suffix in [\".xls\", \".xlsx\"]:\n                        sheets_names = File.get_sheets_names(file.path)\n                    file.rename(new_name, sheets_names)\n                except IsADirectoryError:\n                    QMessageBox.warning(\n                        self, self.tr(\"Error\"), self.tr(\"Source is a file but destination a directory.\")\n                    )\n                except NotADirectoryError:\n                    QMessageBox.warning(\n                        self, self.tr(\"Error\"), self.tr(\"Source is a directory but destination a file.\")\n                    )\n                except PermissionError:\n                    # Since we have a managed PROJECT_PATH this should never happen.\n                    QMessageBox.warning(self, self.tr(\"Error\"), self.tr(\"Operation not permitted.\"))\n                except OSError:\n                    QMessageBox.warning(self, self.tr(\"Error\"), self.tr(\"File with this name already exists.\"))\n                else:\n                    self.window().statusBar().showMessage(self.tr(\"Item renamed successfuly.\"))\n\n    def _open_file_navigator_location(self):\n        \"\"\"Open the folder where the file lives using the OS application.\"\"\"\n        index = self.file_navigator.currentIndex()\n        if index.isValid():\n            path = self.file_model.filePath(index)\n            if sys.platform == \"win32\":\n                os.system(f'explorer.exe /select,\"{Path(path)}\"')\n            elif sys.platform == \"darwin\":\n                os.system(f'osascript -e \\'tell application \"Finder\" to reveal (POSIX file \"{path}\")\\'')\n                os.system(\"osascript -e 'tell application \\\"Finder\\\" to activate'\")\n            else:\n                cmd_run = f'dbus-send --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"{path}\" string:\"\"'\n                os.system(cmd_run)\n\n    def _delete_file_navitagor_item(self):\n        \"\"\"Delete a file/folder from the file navigator (and the OS).\"\"\"\n        index = self.file_navigator.currentIndex()\n        if index.isValid():\n            file = File(self.file_model.filePath(index))\n            is_selected = self.window().selected_file_path == file.path\n            if DeleteDialog.confirm(self, file.path.name):\n                try:\n                    sheets_names = None\n                    if file.path.suffix in [\".xls\", \".xlsx\"]:\n                        sheets_names = File.get_sheets_names(file.path)\n                    file.remove(sheets_names)\n                except OSError as e:\n                    QMessageBox.warning(self, self.tr(\"Error\"), str(e))\n                else:\n                    if is_selected:\n                        self.window().show_welcome_screen()\n                    self.window().statusBar().showMessage(self.tr(\"Item deleted successfuly.\"))\n\n    def _show_only_name_column_in_file_navigator(self, file_model, file_navigator):\n        \"\"\"Hide all columns except for the name column (column 0)\"\"\"\n        for column in range(file_model.columnCount()):\n            if column != 0:  # 0 is the name column\n                file_navigator.setColumnHidden(column, True)\n\n\nclass ErrorsReportButton(QPushButton):\n    \"\"\"Toolbar button for the Errors Report that contains Icon, Text and ErrorCount.\n\n    QPushButton (Icon+Text) is not enough since we need a three part button: Icon+Text+ErrorCount.\n    In order for the ErrorCount Label to be part of the button (background, hover, clickable) we\n    need to extend the basic QPushButton and override its layout and some methods.\n    \"\"\"\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        layout = QHBoxLayout(self)\n        layout.setSpacing(2)  # Aligns better with QPushButton look & feel\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        self.icon_label = QLabel()\n        self.icon_label.setFixedSize(20, 20)  # Match icon size\n        layout.addWidget(self.icon_label)\n\n        self.text_label = QLabel()\n        layout.addWidget(self.text_label)\n\n        self.error_label = QLabel()\n        self.error_label.setProperty(\"error\", True)  # For referencing in our style.qss file\n        layout.addWidget(self.error_label)\n\n        # This is some Qt Magic to properly display the button and of all its labels\n        # (auto-expanding content-based width)\n        layout.setSizeConstraint(QHBoxLayout.SizeConstraint.SetMinimumSize)\n        self.setLayout(layout)\n\n    def setText(self, text):\n        self.text_label.setText(text)\n        self.updateGeometry()  # Force layout recalc\n\n    def setIcon(self, icon):\n        \"\"\"Overrides the QPushButton method to set the icon to our icon_label.\n\n        A difference with QPushButton is that we handle the size here instead of calling\n        QPushButton.setIconSize().\n        \"\"\"\n        if icon.isNull():\n            self.icon_label.clear()\n        else:\n            pixmap = icon.pixmap(QSize(20, 20))\n            self.icon_label.setPixmap(pixmap)\n        self.updateGeometry()\n\n    def enable(self, number):\n        \"\"\"Enables the button and displays the error number.\n\n        All children labels should also be enabled so we can use QSS pseudo-states for styling.\n        \"\"\"\n        self.setEnabled(True)\n        self.icon_label.setEnabled(True)\n        self.text_label.setEnabled(True)\n        self.error_label.setEnabled(True)\n        if number <= 999:\n            self.error_label.setText(str(number))\n        else:\n            self.error_label.setText(\"+999\")\n        self.error_label.show()\n        self.updateGeometry()\n\n    def disable(self):\n        \"\"\"Disables the button and hides the error number.\n\n        Disabled button will have a grey color and no hover style. All children labels\n        should also be disabled (so we can use QSS pseudo-states for styling)\n        \"\"\"\n        self.setEnabled(False)\n        self.icon_label.setEnabled(False)\n        self.text_label.setEnabled(False)\n        self.error_label.setEnabled(False)\n        self.error_label.hide()\n        self.updateGeometry()\n\n\nclass Toolbar(QWidget):\n    \"\"\"Widget containing ODE's toolbar.\n\n    The toolbar contains:\n     - Buttons that allow the user to navigate between the panels (Data, Metadata, Errors,\n     Source, etc)\n     - Buttons for the main actions like AI, Export and Save.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        layout = QHBoxLayout()\n\n        # Buttons on the left\n        # Setting the cursor to PointingHandCursor to indicate that the button is clickable because\n        # is not working with the style.qss file.\n        self.button_data = QPushButton()\n        self.button_data.setCursor(Qt.CursorShape.PointingHandCursor)\n        self.button_errors = ErrorsReportButton()\n        self.button_errors.setCursor(Qt.CursorShape.PointingHandCursor)\n        self.button_errors.setIcon(QIcon(Paths.asset(\"icons/24/rule.svg\")))\n        self.button_source = QPushButton()\n        self.button_source.setCursor(Qt.CursorShape.PointingHandCursor)\n        self.button_source.setIcon(QIcon(Paths.asset(\"icons/24/code.svg\")))\n        self.button_source.setIconSize(QSize(20, 20))\n        layout.addWidget(self.button_data)\n        layout.addWidget(self.button_errors)\n        layout.addWidget(self.button_source)\n\n        # Excel Sheet Selection\n        self.excel_sheet_layout = QHBoxLayout()\n        self.excel_sheet_container = QWidget()\n        self.excel_sheet_label = QLabel()\n        self.excel_sheet_label.setObjectName(\"excelSheetLabel\")\n\n        self.excel_sheet_combo = QComboBox()\n        self.excel_sheet_combo.setObjectName(\"excelSheetCombo\")\n\n        self.excel_sheet_layout.addWidget(self.excel_sheet_label)\n        self.excel_sheet_layout.addWidget(self.excel_sheet_combo)\n\n        self.excel_sheet_container.setLayout(self.excel_sheet_layout)\n\n        layout.addWidget(self.excel_sheet_container)\n\n        # Spacer to push right-side buttons to the end\n        layout.addStretch()\n\n        # Buttons on the right\n        self.button_ai = QPushButton(objectName=\"button_ai\")\n        self.button_ai.setIcon(QIcon(Paths.asset(\"icons/24/wand.svg\")))\n        self.button_ai.setIconSize(QSize(20, 20))\n        self.button_ai.setFixedWidth(90)\n        self.button_export = QPushButton(objectName=\"button_export\")\n        self.button_export.setIcon(QIcon(Paths.asset(\"icons/24/file-download.svg\")))\n        self.button_export.setIconSize(QSize(20, 20))\n        self.button_export.setEnabled(False)\n        self.button_save = QPushButton(objectName=\"button_save\")\n        self.button_save.setMinimumSize(QSize(117, 35))\n        self.button_save.setIcon(QIcon(Paths.asset(\"icons/24/check.svg\")))\n        self.button_save.setIconSize(QSize(20, 20))\n        self.button_save.setEnabled(False)\n        # self.update_qss_button = QPushButton(\"QSS\")\n        # layout.addWidget(self.update_qss_button)\n        layout.addWidget(self.button_ai)\n        layout.addWidget(self.button_export)\n        layout.addWidget(self.button_save)\n\n        self.setLayout(layout)\n\n    def retranslateUI(self):\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.button_data.setText(self.tr(\"Data\"))\n        self.button_errors.setText(self.tr(\"Errors Report\"))\n        self.button_source.setText(self.tr(\"Source code\"))\n        self.button_export.setText(self.tr(\"Export\"))\n        self.button_save.setText(self.tr(\"Save changes\"))\n        self.button_ai.setText(self.tr(\"AI\"))\n        self.excel_sheet_label.setText(self.tr(\"Sheet:\"))\n\n\nclass Content(QWidget):\n    \"\"\"Widget to display the main section of the ODE.\n\n    This widget represents the main content area of the Open Data Editor. If\n    a file is selected, it will display the Toolbar and Panels. If no file is selected\n    it will display a Welcoming widget with an upload button.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        layout = QVBoxLayout()\n        layout.setSpacing(0)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        self.toolbar = Toolbar()\n        layout.addWidget(self.toolbar)\n\n        self.panels = QWidget(self)\n        self.stacked_layout = QStackedLayout()\n        self.panels.setLayout(self.stacked_layout)\n\n        self.data_view = DataViewer()\n        self.errors_view = ErrorsWidget()\n        self.source_view = SourceViewer()\n        self.ai_llama = LlamaDialog(self)\n\n        self.stacked_layout.addWidget(self.data_view)  # ContentIndex.DATA = 0\n        self.stacked_layout.addWidget(self.errors_view)  # ContentIndex.ERRORS = 1\n        self.stacked_layout.addWidget(self.source_view)  # ContentIndex.SOURCE = 2\n\n        layout.addWidget(self.panels)\n        self.setLayout(layout)\n\n\nclass Welcome(QWidget):\n    \"\"\"Displays an Upload button when no files are selected.\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        main_layout = QVBoxLayout()\n        main_layout.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)\n\n        image_label = QLabel(self)\n        pixmap = QPixmap(Paths.asset(\"images/welcome_screen.png\"))\n        image_label.setPixmap(pixmap)\n        image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.label_top = QLabel()\n        self.label_top.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.label_top.setStyleSheet(\"font-size: 14px; font-weight: 800;\")\n        self.button_upload = QPushButton()\n        self.label_bottom = QLabel()\n        self.label_bottom.setStyleSheet(\"font-size: 14px;\")\n        main_layout.addWidget(image_label)\n        main_layout.addWidget(self.label_top)\n        main_layout.addWidget(self.button_upload)\n        main_layout.addWidget(self.label_bottom)\n\n        self.setLayout(main_layout)\n        self.retranslateUI()\n\n    def retranslateUI(self):\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.label_top.setText(self.tr(\"The ODE supports Excel & csv files\"))\n        self.label_bottom.setText(self.tr(\"You can also add links to online tables\"))\n        self.button_upload.setText(self.tr(\"Upload your data\"))\n\n\nclass MainWindow(QMainWindow):\n    \"\"\"Main Window of the Open Data Editor.\n\n    This class is also the main Controller of the application with two reponsibilites:\n     - Connect signals/slots of all widgets and elements (including children).\n     - Handle custom logic that that requires children interactions with each other.\n\n    The main window is composed by:\n      - A Sidebar with the file navigator and buttons for several actions.\n      - A Main area that can display two widgets:\n        - A Welcome widget if no file is selected.\n        - A Toolbar + Panel widgets if a file is selected.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n\n        self.setWindowTitle(\"Open Data Editor\")\n        icon = QIcon(Paths.asset(\"icons/icon.png\"))\n        self.setWindowIcon(icon)\n\n        self.threadpool = QThreadPool()\n        self.selected_file_path = Path()\n\n        central_widget = QWidget(objectName=\"central_widget\")\n        layout = QHBoxLayout(central_widget)\n        self.setCentralWidget(central_widget)\n\n        splitter = QSplitter(Qt.Orientation.Horizontal)\n        layout.addWidget(splitter)\n\n        self.sidebar = Sidebar()\n\n        self.main = QWidget()\n        self.main_layout = QStackedLayout()\n        self.welcome = Welcome()\n        self.content = Content()\n        self.main_layout.addWidget(self.welcome)\n        self.main_layout.addWidget(self.content)\n        self.main.setLayout(self.main_layout)\n\n        splitter.addWidget(self.sidebar)\n        splitter.addWidget(self.main)\n\n        # Set splitter proportions (15% for sidebar)\n        splitter.setSizes([int(self.width() * 0.15), int(self.width() * 0.85)])\n\n        self._menu_bar()\n\n        # Handle Slot/Signals\n        self.sidebar.button_upload.clicked.connect(self.on_button_upload_click)\n        self.welcome.button_upload.clicked.connect(self.on_button_upload_click)\n        self.sidebar.file_navigator.clicked.connect(self.on_tree_click)\n        self.sidebar.file_navigator.activated.connect(self.on_tree_click)\n        self.sidebar.user_guide.clicked.connect(self.open_user_guide)\n        self.sidebar.report_issue.clicked.connect(self.open_report_issue)\n        self.sidebar.language.activated.connect(self.on_language_change)\n\n        self.content.toolbar.button_export.clicked.connect(self.on_export_click)\n        self.content.toolbar.button_save.clicked.connect(self.on_save_click)\n        self.content.toolbar.button_ai.clicked.connect(self.on_ai_click)\n        self.content.toolbar.button_data.clicked.connect(lambda: self.change_active_panel(ContentIndex.DATA))\n        self.content.toolbar.button_errors.clicked.connect(lambda: self.change_active_panel(ContentIndex.ERRORS))\n        self.content.toolbar.button_source.clicked.connect(lambda: self.change_active_panel(ContentIndex.SOURCE))\n\n        self.content.toolbar.excel_sheet_combo.currentTextChanged.connect(self.on_excel_sheet_selection_changed)\n\n        self.content.data_view.on_save.connect(self.on_data_view_save)\n\n        self.sidebar.file_navigator.empty_area_click.connect(self.show_welcome_screen)\n        self.sidebar.icon_label.clicked.connect(self.show_welcome_screen)\n\n        # Shortcuts\n        self.shortcut_f5 = QShortcut(QKeySequence(\"F5\"), self)\n        self.shortcut_f5.activated.connect(self.on_ai_click)\n\n        # Data Panel\n        self.shortcut_alt_d = QShortcut(QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_D), self)\n        self.shortcut_alt_d.activated.connect(lambda: self.change_active_panel(ContentIndex.DATA))\n\n        # Errors Panel\n        self.shortcut_alt_r = QShortcut(QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_R), self)\n        self.shortcut_alt_r.activated.connect(lambda: self.change_active_panel(ContentIndex.ERRORS))\n\n        # Source Panel\n        self.shortcut_alt_s = QShortcut(QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_S), self)\n        self.shortcut_alt_s.activated.connect(lambda: self.change_active_panel(ContentIndex.SOURCE))\n\n        # Save\n        if sys.platform == \"darwin\":\n            self.shortcut_control_s = QShortcut(QKeySequence(Qt.MetaModifier | Qt.Key_S), self)\n        else:\n            self.shortcut_control_s = QShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_S), self)\n\n        self.shortcut_control_s.activated.connect(self.on_save_click)\n\n        # Translation\n        self.translator = QTranslator()\n        self.retranslateUI()\n\n        # self.content.toolbar.update_qss_button.clicked.connect(self.apply_stylesheet)\n        self.apply_stylesheet()\n\n        self._create_status_bar()\n\n    def _create_status_bar(self):\n        self.statusBar().showMessage(self.tr(\"Ready.\"))\n\n    def _menu_bar(self):\n        \"\"\"Creates the menu bar and assign all its actions.\n\n        Names and titles are going to be set in retranslateUI.\n        \"\"\"\n\n        # File\n        self.menu_file = QMenu()\n        self.menu_file_add = QMenu()\n\n        self.menu_file_add_action_upload_file = QAction()\n        self.menu_file_add_action_upload_file.triggered.connect(self.upload_data)\n        self.menu_file_add.addAction(self.menu_file_add_action_upload_file)\n\n        self.menu_file_add_action_upload_external_url = QAction()\n        self.menu_file_add_action_upload_external_url.triggered.connect(lambda: self.upload_data(external_first=True))\n        self.menu_file_add.addAction(self.menu_file_add_action_upload_external_url)\n\n        self.menu_file.addMenu(self.menu_file_add)\n        self.menuBar().addMenu(self.menu_file)\n\n        # View\n        self.menu_view = QMenu()\n\n        # By default is disabled because not file is selected\n        self.menu_view.setEnabled(False)\n\n        self.menu_view_action_errors_panel = QAction()\n        self.menu_view_action_errors_panel.triggered.connect(lambda: self.change_active_panel(ContentIndex.ERRORS))\n        self.menu_view.addAction(self.menu_view_action_errors_panel)\n\n        self.menu_view_action_source_panel = QAction()\n        self.menu_view_action_source_panel.triggered.connect(lambda: self.change_active_panel(ContentIndex.SOURCE))\n        self.menu_view.addAction(self.menu_view_action_source_panel)\n\n        self.menuBar().addMenu(self.menu_view)\n\n        # Help\n        self.menu_help = QMenu()\n        self.menuBar().addMenu(self.menu_help)\n\n        self.menu_help_action_user_guide = QAction()\n        self.menu_help_action_user_guide.triggered.connect(self.open_user_guide)\n        self.menu_help.addAction(self.menu_help_action_user_guide)\n\n        self.menu_help_action_report_issue = QAction()\n        self.menu_help_action_report_issue.triggered.connect(self.open_report_issue)\n        self.menu_help.addAction(self.menu_help_action_report_issue)\n\n        self.menu_help_action_show_logs = QAction()\n        self.menu_help_action_show_logs.triggered.connect(self.show_logs_content)\n        self.menu_help.addAction(self.menu_help_action_show_logs)\n\n        self.menu_help_action_about = QAction()\n        self.menu_help_action_about.triggered.connect(self.open_about_dialog)\n        self.menu_help.addAction(self.menu_help_action_about)\n\n    def apply_stylesheet(self):\n        \"\"\"Reads our main style QSS file and applies it to the application.\n\n        Tip: this method can be connected to a button to live reload changes.\n        \"\"\"\n        qss_file = QFile(Paths.asset(\"style.qss\"))\n        qss_file.open(QFile.ReadOnly)\n        qss_content = QTextStream(qss_file).readAll()\n        self.setStyleSheet(qss_content)\n\n    @Slot()\n    def show_welcome_screen(self):\n        \"\"\"Focus on the welcome screen and clear file navigator selection.\"\"\"\n        self.sidebar.file_navigator.selectionModel().clear()\n        self.main_layout.setCurrentIndex(0)\n\n        # No file is selected, disable the View menu\n        self.menu_view.setEnabled(False)\n\n    def on_export_click(self):\n        \"\"\"Handle the click on the Export button.\"\"\"\n        # TODO: we are using a proxy variable to check if the file has errors. We should find a\n        # better state variable for it.\n        has_errors = self.content.toolbar.button_errors.isEnabled()\n        download_dialog = DownloadDialog(self, self.selected_file_path, has_errors)\n        download_dialog.download_data_with_errors.connect(self.on_download_error_file)\n        download_dialog.show()\n\n    def on_data_changed(self):\n        \"\"\"Action that enables the Save Button.\"\"\"\n        self.content.toolbar.button_save.setEnabled(True)\n\n    def on_ai_click(self):\n        \"\"\"Handle the click on the AI button.\"\"\"\n        if not LLMWarningDialog.confirm(self):\n            return\n\n        ai_llama_download = LlamaDownloadDialog(self)\n        if ai_llama_download.exec() == QDialog.DialogCode.Accepted:\n            selected_model = ai_llama_download.selected_model_path\n            if selected_model:\n                self.loading_dialog = LoadingDialog(self)\n\n                self.worker_thread = QThread()\n                self.worker = LlamaInitWorker(self.content.ai_llama, selected_model)\n                # Move to worker thread instead of QThread inheritance to avoid concurrency conflicts\n                self.worker.moveToThread(self.worker_thread)\n\n                # Connecting signals\n                self.worker_thread.started.connect(self.worker.init_llm)\n                self.worker.finished.connect(self.on_llm_init_finished)\n                self.worker.error.connect(self.on_llm_init_error)\n                self.worker.progress.connect(self.loading_dialog.show_message)\n\n                # Signal to clean up the thread when the work is done\n                self.worker.finished.connect(self.worker_thread.quit)\n                self.worker.error.connect(self.worker_thread.quit)\n                self.worker_thread.finished.connect(self.worker_thread.deleteLater)\n\n                # Show the loading dialog and start the thread\n                self.loading_dialog.show_immediately()\n                self.worker_thread.start()\n\n    @Slot()\n    def on_llm_init_finished(self):\n        \"\"\"Callback to be called when the LLM is initialized.\"\"\"\n        self.loading_dialog.accept()\n        self.content.ai_llama.show()\n\n    @Slot(str)\n    def on_llm_init_error(self, error_message):\n        \"\"\"Callback to be called when there is an error initializing the LLM.\"\"\"\n        self.loading_dialog.reject()\n        logger.error(f\"Error initializing the LLM: {error_message}\")\n        QMessageBox.critical(self, self.tr(\"Error\"), self.tr(\"Error initializing the LLM:\\n\") + error_message)\n\n    def change_active_panel(self, panel_index: ContentIndex):\n        \"\"\"Change the active panel in the content area and highlight its toolbar button.\n\n        This method changes the active panel in the content area based on the\n        provided panel index and sets the \"active\" property of the related button in the\n        toolbar.\n        \"\"\"\n        if panel_index < 0 or panel_index >= self.content.stacked_layout.count():\n            raise ValueError(\"Invalid panel index.\")\n\n        self.content.stacked_layout.setCurrentIndex(panel_index)\n\n        buttons = [\n            self.content.toolbar.button_data,  # ContentIndex.DATA = 0\n            self.content.toolbar.button_errors,  # ContentIndex.ERRORS = 1\n            self.content.toolbar.button_source,  # ContentIndex.SOURCE = 2\n        ]\n\n        for i, button in enumerate(buttons):\n            button.setProperty(\"active\", i == panel_index)\n            button.style().polish(button)  # Force the button to update its style\n\n        # For the Errors label we need to check if it is enabled and update its style\n        # to hide the error label styles if we are in the Errors panel.\n        button_error_label = self.content.toolbar.button_errors.error_label\n        hide_styles_for_error_label = panel_index != ContentIndex.ERRORS\n        button_error_label.setProperty(\"error\", hide_styles_for_error_label)\n        button_error_label.style().polish(button_error_label)\n\n    def retranslateUI(self):\n        \"\"\"Set the text of all the UI elements using a translation function.\n\n        retranslateUI is a pattern used in Qt to handle the dynamic updates\n        of languages. All text that needs to be translated needs to be set inside\n        this function so we can call it to refresh the UI.\n\n        This method should be call when:\n          a) The application is load for the first time (MainWindow.__init__)\n          b) Every time the user selects a different language (language ComboBox)\n          c) An event related to language change is fired (like the user changing\n          the language in the OS. Not Implemented yet).\n        \"\"\"\n        # Update translated text for menus\n\n        # File menu\n        self.menu_file.setTitle(self.tr(\"File\"))\n        self.menu_file_add.setTitle(self.tr(\"Add\"))\n        self.menu_file_add_action_upload_file.setText(self.tr(\"File/Folder\"))\n        self.menu_file_add_action_upload_external_url.setText(self.tr(\"External URL\"))\n\n        # View\n        self.menu_view.setTitle(self.tr(\"View\"))\n        self.menu_view_action_errors_panel.setText(self.tr(\"Errors panel\"))\n        self.menu_view_action_source_panel.setText(self.tr(\"Source panel\"))\n\n        # Help\n        self.menu_help.setTitle(self.tr(\"Help\"))\n        self.menu_help_action_user_guide.setText(self.tr(\"User Guide\"))\n        self.menu_help_action_report_issue.setText(self.tr(\"Report an Issue\"))\n        self.menu_help_action_show_logs.setText(self.tr(\"View logs\"))\n        self.menu_help_action_about.setText(self.tr(\"About\"))\n\n        # Hook retranslateUI for main widgets\n        self.sidebar.retranslateUI()\n        self.welcome.retranslateUI()\n        self.content.toolbar.retranslateUI()\n\n        # Hook retranslateUI for all panels (data, errors, metadata, etc)\n        self.content.data_view.retranslateUI()\n        self.content.errors_view.retranslateUI()\n        self.content.source_view.retranslateUI()\n        self.content.ai_llama.retranslateUI()\n\n        self.excel_sheet_name = None\n\n    def on_language_change(self, index):\n        \"\"\"Gets a *.qm translation file and calls retranslateUI.\n\n        Translation files are generated using Qt tools pyside6-lupdate and\n        pyside6-lrelease.\n        \"\"\"\n        locale = self.sidebar.language.itemData(index)\n        app = QApplication.instance()\n        if not locale:\n            app.removeTranslator(self.translator)\n            self.retranslateUI()\n            return\n\n        filename = locale + \".qm\"\n        filepath = Paths.translation(filename)\n        if self.translator.load(filepath):\n            app.installTranslator(self.translator)\n        else:\n            print(f\"Error when loading {filepath} translator file. Fallbacking to English.\")\n            app.removeTranslator(self.translator)\n        self.retranslateUI()\n        self.statusBar().showMessage(self.tr(\"Language changed.\"))\n\n    def upload_data(self, external_first=False):\n        \"\"\"Copy data file to the project folder of ode.\n\n        After successful upload the file should be selected, validated, and displayed.\n        \"\"\"\n        dialog = DataUploadDialog(self, external_first=external_first)\n\n        ok, path = dialog.upload_dialog()\n        if ok and path:\n            self.selected_file_path = path\n            # Calling file_model.index() has a weird side-effect in the QTreeView that will display the new\n            # uploaded file at the end of the list instead of the default alphabetical order.\n            if path.suffix in [\".xls\", \".xlsx\"]:\n                self.excel_sheet_name = File.get_sheets_names(path)[0]\n            else:\n                self.excel_sheet_name = None\n\n            index = self.sidebar.file_model.index(str(path))\n            self.sidebar.file_navigator.selectionModel().select(index, QItemSelectionModel.SelectionFlag.ClearAndSelect)\n            self.read_validate_and_display_file(path)\n\n    def on_button_upload_click(self):\n        self.upload_data()\n\n    def on_save_click(self):\n        \"\"\"Saves changes made in the Table View into the file.\"\"\"\n        self.table_model.write_data(self.selected_file_path, sheet_name=self.excel_sheet_name)\n        # TODO: Since the file is already in memory we should only validate/display to avoid unecessary tasks.\n        self.read_validate_and_display_file(self.selected_file_path)\n        self.statusBar().showMessage(self.tr(\"File and Metadata changes saved.\"))\n\n    def on_data_view_save(self, save_data):\n        \"\"\"\n        Reloads the file and updates the views. when is saved in the data view\n        \"\"\"\n        if save_data:\n            self.table_model.write_data(self.selected_file_path, sheet_name=self.excel_sheet_name)\n\n        self.read_validate_and_display_file(self.selected_file_path)\n\n    @Slot(tuple)\n    def update_views(self, worker_data):\n        \"\"\"Update all the main views with the data provided by the read worker.\n\n        This method is connected to the data widget Worker's signal and it will\n        receive the data, the frictionless report and a list of errors.\n        \"\"\"\n        filepath, data, errors = worker_data\n        self.table_model = FrictionlessTableModel(data, errors)\n        self.table_model.dataChanged.connect(self.on_data_changed)\n        self.content.data_view.display_data(self.table_model, filepath, sheet_name=self.excel_sheet_name)\n        self.content.errors_view.display_errors(errors, self.table_model)\n        self.content.source_view.open_file(filepath)\n        self.content.ai_llama.set_data(data)\n\n        self.update_excel_sheet_dropdown(filepath)\n\n        # Always focus back to the data view.\n        self.main_layout.setCurrentIndex(1)\n        self.change_active_panel(ContentIndex.DATA)\n\n    def update_excel_sheet_dropdown(self, filepath):\n        \"\"\"\n        Update the Excel sheet dropdown with the names of the sheets in the selected file.\n        If there are no sheets, the dropdown and label will be hidden.\n        \"\"\"\n        self.content.toolbar.excel_sheet_combo.blockSignals(True)\n\n        self.content.toolbar.excel_sheet_combo.clear()\n\n        sheets_names = File.get_sheets_names(filepath)\n        if len(sheets_names) > 0:\n            if self.excel_sheet_name is None:\n                self.excel_sheet_name = sheets_names[0]\n\n            # We only show the dropdown if there are multiple sheets\n            if len(sheets_names) > 1:\n                self.content.toolbar.excel_sheet_combo.addItems(sheets_names)\n                self.content.toolbar.excel_sheet_combo.setCurrentText(self.excel_sheet_name)\n                self.content.toolbar.excel_sheet_container.setVisible(True)\n        else:\n            self.content.toolbar.excel_sheet_container.setVisible(False)\n            self.excel_sheet_name = None\n\n        self.content.toolbar.excel_sheet_combo.blockSignals(False)\n\n    def on_excel_sheet_selection_changed(self, sheet_name: str):\n        \"\"\"\n        Handle the change of the selected Excel sheet in the dropdown.\n        Reloads the file with the selected sheet name and updates the views.\n        \"\"\"\n        if self.selected_file_path.suffix in [\".xls\", \".xlsx\"]:\n            self.excel_sheet_name = sheet_name\n            self.read_validate_and_display_file(self.selected_file_path)\n        else:\n            raise ValueError(\"Selected file is not an Excel file.\")\n\n    @Slot(tuple)\n    def update_toolbar(self, worker_data):\n        \"\"\"\n        Updates the toolbar based on the data provided by the read worker.\n\n        This method is connected to the data widget Worker's signal and it will\n        receive the data, the frictionless report and a list of errors.\n        \"\"\"\n        _, _, errors = worker_data\n        errors_count = len(errors)\n\n        # If we don't have errors we don't enable the Errors Report tab.\n        if errors_count == 0:\n            self.content.toolbar.button_errors.disable()\n        else:\n            self.content.toolbar.button_errors.enable(errors_count)\n\n        # Save button should be disabled everytime we load and display a new file.\n        self.content.toolbar.button_save.setEnabled(False)\n\n    @Slot(tuple)\n    def update_menu_bar(self, worker_data):\n        \"\"\"\n        Updates the menu bar based on the data provided by the read worker.\n\n        This method is connected to the data widget Worker's signal and it will\n        receive the data, the frictionless report and a list of errors.\n        \"\"\"\n        self.menu_view.setEnabled(True)\n\n        _, _, errors = worker_data\n        errors_count = len(errors)\n\n        if errors_count == 0:\n            self.menu_view_action_errors_panel.setEnabled(False)\n        else:\n            self.menu_view_action_errors_panel.setEnabled(True)\n\n    def read_validate_and_display_file(self, file_path, fn_callback: Callable | None = None):\n        \"\"\"Reads a file, validates it and refresh the whole UI.\n        This method is called when the user selects a file in our QTreeView but there could\n        be other workflows in the application that will require this logic (like Uploading a File).\n\n        It will create a Worker to read data in the background and display a ProgressDialog if it\n        is taking too long. Reading with a worker is a requirement to display a proper QProgressDialog.\n\n        Args:\n            file_path (Path): The path to the file to read.\n            fn_finished (Callable, optional): A function to call when the worker finishes. Defaults to None.\n                It cannot be a lambda function since it will not be picked and sent to the worker thread.\n        \"\"\"\n        info = QFileInfo(file_path)\n        if info.isFile() and info.suffix() in [\"csv\", \"xls\", \"xlsx\"]:\n            self.loading_dialog = LoadingDialog(self)\n\n            worker = DataWorker(file_path, self.excel_sheet_name)\n            worker.signals.finished.connect(self.update_views)\n            worker.signals.finished.connect(self.update_toolbar)\n            worker.signals.finished.connect(self.update_menu_bar)\n            worker.signals.finished.connect(self.loading_dialog.close)\n            worker.signals.finished.connect(self.loading_dialog.cancel_loading_timer)\n\n            if fn_callback:\n                worker.signals.finished.connect(fn_callback)\n\n            worker.signals.messages.connect(self.statusBar().showMessage)\n            worker.signals.messages.connect(self.loading_dialog.show_message)\n\n            self.threadpool.start(worker)\n            self.loading_dialog.show()\n\n    def on_tree_click(self, index):\n        \"\"\"Handles the click action of our File Navigator.\"\"\"\n        self.selected_file_path = Path(self.sidebar.file_model.filePath(index))\n        if self.selected_file_path.is_file():\n            # Reset the excel sheet name to None to avoid displaying the previous file's sheet\n            if self.selected_file_path.suffix in [\".xls\", \".xlsx\"]:\n                self.excel_sheet_name = File.get_sheets_names(self.selected_file_path)[0]\n            else:\n                self.excel_sheet_name = None\n\n            self.read_validate_and_display_file(self.selected_file_path)\n            self.content.toolbar.button_export.setEnabled(True)\n        else:\n            self.content.toolbar.button_export.setEnabled(False)\n\n    def open_about_dialog(self):\n        text = f\"Version: {_VERSION}<br><a href='https://opendataeditor.okfn.org'>Website</a>\"\n        QMessageBox.about(self, \"Open Data Editor\", text)\n\n    def open_user_guide(self):\n        QDesktopServices.openUrl(\"https://opendataeditor.okfn.org/documentation/welcome\")\n\n    def open_report_issue(self):\n        QDesktopServices.openUrl(\"https://github.com/okfn/opendataeditor\")\n\n    # Then define the function that will be executed when the action is triggered\n    def show_logs_content(self):\n        file_path = LOGS_PATH / \"info.log\"\n\n        try:\n            # Read only the last 40 lines of the file\n            with open(file_path, \"r\", encoding=\"utf-8\") as file:\n                # Read all lines and store in a list\n                all_lines = file.readlines()\n                # Get the last 40 lines (or all if less than 40)\n                last_lines = all_lines[-100:] if len(all_lines) > 40 else all_lines\n                # Join the lines into a single string\n                content = \"\".join(last_lines)\n\n            # Create a dialog to show the content\n            dialog = QDialog(self)\n            dialog.setWindowTitle(self.tr(\"Last 100 Lines\"))\n            dialog.resize(900, 500)\n\n            # Create a layout for the dialog\n            layout = QVBoxLayout(dialog)\n\n            # Create a text widget to display the content\n            text_edit = QTextEdit()\n            font = QFont(\"Courier New\")\n            font.setStyleHint(QFont.StyleHint.Monospace)\n            text_edit.setFont(font)\n            text_edit.setReadOnly(True)\n            text_edit.setText(content)\n\n            layout.addWidget(text_edit)\n\n            # Create a horizontal layout for buttons\n            button_layout = QHBoxLayout()\n\n            # Close button\n            close_button = QPushButton(self.tr(\"Close\"))\n            close_button.clicked.connect(dialog.close)\n            button_layout.addWidget(close_button)\n\n            # Copy button\n            copy_button = QPushButton(self.tr(\"Copy to Clipboard\"))\n            copy_button.clicked.connect(lambda: QApplication.clipboard().setText(text_edit.toPlainText()))\n            button_layout.addWidget(copy_button)\n\n            layout.addLayout(button_layout)\n\n            # Show the dialog\n            dialog.exec()\n\n        except Exception as e:\n            QMessageBox.critical(self, \"Error\", f\"Could not open file: {str(e)}\")\n\n    def on_download_error_file(self):\n        \"\"\"\n        Downloads the file with errors to the user's Downloads folder.\n        \"\"\"\n        self.table_model.finished.connect(self.loading_dialog.close)\n        self.table_model.finished.connect(self.loading_dialog.cancel_loading_timer)\n        self.loading_dialog.show_message(self.tr(\"Downloading data with errors...\"))\n        # We are showing the dialog instantly without waiting the 300ms delay\n        self.loading_dialog.show(0)\n\n        # We use QTimer to ensure the download is performed after the dialog is shown\n        QTimer.singleShot(100, self._perform_download)\n\n    def _perform_download(self):\n        \"\"\"\n        Performs the actual download of the file with errors to the user's Downloads folder.\n        \"\"\"\n        downloads_path = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation)\n        filename = self.selected_file_path.name\n        # TODO: do no overwrite existing files\n        filepath = Path(downloads_path, filename)\n        filepath = filepath.with_stem(f\"{filepath.stem}_errors\").with_suffix(\".xlsx\")\n        self.table_model.write_error_xlsx(filepath)\n\n        success_text = self.tr(\"File downloaded successfully to:\\n{}\").format(filepath)\n        QMessageBox.information(self, self.tr(\"Success\"), success_text)\n\n\ndef main():\n    app = QApplication(sys.argv)\n    app.setOrganizationName(\"Open Knowledge Foundation\")\n    app.setApplicationName(\"Open Data Editor\")\n    app.setApplicationVersion(_VERSION)\n    app.setStyle(\"Fusion\")\n\n    # Migration to ODE 1.4\n    migrate_metadata_store()\n\n    setup_ode_internal_folders()\n\n    window = MainWindow()\n    window.showMaximized()\n    window.show()\n    sys.exit(app.exec())\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/ode/panels/__init__.py",
    "content": "\"\"\" Open Data Editor main widgets module.\n\nThis module contains the widgets that implements the main functionalities of ODE:\n - data.py: MVC for displaying the file contents.\n - errors.py: Views to display a summary of the errors detected in the data.\n - source.py: Raw view of the file.\n\nThis module should contain high level widgets that implements core functionalities,\nlow level widgets like custom buttons, labels or titles should be implemented among\nthe high level widget that uses it.\n\n\"\"\"\n"
  },
  {
    "path": "src/ode/panels/data.py",
    "content": "import json\nimport csv\nimport logging\nfrom pathlib import Path\nfrom frictionless import system\n\nfrom PySide6.QtCore import Qt, QAbstractTableModel, QObject, Signal, Slot, QRunnable, QRect, QEvent\nfrom PySide6.QtGui import QColor, QIcon, QKeyEvent, QPen\nfrom PySide6.QtWidgets import QWidget, QVBoxLayout, QTableView, QLabel, QApplication, QStyledItemDelegate, QStyle\n\nfrom openpyxl import Workbook, load_workbook\nfrom openpyxl.styles import PatternFill\n\nimport xlwt\nimport xlrd\nfrom xlutils.copy import copy\n\n\nfrom ode import utils\nfrom ode.dialogs.metadata import ColumnMetadataDialog, ColumnMetadataField\nfrom ode.file import File\nfrom ode.shared import COLOR_RED, COLOR_BLUE\nfrom ode.paths import Paths\n\n\nDEFAULT_LIMIT_ERRORS = 1000\n\nlogger = logging.getLogger(__name__)\n\n\nclass DataWorkerSignals(QObject):\n    \"\"\"Define the signals for the DataWorker.\"\"\"\n\n    finished = Signal(tuple)\n    messages = Signal(str)\n\n\nclass DataWorker(QRunnable):\n    \"\"\"Worker to execute all the reading and validation tasks.\n\n    This worker will allow us to read and validate the data (that can take several\n    seconds) in the background. By moving this logic to a Worker we can avoid the\n    application to get freeze while reading and instead display proper messages to\n    the user.\n    \"\"\"\n\n    def __init__(self, filepath, sheet_name=None):\n        super().__init__()\n        self.file = File(filepath, sheet_name)\n        self.signals = DataWorkerSignals()\n        self.sheet_name = sheet_name\n        self.resource = self.file.get_or_create_metadata(sheet_name).get(\"resource\")\n\n    @Slot()\n    def run(self):\n        \"\"\"Reads and validates the data.\n\n        We are using Resource.read_cells() because we want to read the data\n        as is in the file in order to properly show data errors.\n\n        We are using the system context to allow us to read from non-relative paths.\n\n        This method emits a finished signal with all the data that the main UI requires to\n        display the table and the errors.\n        \"\"\"\n        with system.use_context(trusted=True):\n            self.signals.messages.emit(QApplication.translate(\"DataWorker\", \"Reading file...\"))\n            data = self.resource.read_cells()\n            self.signals.messages.emit(QApplication.translate(\"DataWorker\", \"Checking errors...\"))\n            report = self.resource.validate(limit_errors=DEFAULT_LIMIT_ERRORS)\n\n        self.signals.messages.emit(QApplication.translate(\"DataWorker\", \"Drawing table...\"))\n\n        errors = []\n        if not report.valid:\n            try:\n                # report.error is only available for single error report\n                errors.append(report.error)\n            except Exception:\n                errors = report.tasks[0].errors\n\n        self.signals.messages.emit(QApplication.translate(\"DataWorker\", \"Read and error checking finished.\"))\n        self.signals.finished.emit((self.file.path, data, errors))\n\n\nclass ColumnMetadataIconDelegate(QStyledItemDelegate):\n    \"\"\"\n    Custom delegate to render an icon in the first row of the table.\n    \"\"\"\n\n    icon_clicked = Signal(object)\n\n    def __init__(self, icon_path, parent=None):\n        super().__init__(parent)\n        self.icon_size = 14\n        self.icon = QIcon(icon_path)\n\n    def _get_icon_rect(self, option):\n        \"\"\"Get the rectangle where the icon should be painted.\"\"\"\n        return QRect(\n            option.rect.right() - self.icon_size,\n            option.rect.top(),\n            self.icon_size,\n            self.icon_size,\n        )\n\n    def paint(self, painter, option, index):\n        \"\"\"Paint the icon in the first row of the table adds blue background if mouse over.\"\"\"\n        super().paint(painter, option, index)\n        if index.row() == 0:\n            self.icon.paint(painter, self._get_icon_rect(option))\n\n            if option.state & QStyle.StateFlag.State_MouseOver:\n                pen = QPen(COLOR_BLUE)\n                pen.setWidth(3)\n                painter.setPen(pen)\n\n                # We do it like this because it was painting outside the cell\n                rect = option.rect.adjusted(1, 1, -1, -1)\n\n                painter.drawLine(rect.topLeft(), rect.topRight())\n                painter.drawLine(rect.topLeft(), rect.bottomLeft())\n                painter.drawLine(rect.topRight(), rect.bottomRight())\n                painter.drawLine(rect.bottomLeft(), rect.bottomRight())\n\n    def editorEvent(self, event, model, option, index):\n        \"\"\"Handle mouse events for the first row.\"\"\"\n        if index.row() == 0 and event.type() == QEvent.Type.MouseButtonPress:\n            self.icon_clicked.emit(index.column())\n            return True\n\n        return super().editorEvent(event, model, option, index)\n\n\nclass FrictionlessTableModel(QAbstractTableModel):\n    finished = Signal()\n\n    def __init__(\n        self,\n        data=[],\n        errors=[],\n    ):\n        super().__init__()\n        self._data = data\n        self._row_count = self._get_row_count()\n        self.errors = self._get_errors(errors)\n        self._column_count = self._get_column_count()\n\n    def write_data(self, filepath: Path, sheet_name=None):\n        \"\"\"\n        Write the data to a file in the format specified by the file extension.\n        \"\"\"\n        extension = filepath.suffix.lower()\n        if extension == \".csv\":\n            self.write_data_csv(filepath)\n        elif extension == \".xlsx\":\n            self.write_data_xlsx(filepath, sheet_name)\n        elif extension == \".xls\":\n            self.write_data_xls(str(filepath), sheet_name)\n        else:\n            raise ValueError(f\"Unsupported format: {extension}. Use .csv, .xlsx or .xls\")\n\n    def write_data_csv(self, filepath: Path):\n        \"\"\"\n        Write the data to a CSV file.\n        \"\"\"\n        logger.info(f\"Writing data to CSV file: {filepath}\")\n        resource = File(filepath).get_or_create_metadata().get(\"resource\")\n        dialect = resource.dialect.to_dict()\n        csv_config = dialect.get(\"csv\", None)\n\n        # Default delimiter\n        delimiter = \",\"\n        if csv_config and \"delimiter\" in csv_config:\n            delimiter = csv_config[\"delimiter\"]\n\n        with open(filepath, \"w\", newline=\"\", encoding=\"utf-8\") as csvfile:\n            writer = csv.writer(csvfile, delimiter=delimiter)\n            # Write header\n            writer.writerow(self._data[0])\n            # Write data rows\n            writer.writerows(self._data[1:])\n\n        logger.info(f\"Data saved in CSV format: {filepath}\")\n\n    def write_data_xlsx(self, filepath: Path, sheet_name=None):\n        \"\"\"\n        Write the data to an Excel file.\n        \"\"\"\n        logger.info(f\"Writing data to Excel file: {filepath}\")\n\n        wb = load_workbook(filepath)\n\n        if sheet_name in wb.sheetnames:\n            ws = wb[sheet_name]\n            logger.info(f\"Deleting existing data in sheet: {sheet_name}\")\n            ws.delete_rows(1, ws.max_row)  # Delete all rows\n        else:\n            logger.error(f\"Sheet {sheet_name} does not exist in the workbook: {filepath}\")\n            raise ValueError(f\"Sheet {sheet_name} does not exist in the workbook: {filepath}\")\n\n        # Header row\n        ws.append(self._data[0])\n\n        # Data rows\n        rows = self._data[1:]\n        for row in rows:\n            ws.append(row)\n\n        wb.save(filepath)\n        logger.info(f\"Data saved in Excel format: {filepath}\")\n\n    def write_data_xls(self, filepath: str, sheet_name=None):\n        \"\"\"\n        Write the data to an Excel XLS file.\n\n        The filepath must be a string because xlwt does not support Path objects.\n        \"\"\"\n        logger.info(f\"Writing data to XLS file: {filepath}\")\n\n        wb = xlwt.Workbook()\n\n        rb = xlrd.open_workbook(filepath, formatting_info=True)\n        try:\n            sheet_name_index = rb.sheet_names().index(sheet_name)\n        except ValueError:\n            raise ValueError(f\"Sheet {sheet_name} does not exist in the workbook: {filepath}\")\n\n        # We use xlutils to transform the xlrd book into an xlwt book\n        # This will allow us to modify existing XLS files\n        wb = copy(rb)\n        ws = wb.get_sheet(sheet_name_index)\n\n        for row_idx, row_data in enumerate(self._data):\n            for col_idx, cell_value in enumerate(row_data):\n                ws.write(row_idx, col_idx, cell_value)\n\n        wb.save(filepath)\n        logger.info(f\"Data saved in XLS format: {filepath}\")\n\n    def write_error_xlsx(self, filepath: Path):\n        \"\"\"\n        Write the errors to an Excel file in the specified directory\n        painting with red the cells with errors.\n        \"\"\"\n        wb = Workbook()\n        data_sheet = wb.active\n        data_sheet.title = self.tr(\"Data\")\n        errors_sheet = wb.create_sheet(\"Errors Description\")\n        errors_sheet.append([\"Row\", \"Column\", \"Error Title\", \"Error Description\"])\n\n        blank_sheet = wb.create_sheet(\"Blank Rows\")\n        blank_sheet.append([\"Row\", \"Error Description\"])\n\n        red_fill = PatternFill(start_color=\"FF0000\", end_color=\"FF0000\", fill_type=\"solid\")\n        errors_cells = list()\n        blank_rows = set()\n\n        for row_index, errors_in_row in enumerate(self.errors):\n            if errors_in_row:\n                for error_column, error_type, error_description in errors_in_row:\n                    if error_type == \"blank-row\":\n                        blank_rows.add(row_index)\n                    else:\n                        errors_cells.append((row_index, error_column, error_type, error_description))\n\n        for row_index, row in enumerate(self._data):\n            data_sheet.append(row)\n\n        errors_cells.sort(key=lambda x: (x[0], x[1]))  # Sort by row and column index\n        for row_index, col_index, error_type, error_description in errors_cells:\n            excel_row = row_index + 1\n            excel_col = col_index + 1\n            error_title = utils.ErrorTexts.get_error_title(error_type)\n            if not error_title:\n                error_title = error_type.replace(\"-\", \" \").title()\n\n            errors_sheet.append(\n                [\n                    excel_row,\n                    excel_col,\n                    error_title,\n                    utils.ErrorTexts.get_error_description(error_type) or error_description,\n                ]\n            )\n\n            # Paint the cell with red if it has an error in the Data Sheet\n            data_sheet.cell(row=excel_row, column=excel_col).fill = red_fill\n\n        for row_index in blank_rows:\n            excel_row = row_index + 1\n\n            blank_sheet.append(\n                [\n                    excel_row,\n                    utils.ErrorTexts.get_error_description(\"blank-row\"),\n                ]\n            )\n\n            # Paint the cell with red if it has an error in the Data Sheet\n            for col_index in range(1, data_sheet.max_column + 1):\n                data_sheet.cell(row=excel_row, column=col_index).fill = red_fill\n\n        wb.save(filepath)\n        self.finished.emit()\n        logger.info(f\"Errors saved in Excel format: {filepath}\")\n\n    def _get_errors(self, errors):\n        \"\"\"Return an array with errors information to use when rendering the table.\n\n        The array has same lenght as our data with None or a Tuple:\n          [None, None, (3, 'blank-label', 'error mesage to be displayed'), None]\n\n        Main actions:\n         - Moves from Frictionless' 1-index to 0-index\n         - Handles inconsistency in Fricionless Error Object API\n         - Builds an array for easy access to error information to render performant tables.\n        \"\"\"\n        result = [None] * self._row_count\n        for error in errors:\n            if error.type == \"source-error\":\n                # SourceError happens with files that cannot be read and do not have row_number nor field_number.\n                return result\n            elif error.type in [\"blank-label\", \"duplicate-label\", \"incorrect-label\", \"missing-label\", \"extra-label\"]:\n                # https://github.com/frictionlessdata/frictionless-py/issues/1710\n                row = error.row_numbers[0] - 1\n                column = error.field_number - 1\n            elif error.type == \"blank-row\":\n                row = error.row_number - 1\n                # BlankRow error does not have field_number\n                column = 0\n            elif not hasattr(error, \"row_number\"):\n                row = None\n                column = None\n            else:\n                row = error.row_number - 1\n                column = error.field_number - 1\n\n            if row is not None:\n                if result[row] is None:\n                    result[row] = list()\n\n                result[row].append((column, error.type, error.message))\n\n        return result\n\n    def _get_row_count(self):\n        return len(self._data)\n\n    def _get_column_count(self):\n        \"\"\"Get the amout of columns.\n\n        We are expecting malformed CSVs, so the amout of columns should always\n        be the size of the longest row.\n        \"\"\"\n        try:\n            return max(map(len, self._data))\n        except ValueError:\n            return 0\n\n    def get_header_data(self):\n        \"\"\"Returns the first row of the file.\"\"\"\n        return self._data[0]\n\n    def rowCount(self, parent=None):\n        \"\"\"Returning from a pre-calculated private attribute for performance improvements.\"\"\"\n        return self._row_count\n\n    def columnCount(self, parent=None):\n        \"\"\"Returning from a pre-calculated private attribute for performance improvements.\"\"\"\n        return self._column_count\n\n    def data(self, index, role):\n        \"\"\"Returns information to be used to render the Data Table View.\n\n        For each cell we return:\n         - The value to be displayed in the cell\n         - If there is an error in that cell, a red color for the Background.\n         - If there is an error in that cell, a message for the tooltip.\n        \"\"\"\n        if not index.isValid():\n            return None\n\n        if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:\n            try:\n                # We convert the data as string to avoid PySide6 treating numbers differently\n                value = self._data[index.row()][index.column()]\n                if value is None:\n                    return \"\"\n                return str(value)\n            except IndexError:\n                # Our data could be irregular (missing columns and rows)\n                # So it is okay to return None and keep iterating.\n                return None\n\n        if not self.errors[index.row()]:\n            return None\n\n        if role == Qt.ItemDataRole.BackgroundRole:\n            for error_column, error_type, _ in self.errors[index.row()]:\n                if error_type == \"blank-row\":\n                    # BlankRowError does not have field_number, we paint all the cells.\n                    return COLOR_RED\n                if error_column == index.column():\n                    return COLOR_RED\n        elif role == Qt.ItemDataRole.ForegroundRole:\n            for error_column, _, _ in self.errors[index.row()]:\n                if error_column == index.column():\n                    return QColor(255, 255, 255)\n\n    def flags(self, index):\n        \"\"\"Enable edition mode\"\"\"\n        if not index.isValid():\n            return Qt.ItemFlag.NoItemFlags\n\n        if index.row() == 0:\n            return super().flags(index)\n\n        return super().flags(index) | Qt.ItemFlag.ItemIsEditable\n\n    def setData(self, index, value, role):\n        \"\"\"Insert the edited value at the specific cell.\n\n        Since the TableView will always have enough rows and enough columns,\n        when editing the raw data we encounter two scenarios:\n          a) The cell in the raw data exist and therefore we just replace\n          b) The cell in the raw data do not exist and therefore we need to\n          create empty cells until we get to the exact column we want to insert.\n        \"\"\"\n        if role == Qt.ItemDataRole.EditRole:\n            currentRow = self._data[index.row()]\n            try:\n                # a) raw cell exist\n                currentRow[index.column()] = value\n            except IndexError:\n                # b) we create empty cells and then insert at specific column\n                currentRow += [None] * (index.column() - len(currentRow))\n                currentRow.insert(index.column(), value)\n            self.dataChanged.emit(index, index)\n            return True\n        return False\n\n\nclass CustomTableView(QTableView):\n    \"\"\"Custom QTableView to handle specific key events.\"\"\"\n\n    on_click_first_row = Signal(object)\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n    def keyPressEvent(self, event: QKeyEvent):\n        \"\"\"\n        We check if the user pressed Enter or Return on the first row of the table.\n        If so, we emit a signal with the column index of the clicked cell.\n        \"\"\"\n        index = self.currentIndex()\n\n        if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter) and index.row() == 0 and index.isValid():\n            self.on_click_first_row.emit(index.column())\n            return\n\n        super().keyPressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        \"\"\"Changes the cursor to Pointing Hand if positing is in first row.\"\"\"\n        index = self.indexAt(event.pos())\n        if index.row() == 0:\n            self.setCursor(Qt.CursorShape.PointingHandCursor)\n        else:\n            self.unsetCursor()\n        return super().mouseMoveEvent(event)\n\n\nclass DataViewer(QWidget):\n    \"\"\"Widget to display the content of tabular data.\"\"\"\n\n    # Signal to notify that the metadata has been saved\n    on_save = Signal(object)\n\n    def __init__(self):\n        super().__init__()\n\n        utils.set_common_style(self)\n\n        layout = QVBoxLayout()\n        self.setLayout(layout)\n\n        self.label = QLabel()\n        self.label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)\n        self.table_view = CustomTableView()\n        self.table_view.on_click_first_row.connect(self.show_column_metadata_dialog)\n        # TableView's corner button hangs the application when working with huge datasets so we disable it.\n        self.table_view.setCornerButtonEnabled(False)\n        self.table_view.setTabKeyNavigation(False)\n        self.table_view.hide()\n\n        self.delegate = ColumnMetadataIconDelegate(Paths.asset(\"icons/three-lines.png\"))\n        self.delegate.icon_clicked.connect(self.show_column_metadata_dialog)\n\n        layout.addWidget(self.label)\n        layout.addWidget(self.table_view)\n\n        self.sheet_name = None\n\n        self.retranslateUI()\n\n    def display_data(self, model, filepath, sheet_name=None):\n        \"\"\"Set the model of the QTableView\n\n        When a tabular file is selected, the main application will create a\n        FrictionlessTableModel and call this function using the model as a parametner.\n        \"\"\"\n        self.table_view.setModel(model)\n\n        self.table_view.setItemDelegate(self.delegate)\n\n        self.table_view.horizontalHeader().setDefaultSectionSize(120)\n        self.table_view.setMouseTracking(True)\n\n        self.sheet_name = sheet_name\n        self.metadata = File(filepath, sheet_name).get_or_create_metadata(sheet_name)\n        self.resource = self.metadata.get(\"resource\")\n\n        self.label.hide()\n        self.table_view.show()\n\n    def show_column_metadata_dialog(self, field_index):\n        \"\"\"\n        Shows a dialog to edit a column's metadata.\n        \"\"\"\n        model = self.table_view.model()\n        column_count = model.columnCount()\n        headers = []\n        for column in range(column_count):\n            index = model.index(0, column)\n            value = model.data(index, Qt.ItemDataRole.DisplayRole)\n            headers.append(value)\n\n        field = self.resource.schema.fields[field_index]\n        column_metadata_field = ColumnMetadataField(\n            headers[field_index], field.type, field.description, field.constraints\n        )\n        dialog = ColumnMetadataDialog(self, column_metadata_field, field_index, headers)\n        dialog.save_clicked.connect(self.save_metadata_to_descriptor_file)\n        dialog.exec()\n\n    def clear(self, model):\n        \"\"\"Reset the view to the default state.\n\n        This view depends of the main application self.table_model attribute. This\n        method should always receive an empty model\n        \"\"\"\n        self.table_view.setModel(model)\n        self.label.show()\n        self.table_view.hide()\n\n    def retranslateUI(self):\n        \"\"\"Apply translations to class elements.\"\"\"\n        self.label.setText(self.tr(\"Preview not available for this item.\"))\n\n    def save_metadata_to_descriptor_file(self, field_form: dict):\n        \"\"\"Save the metadata to the descriptor file.\"\"\"\n        field_index = field_form.get(\"index\")\n        field = self.resource.schema.fields[field_index]\n\n        field.name = field_form.get(\"name\")\n        field.title = field_form.get(\"name\")\n        field.description = field_form.get(\"description\", \"\")\n        field.constraints = {\n            \"required\": field_form.get(\"constraints\").get(\"required\"),\n        }\n\n        type = field_form.get(\"type\")\n\n        if type == \"string\":\n            field.constraints[\"minLength\"] = field_form.get(\"constraints\").get(\"minLength\")\n            field.constraints[\"maxLength\"] = field_form.get(\"constraints\").get(\"maxLength\")\n        else:\n            # If the type is not Text, we remove the minLength and maxLength constraints\n            field.constraints.pop(\"minLength\", None)\n            field.constraints.pop(\"maxLength\", None)\n\n        # Update the field in the schema\n        self.resource.schema.set_field(field)\n\n        # Field.type cannot be updated directly, we need to use set_field_type\n        # it needs to be after the set_field to avoid being overridden\n        self.resource.schema.set_field_type(field.name, type)\n\n        self.metadata[\"resource\"] = self.resource.to_descriptor()\n        file = File(self.resource.path, self.sheet_name)\n\n        # We remove the dialect from the metatada, because Frictionless will be stuck\n        # on this sheet otherwise\n        self.metadata[\"resource\"].pop(\"dialect\", None)\n\n        with open(file.metadata_path, \"w\") as f:\n            print(f\"Saving metadata {file.metadata_path}\")\n            json.dump(self.metadata, f)\n\n        # Check if we name was changed, if so we need to update the header\n        model = self.table_view.model()\n        index = model.index(0, field_index)\n        original_name = model.data(index, Qt.ItemDataRole.DisplayRole)\n\n        table_view_changed = False\n        if original_name != field.name:\n            model.setData(index, field.name, Qt.ItemDataRole.EditRole)\n            table_view_changed = True\n\n        self.on_save.emit(table_view_changed)\n"
  },
  {
    "path": "src/ode/panels/errors.py",
    "content": "import collections\n\nfrom PySide6.QtCore import Qt, QSortFilterProxyModel\nfrom PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QTableView\nfrom PySide6.QtGui import QFont\n\nfrom ode import utils\nfrom ode.panels.data import DEFAULT_LIMIT_ERRORS\nfrom ode.shared import COLOR_RED\n\n\nclass ErrorFilterProxyModel(QSortFilterProxyModel):\n    \"\"\"Proxy model to display only the rows of the given error type.\n\n    As recommended by Qt, ODE reuses the same FrictionlessTableModel for all the TableViews.\n    For ErrorReports we filter and show only the rows containing the specific error_type\n    we want to display.\n    \"\"\"\n\n    def __init__(self, error_type):\n        super().__init__()\n        self.error_type = error_type\n\n    def filterAcceptsRow(self, source_row, source_parent):\n        \"\"\"Accept rows that contains an error.\n\n        The source_model is a FrictionlessTableModel and its errors attribute\n        contains a list of tuples:\n            [..., (row_number, error_type, error_message), ...]\n        \"\"\"\n        source_model = self.sourceModel()\n        if source_model.errors[source_row] is None or len(source_model.errors[source_row]) == 0:\n            return False\n\n        for error in source_model.errors[source_row]:\n            if error[1] == self.error_type:\n                return True\n\n        return False\n\n    def data(self, index, role):\n        \"\"\"Overrides the data method to set the background color of the cells according the error type.\"\"\"\n        if not index.isValid():\n            return None\n\n        # Converts the index to the source model so we can map it with the errors list\n        source_index = self.mapToSource(index)\n        source_row = source_index.row()\n        source_column = source_index.column()\n\n        if role == Qt.ItemDataRole.BackgroundRole:\n            source_model = self.sourceModel()\n\n            if source_model.errors[source_row] is None or len(source_model.errors[source_row]) == 0:\n                # Default color\n                return None\n\n            for error in source_model.errors[source_row]:\n                if self.error_type == \"blank-row\":\n                    # BlankRowError does not have field_number, we paint all the cells.\n                    return COLOR_RED\n                elif error[0] == source_column and error[1] == self.error_type:\n                    return COLOR_RED\n\n            # Default color\n            return None\n\n        return super().data(index, role)\n\n\nclass ErrorReport(QWidget):\n    \"\"\"Widget to show a single-type Error report.\n\n    This widget will be use in the Errors view for every type of error that\n    frictionless validate finds. It display the title, description and table\n    preview for a specific type of error.\n\n    The error argument is a list of Frictionless errors object of the same type\n    of error.\n    \"\"\"\n\n    def __init__(self, errors, model, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.errors = errors\n        utils.set_common_style(self)\n\n        # Title of each error report: Label + count\n        title = QWidget()\n        title_layout = QHBoxLayout()\n        title_layout.setContentsMargins(0, 0, 0, 0)\n\n        self.title_label = QLabel(objectName=\"title_label\")\n        self.count_label = QLabel(objectName=\"count_label\")\n\n        title_layout.addWidget(self.title_label)\n        title_layout.addWidget(self.count_label)\n        title_layout.addStretch()\n\n        title.setLayout(title_layout)\n\n        # Description of the error\n        self.description = QLabel()\n        font = self.description.font()\n        font.setPointSize(12)\n        self.description.setFont(font)\n        self.description.setWordWrap(True)\n\n        # Previsualization table\n        self.proxy_model = ErrorFilterProxyModel(self.errors[0].type)\n        self.proxy_model.setSourceModel(model)\n        self.table = QTableView()\n        self.table.setModel(self.proxy_model)\n\n        vbox = QVBoxLayout()\n        vbox.addWidget(title)\n        vbox.addWidget(self.description)\n        vbox.addWidget(self.table)\n        self.setLayout(vbox)\n\n        self.setStyleSheet(\n            \"\"\"\n            QLabel#title_label {\n              font-weight: bold;\n            }\n            QLabel#count_label {\n              background: #D32F2F;\n              color: #FFF;\n              padding: 2px 2px;\n              border-style: outset;\n              border-width: 1px;\n              border-radius: 4px;\n              border-color: #D32F2F;\n            }\n        \"\"\"\n        )\n\n        self.retranslateUI()\n\n    def retranslateUI(self):\n        error_title = utils.ErrorTexts.get_error_title(self.errors[0].type) or self.errors[0].title\n        error_count = str(len(self.errors))\n        errors_description = utils.ErrorTexts.get_error_description(self.errors[0].type) or self.errors[0].description\n\n        self.title_label.setText(error_title)\n        self.count_label.setText(error_count)\n        self.description.setText(errors_description)\n\n\n\nclass ErrorsWidget(QWidget):\n    \"\"\"Widget to dynamically show errors reports.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        layout = QVBoxLayout()\n\n        self.max_errors_label = QLabel()\n        font = QFont()\n        font.setItalic(True)\n        self.max_errors_label.setFont(font)\n        self.max_errors_label.setStyleSheet(\"font-size: 17px\")\n        self.max_errors_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)\n\n        self.reports = QWidget()\n        self.reports_layout = QVBoxLayout()\n        self.reports_layout.setContentsMargins(0, 0, 0, 0)\n        self.reports.setLayout(self.reports_layout)\n\n        layout.addWidget(self.max_errors_label)\n        layout.addWidget(self.reports)\n\n        self.setLayout(layout)\n\n    def display_errors(self, errors, model):\n        \"\"\"Builds and display the entire error report.\n\n        This method should be called when reading and validating a\n        tabular file. It is currently triggered when the user clicks on\n        a file in the FileTreeNavigator\n        \"\"\"\n        self.clear()\n        if not errors:\n            return\n\n        errors_list = self._sort_frictionless_errors(errors)\n        total_errors = 0\n        for error in errors_list:\n            errorReport = ErrorReport(error, model)\n            self.reports_layout.addWidget(errorReport)\n            total_errors += len(error)\n        self.reports.show()\n\n        if total_errors >= DEFAULT_LIMIT_ERRORS:\n            self.max_errors_label.show()\n        else:\n            self.max_errors_label.hide()\n\n    def clear(self):\n        \"\"\"Removes all the ErrorReports that have been added to this widget.\"\"\"\n        while self.reports_layout.count():\n            errorReport = self.reports_layout.takeAt(0)\n            errorReport.widget().deleteLater()\n        self.reports.hide()\n\n    def _sort_frictionless_errors(self, errors):\n        \"\"\"Splits a list of dictionaries into several lists grouped by type.\n\n        Frictionless returns an array of Error objects, since we want to create an\n        ErrorReport for each type of error, we rearrange the array into a list of\n        arrays in which each one contains only one error type.\n        \"\"\"\n        result = collections.defaultdict(list)\n        for error in errors:\n            result[error.type].append(error)\n        return list(result.values())\n\n    def retranslateUI(self):\n        self.max_errors_label.setText(\n            self.tr(\"Please note that the ODE currently detects errors in tables, with a maximum of \")\n            + str(DEFAULT_LIMIT_ERRORS)\n        )\n        for i in range(self.reports_layout.count()):\n            self.reports_layout.itemAt(i).widget().retranslateUI()\n"
  },
  {
    "path": "src/ode/panels/source.py",
    "content": "import sys\n\nfrom PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPlainTextEdit, QLabel\nfrom PySide6.QtGui import QFont\nfrom PySide6.QtCore import Qt, QFileInfo\n\nfrom ode import utils\n\n\nclass SourceViewer(QWidget):\n    \"\"\"Widget to display files as they are (raw).\n\n    This class needs to properly detect the enconding of a file. For now\n    UTF-8 and ISO-8859-1 will cover most of our scenarios but we will fail\n    to display source of minority languages.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n\n        utils.set_common_style(self)\n\n        layout = QVBoxLayout()\n        self.setLayout(layout)\n\n        self.label = QLabel()\n        self.label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)\n\n        self.text_edit = QPlainTextEdit()\n        self.text_edit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)\n        self.text_edit.setReadOnly(True)\n        self.text_edit.setFont(QFont(\"Courier, monospace\"))\n        self.text_edit.hide()\n\n        layout.addWidget(self.label)\n        layout.addWidget(self.text_edit)\n\n    def _read_file(self, filepath):\n        \"\"\"Function to read the file from disk and return a string.\n\n        We could implement a more fancy approach with chardet to detect the file encoding\n        but early tests where not performat enough. We are brute-forcing the two most popular\n        encodings in the web.\n        \"\"\"\n        content = \"\"\n        try:\n            with open(filepath, \"r\", encoding=\"utf-8\") as file:\n                content = file.read()\n            return content\n        except Exception as e:\n            content = f\"Error while reading the file with encoding UTF-8: {e}\"\n\n        try:\n            with open(filepath, \"r\", encoding=\"iso-8859-1\") as file:\n                content = file.read()\n            return content\n        except Exception as e:\n            content += f\"\\nError while reading the file with encoding ISO-8859-1: {e}\"\n\n        return content\n\n    def open_file(self, filepath):\n        \"\"\"Reads the file and sets the QPlainText.\"\"\"\n        info = QFileInfo(filepath)\n        if not info.isFile() or info.suffix().lower() not in [\"csv\"]:\n            self.label.show()\n            self.text_edit.hide()\n            return\n\n        content = self._read_file(filepath)\n\n        self.label.hide()\n        self.text_edit.show()\n        self.text_edit.setPlainText(content)\n\n    def clear(self):\n        \"\"\"Shows an empty view.\"\"\"\n        self.label.show()\n        self.text_edit.hide()\n\n    def retranslateUI(self):\n        self.label.setText(self.tr(\"This view is only available for CSV files.\"))\n\n\nif __name__ == \"__main__\":\n    app = QApplication(sys.argv)\n\n    viewer = SourceViewer()\n    viewer.show()\n\n    sys.exit(app.exec())\n"
  },
  {
    "path": "src/ode/paths.py",
    "content": "import os\nfrom pathlib import Path\n\n# This is the Project path where all files are stored and\n# it will be hardcoded to the home folder of the user until\n# we define how to properly handle projects in Open Data Editor.\nPROJECT_PATH = Path.home() / \".opendataeditor/tmp\"\nMETADATA_PATH = PROJECT_PATH / \".metadata\"\nLOGS_PATH = PROJECT_PATH / \".logs\"\nAI_MODELS_PATH = PROJECT_PATH / \".ai_models\"\n\n\nclass Paths:\n    \"\"\"Utility class to handle relative paths.\"\"\"\n\n    base = os.path.dirname(__file__)\n    assets = os.path.join(base, \"assets\")\n\n    @classmethod\n    def asset(cls, filename):\n        return os.path.join(cls.assets, filename)\n\n    @classmethod\n    def translation(cls, filename):\n        return os.path.join(cls.assets, \"translations\", filename)\n\n    @classmethod\n    def get_unique_destination_filepath(cls, src_filepath) -> Path:\n        \"\"\"Returns a unique destination_filepath by appending a number if the file already exists.\n\n        If the specified file already exists, the method will generate a new filename by\n        appending a number in parentheses to the original name. For example:\n\n          - For a file named 'myfile.csv', if it doesn't exist, it will return 'myfile.csv'.\n          - If 'myfile.csv' already exists, it will return 'myfile(1).csv'.\n          - If 'myfile(1).csv' also exists, it will return 'myfile(2).csv', and so on.\n        \"\"\"\n\n        src_filepath = Path(src_filepath) if isinstance(src_filepath, str) else src_filepath\n\n        destination_filepath = PROJECT_PATH / src_filepath.name\n\n        # If already exists we increment to `filename (n) until we find one not taking\n        counter = 1\n        while destination_filepath.exists():\n            destination_filepath = destination_filepath.with_stem(f\"{src_filepath.stem}({counter})\")\n            counter += 1\n\n        return destination_filepath\n"
  },
  {
    "path": "src/ode/shared.py",
    "content": "from PySide6.QtGui import QColor\n\nCOLOR_RED = QColor(\"#D32F2F\")\nCOLOR_BLUE = QColor(\"#0288D1\")\n"
  },
  {
    "path": "src/ode/utils.py",
    "content": "import json\nimport platform\nimport subprocess\n\nfrom pathlib import Path\n\nfrom frictionless.resources import TableResource\nfrom frictionless import system\n\nfrom ode import paths\n\nfrom PySide6.QtWidgets import QApplication, QMessageBox\n\n\ndef setup_ode_internal_folders():\n    \"\"\"Creates the folders to store the files and metadata files.\"\"\"\n    paths.METADATA_PATH.mkdir(parents=True, exist_ok=True)\n    if platform.system() == \"Windows\":\n        # Set the .metadata folder hidden so it is not shown in the ODE file navigator\n        # This is the default behaviour in Linux/MacOs since the directory name starts with a dot.\n        subprocess.run([\"attrib\", \"+H\", f\"{str(paths.METADATA_PATH)}\"], check=True)\n        subprocess.run([\"attrib\", \"+H\", f\"{str(paths.AI_MODELS_PATH)}\"], check=True)\n        subprocess.run([\"attrib\", \"+H\", f\"{str(paths.LOGS_PATH)}\"], check=True)\n\n\ndef migrate_metadata_store():\n    \"\"\"Migrates all the metadata information to separated files.\n\n    Previously ODE stored the Frictionless metadata and other info of\n    every file in a single \"records\" dictionary stored in a metadata.json.\n    This created several issues like: custom logic to deduplicate file names,\n    a third database to link files with records, etc.\n\n    We will now store metadata on independent files under a `.metadata` folder.\n    Each file will have the same name of the original file with a `metadata.json` append.\n    We will also mimic the folder structure.\n    \"\"\"\n    # Path to ODE v1.3 metadata.json file\n    metadata_file_path = paths.PROJECT_PATH / \".opendataeditor/metadata.json\"\n    if not metadata_file_path.exists():\n        # ODE has never been used in this machine. Nothing to migrate.\n        return\n\n    new_metadata_dir = paths.PROJECT_PATH / \".metadata/\"\n    if new_metadata_dir.exists():\n        # If folder exist we asume migrated and return.\n        return\n\n    ode_dir = paths.PROJECT_PATH / \".opendataeditor/\"\n    if ode_dir.exists() and platform.system() == \"Windows\":\n        # Hid .opendataeditor directory. This directory is no longer used.\n        subprocess.run([\"attrib\", \"+H\", f\"{str(ode_dir)}\"], check=True)\n\n    # ODE v1.3 has been used and we need to migrate.\n    with open(metadata_file_path, \"r\") as file:\n        try:\n            metadata = json.load(file)\n        except Exception as e:\n            # We are receiving json.decoder.JSONDecodeError error reports from users.\n            # So we skip the migration if the file cannot be read.\n            print(f\"Cannot read ode v1.3 metadata file. Skipping migration: {e}\")\n            return\n\n    records = metadata[\"record\"]\n\n    for _, record_data in records.items():\n        path = record_data[\"path\"]\n        try:\n            # Infer Frictionless Statistics, it is mandatory for the newest version of ODE.\n            with system.use_context(trusted=True):\n                resource = TableResource(record_data.get(\"resource\"))\n                # Patch the original resource path with the absolute path to the file.\n                resource.path = str(paths.PROJECT_PATH / path)\n                resource.infer()\n                record_data[\"resource\"] = resource.to_descriptor()\n        except Exception as e:\n            # This should happen only if the user did file/metadata editing outside ODE.\n            print(f\"Error when creating TableResource: {e}\")\n            continue\n\n        # Set metadata file name as <filename>.json\n        # Example: my-file.csv -> my-file.json\n        # Example: subfolder/my-file.csv -> subfolder/my-file.json\n        filename = path.rsplit(\".\", 1)[0]\n        metadata_filename = str(new_metadata_dir / filename) + \".json\"\n\n        # Ensure the directory structure exists\n        # Example: if we are migrating a file located in a subfolder, we need to create\n        # it before the json.dump file.\n        Path(metadata_filename).parent.mkdir(parents=True, exist_ok=True)\n\n        with open(metadata_filename, \"w\") as json_file:\n            json.dump(record_data, json_file, indent=4)\n\n    print(\"Migration completed successfully!\")\n\n\ndef set_common_style(widget):\n    widget.setStyleSheet(\"font-size: 17px;\")\n\n\ndef show_error_dialog(message=None, title=\"Error\"):\n    if message is None:\n        message = \"An unexpected error occurred in the application.\"\n\n    error_box = QMessageBox()\n    error_box.setIcon(QMessageBox.Icon.Critical)\n    error_box.setWindowTitle(title)\n    error_box.setText(\"An error has occurred\")\n    error_box.setInformativeText(message)\n    error_box.setStandardButtons(QMessageBox.StandardButton.Ok)\n\n    return error_box.exec()\n\n\nclass ErrorTexts:\n    @classmethod\n    def get_error_title(cls, error_type):\n        \"\"\"Returns a more user-friendly title if exists.\"\"\"\n        ERROR_TITLES = {\n            \"missing-label\": QApplication.translate(\"ErrorsMessages\", \"Missing header\"),\n            \"duplicate-label\": QApplication.translate(\"ErrorsMessages\", \"Duplicated header\"),\n            \"blank-row\": QApplication.translate(\"ErrorsMessages\", \"Empty row\"),\n            \"type-error\": QApplication.translate(\"ErrorsMessages\", \"Type mismatch\"),\n            \"missing-cell\": QApplication.translate(\"ErrorsMessages\", \"Missing value\"),\n            \"extra-cell\": QApplication.translate(\"ErrorsMessages\", \"Extra cell\"),\n            \"blank-header\": QApplication.translate(\"ErrorsMessages\", \"Missing header\"),\n            \"blank-label\": QApplication.translate(\"ErrorsMessages\", \"Blank Label\"),\n        }\n\n        return ERROR_TITLES.get(error_type, None)\n\n    @classmethod\n    def get_error_description(cls, error_type):\n        \"\"\"Returns a more user-friendly description if exists.\"\"\"\n        ERROR_DESCRIPTIONS = {\n            \"missing-label\": QApplication.translate(\"ErrorsMessages\", \"A column in the header row has no name. Every column should have a unique, non-empty header.\"),\n            \"duplicate-label\": QApplication.translate(\"ErrorsMessages\", \"Two or more columns share the same name. Column names must be unique.\"),\n            \"blank-row\": QApplication.translate(\"ErrorsMessages\", \"This row has no data. Rows should contain at least one cell with data.\"),\n            \"type-error\": QApplication.translate(\"ErrorsMessages\", \"A cell value doesn't match the expected data type or format for the column.\"),\n            \"missing-cell\": QApplication.translate(\"ErrorsMessages\", \"This cell is missing data\"),\n            \"extra-cell\": QApplication.translate(\"ErrorsMessages\", \"This row has more values compared to the header row.\"),\n            \"blank-header\": QApplication.translate(\"ErrorsMessages\", \"A column in the header row has no name. Every column should have a unique, non-empty header.\"),\n            \"blank-label\": QApplication.translate(\"ErrorsMessages\", \"A label in the header row is missing a value. Label should be provided and not be blank.\"),\n        }\n\n        return ERROR_DESCRIPTIONS.get(error_type, None)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "import pytest\nfrom ode import paths, main\n\n\n@pytest.fixture(autouse=True)\ndef project_folder(tmp_path):\n    # Patch the PROJECT_PATH to use temporary directory\n    paths.PROJECT_PATH = tmp_path\n    paths.METADATA_PATH = tmp_path / \".metadata\"\n    return tmp_path\n\n\n@pytest.fixture(autouse=True)\ndef window(qtbot, project_folder):\n    win = main.MainWindow()\n    qtbot.addWidget(win)\n    win.show()\n    return win\n"
  },
  {
    "path": "tests/ode/__init__.py",
    "content": ""
  },
  {
    "path": "tests/ode/test_application.py",
    "content": "from PySide6.QtCore import Qt\n\nfrom ode.shared import COLOR_RED\n\n\ndef test_file_is_displayed(qtbot, window, project_folder):\n    p1 = project_folder / \"example.csv\"\n    p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n\n    # Assert Welcome screen is selected by default.\n    assert window.main_layout.currentIndex() == 0\n    assert window.content.data_view.isVisible() is False\n\n    # Simulate click event\n    index = window.sidebar.file_model.index(str(p1))\n    window.on_tree_click(index)\n\n    qtbot.waitUntil(lambda: window.main_layout.currentIndex() == 1)\n    assert window.main_layout.currentIndex() == 1\n    assert window.content.data_view.isVisible()\n\n    # Test our TableView model has 3 rows and 2 columns (data was loaded properly)\n    assert window.content.data_view.table_view.model().rowCount() == 3\n    assert window.content.data_view.table_view.model().columnCount() == 2\n\n\ndef test_button_errors_displays_error_count(qtbot, window, project_folder):\n    p1 = project_folder / \"missing-header.csv\"\n    p1.write_text(\"name,\\nAlice,30\\nBob,25\")\n\n    # Simulate click event\n    index = window.sidebar.file_model.index(str(p1))\n    window.on_tree_click(index)\n\n    qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"1\")\n    error_report = window.content.errors_view.reports_layout.takeAt(0)\n    error_text = error_report.widget().description.text()\n\n    # Test Frictionless error\n    assert \"Label should be provided and not be blank.\" in str(error_text)\n\n\ndef test_error_reports_show_two_blank_lines_in_red(qtbot, window, project_folder):\n    \"\"\"Test that two blank lines are shown in red in the error report.\"\"\"\n    p1 = project_folder / \"two-blank-lines.csv\"\n    p1.write_text(\"name,surname\\n,\\n,\")\n\n    # Simulate click event\n    index = window.sidebar.file_model.index(str(p1))\n    window.on_tree_click(index)\n\n    qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"2\")\n    # Get error report\n    error_report = window.content.errors_view.reports_layout.takeAt(0)\n\n    proxy = error_report.widget().proxy_model\n    total_rows = proxy.rowCount()\n    red_rows = 0\n    red_background = COLOR_RED.name()\n\n    for row in range(total_rows):\n        # Get the proxy index for this row\n        index = proxy.index(row, 0)\n\n        # Get background color\n        background_color = proxy.data(index, Qt.BackgroundRole)\n\n        # If the color is red, increment counter\n        if background_color is not None and background_color.name() == red_background:\n            red_rows += 1\n\n    assert red_rows == 2\n\n\ndef test_error_reports_show_two_errors_in_same_row(qtbot, window, project_folder):\n    \"\"\"\n    This test is for the case where there are two errors in the same row, and\n    both errors are shown in red.\n\n    Error 1: Duplicated header\n    Error 2: Empty header\n    \"\"\"\n    p1 = project_folder / \"header-errors.csv\"\n    p1.write_text(\"name,name,,empty\\nname,surname,empty,empty\")\n\n    # Simulate click event\n    index = window.sidebar.file_model.index(str(p1))\n    window.on_tree_click(index)\n\n    qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"2\")\n\n    red_background = COLOR_RED.name()\n\n    # Check that we have two error report tables\n    assert window.content.errors_view.reports_layout.count() == 2\n\n    # Check we have on error in each report\n    # Error 1: Duplicated header\n    proxy_model = window.content.errors_view.reports_layout.itemAt(0).widget().proxy_model\n    assert proxy_model.error_type == \"duplicate-label\"\n    # Check the exact column\n    index = proxy_model.index(0, 1)\n    background_color = proxy_model.data(index, Qt.BackgroundRole)\n    assert background_color.name() == red_background\n\n    # Error 2: Empty header\n    proxy_model = window.content.errors_view.reports_layout.itemAt(1).widget().proxy_model\n    assert proxy_model.error_type == \"blank-label\"\n    # Check the exact column\n    index = proxy_model.index(0, 2)\n    background_color = proxy_model.data(index, Qt.BackgroundRole)\n    assert background_color.name() == red_background\n"
  },
  {
    "path": "tests/ode/test_files.py",
    "content": "import json\nimport pytest\n\nfrom ode.file import File\n\n\nclass TestFiles:\n    def test_constructor(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        file = File(p1)\n\n        assert file.path == p1\n        assert file.metadata_path == (project_folder / \".metadata/example.json\")\n\n    def test_path_to_metadata_file(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        m1 = project_folder / \".metadata/example.json\"\n        assert File(p1).metadata_path == m1\n\n    def test_path_to_metadata_subfolder(self, project_folder):\n        p2 = project_folder / \"subfolder/example-1.csv\"\n        m2 = project_folder / \".metadata/subfolder/example-1.json\"\n        assert File(p2).metadata_path == m2\n\n    def test_path_to_metadata_folder(self, project_folder):\n        p3 = project_folder / \"subfolder/\"\n        p3.mkdir()\n        m3 = project_folder / \".metadata/subfolder/\"\n        assert File(p3).metadata_path == m3\n\n    def test_get_create_metadata(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n\n        file = File(p1)\n        metadata = file.get_or_create_metadata()\n        assert file.metadata_path.exists()\n        assert metadata[\"resource\"]\n        assert metadata[\"resource\"].path == str(p1)\n\n    def test_get_metadata_dict(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n        file = File(p1)\n        file.get_or_create_metadata()\n        metadata = file.get_metadata_dict(file.metadata_path)\n\n        assert metadata\n        assert isinstance(metadata, dict)\n        assert metadata[\"resource\"][\"path\"] == str(p1)\n        assert (project_folder / \".metadata/example.json\").exists()\n\n    def test_rename_file(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n        file = File(p1)\n\n        file.rename(\"bar\")\n        assert not p1.exists()\n        assert (project_folder / \"bar.csv\").exists()\n\n    def test_rename_folder(self, project_folder):\n        m1 = project_folder / \"subfolder\"\n        m1.mkdir()\n        p1 = project_folder / \"subfolder/example.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n\n        ode_folder = File(m1)\n        ode_folder.rename(\"bar\")\n\n        assert not m1.exists()\n        assert (project_folder / \"bar\").exists()\n        assert (project_folder / \"bar/example.csv\").exists()\n\n    def test_rename_raises_error_if_target_exist(self, project_folder):\n        p1 = project_folder / \"foo.csv\"\n        p1.write_text(\"foo\")\n        p2 = project_folder / \"bar.csv\"\n        p2.write_text(\"bar\")\n\n        file = File(p1)\n        with pytest.raises(OSError):\n            file.rename(\"bar\")\n\n    def test_rename_also_updates_object_attributes(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n        file = File(p1)\n        file.get_or_create_metadata()\n        file.rename(\"bar\")\n        assert file.path == (project_folder / \"bar.csv\")\n        assert str(file.metadata_path) == str(project_folder / \".metadata/bar.json\")\n\n    def test_rename_file_metadata(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n        file = File(p1)\n        file.get_or_create_metadata()\n        file.rename(\"bar\")\n\n        metadata = file.get_metadata_dict(file.metadata_path)\n        expected = project_folder / \"bar.csv\"\n\n        assert metadata[\"resource\"][\"path\"] == str(expected)\n\n    def test_rename_folder_metadata(self, project_folder):\n        \"\"\"Test that renaming folders updates all metadata files of children files.\"\"\"\n        m1 = project_folder / \"subfolder\"\n        m1.mkdir()\n        p1 = project_folder / \"subfolder/foo.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25\")\n        File(p1).get_or_create_metadata()\n        p2 = project_folder / \"subfolder/bar.csv\"\n        p2.write_text(\"name,age\\nAlice,30\\nBob,25\")\n        File(p2).get_or_create_metadata()\n\n        file = File(m1)\n        file.rename(\"new_name\")\n\n        assert (project_folder / \".metadata/new_name/foo.json\").exists()\n        assert (project_folder / \".metadata/new_name/bar.json\").exists()\n\n        metadata_path = project_folder / \".metadata/new_name/foo.json\"\n        with open(metadata_path) as f:\n            metadata = json.load(f)\n            assert metadata[\"resource\"][\"path\"] == str(project_folder / \"new_name/foo.csv\")\n\n        metadata_path = project_folder / \".metadata/new_name/bar.json\"\n        with open(metadata_path) as f:\n            metadata = json.load(f)\n            assert metadata[\"resource\"][\"path\"] == str(project_folder / \"new_name/bar.csv\")\n\n    def test_remove_file_and_metadata(self, project_folder):\n        p1 = project_folder / \"example.csv\"\n        p1.touch()\n        file = File(p1)\n        file.get_or_create_metadata()\n        file.remove()\n\n        assert file.path.exists() is False\n        assert file.metadata_path.exists() is False\n\n    def test_delete_folder_and_metadata(self, project_folder):\n        m1 = project_folder / \"subfolder\"\n        m1.mkdir()\n        p1 = project_folder / \"subfolder/foo.csv\"\n        p1.touch()\n        f1 = File(p1)\n        f1.get_or_create_metadata()\n        p2 = project_folder / \"subfolder/bar.csv\"\n        p2.touch()\n        f2 = File(p2)\n        f2.get_or_create_metadata()\n        p3 = project_folder / \"subfolder/zoo.csv\"\n        p3.touch()\n        f3 = File(p3)  # f3 should not have a metadata fail and it should not fail when deleting.\n\n        file = File(m1)\n        file.remove()\n\n        assert p1.exists() is False\n        assert f1.metadata_path.exists() is False\n        assert p2.exists() is False\n        assert f2.metadata_path.exists() is False\n        assert p3.exists() is False\n        assert f3.metadata_path.exists() is False\n"
  },
  {
    "path": "tests/ode/test_frictionless_errors.py",
    "content": "from PySide6.QtCore import Qt\n\nfrom PySide6.QtWidgets import QDialog\n\nfrom ode.dialogs.metadata import ColumnMetadataDialog\nfrom ode.panels.data import ColumnMetadataField\nfrom ode.shared import COLOR_RED\n\n\nclass TestFrictionlessErrors:\n    \"\"\"Test that FrictionlessTableModel returns correct background for errors.\n\n    Our main QTableView calls the data() endpoint of our table_model with\n    Qt.ItemDataRole.BackgroundRole to request wich color should the cell be painted.\n    We also assume that if the value returned is QColor(\"red\") then the table will be\n    displayed properly.\n    \"\"\"\n\n    def test_blank_header_error(self, qtbot, window, project_folder):\n        p1 = project_folder / \"blank-header.csv\"\n        p1.write_text(\"name,\\nAlice,30\\nBob,25\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain only 1 error.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"1\")\n\n        # Test FrictionlessModel returns a Red Background for the error cell.\n        index = window.table_model.index(0, 1)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n\n        # Test FrictionlessModel do not return a Red Background for other cells.\n        index = window.table_model.index(1, 0)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background != COLOR_RED\n\n    def test_duplicate_label_error(self, qtbot, window, project_folder):\n        p1 = project_folder / \"duplicate-label.csv\"\n        p1.write_text(\"name,name\\nAlice,30\\nBob,25\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain only 1 error.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"1\")\n\n        # Test FrictionlessModel returns a Red Background for the error cell.\n        index = window.table_model.index(0, 1)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n\n        # Test FrictionlessModel do not return a Red Background for first header.\n        index = window.table_model.index(0, 0)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background != COLOR_RED\n\n    def test_blank_row_error(self, qtbot, window, project_folder):\n        p1 = project_folder / \"blank-row.csv\"\n        p1.write_text(\"name,age,city\\nAlice,30,Barcelona\\n,,\\nBob,25,Valencia\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain only 1 error.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"1\")\n\n        # Blank Row error should paint the whole row in red.\n        index = window.table_model.index(2, 0)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n        index = window.table_model.index(2, 1)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n        index = window.table_model.index(2, 2)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n\n    def test_blank_row_and_duplicated_label_error(self, qtbot, window, project_folder):\n        p1 = project_folder / \"blank-row-and-duplicated-label.csv\"\n        p1.write_text(\"name,name\\nAlice,30\\n,\\nBob,25\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain only 2 errors: blank row and duplicated label.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"2\")\n\n        # Blank Row error should paint the whole row in red.\n        index = window.table_model.index(2, 0)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n        index = window.table_model.index(2, 1)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n\n        # Duplicated Label should paint the cell\n        index = window.table_model.index(0, 1)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n\n    def test_missing_cell_error(self, qtbot, window, project_folder):\n        p1 = project_folder / \"missing-cell.csv\"\n        p1.write_text(\"name,age,city\\nAlice,30\\nBob,25\\nTom,15\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain 3 errors.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"3\")\n\n        # Missing Cell should paint the third column (except the header).\n        index = window.table_model.index(0, 2)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background != COLOR_RED\n        index = window.table_model.index(1, 2)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n        index = window.table_model.index(2, 2)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n        index = window.table_model.index(3, 2)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n\n    def test_extra_cell_error(self, qtbot, window, project_folder):\n        p1 = project_folder / \"extra-cell.csv\"\n        p1.write_text(\"name,age\\nAlice,30\\nBob,25,extra\\nTom,15\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain 1 errors.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"1\")\n\n        # Extra Cell should paint the extra cell.\n        index = window.table_model.index(2, 2)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background == COLOR_RED\n        # The value of the extra cell should be \"extra\"\n        value = window.table_model.data(index, Qt.ItemDataRole.DisplayRole)\n        assert value == \"extra\"\n        # Other cells should not be red\n        index = window.table_model.index(2, 1)\n        background = window.table_model.data(index, Qt.ItemDataRole.BackgroundRole)\n        assert background != COLOR_RED\n\n    def test_custom_errors_descriptions_are_shown(self, qtbot, window, project_folder):\n        \"\"\"Test that the application is using our custom error descriptions.\"\"\"\n        p1 = project_folder / \"blank-row.csv\"\n        p1.write_text(\"name,age,city\\nAlice,30,Barcelona\\n,,\\nBob,25,Valencia\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain 1 errors.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"1\")\n\n        window.content.toolbar.button_errors.click()\n        # If we want to see the change in the table, we need to update the window\n        window.update()\n        qtbot.wait(100)\n        assert window.content.errors_view.reports_layout.count() == 1\n        error_report = window.content.errors_view.reports_layout.itemAt(0).widget()\n        assert (\n            error_report.description.text() == \"This row has no data. Rows should contain at least one cell with data.\"\n        )\n        assert error_report.title_label.text() == \"Empty row\"\n\n    def test_default_frictionless_errors_if_missing_custom(self, qtbot, window, project_folder):\n        \"\"\"Test that the application fallbacks to Frictionless errors if we do not provide a custom description.\"\"\"\n        p1 = project_folder / \"blank-label.csv\"\n        p1.write_text(\"name,,city\\nAlice,30,Barcelona\\nBob,25,Valencia\")\n\n        # Simulate click event\n        index = window.sidebar.file_model.index(str(p1))\n        window.on_tree_click(index)\n\n        # This file should contain 1 errors.\n        qtbot.waitUntil(lambda: window.content.toolbar.button_errors.error_label.text() == \"1\")\n\n        window.content.toolbar.button_errors.click()\n        # If we want to see the change in the table, we need to update the window\n        window.update()\n        qtbot.wait(100)\n        assert window.content.errors_view.reports_layout.count() == 1\n        error_report = window.content.errors_view.reports_layout.itemAt(0).widget()\n        # The following are Frictionless default texts: https://framework.frictionlessdata.io/docs/errors/label.html#blank-label\n        assert (\n            error_report.description.text()\n            == \"A label in the header row is missing a value. Label should be provided and not be blank.\"\n        )\n        assert error_report.title_label.text() == \"Blank Label\"\n\n    def test_changing_column_header_name_fixes_error_with_dialog(self, qtbot, window, project_folder):\n        \"\"\"Test that changing the header name of a column with a dialog fixes the blank header error.\"\"\"\n        # The file should contain a blank header error\n        p0 = project_folder / \"temp.csv\"\n        p0.write_text(\"name,\\nAlice,A\\nBob,B\")\n\n        # Choose the file\n        index = window.sidebar.file_model.index(str(p0))\n        window.on_tree_click(index)\n\n        # Check that the file is loaded and has an error\n        qtbot.wait(100)\n        assert window.content.errors_view.reports_layout.count() == 1\n\n        # Create and show the dialog\n        blank_field = ColumnMetadataField(\"\", \"string\", \"\", {\"required\": False, \"minLength\": 0, \"maxLength\": 100})\n        field_names = [\"name\", \"\"]\n        dialog = ColumnMetadataDialog(\n            parent=window, field=blank_field, field_index=1, field_names=field_names  # Index of the blank column\n        )\n\n        # Use qtbot to interact with the dialog\n        qtbot.addWidget(dialog)\n        dialog.show()  # For debugging purposes\n\n        # Set the new name in the dialog\n        dialog.form.name.setText(\"surname\")\n\n        # Verify the dialog state\n        assert dialog.form.name.text() == \"surname\"\n\n        # Connect to the save signal to capture the emitted data\n        saved_data = []\n        dialog.save_clicked.connect(lambda data: saved_data.append(data))\n\n        # Click the save button\n        qtbot.mouseClick(dialog.save_button, Qt.MouseButton.LeftButton)\n\n        # For debugging\n        # qtbot.wait(100)\n        # window.update()\n\n        # Verify the dialog was accepted and data was emitted\n        assert dialog.result() == QDialog.DialogCode.Accepted\n        assert len(saved_data) == 1\n        assert saved_data[0][\"name\"] == \"surname\"\n        assert saved_data[0][\"index\"] == 1\n        window.content.data_view.save_metadata_to_descriptor_file(saved_data[0])\n\n        # Wait for the changes to be applied\n        qtbot.wait(1000)\n        window.update()\n\n        # Check that the error is gone\n        assert window.content.errors_view.reports_layout.count() == 0\n\n    def test_changing_column_type_with_metadata_dialog(self, qtbot, window, project_folder):\n        \"\"\"Test changing for and back the column type with the metadata dialog show and fixes the error.\"\"\"\n\n        # Create file\n        p0 = project_folder / \"temp.csv\"\n        p0.write_text(\"name,surname\\nAlice,A\\nBob,B\")\n\n        # Choose the file\n        index = window.sidebar.file_model.index(str(p0))\n        window.on_tree_click(index)\n\n        # Check that the file is loaded and has zero error\n        qtbot.wait(100)\n        assert window.content.errors_view.reports_layout.count() == 0\n\n        # Create and show the dialog\n        blank_field = ColumnMetadataField(\n            \"surname\", \"string\", \"\", {\"required\": False, \"minLength\": 0, \"maxLength\": 100}\n        )\n        field_names = [\"name\", \"surname\"]\n\n        # We are changing the \"surname\" column type to \"integer\"\n        dialog = ColumnMetadataDialog(parent=window, field=blank_field, field_index=1, field_names=field_names)\n\n        # Use qtbot to interact with the dialog\n        qtbot.addWidget(dialog)\n        dialog.show()  # For debugging purposes\n\n        dialog.form.type.setCurrentText(\"Number\")\n\n        # Verify the dialog state\n        assert dialog.form.type.currentText() == \"Number\"\n\n        # Connect to the save signal to capture the emitted data\n        saved_data = []\n        dialog.save_clicked.connect(lambda data: saved_data.append(data))\n\n        # Click the save button\n        qtbot.mouseClick(dialog.save_button, Qt.MouseButton.LeftButton)\n\n        # Verify the dialog was accepted and data was emitted\n        assert dialog.result() == QDialog.DialogCode.Accepted\n        assert len(saved_data) == 1\n        assert saved_data[0][\"type\"] == \"number\"\n        assert saved_data[0][\"index\"] == 1\n\n        window.content.data_view.save_metadata_to_descriptor_file(saved_data[0])\n\n        # Wait for the changes to be applied\n        qtbot.wait(1000)\n        window.update()\n\n        # Check that we have an error now\n        assert window.content.errors_view.reports_layout.count() == 1\n\n        # Change the column type back to \"string\" to fix the error\n        dialog.form.type.setCurrentText(\"Text\")\n        qtbot.mouseClick(dialog.save_button, Qt.MouseButton.LeftButton)\n\n        # Verify the dialog was accepted and data was emitted\n        assert dialog.result() == QDialog.DialogCode.Accepted\n        assert len(saved_data) == 2\n        assert saved_data[1][\"type\"] == \"string\"\n        assert saved_data[1][\"index\"] == 1\n\n        window.content.data_view.save_metadata_to_descriptor_file(saved_data[1])\n\n        # Wait for the changes to be applied\n        qtbot.wait(1000)\n        window.update()\n\n        # Check that we have an error now\n        assert window.content.errors_view.reports_layout.count() == 0\n"
  },
  {
    "path": "tests/ode/test_paths.py",
    "content": "from ode.paths import Paths\n\n\nclass TestPaths:\n    def test_no_conflict(self, project_folder):\n        test_file = project_folder / \"tempfile.txt\"\n        result = Paths.get_unique_destination_filepath(test_file)\n        assert result == test_file\n\n    def test_single_conflict(self, project_folder):\n        # Create same temp file to generate a conflict\n        (project_folder / \"tempfile.txt\").touch()\n        result = Paths.get_unique_destination_filepath(project_folder / \"tempfile.txt\")\n\n        assert result == (project_folder / \"tempfile(1).txt\")\n\n    def test_multiple_conflicts(self, project_folder):\n        # Create two temp files to see if increased to the next one\n        (project_folder / \"tempfile.txt\").touch()\n        (project_folder / \"tempfile(1).txt\").touch()\n        result = Paths.get_unique_destination_filepath(project_folder / \"tempfile.txt\")\n        assert result == (project_folder / \"tempfile(2).txt\")\n"
  }
]