[
  {
    "path": ".coveragerc",
    "content": "[run]\nomit =\n    # Do not run coverage on environment files\n    *env*\n    # Skip GUI coverage\n    kintree/kintree_gui.py\n    kintree/gui/*\n    kintree/common/progress.py\n    # Skip test script\n    run_tests.py"
  },
  {
    "path": ".github/workflows/test_deploy.yaml",
    "content": "name: tests | linting | publishing\n\non:\n  push:\n    branches:\n      - main\n    tags:\n      - \"*.*.*\"\n    paths-ignore:\n      - README.md\n      - images/**\n  pull_request:\n    branches:\n      - main\n\njobs:\n  style:\n    name: Style checks\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        python-version: ['3.9', '3.10', '3.11', '3.12']\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n      - name: set up python ${{ matrix.python-version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          pip install -U flake8 invoke\n      - name: PEP checks\n        run: >\n          invoke style\n\n  tests:\n    name: Integration tests\n\n    runs-on: ubuntu-latest\n    env:\n      INVENTREE_DB_ENGINE: django.db.backends.sqlite3\n      INVENTREE_DB_NAME: ${{ github.workspace }}/InvenTree/inventree_default_db.sqlite3\n      INVENTREE_MEDIA_ROOT: ${{ github.workspace }}/InvenTree\n      INVENTREE_STATIC_ROOT: ${{ github.workspace }}/InvenTree/static\n      INVENTREE_BACKUP_DIR: ${{ github.workspace }}/InvenTree/backup\n      INVENTREE_ENV: 0\n      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      TOKEN_DIGIKEY: ${{ secrets.TOKEN_DIGIKEY }}\n      DIGIKEY_CLIENT_ID: ${{ secrets.DIGIKEY_CLIENT_ID }}\n      DIGIKEY_CLIENT_SECRET: ${{ secrets.DIGIKEY_CLIENT_SECRET }}\n      DIGIKEY_LOCAL_SITE: US\n      DIGIKEY_LOCAL_LANGUAGE: en\n      DIGIKEY_LOCAL_CURRENCY: USD\n      TME_API_TOKEN: ${{ secrets.TME_API_TOKEN }}\n      TME_API_SECRET: ${{ secrets.TME_API_SECRET }}\n\n    continue-on-error: true\n    strategy:\n      matrix:\n        python-version: ['3.9', '3.10', '3.11', '3.12']\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          sudo apt install sqlite3\n          pip install -U pip invoke coveralls\n      - name: InvenTree setup\n        run: |\n          git clone https://github.com/inventree/InvenTree/\n          mkdir InvenTree/static\n          cp tests/files/inventree_default_db.sqlite3 InvenTree/\n          cd InvenTree/ && git switch stable && invoke install && invoke migrate && cd -\n      - name: Ki-nTree setup\n        run: |\n          invoke install\n          mkdir -p ~/.config/kintree/user/ && mkdir -p ~/.config/kintree/cache/search/\n          cp tests/files/inventree_dev.yaml ~/.config/kintree/user/\n          cp tests/files/kicad_map.yaml ~/.config/kintree/user/\n          cp tests/files/digikey_config.yaml ~/.config/kintree/user/\n          cp tests/files/results.tgz ~/.config/kintree/cache/search/\n          cd ~/.config/kintree/cache/search/ && tar xvf results.tgz && cd -\n      - name: GUI test\n        run: |\n          python kintree_gui.py b > gui.log 2>&1 &\n          sleep 2\n          cat gui.log\n          export len_log=$(cat gui.log | wc -l)\n          [[ ${len_log} -eq 0 ]] && true || false\n      - name: Setup Digi-Key token\n        if: ${{ github.ref == 'refs/heads/main' || github.event.pull_request.head.repo.full_name == 'sparkmicro/Ki-nTree' }}\n        run: |\n          git clone https://$TOKEN_DIGIKEY@github.com/eeintech/digikey-token.git\n          cd digikey-token/\n          python digikey_token_refresh.py\n          git config --global user.email \"kintree@github.actions\"\n          git config --global user.name \"Ki-nTree Github Actions\"\n          git add -u\n          git diff-index --quiet HEAD || git commit -m \"Update token\"\n          git push origin master\n          cp token_storage.json ~/.config/kintree/cache/\n          dk_token=$(cat ~/.config/kintree/cache/token_storage.json)\n          echo -e \"Digi-Key Token: $dk_token\\n\"\n          cd ..\n      - name: Run tests\n        if: ${{ github.ref == 'refs/heads/main' || github.event.pull_request.head.repo.full_name == 'sparkmicro/Ki-nTree' }}\n        run: |\n          invoke test -e 1\n        env:\n          MOUSER_PART_API_KEY: ${{ secrets.MOUSER_PART_API_KEY }}\n          ELEMENT14_PART_API_KEY: ${{ secrets.ELEMENT14_PART_API_KEY }}\n      - name: Run tests (skip APIs)\n        if: ${{ github.ref != 'refs/heads/main' && github.event.pull_request.head.repo.full_name != 'sparkmicro/Ki-nTree' }}\n        run: |\n          invoke test -e 0\n      - name: Coveralls\n        if: ${{ github.ref == 'refs/heads/main' || github.event.pull_request.head.repo.full_name == 'sparkmicro/Ki-nTree' }}\n        run: |\n          coveralls --version\n          coveralls --service=github\n      - name: Run build\n        run: |\n          invoke build\n\n  test-publish:\n    name: Publish to Test PyPI, then PyPI\n    if: startsWith(github.ref, 'refs/tags/')\n    runs-on: ubuntu-latest\n    needs:\n      - style\n      - tests\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n      - name: Alter the version in pyproject.toml and overwrite __version__\n        run: >\n          GTAG=$(echo $REF | sed -e 's#.*/##') &&\n          sed\n          --in-place\n          --expression\n          \"s/version = \\\".*\\\" # placeholder/version = \\\"$GTAG\\\"/g\"\n          pyproject.toml\n          && echo \"__version__ = '$GTAG'\" > kintree/__init__.py\n        env:\n          REF: ${{ github.ref }}\n      - name: Display the inferred version\n        run: |\n          head pyproject.toml\n          head kintree/__init__.py\n      - name: Set up Python 3.10\n        uses: actions/setup-python@v2\n        with:\n          python-version: '3.10'\n      - name: Install dependencies\n        run: pip install -U poetry\n      - name: Install poetry dependencies\n        run: poetry install --no-root --no-interaction\n      - name: Build the package\n        run: poetry build --no-interaction\n      - name: Set up TestPyPI repo in poetry\n        run: poetry config repositories.test https://test.pypi.org/legacy/\n      - name: Publish to Test PyPI\n        run: >\n          poetry publish\n          --repository \"test\"\n          --username \"__token__\"\n          --password \"$TOKEN_TEST_PYPI\"\n        env:\n          TOKEN_TEST_PYPI: ${{ secrets.TOKEN_TEST_PYPI }}\n      - name: Publish to PyPI\n        run: >\n          poetry publish\n          --username \"__token__\"\n          --password \"$TOKEN_PYPI\"\n        env:\n          TOKEN_PYPI: ${{ secrets.TOKEN_PYPI }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Build files\ndist/\nbuild/\n*.spec\n# Cache and backup files\n*__pycache__*\n*.bck\n# Test files\nkintree/tests/*\n.coverage\nhtmlcov/\n.vscode/launch.json\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# <img src=\"https://raw.githubusercontent.com/sparkmicro/Ki-nTree/main/images/logo.png\" width=\"auto\" height=\"32\"> Ki-nTree\n### Fast part creation for [KiCad](https://kicad.org/) and [InvenTree](https://inventree.org/) \n[![License: GPL v3.0](https://img.shields.io/badge/license-GPL_v3.0-green.svg)](https://www.gnu.org/licenses/gpl-3.0)\n[![Python Versions](https://raw.githubusercontent.com/sparkmicro/Ki-nTree/main/images/python_versions.svg)](https://www.python.org/)\n[![PyPI](https://img.shields.io/pypi/v/kintree)](https://pypi.org/project/kintree/)\n[![Tests | Linting | Publishing](https://github.com/sparkmicro/Ki-nTree/actions/workflows/test_deploy.yaml/badge.svg)](https://github.com/sparkmicro/Ki-nTree/actions)\n[![Coverage Status](https://coveralls.io/repos/github/sparkmicro/Ki-nTree/badge.svg?branch=main&service=github)](https://coveralls.io/github/sparkmicro/Ki-nTree?branch=main)\n\n## :warning: InvenTree Compatibility\nInvenTree `0.12` introduced a new parameter template system which was not supported until version `0.12.6` came out, see https://github.com/sparkmicro/Ki-nTree/issues/165 for more details. In short, Ki-nTree currently supports InvenTree `0.11` or older versions, and `0.12.6` and future versions. From `0.13.0` onwards a InvenTree setting needs to be modified for Ki-nTree to work: `InvenTree Settings` -> `Part Parameters` -> `Enforce Parameter Units` -> **OFF** (requires administrator rights)\n\n## :fast_forward: [Demo Video](https://youtu.be/YeWBqOCb4pw)\n\n<img src=\"https://raw.githubusercontent.com/sparkmicro/Ki-nTree/main/images/doc/kintree_v1_example.png\" width=\"auto\" height=\"auto\">\n\n## Introduction\nKi-nTree (pronounced \"Key Entry\" or \"Key 'n' Tree\") aims to:\n* automate part creation of KiCad library parts\n* automate part creation of InvenTree parts\n* synchronize parts data between KiCad and InvenTree\n\nKi-nTree works with:\n- [Digi-Key](https://developer.digikey.com/), [Mouser](https://www.mouser.com/api-hub/), [Element14](https://partner.element14.com/docs) and [LCSC](https://lcsc.com/) **enormous** part databases and free APIs\n- the awesome open-source [InvenTree Inventory Management System](https://github.com/inventree/inventree) built and maintained by [@SchrodingersGat](https://github.com/SchrodingersGat) and [@matmair](https://github.com/matmair)\n- the reliable and SCM-friendly KiCad file parser [KiUtils](https://github.com/mvnmgrx/kiutils) built and maintained by [@mvnmgrx](https://github.com/mvnmgrx)\n- the amazing [Digi-Key API python library](https://github.com/peeter123/digikey-api) built and maintained by [@peeter123](https://github.com/peeter123)\n- the [Mouser Python API](https://github.com/sparkmicro/mouser-api/) built and maintained by [@eeintech](https://github.com/eeintech)\n\n> :warning: **Important Note**\n>\n> Ki-nTree version `1.2.x` and forward support Digi-Key API version **4 only**.\n>\n> Ki-nTree version `1.0.x` and forward support KiCad versions **6 and up**.\n>\n> Ki-nTree versions `0.5.x` and `0.6.x` only support KiCad version **6** (`pip install kintree==0.6.6`).\n>\n> To use with KiCad version **5**, use older Ki-nTree `0.4.x` versions (`pip install kintree==0.4.8`).\n\nKi-nTree was developed by [@eeintech](https://github.com/eeintech) for [SPARK Microsystems](https://www.sparkmicro.com/), who generously accepted to make it open-source!\n\n## Get Started\n\n### Requirements\n\n* Ki-nTree is currently tested for Python 3.9 to 3.12 versions.\n* Ki-nTree requires a Digi-Key **production** API instance. To create one, go to https://developer.digikey.com/. Create an account, an organization and add a **production** API to your organization. Save both Client ID and Secret keys.\n> [Here is a video](https://youtu.be/OI1EGEc0Ju0) to help with the different steps\n* Ki-nTree requires a Mouser Search API key. To request one, head over to https://www.mouser.ca/api-search/ and click on \"Sign Up for Search API\"\n* Ki-nTree requires an Element14 Product Search API key to fetch part information for the following suppliers: Farnell (Europe), Newark (North America) and Element14 (Asia-Pacific). To request one, head over to https://partner.element14.com/ and click on \"Register\"\n* on rolling release distributions like Arch Linux some Flet dependencies need to be repaired manually:\n```\nsudo pacman -S mpv\nsudo ln -s /usr/lib/libmpv.so /usr/lib/libmpv.so.1 \n```\n\n### Installation (system wide)\n\n1. Install using Pip\n\n``` bash\npip install -U kintree\n```\n\n2. Run Ki-nTree\n\n``` bash\nkintree\n```\n\n### Run in virtual environment (contained)\n\n##### Linux / MacOS\n\nCreate a virtual environment and activate it with:\n\n``` bash\n$ python3 -m venv env-kintree\n$ source env-kintree/bin/activate\n```\n\nThen follow the steps from the [installation section](#installation-system-wide).\n\n##### Windows\n\nIn Git Bash, use the following commands to create and activate a virtual environment:\n``` bash\n$ python3 -m venv env-kintree\n$ source env-kintree/Scripts/activate\n```\nFor any other Windows terminal, refer to the [official documentation](https://docs.python.org/library/venv.html#creating-virtual-environments)\n\n### Packages\n#### Arch Linux\n\nKi-nTree is [available on Arch Linux's AUR](https://aur.archlinux.org/packages/python-kintree/) as `python-kintree`.\n\n### Usage Instructions\n\n#### Before Starting\n\nIf you intend to use Ki-nTree with InvenTree, this tool offers to setup the InvenTree category tree with a simple script that you can run as follow:\n\n> :warning: Warning: Before running it, make sure you have setup your category tree in your `categories.yaml` configuration file according to your own preferences, else it will use the [default setup](https://github.com/sparkmicro/Ki-nTree/blob/main/kintree/config/inventree/categories.yaml).\n\n``` bash\npython3 -m kintree.setup_inventree\n```\n\nIf the InvenTree category tree is **not setup** before starting to use Ki-nTree, you **won't be able to add parts** to InvenTree.\n\n#### Advanced Configuration\n\nConfiguration files are stored in the folder pointed by the `Configuration Files Folder` path in the \"User Settings\" window:\n\n<img src=\"https://raw.githubusercontent.com/sparkmicro/Ki-nTree/main/images/doc/kintree_v1_settings_user.png\" width=\"600\" height=\"auto\">\n\n<details>\n<summary><b>Click here to read about configuration files</b></summary>\n<p>\n\nKi-nTree uses a number of YAML configuration files to function. New users shouldn't need to worry about them to try out Ki-nTree (except for `categories.yaml` as mentioned in the previous section), however they can be modified to customize behavior and workflow.\n\nBelow is a summary table of the different configuration files, their function and if they are updated by the GUI:\n| Filename | Function | GUI Update? |\n| --- | --- | --- |\n| `categories.yaml` | InvenTree categories hierarchy tree and category codes for IPN generation (see [Before Starting section](#before-starting)) | :x: |\n| `general.yaml` | General user settings | :heavy_check_mark: |\n| `internal_part_number.yaml` | Controls for IPN generation | :heavy_check_mark: |\n| `inventree_<env>.yaml` | InvenTree login credentials, per environment (`<env>=['dev', 'prod']`) | :heavy_check_mark: |\n| `kicad.yaml` | KiCad symbol, footprint and library paths | :heavy_check_mark: |\n| `kicad_map.yaml` | Mapping between InvenTree parent categories and KiCad symbol/footprint libraries and templates | :x: |\n| `parameters.yaml` | List of InvenTree parameters templates (see [InvenTree Part Parameters documentation](https://docs.inventree.org/en/latest/part/parameter/)) | :x: |\n| `parameters_filters.yaml` | Mapping between InvenTree parent categories and InvenTree parameters templates | :x: |\n| `search_api.yaml` | Generic controls for Supplier search APIs like cache validity | :heavy_check_mark: |\n| `supplier_parameters.yaml` | Mapping between InvenTree parameters templates and suppliers parameters/attributes, sorted by InvenTree parent categories (see [Part Parameters section](#part-parameters)) | :x: |\n| `<supplier>_config.yaml` | Mapping for supplier name and search results fields, to overwrite defaults (`<supplier>=['digikey', 'element14', 'lcsc', 'mouser']`) | :x: |\n| `<supplier>_api.yaml` | Required supplier API fields, custom to each supplier (`<supplier>=['digikey', 'element14', 'lcsc', 'mouser']`) | :heavy_check_mark: |\n| `digikey_categories.yaml` | Mapping between InvenTree categories and Digi-Key categories | :x: |\n| `digikey_parameters.yaml` | Mapping between InvenTree parameters and Digi-Key parameters/attributes | :x: |\n\n> Ki-nTree only supports matching between InvenTree and Digi-Key categories and parameters/attributes (help wanted!)\n\n</p>\n</details>\n\n#### InvenTree Permissions\n\nEach InvenTree user has a set of permissions associated to them.\nPlease refer to the [InvenTree documentation](https://inventree.readthedocs.io/en/latest/settings/permissions/) to understand how to setup user permissions.\n\nThe minimum set of user/group permission required to add parts to InvenTree is:\n- \"Part - Add\" to add InvenTree parts\n- \"Purchase Orders - Add\" to add manufacturers, suppliers and their associated parts\n\nIf you wish to automatically add subcategories while creating InvenTree parts, you will need to enable the \"Part Categories - Add\" permission.\n\nNote that each time you enable the \"Add\" permission to an object, InvenTree automatically enables the \"Change\" permission to that object too.\n\n#### Settings\n1. With Ki-nTree GUI open, click on \"Settings > Supplier > Digi-Key\" and fill in both Digi-Key API Client ID and Secret keys (optional: click on \"Test\" to [get an API token](#get-digi-key-api-token))\n2. Click on \"Settings > Supplier > Mouser\" and fill in the Mouser part search API key\n3. Click on \"Settings > Supplier > Element14\" and fill in the Element14 product search API key (key is shared with Farnell and Newark)\n4. Click on \"Settings > KiCad\", browse to the location where KiCad symbol, template and footprint libraries are stored on your computer then click \"Save\"\n5. If you intend to use InvenTree with this tool, click on \"Settings > InvenTree\" and fill in your InvenTree server address and credentials then click \"Save\" (optional: click on \"Test\" to check communication with server)  \n  a. It is possible to define a Proxy Server over which all interactions with InvenTree will be routed. To set a proxy server use the \"Enable Proxy Support\" switch in \"Settings > InvenTree\" and define the proxy address in the new input field.  \n  b. Instead of user credential authentication token authentication is also supported. To use a token add it it to the \"Password or Token\" field and leave the \"Username\" empty. You can retrieve your personal access token from your InvenTree server by sending an get-request to its api url `api/user/token/`.\n  c. If needed this tool can try to download the parts datasheet from the suppliers and upload it it to the attachment section of each part. For this just activate \"Upload Datasheets to InvenTree\" in the InvenTree settings\n  d. It is also possible to sync the prices in InvenTree with the latest supplier prices. For this enable \"Upload Pricing Data to InvenTree\"\n6. If your InvenTree server requires a IPN in a specific pattern make sure to adjust \"Settings > InvenTree > Internal Part Number\" to match it or adjust the servers pattern to the one yo set in Ki-nTree \n\n> Note: All URLs should start with \"http://\" if they do not have a valid SSL certificate.\n\n#### Get Digi-Key API token\n<details>\n<summary>Show steps (click to expand)</summary>\n<p>\n\nEnter your Digi-Key developer account credentials then login. The following page will appear (`user@email.com` will show your email address):\n\n<img src=\"https://raw.githubusercontent.com/sparkmicro/Ki-nTree/main/images/doc/digikey_api_approval_request.png\" width=\"600\" height=\"auto\">\n\nClick on \"Allow\", another page will open.  \nClick on the \"Advanced\" button, then click on \"Proceed to localhost (unsafe)\" at the bottom of the page:\n\n<img src=\"https://raw.githubusercontent.com/sparkmicro/Ki-nTree/main/images/doc/digikey_api_approval_request2.png\"  width=\"600\" height=\"auto\">\n\n> On Chrome, if the \"Proceed to localhost (unsafe)\" link does not appear, enable the following flag: [chrome://flags/#allow-insecure-localhost](chrome://flags/#allow-insecure-localhost)\n\nLastly, a new page will open with a \"You may now close this window.\" message, proceed to get the token.\n\n</p>\n</details>\n\n#### Part Parameters\n\nKi-nTree uses **supplier** parameters to populate **InvenTree** parameters. In order to match between supplier and InvenTree, users need to setup the configuration file `supplier_parameters.yaml` with the following mapping for each category:\n``` yaml\nCATEGORY_NAME:\n  INVENTREE_PARAMETER_NAME:\n    - SUPPLIER_1_PARAMETER_NAME_1\n    - SUPPLIER_1_PARAMETER_NAME_2\n    - SUPPLIER_2_PARAMETER_NAME_1\n```\n\nIt is also possible to cross reference the mappings of different categories. To define one or multiple parent categories a parameter named `parent` can be added where the items then are the desired parent categories. If a parameter name is present in both parent and child, the childs definition will override the parent.\n\nA template image for an category can be set by using the `image` parameter. The sole item in this parameter must the filename of an already existing part image on the the InvenTree server.\n\nRefer to [this file](https://github.com/sparkmicro/Ki-nTree/blob/main/kintree/config/inventree/supplier_parameters.yaml) as a starting point / example.\n\n#### Part Number Search\n\nKi-nTree currently supports APIs for the following electronics suppliers: Digi-Key, Mouser, Element14, TME and LCSC.\n\n1. In the main window, enter the part number and select the supplier in drop-down list, then click the \"Submit\" button (arrow). It will start by fetching part data using the supplier's API\n2. In the case Digi-Key has been selected and the API token is not found or expired, a browser window will pop-up. To get a new token: [follow those steps](#get-digi-key-api-token)\n3. Once the part data has been successfully fetched from the supplier's API, you can review the part information in the different fields and edit them, if needed.\n4. Then, go to the Inventree tabl to pick the `Category` and `Subcategory` to use for this part\n5. If you desire to add this part to KiCad, click the KiCad tab and select the KiCad symbol library, the template and the footprint library to use for this part\n6. Finally, go to the Create tab and launch the part creation. It will take some time to complete the process in InvenTree and/or KiCad, once it finishes you'll be notified of the result  \n\nIf the part was created or found in InvenTree, and if you have selected this option in the settings, your browser will automatically open and navigate to the new Inventree part page.\n\n#### Kicad Templates\n\nThe automatic part generation in KiCad is controlled via templates:\n\n* Template examples are shipped together with Ki-nTree, these can be adjusted to your liking or you also can create completely new ones.\n* Each template has its own library file where the file name defines the templates name.\n* The templates can use the parameters and attributes of the InvenTree part on a wildcard base. So you can add for example `Resistance@Tolerance` into a field and the resulting part will then have the resistance and the tolerance value inside this text field. \n* Using the templates and wildcards without the InvenTree functions enabled is also possible. In this case the library parameter wildcards need to be configured in the `supplier_parameters.yaml` for each library individually.\n\n\nEnjoy!\n\n*For any problem/bug you find, please [report an issue](https://github.com/sparkmicro/Ki-nTree/issues).*\n\n## Development\n\n### Requirements\n\nYou need `python>=3.9` and `poetry`.\n\nYou can install poetry by following the instructions [on its official website](https://python-poetry.org/docs/master/#installation), by using `pip install poetry` or by installing a package on your Linux distro.\n\n### Setup and run\n1. Clone this repository\n``` bash\ngit clone https://github.com/sparkmicro/Ki-nTree\n```\n\n2. Install the requirements into a `poetry`-managed virtual environment\n``` bash\npoetry install\nInstalling dependencies from lock file\n...\nInstalling the current project: kintree (1.1.99)\n```\n> Note: the version is not accurate (placeholder only)\n\n3. Run Ki-nTree in the virtual environment\n```bash\npoetry run python -m kintree_gui\n```\nor\n\n```bash\n$ poetry shell\n$ python -m kintree_gui\n```\n\n#### Build\n1. Make sure you followed the previous installation steps, then run:\n``` bash\n$ poetry build\nBuilding kintree (1.1.99)\n  - Building sdist\n  - Built kintree-1.1.99.tar.gz\n  - Building wheel\n  - Built kintree-1.1.99-py3-none-any.whl\n```\n2. Exit the virtual environment (`Ctrl + D` on Linux; you can also close the\n   terminal and reopen it in the same folder).\n\n   Run `pip install dist/<wheel_file>.whl` with the file name from the previous\n   step. For example:\n\n```bash\npip install dist/kintree-1.1.99-py3-none-any.whl\n```\n\n3. You can now start Ki-nTree by typing `kintree` in the terminal, provided\n   that your python dist path is a part of your `$PATH`.\n\n## License\nThe Ki-nTree source code is licensed under the [GPL3.0 license](https://github.com/sparkmicro/Ki-nTree/blob/main/LICENSE) as it uses source code under that license:\n* https://github.com/mvnmgrx/kiutils\n* https://github.com/peeter123/digikey-api\n\nThe [KiCad templates](https://github.com/sparkmicro/Ki-nTree/tree/main/kintree/kicad/templates) are licensed under the [Creative Commons CC0 1.0 license](https://github.com/sparkmicro/Ki-nTree/blob/main/kintree/kicad/templates/LICENSE) which means that \"you can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission\" ([reference](https://creativecommons.org/publicdomain/zero/1.0/)).\n"
  },
  {
    "path": "invoke.yaml",
    "content": "debug: true\nrun:\n    echo: false"
  },
  {
    "path": "kintree/__init__.py",
    "content": "# WARNING: This file is overwriten when publishing to PyPI\n# __version__ refers to the tag version instead\n\n# VERSION INFORMATION\nversion_info = {\n    'MAJOR_REVISION': 1,\n    'MINOR_REVISION': 2,\n    'RELEASE_STATUS': '1',\n}\n\n__version__ = '.'.join([str(v) for v in version_info.values()])\n"
  },
  {
    "path": "kintree/common/part_tools.py",
    "content": "import re\n\nfrom ..config import settings\nfrom ..config import config_interface\nfrom .tools import cprint\n\n\ndef generate_part_number(category: str, part_pk: int, category_code='') -> str:\n    ''' Generate Internal Part Number (IPN) '''\n    ipn_elements = []\n\n    # Prefix\n    if settings.CONFIG_IPN.get('IPN_ENABLE_PREFIX', False):\n        ipn_elements.append(settings.CONFIG_IPN.get('IPN_PREFIX', ''))\n    \n    # Category code\n    if settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False):\n        if not category_code:\n            CATEGORY_CODES = config_interface.load_file(settings.CONFIG_CATEGORIES)['CODES']\n            try:\n                category_code = CATEGORY_CODES.get(category, '')\n            except AttributeError:\n                category_code = None\n        if category_code:\n            ipn_elements.append(category_code)\n\n    # Unique ID (mandatory)\n    try:\n        unique_id = str(part_pk).zfill(int(settings.CONFIG_IPN.get('IPN_UNIQUE_ID_LENGTH', '6')))\n    except:\n        return None\n    ipn_elements.append(unique_id)\n    \n    # Suffix\n    if settings.CONFIG_IPN.get('IPN_ENABLE_SUFFIX', False):\n        ipn_elements.append(settings.CONFIG_IPN.get('IPN_SUFFIX', ''))\n    \n    # Build IPN\n    ipn = '-'.join(ipn_elements)\n\n    return ipn\n\n\ndef compare(new_part_parameters: dict, db_part_parameters: dict, include_filters: list) -> bool:\n    ''' Compare two InvenTree parts based on parameters (specs) '''\n    try:\n        for parameter, value in new_part_parameters.items():\n            # Check for filters\n            if include_filters:\n                # Compare only parameters present in include_filters\n                if parameter in include_filters and value != db_part_parameters[parameter]:\n                    return False\n            else:\n                # Compare all parameters\n                if value != db_part_parameters[parameter]:\n                    return False\n    except KeyError:\n        cprint('[INFO]\\tWarning: Failed to compare part with database', silent=settings.HIDE_DEBUG)\n        return False\n\n    return True\n\n\ndef clean_parameter_value(category: str, name: str, value: str) -> str:\n    ''' Clean-up parameter value for consumption in InvenTree and KiCad '''\n    category = category.lower()\n    name = name.lower()\n\n    # Parameter specific filters\n    # Package\n    if 'package' in name and 'size' not in name:\n        space_split = value.split()\n\n        # Return value before the space\n        if len(space_split) > 1:\n            value = space_split[0].replace(',', '')\n\n    # Sizes\n    if 'size' in name or \\\n            'height' in name or \\\n            'pitch' in name or \\\n            'outline' in name:\n        # imperial = re.findall('[.0-9]*\"', value)\n        metric = re.findall('[.0-9]*mm', value)\n        len_metric = len(metric)\n\n        # Return only the metric dimensions\n        if len_metric > 0 and len_metric <= 1:\n            # One dimension\n            if 'dia' in value.lower():\n                # Check if diameter value\n                value = '⌀' + metric[0]\n            else:\n                value = metric[0]\n        elif len_metric > 1 and len_metric <= 2:\n            # Two dimensions\n            value = metric[0].replace('mm', '') + 'x' + metric[1]\n        elif len_metric > 2 and len_metric <= 3:\n            # Three dimensions\n            value = metric[0].replace('mm', '') + 'x' + metric[1].replace('mm', '') + 'x' + metric[2]\n\n    # Power\n    if 'power' in name:\n        # decimal = re.findall('[0-9]\\.[0-9]*W', value)\n        ratio = re.findall('[0-9]/[0-9]*W', value)\n\n        # Return ratio\n        if len(ratio) > 0:\n            value = ratio[0]\n\n    # ESR, DCR, RDS\n    if 'esr' in name or \\\n            'dcr' in name or \\\n            'rds' in name:\n        value = value.replace('Max', '').replace(' ', '').replace('Ohm', 'R')\n\n    # Category specific filters\n    # RESISTORS\n    if 'resistor' in category:\n        if 'resistance' in name:\n            space_split = value.split()\n\n            if len(space_split) > 1:\n                resistance = space_split[0]\n                unit = space_split[1]\n\n                unit_filter = ['kOhms', 'MOhms', 'GOhms']\n                if unit in unit_filter:\n                    unit = unit.replace('Ohms', '').upper()\n                else:\n                    unit = unit.replace('Ohms', 'R')\n\n                value = resistance + unit\n\n    # General filters\n    # Clean-up ranges\n    separator = '~'\n    if separator in value:\n        space_split = value.split()\n        first_value = space_split[0]\n        if len(space_split) > 2:\n            second_value = space_split[2]\n\n            # Substract digits, negative sign, points from first value to get unit\n            unit = first_value.replace(re.findall('[-.0-9]*', first_value)[0], '')\n\n            if unit:\n                value = first_value.replace(unit, '') + separator + second_value\n\n    # Remove parenthesis section\n    if '(' in value:\n        parenthesis = re.findall(r'\\(.*\\)', value)\n\n        if parenthesis:\n            for item in parenthesis:\n                value = value.replace(item, '')\n\n            # Remove leftover spaces\n            value = value.replace(' ', '')\n\n    # Remove spaces (for specific cases)\n    if '@' in value:\n        value = value.replace(' ', '')\n\n    # Escape double-quote (else causes library error in KiCad)\n    if '\"' in value:\n        value = value.replace('\"', '\\\\\"')\n\n    # cprint(value)\n    return value\n"
  },
  {
    "path": "kintree/common/progress.py",
    "content": "import time\n\nCREATE_PART_PROGRESS: float\nMAX_PROGRESS = 1.0\nDEFAULT_PROGRESS = 0.1\n\n\ndef reset_progress_bar(progress_bar) -> bool:\n    ''' Reset progress bar '''\n    global CREATE_PART_PROGRESS\n\n    # Reset progress\n    CREATE_PART_PROGRESS = 0\n    progress_bar.color = None\n    progress_bar.value = 0\n    progress_bar.update()\n    time.sleep(0.1)\n\n    return True\n\n\ndef progress_increment(inc):\n    ''' Increment progress '''\n    global CREATE_PART_PROGRESS, MAX_PROGRESS\n\n    if CREATE_PART_PROGRESS + inc < MAX_PROGRESS:\n        CREATE_PART_PROGRESS += inc\n    else:\n        CREATE_PART_PROGRESS = MAX_PROGRESS\n\n    return CREATE_PART_PROGRESS\n\n\ndef update_progress_bar(progress_bar, increment=0) -> bool:\n    ''' Update progress bar during part creation '''\n    global DEFAULT_PROGRESS\n\n    if not progress_bar:\n        return True\n\n    if increment:\n        inc = increment\n    else:\n        # Default\n        inc = DEFAULT_PROGRESS\n\n    current_value = progress_bar.value * 100\n    new_value = progress_increment(inc) * 100\n    # Smooth progress\n    for i in range(int(new_value - current_value)):\n        progress_bar.value += i / 100\n        progress_bar.update()\n        time.sleep(0.05)\n\n    return True\n"
  },
  {
    "path": "kintree/common/tools.py",
    "content": "import builtins\nimport json\nimport os\nfrom shutil import copyfile\n\n\n# CUSTOM PRINT METHOD\nclass pcolors:\n    HEADER = '\\033[95m'\n    OKBLUE = '\\033[94m'\n    OKGREEN = '\\033[92m'\n    WARNING = '\\033[93m'\n    ERROR = '\\033[91m'\n    ENDC = '\\033[0m'\n    BOLD = '\\033[1m'\n    UNDERLINE = '\\033[4m'\n\n# Overload print function with custom pretty-print\n\n\ndef cprint(*args, **kwargs):\n    # Check if silent is set\n    try:\n        silent = kwargs.pop('silent')\n    except:\n        silent = False\n    if not silent:\n        if type(args[0]) is dict:\n            return builtins.print(json.dumps(*args, **kwargs, indent=4, sort_keys=True))\n        else:\n            try:\n                args = list(args)\n                if 'warning' in args[0].lower():\n                    args[0] = f'{pcolors.WARNING}{args[0]}{pcolors.ENDC}'\n                elif 'error' in args[0].lower():\n                    args[0] = f'{pcolors.ERROR}{args[0]}{pcolors.ENDC}'\n                elif 'fail' in args[0].lower():\n                    args[0] = f'{pcolors.ERROR}{args[0]}{pcolors.ENDC}'\n                elif 'success' in args[0].lower():\n                    args[0] = f'{pcolors.OKGREEN}{args[0]}{pcolors.ENDC}'\n                elif 'pass' in args[0].lower():\n                    args[0] = f'{pcolors.OKGREEN}{args[0]}{pcolors.ENDC}'\n                elif 'main' in args[0].lower():\n                    args[0] = f'{pcolors.HEADER}{args[0]}{pcolors.ENDC}'\n                elif 'skipping' in args[0].lower():\n                    args[0] = f'{pcolors.BOLD}{args[0]}{pcolors.ENDC}'\n                args = tuple(args)\n            except:\n                pass\n            return builtins.print(*args, **kwargs, flush=True)\n###\n\n\ndef create_library(library_path: str, symbol: str, template_lib: str):\n    ''' Create library files if they don\\'t exist '''\n    if not os.path.exists(library_path):\n        os.mkdir(library_path)\n    new_kicad_sym_file = os.path.join(library_path, f'{symbol}.kicad_sym')\n    if not os.path.exists(new_kicad_sym_file):\n        copyfile(template_lib, new_kicad_sym_file)\n\n\ndef get_image_with_retries(url, headers, retries=3, wait=5, silent=False):\n    \"\"\" Method to download image with cloudscraper library and retry attempts\"\"\"\n    import cloudscraper\n    import time\n    scraper = cloudscraper.create_scraper()\n    for attempt in range(retries):\n        try:\n            response = scraper.get(url, headers=headers, timeout=wait)\n            if response.status_code == 200:\n                return response\n            else:\n                cprint(f'[INFO]\\tWarning: Image download Attempt {attempt + 1} failed with status code {response.status_code}. Retrying in {wait} seconds...', silent=silent)\n        except Exception as e:\n            cprint(f'[INFO]\\tWarning: Image download Attempt {attempt + 1} encountered an error: {e}. Retrying in {wait} seconds...', silent=silent)\n        time.sleep(wait)\n    cprint('[INFO]\\tWarning: All Image download attempts failed. Could not retrieve the image.', silent=silent)\n    return None\n\n\ndef download(url, filetype='API data', fileoutput='', timeout=3, enable_headers=False, requests_lib=False, try_cloudscraper=False, silent=False):\n    ''' Standard method to download URL content, with option to save to local file (eg. images) '''\n\n    import socket\n    import urllib.request\n    import requests\n\n    # A more detailed headers was needed for request to Jameco\n    headers = {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36',\n        'Accept': 'applicaiton/json,image/webp,image/apng,image/*,*/*;q=0.8',\n        'Accept-Encoding': 'Accept-Encoding: gzip, deflate, br',\n        'Accept-Language': 'en-US,en;q=0.9',\n        'Connection': 'keep-alive',\n        'Cache-Control': 'no-cache',\n    }\n\n    # Set default timeout for download socket\n    socket.setdefaulttimeout(timeout)\n    if enable_headers and not requests_lib:\n        opener = urllib.request.build_opener()\n        opener.addheaders = list(headers.items())\n        urllib.request.install_opener(opener)\n    try:\n        if filetype == 'PDF':\n            # some distributors/manufacturers implement\n            # redirects which don't allow direct downloads\n            if 'gotoUrl' in url and 'www.ti.com' in url:\n                mpn = url.split('%2F')[-1]\n                url = f'https://www.ti.com/lit/ds/symlink/{mpn}.pdf'\n        if filetype == 'Image' or filetype == 'PDF':\n            # Enable use of requests library for downloading files (some URLs do NOT work with urllib)\n            if requests_lib:\n                response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True)\n                if filetype.lower() not in response.headers['Content-Type'].lower():\n                    cprint(f'[INFO]\\tWarning: {filetype} download returned the wrong file type', silent=silent)\n                    return None\n                with open(fileoutput, 'wb') as file:\n                    file.write(response.content)\n            elif try_cloudscraper:\n                response = get_image_with_retries(url, headers=headers)\n                if filetype.lower() not in response.headers['Content-Type'].lower():\n                    cprint(f'[INFO]\\tWarning: {filetype} download returned the wrong file type', silent=silent)\n                    return None\n                with open(fileoutput, 'wb') as file:\n                    file.write(response.content)\n            else:\n                (file, headers) = urllib.request.urlretrieve(url, filename=fileoutput)\n                if filetype.lower() not in headers['Content-Type'].lower():\n                    cprint(f'[INFO]\\tWarning: {filetype} download returned the wrong file type', silent=silent)\n                    return None\n            return file\n        else:\n            # some suppliers work with requests.get(), others need urllib.request.urlopen()\n            try:\n                response = requests.get(url)\n                data_json = response.json()\n                return data_json\n            except requests.exceptions.JSONDecodeError:\n                try:\n                    url_data = urllib.request.urlopen(url)\n                    data = url_data.read()\n                    data_json = json.loads(data.decode('utf-8'))\n                    return data_json\n                finally:\n                    pass\n    except (socket.timeout, requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout):\n        cprint(f'[INFO]\\tWarning: {filetype} download socket timed out ({timeout}s)', silent=silent)\n    except (urllib.error.HTTPError, requests.exceptions.ConnectionError):\n        cprint(f'[INFO]\\tWarning: {filetype} download failed (HTTP Error)', silent=silent)\n    except (urllib.error.URLError, ValueError, AttributeError):\n        cprint(f'[INFO]\\tWarning: {filetype} download failed (URL Error)', silent=silent)\n    except requests.exceptions.SSLError:\n        cprint(f'[INFO]\\tWarning: {filetype} download failed (SSL Error)', silent=silent)\n    except FileNotFoundError:\n        cprint(f'[INFO]\\tWarning: {os.path.dirname(fileoutput)} folder does not exist', silent=silent)\n    return None\n\n\ndef download_with_retry(url: str, full_path: str, silent=False, **kwargs) -> str:\n    ''' Standard method to download image URL to local file '''\n\n    if not url:\n        cprint('[INFO]\\tError: Missing image URL', silent=silent)\n        return False\n    \n    # Try without headers\n    file = download(url, fileoutput=full_path, silent=silent, **kwargs)\n\n    if not file:\n        # Try with headers\n        file = download(url, fileoutput=full_path, enable_headers=True, silent=silent, **kwargs)\n\n    if not file:\n        # Try with requests library\n        file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=True, silent=silent, **kwargs)\n    \n    if not file:\n        # Try with cloudscraper\n        file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=False, try_cloudscraper=True, silent=silent, **kwargs)\n\n    # Still nothing\n    if not file:\n        return False\n\n    cprint(f'[INFO]\\tDownload success ({url=})', silent=silent)\n    return True\n"
  },
  {
    "path": "kintree/config/automationdirect/automationdirect_api.yaml",
    "content": "AUTOMATIONDIRECT_API_ROOT_URL: \"https://www.automationdirect.com\"\nAUTOMATIONDIRECT_API_URL: \"https://www.automationdirect.com/ajax?&fctype=adc.falcon.search.SearchFormCtrl&cmd=AjaxSearch\"\nAUTOMATIONDIRECT_API_SEARCH_QUERY: \"&searchquery=\"              # can be anything but probably best to set to search term\nAUTOMATIONDIRECT_API_SEARCH_STRING: \"&solrQueryString=q%3D\"     # append search term to this and combine with other params\nAUTOMATIONDIRECT_API_IMAGE_PATH: \"https://cdn.automationdirect.com/images/products/medium/m_\" # image path for medium size image on product page"
  },
  {
    "path": "kintree/config/automationdirect/automationdirect_config.yaml",
    "content": "SUPPLIER_DATABASE_NAME: Automation Direct\nSEARCH_NAME: null\nSEARCH_DESCRIPTION: null\nSEARCH_REVISION: null\nSEARCH_KEYWORDS: null\nSEARCH_SKU: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_SUPPLIER_URL: null\nSEARCH_DATASHEET: null\nEXTRA_FIELDS: null"
  },
  {
    "path": "kintree/config/config_interface.py",
    "content": "import base64\nimport copy\nimport os\nfrom sys import platform\n\nimport yaml\nfrom ..common.tools import cprint\n\nFUNCTION_FILTER_KEY = '__'\n\n\ndef load_file(file_path: str, silent=True) -> dict:\n    ''' Safe load YAML file '''\n    try:\n        with open(file_path, 'r') as file:\n            try:\n                data = yaml.safe_load(file)\n            except yaml.YAMLError as exc:\n                print(exc)\n                return None\n    except FileNotFoundError:\n        cprint(f'[ERROR]\\tFile {file_path} does not exists!', silent=silent)\n        return None\n\n    return data\n\n\ndef dump_file(data: dict, file_path: str) -> bool:\n    ''' Safe dump YAML file '''\n    with open(file_path, 'w') as file:\n        try:\n            if platform == \"win32\":\n                yaml.safe_dump(data, file, default_flow_style=False)\n            else:\n                yaml.safe_dump(data, file, default_flow_style=False, allow_unicode=True)\n        except yaml.YAMLError as exc:\n            print(exc)\n            return False\n\n    return True\n\n\ndef load_user_paths(home_dir='') -> dict:\n    ''' Load user config and cache paths '''\n\n    user_settings_file = os.path.join(home_dir, 'settings.yaml')\n    user_config = load_file(user_settings_file)\n\n    if not user_config:\n        user_config = {\n            'USER_FILES': os.path.join(home_dir, 'user', ''),\n            'USER_CACHE': os.path.join(home_dir, 'cache', ''),\n        }\n        dump_file(user_config, user_settings_file)\n\n    return user_config\n\n\ndef load_user_config_files(path_to_root: str, path_to_user_files: str, silent=True) -> bool:\n    ''' Load user configuration files '''\n    result = True\n\n    def load_config(path):\n        for template_file in os.listdir(path):\n            filename = os.path.basename(template_file)\n            template_data = load_file(os.path.join(path, filename))\n            try:\n                user_data = load_file(os.path.join(path_to_user_files, filename))\n                if list(template_data.keys()) == list(user_data.keys()):\n                    # Join user data to template data\n                    user_settings = {**template_data, **user_data}\n                else:\n                    user_settings = user_data\n                    # Warn user about config data discrepancies with template data\n                    template_vs_user = set(template_data) - set(user_data)\n                    # user_vs_template = set(user_data) - set(template_data)\n                    if template_vs_user:\n                        print(f'[INFO]\\tTEMPLATE \"{filename}\" configuration file contains the following keys which are NOT in your user settings: {template_vs_user}')\n                    # if user_vs_template:\n                    #     cprint(f'[INFO]\\tUSER SETTINGS {filename} configuration file contains the following keys which are NOT in the template: {user_vs_template}', silent=silent)\n\n            except (TypeError, AttributeError):\n                cprint(f'[INFO]\\tCreating new {filename} configuration file', silent=silent)\n                # Config file does not exists\n                user_settings = template_data\n\n            dump_file(user_settings, os.path.join(path_to_user_files, filename))\n\n    for dir in ['user', 'inventree', 'kicad', 'digikey', 'mouser', 'element14', 'lcsc', 'tme', 'jameco', 'automationdirect']:\n        try:\n            # Load configuration\n            config_files = os.path.join(path_to_root, dir, '')\n            load_config(config_files)\n        except FileNotFoundError:\n            cprint(f'[INFO]\\tWarning: Failed to load {dir.title()} configuration', silent=silent)\n            result = False\n\n    return result\n\n\ndef load_inventree_user_settings(user_config_path: str) -> dict:\n    ''' Load InvenTree user settings from file '''\n    user_settings = load_file(user_config_path)\n\n    try:\n        password = user_settings.get('PASSWORD', None)\n    except AttributeError:\n        return user_settings\n\n    try:\n        # Use base64 encoding to make password unreadable inside the file\n        user_settings['PASSWORD'] = base64.b64decode(password).decode()\n    except TypeError:\n        user_settings['PASSWORD'] = ''\n\n    if 'ENABLE_PROXY' not in user_settings:\n        user_settings['ENABLE_PROXY'] = False\n    proxies = user_settings.get('PROXIES', None)\n    if not proxies:\n        user_settings['PROXY'] = ''\n    else:\n        # loading the proxy independent if it is http or https\n        user_settings['PROXY'] = list(proxies.values())[0]\n\n    if 'DATASHEET_UPLOAD' not in user_settings:\n        user_settings['DATASHEET_UPLOAD'] = False\n    if 'PRICING_UPLOAD' not in user_settings:\n        user_settings['PRICING_UPLOAD'] = False\n    return user_settings\n\n\ndef save_inventree_user_settings(enable: bool,\n                                 server: str,\n                                 username: str,\n                                 password: str,\n                                 enable_proxy: bool,\n                                 proxies: dict,\n                                 datasheet_upload: bool,\n                                 pricing_upload: bool,\n                                 user_config_path: str):\n    ''' Save InvenTree user settings to file '''\n    user_settings = {}\n\n    user_settings['ENABLE'] = enable\n    user_settings['SERVER_ADDRESS'] = server\n    user_settings['USERNAME'] = username\n    # Use base64 encoding to make password unreadable inside the file\n    user_settings['PASSWORD'] = base64.b64encode(password.encode())\n    user_settings['ENABLE_PROXY'] = enable_proxy\n    user_settings['PROXIES'] = proxies\n    user_settings['DATASHEET_UPLOAD'] = datasheet_upload\n    user_settings['PRICING_UPLOAD'] = pricing_upload\n\n    return dump_file(user_settings, user_config_path)\n\n\ndef load_library_path(user_config_path: str, silent=False):\n    ''' Load KiCad library from KiCad settings file '''\n    user_settings = load_file(user_config_path)\n\n    try:\n        if not user_settings['KICAD_SYMBOLS_PATH'] and not silent:\n            print('[INFO]\\tEmpty KiCad library path')\n        return user_settings['KICAD_SYMBOLS_PATH']\n    except:\n        # If not defined: use application root folder\n        return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\ndef add_library_path(user_config_path: str, category: str, symbol_library: str) -> bool:\n    ''' Save KiCad library to KiCad settings file '''\n    user_settings = load_file(user_config_path)\n\n    if category:\n        index = category\n    else:\n        index = symbol_library\n\n    if not user_settings['KICAD_LIBRARIES']:\n        user_settings['KICAD_LIBRARIES'] = {}\n\n    try:\n        if symbol_library not in user_settings['KICAD_LIBRARIES'][index]:\n            user_settings['KICAD_LIBRARIES'][index].append(symbol_library)\n    except:\n        user_settings['KICAD_LIBRARIES'][index] = [symbol_library]\n\n    return dump_file(user_settings, user_config_path)\n\n\ndef load_libraries_paths(user_config_path: str, library_path: str) -> dict:\n    ''' Construct KiCad library files names and paths from KiCad settings file '''\n    user_settings = load_file(user_config_path)\n\n    if not os.path.exists(library_path):\n        return None\n\n    found_library_files = []\n    for file in os.listdir(library_path):\n        if file.endswith('.kicad_sym'):\n            found_library_files.append(file.replace('.kicad_sym', ''))\n\n    symbol_libraries_paths = {}\n    assigned_files = []\n    try:\n        for category, libraries in user_settings['KICAD_LIBRARIES'].items():\n            symbol_libraries_paths[category] = {}\n            if libraries:\n                for library in libraries:\n                    if library in found_library_files:\n                        symbol_libraries_paths[category][library] = library_path + \\\n                            library + '.kicad_sym'\n                        assigned_files.append(library)\n    except:\n        pass\n\n    for file in found_library_files:\n        if file not in assigned_files:\n            try:\n                symbol_libraries_paths['uncategorized'].append(file)\n            except:\n                symbol_libraries_paths['uncategorized'] = [file]\n    try:\n        symbol_libraries_paths['uncategorized'] = sorted(\n            symbol_libraries_paths['uncategorized'])\n    except:\n        pass\n\n    # Check that library paths are loaded\n    path_loaded = False\n    for category, paths in symbol_libraries_paths.items():\n        if paths:\n            path_loaded = True\n            break\n    if not path_loaded:\n        return None\n\n    # print(symbol_libraries_paths)\n    return symbol_libraries_paths\n\n\ndef load_templates_paths(user_config_path: str, template_path: str) -> dict:\n    ''' Construct KiCad template files names and paths from KiCad settings file '''\n    symbol_templates_paths = {}\n    if not template_path or not os.path.exists(template_path):\n        return symbol_templates_paths\n\n    # Load configuration file\n    user_settings = load_file(user_config_path)\n\n    try:\n        for category in user_settings['KICAD_TEMPLATES'].keys():\n            for subcategory, file_name in user_settings['KICAD_TEMPLATES'][category].items():\n                if subcategory == 'Default' and not file_name:\n                    file_name = 'default'\n                if file_name:\n                    try:\n                        symbol_templates_paths[category][subcategory] = template_path + \\\n                            file_name + '.kicad_sym'\n                    except KeyError:\n                        symbol_templates_paths[category] = {\n                            subcategory: template_path + file_name + '.kicad_sym'\n                        }\n    except:\n        pass\n\n    return symbol_templates_paths\n\n\ndef load_footprint_paths(user_config_path: str, footprint_path: str) -> dict:\n    ''' Construct KiCad footprint folder names and paths from KiCad settings file '''\n    user_settings = load_file(user_config_path)\n\n    if not os.path.exists(footprint_path):\n        return None\n\n    found_library_folders = [item.replace('.pretty', '') for item in os.listdir(footprint_path)\n                             if os.path.isdir(footprint_path + item)]\n\n    footprint_libraries_paths = {}\n    assigned_folders = []\n    try:\n        for category, libraries in user_settings['KICAD_FOOTPRINTS'].items():\n            footprint_libraries_paths[category] = {}\n            if libraries:\n                for folder in libraries:\n                    footprint_libraries_paths[category][folder] = footprint_path + \\\n                        folder + '.pretty'\n                    assigned_folders.append(folder)\n    except:\n        pass\n\n    for folder in found_library_folders:\n        if folder not in assigned_folders:\n            try:\n                footprint_libraries_paths['uncategorized'].append(folder)\n            except:\n                footprint_libraries_paths['uncategorized'] = [folder]\n\n        # Sort uncategorized library paths\n        footprint_libraries_paths['uncategorized'] = sorted(\n            footprint_libraries_paths.get('uncategorized', []))\n\n    return footprint_libraries_paths\n\n\ndef add_footprint_library(user_config_path: str, category: str, library_folder: str) -> bool:\n    ''' Add KiCad footprint folder name to KiCad settings file '''\n    user_settings = load_file(user_config_path)\n\n    if category:\n        index = category\n    else:\n        index = library_folder\n\n    if not user_settings['KICAD_FOOTPRINTS']:\n        user_settings['KICAD_FOOTPRINTS'] = {}\n\n    try:\n        if library_folder not in user_settings['KICAD_FOOTPRINTS'][index]:\n            user_settings['KICAD_FOOTPRINTS'][index].append(library_folder)\n    except:\n        user_settings['KICAD_FOOTPRINTS'][index] = [library_folder]\n\n    return dump_file(user_settings, user_config_path)\n\n\ndef load_supplier_categories(supplier_config_path: str, clean=False) -> dict:\n    ''' Load Supplier category mapping from Supplier settings file '''\n    supplier_categories = load_file(supplier_config_path)\n\n    if clean:\n        clean_supplier_categories = copy.deepcopy(supplier_categories)\n\n        for category in supplier_categories:\n            for subcategory in supplier_categories[category]:\n                if FUNCTION_FILTER_KEY in subcategory:\n                    clean_supplier_categories[category][subcategory.replace(FUNCTION_FILTER_KEY, '')] \\\n                        = supplier_categories[category][subcategory]\n                    del clean_supplier_categories[category][subcategory]\n\n        return clean_supplier_categories\n\n    # print(supplier_categories)\n    return supplier_categories\n\n\ndef load_supplier_categories_inversed(supplier_config_path: str) -> dict:\n    ''' Load Supplier category mapping from Supplier settings file (inversed relation) '''\n    supplier_categories = load_file(supplier_config_path)\n\n    try:\n        supplier_categories_inversed = {}\n        for category in supplier_categories.keys():\n            if supplier_categories[category]:\n                for user, supplier in supplier_categories[category].items():\n                    # Supplier is list type\n                    if supplier:\n                        if category not in supplier_categories_inversed.keys():\n                            supplier_categories_inversed[category] = {}\n                        for item in supplier:\n                            supplier_categories_inversed[category][item] = user\n    except:\n        return None\n\n    # print(supplier_categories_inversed)\n    return supplier_categories_inversed\n\n\ndef sync_inventree_supplier_categories(inventree_config_path: str, supplier_config_path: str) -> dict:\n    ''' Synchronize supplier categories dict from InvenTree categories '''\n    inventree_categories = load_file(inventree_config_path)['CATEGORIES']\n    supplier_categories = load_supplier_categories(supplier_config_path, clean=True)\n    updated_supplier_categories = copy.deepcopy(supplier_categories)\n\n    try:\n        for category in inventree_categories:\n            if category not in supplier_categories.keys():\n                updated_supplier_categories[category] = inventree_categories[category]\n    except:\n        pass\n\n    return updated_supplier_categories\n\n\ndef add_supplier_category(categories: dict, supplier_config_path: str) -> bool:\n    ''' Add Supplier category mapping to Supplier settings file\n\n        categories = {\n            'Capacitors':\n                { 'Tantalum': 'Tantalum Capacitors' }\n        }\n    '''\n    try:\n        supplier_categories = load_file(supplier_config_path)\n    except:\n        return None\n\n    for category in categories.keys():\n        for user_subcategory, supplier_category in categories[category].items():\n            try:\n                supplier_category_keys = supplier_categories[category].keys()\n            except:\n                supplier_categories[category] = {\n                    user_subcategory: [supplier_category]}\n                break\n\n            # Function filtered\n            inventree_subcategory_filter = FUNCTION_FILTER_KEY + user_subcategory\n            if inventree_subcategory_filter in supplier_category_keys:\n                try:\n                    if supplier_category not in supplier_categories[category][inventree_subcategory_filter]:\n                        supplier_categories[category][inventree_subcategory_filter].append(\n                            supplier_category)\n                    break\n                except:\n                    pass\n\n                try:\n                    supplier_categories[category][inventree_subcategory_filter] = [\n                        supplier_category]\n                    break\n                except:\n                    pass\n            else:\n                try:\n                    if supplier_category not in supplier_categories[category][user_subcategory]:\n                        supplier_categories[category][user_subcategory].append(\n                            supplier_category)\n                    break\n                except:\n                    pass\n\n                try:\n                    supplier_categories[category][user_subcategory] = [\n                        supplier_category]\n                    break\n                except:\n                    pass\n\n            return False\n\n    return dump_file(supplier_categories, supplier_config_path)\n\n\ndef load_category_parameters(categories: list, supplier_config_path: str) -> dict:\n    ''' Load Supplier parameters mapping from Supplier settings file '''\n    def find_parameters(output_dict, category_list):\n        category_parameters = None\n        combined = ''\n        for category in reversed(category_list):\n            if category:\n                combined = category + combined\n            if combined in category_file:\n                category_parameters = category_file[combined]\n                break\n            if category in category_file:\n                category_parameters = category_file[category]\n                break\n            combined = '/' + combined\n        if not category_parameters:\n            return\n        if 'parent' in category_parameters:\n            for parent in category_parameters['parent']:\n                find_parameters(output_dict, [parent])\n            del category_parameters['parent']\n\n        for parameter in category_parameters.keys():\n            if category_parameters[parameter]:\n                for supplier_parameter in category_parameters[parameter]:\n                    output_dict[supplier_parameter] = parameter\n\n    try:\n        category_file = load_file(supplier_config_path)\n    except:\n        return None\n    category_parameters_inversed = {}\n\n    find_parameters(category_parameters_inversed, categories)\n\n    return category_parameters_inversed\n\n\ndef load_category_parameters_filters(category: str, supplier_config_path: str) -> list:\n    ''' Load Supplier parameters filters from Supplier settings file '''\n    try:\n        parameters_filters = load_file(supplier_config_path)[category]\n    except:\n        return []\n\n    # print(parameters_filters)\n    return parameters_filters\n"
  },
  {
    "path": "kintree/config/digikey/digikey_api.yaml",
    "content": "DIGIKEY_CLIENT_ID: ''\nDIGIKEY_CLIENT_SECRET: ''"
  },
  {
    "path": "kintree/config/digikey/digikey_categories.yaml",
    "content": "Capacitors:\n  Aluminium:\n  - Aluminum Electrolytic Capacitors\n  Ceramic:\n  - Ceramic Capacitors\n  - Ceramic\n  Polymer:\n  - Aluminum - Polymer Capacitors\n  - Tantalum - Polymer Capacitors\n  Super Capacitors:\n  - Electric Double Layer Capacitors (EDLC), Supercapacitors\n  Tantalum:\n  - Tantalum Capacitors\nCircuit Protections:\n  Fuses:\n  - Fuses\n  PTC:\n  - PTC Resettable Fuses\n  TVS:\n  - TVS - Diodes\nConnectors:\n  Board-to-Board:\n  - Rectangular Connectors - Arrays, Edge Type, Mezzanine (Board to Board)\n  - Rectangular Connectors - Spring Loaded\n  Coaxial:\n  - Coaxial Connectors (RF)\n  FPC:\n  - FFC, FPC (Flat Flexible) Connectors\n  Header:\n  - Rectangular Connectors - Headers, Male Pins\n  - Rectangular Connectors - Headers, Receptacles, Female Sockets\n  Interface:\n  - USB, DVI, HDMI Connectors\n  - Barrel - Audio Connectors\n  - Memory Connectors - PC Card Sockets\n  - Modular Connectors - Jacks With Magnetics\nCrystals and Oscillators:\n  Crystals:\n  - Crystals\n  Oscillators:\n  - Oscillators\nDiodes:\n  LED:\n  - LED Indication - Discrete\n  - Addressable, Specialty\n  Zener:\n  - Diodes - Zener - Single\n  __Schottky:\n  - Diodes - Rectifiers - Single\n  __Standard:\n  - Diodes - Rectifiers - Single\nInductors:\n  Ferrite Bead:\n  - Ferrite Beads and Chips\n  Power:\n  - Fixed Inductors\nIntegrated Circuits:\n  Interface:\n  - Interface - CODECs\n  - PMIC - Battery Chargers\n  - Interface - Analog Switches, Multiplexers, Demultiplexers\n  - Interface - Controllers\n  Logic:\n  - Logic - Translators, Level Shifters\n  - Clock/Timing - Clock Generators, PLLs, Frequency Synthesizers\n  - Logic - Buffers, Drivers, Receivers, Transceivers\n  Microcontroller:\n  - Embedded - Microcontrollers\n  Memory:\n  - Memory\n  Sensor:\n  - Humidity, Moisture Sensors\n  - Motion Sensors - IMUs (Inertial Measurement Units)\n  - PMIC - Current Regulation/Management\nMechanicals:\n  Standoff:\n  - Board Spacers, Standoffs\n  Switch:\n  - Tactile Switches\n  - Slide Switches\nPower Management:\n  LDO:\n  - PMIC - Voltage Regulators - Linear\n  __Boost:\n  - PMIC - Voltage Regulators - DC DC Switching Regulators\n  __Buck:\n  - PMIC - Voltage Regulators - DC DC Switching Regulators\nRF:\n  Antenna: null\n  Chipset: null\n  Filter:\n  - Balun\nResistors:\n  Potentiometers:\n  - Potentiometers, Variable Resistors\n  - Rotary Potentiometers, Rheostats\n  Surface Mount:\n  - Chip Resistor - Surface Mount\n  Through Hole:\n  - Through Hole Resistors\nTransistors:\n  __N-Channel FET:\n  - Transistors - FETs, MOSFETs - Single\n  __NPN:\n  - Transistors - Bipolar (BJT) - Single\n  - Transistors - FETs, MOSFETs - Single\n  __P-Channel FET:\n  - Transistors - FETs, MOSFETs - Single\n  __PNP:\n  - Transistors - Bipolar (BJT) - Single\n  Load Switches:\n  - PMIC - Power Distribution Switches, Load Drivers\n"
  },
  {
    "path": "kintree/config/digikey/digikey_config.yaml",
    "content": "SUPPLIER_INVENTREE_NAME: Digi-Key\nSEARCH_NAME: null\nSEARCH_DESCRIPTION: null\nSEARCH_REVISION: null\nSEARCH_KEYWORDS: null\nSEARCH_SKU: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_SUPPLIER_URL: null\nSEARCH_DATASHEET: null\nEXTRA_FIELDS: null"
  },
  {
    "path": "kintree/config/element14/element14_api.yaml",
    "content": "ELEMENT14_PRODUCT_SEARCH_API_KEY: null\nFARNELL_STORE: null\nNEWARK_STORE: null\nELEMENT14_STORE: null"
  },
  {
    "path": "kintree/config/element14/element14_config.yaml",
    "content": "SUPPLIER_INVENTREE_NAME: null\nSEARCH_NAME: null\nSEARCH_DESCRIPTION: null\nSEARCH_REVISION: null\nSEARCH_KEYWORDS: null\nSEARCH_SKU: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_SUPPLIER_URL: null\nSEARCH_DATASHEET: null\nEXTRA_FIELDS: null"
  },
  {
    "path": "kintree/config/inventree/categories.yaml",
    "content": "CATEGORIES:\n  Assemblies:\n    Printed-Circuit Board Assembly: null\n    Product: null\n  Capacitors:\n    Aluminium: null\n    Ceramic:\n      '0402': null\n      '0603': null\n      '0805': null\n    Polymer: null\n    Super Capacitors: null\n    Tantalum: null\n  Circuit Protections:\n    Fuses: null\n    PTC: null\n    TVS: null\n  Connectors:\n    Battery: null\n    Board-to-Board: null\n    Coaxial: null\n    FPC: null\n    Header: null\n    Interface: null\n  Crystals and Oscillators:\n    Crystals: null\n    Oscillators: null\n  Diodes:\n    LED: null\n    Schottky: null\n    Standard: null\n    Zener: null\n  Inductors:\n    Ferrite Bead: null\n    Power: null\n  Integrated Circuits:\n    Interface: null\n    Logic: null\n    Memory: null\n    Microcontroller: null\n    Sensor: null\n  Mechanicals:\n    Nuts: null\n    Screws: null\n    Standoff: null\n    Switch: null\n  Miscellaneous:\n    Batteries: null\n  Modules: null\n  Power Management:\n    Boost: null\n    Buck: null\n    LDO: null\n    PMIC: null\n  Printed-Circuit Boards: null\n  RF:\n    Antenna: null\n    Chipset: null\n    Filter: null\n    Shield: null\n  Resistors:\n    NTC: null\n    Potentiometers: null\n    Surface Mount: null\n    Through Hole: null\n  Transistors:\n    Load Switches: null\n    N-Channel FET: null\n    NPN: null\n    P-Channel FET: null\n    PNP: null\nCODES:\n  Assemblies: PCA\n  Capacitors: CAP\n  Circuit Protections: PRO\n  Connectors: CON\n  Crystals and Oscillators: CLK\n  Diodes: DIO\n  Inductors: IND\n  Integrated Circuits: ICS\n  Mechanicals: MEC\n  Miscellaneous: MIS\n  Modules: MOD\n  Power Management: PWR\n  Printed-Circuit Boards: PCB\n  RF: RFC\n  Resistors: RES\n  Transistors: TRA"
  },
  {
    "path": "kintree/config/inventree/inventree_dev.yaml",
    "content": "ENABLE: true\nENABLE_PROXY: false\nPASSWORD: !!binary |\n  ''\nPROXIES: null\nSERVER_ADDRESS: ''\nUSERNAME: ''\nDATASHEET_UPLOAD: false"
  },
  {
    "path": "kintree/config/inventree/inventree_prod.yaml",
    "content": "ENABLE: true\nENABLE_PROXY: false\nPASSWORD: !!binary |\n  ''\nPROXIES: null\nSERVER_ADDRESS: ''\nUSERNAME: ''\nDATASHEET_UPLOAD: false"
  },
  {
    "path": "kintree/config/inventree/parameters.yaml",
    "content": "# Parameters\n# Name: Unit\nMin Output Voltage: V\nAntenna Type: null\nB Constant: K\nBreakdown Voltage: V\nCapacitance: nF\nClamping Voltage: V\nCollector Gate Voltage: V\nDC Resistance: \"m\\u03A9\"\nESR: \"m\\u03A9\"\nFootprint: null\nForward Voltage: V\nFrequency: Hz\nFrequency Stability: ppm\nFrequency Tolerance: ppm\nFunction Type: null\nInterface Type: null\nLED Color: null\nLoad Capacitance: pF\nLocking: null\nMating Height: mm\nMax Input Voltage: V\nMax Output Voltage: V\nMaximum Gate Voltage: V\nMemory Size: null\nMin Input Voltage: V\nMounting Type: null\nNumber of Channels: null\nNumber of Contacts: null\nNumber of Elements: null\nNumber of Rows: null\nOrientation: null\nOutput Current: A\nOutput Type: null\nPackage Height: mm\nPackage Size: mm\nPackage Type: null\nPitch: mm\nPolarity: null\nQuiescent Current: A\nRDS On Resistance: \"\\u03A9\"\nRDS On Voltage: V\nRated Current: A\nRated Power: W\nRated Voltage: V\nSaturation Current: A\nShielding: null\nStandoff Voltage: V\nSymbol: null\nTemperature Grade: null\nTemperature Range: \"\\xB0C\"\nTolerance: '%'\nValue: null\n"
  },
  {
    "path": "kintree/config/inventree/parameters_filters.yaml",
    "content": "Capacitors:\n- Value\n- Rated Voltage\n- Tolerance\n- Package Type\n- Temperature Grade\nCircuit Protections:\n- Value\nConnectors:\n- Value\nCrystals and Oscillators:\n- Value\n- Package Type\n- Package Size\n- Temperature Range\nDiodes:\n- Value\nInductors:\n- Value\n- Rated Current\n- Package Type\n- Package Size\n- Temperature Range\nIntegrated Circuits:\n- Value\nMechanicals:\n- Value\nModules:\n- null\nPower Management:\n- Value\nPrinted-Circuit Boards:\n- null\nRF:\n- Value\nResistors:\n- Value\n- Tolerance\n- Rated Power\n- Package Type\n- Temperature Range\nTransistors:\n- Value\n"
  },
  {
    "path": "kintree/config/inventree/stock_locations.yaml",
    "content": "STOCK_LOCATIONS: null"
  },
  {
    "path": "kintree/config/inventree/supplier_parameters.yaml",
    "content": "# Parameter Mapping between InvenTree parameter template and suppliers parameters naming\n# Each template parameter can match to multiple suppliers parameters\n# Categories (main keys) should match categories in the categories.yaml file\n# Parameter template names should match those found in the parameters.yaml file\nBase:\n  Temperature Range:\n  - Operating Temperature\n  Package Type:\n  - Package / Case\nPassives:\n  Tolerance:\n  - Tolerance\nCapacitors:\n  parent:\n  - Base\n  - Passives\n  ESR:\n  - ESR (Equivalent Series Resistance)\n  Package Height:\n  - Height - Seated (Max)\n  - Thickness (Max)\n  Package Size:\n  - Size / Dimension\n  Rated Voltage:\n  - Voltage - Rated\n  - Voltage Rated\n  Temperature Grade:\n  - Temperature Coefficient\n  Temperature Range:\n  - Operating Temperature\n  Value:\n  - Capacitance\nCircuit Protections:\n  parent:\n  - Base\n  Breakdown Voltage:\n  - Voltage - Breakdown (Min)\n  Capacitance:\n  - Capacitance @ Frequency\n  Clamping Voltage:\n  - Voltage - Clamping (Max) @ Ipp\n  Rated Current:\n  - Current Rating (Amps)\n  - Current - Max\n  Rated Power:\n  - Power - Peak Pulse\n  Rated Voltage:\n  - Voltage Rating - DC\n  - Voltage - Max\n  Standoff Voltage:\n  - Voltage - Reverse Standoff (Typ)\n  Value:\n  - Manufacturer Part Number\nConnectors:\n  Frequency:\n  - Frequency - Max\n  Interface Type:\n  - Connector Type\n  - Flat Flex Type\n  Locking:\n  - Locking Feature\n  - Fastening Type\n  Mating Height:\n  - Mated Stacking Heights\n  Mounting Type:\n  - Mounting Type\n  Number of Contacts:\n  - Number of Contacts\n  - Number of Positions\n  Number of Rows:\n  - Number of Rows\n  Orientation:\n  - Orientation\n  Package Height:\n  - Height Above Board\n  - Insulation Height\n  Pitch:\n  - Pitch\n  - Pitch - Mating\n  Polarity:\n  - Gender\n  Shielding:\n  - Shielding\n  Temperature Range:\n  - Operating Temperature\n  Value:\n  - Manufacturer Part Number\nCrystals and Oscillators:\n  parent:\n  - Base\n  Frequency Stability:\n  - Frequency Stability\n  Frequency Tolerance:\n  - Frequency Tolerance\n  Load Capacitance:\n  - Load Capacitance\n  Package Height:\n  - Height - Seated (Max)\n  Package Size:\n  - Size / Dimension\n  Rated Current:\n  - Current - Supply (Max)\n  Rated Voltage:\n  - Voltage - Supply\n  Value:\n  - Frequency\nDiodes:\n  parent:\n  - Base\n  Forward Voltage:\n  - Voltage - Forward (Vf) (Max) @ If\n  - Voltage - Forward (Vf) (Typ)\n  Function Type:\n  - Diode Type\n  LED Color:\n  - Color\n  Rated Current:\n  - Current - Average Rectified (Io)\n  Rated Power:\n  - Power - Max\n  Rated Voltage:\n  - Voltage - DC Reverse (Vr) (Max)\n  - Voltage - Zener (Nom) (Vz)\n  Temperature Range:\n  - Operating Temperature - Junction\n  Value:\n  - Manufacturer Part Number\nInductors:\n  parent:\n  - Base\n  - Passives\n  ESR:\n  - DC Resistance (DCR)\n  - DC Resistance (DCR) (Max)\n  Package Height:\n  - Height - Seated (Max)\n  - Height (Max)\n  Package Size:\n  - Size / Dimension\n  Rated Current:\n  - Current Rating (Max)\n  - Current Rating (Amps)\n  Saturation Current:\n  - Current - Saturation\n  Shielding:\n  - Shielding\n  Value:\n  - Inductance\n  - Impedance @ Frequency\nIntegrated Circuits:\n  parent:\n  - Base\n  Frequency:\n  - Clock Frequency\n  - Speed\n  - Data Rate\n  - Frequency Range\n  - -3db Bandwidth\n  Function Type:\n  - Translator Type\n  - Technology\n  - Core Processor\n  - Type\n  - Sensor Type\n  Memory Size:\n  - Program Memory Size\n  - Memory Size\n  Number of Channels:\n  - Channels per Circuit\n  Rated Voltage:\n  - Voltage - VCCA\n  - Voltage - VCCB\n  - Voltage - Supply\n  - Voltage - Supply (Vcc/Vdd)\n  - Voltage - Supply, Digital\n  - Voltage - Supply, Single (V+)\n  Value:\n  - Manufacturer Part Number\nMechanicals:\n  parent:\n  - Base\n  Function Type:\n  - Circuit\n  - Type\n  Mounting Type:\n  - Mounting Type\n  - Features\n  Package Height:\n  - Between Board Height\n  Package Size:\n  - Outline\n  - Diameter - Outside\n  Package Type:\n  - Screw, Thread Size\n  Rated Current:\n  - Contact Rating @ Voltage\n  Value:\n  - Manufacturer Part Number\nPower Management:\n  parent:\n  - Base\n  Min Output Voltage:\n  - Voltage - Output (Min/Fixed)\n  Frequency:\n  - Frequency - Switching\n  Function Type:\n  - Topology\n  Max Input Voltage:\n  - Voltage - Input (Max)\n  Max Output Voltage:\n  - Voltage - Output (Max)\n  Min Input Voltage:\n  - Voltage - Input (Min)\n  Output Type:\n  - Output Type\n  Package Type:\n  - Supplier Device Package\n  Quiescent Current:\n  - Current - Quiescent (Iq)\n  Rated Current:\n  - Current - Output\n  Value:\n  - Manufacturer Part Number\nRF:\n  parent:\n  - Base\n  Frequency:\n  - Frequency Range\n  Function Type: null\n  Rated Voltage: null\n  Value:\n  - Manufacturer Part Number\nResistors:\n  parent:\n  - Passives\n  Package Type:\n  - Supplier Device Package\n  Rated Power:\n  - Power (Watts)\n  Temperature Range:\n  - Operating Temperature\n  Value:\n  - Resistance\nTransistors:\n  Collector-Gate Voltage:\n  - Vce Saturation (Max) @ Ib, Ic\n  - Vgs(th) (Max) @ Id\n  Function Type:\n  - Transistor Type\n  - FET Type\n  Maximum Gate Voltage:\n  - Vgs (Max)\n  Package Type:\n  - Supplier Device Package\n  RDS On Resistance:\n  - Rds On (Max) @ Id, Vgs\n  Rated Current:\n  - Current - Collector (Ic) (Max)\n  - \"Current - Continuous Drain (Id) @ 25\\xB0C\"\n  Rated Power:\n  - Power - Max\n  - Power Dissipation (Max)\n  Rated Voltage:\n  - Voltage - Collector Emitter Breakdown (Max)\n  - Drain to Source Voltage (Vdss)\n  Temperature Range:\n  - Operating Temperature\n  Value:\n  - Manufacturer Part Number\n"
  },
  {
    "path": "kintree/config/inventree/suppliers.yaml",
    "content": "Digi-Key:\n  enable: true\n  name: Digi-Key\nMouser:\n  enable: true\n  name: Mouser\nElement14:\n  enable: true\n  name: Element14\nFarnell:\n  enable: true\n  name: Farnell\nJameco:\n  enable: true\n  name: Jameco\nAutomationDirect:\n  enable: true\n  name: Automation Direct\nNewark:\n  enable: true\n  name: Newark\nLCSC:\n  enable: true\n  name: LCSC\nTME:\n  enable: true\n  name: TME"
  },
  {
    "path": "kintree/config/jameco/jameco_api.yaml",
    "content": "JAMECO_API_URL: https://ahzbkf.a.searchspring.io/api/search/search.json?ajaxCatalog=v3&resultsFormat=native&siteId=ahzbkf&q="
  },
  {
    "path": "kintree/config/jameco/jameco_config.yaml",
    "content": "SUPPLIER_INVENTREE_NAME: Jameco Electronics\nSEARCH_NAME: null\nSEARCH_DESCRIPTION: null\nSEARCH_REVISION: null\nSEARCH_KEYWORDS: null\nSEARCH_SKU: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_SUPPLIER_URL: null\nSEARCH_DATASHEET: null\nEXTRA_FIELDS: null"
  },
  {
    "path": "kintree/config/kicad/kicad.yaml",
    "content": "KICAD_SYMBOLS_PATH: ''\nKICAD_TEMPLATES_PATH: kintree/kicad/templates/\nKICAD_FOOTPRINTS_PATH: ''"
  },
  {
    "path": "kintree/config/kicad/kicad_map.yaml",
    "content": "KICAD_FOOTPRINTS:\nKICAD_LIBRARIES:\nKICAD_TEMPLATES:\n  Capacitors:\n    Aluminium: capacitor-polarized\n    Ceramic: capacitor\n    Default: capacitor\n    Polymer: capacitor-polarized\n    Super Capacitors: capacitor-polarized\n    Tantalum: capacitor-polarized\n  Circuit Protections:\n    Default: protection-unidir\n    Fuse: fuse\n    TVS: protection-unidir\n  Connectors:\n    Default: connector\n  Crystals and Oscillators:\n    Crystal 2P: crystal-2p\n    Default: crystal-2p\n    Oscillator 4P: oscillator-4p\n  Diodes:\n    Default: diode-standard\n    LED: diode-led\n    Schottky: diode-schottky\n    Standard: diode-standard\n    Zener: diode-zener\n  Inductors:\n    Default: inductor\n    Ferrite Bead: ferrite-bead\n    Power: inductor\n  Integrated Circuits:\n    Default: integrated-circuit\n  Mechanicals:\n    Default: default\n  Power Management:\n    Default: integrated-circuit\n  Resistors:\n    Default: resistor\n    Surface Mount: resistor-sm\n    Through Hole: resistor\n  RF:\n    Default: integrated-circuit\n  Transistors:\n    Default: transistor-nfet\n    N-Channel FET: transistor-nfet\n    NPN: transistor-npn\n    P-Channel FET: transistor-pfet\n    PNP: transistor-pnp\n"
  },
  {
    "path": "kintree/config/lcsc/lcsc_api.yaml",
    "content": "LCSC_API_URL: https://wmsc.lcsc.com/ftps/wm/product/detail?productCode=\n"
  },
  {
    "path": "kintree/config/lcsc/lcsc_config.yaml",
    "content": "SUPPLIER_INVENTREE_NAME: LCSC Electronics\nSEARCH_NAME: null\nSEARCH_DESCRIPTION: null\nSEARCH_REVISION: null\nSEARCH_KEYWORDS: null\nSEARCH_SKU: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_SUPPLIER_URL: null\nSEARCH_DATASHEET: null\nEXTRA_FIELDS: null"
  },
  {
    "path": "kintree/config/mouser/mouser_api.yaml",
    "content": "MOUSER_PART_API_KEY: null"
  },
  {
    "path": "kintree/config/mouser/mouser_config.yaml",
    "content": "SUPPLIER_INVENTREE_NAME: Mouser Electronics\nSEARCH_NAME: null\nSEARCH_DESCRIPTION: null\nSEARCH_REVISION: null\nSEARCH_KEYWORDS: null\nSEARCH_SKU: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_SUPPLIER_URL: null\nSEARCH_DATASHEET: null\nEXTRA_FIELDS: null"
  },
  {
    "path": "kintree/config/settings.py",
    "content": "import os\nimport sys\nimport platform\nfrom enum import Enum\n\nfrom ..common.tools import cprint\nfrom .import config_interface\n\n# DEBUG\n# Testing\nENABLE_TEST = False\n# Silent Mode\nSILENT = False\n# Debug\nHIDE_DEBUG = True\n\n\ndef enable_test_mode():\n    global ENABLE_TEST\n    global SILENT\n    ENABLE_TEST = True\n    SILENT = True\n\n\n# PATHS\nif getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):\n    PROJECT_DIR = os.path.abspath(os.path.dirname(sys.executable))\nelse:\n    PROJECT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))\n# InvenTree API\nsys.path.append(os.path.join(PROJECT_DIR, 'database', 'inventree-python'))\n# Digi-Key API\nsys.path.append(os.path.join(PROJECT_DIR, 'search', 'digikey_api'))\n# KiCad Library Utils\nsys.path.append(os.path.join(PROJECT_DIR, 'kicad'))\n# Tests\nsys.path.append(os.path.join(PROJECT_DIR, 'tests'))\n\n# HOME FOLDER\nUSER_HOME = os.path.expanduser(\"~\")\n# APP NAME\nAPP_NAME = 'kintree'\n# CONFIG PATH\nif platform.system() == 'Linux':\n    HOME_DIR = os.path.join(USER_HOME, '.config', APP_NAME, '')\nelse:\n    HOME_DIR = os.path.join(USER_HOME, APP_NAME, '')\n# Create config path if it does not exists\nif not os.path.exists(HOME_DIR):\n    os.makedirs(HOME_DIR, exist_ok=True)\n\n\n# USER AND CONFIG FILES\ndef load_user_config():\n    global USER_SETTINGS\n    global CONFIG_ROOT\n    global CONFIG_USER_FILES\n\n    USER_SETTINGS = config_interface.load_user_paths(home_dir=HOME_DIR)\n    CONFIG_ROOT = os.path.join(PROJECT_DIR, 'config', '')\n    CONFIG_USER_FILES = os.path.join(USER_SETTINGS['USER_FILES'], '')\n\n    # Create user files folder if it does not exists\n    if not os.path.exists(CONFIG_USER_FILES):\n        os.makedirs(CONFIG_USER_FILES)\n    # Create user files\n    return config_interface.load_user_config_files(path_to_root=CONFIG_ROOT,\n                                                   path_to_user_files=CONFIG_USER_FILES,\n                                                   silent=HIDE_DEBUG)\n\n\n# Load user config\nUSER_CONFIG_FILE = os.path.join(HOME_DIR, 'settings.yaml')\nif not load_user_config():\n    # Check if configuration files already exist\n    if not os.path.isfile(os.path.join(CONFIG_USER_FILES, 'categories.yaml')):\n        cprint('\\n[ERROR]\\tSome Ki-nTree configuration files seem to be missing')\n        exit(-1)\n\n# KiCad\nKICAD_CONFIG_PATHS = os.path.join(CONFIG_USER_FILES, 'kicad.yaml')\nKICAD_CONFIG_CATEGORY_MAP = os.path.join(CONFIG_USER_FILES, 'kicad_map.yaml')\n\n# Inventree\nCONFIG_CATEGORIES = os.path.join(CONFIG_USER_FILES, 'categories.yaml')\nCONFIG_STOCK_LOCATIONS = os.path.join(CONFIG_USER_FILES, 'stock_locations.yaml')\nCONFIG_PARAMETERS = os.path.join(CONFIG_USER_FILES, 'parameters.yaml')\nCONFIG_PARAMETERS_FILTERS = os.path.join(\n    CONFIG_USER_FILES, 'parameters_filters.yaml')\n\n# INTERNAL PART NUMBERS\nCONFIG_IPN_PATH = os.path.join(CONFIG_USER_FILES, 'internal_part_number.yaml')\n\n\ndef load_ipn_settings():\n    global CONFIG_IPN\n    CONFIG_IPN = config_interface.load_file(CONFIG_IPN_PATH)\n\n\nload_ipn_settings()\n\n# GENERAL SETTINGS\nCONFIG_GENERAL_PATH = os.path.join(CONFIG_USER_FILES, 'general.yaml')\nCONFIG_GENERAL = config_interface.load_file(CONFIG_GENERAL_PATH)\n# Datasheets\nDATASHEET_SAVE_ENABLED = CONFIG_GENERAL.get('DATASHEET_SAVE_ENABLED', False)\nDATASHEET_SAVE_PATH = CONFIG_GENERAL.get('DATASHEET_SAVE_PATH', '')\n# Open Browser\nAUTOMATIC_BROWSER_OPEN = CONFIG_GENERAL.get('AUTOMATIC_BROWSER_OPEN', False)\n# Default Supplier\nDEFAULT_SUPPLIER = CONFIG_GENERAL.get('DEFAULT_SUPPLIER', 'Digi-Key')\n\n\n# Load enable flags\ndef reload_enable_flags():\n    global ENABLE_KICAD\n    global ENABLE_INVENTREE\n    global ENABLE_ALTERNATE\n    global UPDATE_INVENTREE\n    global CHECK_EXISTING\n\n    try:\n        ENABLE_KICAD = CONFIG_GENERAL.get('ENABLE_KICAD', False)\n        ENABLE_INVENTREE = CONFIG_GENERAL.get('ENABLE_INVENTREE', False)\n        ENABLE_ALTERNATE = CONFIG_GENERAL.get('ENABLE_ALTERNATE', False)\n        UPDATE_INVENTREE = CONFIG_GENERAL.get('UPDATE_INVENTREE', False)\n        CHECK_EXISTING = CONFIG_GENERAL.get('CHECK_EXISTING', True)\n        return True\n    except TypeError:\n        pass\n\n    return False\n\n\nreload_enable_flags()\n\n# Supported suppliers APIs\nCONFIG_SUPPLIERS_PATH = os.path.join(CONFIG_USER_FILES, 'suppliers.yaml')\nCONFIG_SUPPLIERS = config_interface.load_file(CONFIG_SUPPLIERS_PATH)\nSUPPORTED_SUPPLIERS_API = []\n\n\n# Load suppliers\ndef load_suppliers():\n    global CONFIG_SUPPLIERS\n    global SUPPORTED_SUPPLIERS_API\n\n    update_supplier_config = {}\n    SUPPORTED_SUPPLIERS_API = []\n    for supplier, data in CONFIG_SUPPLIERS.items():\n        try:\n            if data['enable']:\n                if data['name']:\n                    supplier_name = data['name'].replace(' ', '')\n                    SUPPORTED_SUPPLIERS_API.append(supplier_name)\n                else:\n                    supplier_key = supplier.replace(' ', '')\n                    SUPPORTED_SUPPLIERS_API.append(supplier_key)\n        except (TypeError, KeyError):\n            update_supplier_config[supplier] = {\n                'enable': True,\n                'name': supplier,\n            }\n\n    # Update supplier configuration file\n    if update_supplier_config:\n        config_interface.dump_file({**CONFIG_SUPPLIERS, **update_supplier_config}, CONFIG_SUPPLIERS_PATH)\n        CONFIG_SUPPLIERS = config_interface.load_file(CONFIG_SUPPLIERS_PATH)\n        return False\n    return True\n\n\nif not load_suppliers():\n    # Re-load updated configuration file\n    load_suppliers()\n\n# Generic API user configuration\nCONFIG_SUPPLIER_PARAMETERS = os.path.join(CONFIG_USER_FILES, 'supplier_parameters.yaml')\nCONFIG_SEARCH_API_PATH = os.path.join(CONFIG_USER_FILES, 'search_api.yaml')\nCONFIG_SEARCH_API = config_interface.load_file(CONFIG_SEARCH_API_PATH)\n\n# Digi-Key user configuration\nCONFIG_DIGIKEY = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'digikey_config.yaml'))\nCONFIG_DIGIKEY_API = os.path.join(CONFIG_USER_FILES, 'digikey_api.yaml')\nCONFIG_DIGIKEY_CATEGORIES = os.path.join(CONFIG_USER_FILES, 'digikey_categories.yaml')\n\n# Mouser user configuration\nCONFIG_MOUSER = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'mouser_config.yaml'))\nCONFIG_MOUSER_API = os.path.join(CONFIG_USER_FILES, 'mouser_api.yaml')\n\n# Element14 user configuration (includes Farnell, Newark and Element14)\nCONFIG_ELEMENT14 = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'element14_config.yaml'))\nCONFIG_ELEMENT14_API = os.path.join(CONFIG_USER_FILES, 'element14_api.yaml')\n\n# LCSC user configuration\nCONFIG_LCSC = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'lcsc_config.yaml'))\nCONFIG_LCSC_API = os.path.join(CONFIG_USER_FILES, 'lcsc_api.yaml')\n\n# JAMECO user configuration\nCONFIG_JAMECO = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'jameco_config.yaml'))\nCONFIG_JAMECO_API = os.path.join(CONFIG_USER_FILES, 'jameco_api.yaml')\n\n# AUTOMATIONDIRECT user configuration\nCONFIG_AUTOMATIONDIRECT = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'automationdirect_config.yaml'))\nCONFIG_AUTOMATIONDIRECT_API = os.path.join(CONFIG_USER_FILES, 'automationdirect_api.yaml')\n\n# TME user configuration\nCONFIG_TME = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'tme_config.yaml'))\nCONFIG_TME_API = os.path.join(CONFIG_USER_FILES, 'tme_api.yaml')\n\n# Automatic category match confidence level (from 0 to 100)\nCATEGORY_MATCH_RATIO_LIMIT = CONFIG_SEARCH_API.get('CATEGORY_MATCH_RATIO_LIMIT', 100)\n# Search results caching (stored in files)\nCACHE_ENABLED = CONFIG_SEARCH_API.get('CACHE_ENABLED', True)\n# Cache validity in days\nCACHE_VALID_DAYS = int(CONFIG_SEARCH_API.get('CACHE_VALID_DAYS', '7'))\n\n\n# Caching settings\ndef load_cache_settings():\n    global search_results\n    global search_images\n    global search_datasheets\n    global CACHE_ENABLED\n    global DIGIKEY_STORAGE_PATH\n\n    USER_SETTINGS = config_interface.load_user_paths(home_dir=HOME_DIR)\n\n    search_results = {\n        'directory': os.path.join(USER_SETTINGS['USER_CACHE'], 'search', ''),\n        'extension': '.yaml',\n    }\n    # Create folder if it does not exists\n    if not os.path.exists(search_results['directory']):\n        os.makedirs(search_results['directory'])\n\n    # Part images\n    search_images = os.path.join(USER_SETTINGS['USER_CACHE'], 'images', '')\n    # Create folder if it does not exists\n    if not os.path.exists(search_images):\n        os.makedirs(search_images)\n\n    # Part images\n    search_datasheets = os.path.join(\n        USER_SETTINGS['USER_CACHE'], 'datasheets', '')\n    # Create folder if it does not exists\n    if not os.path.exists(search_datasheets):\n        os.makedirs(search_datasheets)\n\n    # API token storage path\n    DIGIKEY_STORAGE_PATH = os.path.join(USER_SETTINGS['USER_CACHE'], '')\n\n\n# Load cache settings\nload_cache_settings()\n\n# KICAD\n# User Settings\nKICAD_SETTINGS = {}\n\n\ndef load_kicad_settings():\n    global KICAD_CONFIG_PATHS\n    global KICAD_SETTINGS\n\n    kicad_user_settings = config_interface.load_file(KICAD_CONFIG_PATHS, silent=False)\n    if kicad_user_settings:\n        KICAD_SETTINGS['KICAD_SYMBOLS_PATH'] = kicad_user_settings.get('KICAD_SYMBOLS_PATH', None)\n        KICAD_SETTINGS['KICAD_TEMPLATES_PATH'] = kicad_user_settings.get('KICAD_TEMPLATES_PATH', None)\n        KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'] = kicad_user_settings.get('KICAD_FOOTPRINTS_PATH', None)\n\n\n# Load kicad settings\nload_kicad_settings()\n\n\ndef set_default_supplier(value: str, save=False):\n    global DEFAULT_SUPPLIER\n    DEFAULT_SUPPLIER = value\n    if save:\n        user_settings = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'general.yaml'))\n        user_settings['DEFAULT_SUPPLIER'] = value\n        config_interface.dump_file(user_settings, os.path.join(CONFIG_USER_FILES, 'general.yaml'))\n    return\n\n\n# Library Paths\nif not ENABLE_TEST:\n    symbol_libraries_paths = config_interface.load_libraries_paths(\n        KICAD_CONFIG_CATEGORY_MAP,\n        KICAD_SETTINGS['KICAD_SYMBOLS_PATH'],\n    )\n# cprint(symbol_libraries_paths)\n\n# Template Paths\nsymbol_templates_paths = config_interface.load_templates_paths(\n    KICAD_CONFIG_CATEGORY_MAP,\n    KICAD_SETTINGS['KICAD_TEMPLATES_PATH'],\n)\n# cprint(symbol_templates_paths)\n\n# Footprint Libraries\nfootprint_libraries_paths = config_interface.load_footprint_paths(\n    KICAD_CONFIG_CATEGORY_MAP,\n    KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'],\n)\n# cprint(footprint_libraries_paths)\nfootprint_name_default = 'TBD'\n\nAUTO_GENERATE_LIB = True\nsymbol_template_lib = os.path.join(\n    PROJECT_DIR,\n    'kicad',\n    'templates',\n    'library_template.kicad_sym'\n)\n\n\n# INVENTREE\nclass Environment(Enum):\n    '''\n    Server/Remote Development: DEVELOPMENT\n    Server/Remote Production: PRODUCTION\n    '''\n    DEVELOPMENT = 0\n    PRODUCTION = 1\n\n\n# Pick environment\nenvironment = CONFIG_GENERAL.get('INVENTREE_ENV', None)\nenvironment = os.environ.get('INVENTREE_ENV', environment)\n\ntry:\n    environment = int(environment)\nexcept TypeError:\n    environment = 0\n\n# Load correct user file\nif environment == Environment.PRODUCTION.value:\n    INVENTREE_CONFIG = os.path.join(CONFIG_USER_FILES, 'inventree_prod.yaml')\nelse:\n    INVENTREE_CONFIG = os.path.join(CONFIG_USER_FILES, 'inventree_dev.yaml')\n\n# Load user settings\ninventree_settings = config_interface.load_inventree_user_settings(INVENTREE_CONFIG)\n\n\n# Server settings\ndef load_inventree_settings():\n    global SERVER_ADDRESS\n    global USERNAME\n    global PASSWORD\n    global ENABLE_PROXY\n    global PROXIES\n    global PART_URL_ROOT\n    global DATASHEET_UPLOAD\n    global PRICING_UPLOAD\n\n    inventree_settings = config_interface.load_inventree_user_settings(INVENTREE_CONFIG)\n\n    SERVER_ADDRESS = inventree_settings.get('SERVER_ADDRESS', None)\n    USERNAME = inventree_settings.get('USERNAME', None)\n    PASSWORD = inventree_settings.get('PASSWORD', None)\n    ENABLE_PROXY = inventree_settings.get('ENABLE_PROXY', False)\n    PROXIES = inventree_settings.get('PROXIES', None)\n    DATASHEET_UPLOAD = inventree_settings.get('DATASHEET_UPLOAD', False)\n    PRICING_UPLOAD = inventree_settings.get('PRICING_UPLOAD', False)\n    # Part URL\n    if SERVER_ADDRESS:\n        # If missing, append slash to root URL\n        root_url = SERVER_ADDRESS\n        if not SERVER_ADDRESS.endswith('/'):\n            root_url = root_url + '/'\n        # Set part URL\n        PART_URL_ROOT = root_url + 'part/'\n\n\n# InvenTree part dictionary template\ninventree_part_template = {\n    'name': None,\n    'description': None,\n    'IPN': None,\n    'revision': None,\n    'keywords': None,\n    'image': None,\n    'inventree_url': None,\n    'manufacturer_name': None,\n    'manufacturer_part_number': None,\n    'datasheet': None,\n    'supplier_name': None,\n    'supplier_part_number': None,\n    'supplier_link': None,\n    'parameters': {},\n}\n\n\n# Enable flags\ndef set_enable_flag(key: str, value: bool):\n    global CONFIG_GENERAL\n\n    user_settings = CONFIG_GENERAL\n    if key in ['kicad', 'inventree', 'alternate', 'update', 'check_existing']:\n        if key == 'kicad':\n            user_settings['ENABLE_KICAD'] = value\n        elif key == 'inventree':\n            user_settings['ENABLE_INVENTREE'] = value\n        elif key == 'alternate':\n            user_settings['ENABLE_ALTERNATE'] = value\n        elif key == 'update':\n            user_settings['UPDATE_INVENTREE'] = value\n        elif key == 'check_existing':\n            user_settings['CHECK_EXISTING'] = value\n\n        # Save\n        config_interface.dump_file(\n            data=user_settings,\n            file_path=os.path.join(CONFIG_USER_FILES, 'general.yaml'),\n        )\n\n    return reload_enable_flags()\n"
  },
  {
    "path": "kintree/config/tme/tme_api.yaml",
    "content": "TME_API_TOKEN: NULL\nTME_API_SECRET: NULL\nTME_API_COUNTRY: US\nTME_API_LANGUAGE: EN"
  },
  {
    "path": "kintree/config/tme/tme_config.yaml",
    "content": "SUPPLIER_INVENTREE_NAME: TME\nSEARCH_NAME: null\nSEARCH_DESCRIPTION: null\nSEARCH_REVISION: null\nSEARCH_KEYWORDS: null\nSEARCH_SKU: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_SUPPLIER_URL: null\nSEARCH_DATASHEET: null"
  },
  {
    "path": "kintree/config/user/general.yaml",
    "content": "DATASHEET_SAVE_ENABLED: false\nDATASHEET_SAVE_PATH: null\nDATASHEET_INVENTREE_ENABLED: false\nAUTOMATIC_BROWSER_OPEN: true\nINVENTREE_ENV: null\nDEFAULT_SUPPLIER: Digi-Key\nENABLE_KICAD: false\nENABLE_INVENTREE: false\nENABLE_ALTERNATE: false\nCHECK_EXISTING: true"
  },
  {
    "path": "kintree/config/user/internal_part_number.yaml",
    "content": "IPN_ENABLE_CREATE: true\nIPN_USE_MANUFACTURER_PART_NUMBER: false\nIPN_PREFIX: null\nIPN_CATEGORY_CODE: true\nIPN_UNIQUE_ID_LENGTH: '6'\nIPN_ENABLE_PREFIX: false\nIPN_ENABLE_SUFFIX: true\nIPN_SUFFIX: '00'\nINVENTREE_DEFAULT_REV: 'A'"
  },
  {
    "path": "kintree/config/user/search_api.yaml",
    "content": "CATEGORY_MATCH_RATIO_LIMIT: 100\nCACHE_ENABLED: true\nCACHE_VALID_DAYS: '7'"
  },
  {
    "path": "kintree/database/inventree_api.py",
    "content": "from ..config import settings\nimport validators\nfrom ..common import part_tools\nfrom ..common.tools import cprint, download_with_retry\nfrom ..config import config_interface\nimport re\n\n# Required to use local CA certificates on Linux\n# For more details, refer to https://github.com/sparkmicro/Ki-nTree/pull/45\nimport platform\nimport os\nif platform.system() == 'Linux':\n    cert_path = '/etc/ssl/certs/ca-certificates.crt'\n    if os.path.isfile(cert_path):\n        os.environ['REQUESTS_CA_BUNDLE'] = cert_path\n\n# InvenTree\nfrom inventree.api import InvenTreeAPI\nfrom inventree.company import Company, ManufacturerPart, SupplierPart, SupplierPriceBreak\nfrom inventree.part import Part, PartCategory\nfrom inventree.currency import CurrencyManager\nfrom inventree.stock import StockLocation\nfrom inventree.stock import StockItem\nfrom inventree.base import ParameterTemplate, Parameter\n\n\ndef connect(server: str,\n            username: str,\n            password: str,\n            connect_timeout=5,\n            silent=False,\n            proxies=None,\n            token='') -> bool:\n    ''' Connect to InvenTree server and create API object '''\n    from wrapt_timeout_decorator import timeout\n    global inventree_api\n\n    @timeout(dec_timeout=connect_timeout)\n    def get_inventree_api_timeout():\n        return InvenTreeAPI(server,\n                            username=username,\n                            password=password,\n                            proxies=proxies,\n                            token=token)\n\n    try:\n        inventree_api = get_inventree_api_timeout()\n    except:\n        return False\n\n    if inventree_api.token:\n        return True\n    return False\n\n\ndef set_inventree_db_test_mode():\n    ''' InvenTree test database setup '''\n    global inventree_api\n\n    inventree_api.patch('settings/global/PARAMETER_ENFORCE_UNITS/', {'value': False})\n\n\ndef get_inventree_category_id(category_tree: list) -> int:\n    ''' Get InvenTree category ID from name, specificy parent if subcategory '''\n    global inventree_api\n\n    # Fetch all categories\n    part_categories = PartCategory.list(inventree_api, name=category_tree[-1])\n    if len(part_categories) == 1:\n        return part_categories[0].pk\n    else:\n        if len(category_tree) > 1:\n            # Match the parent category\n            parent_category_id = get_inventree_category_id(category_tree[:-1])\n            if parent_category_id:\n                for category in part_categories:\n                    try:\n                        if parent_category_id == category.getParentCategory().pk:\n                            return category.pk\n                    except AttributeError:\n                        pass\n                    #     # Check parent id match (if passed as argument)\n                    #     match = True\n                    #     if parent_category_id:\n                    #         cprint(f'[TREE]\\t{item.getParentCategory().pk} ?= {parent_category_id}', silent=settings.HIDE_DEBUG)\n                    #         if item.getParentCategory().pk != parent_category_id:\n                    #             match = False\n                    #     if match:\n                    #         cprint(f'[TREE]\\t{item.name} ?= {category_name} => True', silent=settings.HIDE_DEBUG)\n                    #         return item.pk\n                    # else:\n                    #     cprint(f'[TREE]\\t{item.name} ?= {category_name} => False', silent=settings.HIDE_DEBUG)\n\n    return -1\n\n\ndef get_inventree_stock_location_id(stock_location_tree: list) -> int:\n    ''' Get InvenTree stock location ID from name, specificy parent if subcategory '''\n    global inventree_api\n\n    # Fetch all categories\n    stock_locations = StockLocation.list(inventree_api, name=stock_location_tree[-1])\n    if len(stock_locations) == 1:\n        return stock_locations[0].pk\n    else:\n        if len(stock_location_tree) > 1:\n            # Match the parent category\n            parent_stock_location_id = get_inventree_category_id(stock_location_tree[:-1])\n            if parent_stock_location_id:\n                for location in stock_locations:\n                    try:\n                        if parent_stock_location_id == location.getParentLocation().pk:\n                            return location.pk\n                    except AttributeError:\n                        pass\n                    #     # Check parent id match (if passed as argument)\n                    #     match = True\n                    #     if parent_stock_location_id:\n                    #         cprint(f'[TREE]\\t{item.getParentCategory().pk} ?= {parent_stock_location_id}', silent=settings.HIDE_DEBUG)\n                    #         if item.getParentCategory().pk != parent_stock_location_id:\n                    #             match = False\n                    #     if match:\n                    #         cprint(f'[TREE]\\t{item.name} ?= {category_name} => True', silent=settings.HIDE_DEBUG)\n                    #         return item.pk\n                    # else:\n                    #     cprint(f'[TREE]\\t{item.name} ?= {category_name} => False', silent=settings.HIDE_DEBUG)\n\n    return -1\n\n\ndef get_categories() -> dict:\n    '''Fetch InvenTree categories'''\n    global inventree_api\n\n    categories = {}\n    # Get all categories (list)\n    db_categories = PartCategory.list(inventree_api)\n\n    def deep_add(tree: dict, keys: list, item: dict):\n        if len(keys) == 1:\n            try:\n                tree[keys[0]].update(item)\n            except (KeyError, AttributeError):\n                tree[keys[0]] = item\n            return\n        return deep_add(tree.get(keys[0]), keys[1:], item)\n\n    for category in db_categories:\n        parent = category.getParentCategory()\n        children = category.getChildCategories()\n\n        if not parent and not children:\n            categories[category.name] = None\n            continue\n        elif parent:\n            parent_list = []\n            while parent:\n                parent_list.insert(0, parent.name)\n                parent = parent.getParentCategory()\n            cat = {category.name: None}\n            deep_add(categories, parent_list, cat)\n\n    return categories\n\n\ndef get_stock_locations() -> dict:\n    '''Fetch InvenTree stock locations'''\n    global inventree_api\n\n    categories = {}\n    # Get all categories (list)\n    db_categories = StockLocation.list(inventree_api)\n\n    def deep_add(tree: dict, keys: list, item: dict):\n        if len(keys) == 1:\n            try:\n                tree[keys[0]].update(item)\n            except (KeyError, AttributeError):\n                tree[keys[0]] = item\n            return\n        return deep_add(tree.get(keys[0]), keys[1:], item)\n\n    for category in db_categories:\n        parent = category.getParentLocation()\n        children = category.getChildLocations()\n\n        if not parent and not children:\n            categories[category.name] = None\n            continue\n        elif parent:\n            parent_list = []\n            while parent:\n                parent_list.insert(0, parent.name)\n                parent = parent.getParentLocation()\n            cat = {category.name: None}\n            deep_add(categories, parent_list, cat)\n\n    return categories\n\n\ndef get_category_tree(category_id: int) -> dict:\n    ''' Get all parents of a category'''\n    category = PartCategory(inventree_api, category_id)\n    category_list = {category_id: category.name}\n\n    while category.parent:\n        category = category.getParentCategory()\n        category_list[category.pk] = category.name\n\n    return category_list\n\n\ndef get_stock_location_tree(id: int) -> dict:\n    ''' Get all parents of a stock_location'''\n    location = StockLocation(inventree_api, id)\n    list = {id: location.name}\n\n    while location.parent:\n        location = location.getParentLocation()\n        list[location.pk] = location.name\n\n    return list\n\n\ndef create_stock(stock_data: dict) -> dict:\n    return StockItem.create(inventree_api, stock_data)\n\n\ndef get_category_parameters(category_id: int) -> list:\n    ''' Get all default parameter templates for category '''\n    global inventree_api\n\n    parameter_templates = []\n\n    category = PartCategory(inventree_api, category_id)\n\n    try:\n        category_templates = category.getCategoryParameterTemplates(fetch_parent=True)\n    except AttributeError:\n        category_templates = None\n\n    if category_templates:\n        for template in category_templates:\n\n            default_value = template.default_value\n            if not default_value:\n                default_value = '-'\n\n            parameter_templates.append([template.getTemplate().name, default_value])\n\n    return parameter_templates\n\n\ndef get_part_info(part_id: int) -> str:\n    ''' Get InvenTree part info from specified Part ID '''\n    global inventree_api\n\n    part = Part(inventree_api, part_id)\n    part_info = {'IPN': part.IPN}\n    attachment = part.getAttachments()\n    if attachment:\n        part_info['datasheet'] = f'{inventree_api.base_url.strip(\"/\")}{attachment[0][\"attachment\"]}'\n    return part_info\n\n\ndef set_part_number(part_id: int, ipn: str) -> bool:\n    ''' Set InvenTree part number for specified Part ID '''\n    data = {'IPN': ipn}\n    update_part(part_id, data)\n\n    if Part(inventree_api, part_id).IPN == ipn:\n        return True\n    else:\n        return False\n\n\ndef get_part_from_ipn(part_ipn='') -> int:\n    ''' Get Part ID from Part IPN '''\n    global inventree_api\n\n    parts = Part.list(inventree_api, IPN=part_ipn)\n\n    if not parts:\n        # No part found\n        return None\n    else:\n        # parts should have only one entry\n        return parts[0]\n\n\ndef fetch_part(part_id='', part_ipn='') -> int:\n    ''' Fetch part from database using either ID or IPN '''\n    from requests.exceptions import HTTPError\n    global inventree_api\n\n    part = None\n    if part_id:\n        try:\n            part = Part(inventree_api, part_id)\n        except TypeError:\n            # Part ID is invalid (eg. decimal value)\n            cprint('[TREE] Error: Part ID type is invalid')\n        except ValueError:\n            # Part ID is not a positive integer\n            cprint('[TREE] Error: Part ID must be positive')\n        except HTTPError:\n            # Part ID does not exist\n            cprint(f'[TREE] Error: Part with ID={part_id} does not exist in database')\n    elif part_ipn:\n        part = get_part_from_ipn(part_ipn)\n    else:\n        pass\n\n    return part\n\n\ndef is_new_part(category_id: int, part_info: dict) -> int:\n    ''' Check if part exists based on parameters (or description) '''\n    global inventree_api\n\n    # Get category object\n    part_category = PartCategory(inventree_api, category_id)\n\n    # Fetch all parts from category and subcategories\n    part_list = []\n    part_list.extend(part_category.getParts())\n    for subcategory in part_category.getChildCategories():\n        part_list.extend(subcategory.getParts())\n\n    # Extract parameter from part info\n    # Verify parameters values are not empty\n    new_part_parameters = part_info['parameters'] if list(set(part_info['parameters'].values())) != ['-'] else None\n\n    template_list = ParameterTemplate.list(inventree_api)\n\n    def fetch_template_name(template_id):\n        for item in template_list:\n            if item.pk == template_id:\n                return item.name\n\n    # Retrieve parent category name for parameters compare\n    try:\n        category_name = part_category.getParentCategory().name\n    except AttributeError:\n        category_name = part_category.name\n    filters = config_interface.load_category_parameters_filters(category=category_name,\n                                                                supplier_config_path=settings.CONFIG_PARAMETERS_FILTERS)\n    # cprint(filters)\n\n    for part in part_list:\n        # TODO: This statement below seems erroneous...\n        # Compare fields (InvenTree does not allow those to be identicals between two parts)\n        # compare_fields = part_info['name'] == part.name and part_info['revision'] == part.revision\n        # if compare_fields:\n        #     cprint(f'[TREE]\\tWarning: Found part with same name and revision (pk = {part.pk})', silent=settings.SILENT)\n        #     return part.pk\n\n        # Compare parameters\n        compare_parameters = False\n        # Get part parameters\n        db_part_parameters = part.getParameters()\n        part_parameters = {}\n        for parameter in db_part_parameters:\n            parameter_name = fetch_template_name(parameter.template)\n            parameter_value = parameter.data\n            part_parameters[parameter_name] = parameter_value\n\n        if new_part_parameters and part_parameters:\n            # Compare database part with new part\n            compare_parameters = part_tools.compare(new_part_parameters=new_part_parameters,\n                                                    db_part_parameters=part_parameters,\n                                                    include_filters=filters)\n                                                            \n        if compare_parameters:\n            cprint(f'[TREE]\\tWarning: Found part with same parameters in database (pk = {part.pk})', silent=settings.SILENT)\n            return part.pk\n\n    # Check if manufacturer part exists in database\n    manufacturer = part_info['manufacturer_name']\n    mpn = part_info['manufacturer_part_number']\n    part_pk = is_new_manufacturer_part(manufacturer, mpn, create=False)\n\n    if part_pk:\n        cprint(f'[TREE]\\tWarning: Found part with same manufacturer and MPN in database (pk = {part_pk})', silent=settings.SILENT)\n        return part_pk\n\n    cprint('\\n[TREE]\\tNo match found in database', silent=settings.HIDE_DEBUG)\n    return 0\n\n\ndef create_category(parent: str, name: str):\n    ''' Create InvenTree category, use parent for subcategories '''\n    global inventree_api\n\n    parent_id = 0\n    is_new_category = False\n\n    # Check if category already exists\n    category_list = PartCategory.list(inventree_api)\n    for category in category_list:\n        if name == category.name:\n            try:\n                # Check if parents are the same\n                if category.getParentCategory().name == parent:\n                    # Return category ID\n                    return category.pk, is_new_category\n            except:\n                return category.pk, is_new_category\n        elif parent == category.name:\n            # Get Parent ID\n            parent_id = category.pk\n        else:\n            pass\n\n    if parent:\n        if parent_id > 0:\n            category = PartCategory.create(inventree_api, {\n                'name': name,\n                'parent': parent_id,\n            })\n\n            is_new_category = True\n        else:\n            cprint(f'[TREE]\\tError: Check parent category name ({parent})', silent=settings.SILENT)\n            return -1, is_new_category\n    else:\n        # No parent\n        category = PartCategory.create(inventree_api, {\n            'name': name,\n            'parent': None,\n        })\n        is_new_category = True\n\n    try:\n        category_pk = category.pk\n    except AttributeError:\n        # User does not have the permission to create categories\n        category_pk = 0\n\n    return category_pk, is_new_category\n\n\ndef upload_part_image(image_url: str, part_id: int, silent=False) -> bool:\n    ''' Upload InvenTree part thumbnail'''\n    global inventree_api\n\n    # Get image full path\n    image_name = f'{str(part_id)}_thumbnail.jpeg'\n    image_location = settings.search_images + image_name\n\n    # Download image (multiple attempts)\n    if not download_with_retry(image_url, image_location, filetype='Image', silent=silent):\n        return False\n\n    # Upload image to InvenTree\n    part = Part(inventree_api, part_id)\n    if part:\n        try:\n            return part.uploadImage(image=image_location)\n        except Exception:\n            return False\n    else:\n        return False\n\n\ndef upload_part_datasheet(datasheet_url: str, part_ipn: int, part_pk: int, silent=False) -> str:\n    ''' Upload InvenTree part attachment'''\n    global inventree_api\n\n    datasheet_name = f'{part_ipn}.pdf'\n    # Get datasheet path based on user settings for local storage\n    if settings.DATASHEET_SAVE_ENABLED:\n        datasheet_location = os.path.join(settings.DATASHEET_SAVE_PATH, datasheet_name)\n    else:\n        datasheet_location = os.path.join(settings.search_datasheets, datasheet_name)\n\n    if not os.path.isfile(datasheet_location):\n        # Download datasheet (multiple attempts)\n        if not download_with_retry(\n            datasheet_url,\n            datasheet_location,\n            filetype='PDF',\n            timeout=10,\n            silent=silent,\n        ):\n            return ''\n\n    # Upload Datasheet to InvenTree\n    part = Part(inventree_api, part_pk)\n    if part:\n        try:\n            attachment = part.uploadAttachment(attachment=datasheet_location)\n            return f'{inventree_api.base_url.strip(\"/\")}{attachment[\"attachment\"]}'\n        except Exception:\n            return ''\n    else:\n        return ''\n\n\ndef create_part(category_id: int, name: str, description: str, revision: str, ipn: str, keywords=None) -> int:\n    ''' Create InvenTree part '''\n    global inventree_api\n\n    try:\n        part = Part.create(inventree_api, {\n            'name': name,\n            'description': description,\n            'category': category_id,\n            'keywords': keywords,\n            'revision': revision,\n            'IPN': ipn,\n            'active': True,\n            'virtual': False,\n            'component': True,\n            'purchaseable': True,\n        })\n    except Exception as e:\n        cprint('[TREE]\\tError: Part creation failed. Check if Ki-nTree settings match InvenTree part settings.', silent=settings.SILENT)\n        cprint(repr(e), silent=settings.SILENT)\n        return 0\n\n    if part:\n        return part.pk\n    else:\n        return 0\n\n\ndef set_part_default_location(part_pk: int, location_pk: int):\n    global inventree_api\n\n    # Retrieve part instance with primary-key of 1\n    part = Part(inventree_api, pk=part_pk)\n\n    # Update specified part parameters\n    part.save(data={\n        \"default_location\": location_pk,\n    })\n\n\ndef update_part(pk: int, data: dict) -> int:\n    '''Update an existing parts data'''\n    global inventree_api\n\n    part = Part(inventree_api, pk)\n    if part:\n        part.save(data=data)\n        return part.pk\n    else:\n        return 0\n\n\ndef create_company(company_name: str, manufacturer=False, supplier=False) -> bool:\n    ''' Create InvenTree company '''\n    global inventree_api\n\n    if not manufacturer and not supplier:\n        return None\n\n    company = Company.create(inventree_api, {\n        'name': company_name,\n        'description': company_name,\n        'is_customer': False,\n        'is_supplier': supplier,\n        'is_manufacturer': manufacturer,\n    })\n\n    return company\n\n\ndef get_all_companies() -> dict:\n    ''' Get all existing companies (supplier/manufacturer) from database '''\n    global inventree_api\n\n    company_list = Company.list(inventree_api)\n    companies = {}\n    for company in company_list:\n        companies[company.name] = company.pk\n\n    return companies\n\n\ndef get_company_id(company_name: str) -> int:\n    ''' Get company (supplier/manufacturer) primary key (ID) '''\n\n    try:\n        return get_all_companies()[company_name]\n    except:\n        return 0\n\n\ndef is_new_manufacturer_part(manufacturer_name: str, manufacturer_mpn: str, create=True) -> int:\n    ''' Check if InvenTree manufacturer part exists to avoid duplicates '''\n    global inventree_api\n\n    if not manufacturer_name:\n        return 0\n\n    # Fetch all companies\n    cprint('[TREE]\\tFetching manufacturers', silent=settings.HIDE_DEBUG)\n    company_list = Company.list(inventree_api, is_manufacturer=True, is_customer=False)\n    companies = {}\n    for company in company_list:\n        companies[company.name] = company\n\n    try:\n        # Get all parts\n        part_list = companies[manufacturer_name].getManufacturedParts()\n    except:\n        part_list = None\n\n    if part_list is None:\n        if create:\n            # Create manufacturer\n            cprint(f'[TREE]\\tCreating new manufacturer \"{manufacturer_name}\"', silent=settings.SILENT)\n            create_company(\n                company_name=manufacturer_name,\n                manufacturer=True,\n            )\n        # Get all parts\n        part_list = []\n\n    for item in part_list:\n        try:\n            if manufacturer_mpn in item.MPN:\n                cprint(f'[TREE]\\t{item.MPN} ?= {manufacturer_mpn} => True', silent=settings.HIDE_DEBUG)\n                return item.part\n            else:\n                cprint(f'[TREE]\\t{item.MPN} ?= {manufacturer_mpn} => False', silent=settings.HIDE_DEBUG)\n        except TypeError:\n            cprint(f'[TREE]\\t{item.MPN} ?= {manufacturer_mpn} => *** SKIPPED ***', silent=settings.HIDE_DEBUG)\n\n    return 0\n\n\ndef is_new_supplier_part(supplier_name: str, supplier_sku: str):\n    ''' Check if InvenTree supplier part exists to avoid duplicates '''\n    global inventree_api\n\n    # Fetch all companies\n    cprint('[TREE]\\tFetching suppliers', silent=settings.HIDE_DEBUG)\n    company_list = Company.list(inventree_api, is_supplier=True, is_customer=False)\n    companies = {}\n    for company in company_list:\n        companies[company.name] = company\n\n    try:\n        # Get all parts\n        part_list = companies[supplier_name].getSuppliedParts()\n    except:\n        part_list = None\n\n    if part_list is None:\n        # Create\n        cprint(f'[TREE]\\tCreating new supplier \"{supplier_name}\"', silent=settings.SILENT)\n        create_company(\n            company_name=supplier_name,\n            supplier=True,\n        )\n        # Get all parts\n        part_list = []\n\n    for item in part_list:\n        if supplier_sku in item.SKU:\n            cprint(f'[TREE]\\t{item.SKU} ?= {supplier_sku} => True', silent=settings.HIDE_DEBUG)\n            return False, item\n        else:\n            cprint(f'[TREE]\\t{item.SKU} ?= {supplier_sku} => False', silent=settings.HIDE_DEBUG)\n\n    return True, False\n\n\ndef create_manufacturer_part(part_id: int, manufacturer_name: str, manufacturer_mpn: str, description: str, datasheet: str) -> bool:\n    ''' Create InvenTree manufacturer part\n\n        part_id: Part the manufacturer data is linked to\n        manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!)\n        MPN: Manufacture part number\n        datasheet: Datasheet link\n        description: Descriptive notes field\n    '''\n    global inventree_api\n\n    # Get Manufacturer ID\n    manufacturer_id = get_company_id(manufacturer_name)\n\n    if manufacturer_id:\n        # Validate datasheet link\n        if not validators.url(datasheet):\n            datasheet = ''\n\n        manufacturer_part = ManufacturerPart.create(inventree_api, {\n            'part': part_id,\n            'manufacturer': manufacturer_id,\n            'MPN': manufacturer_mpn,\n            'link': datasheet,\n            'description': description,\n        })\n\n        if manufacturer_part:\n            return True\n    else:\n        cprint(f'[TREE]\\tError: Manufacturer \"{manufacturer_name}\" not found (failed to create manufacturer part)',\n               silent=settings.SILENT)\n\n    return False\n\n\ndef create_supplier_part(part_id: int, manufacturer_name: str, manufacturer_mpn: str, supplier_name: str, supplier_sku: str, description: str, link: str):\n    ''' Create InvenTree supplier part\n\n        part_id: Part the supplier data is linked to\n        manufacturer_name: Manufacturer the supplier data is linked to\n        manufacturer_mpn: MPN the supplier data is linked to\n        supplier: Company that supplies this SupplierPart object\n        SKU: Stock keeping unit (supplier part number)\n        manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!)\n        MPN: Manufacture part number\n        link: Link to part detail page on supplier's website\n        description: Descriptive notes field\n    '''\n    global inventree_api\n\n    # Get Supplier ID\n    supplier_id = get_company_id(supplier_name)\n\n    if not manufacturer_name or not manufacturer_mpn:\n        # Unset manufacturer data\n        manufacturer_name = None\n        manufacturer_mpn = None\n\n    if supplier_id:\n        # Validate supplier link\n        if not validators.url(link):\n            link = ''\n\n        supplier_part = SupplierPart.create(inventree_api, {\n            'part': part_id,\n            'manufacturer': manufacturer_name,\n            'MPN': manufacturer_mpn,\n            'supplier': supplier_id,\n            'SKU': supplier_sku,\n            'link': link,\n            'description': description,\n        })\n\n        if supplier_part:\n            return True, supplier_part\n    else:\n        cprint(f'[TREE]\\tError: Supplier \"{supplier_name}\" not found (failed to create supplier part)',\n               silent=settings.SILENT)\n\n    return False, False\n\n\ndef update_price_breaks(supplier_part,\n                        price_breaks: dict,\n                        currency='USD') -> bool:\n    ''' Update the Price Breaks associated with a supplier part '''\n    def sanitize_price(price_in):\n        price = re.findall(r'\\d+.\\d+', price_in)[0]\n        price = price.replace(',', '.')\n        price = price.replace('\\xa0', '')\n        return price\n\n    def convert_currency(price):\n        manager = CurrencyManager(inventree_api)\n        base = manager.getBaseCurrency()\n        if base != currency:\n            try:\n                price = manager.convertCurrency(float(price), currency, base)\n            except Exception:\n                cprint('[TREE]\\tWarning: Currency conversion failed.',\n                       silent=settings.SILENT)\n        return price\n\n    if not isinstance(supplier_part, SupplierPart):\n        try:\n            supplier_part = SupplierPart(inventree_api, supplier_part)\n        except:\n            cprint('[TREE]\\tWarning: Supplier part not found, skipping price break update',\n                   silent=settings.SILENT)\n            return False\n    if not price_breaks:\n        cprint('[TREE]\\tWarning: No price breaks found, skipping.', silent=settings.SILENT)\n        return False\n\n    old_price_breaks = supplier_part.getPriceBreaks()\n    updated = []\n    # First process existing price breaks\n    for old_price_break in old_price_breaks:\n        quantity = old_price_break.quantity\n        if quantity in price_breaks:\n            price = price_breaks[quantity]\n            # remove everything but the numbers from the price break\n            if isinstance(price, str):\n                price = sanitize_price(price)\n            price = convert_currency(price)\n            old_price_break.save(data={'price': price})\n            updated.append(quantity)\n        else:\n            old_price_break.delete()\n    for quantity in updated:\n        del price_breaks[quantity]\n    # if any price breaks are left over these will be created\n    for quantity, price in price_breaks.items():\n        # remove everything but the numbers from the price break\n        if isinstance(price, str):\n            price = sanitize_price(price)\n        price = convert_currency(price)\n        SupplierPriceBreak.create(inventree_api, {\n            'part': supplier_part.pk,\n            'quantity': quantity,\n            'price': price,\n        })\n    cprint('[INFO]\\tSuccess: The price breaks were updated', silent=settings.SILENT)\n    return True\n\n\ndef create_parameter_template(name: str, units: str) -> int:\n    ''' Create InvenTree parameter template '''\n    global inventree_api\n\n    parameter_templates = ParameterTemplate.list(inventree_api)\n    for item in parameter_templates:\n        if name == item.name:\n            return 0\n\n    try:\n        parameter_template = ParameterTemplate.create(inventree_api, {\n            'name': name,\n            'units': units if units else '',\n        })\n    except:\n        cprint(f'[TREE]\\tError: Failed to create parameter template \"{name}\".', silent=settings.SILENT)\n        return 0\n\n    if parameter_template:\n        return parameter_template.pk\n    else:\n        return 0\n\n\ndef create_parameter(part_id: int, template_name: int, value: str):\n    ''' Create InvenTree part parameter based on template '''\n    global inventree_api\n\n    parameter_template_list = ParameterTemplate.list(inventree_api)\n\n    template_id = 0\n    for item in parameter_template_list:\n        if template_name == item.name:\n            template_id = item.pk\n            break\n\n    # Check if template_id already exists for this part\n    part = Part(inventree_api, part_id)\n    part_parameters = part.getParameters()\n    is_new_part_parameters_template_id = True\n    was_updated = False\n    parameter = None\n    for item in part_parameters:\n        # cprint(f'[TREE]\\t{parameter.template} ?= {template_id}', silent=SILENT)\n        if item.template == template_id:\n            is_new_part_parameters_template_id = False\n            if settings.UPDATE_INVENTREE:\n                if value != item.data and value != '-':\n                    parameter = item\n                    was_updated = True\n                    try:\n                        parameter.save(data={\n                            'data': value\n                        })\n                    except Exception as e:\n                        cprint(f'[TREE]\\tError: Failed to update part parameter \"{template_name}\".', silent=settings.SILENT)\n                        if \"Could not convert\" in e.args[0]['body'].__str__():\n                            cprint(f'[TREE]\\tError: Parameter value \"{value}\" is not allowed by server settings.', silent=settings.SILENT)\n            break\n    # cprint(part_parameters, silent=SILENT)\n\n    '''\n        Create parameter only if:\n        - template exists\n        - parameter does not exist for this part\n    '''\n    if template_id > 0 and is_new_part_parameters_template_id:\n        try:\n            parameter = Parameter.create(inventree_api, {\n                'model_type': 'part',\n                'model_id': part.pk,\n                'template': template_id,\n                'data': value,\n            })\n        except Exception as e:\n            cprint(f'[TREE]\\tError: Failed to create part parameter \"{template_name}\".', silent=settings.SILENT)\n            if \"Could not convert\" in e.args[0]['body'].__str__():\n                cprint(f'[TREE]\\tError: Parameter value \"{value}\" is not allowed by server settings.', silent=settings.SILENT)\n\n    if parameter:\n        return parameter.pk, is_new_part_parameters_template_id, was_updated\n    else:\n        if template_id == 0:\n            cprint(f'[TREE]\\tError: Parameter template \"{template_name}\" does not exist', silent=settings.SILENT)\n        return 0, False, False\n"
  },
  {
    "path": "kintree/database/inventree_interface.py",
    "content": "import copy\n\nfrom ..config import settings\nfrom ..common import part_tools, progress\nfrom ..common.tools import cprint\nfrom ..config import config_interface\nfrom ..database import inventree_api\nfrom ..search import search_api, automationdirect_api, digikey_api, mouser_api, element14_api, lcsc_api, jameco_api, tme_api\n\ncategory_separator = '/'\n\n\ndef connect_to_server(timeout=5) -> bool:\n    ''' Connect to InvenTree server using user settings '''\n    connect = False\n    settings.load_inventree_settings()\n    if not settings.USERNAME:\n        token = settings.PASSWORD\n    else:\n        token = ''\n\n    try:\n        connect = inventree_api.connect(server=settings.SERVER_ADDRESS,\n                                        username=settings.USERNAME,\n                                        password=settings.PASSWORD,\n                                        proxies=settings.PROXIES,\n                                        token=token,\n                                        connect_timeout=timeout)\n    except TimeoutError:\n        pass\n\n    if not connect:\n        if not settings.SERVER_ADDRESS:\n            cprint('[TREE]\\tError connecting to InvenTree server: missing server address')\n            return connect\n        if not settings.USERNAME:\n            cprint('[TREE]\\tError connecting to InvenTree server: missing username')\n            return connect\n        if not settings.PASSWORD:\n            cprint('[TREE]\\tError connecting to InvenTree server: missing password')\n            return connect\n        cprint('[TREE]\\tError connecting to InvenTree server: invalid address, username or password')\n    else:\n        env = [env_type.name for env_type in settings.Environment\n               if env_type.value == settings.environment][0]\n        cprint(f'[TREE]\\tSuccessfully connected to InvenTree server (ENV={env})', silent=settings.SILENT)\n\n    return connect\n\n\ndef category_tree(tree: str) -> str:\n    import re\n    find_prefix = re.match(r'^-+ (.+?)$', tree)\n    if find_prefix:\n        return find_prefix.group(1)\n    return tree\n\n\ndef split_category_tree(tree: str) -> list:\n    return category_tree(tree).split(category_separator)\n\n\ndef build_category_tree(reload=False, category=None) -> dict:\n    '''Build InvenTree category tree from database data'''\n\n    category_data = config_interface.load_file(settings.CONFIG_CATEGORIES)\n\n    def build_tree(tree, left_to_go, level) -> list:\n        try:\n            last_entry = f' {category_tree(tree[-1])}{category_separator}'\n        except IndexError:\n            last_entry = ''\n        if isinstance(left_to_go, dict):\n            for key, value in left_to_go.items():\n                tree.append(f'{\"-\" * level}{last_entry}{key}')\n                build_tree(tree, value, level + 1)\n        elif isinstance(left_to_go, list):\n            # Supports legacy structure\n            for item in left_to_go:\n                tree.append(f'{\"-\" * level}{last_entry}{item}')\n        elif left_to_go is None:\n            pass\n        return\n\n    if reload:\n        categories = inventree_api.get_categories()\n        category_data.update({'CATEGORIES': categories})\n        config_interface.dump_file(category_data, settings.CONFIG_CATEGORIES)\n    else:\n        categories = category_data.get('CATEGORIES', {})\n\n    # Get specified branch\n    if category:\n        categories = {category: categories.get(category, {})}\n\n    inventree_categories = []\n    # Build category tree\n    build_tree(inventree_categories, categories, 0)\n\n    return inventree_categories\n\n\ndef build_stock_location_tree(reload=False, location=None) -> dict:\n    '''Build InvenTree stock locations tree from database data'''\n\n    locations_data = config_interface.load_file(settings.CONFIG_STOCK_LOCATIONS)\n\n    def build_tree(tree, left_to_go, level) -> list:\n        try:\n            last_entry = f' {category_tree(tree[-1])}{category_separator}'\n        except IndexError:\n            last_entry = ''\n        if isinstance(left_to_go, dict):\n            for key, value in left_to_go.items():\n                tree.append(f'{\"-\" * level}{last_entry}{key}')\n                build_tree(tree, value, level + 1)\n        elif isinstance(left_to_go, list):\n            # Supports legacy structure\n            for item in left_to_go:\n                tree.append(f'{\"-\" * level}{last_entry}{item}')\n        elif left_to_go is None:\n            pass\n        return\n\n    if reload:\n        stock_locations = inventree_api.get_stock_locations()\n        locations_data.update({'STOCK_LOCATIONS': stock_locations})\n        config_interface.dump_file(locations_data, settings.CONFIG_STOCK_LOCATIONS)\n    else:\n        stock_locations = locations_data.get('STOCK_LOCATIONS', {})\n\n    # Get specified branch\n    if location:\n        stock_locations = {location: stock_locations.get(location, {})}\n\n    inventree_stock_locations = []\n    # Build category tree\n    build_tree(inventree_stock_locations, stock_locations, 0)\n\n    return inventree_stock_locations\n\n\ndef get_categories_from_supplier_data(part_info: dict, supplier_only=False) -> list:\n    ''' Find categories from part supplier data, use \"somewhat automatic\" matching '''\n    from thefuzz import fuzz\n    \n    categories = [None, None]\n\n    try:\n        supplier_category = str(part_info['category_tree'][0])\n        supplier_subcategory = str(part_info['category_tree'][1])\n    except KeyError:\n        return categories\n\n    # Return supplier category, if match not needed\n    if supplier_only:\n        categories[0] = supplier_category\n        categories[1] = supplier_subcategory\n        return categories\n\n    function_filter = False\n    # TODO: Make 'filter_parameter' user defined?\n    filter_parameter = 'Function Type'\n\n    # Check existing matches\n    # Load inversed category map\n    category_map = config_interface.load_supplier_categories_inversed(supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES)\n\n    try:\n        for inventree_category in category_map.keys():\n            for key, inventree_subcategory in category_map[inventree_category].items():\n                if supplier_subcategory == key:\n                    categories[0] = inventree_category\n                    # Check if filtering by function\n                    if inventree_subcategory.startswith(config_interface.FUNCTION_FILTER_KEY):\n                        function_filter = True\n\n                    # Save subcategory if not function filtered\n                    if not function_filter:\n                        categories[1] = inventree_subcategory\n\n                    break\n    except:\n        pass\n\n    # Function Filter\n    if not categories[1] and function_filter:\n        cprint(f'[INFO]\\tSubcategory is filtered using \"{filter_parameter}\" parameter', silent=settings.SILENT, end='')\n        # Load parameter map\n        parameter_map = config_interface.load_category_parameters(categories, settings.CONFIG_SUPPLIER_PARAMETERS)\n        # Build compare list\n        compare = []\n        for supplier_parameter, inventree_parameter in parameter_map.items():\n            if (supplier_parameter in part_info['parameters'].keys() and inventree_parameter == filter_parameter):\n                compare.append(part_info['parameters'][supplier_parameter])\n\n        # Load subcategory map\n        category_map = config_interface.load_supplier_categories(supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES)[categories[0]]\n        for inventree_subcategory in category_map.keys():\n            for item in compare:\n                fuzzy_match = fuzz.partial_ratio(inventree_subcategory, item)\n                display_result = f'\"{inventree_subcategory}\" ?= \"{item}\"'.ljust(50)\n                cprint(f'{display_result} => {fuzzy_match}', silent=settings.HIDE_DEBUG)\n                if fuzzy_match >= settings.CATEGORY_MATCH_RATIO_LIMIT:\n                    categories[1] = inventree_subcategory.replace(config_interface.FUNCTION_FILTER_KEY, '')\n                    break\n\n            if categories[1]:\n                cprint('\\t[ PASS ]', silent=settings.SILENT)\n                break\n\n    if not categories[1] and function_filter:\n        cprint('\\t[ FAILED ]', silent=settings.SILENT)\n\n    # Automatic Match\n    if not (categories[0] and categories[1]):\n        # Load category map\n        category_map = config_interface.load_supplier_categories(supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES)\n\n        def find_supplier_category_match(supplier_category: str, ignore_categories=False):\n            # Check for match with Inventree categories\n            category_match = None\n            subcategory_match = None\n\n            for inventree_category in category_map.keys():\n                fuzzy_match = 0\n                \n                if not ignore_categories:\n                    fuzzy_match = fuzz.partial_ratio(supplier_category, inventree_category)\n                    display_result = f'\"{supplier_category}\" ?= \"{inventree_category}\"'.ljust(50)\n                    cprint(f'{display_result} => {fuzzy_match}', silent=settings.HIDE_DEBUG)\n\n                if fuzzy_match < settings.CATEGORY_MATCH_RATIO_LIMIT and category_map[inventree_category]:\n                    # Compare to subcategories\n                    for inventree_subcategory in category_map[inventree_category]:\n                        fuzzy_match = fuzz.partial_ratio(supplier_category, inventree_subcategory)\n                        display_result = f'\"{supplier_category}\" ?= \"{inventree_subcategory}\"'.ljust(50)\n                        cprint(f'{display_result} => {fuzzy_match}', silent=settings.HIDE_DEBUG)\n\n                        if fuzzy_match >= settings.CATEGORY_MATCH_RATIO_LIMIT:\n                            subcategory_match = inventree_subcategory\n                            break\n\n                if fuzzy_match >= settings.CATEGORY_MATCH_RATIO_LIMIT:\n                    category_match = inventree_category\n                    break\n\n            return category_match, subcategory_match\n\n        # Find category and subcategories match\n        category, subcategory = find_supplier_category_match(supplier_category)\n        if category:\n            categories[0] = category\n        if subcategory:\n            categories[1] = subcategory\n\n        # Run match with supplier subcategory\n        if not categories[0] or not categories[1]:\n            if categories[0]:\n                # If category was found: ignore them for the comparison\n                category, subcategory = find_supplier_category_match(supplier_subcategory, ignore_categories=True)\n            else:\n                category, subcategory = find_supplier_category_match(supplier_subcategory)\n\n        if category and not categories[0]:\n            categories[0] = category\n        if subcategory and not categories[1]:\n            categories[1] = subcategory\n\n    # Final checks\n    if not categories[0]:\n        cprint(f'[INFO]\\tWarning: \"{part_info[\"category_tree\"][0]}\" did not match any supplier category ', silent=settings.SILENT)\n    else:\n        cprint(f'[INFO]\\tCategory: \"{categories[0]}\"', silent=settings.SILENT)\n    if not categories[1]:\n        cprint(f'[INFO]\\tWarning: \"{part_info[\"category_tree\"][1]}\" did not match any supplier subcategory ', silent=settings.SILENT)\n    else:\n        cprint(f'[INFO]\\tSubcategory: \"{categories[1]}\"', silent=settings.SILENT)\n    \n    # print(f'{supplier_category=} | {supplier_subcategory=} | {categories[0]=} | {categories[1]=}')\n    return categories\n\n\ndef translate_form_to_inventree(part_info: dict, category_tree: list, is_custom=False) -> dict:\n    ''' Using supplier part data and categories, fill-in InvenTree part dictionary '''\n\n    # Copy template\n    inventree_part = copy.deepcopy(settings.inventree_part_template)\n\n    # Translate form data to inventree part\n    inventree_part['category_tree'] = category_tree\n    inventree_part['name'] = part_info['name']\n    inventree_part['description'] = part_info['description']\n    inventree_part['revision'] = part_info['revision']\n    inventree_part['keywords'] = part_info['keywords']\n    inventree_part['supplier_name'] = part_info['supplier_name']\n    inventree_part['supplier_part_number'] = part_info['supplier_part_number']\n    inventree_part['manufacturer_name'] = part_info['manufacturer_name']\n    inventree_part['manufacturer_part_number'] = part_info['manufacturer_part_number']\n    inventree_part['IPN'] = part_info.get('IPN', '')\n    # Replace whitespaces in URL\n    inventree_part['supplier_link'] = part_info['supplier_link'].replace(' ', '%20')\n    inventree_part['datasheet'] = part_info['datasheet'].replace(' ', '%20')\n    # Image URL is not shown to user so force default key/value\n    try:\n        inventree_part['image'] = part_info['image'].replace(' ', '%20')\n    except AttributeError:\n        # Part image URL is null (no product picture)\n        pass\n    inventree_part['pricing'] = part_info.get('pricing', {})\n    inventree_part['currency'] = part_info.get('currency', 'USD')\n\n    parameters = part_info.get('parameters', {})\n\n    # Load parameters map\n    if category_tree:\n        parameter_map = config_interface.load_category_parameters(\n            categories=category_tree,\n            supplier_config_path=settings.CONFIG_SUPPLIER_PARAMETERS,\n        )\n    else:\n        cprint('[INFO]\\tWarning: Parameter map not loaded (no category selected)', silent=settings.SILENT)\n\n    if not is_custom:\n        # Add Parameters\n        if parameter_map:\n            parameters_missing = []\n            for supplier_param, inventree_param in parameter_map.items():\n                # Some parameters may not be mapped\n                if inventree_param not in inventree_part['parameters'].keys():\n                    if supplier_param == 'Manufacturer Part Number':\n                        inventree_part['parameters'][inventree_param] = part_info['manufacturer_part_number']\n                    elif inventree_param == 'image':\n                        inventree_part['existing_image'] = supplier_param\n                    else:\n                        try:\n                            parameter_value = part_tools.clean_parameter_value(\n                                category=category_tree[0],\n                                name=supplier_param,\n                                value=parameters[supplier_param],\n                            )\n                            inventree_part['parameters'][inventree_param] = parameter_value\n                        except KeyError:\n                            parameters_missing.append(supplier_param)\n            if parameters_missing:\n                msg = '[INFO]\\tWarning: The following parameters were not found in supplier data:\\n'\n                msg += str(parameters_missing)\n                cprint(msg, silent=settings.SILENT)\n\n            # Check for missing InvenTree parameters and fill value with dash\n            for inventree_param in parameter_map.values():\n                if inventree_param == 'image':\n                    continue\n                if inventree_param not in inventree_part['parameters'].keys():\n                    inventree_part['parameters'][inventree_param] = '-'\n\n            # Check for extra parameters which weren't mapped\n            parameters_unmapped = []\n            for search_param in parameters.keys():\n                if search_param not in parameter_map.keys():\n                    parameters_unmapped.append(search_param)\n            \n            if parameters_unmapped:\n                if not settings.SILENT:\n                    msg = f'[INFO]\\tThe following parameters are not mapped in {inventree_part[\"supplier_name\"]} parameters configuration:\\n'\n                    msg += str(parameters_unmapped)\n                    print(msg)\n        else:\n            cprint(f'[INFO]\\tWarning: Parameter map for \"{category_tree[0]}\" does not exist or is empty', silent=settings.SILENT)\n\n    return inventree_part\n\n\ndef get_supplier_name(supplier: str) -> str:\n    ''' Get InvenTree supplier name '''\n\n    supplier_name = supplier\n\n    for supplier, data in settings.CONFIG_SUPPLIERS.items():\n        if data['name'] == supplier_name:\n            # Update supplier name\n            supplier_name = supplier\n            break\n    \n    return supplier_name\n\n\ndef translate_supplier_to_form(supplier: str, part_info: dict) -> dict:\n    ''' Translate supplier data to user form format '''\n\n    part_form = {}\n\n    def get_value_from_user_key(user_key: str, default_key: str, default_value=None) -> str:\n        ''' Get value mapped from user search key, else default search key '''\n        user_search_key = None\n        if supplier == 'Digi-Key':\n            user_search_key = settings.CONFIG_DIGIKEY.get(user_key, None)\n        elif supplier == 'Mouser':\n            user_search_key = settings.CONFIG_MOUSER.get(user_key, None)\n        elif supplier in ['Farnell', 'Newark', 'Element14']:\n            user_search_key = settings.CONFIG_ELEMENT14.get(user_key, None)\n        elif supplier == 'LCSC':\n            user_search_key = settings.CONFIG_LCSC.get(user_key, None)\n        elif supplier == 'Jameco':\n            user_search_key = settings.CONFIG_JAMECO.get(user_key, None)\n        elif supplier == 'TME':\n            user_search_key = settings.CONFIG_TME.get(user_key, None)\n        elif supplier == 'AutomationDirect':\n            user_search_key = settings.CONFIG_AUTOMATIONDIRECT.get(user_key, None)\n\n        else:\n            return default_value\n        \n        # If no user key, use default\n        if not user_search_key:\n            return part_info.get(default_key, default_value)\n\n        # Get value for user key, return value from default key if not found\n        return part_info.get(user_search_key, part_info.get(default_key, default_value))\n\n    # Check that supplier argument is valid\n    if not supplier and supplier != 'custom':\n        return part_form\n    # Get default keys\n    if supplier == 'Digi-Key':\n        default_search_keys = digikey_api.get_default_search_keys()\n    elif supplier == 'Mouser':\n        default_search_keys = mouser_api.get_default_search_keys()\n    elif supplier in ['Farnell', 'Newark', 'Element14']:\n        default_search_keys = element14_api.get_default_search_keys()\n    elif supplier == 'LCSC':\n        default_search_keys = lcsc_api.get_default_search_keys()\n    elif supplier == 'Jameco':\n        default_search_keys = jameco_api.get_default_search_keys()\n    elif supplier == 'TME':\n        default_search_keys = tme_api.get_default_search_keys()\n    elif supplier == 'AutomationDirect':\n        default_search_keys = automationdirect_api.get_default_search_keys()\n    else:\n        # Empty array of default search keys\n        default_search_keys = [''] * len(digikey_api.get_default_search_keys())\n\n    # Default revision\n    revision = settings.CONFIG_IPN.get('INVENTREE_DEFAULT_REV', '')\n    # Translate supplier data to form fields\n    part_form['name'] = get_value_from_user_key('SEARCH_NAME', default_search_keys[0], default_value='')\n    part_form['description'] = get_value_from_user_key('SEARCH_DESCRIPTION', default_search_keys[1], default_value='')\n    part_form['revision'] = get_value_from_user_key('SEARCH_REVISION', default_search_keys[2], default_value=revision)\n    part_form['keywords'] = get_value_from_user_key('SEARCH_KEYWORDS', default_search_keys[3], default_value='')\n    part_form['supplier_name'] = settings.CONFIG_SUPPLIERS[supplier]['name']\n    part_form['supplier_part_number'] = get_value_from_user_key('SEARCH_SKU', default_search_keys[4], default_value='')\n    part_form['supplier_link'] = get_value_from_user_key('SEARCH_SUPPLIER_URL', default_search_keys[7], default_value='')\n    part_form['manufacturer_name'] = get_value_from_user_key('SEARCH_MANUFACTURER', default_search_keys[5], default_value='')\n    part_form['manufacturer_part_number'] = get_value_from_user_key('SEARCH_MPN', default_search_keys[6], default_value='')\n    part_form['datasheet'] = get_value_from_user_key('SEARCH_DATASHEET', default_search_keys[8], default_value='')\n    part_form['image'] = get_value_from_user_key('', default_search_keys[9], default_value='')\n    \n    return part_form\n\n\ndef supplier_search(supplier: str, part_number: str, test_mode=False) -> dict:\n    ''' Wrapper for supplier search, allow use of cached data (limited daily API calls) '''\n    part_info = {}\n    # Check part number exist\n    if not part_number:\n        cprint('\\n[MAIN]\\tError: Missing Part Number', silent=settings.SILENT)\n        return part_info\n\n    store = ''\n    if supplier in ['Farnell', 'Newark', 'Element14']:\n        try:\n            element14_config = config_interface.load_file(settings.CONFIG_ELEMENT14_API)\n            store = element14_config.get(f'{supplier.upper()}_STORE', '').replace(' ', '')\n        except AttributeError:\n            cprint(f'\\n[INFO]\\tWarning: {supplier.upper()}_STORE value not found', silent=False)\n\n    search_filename = f\"{settings.search_results['directory']}{supplier}{store}_{part_number}{settings.search_results['extension']}\"\n    # Get cached data, if cache is enabled (else returns None)\n    part_cache = search_api.load_from_file(search_filename, test_mode)\n\n    if part_cache:\n        cprint(f'\\n[MAIN]\\tUsing {supplier} cached data for {part_number}', silent=settings.SILENT)\n        part_info = part_cache\n    else:\n        cprint(f'\\n[MAIN]\\t{supplier} search for {part_number}', silent=settings.SILENT)\n        if supplier == 'Digi-Key':\n            part_info = digikey_api.fetch_part_info(part_number)\n        elif supplier == 'Mouser':\n            part_info = mouser_api.fetch_part_info(part_number)\n        elif supplier in ['Farnell', 'Newark', 'Element14']:\n            part_info = element14_api.fetch_part_info(part_number, supplier)\n        elif supplier == 'LCSC':\n            part_info = lcsc_api.fetch_part_info(part_number)\n        elif supplier == 'Jameco':\n            part_info = jameco_api.fetch_part_info(part_number)\n        elif supplier == 'TME':\n            part_info = tme_api.fetch_part_info(part_number)\n        elif supplier == 'AutomationDirect':\n            part_info = automationdirect_api.fetch_part_info(part_number)\n\n    # Check supplier data exist\n    if not part_info:\n        cprint(f'[INFO]\\tError: Failed to fetch data for \"{part_number}\"', silent=settings.SILENT)\n\n    # Save search results\n    if part_info:\n        update_ts = not bool(part_cache) or test_mode\n        search_api.save_to_file(part_info, search_filename, update_ts=update_ts)\n\n    return part_info\n\n\ndef inventree_fuzzy_company_match(name: str) -> str:\n    ''' Fuzzy match company name to exisiting companies '''\n    from thefuzz import fuzz\n    \n    inventree_companies = inventree_api.get_all_companies()\n\n    for company_name in inventree_companies.keys():\n        cprint(f'{name.lower()} == {company_name.lower()} % {fuzz.partial_ratio(name.lower(), company_name.lower())}',\n               silent=settings.HIDE_DEBUG)\n        if fuzz.partial_ratio(name.lower(), company_name.lower()) == 100 and len(name) == len(company_name):\n            return company_name\n    \n    return name\n\n\ndef inventree_create_manufacturer_part(part_id: int, manufacturer_name: str, manufacturer_mpn: str, datasheet: str, description: str) -> bool:\n    ''' Create manufacturer part '''\n\n    cprint('\\n[MAIN]\\tCreating manufacturer part', silent=settings.SILENT)\n    manufacturer_part = inventree_api.is_new_manufacturer_part(manufacturer_name=manufacturer_name,\n                                                               manufacturer_mpn=manufacturer_mpn)\n\n    if manufacturer_part:\n        cprint('[INFO]\\tManufacturer part already exists, skipping.', silent=settings.SILENT)\n    else:\n        # Create a new manufacturer part\n        is_manufacturer_part_created = inventree_api.create_manufacturer_part(part_id=part_id,\n                                                                              manufacturer_name=manufacturer_name,\n                                                                              manufacturer_mpn=manufacturer_mpn,\n                                                                              datasheet=datasheet,\n                                                                              description=description)\n\n        if is_manufacturer_part_created:\n            cprint('[INFO]\\tSuccess: Added new manufacturer part', silent=settings.SILENT)\n            return True\n\n    return False\n\n\ndef inventree_create_supplier_part(part) -> bool:\n    return\n\n\ndef get_inventree_stock_location_id(stock_location_tree: list):\n    return inventree_api.get_inventree_stock_location_id(stock_location_tree)\n\n\ndef inventree_create(part_info: dict, stock=None, kicad=False, symbol=None, footprint=None, show_progress=True, is_custom=False, enable_upload=True):\n    ''' Create InvenTree part from supplier part data and categories '''\n\n    part_pk = 0\n    new_part = False\n\n    category_tree = part_info['category_tree']\n    if not category_tree:\n        cprint(f'[INFO]\\tError: Category tree is empty {category_tree=}', silent=settings.SILENT)\n        return new_part, part_pk, {}\n\n    # Translate to InvenTree part format\n    inventree_part = translate_form_to_inventree(\n        part_info=part_info,\n        category_tree=category_tree,\n        is_custom=is_custom,\n    )\n\n    if not inventree_part:\n        cprint('\\n[MAIN]\\tError: Failed to process form data', silent=settings.SILENT)\n\n    category_pk = inventree_api.get_inventree_category_id(category_tree)\n    if category_pk <= 0:\n        cprint(f'[ERROR]\\tCategory ({category_tree}) does not exist in InvenTree', silent=settings.SILENT)\n    else:\n        if settings.CHECK_EXISTING:\n            # Check if part already exists\n            part_pk = inventree_api.is_new_part(category_pk, inventree_part)\n            # Part exists\n            if part_pk > 0:\n                cprint('[INFO]\\tPart already exists, skipping.', silent=settings.SILENT)\n                info = inventree_api.get_part_info(part_pk)\n                if info:\n                    # Update InvenTree part number\n                    inventree_part = {**inventree_part, **info}\n                    # Update InvenTree URL\n                    inventree_part['inventree_url'] = f'{settings.PART_URL_ROOT}{part_pk}/'\n                else:\n                    inventree_part['inventree_url'] = f'{settings.PART_URL_ROOT}{part_pk}/'\n        # Part is new\n        if not part_pk:\n            new_part = True\n            if settings.CONFIG_IPN.get('IPN_ENABLE_CREATE', True):\n                # Generate Placeholder Internal Part Number\n                ipn = part_tools.generate_part_number(\n                    category=category_tree[0],\n                    part_pk=0,\n                    category_code=part_info.get('category_code', ''),\n                )\n            else:\n                ipn = ''\n            # Create a new Part\n            # Use the pk (primary-key) of the category\n            part_pk = inventree_api.create_part(\n                category_id=category_pk,\n                name=inventree_part['name'],\n                description=inventree_part['description'],\n                revision=inventree_part['revision'],\n                keywords=inventree_part['keywords'],\n                ipn=ipn)\n\n            # Check part primary key\n            if not part_pk:\n                return new_part, part_pk, inventree_part\n            # Progress Update\n            if not progress.update_progress_bar(show_progress):\n                return new_part, part_pk, inventree_part\n\n            if settings.CONFIG_IPN.get('IPN_ENABLE_CREATE', True):\n                # Generate Internal Part Number\n                cprint('\\n[MAIN]\\tGenerating Internal Part Number', silent=settings.SILENT)\n                if settings.CONFIG_IPN.get('IPN_USE_MANUFACTURER_PART_NUMBER', False):\n                    ipn = inventree_part['manufacturer_part_number']\n                else:\n                    ipn = part_tools.generate_part_number(\n                        category=category_tree[0],\n                        part_pk=part_pk,\n                        category_code=part_info.get('category_code', ''),\n                    )\n                cprint(f'[INFO]\\tInternal Part Number = {ipn}', silent=settings.SILENT)\n                # Update InvenTree part number\n                ipn_update = inventree_api.set_part_number(part_pk, ipn)\n                if not ipn_update:\n                    cprint('\\n[INFO]\\tError updating IPN', silent=settings.SILENT)\n                inventree_part['IPN'] = ipn\n            # Update InvenTree URL\n            inventree_part['inventree_url'] = f'{settings.PART_URL_ROOT}{part_pk}/'\n\n    # Progress Update\n    if not progress.update_progress_bar(show_progress):\n        return new_part, part_pk, inventree_part\n\n    if part_pk > 0:\n        if new_part:\n            cprint('[INFO]\\tSuccess: Added new part to InvenTree', silent=settings.SILENT)\n            if inventree_part.get('existing_image', ''):\n                inventree_api.update_part(\n                    part_pk,\n                    data={'existing_image': inventree_part['existing_image']})\n            elif inventree_part['image']:\n                if enable_upload:\n                    # Add image\n                    image_result = inventree_api.upload_part_image(inventree_part['image'], part_pk, silent=settings.SILENT)\n                    if not image_result:\n                        cprint('[TREE]\\tWarning: Failed to upload part image', silent=settings.SILENT)\n        if inventree_part['datasheet'] and settings.DATASHEET_UPLOAD:\n            if enable_upload:\n                # Upload datasheet\n                datasheet_link = inventree_api.upload_part_datasheet(\n                    datasheet_url=inventree_part['datasheet'],\n                    part_ipn=inventree_part['IPN'],\n                    part_pk=part_pk,\n                    silent=settings.SILENT,\n                )\n                if not datasheet_link:\n                    cprint('[TREE]\\tWarning: Failed to upload part datasheet', silent=settings.SILENT)\n                else:\n                    cprint('[TREE]\\tSuccess: Uploaded part datasheet', silent=settings.SILENT)\n\n        if kicad:\n            try:\n                symbol_name = ipn\n            except UnboundLocalError:\n                symbol_name = inventree_part.get('manufacturer_part_number')\n\n            # Create symbol & footprint parameters\n            if symbol:\n                symbol = f'{symbol.split(\":\")[0]}:{symbol_name}'\n                inventree_part['parameters']['Symbol'] = symbol\n            if footprint:\n                inventree_part['parameters']['Footprint'] = footprint\n\n        if not inventree_part['parameters']:\n            category_parameters = inventree_api.get_category_parameters(category_pk)\n\n            # Add category-defined parameters\n            for parameter in category_parameters:\n                inventree_part['parameters'][parameter[0]] = parameter[1]\n\n        # Create parameters\n        if len(inventree_part['parameters']) > 0:\n            if not inventree_process_parameters(\n                    part_id=part_pk,\n                    parameters=inventree_part['parameters'],\n                    show_progress=show_progress):\n                return new_part, part_pk, inventree_part\n            \n        # Create manufacturer part\n        if inventree_part['manufacturer_name'] and inventree_part['manufacturer_part_number']:\n            # Overwrite manufacturer name with matching one from database\n            manufacturer_name = inventree_fuzzy_company_match(inventree_part['manufacturer_name'])\n            # Get MPN\n            manufacturer_mpn = inventree_part['manufacturer_part_number']\n\n            cprint('\\n[MAIN]\\tCreating manufacturer part', silent=settings.SILENT)\n            manufacturer_part = inventree_api.is_new_manufacturer_part(\n                manufacturer_name=manufacturer_name,\n                manufacturer_mpn=manufacturer_mpn,\n            )\n\n            if manufacturer_part:\n                cprint('[INFO]\\tManufacturer part already exists, skipping.', silent=settings.SILENT)\n            else:\n                # Create a new manufacturer part\n                is_manufacturer_part_created = inventree_api.create_manufacturer_part(\n                    part_id=part_pk,\n                    manufacturer_name=manufacturer_name,\n                    manufacturer_mpn=manufacturer_mpn,\n                    datasheet=inventree_part['datasheet'],\n                    description=inventree_part['description'],\n                )\n\n                if is_manufacturer_part_created:\n                    cprint('[INFO]\\tSuccess: Added new manufacturer part', silent=settings.SILENT)\n\n        # Create supplier part\n        if inventree_part['supplier_name'] and inventree_part['supplier_part_number']:\n            # Overwrite manufacturer name with matching one from database\n            supplier_name = inventree_fuzzy_company_match(inventree_part['supplier_name'])\n            # Get SKU\n            supplier_sku = inventree_part['supplier_part_number']\n\n            cprint('\\n[MAIN]\\tCreating supplier part', silent=settings.SILENT)\n            is_new_supplier_part, supplier_part = inventree_api.is_new_supplier_part(\n                supplier_name=supplier_name,\n                supplier_sku=supplier_sku)\n\n            if not is_new_supplier_part:\n                cprint('[INFO]\\tSupplier part already exists, skipping.', silent=settings.SILENT)\n            else:\n                # Create a new supplier part\n                is_supplier_part_created, supplier_part = inventree_api.create_supplier_part(\n                    part_id=part_pk,\n                    manufacturer_name=manufacturer_name,\n                    manufacturer_mpn=manufacturer_mpn,\n                    supplier_name=supplier_name,\n                    supplier_sku=supplier_sku,\n                    description=inventree_part['description'],\n                    link=inventree_part['supplier_link'],\n                )\n\n                if is_supplier_part_created:\n                    cprint('[INFO]\\tSuccess: Added new supplier part', silent=settings.SILENT)\n            \n            if supplier_part and settings.PRICING_UPLOAD:\n                cprint('\\n[MAIN]\\tProcessing Price Breaks', silent=settings.SILENT)\n                inventree_api.update_price_breaks(\n                    supplier_part=supplier_part,\n                    price_breaks=inventree_part['pricing'],\n                    currency=inventree_part['currency'])\n\n        if stock is not None:\n            stock['part'] = part_pk\n            inventree_api.create_stock(stock)\n            if stock['make_default']:\n                inventree_api.set_part_default_location(part_pk, stock['location'])\n\n    # Progress Update\n    if not progress.update_progress_bar(show_progress):\n        pass\n\n    return new_part, part_pk, inventree_part\n\n\ndef inventree_process_parameters(part_id: str, parameters: dict, show_progress=True) -> bool:\n    ''' Create or Update parameters for an InvenTree part'''\n    cprint('\\n[MAIN]\\tCreating parameters', silent=settings.SILENT)\n    parameters_lists = [\n        [],  # Store new parameters\n        [],  # Store updated parameters\n        [],  # Store unchanged parameters\n    ]\n    for name, value in parameters.items():\n        parameter, is_new_parameter, was_updated = inventree_api.create_parameter(part_id=part_id, template_name=name, value=value)\n        # Progress Update\n        if not progress.update_progress_bar(show_progress, increment=0.03):\n            return False\n        if is_new_parameter:\n            parameters_lists[0].append(name)\n        elif was_updated:\n            parameters_lists[1].append(name)\n        else:\n            parameters_lists[2].append(name)\n    if parameters_lists[0]:\n        cprint('[INFO]\\tSuccess: The following parameters were created:', silent=settings.SILENT)\n        for item in parameters_lists[0]:\n            cprint(f'--->\\t{item}', silent=settings.SILENT)\n    if parameters_lists[1]:\n        cprint('[INFO]\\tSuccess: The following parameters were updated:', silent=settings.SILENT)\n        for item in parameters_lists[1]:\n            cprint(f'--->\\t{item}', silent=settings.SILENT)\n    if parameters_lists[2]:\n        cprint('[TREE]\\tWarning: The following parameters were skipped:', silent=settings.SILENT)\n        for item in parameters_lists[2]:\n            cprint(f'--->\\t{item}', silent=settings.SILENT)\n    return True\n\n\ndef inventree_create_alternate(part_info: dict, part_id='', part_ipn='', show_progress=None) -> bool:\n    ''' Create alternate manufacturer and supplier entries for an existing InvenTree part '''\n\n    result = False\n    cprint('\\n[MAIN]\\tSearching for original part in database', silent=settings.SILENT)\n    part = inventree_api.fetch_part(part_id, part_ipn)\n\n    if part:\n        part_pk = part.pk\n        part_description = part.description\n        cprint(f'[INFO] Success: Found original part in database (ID = {part_pk} | Description = \"{part_description}\")', silent=settings.SILENT)\n    else:\n        cprint('[INFO] Error: Original part was not found in database', silent=settings.SILENT)\n        return result\n    # Translate to InvenTree part format\n    category_tree = inventree_api.get_category_tree(part.category)\n    category_tree = list(category_tree.values())\n    category_tree.reverse()\n    inventree_part = translate_form_to_inventree(\n        part_info=part_info,\n        category_tree=category_tree,\n    )\n\n    # If the part has no image yet try to upload it from the data\n    if not part.image:\n        image = part_info.get('image', '')\n        existing_image = inventree_part.get('existing_image', '')\n        if existing_image:\n            inventree_api.update_part(pk=part_pk,\n                                      data={'existing_image': existing_image})\n        elif image:\n            inventree_api.upload_part_image(image_url=image, part_id=part_pk, silent=settings.SILENT)\n\n    # create or update parameters\n    if inventree_part.get('parameters', {}):\n        inventree_process_parameters(part_id=part_pk,\n                                     parameters=inventree_part['parameters'],\n                                     show_progress=show_progress)\n\n    # Overwrite manufacturer name with matching one from database\n    manufacturer_name = inventree_fuzzy_company_match(part_info.get('manufacturer_name', ''))\n    manufacturer_mpn = part_info.get('manufacturer_part_number', '')\n    datasheet = part_info.get('datasheet', '')\n\n    attachment = part.getAttachments()\n    # if datasheet upload is enabled and no attachment present yet then upload\n    if settings.DATASHEET_UPLOAD and not attachment:\n        if datasheet:\n            part_info['datasheet'] = inventree_api.upload_part_datasheet(\n                datasheet_url=datasheet,\n                part_ipn=part_ipn,\n                part_pk=part_id,\n                silent=settings.SILENT,\n            )\n            if not part_info['datasheet']:\n                cprint('[TREE]\\tWarning: Failed to upload part datasheet', silent=settings.SILENT)\n            else:\n                cprint('[TREE]\\tSuccess: Uploaded part datasheet', silent=settings.SILENT)\n    # if an attachment is present, set it as the datasheet field\n    if attachment:\n        part_info['datasheet'] = f'{inventree_api.inventree_api.base_url.strip(\"/\")}{attachment[0][\"attachment\"]}'\n\n    # Create manufacturer part\n    if manufacturer_name and manufacturer_mpn:\n        inventree_create_manufacturer_part(part_id=part_pk,\n                                           manufacturer_name=manufacturer_name,\n                                           manufacturer_mpn=manufacturer_mpn,\n                                           datasheet=datasheet,\n                                           description=part_description)\n    else:\n        cprint('[INFO]\\tWarning: No manufacturer part to create', silent=settings.SILENT)\n\n    # Progress Update\n    if not progress.update_progress_bar(show_progress, increment=0.2):\n        return\n\n    supplier_name = part_info.get('supplier_name', '')\n    supplier_sku = part_info.get('supplier_part_number', '')\n    supplier_link = part_info.get('supplier_link', '')\n\n    # Add supplier alternate\n    if supplier_name and supplier_sku:\n        cprint('\\n[MAIN]\\tCreating supplier part', silent=settings.SILENT)\n        is_new_supplier_part, supplier_part = inventree_api.is_new_supplier_part(\n            supplier_name=supplier_name,\n            supplier_sku=supplier_sku)\n\n        if not is_new_supplier_part:\n            cprint('[INFO]\\tSupplier part already exists, skipping.', silent=settings.SILENT)\n        else:\n            # Create a new supplier part\n            is_supplier_part_created, supplier_part = inventree_api.create_supplier_part(\n                part_id=part_pk,\n                manufacturer_name=manufacturer_name,\n                manufacturer_mpn=manufacturer_mpn,\n                supplier_name=supplier_name,\n                supplier_sku=supplier_sku,\n                description=part_description,\n                link=supplier_link)\n\n            if is_supplier_part_created:\n                cprint('[INFO]\\tSuccess: Added new supplier part', silent=settings.SILENT)\n                result = True\n\n        if supplier_part and settings.PRICING_UPLOAD:\n            cprint('\\n[MAIN]\\tProcessing Price Breaks', silent=settings.SILENT)\n            inventree_api.update_price_breaks(\n                supplier_part=supplier_part,\n                price_breaks=inventree_part['pricing'],\n                currency=inventree_part['currency'])\n            result = True\n    \n    else:\n        cprint('[INFO]\\tWarning: No supplier part to create', silent=settings.SILENT)\n\n    return result\n"
  },
  {
    "path": "kintree/gui/gui.py",
    "content": "import os\nimport flet as ft\n\nfrom ..config import settings\n\nfrom .views.common import update_theme, handle_transition\nfrom .views.main import (\n    PartSearchView,\n    InventreeView,\n    KicadView,\n    CreateView,\n)\nfrom .views.settings import (\n    UserSettingsView,\n    SupplierSettingsView,\n    InvenTreeSettingsView,\n    KiCadSettingsView,\n)\n\n\ndef init_gui(page: ft.Page):\n    '''Initialize page'''\n    # Alignments\n    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER\n    page.vertical_alignment = ft.MainAxisAlignment.CENTER\n    page.scroll = ft.ScrollMode.ALWAYS\n\n    # Window Icon\n    page.window.icon = os.path.join(settings.PROJECT_DIR, 'gui', 'logo.ico')\n    page.window.title_bar_hidden = True\n    \n    # Theme\n    update_theme(page)\n\n    # Creating a progress bar that will be used\n    # to show the user that the app is busy doing something\n    page.splash = ft.ProgressBar(visible=False)\n\n    # Update\n    page.update()\n\n\ndef kintree_gui(page: ft.Page):\n    '''Ki-nTree GUI'''\n    # Init\n    init_gui(page)\n    # Create main views\n    part_view = PartSearchView(page)\n    inventree_view = InventreeView(page)\n    kicad_view = KicadView(page)\n    create_view = CreateView(page)\n    # Create settings views\n    user_settings_view = UserSettingsView(page)\n    supplier_settings_view = SupplierSettingsView(page)\n    inventree_settings_view = InvenTreeSettingsView(page)\n    kicad_settings_view = KiCadSettingsView(page)\n\n    # Routing\n    def route_change(route):\n        # print(f'\\n--> Routing to {route.route}')\n        if '/main' in page.route or page.route == '/':\n            page.views.clear()\n            if 'part' in page.route or page.route == '/':\n                page.views.append(part_view)\n            if 'inventree' in page.route:\n                page.views.append(inventree_view)\n            elif 'kicad' in page.route:\n                page.views.append(kicad_view)\n            elif 'create' in page.route:\n                page.views.append(create_view)\n        elif '/settings' in page.route:\n            if '/settings' in page.views[-1].route:\n                page.views.pop()\n            if 'user' in page.route:\n                page.views.append(user_settings_view)\n            elif 'supplier' in page.route:\n                page.views.append(supplier_settings_view)\n            elif 'inventree' in page.route:\n                page.views.append(inventree_settings_view)\n            elif 'kicad' in page.route:\n                page.views.append(kicad_settings_view)\n            else:\n                page.views.append(user_settings_view)\n        page.update()\n\n    def view_pop(view):\n        '''Pop setting view'''\n        page.views.pop()\n        top_view = page.views[-1]\n        if 'main' in top_view.route:\n            handle_transition(page, transition=True)\n        # Route and render\n        page.go(top_view.route)\n        if 'main' in top_view.route:\n            handle_transition(\n                page,\n                transition=False,\n                update_page=True,\n                timeout=0.3,\n            )\n        if '/main/part' in top_view.route or '/main/inventree' in top_view.route:\n            top_view.partial_update()\n\n    page.on_route_change = route_change\n    page.on_view_pop = view_pop\n\n    page.go(page.route)\n"
  },
  {
    "path": "kintree/gui/views/common.py",
    "content": "from enum import Enum\nfrom typing import Optional, List\n\nimport flet as ft\n\nGUI_PARAMS = {\n    'nav_rail_min_width': 100,\n    'nav_rail_width': 400,\n    'nav_rail_alignment': -0.9,\n    'nav_rail_icon_size': 40,\n    'nav_rail_text_size': 16,\n    'nav_rail_padding': 10,\n    'textfield_width': 600,\n    'textfield_dense': True,\n    'textfield_space_after': 3,\n    'dropdown_width': 600,\n    'dropdown_dense': False,\n    'searchfield_width': 300,\n    'button_width': 110,\n    'button_height': 56,\n    'icon_size': 40,\n    'text_size': 16,\n}\n# Contains data from all views\ndata_from_views = {}\n\n\nclass DialogType(Enum):\n    VALID = 'valid'\n    WARNING = 'warning'\n    ERROR = 'error'\n\n\ndef handle_transition(page: ft.Page, transition: bool, update_page=False, timeout=0):\n    # print(f'{transition=} | {update_page=} | {timeout=}')\n    if transition:\n        transition = ft.PageTransitionTheme.CUPERTINO\n        page.theme.page_transitions.android = transition\n        page.theme.page_transitions.ios = transition\n        page.theme.page_transitions.linux = transition\n        page.theme.page_transitions.macos = transition\n        page.theme.page_transitions.windows = transition\n    else:\n        page.theme.page_transitions.android = ft.PageTransitionTheme.NONE\n        page.theme.page_transitions.ios = ft.PageTransitionTheme.NONE\n        page.theme.page_transitions.linux = ft.PageTransitionTheme.NONE\n        page.theme.page_transitions.macos = ft.PageTransitionTheme.NONE\n        page.theme.page_transitions.windows = ft.PageTransitionTheme.NONE\n\n    # Wait\n    if timeout:\n        import time\n        time.sleep(timeout)\n\n    # Update\n    if update_page:\n        page.update()\n\n\ndef update_theme(page: ft.Page, mode='light', transition=False, compact=True):\n    # Color theme\n    page.theme_mode = mode\n\n    # UI theme\n    theme = ft.Theme()\n    page.theme = theme\n\n    # Make it more compact\n    if compact:\n        page.theme.visual_density = ft.ThemeVisualDensity.COMPACT\n    else:\n        page.theme.visual_density = ft.ThemeVisualDensity.STANDARD\n\n    # Disable transitions by default\n    handle_transition(page, transition=False)\n\n\nclass CommonView(ft.View):\n    '''Common view to all GUI views'''\n\n    _page = None\n    navigation_rail = None\n    title = None\n    column = None\n    fields = None\n    data = None\n    dialog = None\n    \n    def __init__(self, page: ft.Page, appbar: ft.AppBar, navigation_rail: ft.NavigationRail):\n        # Store page pointer\n        self._page = page\n\n        # Init view\n        super().__init__(route=self.route, appbar=appbar)\n\n        # Set navigation rail\n        if not self.navigation_rail:\n            self.navigation_rail = navigation_rail\n\n    def build_column(self):\n        # Empty column (to be set inside the children views)\n        self.column = ft.Column()\n\n    def build(self):\n        # Build column\n        if not self.column:\n            self.build_column()\n        # Set view controls\n        self.controls = [\n            ft.Row(\n                controls=[\n                    self.navigation_rail,\n                    ft.VerticalDivider(width=1),\n                    self.column,\n                ],\n                expand=True,\n            ),\n        ]\n\n    def build_dialog(self):\n        return None\n    \n    def build_snackbar(self, d_type: DialogType, message: str):\n        if d_type == DialogType.VALID:\n            self.dialog = ft.SnackBar(\n                bgcolor=ft.colors.GREEN_100,\n                content=ft.Text(\n                    message,\n                    color=ft.colors.GREEN_700,\n                    size=GUI_PARAMS['nav_rail_text_size'],\n                    weight=ft.FontWeight.BOLD,\n                ),\n            )\n        elif d_type == DialogType.WARNING:\n            self.dialog = ft.SnackBar(\n                bgcolor=ft.colors.AMBER_100,\n                content=ft.Text(\n                    message,\n                    color=ft.colors.AMBER_800,\n                    size=GUI_PARAMS['nav_rail_text_size'],\n                    weight=ft.FontWeight.BOLD,\n                ),\n            )\n        elif d_type == DialogType.ERROR:\n            self.dialog = ft.SnackBar(\n                bgcolor=ft.colors.RED_100,\n                content=ft.Text(\n                    message,\n                    color=ft.colors.RED_700,\n                    size=GUI_PARAMS['nav_rail_text_size'],\n                    weight=ft.FontWeight.BOLD,\n                ),\n            )\n\n    def show_dialog(\n            self,\n            d_type: Optional[DialogType] = None,\n            message: Optional[str] = None,\n            snackbar=True,\n            open=True,\n    ):\n        if snackbar:\n            self.build_snackbar(d_type, message)\n        if isinstance(self.dialog, ft.SnackBar):\n            self._page.snack_bar = self.dialog\n            self._page.snack_bar.open = True\n        elif isinstance(self.dialog, ft.Banner):\n            self._page.banner = self.dialog\n            self._page.banner.open = open\n        elif isinstance(self.dialog, ft.AlertDialog):\n            if open:\n                self._page.open(self.dialog)\n            else:\n                self._page.close(self.dialog)\n        self._page.update()\n\n\nclass SwitchWithRefs(ft.Switch):\n    '''Link the visibility of other fields to a switch value'''\n\n    linked_refs = []\n    \n    def __init__(\n        self,\n        refs: List[ft.Ref] = None,\n        reverse_dir: bool = False,\n        **kwargs,\n    ):\n        super().__init__(**kwargs)\n        if refs:\n            self.refs = refs\n            self.enable_refs(self.value)\n        self.reverse_dir = reverse_dir\n\n    def enable_refs(self, enable):\n        if self.reverse_dir:\n            enable = not enable\n        for ref in self.linked_refs:\n            ref.current.visible = enable\n            try:\n                ref.current.update()\n            except AssertionError:\n                # Control not added to page yet\n                pass\n    \n    def process_change(self, e, handler, *args, **kwargs):\n        enable = False\n        if e.data == 'true':\n            enable = True\n        self.enable_refs(enable)\n        handler(e, *args, **kwargs)\n\n    @property\n    def refs(self):\n        return self.linked_refs\n    \n    @refs.setter\n    def refs(self, references: List[ft.Ref]):\n        if references:\n            self.linked_refs = []\n            for ref in references:\n                try:\n                    if ref.current is None:\n                        raise Exception(f'Reference \"{ref.current}\" needs to be added to the page first')\n                except AttributeError:\n                    raise Exception(f'\"{ref}\" is not a Flet Ref (type: {type(ref)})')\n                # if ft.Control not in ref.current.__class__.__mro__:\n                #     raise Exception(f'\"{ref.current}\" is not a Flet Control ({type(ref.current)})')\n                self.linked_refs.append(ref)\n            if self.linked_refs:\n                self.enable_refs(self.value)\n\n    @ft.Switch.on_change.setter\n    def on_change(self, handler, *args, **kwargs):\n        ft.Switch.on_change.fset(\n            self,\n            lambda e: self.process_change(e, handler, *args, **kwargs)\n        )\n\n\nclass DropdownWithSearch(ft.UserControl):\n    '''Implements a dropdown with search box'''\n\n    dropdown = None\n    search_button = None\n    search_field = None\n    search_box = None\n    search_width = None\n    \n    def build(self):\n        return ft.Row([\n            self.dropdown,\n            self.search_box,\n            self.search_button,\n        ])\n    \n    def __str__(self):\n        return f'dropdown_with_search {{dropdown: {self.dropdown}, search_field: {self.search_field}}}'\n\n    def __init__(\n        self,\n        label: Optional[str] = None,\n        dr_width: Optional[int] = None,\n        sr_width: Optional[int] = None,\n        dense: Optional[bool] = None,\n        disabled=False,\n        sr_animate=100,\n        options=None,\n        on_change=None,\n        **kwargs,\n    ):\n        super().__init__(**kwargs)\n        self._options = options\n        self.dropdown = ft.Dropdown(\n            label=label,\n            width=dr_width,\n            dense=dense,\n            options=options,\n            on_change=on_change,\n        )\n        self.search_button = ft.IconButton(\n            'search',\n            on_click=self.search_now\n        )\n        self.search_field = ft.TextField(\n            border=\"none\",\n            width=sr_width,\n            dense=dense,\n            on_change=self.on_search,\n        )\n        self.search_box = ft.Container(\n            content=self.search_field,\n            width=0,\n            animate=ft.Animation(sr_animate),\n        )\n        self.disabled = disabled\n        self.search_width = sr_width\n\n    @property\n    def label(self):\n        return self.dropdown.label\n    \n    @label.setter\n    def label(self, label):\n        self.dropdown.label = label\n        \n    @property\n    def value(self):\n        return self.dropdown.value\n    \n    @value.setter\n    def value(self, value):\n        self.dropdown.value = value\n        if value is None:\n            self.search_field.value = None\n            self.done_search()\n\n    @property\n    def disabled(self):\n        return self.dropdown.disabled\n    \n    @disabled.setter\n    def disabled(self, disabled):\n        try:\n            self.dropdown.disabled = disabled\n            self.dropdown.update()\n            self.search_button.disabled = disabled\n            self.search_button.update()\n            self.search_field.disabled = disabled\n            self.search_field.update()\n            self.search_box.disabled = disabled\n            self.done_search()\n        except (AttributeError, AssertionError):\n            pass\n    \n    @property\n    def options(self):\n        return self.dropdown.options\n    \n    @options.setter\n    def options(self, options):\n        self._options = options\n        self.dropdown.options = self._options\n\n    @property\n    def on_change(self):\n        return self.dropdown.on_change\n    \n    @on_change.setter\n    def on_change(self, on_change):\n        self.dropdown.on_change = on_change\n\n    def update_option_list(self, input: str):\n        new_list_options = []\n        for option in self._options:\n            if input.lower() in option.key.lower():\n                new_list_options.append(option)\n        return new_list_options\n\n    def on_search(self, e):\n        if self.search_field.value.replace(' ', ''):\n            self.dropdown.options = self.update_option_list(self.search_field.value)\n            if len(self.dropdown.options) == 1:\n                self.dropdown.value = self.dropdown.options[0].key\n                self.on_change(e, label=self.label, value=self.value)\n            else:\n                self.dropdown.value = None\n        else:\n            self.dropdown.options = self._options\n        self.dropdown.update()\n        self.on_change()\n\n    def search_now(self, e):\n        self.search_box.width = self.search_width\n        self.search_box.update()\n        self.search_button.icon = 'highlight_remove'\n        self.search_button.on_click = self.done_search\n        self.search_button.update()\n        self.search_field.border = \"outline\"\n        self.search_field.update()\n        self.search_field.focus()\n        if self.search_field.value:\n            self.on_search(e)\n    \n    def done_search(self, e=None):\n        self.search_box.width = 0\n        self.search_box.update()\n        self.search_button.icon = 'search'\n        self.search_button.on_click = self.search_now\n        self.search_button.update()\n        self.search_field.border = \"none\"\n        self.search_field.update()\n        self.options = self._options\n        self.dropdown.update()\n\n        \nclass MenuButton(ft.Container):\n    def __init__(\n        self,\n        title: str,\n        icon: Optional[ft.Control] = None,\n        selected: bool = False,\n        radio: Optional[ft.Radio] = None,\n    ):\n        super().__init__()\n        self.icon = icon\n        self.title = title\n        self._selected = selected\n        self.padding = ft.padding.only(left=43)\n        self.height = 38\n        self.border_radius = 4\n        self.ink = True\n        self.on_click = self.item_click\n        self.radio = radio\n\n    def item_click(self, _):\n        pass\n\n    def build(self):\n        row = ft.Row()\n        if self.icon is not None:\n            row.controls.append(self.icon)\n        if self.radio:\n            row.controls.append(self.radio)\n        else:\n            row.controls.append(ft.Text(self.title))\n        self.content = row\n\n    def _before_build_command(self):\n        self.bgcolor = \"surfacevariant\" if self._selected else None\n        super()._before_build_command()\n"
  },
  {
    "path": "kintree/gui/views/main.py",
    "content": "import os\nimport copy\nimport flet as ft\n\n# Version\nfrom ... import __version__\n# Common view\nfrom .common import GUI_PARAMS, data_from_views\nfrom .common import DialogType\nfrom .common import CommonView\nfrom .common import DropdownWithSearch, SwitchWithRefs\nfrom .common import handle_transition\n# Tools\nfrom ...common.tools import cprint, download_with_retry\n# Settings\nfrom ...common import progress\nfrom ...config import settings, config_interface\n# InvenTree\nfrom ...database import inventree_interface\n# KiCad\nfrom ...kicad import kicad_interface\n# SnapEDA\nfrom ...search import snapeda_api\n\n# Main AppBar\nmain_appbar = ft.AppBar(\n    leading=ft.WindowDragArea(\n        ft.Container(\n            content=ft.Image(\n                src=os.path.join(settings.PROJECT_DIR, 'gui', 'logo.ico'),\n                fit=ft.ImageFit.CONTAIN,\n            ),\n            padding=ft.padding.only(left=10),\n            expand=True,\n        ),\n        maximizable=True,\n    ),\n    leading_width=40,\n    title=ft.WindowDragArea(ft.Container(ft.Text(f'Ki-nTree | {__version__}'),\n                                         width=10000), maximizable=True),\n    center_title=False,\n    bgcolor=ft.colors.SURFACE_VARIANT,\n    actions=[],\n)\n\n# Navigation Controls\nMAIN_NAVIGATION = {\n    'Part Search': {\n        'nav_index': 0,\n        'route': '/main/part'\n    },\n    'InvenTree': {\n        'nav_index': 1,\n        'route': '/main/inventree'\n    },\n    'KiCad': {\n        'nav_index': 2,\n        'route': '/main/kicad'\n    },\n    'Create': {\n        'nav_index': 3,\n        'route': '/main/create'\n    },\n}\n\n# Load navigation indexes\nNAV_BAR_INDEX = {}\nfor view in MAIN_NAVIGATION.values():\n    NAV_BAR_INDEX[view['nav_index']] = view['route']\n\n# Main NavRail\nmain_navrail = ft.NavigationRail(\n    selected_index=0,\n    label_type=ft.NavigationRailLabelType.ALL,\n    min_width=100,\n    min_extended_width=400,\n    group_alignment=-0.9,\n    destinations=[\n        ft.NavigationRailDestination(\n            icon_content=ft.Icon(name=ft.icons.SCREEN_SEARCH_DESKTOP_OUTLINED, size=40),\n            selected_icon_content=ft.Icon(name=ft.icons.SCREEN_SEARCH_DESKTOP_SHARP, size=40),\n            label_content=ft.Text(\"Part Search\", size=16),\n            padding=10,\n        ),\n        ft.NavigationRailDestination(\n            icon_content=ft.Icon(name=ft.icons.INVENTORY_2_OUTLINED, size=40),\n            selected_icon_content=ft.Icon(name=ft.icons.INVENTORY_2, size=40),\n            label_content=ft.Text(\"InvenTree\", size=16),\n            padding=10,\n        ),\n        ft.NavigationRailDestination(\n            icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT_OUTLINED, size=40),\n            selected_icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT, size=40),\n            label_content=ft.Text(\"KiCad\", size=16),\n            padding=10,\n        ),\n        ft.NavigationRailDestination(\n            icon_content=ft.Icon(name=ft.icons.BUILD_OUTLINED, size=40),\n            selected_icon_content=ft.Icon(name=ft.icons.BUILD, size=40),\n            label_content=ft.Text(\"Create\", size=16),\n            padding=10,\n        ),\n    ],\n    on_change=None,\n)\n\n\nclass MainView(CommonView):\n    '''Main view'''\n\n    route = None\n    data = None\n\n    def __init__(self, page: ft.Page):\n        # Get route\n        self.route = MAIN_NAVIGATION[self.title].get('route', '/')\n\n        # Init view\n        super().__init__(page=page, appbar=main_appbar, navigation_rail=main_navrail)\n\n        # Update application bar\n        if not self.appbar.actions:\n            self.appbar.actions.extend(\n                [\n                    ft.IconButton(\n                        ft.icons.SETTINGS,\n                        on_click=self.call_settings,\n                    ),\n                    ft.IconButton(\n                        ft.icons.CLOSE,\n                        on_click=lambda _: page.window.close(),\n                    ),\n                ]\n            )\n        else:\n            self.appbar.actions[0].on_click = self.call_settings\n\n        # Update navigation rail\n        self.navigation_rail.on_change = self.nav_rail_redirect\n\n        # Init data\n        self.data = {}\n\n        # Process enable switch\n        if 'enable' in self.fields:\n            self.fields['enable'].on_change = self.process_enable\n\n        # Add floating button to reset view\n        self.floating_action_button = ft.FloatingActionButton(\n            icon=ft.icons.REPLAY, on_click=self.reset_view,\n        )\n\n    def nav_rail_redirect(self, e):\n        self._page.go(NAV_BAR_INDEX[e.control.selected_index])\n\n    def call_settings(self, e):\n        handle_transition(self._page, transition=True)\n        self._page.go('/settings')\n\n    def reset_view(self, e, ignore=['enable'], hidden={}):\n        def reset_field(field):\n            if isinstance(field, ft.ProgressBar):\n                field.value = 0\n            else:\n                field.value = None\n\n        for name, field in self.fields.items():\n            if isinstance(field, dict):\n                for key, value in field.items():\n                    value.disabled = True\n                    reset_field(value)\n            else:\n                if name not in ignore:\n                    reset_field(field)\n\n        if hidden:\n            for key, value in hidden.items():\n                if not value:\n                    self.data[key] = value\n                else:\n                    self.data[key] = None\n\n        # Clear data\n        self.push_data()\n        self._page.update()\n\n    def partial_update(self):\n        '''Process partial view updates'''\n        return\n\n    def process_enable(self, e, value=None, ignore=['enable']):\n        disabled = False\n        if e.data.lower() == 'false':\n            disabled = True\n\n        # Overwrite with value\n        if value is not None:\n            disabled = not value\n\n        key = e.control.label.lower()\n        settings.set_enable_flag(key, not disabled)\n\n        for name, field in self.fields.items():\n            if name not in ignore:\n                field.disabled = disabled\n                field.update()\n        self.push_data(e)\n\n    def sanitize_data(self):\n        return\n\n    def push_data(self, e=None, hidden={}):\n        for key, field in self.fields.items():\n            try:\n                self.data[key] = field.value\n            except AttributeError:\n                pass\n\n        if hidden:\n            for key, value in hidden.items():\n                self.data[key] = value\n\n        # Sanitize data before pushing\n        self.sanitize_data()\n        # Push\n        data_from_views[self.title] = self.data\n\n    def did_mount(self, enable=False):\n        handle_transition(self._page, transition=False, update_page=True)\n        if self.fields.get('enable', None) is not None:\n            # Create enable event\n            e = ft.ControlEvent(\n                target=None,\n                name='did_mount_enable',\n                data='true' if enable else 'false',\n                page=self._page,\n                control=self.fields['enable'],\n            )\n            # Process enable\n            self.process_enable(e)\n        return super().did_mount()\n\n\nclass PartSearchView(MainView):\n    '''Part search view'''\n\n    title = 'Part Search'\n\n    # List of search fields\n    search_fields_list = [\n        'name',\n        'description',\n        'revision',\n        'keywords',\n        'supplier_name',\n        'supplier_part_number',\n        'supplier_link',\n        'manufacturer_name',\n        'manufacturer_part_number',\n        'datasheet',\n        'image',\n    ]\n\n    fields = {\n        'part_number': ft.TextField(\n            label=\"Part Number\",\n            dense=True,\n            hint_text=\"Part Number\",\n            width=250,\n            expand=True,\n        ),\n        'supplier': ft.Dropdown(\n            label=\"Supplier\",\n            dense=True,\n            width=250\n        ),\n        'search_button': ft.IconButton(\n            icon=ft.icons.SEND,\n            icon_color=\"blue900\",\n            icon_size=32,\n            height=48,\n            width=48,\n            tooltip=\"Submit\",\n        ),\n        'parameter_view': ft.Switch(\n            label='View Parameters',\n            disabled=True\n        ),\n        'search_form': {},\n        'parameter_form': {},\n    }\n\n    def reset_view(self, e, ignore=['enable']):\n        hidden_fields = {\n            'searched_part_number': '',\n            'custom_part': None,\n        }\n        self.fields['parameter_form'] = {}\n        try:\n            self.fields['part_number'].focus()\n        except AssertionError:\n            pass\n        return super().reset_view(e, ignore=ignore, hidden=hidden_fields)\n\n    def enable_search_fields(self):\n        for form_field in self.fields['search_form'].values():\n            form_field.disabled = False\n        self.fields['parameter_view'].disabled = False\n        self._page.update()\n        return\n\n    def run_search(self, e):\n        # Reset view\n        self.reset_view(e, ignore=['part_number', 'supplier'])\n        self.switch_view()\n        # Validate form\n        if bool(self.fields['part_number'].value) != bool(self.fields['supplier'].value) or not self.fields['part_number'].value and not self.fields['supplier'].value:\n            if not self.fields['part_number'].value:\n                error_msg = 'Missing Part Number'\n            else:\n                error_msg = 'Missing Supplier'\n            self.show_dialog(\n                d_type=DialogType.ERROR,\n                message=error_msg,\n            )\n        else:\n            self.fields['part_number'].value = self.fields['part_number'].value.strip()\n            self._page.splash.visible = True\n            self._page.update()\n\n            if not self.fields['part_number'].value and not self.fields['supplier'].value:\n                self.data['custom_part'] = True\n                self.enable_search_fields()\n            else:\n                self.data['custom_part'] = False\n\n                # Get supplier\n                supplier = inventree_interface.get_supplier_name(self.fields['supplier'].value)\n                # Supplier search\n                part_supplier_info = inventree_interface.supplier_search(\n                    supplier,\n                    self.fields['part_number'].value\n                )\n\n                part_supplier_form = None\n\n                if part_supplier_info:\n                    # Translate to user form format\n                    part_supplier_form = inventree_interface.translate_supplier_to_form(\n                        supplier=supplier,\n                        part_info=part_supplier_info,\n                    )\n                    if part_supplier_form:\n                        for field_idx, field_name in enumerate(self.fields['search_form'].keys()):\n                            # print(field_idx, field_name, get_default_search_keys()[field_idx], search_form_field[field_name])\n                            try:\n                                self.fields['search_form'][field_name].value = part_supplier_form.get(field_name, '')\n                            except IndexError:\n                                pass\n                            # Enable editing\n                            self.enable_search_fields()\n                    # Stitch parameters\n                    if part_supplier_info.get('parameters', None):\n                        self.data['parameters'] = part_supplier_info['parameters']\n                        for parameter, value in self.data['parameters'].items():\n                            text_field = ft.TextField(\n                                label=parameter,\n                                value=value,\n                                expand=True,\n                                on_change=self.push_data,\n                            )\n                            self.fields['parameter_form'][parameter] = text_field\n                    # and pricing\n                    if part_supplier_info.get('pricing', None):\n                        self.data['pricing'] = part_supplier_info['pricing']\n                        self.data['currency'] = part_supplier_info.get('currency', None)\n\n            # Add to data buffer\n            self.push_data()\n            self._page.splash.visible = False\n\n            if not self.data['supplier_part_number'] and not self.data['custom_part']:\n                self.show_dialog(\n                    d_type=DialogType.ERROR,\n                    message='Part not found',\n                )\n            elif not self.data['manufacturer_part_number']:\n                self.show_dialog(\n                    d_type=DialogType.ERROR,\n                    message='Found part has no manufacturer part number',\n                )\n            elif self.data['searched_part_number'].lower() != self.data['manufacturer_part_number'].lower():\n                self.show_dialog(\n                    d_type=DialogType.WARNING,\n                    message='Found manufacturer part number does not match the requested part number',\n                )\n            self._page.update()\n        return\n\n    def push_data(self, e=None):\n        hidden_fields = {\n            'searched_part_number': self.fields['part_number'].value,\n            'custom_part': self.data.get('custom_part', None),\n        }\n        for key, field in self.fields['search_form'].items():\n            self.data[key] = field.value\n        for key, field in self.fields['parameter_form'].items():\n            self.data['parameters'][key] = field.value\n        return super().push_data(e, hidden=hidden_fields)\n        \n    def partial_update(self):\n        # Update supplier options\n        self.update_suppliers()\n    \n    def update_suppliers(self):\n        # Reload suppliers\n        self.fields['supplier'].options = [\n            ft.dropdown.Option(supplier) for supplier in settings.SUPPORTED_SUPPLIERS_API\n        ]\n        if len(self.fields['supplier'].options) == 1:\n            self.fields['supplier'].value = self.fields['supplier'].options[0].key\n        else:\n            self.fields['supplier'].value = None\n        try:\n            self.fields['supplier'].update()\n        except AssertionError:\n            # Control not added to page yet\n            pass\n\n    def switch_view(self, e=None):\n        # show parameters instead of part information\n        parameters_view = self.fields['parameter_view'].value\n        self.column.controls[0].content.controls = [\n            ft.Row(),\n            ft.Row(\n                controls=[\n                    self.fields['part_number'],\n                    self.fields['supplier'],\n                    self.fields['search_button'],\n                    self.fields['parameter_view'],\n                ],\n            ),\n            ft.Divider(),\n        ]\n        if not parameters_view:\n            for field, text_field in self.fields['search_form'].items():\n                self.column.controls[0].content.controls.append(ft.Row([text_field]))\n        else:\n            for field, text_field in self.fields['parameter_form'].items():\n                self.column.controls[0].content.controls.append(ft.Row([text_field]))\n        self._page.update()\n\n    def perform_pn_search(self, e):\n        self.run_search(e)\n        try:\n            self.fields['part_number'].focus()\n        except AssertionError:\n            pass\n\n    def build_column(self):\n        self.update_suppliers()\n        # Enable search method\n        self.fields['search_button'].on_click = self.run_search\n        self.fields['parameter_view'].on_change = self.switch_view\n        self.fields['part_number'].on_submit = self.perform_pn_search\n\n        self.column = ft.Column(\n            controls=[\n                ft.Container(\n                    content=ft.Column(\n                        controls=[\n                            ft.Row(),\n                            ft.Row(\n                                controls=[\n                                    self.fields['part_number'],\n                                    self.fields['supplier'],\n                                    self.fields['search_button'],\n                                    self.fields['parameter_view'],\n                                ],\n                            ),\n                            ft.Divider(),\n                        ],\n                        scroll=ft.ScrollMode.HIDDEN,\n                    ),\n                    expand=True,\n                ),\n            ],\n            alignment=ft.MainAxisAlignment.END,\n            expand=True,\n        )\n\n        # Create search form\n        for field in self.search_fields_list:\n            label = field.replace('_', ' ').title()\n            text_field = ft.TextField(\n                label=label,\n                dense=True,\n                hint_text=label,\n                disabled=True,\n                expand=True,\n                on_change=self.push_data,\n            )\n            self.column.controls[0].content.controls.append(ft.Row([text_field]))\n            self.fields['search_form'][field] = text_field\n\n    def did_mount(self, enable=False):\n        if (\n            not self.fields['part_number'].value\n            and self.fields['supplier'].value is None\n            and self.data.get('custom_part', None) is None\n        ):\n            self.show_dialog(\n                d_type=DialogType.WARNING,\n                message='To create a Custom Part click on the Submit button',\n            )\n        return super().did_mount(enable)\n\n\nclass InventreeView(MainView):\n    '''InvenTree categories view'''\n\n    title = 'InvenTree'\n    fields = {\n        'enable': ft.Switch(\n            label='InvenTree',\n            value=settings.ENABLE_INVENTREE,\n        ),\n        'alternate': ft.Switch(\n            label='Update existing',\n            value=settings.ENABLE_ALTERNATE if settings.ENABLE_INVENTREE else False,\n            disabled=not settings.ENABLE_INVENTREE,\n        ),\n        'load_categories': ft.ElevatedButton(\n            'Reload InvenTree Categories',\n            width=GUI_PARAMS['button_width'] * 2.6,\n            height=36,\n            icon=ft.icons.REPLAY,\n            disabled=False,\n        ),\n        'load_stock_locations': ft.ElevatedButton(\n            'Reload InvenTree Stock locations',\n            width=GUI_PARAMS['button_width'] * 2.8,\n            height=36,\n            icon=ft.icons.REPLAY,\n            disabled=False,\n        ),\n        'Category': DropdownWithSearch(\n            label='Category',\n            dr_width=GUI_PARAMS['textfield_width'],\n            sr_width=GUI_PARAMS['searchfield_width'],\n            dense=GUI_PARAMS['textfield_dense'],\n            disabled=settings.ENABLE_ALTERNATE,\n            options=[],\n        ),\n        'IPN: Category Code': ft.Dropdown(\n            label='IPN: Category Code',\n            width=GUI_PARAMS['textfield_width'] / 2 - 5,\n            dense=GUI_PARAMS['textfield_dense'],\n            # disabled=settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False),\n            options=[],\n        ),\n        'Create New Code': SwitchWithRefs(\n            label='Create New Code',\n        ),\n        'check_existing': ft.Switch(\n            label='Check for existing Parts',\n            value=settings.CHECK_EXISTING if settings.ENABLE_INVENTREE else False,\n            disabled=not settings.ENABLE_INVENTREE,\n        ),\n        'New Category Code': ft.TextField(\n            label='New Category Code',\n            width=GUI_PARAMS['textfield_width'] / 2 - 5,\n            dense=GUI_PARAMS['textfield_dense'],\n            visible=False,\n        ),\n        'Existing Part ID': ft.TextField(\n            label='Existing Part ID',\n            width=GUI_PARAMS['textfield_width'] / 2 - 5,\n            dense=GUI_PARAMS['textfield_dense'],\n            visible=True,\n        ),\n        'Existing Part IPN': ft.TextField(\n            label='Existing Part IPN',\n            width=GUI_PARAMS['textfield_width'] / 2 - 5,\n            dense=GUI_PARAMS['textfield_dense'],\n            visible=True,\n        ),\n        'Update Parameter': SwitchWithRefs(\n            label='Update Parameter',\n            value=settings.UPDATE_INVENTREE if settings.ENABLE_INVENTREE else False,\n            disabled=not settings.ENABLE_INVENTREE,\n        ),\n        'Create stock': SwitchWithRefs(\n            label='Create Stock',\n            disabled=not settings.ENABLE_INVENTREE,\n        ),\n        'Stock location': DropdownWithSearch(\n            label='Stock Location',\n            disabled=not settings.ENABLE_INVENTREE,\n            dr_width=GUI_PARAMS['textfield_width'],\n            sr_width=GUI_PARAMS['searchfield_width'],\n            dense=GUI_PARAMS['textfield_dense'],\n            options=[],\n        ),\n        'Stock quantity': ft.TextField(\n            label='Stock Quantity',\n            disabled=not settings.ENABLE_INVENTREE,\n            keyboard_type=ft.KeyboardType.NUMBER,\n            value='1',\n        ),\n        'Make stock location default': ft.Checkbox(\n            label=\"Set this location as the part\\'s default location\",\n            disabled=not settings.ENABLE_INVENTREE,\n            value=False,\n        ),\n    }\n\n    def __init__(self, page: ft.Page):\n        self.category_row_ref = ft.Ref[ft.Row]()\n        self.ipncode_row_ref = ft.Ref[ft.Row]()\n        self.alternate_row_ref = ft.Ref[ft.Row]()\n        self.create_stock_widgets_ref = ft.Ref[ft.Row]()\n        super().__init__(page)\n\n    def partial_update(self):\n        # Update IPN row\n        self.process_ipncode()\n    \n    def sanitize_data(self):\n        category_tree = self.data.get('Category', None)\n        if category_tree:\n            self.data['Category'] = inventree_interface.split_category_tree(category_tree)\n        stock_location_tree = self.data.get('Stock location', None)\n        if stock_location_tree:\n            self.data['Stock location'] = inventree_interface.split_category_tree(stock_location_tree)\n\n    def process_enable(self, e):\n        inventree_enable = True\n        # Switch control: override\n        if e.data.lower() == 'false':\n            inventree_enable = False\n        \n        super().process_enable(e, value=inventree_enable, ignore=['enable', 'IPN: Category Code'])\n        if not inventree_enable:\n            # If InvenTree disabled\n            self.fields['alternate'].value = inventree_enable\n            self.fields['alternate'].update()\n            self.process_alternate(e, value=inventree_enable)\n            self.process_create_stock(e, value=inventree_enable)\n        else:\n            alternate_enable = self.fields['alternate'].value\n            self.process_alternate(e, value=alternate_enable)\n            stock_create_enabled = self.fields['Create stock'].value\n            self.process_create_stock(e, value=stock_create_enabled)\n\n        self.process_ipncode()\n\n    def process_alternate(self, e, value=None):\n        if value is not None:\n            alt_visible = value\n        else:\n            # Switch control\n            # Reset view\n            self.reset_view(e, ignore=['enable', 'alternate'])\n            self.fields['New Category Code'].visible = False\n            # Get switch value\n            alt_visible = False\n            if e.data.lower() == 'true':\n                alt_visible = True\n\n        # Load category button\n        self.fields['load_categories'].disabled = alt_visible\n        self.fields['load_categories'].update()\n\n        # Category row visibility\n        self.category_row_ref.current.visible = not alt_visible\n        self.category_row_ref.current.update()\n\n        # Alternate row visibility\n        self.alternate_row_ref.current.visible = alt_visible\n        self.alternate_row_ref.current.update()\n\n        # Update settings\n        settings.set_enable_flag('alternate', alt_visible)\n        settings.set_enable_flag('update', alt_visible)\n        # User dialog\n        if alt_visible:\n            self.show_dialog(\n                d_type=DialogType.WARNING,\n                message='Alternate Mode Enabled: Enter Existing Part ID or Part IPN',\n            )\n\n        self.push_data(e)\n\n    def process_update(self, e, value=None):\n        if value is not None:\n            update_enabled = value\n        else:\n            # Get switch value\n            update_enabled = False\n            if e.data.lower() == 'true':\n                update_enabled = True\n        settings.set_enable_flag('update', update_enabled)\n        self.push_data(e)\n\n    def process_button(self, e, value=None):\n        if value is not None:\n            button_enabled = value\n        else:\n            # Get switch value\n            button_enabled = False\n            if e.data.lower() == 'true':\n                button_enabled = True\n        if e.control.label == 'Update existing':\n            settings.set_enable_flag('update', button_enabled)\n        elif e.control.label == 'Check for existing Parts':\n            settings.set_enable_flag('check_existing', button_enabled)\n        self.push_data(e)\n\n    def process_category(self, e=None, label=None, value=None):\n        parent_category = None\n        if isinstance(self.fields['Category'].value, str):\n            parent_category = inventree_interface.split_category_tree(self.fields['Category'].value)[0]\n        # Check for category codes\n        options = self.get_code_options()\n        if options:\n            self.fields['IPN: Category Code'].options = options\n            # Select category code corresponding to selected category\n            code = config_interface.load_file(settings.CONFIG_CATEGORIES)['CODES'].get(parent_category, None)\n            if code and not self.fields['Create New Code'].value:\n                self.fields['IPN: Category Code'].value = code\n            self.fields['IPN: Category Code'].update()\n        self.push_data(e)\n\n    def process_location(self, e=None, label=None, value=None):\n        self.fields['Stock location'].options = self.get_stock_location_options()\n        self.push_data(e)\n\n    def process_ipncode(self):\n        ipncode_enable = bool(\n            settings.CONFIG_IPN.get('IPN_ENABLE_CREATE', False) and settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False)\n        )\n        self.ipncode_row_ref.current.visible = ipncode_enable\n        self.ipncode_row_ref.current.update()\n\n    def process_create_stock(self, e, value=None):\n        if value is not None:\n            create_stock_visible = value\n        else:\n            self.fields['New Category Code'].visible = False\n            # Get switch value\n            create_stock_visible = False\n            if e.data.lower() == 'true':\n                create_stock_visible = True\n\n        # Stock create row visibility\n        self.create_stock_widgets_ref.current.visible = create_stock_visible\n        self.create_stock_widgets_ref.current.update()\n\n    def get_code_options(self):\n        try:\n            return [\n                ft.dropdown.Option(code)\n                for code in config_interface.load_file(settings.CONFIG_CATEGORIES)['CODES'].values()\n            ]\n        except AttributeError:\n            return []\n\n    def get_category_options(self, reload=False):\n        return [\n            ft.dropdown.Option(category)\n            for category in inventree_interface.build_category_tree(reload=reload)\n        ]\n\n    def get_stock_location_options(self, reload=False):\n        return [\n            ft.dropdown.Option(location)\n            for location in inventree_interface.build_stock_location_tree(reload=reload)\n        ]\n        \n    def reload_categories(self, e):\n        self._page.splash.visible = True\n        self._page.update()\n\n        # Check connection\n        if not inventree_interface.connect_to_server():\n            self.show_dialog(DialogType.ERROR, 'ERROR: Failed to connect to InvenTree server')\n        else:\n            self.fields['Category'].options = self.get_category_options(reload=True)\n            self.fields['Category'].update()\n\n        self._page.splash.visible = False\n        self._page.update()\n\n    def reload_stock_locations(self, e):\n        self._page.splash.visible = True\n        self._page.update()\n\n        # Check connection\n        if not inventree_interface.connect_to_server():\n            self.show_dialog(DialogType.ERROR, 'ERROR: Failed to connect to InvenTree server')\n        else:\n            self.fields['Stock location'].options = self.get_stock_location_options(reload=True)\n            self.fields['Stock location'].update()\n\n        self._page.splash.visible = False\n        self._page.update()\n\n    def create_ipn_code(self, e):\n        # Get switch value\n        new_code = True\n        if e.data.lower() == 'false':\n            new_code = False\n\n        self.fields['IPN: Category Code'].disabled = new_code\n        self.fields['IPN: Category Code'].update()\n        if not new_code:\n            self.process_category()\n        else:\n            self.push_data(e)\n\n    def build_column(self):\n        # Update dropdown with category options\n        self.fields['Category'].options = self.get_category_options()\n        self.fields['Category'].on_change = self.process_category\n        self.fields['load_categories'].on_click = self.reload_categories\n        # Category codes\n        self.fields['IPN: Category Code'].options = self.get_code_options()\n        self.fields['IPN: Category Code'].on_change = self.push_data\n        self.fields['Create New Code'].on_change = self.create_ipn_code\n        self.fields['New Category Code'].on_change = self.push_data\n        # Other Settings\n        self.fields['check_existing'].on_change = self.process_button\n        # Alternate fields\n        self.fields['alternate'].on_change = self.process_alternate\n        self.fields['Existing Part ID'].on_change = self.push_data\n        self.fields['Existing Part IPN'].on_change = self.push_data\n        self.fields['Update Parameter'].on_change = self.process_update\n        # Create stock location\n        self.fields['Stock location'].options = self.get_stock_location_options()\n        self.fields['Stock location'].on_change = self.process_location\n        self.fields[\"Create stock\"].on_change = self.process_create_stock\n        self.fields['Stock location'].on_change = self.push_data\n        self.fields['Stock quantity'].on_change = self.push_data\n        self.fields['Make stock location default'].on_change = self.push_data\n        self.fields['load_stock_locations'].on_click = self.reload_stock_locations\n\n        self.column = ft.Column(\n            controls=[\n                ft.Row(),\n                ft.Row(\n                    [\n                        self.fields['enable'],\n                        self.fields['alternate'],\n                        self.fields['load_categories'],\n                    ],\n                    width=GUI_PARAMS['dropdown_width'],\n                    alignment=ft.MainAxisAlignment.SPACE_BETWEEN,\n                ),\n                ft.Row(\n                    ref=self.category_row_ref,\n                    controls=[\n                        ft.Column(\n                            [\n                                ft.Row([self.fields['Category'],]),\n                                ft.Row(\n                                    ref=self.ipncode_row_ref,\n                                    controls=[\n                                        ft.Column(\n                                            [\n                                                ft.Row(\n                                                    [\n                                                        self.fields['IPN: Category Code'],\n                                                        self.fields['Create New Code'],\n                                                    ]\n                                                ),\n                                                ft.Row([self.fields['New Category Code']]),\n                                            ],\n                                        ),\n                                    ],\n                                ),\n                                ft.Row(\n                                    [\n                                        self.fields['check_existing'],\n                                    ],\n                                ),\n                            ],\n                        ),\n                    ],\n                ),\n                ft.Column(\n                    ref=self.alternate_row_ref,\n                    controls=[\n                        ft.Row(\n                            controls=[\n                                self.fields['Existing Part ID'],\n                                self.fields['Existing Part IPN'],\n                            ],\n                        ),\n                        ft.Row(\n                            controls=[self.fields['Update Parameter']]\n                        )\n                    ]\n                ),\n                ft.Column(\n                    ref=self.create_stock_widgets_ref,\n                    controls=[\n                        ft.Row(\n                            controls=[\n                                self.fields['Create stock'],\n                                self.fields['load_stock_locations']\n                            ]\n                        )\n                    ]\n                ),\n                ft.Column(\n                    ref=self.create_stock_widgets_ref,\n                    controls=[\n                        ft.Row(\n                            controls=[self.fields['Stock location']],\n                        ),\n                        ft.Row(\n                            controls=[self.fields['Stock quantity']],\n                        ),\n                        ft.Row(\n                            controls=[self.fields['Make stock location default']],\n                        ),\n                    ]\n                )\n            ],\n        )\n\n        # Connect New Category Code fields\n        cc_ref = ft.Ref[ft.TextField]()\n        cc_ref.current = self.fields['New Category Code']\n        self.fields['Create New Code'].refs = [cc_ref]\n    \n    def did_mount(self):\n        return super().did_mount(enable=settings.ENABLE_INVENTREE)\n\n\nclass KicadView(MainView):\n    '''KiCad view'''\n\n    title = 'KiCad'\n    fields = {\n        'enable': ft.Switch(\n            label='KiCad',\n            value=settings.ENABLE_KICAD,\n        ),\n        'Symbol Library': DropdownWithSearch(\n            label='',\n            dr_width=GUI_PARAMS['textfield_width'],\n            sr_width=GUI_PARAMS['searchfield_width'],\n            dense=GUI_PARAMS['textfield_dense'],\n            options=[],\n        ),\n        'Symbol Template': DropdownWithSearch(\n            label='',\n            dr_width=GUI_PARAMS['textfield_width'],\n            sr_width=GUI_PARAMS['searchfield_width'],\n            dense=GUI_PARAMS['textfield_dense'],\n            options=[],\n        ),\n        'Footprint Library': DropdownWithSearch(\n            label='',\n            dr_width=GUI_PARAMS['textfield_width'],\n            sr_width=GUI_PARAMS['searchfield_width'],\n            dense=GUI_PARAMS['textfield_dense'],\n            options=[],\n        ),\n        'Footprint': DropdownWithSearch(\n            label='',\n            dr_width=GUI_PARAMS['textfield_width'],\n            sr_width=GUI_PARAMS['searchfield_width'],\n            dense=GUI_PARAMS['textfield_dense'],\n            options=[],\n        ),\n        'New Footprint': SwitchWithRefs(\n            label='New Footprint',\n        ),\n        'New Footprint Name': ft.TextField(\n            label='New Footprint Name',\n            width=GUI_PARAMS['textfield_width'],\n            dense=GUI_PARAMS['textfield_dense'],\n            visible=False,\n        ),\n        'Check SnapEDA': ft.ElevatedButton(\n            content=ft.Row(\n                [\n                    ft.Icon('search'),\n                    ft.Text('Check SnapEDA', size=16),\n                ]\n            ),\n            height=GUI_PARAMS['button_height'],\n            width=GUI_PARAMS['button_width'] * 2,\n        ),\n    }\n\n    def build_alert_dialog(self, symbol: str, footprint: str, download: str, single_result=False):\n        modal_content = ft.Row()\n        modal_msg = ft.Text('Symbol and footprint are not available on SnapEDA')\n        # Build content\n        if symbol:\n            modal_content.controls.append(ft.Image(symbol))\n            modal_msg = ft.Text('Symbol is available on SnapEDA')\n        if footprint:\n            modal_content.controls.append(ft.Image(footprint))\n            if symbol:\n                modal_msg = ft.Text('Symbol and footprint are available on SnapEDA')\n            else:\n                modal_msg = ft.Text('Footprint is available on SnapEDA')\n        # Build actions\n        modal_actions = []\n        if download:\n            if not symbol and not footprint:\n                if single_result:\n                    modal_actions.append(ft.TextButton('Check Part', on_click=lambda _: self._page.launch_url(download)))\n                else:\n                    modal_msg = ft.Text('Multiple matches found on SnapEDA')\n                    modal_actions.append(ft.TextButton('See Results', on_click=lambda _: self._page.launch_url(download)))\n            else:\n                modal_actions.append(ft.TextButton('Download', on_click=lambda _: self._page.launch_url(download)))\n        modal_actions.append(ft.TextButton('Close', on_click=lambda _: self.show_dialog(open=False)))\n        \n        return ft.AlertDialog(\n            modal=True,\n            title=modal_msg,\n            content=modal_content,\n            actions=modal_actions,\n            actions_alignment=ft.MainAxisAlignment.END,\n            # on_dismiss=None,\n        )\n    \n    def process_enable(self, e, value=None, ignore=['enable']):\n        super().process_enable(e, value, ignore)\n        if self.fields['enable'].value:\n            self.fields['Footprint'].disabled = self.fields['New Footprint'].value\n            self.fields['Footprint'].update()\n        \n    def push_data(self, e=None, label=None, value=None):\n        super().push_data(e)\n        if label or e:\n            try:\n                if 'Footprint Library' in [label, e.control.label]:\n                    if value:\n                        selected_footprint_library = value\n                    else:\n                        selected_footprint_library = e.data\n                    self.update_footprint_options(selected_footprint_library)\n            except AttributeError:\n                # Handles condition where search field tries to reset dropdown\n                pass\n\n    def check_snapeda(self, e):\n        if not data_from_views.get('Part Search', {}).get('manufacturer_part_number', ''):\n            self.show_dialog(\n                d_type=DialogType.ERROR,\n                message='Missing Manufacturer Part Number',\n            )\n            return\n        \n        self._page.splash.visible = True\n        self._page.update()\n\n        response = snapeda_api.fetch_snapeda_part_info(data_from_views['Part Search']['manufacturer_part_number'])\n        data = snapeda_api.parse_snapeda_response(response)\n\n        images = {}\n        if data['has_symbol'] or data['has_footprint']:\n            images = snapeda_api.download_snapeda_images(data)\n\n        self._page.splash.visible = False\n        self._page.update()\n        \n        self.dialog = self.build_alert_dialog(\n            images.get('symbol', ''),\n            images.get('footprint', ''),\n            data.get('part_url', ''),\n            data.get('has_single_result', False),\n        )\n        self.show_dialog(snackbar=False, open=True)\n\n    def update_footprint_options(self, library: str):\n        footprint_options = []\n        if library is None:\n            return footprint_options\n        \n        # Load paths\n        footprint_paths = self.get_footprint_libraries()\n        # Get path matching selected footprint library\n        footprint_lib_path = footprint_paths[library]\n        # Load footprints\n        footprints = [\n            item.replace('.kicad_mod', '')\n            for item in sorted(os.listdir(footprint_lib_path))\n            if os.path.isfile(os.path.join(footprint_lib_path, item))\n        ]\n        # Find folder matching value\n        for footprint in footprints:\n            footprint_options.append(ft.dropdown.Option(footprint))\n\n        self.fields['Footprint'].options = footprint_options\n        self.fields['Footprint'].update()\n\n    def get_footprint_libraries(self) -> dict:\n        footprint_libraries = {}\n        try:\n            for folder in sorted(os.listdir(settings.KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'])):\n                if os.path.isdir(os.path.join(settings.KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'], folder)):\n                    footprint_libraries[folder.replace('.pretty', '')] = os.path.join(\n                        settings.KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'],\n                        folder\n                    )\n        except FileNotFoundError:\n            pass\n        return footprint_libraries\n\n    def find_libraries(self, type: str) -> list:\n        found_libraries = []\n        if type == 'symbol':\n            try:\n                found_libraries = [\n                    file.replace('.kicad_sym', '')\n                    for file in sorted(os.listdir(settings.KICAD_SETTINGS['KICAD_SYMBOLS_PATH']))\n                    if file.endswith('.kicad_sym')\n                ]\n            except FileNotFoundError:\n                pass\n        elif type == 'template':\n            templates = config_interface.load_templates_paths(\n                user_config_path=settings.KICAD_CONFIG_CATEGORY_MAP,\n                template_path=settings.KICAD_SETTINGS['KICAD_TEMPLATES_PATH']\n            )\n            for key in templates:\n                for template in templates[key]:\n                    found_libraries.append(f'{key}/{template}')\n        elif type == 'footprint':\n            found_libraries = list(self.get_footprint_libraries().keys())\n        return found_libraries\n\n    def build_library_options(self, type: str):\n        options = []\n        found_libraries = self.find_libraries(type)\n        if found_libraries:\n            options = [ft.dropdown.Option(lib_name) for lib_name in found_libraries]\n        return options\n    \n    def create_footprint(self, e):\n        # Get switch value\n        new_footprint = True\n        if e.data.lower() == 'false':\n            new_footprint = False\n\n        self.fields['Footprint'].disabled = new_footprint\n        self.fields['Footprint'].update()\n        if not new_footprint:\n            self.update_footprint_options(self.fields['Footprint Library'].value)\n        self.push_data(e)\n\n    def build_column(self):\n        # Library options checks\n        self.checks = []\n\n        self.column = ft.Column(\n            controls=[ft.Row()],\n            alignment=ft.MainAxisAlignment.START,\n            expand=True,\n        )\n        kicad_inputs = []\n        for name, field in self.fields.items():\n            # Update callbacks\n            if isinstance(field, ft.ElevatedButton):\n                field.on_click = self.check_snapeda\n            # Update options\n            elif isinstance(field, DropdownWithSearch):\n                field.label = name\n                if name == 'Symbol Library':\n                    field.options = self.build_library_options(type='symbol')\n                elif name == 'Symbol Template':\n                    field.options = self.build_library_options(type='template')\n                elif name == 'Footprint Library':\n                    field.options = self.build_library_options(type='footprint')\n                if not field.options and name != 'Footprint':\n                    self.checks.append(f'KiCad {name} path does not exists or folder is empty')\n\n            if name != 'enable':\n                field.on_change = self.push_data\n\n            kicad_inputs.append(field)\n        \n        self.column.controls.extend(kicad_inputs)\n\n        # Connect New Footprint fields\n        fp_ref = ft.Ref[ft.TextField]()\n        fp_ref.current = self.fields['New Footprint Name']\n        self.fields['New Footprint'].refs = [fp_ref]\n        self.fields['New Footprint'].on_change = self.create_footprint\n        \n    def did_mount(self):\n        if 'InvenTree' in data_from_views:\n            # Get value of alternate switch\n            if data_from_views['InvenTree'].get('alternate', False):\n                self.fields['enable'].disabled = True\n                self.fields['enable'].value = False\n                self.show_dialog(\n                    d_type=DialogType.ERROR,\n                    message='InvenTree Alternate switch is enabled',\n                )\n                return super().did_mount(enable=False)\n            else:\n                self.fields['enable'].disabled = False\n\n        # Process checks\n        if self.checks:\n            error_msg = f'{self.checks[0]}'\n            for check in self.checks[1:]:\n                error_msg += f'\\n{check}'\n            self.show_dialog(\n                d_type=DialogType.ERROR,\n                message=error_msg,\n            )\n\n        return super().did_mount(enable=settings.ENABLE_KICAD)\n\n\nclass CreateView(MainView):\n    '''Create view'''\n\n    title = 'Create'\n    fields = {\n        'inventree_progress': ft.ProgressBar(height=32, width=420, value=0),\n        'kicad_progress': ft.ProgressBar(height=32, width=420, value=0),\n        'create': ft.ElevatedButton(\n            content=ft.Row(\n                [\n                    ft.Icon('build_circle'),\n                    ft.Text('Create Part', size=20),\n                    ft.Icon('build_circle'),\n                ]\n            ),\n            height=GUI_PARAMS['button_height'],\n            width=GUI_PARAMS['button_width'] * 2,\n        ),\n        'cancel': ft.ElevatedButton(\n            content=ft.Row(\n                [\n                    ft.Icon('highlight_remove'),\n                    ft.Text('Cancel', size=20),\n                    ft.Icon('highlight_remove'),\n                ]\n            ),\n            height=GUI_PARAMS['button_height'],\n            width=GUI_PARAMS['button_width'] * 1.6,\n            bgcolor=ft.colors.RED_50,\n            disabled=True,\n        ),\n    }\n    inventree_progress_row = None\n    kicad_progress_row = None\n    create_continue = True\n\n    def show_dialog(self, type: DialogType, message: str):\n        if 'create' in self.fields:\n            self.enable_create(True)\n        return super().show_dialog(type, message)\n\n    def enable_create(self, enable=True):\n        self.fields['create'].disabled = not enable\n        self.fields['create'].update()\n        # Invert cancel button\n        self.enable_cancel(enable=not enable)\n\n    def enable_cancel(self, enable=True):\n        if enable:\n            for item in self.fields['cancel'].content.controls:\n                item.color = ft.colors.RED_ACCENT_700\n        else:\n            for item in self.fields['cancel'].content.controls:\n                item.color = None\n\n        self.fields['cancel'].disabled = not enable\n        self.fields['cancel'].update()\n\n    def cancel(self, e=None):\n        self.create_continue = False\n\n    def process_cancel(self):\n        # if settings.ENABLE_INVENTREE:\n        #     if self.fields['inventree_progress'].value < 1.0:\n        #         self.fields['inventree_progress'].color = \"red\"\n        #         self.fields['inventree_progress'].update()\n        # if settings.ENABLE_KICAD:\n        #     if self.fields['kicad_progress'].value < 1.0:\n        #         self.fields['kicad_progress'].color = \"red\"\n        #         self.fields['kicad_progress'].update()\n        self.show_dialog(DialogType.ERROR, 'Action Cancelled')\n        self.create_continue = True\n        self.enable_create(True)\n        return\n    \n    def reset_progress_bars(self):\n        # Setup progress bars\n        if not settings.ENABLE_INVENTREE:\n            self.inventree_progress_row.current.visible = False\n        else:\n            self.inventree_progress_row.current.visible = True\n            # Reset progress bar\n            progress.reset_progress_bar(self.fields['inventree_progress'])\n        self.inventree_progress_row.current.update()\n\n        if not settings.ENABLE_KICAD:\n            self.kicad_progress_row.current.visible = False\n        else:\n            self.kicad_progress_row.current.visible = True\n            # Reset progress bar\n            progress.reset_progress_bar(self.fields['kicad_progress'])\n        self.kicad_progress_row.current.update()\n\n        if not settings.ENABLE_INVENTREE and not settings.ENABLE_KICAD:\n            self.fields['create'].disabled = True\n        else:\n            self.fields['create'].disabled = False\n        self.fields['create'].update()\n        \n    def create_part(self, e=None):\n        self.reset_progress_bars()\n\n        if not settings.ENABLE_INVENTREE and not settings.ENABLE_KICAD:\n            self.show_dialog(DialogType.ERROR, 'Both InvenTree and KiCad are disabled (nothing to create)')\n\n        # print('data_from_views='); cprint(data_from_views)\n\n        # Check data is present\n        if not data_from_views.get('Part Search', None):\n            self.show_dialog(DialogType.ERROR, 'Missing Part Data (nothing to create)')\n            return\n        \n        # Custom part check\n        part_info = copy.deepcopy(data_from_views['Part Search'])\n        custom = part_info.pop('custom_part')\n        \n        # Part number check\n        part_number = data_from_views['Part Search'].get('manufacturer_part_number', None)\n        if not custom:\n            if not part_number:\n                self.show_dialog(DialogType.ERROR, 'Missing Manufacturer Part Number')\n                return\n            else:\n                # Update IPN (later overwritten)\n                part_info['IPN'] = part_number\n\n        # Button update\n        self.enable_create(False)\n\n        # KiCad data gathering\n        symbol = None\n        template = None\n        footprint = None\n        if settings.ENABLE_KICAD and not settings.ENABLE_ALTERNATE:\n            # Check data is present\n            if not data_from_views.get('KiCad', None):\n                self.show_dialog(DialogType.ERROR, 'Missing KiCad Data')\n                return\n            \n            # Process symbol\n            symbol_lib = data_from_views['KiCad'].get('Symbol Library', None)\n            if symbol_lib:\n                symbol = f\"{symbol_lib}:{part_number}\"\n\n            # Process template\n            template = data_from_views['KiCad'].get('Symbol Template', None)\n\n            # Process footprint\n            footprint_lib = data_from_views['KiCad'].get('Footprint Library', None)\n            if footprint_lib:\n                if data_from_views['KiCad'].get('New Footprint', False):\n                    new_footprint = data_from_views['KiCad'].get('New Footprint Name', 'TBD')\n                    footprint = f\"{footprint_lib}:{new_footprint}\"\n                elif data_from_views['KiCad'].get('Footprint', None):\n                    footprint = f\"{footprint_lib}:{data_from_views['KiCad']['Footprint']}\"\n                else:\n                    pass\n            \n            # print(symbol, template, footprint)\n            if not symbol or not template or not footprint:\n                self.show_dialog(DialogType.ERROR, 'Missing KiCad Data')\n                return\n        \n        if not self.create_continue:\n            return self.process_cancel()\n\n        # InvenTree data processing\n        if settings.ENABLE_INVENTREE:\n            # Check data is present\n            if not data_from_views.get('InvenTree', None):\n                self.show_dialog(DialogType.ERROR, 'Missing InvenTree Data')\n                return\n            # Check connection\n            if not inventree_interface.connect_to_server():\n                self.show_dialog(DialogType.ERROR, 'ERROR: Failed to connect to InvenTree server')\n                return\n            \n            if settings.ENABLE_ALTERNATE:\n                # Check mandatory data\n                if not data_from_views['InvenTree']['Existing Part ID'] and not data_from_views['InvenTree']['Existing Part IPN']:\n                    self.show_dialog(DialogType.ERROR, 'Missing Existing Part ID and Part IPN')\n                    return\n                # Create alternate\n                alt_result = inventree_interface.inventree_create_alternate(\n                    part_info=part_info,\n                    part_id=data_from_views['InvenTree']['Existing Part ID'],\n                    part_ipn=data_from_views['InvenTree']['Existing Part IPN'],\n                    show_progress=self.fields['inventree_progress'],\n                )\n            else:\n                # Check mandatory data\n                if not data_from_views['Part Search'].get('name', None):\n                    self.show_dialog(DialogType.ERROR, 'Missing Part Name')\n                    return\n                if len(data_from_views['Part Search'].get('name', None)) > 100:\n                    self.show_dialog(DialogType.ERROR, 'Part Name too long (>100 characters)')\n                    return\n                if not data_from_views['Part Search'].get('description', None):\n                    self.show_dialog(DialogType.ERROR, 'Missing Part Description')\n                    return\n                # Get relevant data\n                category_tree = data_from_views['InvenTree'].get('Category', None)\n                if not category_tree:\n                    # Check category is present\n                    self.show_dialog(DialogType.ERROR, 'Missing InvenTree Category')\n                    return\n                else:\n                    part_info['category_tree'] = category_tree\n                # Category code\n                if settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False):\n                    if data_from_views['InvenTree'].get('Create New Code', False):\n                        part_info['category_code'] = data_from_views['InvenTree'].get('New Category Code', '')\n                    else:\n                        part_info['category_code'] = data_from_views['InvenTree'].get('IPN: Category Code', '')\n\n                stock = None\n                if data_from_views['InvenTree'].get('Create stock'):\n                    stock_tree = data_from_views['InvenTree'].get('Stock location', None)\n                    if not stock_tree:\n                        # Check category is present\n                        self.show_dialog(DialogType.ERROR, 'Missing InvenTree Stock location')\n                        return\n\n                    stock = {\n                        'location': inventree_interface.get_inventree_stock_location_id(data_from_views['InvenTree'].get('Stock location')),\n                        'quantity': data_from_views['InvenTree'].get('Stock quantity'),\n                        'make_default': data_from_views['InvenTree'].get('Make stock location default'),\n                    }\n\n                # Create new part\n                new_part, part_pk, part_info = inventree_interface.inventree_create(\n                    part_info=part_info,\n                    kicad=settings.ENABLE_KICAD,\n                    symbol=symbol,\n                    footprint=footprint,\n                    show_progress=self.fields['inventree_progress'],\n                    is_custom=custom,\n                    stock=stock,\n                )\n                # print(new_part, part_pk)\n                # cprint(part_info)\n\n            if settings.ENABLE_ALTERNATE:\n                if alt_result:\n                    # Update InvenTree URL\n                    if data_from_views['InvenTree']['Existing Part IPN']:\n                        part_ref = data_from_views['InvenTree']['Existing Part IPN']\n                    else:\n                        part_ref = data_from_views['InvenTree']['Existing Part ID']\n                    part_info['inventree_url'] = f'{settings.PART_URL_ROOT}{part_ref}/'\n                else:\n                    self.fields['inventree_progress'].color = \"amber\"\n                # Complete add operation\n                self.fields['inventree_progress'].value = progress.MAX_PROGRESS\n            else:\n                if part_pk:\n                    # Update symbol\n                    if symbol:\n                        symbol = f'{symbol.split(\":\")[0]}:{part_info[\"IPN\"]}'\n\n                    self.fields['inventree_progress'].color = 'green'\n                    if not new_part:\n                        self.fields['inventree_progress'].color = 'amber'\n                    # Complete add operation\n                    self.fields['inventree_progress'].value = progress.MAX_PROGRESS\n                else:\n                    self.fields['inventree_progress'].color = 'red'\n            \n            self.fields['inventree_progress'].update()\n\n        if not self.create_continue:\n            return self.process_cancel()\n\n        # KiCad data processing\n        if settings.ENABLE_KICAD and not settings.ENABLE_ALTERNATE:\n            # Store \"pseudo-category\" as re-used in multiple places\n            pseudo_category = symbol.split(':')[0]\n            # Translate part info if InvenTree not enabled\n            if not settings.ENABLE_INVENTREE:\n                part_info = inventree_interface.translate_form_to_inventree(\n                    part_info=part_info,\n                    category_tree=[pseudo_category],\n                    is_custom=custom,\n                )\n                # Also add datasheet URL as part page URL\n                part_info['inventree_url'] = part_info['datasheet']\n            part_info['Symbol'] = symbol\n            part_info['Template'] = template.split('/')\n            part_info['Footprint'] = footprint\n\n            symbol_library_path = os.path.join(\n                settings.KICAD_SETTINGS['KICAD_SYMBOLS_PATH'],\n                f'{pseudo_category}.kicad_sym',\n            )\n\n            # Reset progress\n            progress.CREATE_PART_PROGRESS = 0\n            # Add part symbol to KiCAD\n            cprint('\\n[MAIN]\\tAdding part to KiCad', silent=settings.SILENT)\n            kicad_success, kicad_new_part, kicad_part_name = kicad_interface.inventree_to_kicad(\n                part_data=part_info,\n                library_path=symbol_library_path,\n                show_progress=self.fields['kicad_progress'],\n            )\n            # print(kicad_success, kicad_new_part)\n            # Update symbol name in InvenTree\n            if settings.ENABLE_INVENTREE and part_pk:\n                old_state = settings.UPDATE_INVENTREE\n                settings.UPDATE_INVENTREE = True\n                inventree_interface.inventree_process_parameters(\n                    part_pk,\n                    {'Symbol': f\"{symbol_lib}:{kicad_part_name}\"},\n                    show_progress=self.fields['inventree_progress'],\n                )\n                settings.UPDATE_INVENTREE = old_state\n\n            # Complete add operation\n            if kicad_success:\n                self.fields['kicad_progress'].color = 'green'\n                if not kicad_new_part:\n                    self.fields['kicad_progress'].color = 'amber'\n                    self.fields['kicad_progress'].update()\n                self.fields['kicad_progress'].value = progress.MAX_PROGRESS\n                self.fields['kicad_progress'].update()\n            else:\n                self.fields['kicad_progress'].color = 'red'\n                self.fields['kicad_progress'].update()\n\n        if not self.create_continue:\n            return self.process_cancel()\n        \n        # Final operations\n        # Download a local version of the part datasheet\n        if settings.DATASHEET_SAVE_ENABLED:\n            filename = os.path.join(\n                settings.DATASHEET_SAVE_PATH,\n                f'{part_info.get(\"IPN\", \"datasheet\")}.pdf',\n            )\n            if settings.DATASHEET_UPLOAD and os.path.isfile(filename):\n                # Datasheet was already downloaded\n                cprint('\\n[MAIN]\\tDatasheet')\n                cprint(f'[INFO]\\tSuccess: Datasheet file exists ({filename})')\n            else:\n                # Datasheet needs to be downloaded\n                datasheet_url = part_info.get('datasheet', None)\n                if datasheet_url:\n                    cprint('\\n[MAIN]\\tDownloading Datasheet')\n                    if download_with_retry(datasheet_url, filename, filetype='PDF', timeout=10):\n                        cprint(f'[INFO]\\tSuccess: Datasheet saved to {filename}')\n        # Open browser\n        if settings.ENABLE_INVENTREE:\n            if part_info.get('inventree_url', None):\n                if settings.AUTOMATIC_BROWSER_OPEN:\n                    # Auto-Open Browser Window\n                    cprint(\n                        f'\\n[MAIN]\\tOpening URL {part_info[\"inventree_url\"]} in browser',\n                        silent=settings.SILENT\n                    )\n                    try:\n                        self._page.launch_url(part_info['inventree_url'])\n                    except TypeError:\n                        cprint('[INFO]\\tError: Failed to open URL', silent=settings.SILENT)\n                else:\n                    cprint(f'\\n[MAIN]\\tPart page URL: {part_info[\"inventree_url\"]}', silent=settings.SILENT)\n\n        # Button update\n        self.enable_create(True)\n\n    def build_column(self):\n        self.inventree_progress_row = ft.Ref[ft.Row]()\n        self.kicad_progress_row = ft.Ref[ft.Row]()\n\n        # Update callbacks\n        self.fields['create'].on_click = self.create_part\n        self.fields['cancel'].on_click = self.cancel\n\n        self.column = ft.Column(\n            controls=[\n                ft.Row(),\n                ft.Row(\n                    controls=[\n                        self.fields['create'],\n                        self.fields['cancel'],\n                    ],\n                    alignment=ft.MainAxisAlignment.CENTER,\n                    width=600,\n                ),\n                ft.Row(height=16),\n                ft.Row(\n                    ref=self.inventree_progress_row,\n                    controls=[\n                        ft.Icon(ft.icons.INVENTORY_2, size=32),\n                        ft.Text('InvenTree', size=20, weight=ft.FontWeight.BOLD, width=120),\n                        self.fields['inventree_progress'],\n                    ],\n                    width=600,\n                    visible=settings.ENABLE_INVENTREE,\n                ),\n                ft.Row(\n                    ref=self.kicad_progress_row,\n                    controls=[\n                        ft.Icon(ft.icons.SETTINGS_INPUT_COMPONENT, size=32),\n                        ft.Text('KiCad', size=20, weight=ft.FontWeight.BOLD, width=120),\n                        self.fields['kicad_progress'],\n                    ],\n                    width=600,\n                    visible=settings.ENABLE_KICAD,\n                ),\n            ],\n        )\n\n    def did_mount(self):\n        self.reset_progress_bars()\n        return super().did_mount()\n"
  },
  {
    "path": "kintree/gui/views/settings.py",
    "content": "import flet as ft\n\n# Common view\nfrom .common import DialogType\nfrom .common import CommonView\nfrom .common import SwitchWithRefs\nfrom .common import GUI_PARAMS\nfrom .common import handle_transition\n# Settings\nfrom ...config import settings as global_settings\nfrom ...config import config_interface\n\n\n# Load Supplier Settings\nsupplier_settings = {}\nfor supplier, data in global_settings.CONFIG_SUPPLIERS.items():\n    supplier_settings[supplier] = {}\n\n    # Add enable\n    supplier_settings[supplier]['Enable'] = [\n        data['enable'],\n        ft.Switch(),\n        None,\n    ]\n\n    # Add supplier name\n    supplier_settings[supplier]['InvenTree Name'] = [\n        data['name'],\n        ft.TextField(),\n        None,\n    ]\n\n    # Add API fields\n    if supplier == 'Digi-Key':\n        digikey_api_settings = config_interface.load_file(global_settings.CONFIG_DIGIKEY_API)\n        supplier_settings[supplier]['Client ID'] = [\n            digikey_api_settings['DIGIKEY_CLIENT_ID'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['Client Secret'] = [\n            digikey_api_settings['DIGIKEY_CLIENT_SECRET'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['Local Site'] = [\n            digikey_api_settings.get('DIGIKEY_LOCAL_SITE', 'US'),\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['Language'] = [\n            digikey_api_settings.get('DIGIKEY_LOCAL_LANGUAGE', 'en'),\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['Currency'] = [\n            digikey_api_settings.get('DIGIKEY_LOCAL_CURRENCY', 'USD'),\n            ft.TextField(),\n            None,\n        ]\n    elif supplier == 'Mouser':\n        mouser_api_settings = config_interface.load_file(global_settings.CONFIG_MOUSER_API)\n        supplier_settings[supplier]['Part API Key'] = [\n            mouser_api_settings['MOUSER_PART_API_KEY'],\n            ft.TextField(),\n            None,\n        ]\n    elif supplier == 'Element14' or supplier == 'Farnell' or supplier == 'Newark':\n        from ...search.element14_api import STORES\n        element14_api_settings = config_interface.load_file(global_settings.CONFIG_ELEMENT14_API)\n        default_store = element14_api_settings.get(f'{supplier.upper()}_STORE', '')\n\n        supplier_settings[supplier]['Product Search API Key (Element14)'] = [\n            element14_api_settings['ELEMENT14_PRODUCT_SEARCH_API_KEY'],\n            ft.TextField(),\n            None,\n        ]\n        \n        dropdown_options = []\n        for store_name, store_url in STORES[supplier].items():\n            dropdown_options.append(ft.dropdown.Option(f'{store_name} ({store_url})'))\n        supplier_settings[supplier][f'{supplier} Store'] = [\n            default_store,\n            ft.Dropdown(\n                label='Store',\n                width=GUI_PARAMS['dropdown_width'],\n                dense=GUI_PARAMS['dropdown_dense'],\n                options=dropdown_options\n            ),\n            None,\n        ]\n    elif supplier == 'LCSC':\n        lcsc_api_settings = config_interface.load_file(global_settings.CONFIG_LCSC_API)\n        supplier_settings[supplier]['API URL'] = [\n            lcsc_api_settings['LCSC_API_URL'],\n            ft.TextField(),\n            None,\n        ]\n    elif supplier == 'Jameco':\n        jameco_api_settings = config_interface.load_file(global_settings.CONFIG_JAMECO_API)\n        supplier_settings[supplier]['API URL'] = [\n            jameco_api_settings['JAMECO_API_URL'],\n            ft.TextField(),\n            None,\n        ]\n    elif supplier == 'TME':\n        tme_api_settings = config_interface.load_file(global_settings.CONFIG_TME_API)\n        supplier_settings[supplier]['API Token'] = [\n            tme_api_settings['TME_API_TOKEN'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['API Secret'] = [\n            tme_api_settings['TME_API_SECRET'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['API Country'] = [\n            tme_api_settings['TME_API_COUNTRY'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['API Language'] = [\n            tme_api_settings['TME_API_LANGUAGE'],\n            ft.TextField(),\n            None,\n        ]\n    elif supplier == 'AutomationDirect':\n        automationdirect_api_settings = config_interface.load_file(global_settings.CONFIG_AUTOMATIONDIRECT_API)\n        supplier_settings[supplier]['API Top-Level Root Domain'] = [\n            automationdirect_api_settings['AUTOMATIONDIRECT_API_ROOT_URL'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['API URL Path'] = [\n            automationdirect_api_settings['AUTOMATIONDIRECT_API_URL'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['API Search Query'] = [\n            automationdirect_api_settings['AUTOMATIONDIRECT_API_SEARCH_QUERY'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['API Search String'] = [\n            automationdirect_api_settings['AUTOMATIONDIRECT_API_SEARCH_STRING'],\n            ft.TextField(),\n            None,\n        ]\n        supplier_settings[supplier]['API Image Path URL'] = [\n            automationdirect_api_settings['AUTOMATIONDIRECT_API_IMAGE_PATH'],\n            ft.TextField(),\n            None,\n        ]\n\n\nSETTINGS = {\n    'User Settings': {\n        'Configuration Files Folder': [\n            'USER_FILES',\n            ft.TextField(),\n            True,  # Browse enabled\n        ],\n        'Cache Folder': [\n            'USER_CACHE',\n            ft.TextField(),\n            True,  # Browse enabled\n        ],\n        'Save Datasheets to Local Folder': [\n            'DATASHEET_SAVE_ENABLED',\n            SwitchWithRefs(),\n            False,  # Browse enabled\n        ],\n        'Datasheet Folder': [\n            'DATASHEET_SAVE_PATH',\n            ft.TextField(),\n            True,  # Browse enabled\n        ],\n        'Open Browser After Creating Part': [\n            'AUTOMATIC_BROWSER_OPEN',\n            ft.Switch(),\n            False,  # Browse enabled\n        ],\n        'Enable Supplier Search Cache': [\n            'CACHE_ENABLED',\n            SwitchWithRefs(),\n            False,  # Browse enabled\n        ],\n        'CACHE_VALID_DAYS': [\n            'CACHE_VALID_DAYS',\n            ft.TextField(\n                text_align=ft.TextAlign.CENTER,\n                width=60,\n                dense=True,\n                disabled=True,\n            ),\n            False,\n        ]\n    },\n    'Supplier Settings': supplier_settings,\n    'InvenTree Settings': {\n        'Server Address': [\n            'SERVER_ADDRESS',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'Username': [\n            'USERNAME',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'Password or Token': [\n            'PASSWORD',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'Enable Proxy Support': [\n            'ENABLE_PROXY',\n            SwitchWithRefs(),\n            False,  # Browse disabled\n        ],\n        'Proxy': [\n            'PROXY',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'Upload Datasheets to InvenTree': [\n            'DATASHEET_UPLOAD',\n            SwitchWithRefs(),\n            False,  # Browse enabled\n        ],\n        'Upload Pricing Data to InvenTree': [\n            'PRICING_UPLOAD',\n            SwitchWithRefs(),\n            False,  # Browse enabled\n        ],\n        'Default Part Revision': [\n            'INVENTREE_DEFAULT_REV',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'Enable Internal Part Number (IPN)': [\n            'IPN_ENABLE_CREATE',\n            SwitchWithRefs(),\n            False,  # Browse disabled\n        ],\n        'Use Manufacturer Part Number as IPN': [\n            'IPN_USE_MANUFACTURER_PART_NUMBER',\n            SwitchWithRefs(reverse_dir=True),\n            False,  # Browse disabled\n        ],\n        'IPN: Enable Prefix': [\n            'IPN_ENABLE_PREFIX',\n            SwitchWithRefs(),\n            False,  # Browse disabled\n        ],\n        'IPN: Prefix': [\n            'IPN_PREFIX',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'IPN: Enable Category Codes': [\n            'IPN_CATEGORY_CODE',\n            ft.Switch(),\n            False,  # Browse disabled\n        ],\n        'IPN: Length of Unique ID': [\n            'IPN_UNIQUE_ID_LENGTH',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'IPN: Enable Suffix': [\n            'IPN_ENABLE_SUFFIX',\n            SwitchWithRefs(),\n            False,  # Browse disabled\n        ],\n        'IPN: Suffix': [\n            'IPN_SUFFIX',\n            ft.TextField(),\n            False,  # Browse disabled\n        ],\n        'Test': [\n            None,\n            ft.ElevatedButton,\n            False,  # Browse disabled\n        ],\n    },\n    'KiCad Settings': {\n        'Symbol Libraries Folder': [\n            'KICAD_SYMBOLS_PATH',\n            ft.TextField(),\n            True,  # Browse enabled\n        ],\n        'Symbol Templates Folder': [\n            'KICAD_TEMPLATES_PATH',\n            ft.TextField(),\n            True,  # Browse enabled\n        ],\n        'Footprint Libraries Folder': [\n            'KICAD_FOOTPRINTS_PATH',\n            ft.TextField(),\n            True,  # Browse enabled\n        ],\n    },\n}\n\n# Settings AppBar\nsettings_appbar = ft.AppBar(\n    title=ft.WindowDragArea(ft.Container(ft.Text('Ki-nTree Settings'),\n                                         width=10000), maximizable=True),\n    bgcolor=ft.colors.SURFACE_VARIANT\n)\n\n# Settings NavRail\nsettings_navrail = ft.NavigationRail(\n    selected_index=0,\n    label_type=ft.NavigationRailLabelType.ALL,\n    min_width=GUI_PARAMS['nav_rail_min_width'],\n    min_extended_width=GUI_PARAMS['nav_rail_width'],\n    group_alignment=GUI_PARAMS['nav_rail_alignment'],\n    destinations=[\n        ft.NavigationRailDestination(\n            label_content=ft.Text(\"User\", size=GUI_PARAMS['nav_rail_text_size']),\n            icon_content=ft.Icon(name=ft.icons.SUPERVISED_USER_CIRCLE, size=GUI_PARAMS['nav_rail_icon_size']),\n            selected_icon_content=ft.Icon(name=ft.icons.SUPERVISED_USER_CIRCLE_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']),\n            padding=GUI_PARAMS['nav_rail_padding'],\n        ),\n        ft.NavigationRailDestination(\n            label_content=ft.Text(\"Supplier\", size=GUI_PARAMS['nav_rail_text_size']),\n            icon_content=ft.Icon(name=ft.icons.LOCAL_SHIPPING, size=GUI_PARAMS['nav_rail_icon_size']),\n            selected_icon_content=ft.Icon(name=ft.icons.LOCAL_SHIPPING_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']),\n            padding=GUI_PARAMS['nav_rail_padding'],\n        ),\n        ft.NavigationRailDestination(\n            label_content=ft.Text(\"InvenTree\", size=GUI_PARAMS['nav_rail_text_size']),\n            icon_content=ft.Icon(name=ft.icons.INVENTORY_2, size=GUI_PARAMS['nav_rail_icon_size']),\n            selected_icon_content=ft.Icon(name=ft.icons.INVENTORY_2_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']),\n            padding=GUI_PARAMS['nav_rail_padding'],\n        ),\n        ft.NavigationRailDestination(\n            label_content=ft.Text(\"KiCad\", size=GUI_PARAMS['nav_rail_text_size']),\n            icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT, size=GUI_PARAMS['nav_rail_icon_size']),\n            selected_icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']),\n            padding=GUI_PARAMS['nav_rail_padding'],\n        ),\n    ],\n    on_change=None,\n)\n\n# Navigation indexes (settings)\nNAV_BAR_INDEX = {\n    0: '/settings/user',\n    1: '/settings/supplier',\n    2: '/settings/inventree',\n    3: '/settings/kicad',\n}\n\n\nclass SettingsView(CommonView):\n    '''Main settings view'''\n\n    title = 'Settings'\n    route = '/settings'\n    settings = None\n    settings_file = None\n    dialog = None\n\n    def __init__(self, page: ft.Page):\n        # Load setting fields\n        self.fields = {}\n        for field_name, field_data in SETTINGS.get(self.title, {}).items():\n            if isinstance(field_data, list) and field_data[0] is not None:\n                self.fields[field_name] = field_data[1]\n                self.fields[field_name].value = self.settings[field_data[0]]\n\n        # Init view\n        super().__init__(page=page, appbar=settings_appbar, navigation_rail=settings_navrail)\n        if not self.appbar.actions:\n            self.appbar.actions.extend(\n                [\n                    ft.IconButton(\n                        ft.icons.CLOSE,\n                        on_click=lambda _: page.window.close(),\n                    ),\n                ]\n            )\n\n        # Update navigation rail\n        self.navigation_rail.on_change = self.nav_rail_redirect\n\n    def nav_rail_redirect(self, e):\n        self._page.go(NAV_BAR_INDEX[e.control.selected_index])\n    \n    def save(self, settings_file=None, show_dialog=True):\n        '''Save settings'''\n        if settings_file is not None:\n            settings_from_file = config_interface.load_file(settings_file)\n        else:\n            settings_from_file = config_interface.load_file(self.settings_file)\n\n        # Update settings values\n        for key in settings_from_file:\n            for setting in SETTINGS[self.title].values():\n                if key == setting[0]:\n                    settings_from_file[key] = setting[1].value\n\n        # Save\n        if settings_file is not None:\n            config_interface.dump_file(settings_from_file, settings_file)\n        else:\n            config_interface.dump_file(settings_from_file, self.settings_file)\n\n        # Alert user\n        if show_dialog:\n            self.show_dialog(\n                d_type=DialogType.VALID,\n                message=f'{self.title} successfully saved',\n            )\n\n    def on_dialog_result(self, e: ft.FilePickerResultEvent):\n        '''Populate field with user-selected system path'''\n        if e.path:\n            self.fields[e.control.dialog_title].value = e.path\n            self._page.update()\n\n    def path_picker(self, e: ft.ControlEvent, title: str):\n        '''Let user browse to a system path'''\n        if self._page.overlay:\n            self._page.overlay.pop()\n        path_picker = ft.FilePicker(on_result=self.on_dialog_result)\n        self._page.overlay.append(path_picker)\n        self._page.update()\n        if self.fields[title].value:\n            path_picker.get_directory_path(dialog_title=title, initial_directory=self.fields[title].value)\n        else:\n            path_picker.get_directory_path(dialog_title=title, initial_directory=global_settings.HOME_DIR)\n\n    def init_column(self) -> ft.Column:\n        return ft.Column(\n            controls=[\n                ft.Text(self.title, style=\"bodyMedium\"),\n                ft.Row(),\n            ],\n            alignment=ft.MainAxisAlignment.START,\n            expand=True,\n        )\n\n    def update_field(self, name, field, column):\n        if isinstance(field, ft.TextField):\n            field_predefined = bool(field.width)\n            if not field_predefined:\n                field.label = name\n                field.width = GUI_PARAMS['textfield_width']\n                field.dense = GUI_PARAMS['textfield_dense']\n                if 'password' in field.label.lower():\n                    field.password = True\n                field_row = ft.Row(\n                    controls=[\n                        field,\n                    ]\n                )\n                # Add browse button\n                if SETTINGS[self.title][name][2]:\n                    field_row.controls.append(\n                        ft.ElevatedButton(\n                            'Browse',\n                            width=GUI_PARAMS['button_width'],\n                            height=48,\n                            on_click=lambda e, t=name: self.path_picker(e, title=t)\n                        ),\n                    )\n                column.controls.extend(\n                    [\n                        field_row,\n                        ft.Row(height=GUI_PARAMS['textfield_space_after']),\n                    ]\n                )\n        elif isinstance(field, ft.Text):\n            field.value = name\n            field_row = ft.Row(\n                controls=[\n                    field,\n                ]\n            )\n            column.controls.append(field_row)\n            column.controls.append(ft.Divider())\n        elif isinstance(field, ft.TextButton):\n            column.controls.append(\n                ft.ElevatedButton(\n                    name,\n                    width=GUI_PARAMS['button_width'] * 2,\n                    height=GUI_PARAMS['button_height'],\n                    icon=ft.icons.CHECK_OUTLINED,\n                    on_click=lambda e, s=name: self.test_s(e, s=s)\n                ),\n            )\n        elif isinstance(field, ft.Dropdown):\n            field.on_change = lambda _: self.save()\n            column.controls.append(\n                field,\n            )\n        elif isinstance(field, ft.Switch) or isinstance(field, SwitchWithRefs):\n            if 'proxy' in name.lower():\n                field.on_change = lambda _: None\n            else:\n                field.on_change = lambda _: self.save()\n            field.label = name\n            column.controls.append(\n                field,\n            )\n\n    def add_buttons(self, column, test=False) -> ft.Row:\n        test_save_buttons = ft.Row()\n        if test:\n            test_save_buttons.controls.append(\n                ft.ElevatedButton(\n                    'Test',\n                    width=GUI_PARAMS['button_width'],\n                    height=GUI_PARAMS['button_height'],\n                    icon=ft.icons.CHECK_OUTLINED,\n                    on_click=lambda _: self.test(),\n                ),\n            )\n        test_save_buttons.controls.append(\n            ft.ElevatedButton(\n                'Save',\n                width=GUI_PARAMS['button_width'],\n                height=GUI_PARAMS['button_height'],\n                icon=ft.icons.SAVE_OUTLINED,\n                on_click=lambda _: self.save()\n            ),\n        )\n        column.controls.append(test_save_buttons)\n\n    def build_column(self, ignore=[]):\n        # Header\n        self.column = self.init_column()\n\n        # Fields\n        for name, field in self.fields.items():\n            if name not in ignore:\n                self.update_field(name, field, self.column)\n\n        # Test and Save buttons\n        enable_test = bool(list(SETTINGS[self.title])[-1] == 'Test')\n        self.add_buttons(self.column, test=enable_test)\n\n    def did_mount(self):\n        handle_transition(self._page, transition=False, timeout=0.05)\n        return super().did_mount()\n    \n\nclass PathSettingsView(SettingsView):\n    '''Template View for Path Setters'''\n\n    def __init__(self, page: ft.Page):\n        super().__init__(page)\n        self.dialog = self.build_dialog()\n\n    def build_dialog(self):\n        return ft.Banner(\n            bgcolor=ft.colors.AMBER_100,\n            leading=ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.AMBER, size=GUI_PARAMS['icon_size']),\n            content=ft.Text(f'Restart Ki-nTree to load the new {self.title}', weight=ft.FontWeight.BOLD),\n            actions=[\n                ft.TextButton('Discard', on_click=lambda _: self.show_dialog(open=False)),\n            ],\n        )\n    \n    def show_dialog(self, d_type=None, message=None, snackbar=False, open=True):\n        return super().show_dialog(d_type, message, snackbar, open)\n\n\nclass UserSettingsView(PathSettingsView):\n    '''User settings view'''\n\n    title = 'User Settings'\n    route = '/settings/user'\n    settings = {\n        **global_settings.USER_SETTINGS,\n        **{\n            'DATASHEET_SAVE_ENABLED': global_settings.DATASHEET_SAVE_ENABLED,\n            'DATASHEET_SAVE_PATH': global_settings.DATASHEET_SAVE_PATH,\n            'AUTOMATIC_BROWSER_OPEN': global_settings.AUTOMATIC_BROWSER_OPEN\n        },\n        **{\n            'CACHE_ENABLED': global_settings.CACHE_ENABLED,\n            'CACHE_VALID_DAYS': global_settings.CACHE_VALID_DAYS\n        },\n    }\n    settings_file_list = [\n        global_settings.USER_CONFIG_FILE,\n        global_settings.CONFIG_GENERAL_PATH,\n        global_settings.CONFIG_SEARCH_API_PATH,\n    ]\n\n    def save(self):\n        # Save all settings\n        for sf in self.settings_file_list:\n            super().save(settings_file=sf, show_dialog=True)\n    \n    def increment_cache_value(self, inc):\n        field = SETTINGS[self.title]['CACHE_VALID_DAYS'][1]\n        current_value = int(field.value)\n        if not inc:\n            if current_value > 1:\n                field.value = f'{current_value - 1}'\n        else:\n            if current_value < 99:\n                field.value = f'{current_value + 1}'\n        field.on_change(_=None)\n        field.update()\n\n    def build_column(self):\n        # Header\n        self.column = self.init_column()\n        # Fields\n        for name, field in self.fields.items():\n            self.update_field(name, field, self.column)\n    \n        # Create refs\n        datasheet_row_ref = ft.Ref[ft.Row]()\n        cache_row_ref = ft.Ref[ft.Row]()\n\n        # Create row for cache validity\n        SETTINGS[self.title]['CACHE_VALID_DAYS'][1].value = self.settings['CACHE_VALID_DAYS']\n        cache_row = ft.Row(\n            ref=cache_row_ref,\n            controls=[\n                ft.Text('Keep Cache Valid For (Days): '),\n                ft.IconButton(ft.icons.REMOVE, on_click=lambda _: self.increment_cache_value(False)),\n                SETTINGS[self.title]['CACHE_VALID_DAYS'][1],\n                ft.IconButton(ft.icons.ADD, on_click=lambda _: self.increment_cache_value(True)),\n            ],\n        )\n        self.column.controls.append(cache_row)\n        # Add cache row to switch refs\n        SETTINGS[self.title]['Enable Supplier Search Cache'][1].refs = [cache_row_ref]\n\n        for name, field in SETTINGS[self.title].items():\n            if field[0] in ['AUTOMATIC_BROWSER_OPEN', 'DATASHEET_SAVE_ENABLED', 'DATASHEET_SAVE_PATH', 'DATASHEET_INVENTREE_ENABLED']:\n                self.fields[name].on_change = lambda _: self.save()\n            elif field[0] in ['CACHE_ENABLED', 'CACHE_VALID_DAYS']:\n                self.fields[name].on_change = lambda _: self.save()\n        self.settings_file = self.settings_file_list[0]\n\n        # Update datasheet ref\n        for idx, field in enumerate(self.column.controls):\n            if isinstance(field, SwitchWithRefs):\n                if field.label == 'Save Datasheets to Local Folder':\n                    datasheet_row_ref.current = self.column.controls[idx + 1]\n                    SETTINGS[self.title]['Save Datasheets to Local Folder'][1].refs = [datasheet_row_ref]\n        \n        # Save button\n        self.add_buttons(self.column, test=False)\n\n    def did_mount(self):\n        try:\n            # Reset Index\n            self.navigation_rail.selected_index = 0\n            self.navigation_rail.update()\n        except AssertionError:\n            pass\n        return super().did_mount()\n\n\nclass SupplierSettingsView(SettingsView):\n    '''Supplier settings view'''\n\n    title = 'Supplier Settings'\n    route = '/settings/supplier'\n    settings = global_settings.CONFIG_SUPPLIERS\n    settings_file = global_settings.CONFIG_SUPPLIERS_PATH\n\n    def __init__(self, page: ft.Page):\n        super().__init__(page)\n\n    def save_s(self, e: ft.ControlEvent, supplier: str, show_dialog=True):\n        '''Save supplier settings'''\n\n        # Enable/Name settings\n        supplier_settings = self.settings\n        enable_name = {\n            'enable': SETTINGS[self.title][supplier]['Enable'][1].value,\n            'name': SETTINGS[self.title][supplier]['InvenTree Name'][1].value,\n        }\n        supplier_settings.update({supplier: enable_name})\n        config_interface.dump_file(supplier_settings, self.settings_file)\n        # Update suppliers\n        global_settings.load_suppliers()\n        \n        # API settings\n        if supplier == 'Digi-Key':\n            from ...search import digikey_api\n            # Load settings from file\n            settings_from_file = config_interface.load_file(global_settings.CONFIG_DIGIKEY_API)\n            # Update settings values\n            updated_settings = {\n                'DIGIKEY_CLIENT_ID': SETTINGS[self.title][supplier]['Client ID'][1].value,\n                'DIGIKEY_CLIENT_SECRET': SETTINGS[self.title][supplier]['Client Secret'][1].value,\n                'DIGIKEY_LOCAL_SITE': SETTINGS[self.title][supplier]['Local Site'][1].value,\n                'DIGIKEY_LOCAL_LANGUAGE': SETTINGS[self.title][supplier]['Language'][1].value,\n                'DIGIKEY_LOCAL_CURRENCY': SETTINGS[self.title][supplier]['Currency'][1].value,\n            }\n            digikey_settings = {**settings_from_file, **updated_settings}\n            config_interface.dump_file(digikey_settings, global_settings.CONFIG_DIGIKEY_API)\n            digikey_api.setup_environment(force=True)\n        elif supplier == 'Mouser':\n            from ...search import mouser_api\n            # Load settings from file\n            settings_from_file = config_interface.load_file(global_settings.CONFIG_MOUSER_API)\n            # Update settings values\n            updated_settings = {\n                'MOUSER_PART_API_KEY': SETTINGS[self.title][supplier]['Part API Key'][1].value,\n            }\n            mouser_settings = {**settings_from_file, **updated_settings}\n            config_interface.dump_file(mouser_settings, global_settings.CONFIG_MOUSER_API)\n            mouser_api.setup_environment(force=True)\n        elif supplier == 'Element14' or supplier == 'Farnell' or supplier == 'Newark':\n            # Load settings from file\n            settings_from_file = config_interface.load_file(global_settings.CONFIG_ELEMENT14_API)\n            # Update settings values\n            updated_settings = {\n                'ELEMENT14_PRODUCT_SEARCH_API_KEY': SETTINGS[self.title][supplier]['Product Search API Key (Element14)'][1].value,\n                f'{supplier.upper()}_STORE': SETTINGS[self.title][supplier][f'{supplier} Store'][1].value,\n            }\n            element14_settings = {**settings_from_file, **updated_settings}\n            config_interface.dump_file(element14_settings, global_settings.CONFIG_ELEMENT14_API)\n        elif supplier == 'LCSC':\n            # Load settings from file\n            settings_from_file = config_interface.load_file(global_settings.CONFIG_LCSC_API)\n            # Update settings values\n            updated_settings = {\n                'LCSC_API_URL': SETTINGS[self.title][supplier]['API URL'][1].value,\n            }\n            lcsc_settings = {**settings_from_file, **updated_settings}\n            config_interface.dump_file(lcsc_settings, global_settings.CONFIG_LCSC_API)\n        elif supplier == 'Jameco':\n            # Load settings from file\n            settings_from_file = config_interface.load_file(global_settings.CONFIG_JAMECO_API)\n            # Update settings values\n            updated_settings = {\n                'JAMECO_API_URL': SETTINGS[self.title][supplier]['API URL'][1].value,\n            }\n            jameco_settings = {**settings_from_file, **updated_settings}\n            config_interface.dump_file(jameco_settings, global_settings.CONFIG_JAMECO_API)\n        elif supplier == 'TME':\n            # Load settings from file\n            settings_from_file = config_interface.load_file(global_settings.CONFIG_TME_API)\n            # Update settings values\n            updated_settings = {\n                'TME_API_TOKEN': SETTINGS[self.title][supplier]['API Token'][1].value,\n                'TME_API_SECRET': SETTINGS[self.title][supplier]['API Secret'][1].value,\n                'TME_API_COUNTRY': SETTINGS[self.title][supplier]['API Country'][1].value,\n                'TME_API_LANGUAGE': SETTINGS[self.title][supplier]['API Language'][1].value,\n            }\n            tme_settings = {**settings_from_file, **updated_settings}\n            config_interface.dump_file(tme_settings, global_settings.CONFIG_TME_API)\n        elif supplier == 'AutomationDirect':\n            # Load settings from file\n            settings_from_file = config_interface.load_file(global_settings.CONFIG_AUTOMATIONDIRECT_API)\n            # Update settings values\n            updated_settings = {\n                'AUTOMATIONDIRECT_API_ROOT_URL': SETTINGS[self.title][supplier]['API Top-Level Root Domain'][1].value,\n                'AUTOMATIONDIRECT_API_URL': SETTINGS[self.title][supplier]['API URL Path'][1].value,\n                'AUTOMATIONDIRECT_API_SEARCH_QUERY': SETTINGS[self.title][supplier]['API Search Query'][1].value,\n                'AUTOMATIONDIRECT_API_SEARCH_STRING': SETTINGS[self.title][supplier]['API Search String'][1].value,\n                'AUTOMATIONDIRECT_API_IMAGE_PATH': SETTINGS[self.title][supplier]['API Image Path URL'][1].value,\n            }\n            automationdirect_settings = {**settings_from_file, **updated_settings}\n            config_interface.dump_file(automationdirect_settings, global_settings.CONFIG_AUTOMATIONDIRECT_API)\n\n        if show_dialog:\n            self.show_dialog(\n                d_type=DialogType.VALID,\n                message=f'{supplier} Settings successfully saved',\n            )\n\n    def test_s(self, e: ft.ControlEvent, supplier: str):\n        '''Test supplier API settings'''\n        self.save_s(e, supplier, show_dialog=False)\n\n        result = False\n        if supplier == 'Digi-Key':\n            from ...search import digikey_api\n            result = digikey_api.test_api()\n        elif supplier == 'Mouser':\n            from ...search import mouser_api\n            result = mouser_api.test_api()\n        elif supplier == 'Element14' or supplier == 'Farnell' or supplier == 'Newark':\n            from ...search import element14_api\n            result = element14_api.test_api()\n        elif supplier == 'LCSC':\n            from ...search import lcsc_api\n            result = lcsc_api.test_api()\n        elif supplier == 'TME':\n            from ...search import tme_api\n            result = tme_api.test_api()\n        elif supplier == 'Jameco':\n            from ...search import jameco_api\n            result = jameco_api.test_api()\n        elif supplier == 'AutomationDirect':\n            from ...search import automationdirect_api\n            result = automationdirect_api.test_api()\n\n        if result:\n            self.show_dialog(\n                d_type=DialogType.VALID,\n                message=f'Successfully connected to {supplier} API'\n            )\n        else:\n            self.show_dialog(\n                d_type=DialogType.ERROR,\n                message=f'ERROR: Failed to connect to {supplier} API. Verify the {supplier} credentials and re-try'\n            )\n\n    def build_column(self):\n        # Header\n        self.column = self.init_column()\n        \n        # Tabs\n        supplier_tabs = ft.Tabs(\n            selected_index=0,\n            animation_duration=10,\n            expand=1,\n            tabs=[],\n        )\n        for supplier, settings in SETTINGS[self.title].items():\n            supplier_tab_content = [\n                ft.Row(height=10),\n            ]\n            for setting_name, setting_data in settings.items():\n                setting_data[1].label = setting_name\n                setting_data[1].width = GUI_PARAMS['textfield_width']\n                setting_data[1].dense = GUI_PARAMS['textfield_dense']\n                setting_data[1].value = setting_data[0]\n                supplier_tab_content.extend(\n                    [\n                        ft.Row([setting_data[1]]),\n                        ft.Row(height=GUI_PARAMS['textfield_space_after']),\n                    ]\n                )\n\n            # Test and Save buttons\n            supplier_tab_content.append(\n                ft.Row(\n                    controls=[\n                        ft.ElevatedButton(\n                            'Test',\n                            width=GUI_PARAMS['button_width'],\n                            height=GUI_PARAMS['button_height'],\n                            icon=ft.icons.CHECK_OUTLINED,\n                            on_click=lambda e, s=supplier: self.test_s(e, supplier=s),\n                        ),\n                        ft.ElevatedButton(\n                            'Save',\n                            width=GUI_PARAMS['button_width'],\n                            height=GUI_PARAMS['button_height'],\n                            icon=ft.icons.SAVE_OUTLINED,\n                            on_click=lambda e, s=supplier: self.save_s(e, supplier=s),\n                        ),\n                    ]\n                )\n            )\n\n            supplier_tabs.tabs.append(\n                ft.Tab(\n                    tab_content=ft.Text(supplier, size=16),\n                    content=ft.Container(\n                        ft.Column(\n                            controls=supplier_tab_content,\n                        )\n                    )\n                )\n            )\n\n        self.column.controls.append(supplier_tabs)\n\n\nclass InvenTreeSettingsView(SettingsView):\n    '''InvenTree settings view'''\n\n    title = 'InvenTree Settings'\n    route = '/settings/inventree'\n    settings_file = [\n        global_settings.INVENTREE_CONFIG,\n        global_settings.CONFIG_IPN_PATH,\n    ]\n\n    def save(self, file=None, dialog=True):\n        address = SETTINGS[self.title]['Server Address'][1].value\n        proxy = SETTINGS[self.title]['Proxy'][1].value\n        enable_proxy = SETTINGS[self.title]['Enable Proxy Support'][1].value\n        if not enable_proxy:\n            proxies = None\n        elif address.startswith('https'):\n            proxies = {'https': proxy}\n        else:\n            proxies = {'http': proxy}\n        if file is None:\n            # Save to InvenTree file\n            config_interface.save_inventree_user_settings(\n                enable=global_settings.ENABLE_INVENTREE,\n                server=address,\n                username=SETTINGS[self.title]['Username'][1].value,\n                password=SETTINGS[self.title]['Password or Token'][1].value,\n                enable_proxy=enable_proxy,\n                proxies=proxies,\n                datasheet_upload=SETTINGS[self.title][\n                    'Upload Datasheets to InvenTree'][1].value,\n                pricing_upload=SETTINGS[self.title][\n                    'Upload Pricing Data to InvenTree'][1].value,\n                user_config_path=self.settings_file[0]\n            )\n            # Alert user\n            if dialog:\n                self.show_dialog(\n                    d_type=DialogType.VALID,\n                    message=f'{self.title} successfully saved',\n                )\n        else:\n            super().save(settings_file=file, show_dialog=dialog)\n\n        # Reload InvenTree settings\n        global_settings.load_inventree_settings()\n        # Reload IPN settings\n        global_settings.load_ipn_settings()\n\n    def test(self):\n        from ...database import inventree_interface\n        self.save(dialog=False)\n        connection = inventree_interface.connect_to_server()\n        if connection:\n            self.show_dialog(\n                d_type=DialogType.VALID,\n                message='Sucessfully connected to InvenTree server',\n            )\n        else:\n            self.show_dialog(\n                d_type=DialogType.ERROR,\n                message='Failed to connect to InvenTree server. Check InvenTree credentials are correct and server is running',\n            )\n\n    def __init__(self, page: ft.Page):\n        # Load InvenTree and IPN settings\n        self.settings = {\n            **config_interface.load_inventree_user_settings(self.settings_file[0]),\n            **config_interface.load_file(self.settings_file[1]),\n        }\n        super().__init__(page)\n\n    def build_column(self):\n        ipn_file = self.settings_file[1]\n        ipn_fields = [\n            'Default Part Revision',\n            'Enable Internal Part Number (IPN)',\n            'Use Manufacturer Part Number as IPN',\n            'IPN: Enable Prefix',\n            'IPN: Prefix',\n            'IPN: Enable Category Codes',\n            'IPN: Length of Unique ID',\n            'IPN: Enable Suffix',\n            'IPN: Suffix',\n        ]\n\n        # Tabs\n        inventree_tabs = ft.Tabs(\n            selected_index=0,\n            animation_duration=10,\n            expand=1,\n            tabs=[],\n        )\n        \n        # Build server tab content\n        server_col = ft.Column([ft.Row(height=10)])\n        for name, field in self.fields.items():\n            if name not in ipn_fields:\n                self.update_field(name, field, server_col)\n        self.add_buttons(server_col, test=True)\n\n        # Add InvenTree server tab\n        inventree_tabs.tabs.append(\n            ft.Tab(\n                tab_content=ft.Text('Server', size=16),\n                content=ft.Container(\n                    server_col,\n                )\n            )\n        )\n\n        # Link Proxy Switch to the input field\n        ref = ft.Ref[ft.TextField]()\n        ref.current = SETTINGS[self.title]['Proxy'][1]\n        SETTINGS[self.title]['Enable Proxy Support'][1].refs = [ref]\n\n        # Create IPN fields\n        ipn_fields_ref = ft.Ref[ft.Row]()\n        ipn_fields_col = ft.Column(\n            ref=ipn_fields_ref,\n            controls=[],\n        )\n        for name in ipn_fields:\n            SETTINGS[self.title][name][1].label = name\n            SETTINGS[self.title][name][1].on_change = lambda _: self.save(\n                file=ipn_file,\n                dialog=False,\n            )\n            if name.startswith('IPN: '):\n                ipn_fields_col.controls.append(\n                    ft.Row([SETTINGS[self.title][name][1]])\n                )\n        ipn_manufacturer_part_number_ref = ft.Ref[ft.Row]()\n        ipn_manufacturer_part_number_col = ft.Column(\n            ref=ipn_manufacturer_part_number_ref,\n            controls=[\n                ft.Row([SETTINGS[self.title]['Use Manufacturer Part Number as IPN'][1]]),\n                ft.Row([ipn_fields_col]),\n            ],\n        )\n        \n        # Build IPN tab column\n        ipn_tab_col = ft.Column(\n            [\n                ft.Row(height=10),\n                ft.Row([SETTINGS[self.title]['Default Part Revision'][1]]),\n                ft.Row([SETTINGS[self.title]['Enable Internal Part Number (IPN)'][1]]),\n                ft.Row([ipn_manufacturer_part_number_col]),\n            ]\n        )\n    \n        # Link main IPN switch to corresponding fields\n        main_control = 'Enable Internal Part Number (IPN)'\n        secondary_control = 'Use Manufacturer Part Number as IPN'\n        SETTINGS[self.title][main_control][1].refs = [ipn_manufacturer_part_number_ref]\n        SETTINGS[self.title][main_control][1].on_change = lambda _: self.save(\n            file=ipn_file,\n            dialog=False,\n        )\n\n        # Link Manufacturer Part Number switch to corresponding fields\n        SETTINGS[self.title][secondary_control][1].refs = [ipn_fields_ref]\n        SETTINGS[self.title][secondary_control][1].on_change = lambda _: self.save(\n            file=ipn_file,\n            dialog=False,\n        )\n\n        # Link prefix/suffix switches to corresponding fields\n        for name in ['IPN: Enable Prefix', 'IPN: Enable Suffix']:\n            ref = ft.Ref[ft.TextField]()\n            ref.current = SETTINGS[self.title][name.replace('Enable ', '')][1]\n            SETTINGS[self.title][name][1].refs = [ref]\n\n        # Add IPN tab\n        inventree_tabs.tabs.append(\n            ft.Tab(\n                tab_content=ft.Text('Internal Part Number', size=16),\n                content=ft.Container(\n                    ipn_tab_col,\n                )\n            )\n        )\n\n        # Build column\n        self.column = self.init_column()\n        # Add tabs\n        self.column.controls.append(inventree_tabs)\n\n\nclass KiCadSettingsView(PathSettingsView):\n    '''KiCad settings view'''\n\n    title = 'KiCad Settings'\n    route = '/settings/kicad'\n    settings = global_settings.KICAD_SETTINGS\n    settings_file = global_settings.KICAD_CONFIG_PATHS\n"
  },
  {
    "path": "kintree/kicad/kicad_interface.py",
    "content": "from . import kicad_symbol\n\n\ndef inventree_to_kicad(part_data: dict, library_path: str, template_path=None, show_progress=True) -> bool:\n    ''' Create KiCad symbol from InvenTree part data '''\n    klib = kicad_symbol.ComponentLibManager(library_path)\n    return klib.add_symbol_to_library_from_inventree(\n        symbol_data=part_data,\n        template_path=template_path,\n        show_progress=show_progress\n    )\n"
  },
  {
    "path": "kintree/kicad/kicad_symbol.py",
    "content": "import os\n\nfrom ..config import settings\nfrom ..common import progress\nfrom ..common.tools import cprint\nfrom kiutils.symbol import SymbolLib\n\n\n# KiCad Component Library Manager\nclass ComponentLibManager(object):\n    def __init__(self, library_path):\n        # Load library and template paths\n        cprint(f'[KCAD]\\tlibrary_path: {library_path}', silent=settings.SILENT)\n\n        # Check files exist\n        if not os.path.isfile(library_path):\n            cprint(f'[KCAD]\\tError loading library file ({library_path})', silent=settings.SILENT)\n            return None\n\n        # Load library\n        self.kicad_lib = SymbolLib.from_file(library_path)\n        self.library_name = library_path.split(os.sep)[-1]\n        cprint('[KCAD]\\tNumber of parts in library ' + self.library_name + ': ' + str(len(self.kicad_lib.symbols)), silent=settings.SILENT)\n\n    def is_symbol_in_library(self, symbol_id):\n        ''' Check if symbol already exists in library '''\n        for symbol in self.kicad_lib.symbols:\n            cprint(f'[DBUG]\\t{symbol.libId} ?= {symbol_id}', silent=settings.HIDE_DEBUG)\n            if symbol.libId == symbol_id:\n                cprint(f'[KCAD]\\tWarning: Component {symbol_id} already in library', silent=settings.SILENT)\n                return True\n\n        return False\n\n    def add_symbol_to_library_from_inventree(self, symbol_data, template_path=None, show_progress=True):\n        ''' Create symbol in KiCad library '''\n        part_in_lib = False\n        new_part = False\n        part_name = ''\n        parameters = symbol_data.get('parameters', {})\n        parameters = {**symbol_data, **parameters}\n        key_list = list(parameters.keys())\n        key_list.sort(key=len, reverse=True)\n\n        def replace_wildcards(field):\n            for key in key_list:\n                if key in field:\n                    field = field.replace(key, parameters[key])\n            return field\n\n        symbol_id = symbol_data.get('Symbol', '').split(':')\n        if not symbol_id:\n            cprint('[KCAD] Error: Adding a new symbol to a KiCad library requires the \\'Symbol\\' key with the following format: {lib}:{symbol_id}')\n            return part_in_lib, new_part, part_name\n\n        if not template_path:\n            category = symbol_data['Template'][0]\n            subcategory = symbol_data['Template'][1]\n\n            # Fetch template path\n            try:\n                template_path = settings.symbol_templates_paths[category][subcategory]\n            except:\n                template_path = settings.symbol_templates_paths[category]['Default']\n\n        # Check files exist\n        if not self.kicad_lib:\n            return part_in_lib, new_part\n        if not os.path.isfile(template_path):\n            cprint(f'[KCAD]\\tError loading template file ({template_path})', silent=settings.SILENT)\n            return part_in_lib, new_part, part_name\n\n        # Load template\n        templatelib = SymbolLib.from_file(template_path)\n        # Load new symbol\n        if len(templatelib.symbols) == 1:\n            for symbol in templatelib.symbols:\n                new_symbol = symbol\n        else:\n            cprint('[KCAD]\\tError: Found more than 1 symbol template in template file, aborting', silent=settings.SILENT)\n            return part_in_lib, new_part, part_name\n\n        # Update name/ID\n        part_name = replace_wildcards(new_symbol.libId)\n        new_symbol.libId = part_name\n\n        # Check if part already in library\n        try:\n            is_symbol_in_library = self.is_symbol_in_library(part_name)\n            part_in_lib = True\n        except:\n            is_symbol_in_library = False\n        if is_symbol_in_library:\n            return part_in_lib, new_part, part_name\n\n        # Progress Update\n        if not progress.update_progress_bar(show_progress):\n            return part_in_lib, new_part, part_name\n\n        # Update properties\n        for property in new_symbol.properties:\n            property.value = replace_wildcards(property.value)\n\n        # Add symbol to library\n        self.kicad_lib.symbols.append(new_symbol)\n        # Write library\n        self.kicad_lib.to_file(encoding=\"utf-8\")\n\n        cprint(f'[KCAD]\\tSuccess: Component added to library {self.library_name}', silent=settings.SILENT)\n        part_in_lib = True\n        new_part = True\n\n        # Progress Update\n        if not progress.update_progress_bar(show_progress):\n            pass\n\n        return part_in_lib, new_part, part_name\n"
  },
  {
    "path": "kintree/kicad/templates/LICENSE",
    "content": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n    INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n    HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n  i. the right to reproduce, adapt, distribute, perform, display,\n     communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n     likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n     subject to the limitations in paragraph 4(a), below;\n  v. rights protecting the extraction, dissemination, use and reuse of data\n     in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n     European Parliament and of the Council of 11 March 1996 on the legal\n     protection of databases, and under any national implementation\n     thereof, including any amended or successor version of such\n     directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n     world based on applicable law or treaty, and any national\n     implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n    surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n    warranties of any kind concerning the Work, express, implied,\n    statutory or otherwise, including without limitation warranties of\n    title, merchantability, fitness for a particular purpose, non\n    infringement, or the absence of latent or other defects, accuracy, or\n    the present or absence of errors, whether or not discoverable, all to\n    the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n    that may apply to the Work or any use thereof, including without\n    limitation any person's Copyright and Related Rights in the Work.\n    Further, Affirmer disclaims responsibility for obtaining any necessary\n    consents, permissions or other rights required for any use of the\n    Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n    party to this document and has no duty or obligation with respect to\n    this CC0 or use of the Work.\n"
  },
  {
    "path": "kintree/kicad/templates/capacitor-polarized.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"C\" (at 0 3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Capacitance (Farad)\" \"Value\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Tolerance (%)\" \"Tolerance\" (at 5.08 -3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left) hide)\n    )\n    (property \"Voltage Rated (Volt)\" \"Rated Voltage\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Package Type\" \"Package Type\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Package Size\" \"Package Size\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ESR (Ohm)\" \"ESR\" (at 5.08 -6.35 0)\n      (effects (font (size 1.27 1.27)) (justify left) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy -1.778 -0.381)\n          (xy -1.778 -1.651)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.27 0)\n          (xy -0.635 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.143 -1.016)\n          (xy -2.413 -1.016)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -0.635 -1.905)\n          (xy -0.635 1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.524 0)\n          (xy 0.635 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (arc (start 1.524 1.905) (mid 0.8343 0) (end 1.524 -1.905)\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"Positif\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"Negatif\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/capacitor.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"C\" (at 0 3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -24.13 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Capacitance (Farad)\" \"Value\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Tolerance (%)\" \"Tolerance\" (at 7.62 -3.81 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Temperature Grade\" \"Temperature Grade\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Package Type\" \"Package Type\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy -1.27 0)\n          (xy -1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -0.889 1.905)\n          (xy -0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 0.889 1.905)\n          (xy 0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.905 0)\n          (xy 2.54 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/connector.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"J\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/crystal-2p.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"Y\" (at 0 5.08 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -22.86 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Frequency\" \"Value\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Load Capacitance (Farad)\" \"Load Capacitance\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Package Size\" \"Package Size\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_1_1\"\n      (rectangle (start -1.016 3.048) (end 1.016 -3.048)\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -2.54 0)\n          (xy -1.778 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 2.54 0)\n          (xy 1.778 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.778 2.54)\n          (xy -1.778 0)\n          (xy -1.778 -2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 2.54)\n          (xy 1.778 0)\n          (xy 1.778 -2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (pin passive line (at -5.08 0 0) (length 2.54)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 5.08 0 180) (length 2.54)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/default.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"DES\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/diode-led.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"D\" (at 0 3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -22.86 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Forward Voltage (Volt)\" \"Forward Voltage\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"LED Color\" \"LED Color\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 -1.27)\n          (xy 1.27 1.27)\n        )\n        (stroke (width 0.381) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.27 1.27)\n          (xy -1.27 -1.27)\n          (xy 1.27 0)\n          (xy -1.27 1.27)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type background))\n      )\n      (polyline\n        (pts\n          (xy -0.889 -1.778)\n          (xy 0.508 -3.175)\n          (xy 0.254 -2.413)\n          (xy -0.254 -2.921)\n          (xy 0.508 -3.175)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type outline))\n      )\n      (polyline\n        (pts\n          (xy 0.635 -1.651)\n          (xy 2.032 -3.048)\n          (xy 1.27 -2.794)\n          (xy 1.778 -2.286)\n          (xy 2.032 -3.048)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type outline))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"Cathode\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"Anode\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/diode-schottky.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"D\" (at 0 3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -24.13 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Forward Voltage (Volt)\" \"Forward Voltage\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Current (Amps)\" \"Rated Current\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 -1.397)\n          (xy 2.159 -1.397)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 -1.397)\n          (xy 1.27 1.397)\n          (xy 0.381 1.397)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.27 1.27)\n          (xy -1.27 -1.27)\n          (xy 1.27 0)\n          (xy -1.27 1.27)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type background))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"Cathode\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"Anode\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/diode-standard.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"D\" (at 0 3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -24.13 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Forward Voltage (Volt)\" \"Forward Voltage\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Current (Amps)\" \"Rated Current\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 -1.397)\n          (xy 1.27 1.397)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.27 1.27)\n          (xy -1.27 -1.27)\n          (xy 1.27 0)\n          (xy -1.27 1.27)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type background))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"Cathode\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"Anode\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/diode-zener.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"D\" (at 0 3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -24.13 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Forward Voltage (Volt)\" \"Forward Voltage\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Power (Watts)\" \"Rated Power\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 -1.397)\n          (xy 2.159 -1.778)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 -1.397)\n          (xy 1.27 1.397)\n          (xy 0.381 1.778)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.27 1.27)\n          (xy -1.27 -1.27)\n          (xy 1.27 0)\n          (xy -1.27 1.27)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type background))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"Cathode\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"Anode\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/eeprom-sot23.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (in_bom yes) (on_board yes)\n    (property \"Reference\" \"U\" (at 0 8.89 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (rectangle (start -5.08 7.62) (end 5.08 -7.62)\n        (stroke (width 0.254) (type default))\n        (fill (type background))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at -7.62 0 0) (length 2.54)\n        (name \"SCL\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin power_in line (at 7.62 -5.08 180) (length 2.54)\n        (name \"VSS\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -7.62 -5.08 0) (length 2.54)\n        (name \"SDA\" (effects (font (size 1.27 1.27))))\n        (number \"3\" (effects (font (size 1.27 1.27))))\n      )\n      (pin power_in line (at -7.62 5.08 0) (length 2.54)\n        (name \"VCC\" (effects (font (size 1.27 1.27))))\n        (number \"4\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 7.62 5.08 180) (length 2.54)\n        (name \"WP\" (effects (font (size 1.27 1.27))))\n        (number \"5\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/ferrite-bead.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"FB\" (at 0 2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Inductance (Henry)\" \"Value\" (at 0 -2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Current Rating (Ampere)\" \"Rated Current\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ESR (Ohm)\" \"ESR\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy -4.318 0)\n          (xy -4.064 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.27 0)\n          (xy 1.27 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 4.064 0)\n          (xy 4.318 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -2.54 1.27)\n          (xy -2.54 -1.27)\n          (xy 2.54 -1.27)\n          (xy 2.54 1.27)\n          (xy -2.54 1.27)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at -5.08 0 0) (length 2.54)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 5.08 0 180) (length 2.54)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/fuse.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"F\" (at 0 2.794 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Current Rating (A)\" \"Rated Current\" (at 0 -2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Voltage Rating (V)\" \"Rated Voltage\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Package Type\" \"Package Type\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (rectangle (start -3.81 0) (end -3.048 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (arc (start 0 0) (mid 1.524 -1.5174) (end 3.048 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (rectangle (start 3.048 0) (end 3.81 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (arc (start 0 0) (mid -1.524 1.5174) (end -3.048 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (pin passive line (at -5.08 0 0) (length 1.27)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 5.08 0 180) (length 1.27)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/inductor.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"L\" (at 0 2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Inductance (Henry)\" \"Value\" (at 0 -1.27 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Current Rating (Ampere)\" \"Rated Current\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ESR (Ohm)\" \"ESR\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (arc (start -1.905 0) (mid -2.8575 0.9399) (end -3.81 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (arc (start 0 0) (mid -0.9525 0.9399) (end -1.905 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (arc (start 1.905 0) (mid 0.9525 0.9399) (end 0 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (arc (start 3.81 0) (mid 2.8575 0.9399) (end 1.905 0)\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at -6.35 0 0) (length 2.54)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 6.35 0 180) (length 2.54)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/integrated-circuit.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"U\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/library_template.kicad_sym",
    "content": "(kicad_symbol_lib (version 20211014) (generator kicad_converter))"
  },
  {
    "path": "kintree/kicad/templates/oscillator-4p.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (in_bom yes) (on_board yes)\n    (property \"Reference\" \"Y\" (at 0 6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -24.13 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Part Number\" \"Value\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Package Size\" \"Package Size\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_1_1\"\n      (rectangle (start -5.08 5.08) (end 5.08 -5.08)\n        (stroke (width 0) (type default))\n        (fill (type background))\n      )\n      (pin input line (at -10.16 -2.54 0) (length 5.08)\n        (name \"OE\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin power_out line (at 10.16 -2.54 180) (length 5.08)\n        (name \"GND\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n      (pin output line (at 10.16 2.54 180) (length 5.08)\n        (name \"OUT\" (effects (font (size 1.27 1.27))))\n        (number \"3\" (effects (font (size 1.27 1.27))))\n      )\n      (pin power_in line (at -10.16 2.54 0) (length 5.08)\n        (name \"VDD\" (effects (font (size 1.27 1.27))))\n        (number \"4\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/protection-unidir.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"D\" (at 0 3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -13.97 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -21.59 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -24.13 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -16.51 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -19.05 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Standoff Voltage\" \"Standoff Voltage\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Breakdown Voltage\" \"Breakdown Voltage\" (at 0 -8.89 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Peak Power (Watts)\" \"Rated Power\" (at 0 -11.43 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 -1.397)\n          (xy 2.159 -1.778)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 -1.397)\n          (xy 1.27 1.397)\n          (xy 0.381 1.778)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -1.27 1.27)\n          (xy -1.27 -1.27)\n          (xy 1.27 0)\n          (xy -1.27 1.27)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type background))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"Cathode\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"Anode\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/resistor-sm.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"R\" (at 0 2.032 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Resistance (Ohms)\" \"Value\" (at 0 -2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Tolerance (%)\" \"Tolerance\" (at 3.81 -2.54 0)\n      (effects (font (size 1.27 1.27)) (justify left) hide)\n    )\n    (property \"Package Type\" \"Package Type\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Power (Watts)\" \"Rated Power\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy -3.81 0)\n          (xy -3.048 0)\n          (xy -2.54 0.762)\n          (xy -1.524 -0.762)\n          (xy -0.508 0.762)\n          (xy 0.508 -0.762)\n          (xy 1.524 0.762)\n          (xy 2.54 -0.762)\n          (xy 3.048 0)\n          (xy 3.81 0)\n          (xy 3.81 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at -5.08 0 0) (length 1.27)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 5.08 0 180) (length 1.27)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/resistor.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"R\" (at 0 2.032 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Resistance (Ohms)\" \"Value\" (at 0 -2.54 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Tolerance (%)\" \"Tolerance\" (at 3.81 -2.54 0)\n      (effects (font (size 1.27 1.27)) (justify left) hide)\n    )\n    (property \"Package Type\" \"Package Type\" (at 0 -5.08 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Power (Watts)\" \"Rated Power\" (at 0 -7.62 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy -3.81 0)\n          (xy -3.048 0)\n          (xy -2.54 0.762)\n          (xy -1.524 -0.762)\n          (xy -0.508 0.762)\n          (xy 0.508 -0.762)\n          (xy 1.524 0.762)\n          (xy 2.54 -0.762)\n          (xy 3.048 0)\n          (xy 3.81 0)\n          (xy 3.81 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin passive line (at -5.08 0 0) (length 1.27)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 5.08 0 180) (length 1.27)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/transistor-nfet.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"Q\" (at 0 4.318 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 7.62 3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 7.62 1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Current (Amps)\" \"Rated Current\" (at 7.62 -1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Package Type\" \"Package Type\" (at 7.62 -3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.27 -2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.27 2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 -1.27)\n          (xy 1.778 -2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 -0.635)\n          (xy 1.778 0.635)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 1.27)\n          (xy 1.778 2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.81 -2.54)\n          (xy 3.81 -1.905)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 4.953 0.508)\n          (xy 6.223 0.508)\n        )\n        (stroke (width 0.127) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 5.08 -0.635)\n          (xy 4.953 -0.635)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 1.905)\n          (xy 3.81 1.905)\n          (xy 3.81 2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 5.588 -0.635)\n          (xy 5.588 -2.413)\n          (xy 3.81 -2.413)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 5.588 0.635)\n          (xy 5.588 2.286)\n          (xy 3.81 2.286)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 -1.905)\n          (xy 3.81 -1.905)\n          (xy 3.81 0)\n          (xy 3.048 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.905 0)\n          (xy 3.048 0.635)\n          (xy 3.048 -0.635)\n          (xy 1.905 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type outline))\n      )\n      (polyline\n        (pts\n          (xy 4.953 -0.635)\n          (xy 5.588 0.508)\n          (xy 6.223 -0.635)\n          (xy 5.08 -0.635)\n        )\n        (stroke (width 0) (type default))\n        (fill (type outline))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin bidirectional line (at 3.81 7.62 270) (length 5.08)\n        (name \"D\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin input line (at -3.81 0 0) (length 5.08)\n        (name \"G\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin bidirectional line (at 3.81 -7.62 90) (length 5.08)\n        (name \"S\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/transistor-npn.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"Q\" (at 0 4.318 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 5.08 3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 5.08 1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Current (Amps)\" \"Rated Current\" (at 5.08 -1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Package Type\" \"Package Type\" (at 5.08 -3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 2.286)\n          (xy 1.27 -2.286)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.81 -2.54)\n          (xy 1.27 -1.016)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.81 2.54)\n          (xy 1.27 1.016)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.81 -2.54)\n          (xy 3.175 -1.27)\n          (xy 2.413 -2.413)\n          (xy 3.81 -2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type outline))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin input line (at -3.81 0 0) (length 5.08)\n        (name \"B\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin input line (at 3.81 7.62 270) (length 5.08)\n        (name \"C\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin output line (at 3.81 -7.62 90) (length 5.08)\n        (name \"E\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/transistor-pfet.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"Q\" (at 0 4.318 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 7.62 3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 7.62 1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Current (Amps)\" \"Rated Current\" (at 7.62 -1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Package Type\" \"Package Type\" (at 7.62 -3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.27 -2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.27 2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 -1.905)\n          (xy 3.81 -1.905)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 -1.27)\n          (xy 1.778 -2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 -0.635)\n          (xy 1.778 0.635)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 0)\n          (xy 2.667 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 1.27)\n          (xy 1.778 2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.81 -2.54)\n          (xy 3.81 -1.905)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 4.953 -0.635)\n          (xy 6.223 -0.635)\n        )\n        (stroke (width 0.127) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 5.08 0.508)\n          (xy 4.953 0.508)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.778 1.905)\n          (xy 3.81 1.905)\n          (xy 3.81 2.54)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.683 0)\n          (xy 3.81 0)\n          (xy 3.81 -1.905)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 5.588 -0.635)\n          (xy 5.588 -2.413)\n          (xy 3.81 -2.413)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 5.588 0.508)\n          (xy 5.588 2.286)\n          (xy 3.81 2.286)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.683 0)\n          (xy 2.667 0.762)\n          (xy 2.667 -0.762)\n          (xy 3.683 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type outline))\n      )\n      (polyline\n        (pts\n          (xy 4.953 0.508)\n          (xy 5.588 -0.635)\n          (xy 6.223 0.508)\n          (xy 5.08 0.508)\n        )\n        (stroke (width 0) (type default))\n        (fill (type outline))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin bidirectional line (at 3.81 7.62 270) (length 5.08)\n        (name \"D\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin input line (at -3.81 0 0) (length 5.08)\n        (name \"G\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin bidirectional line (at 3.81 -7.62 90) (length 5.08)\n        (name \"S\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates/transistor-pnp.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"IPN\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"Q\" (at 0 4.318 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Value\" \"IPN\" (at 0 -10.16 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Footprint\" \"Footprint\" (at 0 -17.78 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Datasheet\" \"inventree_url\" (at 0 -20.32 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer\" \"Manufacturer\" (at 0 -12.7 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"MPN\" (at 0 -15.24 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Part Number\" \"Value\" (at 5.08 3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Voltage (Volt)\" \"Rated Voltage\" (at 5.08 1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Rated Current (Amps)\" \"Rated Current\" (at 5.08 -1.27 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"Package Type\" \"Package Type\" (at 5.08 -3.81 0)\n      (effects (font (size 1.27 1.27)) (justify left))\n    )\n    (property \"ki_keywords\" \"keywords\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"description\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"IPN_0_1\"\n      (polyline\n        (pts\n          (xy 1.27 -2.286)\n          (xy 1.27 2.286)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.81 -2.54)\n          (xy 1.27 -1.016)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 3.81 2.54)\n          (xy 1.27 1.016)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.397 -1.016)\n          (xy 2.032 -2.286)\n          (xy 2.794 -1.143)\n          (xy 1.397 -1.016)\n        )\n        (stroke (width 0) (type default))\n        (fill (type outline))\n      )\n    )\n    (symbol \"IPN_1_1\"\n      (pin input line (at -3.81 0 0) (length 5.08)\n        (name \"B\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin output line (at 3.81 7.62 270) (length 5.08)\n        (name \"C\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n      (pin input line (at 3.81 -7.62 90) (length 5.08)\n        (name \"E\" (effects (font (size 1.27 1.27))))\n        (number \"~\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "kintree/kicad/templates_project/templates_project.kicad_pcb",
    "content": "(kicad_pcb (version 20221018) (generator pcbnew)\n\n  (general\n    (thickness 1.6)\n  )\n\n  (paper \"A4\")\n  (layers\n    (0 \"F.Cu\" signal)\n    (31 \"B.Cu\" signal)\n    (32 \"B.Adhes\" user \"B.Adhesive\")\n    (33 \"F.Adhes\" user \"F.Adhesive\")\n    (34 \"B.Paste\" user)\n    (35 \"F.Paste\" user)\n    (36 \"B.SilkS\" user \"B.Silkscreen\")\n    (37 \"F.SilkS\" user \"F.Silkscreen\")\n    (38 \"B.Mask\" user)\n    (39 \"F.Mask\" user)\n    (40 \"Dwgs.User\" user \"User.Drawings\")\n    (41 \"Cmts.User\" user \"User.Comments\")\n    (42 \"Eco1.User\" user \"User.Eco1\")\n    (43 \"Eco2.User\" user \"User.Eco2\")\n    (44 \"Edge.Cuts\" user)\n    (45 \"Margin\" user)\n    (46 \"B.CrtYd\" user \"B.Courtyard\")\n    (47 \"F.CrtYd\" user \"F.Courtyard\")\n    (48 \"B.Fab\" user)\n    (49 \"F.Fab\" user)\n    (50 \"User.1\" user)\n    (51 \"User.2\" user)\n    (52 \"User.3\" user)\n    (53 \"User.4\" user)\n    (54 \"User.5\" user)\n    (55 \"User.6\" user)\n    (56 \"User.7\" user)\n    (57 \"User.8\" user)\n    (58 \"User.9\" user)\n  )\n\n  (setup\n    (pad_to_mask_clearance 0)\n    (pcbplotparams\n      (layerselection 0x00010fc_ffffffff)\n      (plot_on_all_layers_selection 0x0000000_00000000)\n      (disableapertmacros false)\n      (usegerberextensions false)\n      (usegerberattributes true)\n      (usegerberadvancedattributes true)\n      (creategerberjobfile true)\n      (dashed_line_dash_ratio 12.000000)\n      (dashed_line_gap_ratio 3.000000)\n      (svgprecision 4)\n      (plotframeref false)\n      (viasonmask false)\n      (mode 1)\n      (useauxorigin false)\n      (hpglpennumber 1)\n      (hpglpenspeed 20)\n      (hpglpendiameter 15.000000)\n      (dxfpolygonmode true)\n      (dxfimperialunits true)\n      (dxfusepcbnewfont true)\n      (psnegative false)\n      (psa4output false)\n      (plotreference true)\n      (plotvalue true)\n      (plotinvisibletext false)\n      (sketchpadsonfab false)\n      (subtractmaskfromsilk false)\n      (outputformat 1)\n      (mirror false)\n      (drillshape 1)\n      (scaleselection 1)\n      (outputdirectory \"\")\n    )\n  )\n\n  (net 0 \"\")\n\n)\n"
  },
  {
    "path": "kintree/kicad/templates_project/templates_project.kicad_prl",
    "content": "{\n  \"board\": {\n    \"active_layer\": 0,\n    \"active_layer_preset\": \"\",\n    \"auto_track_width\": true,\n    \"hidden_netclasses\": [],\n    \"hidden_nets\": [],\n    \"high_contrast_mode\": 0,\n    \"net_color_mode\": 1,\n    \"opacity\": {\n      \"images\": 0.6,\n      \"pads\": 1.0,\n      \"tracks\": 1.0,\n      \"vias\": 1.0,\n      \"zones\": 0.6\n    },\n    \"ratsnest_display_mode\": 0,\n    \"selection_filter\": {\n      \"dimensions\": true,\n      \"footprints\": true,\n      \"graphics\": true,\n      \"keepouts\": true,\n      \"lockedItems\": true,\n      \"otherItems\": true,\n      \"pads\": true,\n      \"text\": true,\n      \"tracks\": true,\n      \"vias\": true,\n      \"zones\": true\n    },\n    \"visible_items\": [\n      0,\n      1,\n      2,\n      3,\n      4,\n      5,\n      8,\n      9,\n      10,\n      11,\n      12,\n      13,\n      14,\n      15,\n      16,\n      17,\n      18,\n      19,\n      20,\n      21,\n      22,\n      23,\n      24,\n      25,\n      26,\n      27,\n      28,\n      29,\n      30,\n      32,\n      33,\n      34,\n      35,\n      36\n    ],\n    \"visible_layers\": \"fffffff_ffffffff\",\n    \"zone_display_mode\": 0\n  },\n  \"meta\": {\n    \"filename\": \"templates_project.kicad_prl\",\n    \"version\": 3\n  },\n  \"project\": {\n    \"files\": []\n  }\n}\n"
  },
  {
    "path": "kintree/kicad/templates_project/templates_project.kicad_pro",
    "content": "{\n  \"board\": {\n    \"3dviewports\": [],\n    \"design_settings\": {\n      \"defaults\": {\n        \"board_outline_line_width\": 0.1,\n        \"copper_line_width\": 0.2,\n        \"copper_text_size_h\": 1.5,\n        \"copper_text_size_v\": 1.5,\n        \"copper_text_thickness\": 0.3,\n        \"other_line_width\": 0.15,\n        \"silk_line_width\": 0.15,\n        \"silk_text_size_h\": 1.0,\n        \"silk_text_size_v\": 1.0,\n        \"silk_text_thickness\": 0.15\n      },\n      \"diff_pair_dimensions\": [],\n      \"drc_exclusions\": [],\n      \"rules\": {\n        \"solder_mask_clearance\": 0.0,\n        \"solder_mask_min_width\": 0.0\n      },\n      \"track_widths\": [],\n      \"via_dimensions\": []\n    },\n    \"layer_presets\": [],\n    \"viewports\": []\n  },\n  \"boards\": [],\n  \"cvpcb\": {\n    \"equivalence_files\": []\n  },\n  \"erc\": {\n    \"erc_exclusions\": [],\n    \"meta\": {\n      \"version\": 0\n    },\n    \"pin_map\": [\n      [\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        0,\n        0,\n        0,\n        0,\n        2\n      ],\n      [\n        0,\n        2,\n        0,\n        1,\n        0,\n        0,\n        1,\n        0,\n        2,\n        2,\n        2,\n        2\n      ],\n      [\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        0,\n        1,\n        0,\n        1,\n        2\n      ],\n      [\n        0,\n        1,\n        0,\n        0,\n        0,\n        0,\n        1,\n        1,\n        2,\n        1,\n        1,\n        2\n      ],\n      [\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        0,\n        0,\n        0,\n        0,\n        2\n      ],\n      [\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        2\n      ],\n      [\n        1,\n        1,\n        1,\n        1,\n        1,\n        0,\n        1,\n        1,\n        1,\n        1,\n        1,\n        2\n      ],\n      [\n        0,\n        0,\n        0,\n        1,\n        0,\n        0,\n        1,\n        0,\n        0,\n        0,\n        0,\n        2\n      ],\n      [\n        0,\n        2,\n        1,\n        2,\n        0,\n        0,\n        1,\n        0,\n        2,\n        2,\n        2,\n        2\n      ],\n      [\n        0,\n        2,\n        0,\n        1,\n        0,\n        0,\n        1,\n        0,\n        2,\n        0,\n        0,\n        2\n      ],\n      [\n        0,\n        2,\n        1,\n        1,\n        0,\n        0,\n        1,\n        0,\n        2,\n        0,\n        0,\n        2\n      ],\n      [\n        2,\n        2,\n        2,\n        2,\n        2,\n        2,\n        2,\n        2,\n        2,\n        2,\n        2,\n        2\n      ]\n    ],\n    \"rule_severities\": {\n      \"bus_definition_conflict\": \"error\",\n      \"bus_entry_needed\": \"error\",\n      \"bus_to_bus_conflict\": \"error\",\n      \"bus_to_net_conflict\": \"error\",\n      \"conflicting_netclasses\": \"error\",\n      \"different_unit_footprint\": \"error\",\n      \"different_unit_net\": \"error\",\n      \"duplicate_reference\": \"error\",\n      \"duplicate_sheet_names\": \"error\",\n      \"endpoint_off_grid\": \"warning\",\n      \"extra_units\": \"error\",\n      \"global_label_dangling\": \"warning\",\n      \"hier_label_mismatch\": \"error\",\n      \"label_dangling\": \"error\",\n      \"lib_symbol_issues\": \"warning\",\n      \"missing_bidi_pin\": \"warning\",\n      \"missing_input_pin\": \"warning\",\n      \"missing_power_pin\": \"error\",\n      \"missing_unit\": \"warning\",\n      \"multiple_net_names\": \"warning\",\n      \"net_not_bus_member\": \"warning\",\n      \"no_connect_connected\": \"warning\",\n      \"no_connect_dangling\": \"warning\",\n      \"pin_not_connected\": \"error\",\n      \"pin_not_driven\": \"error\",\n      \"pin_to_pin\": \"warning\",\n      \"power_pin_not_driven\": \"error\",\n      \"similar_labels\": \"warning\",\n      \"simulation_model_issue\": \"error\",\n      \"unannotated\": \"error\",\n      \"unit_value_mismatch\": \"error\",\n      \"unresolved_variable\": \"error\",\n      \"wire_dangling\": \"error\"\n    }\n  },\n  \"libraries\": {\n    \"pinned_footprint_libs\": [],\n    \"pinned_symbol_libs\": []\n  },\n  \"meta\": {\n    \"filename\": \"templates_project.kicad_pro\",\n    \"version\": 1\n  },\n  \"net_settings\": {\n    \"classes\": [\n      {\n        \"bus_width\": 12,\n        \"clearance\": 0.2,\n        \"diff_pair_gap\": 0.25,\n        \"diff_pair_via_gap\": 0.25,\n        \"diff_pair_width\": 0.2,\n        \"line_style\": 0,\n        \"microvia_diameter\": 0.3,\n        \"microvia_drill\": 0.1,\n        \"name\": \"Default\",\n        \"pcb_color\": \"rgba(0, 0, 0, 0.000)\",\n        \"schematic_color\": \"rgba(0, 0, 0, 0.000)\",\n        \"track_width\": 0.25,\n        \"via_diameter\": 0.8,\n        \"via_drill\": 0.4,\n        \"wire_width\": 6\n      }\n    ],\n    \"meta\": {\n      \"version\": 3\n    },\n    \"net_colors\": null,\n    \"netclass_assignments\": null,\n    \"netclass_patterns\": []\n  },\n  \"pcbnew\": {\n    \"last_paths\": {\n      \"gencad\": \"\",\n      \"idf\": \"\",\n      \"netlist\": \"\",\n      \"specctra_dsn\": \"\",\n      \"step\": \"\",\n      \"vrml\": \"\"\n    },\n    \"page_layout_descr_file\": \"\"\n  },\n  \"schematic\": {\n    \"annotate_start_num\": 0,\n    \"drawing\": {\n      \"dashed_lines_dash_length_ratio\": 12.0,\n      \"dashed_lines_gap_length_ratio\": 3.0,\n      \"default_line_thickness\": 6.0,\n      \"default_text_size\": 50.0,\n      \"field_names\": [],\n      \"intersheets_ref_own_page\": false,\n      \"intersheets_ref_prefix\": \"\",\n      \"intersheets_ref_short\": false,\n      \"intersheets_ref_show\": false,\n      \"intersheets_ref_suffix\": \"\",\n      \"junction_size_choice\": 3,\n      \"label_size_ratio\": 0.25,\n      \"pin_symbol_size\": 25.0,\n      \"text_offset_ratio\": 0.08\n    },\n    \"legacy_lib_dir\": \"\",\n    \"legacy_lib_list\": [],\n    \"meta\": {\n      \"version\": 1\n    },\n    \"net_format_name\": \"\",\n    \"page_layout_descr_file\": \"\",\n    \"plot_directory\": \"\",\n    \"spice_current_sheet_as_root\": false,\n    \"spice_external_command\": \"spice \\\"%I\\\"\",\n    \"spice_model_current_sheet_as_root\": true,\n    \"spice_save_all_currents\": false,\n    \"spice_save_all_voltages\": false,\n    \"subpart_first_id\": 65,\n    \"subpart_id_separator\": 0\n  },\n  \"sheets\": [\n    [\n      \"b588025f-8c23-406b-a049-ad8593913ab0\",\n      \"\"\n    ]\n  ],\n  \"text_variables\": {}\n}\n"
  },
  {
    "path": "kintree/kicad/templates_project/templates_project.kicad_sch",
    "content": "(kicad_sch (version 20230121) (generator eeschema)\n\n  (uuid b588025f-8c23-406b-a049-ad8593913ab0)\n\n  (paper \"A4\")\n\n  (lib_symbols\n  )\n\n\n  (sheet_instances\n    (path \"/\" (page \"1\"))\n  )\n)\n"
  },
  {
    "path": "kintree/kintree_gui.py",
    "content": "import flet as ft\n\nfrom .gui.gui import kintree_gui\n\n\ndef main(view='flet_app'):\n    if view == 'browser':\n        ft.app(target=kintree_gui, view=ft.AppView.WEB_BROWSER)\n        return\n    ft.app(target=kintree_gui, view=ft.AppView.FLET_APP)\n"
  },
  {
    "path": "kintree/search/automationdirect_api.py",
    "content": "from ..common.tools import download\n\n# These are the 'keys' we want to pull out response\nSEARCH_HEADERS = [\n    'item_code',          # name\n    'primary_desc',       # description\n    'revision',           # revision\n    'keywords',           # keywords\n    'item_code',          # suppli er_part_number\n    'manufacturer_name',  # manufacturer_name\n    'item_code',          # manufacturer_part_number\n    'url_fullpath',       # supplier_link\n    'spec_url',           # datasheet\n    'image_file_name',    # image\n\n    'insert_url',         # insert PD\n    'orderable_flg',\n    'prod_status',\n    'price',\n    'manual_url',         # not full path to html page, value is filename.html\n    'unit_of_measure',\n    'leadtime_cd',\n    'production_time',\n    'warranty',\n]\n\nPARAMETERS_MAP = [\n    'tech_attributes',  # List of parameters, not list of dictionaries, changes based on product returned\n]\n\nPRICING_MAP = [\n    'ordering_attributes',  # List, e.g. ['Is Cut To Length: True', 'Maximum Cut Length: 2500', 'Minimum Cut Length: 25']\n    'price',                # Automation Direct only has one price, no price breaks\n    'unit_of_measure',      # e.g. 'FT'\n]\n\n\ndef get_default_search_keys():\n    return [\n        # Order matters\n        'item_code',          # name\n        'primary_desc',       # description\n        'revision',           # revision\n        'keywords',           # keywords\n        'item_code',          # supplier_part_number\n        'manufacturer_name',  # manufacturer_name\n        'item_code',          # manufacturer_part_number\n        'url_fullpath',       # supplier_link\n        'spec_url',           # datasheet\n        'image_file_name',    # image\n    ]\n\n\ndef find_categories(part_details: str):\n    ''' Find categories '''\n    try:\n        return part_details['parentCatalogName'], part_details['catalogName']\n    except:\n        return None, None\n\n\ndef fetch_part_info(part_number: str, silent=False) -> dict:\n    ''' Fetch part data from API '''\n\n    # Load Automation Direct settingss\n    import re\n    from ..common.tools import cprint\n    from ..config import settings, config_interface\n    automationdirect_api_settings = config_interface.load_file(settings.CONFIG_AUTOMATIONDIRECT_API)\n\n    part_info = {}\n\n    def search_timeout(timeout=10):\n        url = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_URL', '') + automationdirect_api_settings.get('AUTOMATIONDIRECT_API_SEARCH_QUERY', '') + part_number + automationdirect_api_settings.get('AUTOMATIONDIRECT_API_SEARCH_STRING', '') + part_number\n        response = download(url, timeout=timeout)\n        return response\n\n    # Query part number\n    try:\n        part = search_timeout()\n        part = part['solrResult']['response']   # extract the data for parts returned\n        if part['numFound'] > 0:\n            if part['numFound'] == 1:\n                cprint(f'[INFO]\\tFound exactly one result for \"{part_number}\"', silent=True)\n            else:\n                cprint(f'[INFO]\\tFound {part[\"numFound\"]} results for \"{part_number}\", selecting first result', silent=False)\n            part = part['docs'][0]              # choose the first part in the returned returned list\n        else:\n            part = None\n    except Exception as e:\n        cprint(f'[INFO]\\tError: fetch_part_info(): {repr(e)}')\n        part = None\n\n    if not part:\n        return part_info\n\n    category, subcategory = find_categories(part)\n    try:\n        part_info['category'] = category\n        part_info['subcategory'] = subcategory\n    except:\n        part_info['category'] = ''\n        part_info['subcategory'] = ''\n\n    headers = SEARCH_HEADERS  # keys we want to search for\n\n    # Get all returned data we want\n    for key in part:\n        if key in headers:\n            if key == 'image_file_name':  # JSON only returns image name, need to add path\n                try:\n                    part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_IMAGE_PATH', '') + part['image_file_name']\n                except IndexError:\n                    pass\n            elif key == 'spec_url':  # datasheet url returns partial path, need to add ROOT URL\n                try:\n                    part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + part['spec_url']\n                except IndexError:\n                    pass\n            elif key == 'insert_url':  # insert url returns partial path, need to add ROOT URL\n                try:\n                    part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + part['insert_url']\n                except IndexError:\n                    pass\n            elif key == 'manual_url':  # manul url returns .html file name, need to build the rest of the URL\n                try:\n                    part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + '/static/manuals/' + str(part['manual_url']).rsplit('.', 1)[0] + '/' + part['manual_url']\n                except IndexError:\n                    pass\n            elif key == 'url_fullpath':  # despite being named fullpath, the URL needs the TLD as a prefix\n                try:\n                    part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + '/adc/shopping' + str(part['url_fullpath'])\n                except IndexError:\n                    pass\n            elif key == 'manufacturer_name':  # taken care of in parameter list below\n                pass\n            else:\n                part_info[key] = part[key]\n\n    # Parameters\n    part_info['parameters'] = {}\n    [parameter_key] = PARAMETERS_MAP\n\n    if part.get(parameter_key, ''):\n        for attribute in part[parameter_key]:\n            attribute_list = [x.strip() for x in attribute.split(':')]\n            parameter_name = attribute_list[0]\n            parameter_name = parameter_name.replace('/', '')\n            parameter_value = attribute_list[1]\n            try:\n                html_li_list = re.split(r\"</?\\s*[a-z-][^>]*\\s*>|(\\&(?:[\\w\\d]+|#\\d+|#x[a-f\\d]+);)\", parameter_value)\n                cleaned_html_li_list = list(filter(None, html_li_list))\n                parameter_value = ', '.join(cleaned_html_li_list)\n            except Exception as e:\n                print(f'{repr(e)}')\n            if parameter_name == \"Brand\":  # Manufacturer Name returned as a parameter, pick it out of parameters list aand store it appropriately\n                part_info['manufacturer_name'] = parameter_value\n            # Nominal Input Voltage gives range min-max, parse it out to put in min/max params\n            if parameter_name == \"Nominal Input Voltage\":\n                if parameter_value.count('-') == 1:\n                    parameter_value = re.sub(r'[^\\d-]+', '', parameter_value)\n                    values_list = parameter_value.split('-')\n                    min_value = min(values_list)\n                    max_value = max(values_list)\n                    part_info['parameters']['Min Input Voltage'] = min_value\n                    part_info['parameters']['Max Input Voltage'] = max_value\n                else:\n                    # more than one range, copy into set param fields\n                    part_info['parameters']['Min Input Voltage'] = parameter_value\n                    part_info['parameters']['Max Input Voltage'] = parameter_value\n            # Nominal Output Voltage gives range min-max, parse it out to put in min/max params\n            if parameter_name == \"Nominal Output Voltage\":\n                if parameter_value.count('-') == 1:\n                    parameter_value = re.sub(r'[^\\d-]+', '', parameter_value)\n                    values_list = parameter_value.split('-')\n                    min_value = min(values_list)\n                    max_value = max(values_list)\n                    part_info['parameters']['Min Output Voltage'] = min_value\n                    part_info['parameters']['Max Output Voltage'] = max_value\n                else:\n                    # more than one range, copy into set param fields\n                    part_info['parameters']['Min Output Voltage'] = parameter_value\n                    part_info['parameters']['Max Output Voltage'] = parameter_value\n            else:\n                # Append to parameters dictionary\n                part_info['parameters'][parameter_name] = parameter_value\n\n    # Pricing\n    part_info['pricing'] = {}\n    [ordering_attributes, price_key, unit_per_price] = PRICING_MAP\n\n    # Parse out ordering attributes\n    pricing_attributes = {}\n    price_per_unit = part.get(price_key, '0')\n    try:\n        for attribute in part[ordering_attributes]:\n            attribute = attribute.split(':')\n            attribute = [x.strip() for x in attribute]\n            pricing_attributes[str(attribute[0])] = attribute[1]\n\n        min_quantity = int(pricing_attributes['Minimum Cut Length'])\n        max_quanitity = int(pricing_attributes['Maximum Cut Length'])\n        \n        price_per_unit = part[price_key]\n\n        # Automation Direct doesn't have price breaks, but we can create common set quanitities for reference\n        quantities = [100, 250, 500, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 12000, 14000, 15000]\n        quantities.insert(0, min_quantity)\n        quantities.append(max_quanitity)\n        quantities.sort()\n        quantities = [qty for qty in quantities if qty <= max_quanitity]\n        for i in range(len(quantities) - 1):\n            part_info['pricing'][quantities[i]] = price_per_unit\n    \n    except KeyError as e:\n        from ..common.tools import cprint\n        cprint(f'[INFO]\\tNo pricing attribute \"{e.args[0]}\" found for \"{part_number}\"', silent=silent)\n        part_info['pricing']['1'] = price_per_unit\n\n    part_info['currency'] = 'USD'\n\n    # Extra search fields\n    if settings.CONFIG_AUTOMATIONDIRECT.get('EXTRA_FIELDS', None):\n        for extra_field in settings.CONFIG_AUTOMATIONDIRECT['EXTRA_FIELDS']:\n            if part.get(extra_field, None):\n                part_info['parameters'][extra_field] = part[extra_field]\n            else:\n                from ..common.tools import cprint\n                cprint(f'[INFO]\\tWarning: Extra field \"{extra_field}\" not found in search results', silent=False)\n\n    return part_info\n\n\ndef test_api() -> bool:\n    ''' Test method for API '''\n\n    test_success = True\n    expected = {\n        'image_file_name': 'https://cdn.automationdirect.com/images/products/medium/m_bx16nd3.jpg',\n        'item_code': 'BX-16ND3',\n        'manual_url': 'https://www.automationdirect.com/static/manuals/brxuserm/brxuserm.html',\n        'unit_of_measure': 'EA',\n        \"parameters\":\n            {\n                'Brand': 'BRX',\n                'Item': 'Input module',\n                'IO Module Type': 'Discrete',\n                'Number of Input Points': '16',\n                'Min Input Voltage': '12',\n                'Max Input Voltage': '24',\n                'Nominal Input Voltage': '12-24',\n                'Discrete Input Type': 'Sinking/sourcing',\n                'Fast Response': 'No', 'Number of Isolated Input Commons': '4',\n                'Number of Points per Common': '4',\n                'Requires': 'BX-RTB10, BX-RTB10-1 or BX-RTB10-2 terminal block kit or ZIPLink pre-wired cables',\n                'Programming Software': 'Do-more Designer programming software v2.0 or later'\n            }\n    }\n\n    test_part = fetch_part_info('BX-16ND3', silent=True)\n    if not test_part:\n        test_success = False\n        \n    # Check content of response\n    if test_success:\n        for key, value in expected.items():\n            if test_part[key] != value:\n                print(f'\"{test_part[key]}\" <> \"{value}\"')\n                test_success = False\n                break\n\n    return test_success\n"
  },
  {
    "path": "kintree/search/digikey_api.py",
    "content": "import logging\nimport os\nimport digikey\n\nfrom ..config import settings, config_interface\n\nSEARCH_HEADERS = [\n    'description',\n    'digi_key_part_number',\n    'manufacturer',\n    'manufacturer_product_number',\n    'product_url',\n    'datasheet_url',\n    'photo_url',\n]\nPARAMETERS_MAP = [\n    'parameters',\n    'parameter_text',\n    'value_text',\n]\n\nPRICING_MAP = [\n    'product_variations',\n    'digi_key_product_number',\n    'standard_pricing',\n    'break_quantity',\n    'unit_price',\n    'package_type'\n]\n\nos.environ['DIGIKEY_STORAGE_PATH'] = settings.DIGIKEY_STORAGE_PATH\n# Check if storage path exists, else create it\nif not os.path.exists(os.environ['DIGIKEY_STORAGE_PATH']):\n    os.makedirs(os.environ['DIGIKEY_STORAGE_PATH'], exist_ok=True)\n\n\ndef disable_api_logger():\n    # Digi-Key API logger\n    logging.getLogger('digikey.v3.api').setLevel(logging.CRITICAL)\n    # Disable DEBUG\n    logging.disable(logging.DEBUG)\n\n\ndef check_environment() -> bool:\n    DIGIKEY_CLIENT_ID = os.environ.get('DIGIKEY_CLIENT_ID', None)\n    DIGIKEY_CLIENT_SECRET = os.environ.get('DIGIKEY_CLIENT_SECRET', None)\n\n    if not DIGIKEY_CLIENT_ID or not DIGIKEY_CLIENT_SECRET:\n        return False\n\n    return True\n\n\ndef setup_environment(force=False) -> bool:\n    if not check_environment() or force:\n        # SETUP the Digikey authentication see https://developer.digikey.com/documentation/organization#production\n        digikey_api_settings = config_interface.load_file(settings.CONFIG_DIGIKEY_API)\n        os.environ['DIGIKEY_CLIENT_ID'] = digikey_api_settings['DIGIKEY_CLIENT_ID']\n        os.environ['DIGIKEY_CLIENT_SECRET'] = digikey_api_settings['DIGIKEY_CLIENT_SECRET']\n        os.environ['DIGIKEY_LOCAL_SITE'] = digikey_api_settings.get('DIGIKEY_LOCAL_SITE', 'US')\n        os.environ['DIGIKEY_LOCAL_LANGUAGE'] = digikey_api_settings.get('DIGIKEY_LOCAL_LANGUAGE', 'en')\n        os.environ['DIGIKEY_LOCAL_CURRENCY'] = digikey_api_settings.get('DIGIKEY_LOCAL_CURRENCY', 'USD')\n    return check_environment()\n\n\ndef get_default_search_keys():\n    return [\n        'product_description',\n        'product_description',\n        'revision',\n        'keywords',\n        'digi_key_part_number',\n        'manufacturer',\n        'manufacturer_product_number',\n        'product_url',\n        'datasheet_url',\n        'photo_url',\n    ]\n\n\ndef find_categories(part_details: str):\n    ''' Find categories '''\n    category = part_details.get('category')\n    subcategory = None\n    if category:\n        subcategory = category.get('child_categories')[0]\n        category = category.get('name')\n    if subcategory:\n        subcategory = subcategory.get('name')\n    return category, subcategory\n\n\ndef fetch_part_info(part_number: str) -> dict:\n    ''' Fetch part data from API '''\n    from wrapt_timeout_decorator import timeout\n\n    part_info = {}\n    if not setup_environment():\n        from ..common.tools import cprint\n        cprint('[INFO]\\tWarning: DigiKey API settings are not configured')\n        return part_info\n\n    # THIS METHOD CAN SOMETIMES RETURN INCORRECT MATCH\n    # Added logic to check the result in the GUI flow\n    @timeout(dec_timeout=20)\n    def digikey_search_timeout():\n        return digikey.product_details(\n            part_number,\n            x_digikey_locale_site=os.environ['DIGIKEY_LOCAL_SITE'],\n            x_digikey_locale_language=os.environ['DIGIKEY_LOCAL_LANGUAGE'],\n            x_digikey_locale_currency=os.environ['DIGIKEY_LOCAL_CURRENCY'],\n        ).to_dict()\n\n    # Method to process price breaks\n    def process_price_break(product_variation):\n        part_info['digi_key_part_number'] = product_variation.get(digi_number_key)\n        for price_break in product_variation[pricing_key]:\n            quantity = price_break[qty_key]\n            price = price_break[price_key]\n            part_info['pricing'][quantity] = price\n\n    # Query part number\n    try:\n        part = digikey_search_timeout()\n    except:\n        part = None\n\n    if not part:\n        return part_info\n    if 'product' not in part or not part['product']:\n        return part_info\n\n    part_info['currency'] = part['search_locale_used']['currency']\n    part = part['product']\n\n    category, subcategory = find_categories(part)\n    try:\n        part_info['category'] = category\n        part_info['subcategory'] = subcategory\n    except:\n        part_info['category'] = ''\n        part_info['subcategory'] = ''\n\n    headers = SEARCH_HEADERS\n\n    for key in part:\n        if key in headers:\n            if key == 'manufacturer':\n                part_info[key] = part['manufacturer'].get('name')\n            elif key == 'description':\n                part_info['product_description'] = part['description'].get('product_description')\n                part_info['detailed_description'] = part['description'].get('detailed_description')\n            else:\n                part_info[key] = part[key]\n\n    # Parameters\n    part_info['parameters'] = {}\n    [parameter_key, name_key, value_key] = PARAMETERS_MAP\n\n    for parameter in part[parameter_key]:\n        parameter_name = parameter.get(name_key, '')\n        parameter_value = parameter.get(value_key, '')\n        # Append to parameters dictionary\n        part_info['parameters'][parameter_name] = parameter_value\n    # process classifications as parameters\n    for classification, value in part.get('classifications', {}).items():\n        part_info['parameters'][classification] = value\n\n    # Pricing\n    part_info['pricing'] = {}\n    [variations_key,\n     digi_number_key,\n     pricing_key,\n     qty_key,\n     price_key,\n     package_key] = PRICING_MAP\n\n    variations = part[variations_key]\n    if len(variations) == 1:\n        process_price_break(variations[0])\n    else:\n        for variation in variations:\n            # we try to get the not TR or Digi-Reel option\n            package_type = variation.get(package_key).get('id')\n            if all(package_type != x for x in [1, 243]):\n                process_price_break(variation)\n                break\n    # if no other option was found use the first one returned\n    if not part_info['pricing'] and variations:\n        process_price_break(variations[0])\n\n    # Extra search fields\n    if settings.CONFIG_DIGIKEY.get('EXTRA_FIELDS'):\n        for extra_field in settings.CONFIG_DIGIKEY['EXTRA_FIELDS']:\n            if part.get(extra_field):\n                part_info['parameters'][extra_field] = part[extra_field]\n            else:\n                from ..common.tools import cprint\n                cprint(f'[INFO]\\tWarning: Extra field \"{extra_field}\" not found in search results', silent=False)\n\n    return part_info\n\n\ndef test_api(check_content=False) -> bool:\n    ''' Test method for API token '''\n    test_success = True\n    expected = {\n        'product_description': 'RES 10K OHM 5% 1/16W 0402',\n        'digi_key_part_number': 'RMCF0402JT10K0CT-ND',\n        'manufacturer': 'Stackpole Electronics Inc',\n        'manufacturer_product_number': 'RMCF0402JT10K0',\n        'product_url': 'https://www.digikey.com/en/products/detail/stackpole-electronics-inc/RMCF0402JT10K0/1758206',\n        'datasheet_url': 'https://www.seielect.com/catalog/sei-rmcf_rmcp.pdf',\n        'photo_url': 'https://mm.digikey.com/Volume0/opasdata/d220001/medias/images/2597/MFG_RMC SERIES.jpg',\n    }\n\n    test_part = fetch_part_info('RMCF0402JT10K0')\n\n    # Check for response\n    if not test_part:\n        test_success = False\n    \n    if not check_content:\n        return test_success\n        \n    # Check content of response\n    if test_success:\n        for key, value in expected.items():\n            if test_part[key] != value:\n                print(f'{test_part[key]} != {value}')\n                test_success = False\n                break\n\n    return test_success\n"
  },
  {
    "path": "kintree/search/element14_api.py",
    "content": "from ..config import settings, config_interface\nfrom ..common.tools import download\n\nELEMENT14_API_URL = 'https://api.element14.com/catalog/products'\n\nSTORES = {\n    'Farnell': {\n        'Bulgaria': 'bg.farnell.com ',\n        'Czechia': 'cz.farnell.com',\n        'Denmark': 'dk.farnell.com',\n        'Austria': 'at.farnell.com ',\n        'Switzerland': 'ch.farnell.com',\n        'Germany': 'de.farnell.com',\n        'CPC UK': 'cpc.farnell.com',\n        'CPC Ireland': 'cpcireland.farnell.com',\n        'Export': 'export.farnell.com',\n        'Onecall': 'onecall.farnell.com',\n        'Ireland': 'ie.farnell.com',\n        'Israel': 'il.farnell.com',\n        'United Kingdom': 'uk.farnell.com',\n        'Spain': 'es.farnell.com',\n        'Estonia': 'ee.farnell.com',\n        'Finland': 'fi.farnell.com',\n        'France': 'fr.farnell.com',\n        'Hungary': 'hu.farnell.com',\n        'Italy': 'it.farnell.com',\n        'Lithuania': 'lt.farnell.com',\n        'Latvia': 'lv.farnell.com',\n        'Belgium': 'be.farnell.com',\n        'Netherlands': 'nl.farnell.com',\n        'Norway': 'no.farnell.com',\n        'Poland': 'pl.farnell.com',\n        'Portugal': 'pt.farnell.com',\n        'Romania': 'ro.farnell.com',\n        'Russia': 'ru.farnell.com',\n        'Slovakia': 'sk.farnell.com',\n        'Slovenia': 'si.farnell.com',\n        'Sweden': 'se.farnell.com',\n        'Turkey': 'tr.farnell.com',\n    },\n    'Newark': {\n        'Canada': 'canada.newark.com',\n        'Mexico': 'mexico.newark.com',\n        'United States': 'www.newark.com',\n    },\n    'Element14': {\n        'China': 'cn.element14.com',\n        'Australia': 'au.element14.com',\n        'New Zealand': 'nz.element14.com',\n        'Hong Kong': 'hk.element14.com',\n        'Singapore': 'sg.element14.com',\n        'Malaysia': 'my.element14.com',\n        'Philippines': 'ph.element14.com',\n        'Thailand': 'th.element14.com',\n        'India': 'in.element14.com',\n        'Taiwan': 'tw.element14.com',\n        'Korea': 'kr.element14.com',\n        'Vietnam': 'vn.element14.com',\n    },\n}\n\nSEARCH_HEADERS = [\n    'brandName',\n    'displayName',\n    'sku',\n    'translatedManufacturerPartNumber',\n    'datasheets',\n    'image',\n    'attributes',\n]\n\nPARAMETERS_MAP = [\n    'attributes',\n    'attributeLabel',\n    'attributeValue',\n]\n\nPRICING_MAP = [\n    'prices',\n    'from',\n    'cost',\n]\n\nCURRENCIES = {\n    STORES['Farnell']['Bulgaria']: 'EUR',\n    STORES['Farnell']['Czechia']: 'CZK',\n    STORES['Farnell']['Denmark']: 'DKK',\n    STORES['Farnell']['Austria']: 'EUR',\n    STORES['Farnell']['Switzerland']: 'CHF',\n    STORES['Farnell']['Germany']: 'EUR',\n    STORES['Farnell']['CPC UK']: 'GBP',\n    STORES['Farnell']['CPC Ireland']: 'EUR',\n    STORES['Farnell']['Export']: 'GBP',\n    STORES['Farnell']['Onecall']: 'GBP',\n    STORES['Farnell']['Ireland']: 'EUR',\n    STORES['Farnell']['Israel']: 'USD',\n    STORES['Farnell']['United Kingdom']: 'GBP',\n    STORES['Farnell']['Spain']: 'EUR',\n    STORES['Farnell']['Estonia']: 'EUR',\n    STORES['Farnell']['Finland']: 'EUR',\n    STORES['Farnell']['France']: 'EUR',\n    STORES['Farnell']['Hungary']: 'HUF',\n    STORES['Farnell']['Italy']: 'EUR',\n    STORES['Farnell']['Lithuania']: 'EUR',\n    STORES['Farnell']['Latvia']: 'EUR',\n    STORES['Farnell']['Belgium']: 'EUR',\n    STORES['Farnell']['Netherlands']: 'EUR',\n    STORES['Farnell']['Norway']: 'NOK',\n    STORES['Farnell']['Poland']: 'PLN',\n    STORES['Farnell']['Portugal']: 'EUR',\n    STORES['Farnell']['Romania']: 'RON',\n    STORES['Farnell']['Russia']: 'EUR',\n    STORES['Farnell']['Slovakia']: 'EUR',\n    STORES['Farnell']['Slovenia']: 'EUR',\n    STORES['Farnell']['Sweden']: 'SEK',\n    STORES['Farnell']['Turkey']: 'EUR',\n    STORES['Newark']['Canada']: 'CAD',\n    STORES['Newark']['Mexico']: 'USD',\n    STORES['Newark']['United States']: 'USD',\n    STORES['Element14']['China']: 'CNY',\n    STORES['Element14']['Australia']: 'AUD',\n    STORES['Element14']['New Zealand']: 'NZD',\n    STORES['Element14']['Hong Kong']: 'HKD',\n    STORES['Element14']['Singapore']: 'SGD',\n    STORES['Element14']['Malaysia']: 'MYR',\n    STORES['Element14']['Philippines']: 'PHP',\n    STORES['Element14']['Thailand']: 'THB',\n    STORES['Element14']['India']: 'INR',\n    STORES['Element14']['Taiwan']: 'TWD',\n    STORES['Element14']['Korea']: 'KRW',\n    STORES['Element14']['Vietnam']: 'USD',\n}\n\n\ndef get_default_search_keys():\n    return [\n        'displayName',\n        'displayName',\n        'revision',\n        'keywords',\n        'sku',\n        'brandName',\n        'translatedManufacturerPartNumber',\n        'store_url',\n        'datasheet_url',\n        'image_url',\n    ]\n\n\ndef get_default_store_url(supplier: str) -> str:\n    ''' Get saved store/location for supplier '''\n    import re\n    user_settings = config_interface.load_file(settings.CONFIG_ELEMENT14_API)\n    default_store = user_settings.get(f'{supplier.upper()}_STORE', '')\n    if not default_store:\n        from ..common.tools import cprint\n        cprint(f'[INFO]\\tWarning: Default store \"{supplier.upper()}_STORE\" value not configured', silent=False)\n    url_match = re.match(r'^(.+?) \\((.+?)\\)$', default_store)\n    if url_match:\n        return url_match.group(2)\n    return STORES[supplier][default_store]\n\n\ndef build_api_url(part_number: str, supplier: str, store_url=None, silent=False) -> str:\n    ''' Build API URL based on user settings '''\n\n    user_settings = config_interface.load_file(settings.CONFIG_ELEMENT14_API)\n    api_key = user_settings.get('ELEMENT14_PRODUCT_SEARCH_API_KEY', '')\n    if not api_key:\n        from ..common.tools import cprint\n        cprint('[INFO]\\tWarning: ELEMENT14_PRODUCT_SEARCH_API_KEY user value not configured', silent=silent)\n\n        import os\n        api_key = os.environ.get('ELEMENT14_PART_API_KEY', None)\n        if not api_key:\n            cprint('[INFO]\\tWarning: ELEMENT14_PRODUCT_SEARCH_API_KEY env variable value not found', silent=False)\n    if not store_url:\n        store_url = get_default_store_url(supplier)\n\n    # Set base URL\n    api_url = ELEMENT14_API_URL\n    # Set response format\n    api_url += '?callInfo.responseDataFormat=JSON'\n    # Set result settings: offset = 0; number of results = 1; size = large (eg. to get attributes)\n    api_url += '&resultsSettings.offset=0&resultsSettings.numberOfResults=1&resultsSettings.responseGroup=large'\n    # Set API key\n    api_url += f'&callInfo.apiKey={api_key}'\n    # Set store URL\n    api_url += f'&storeInfo.id={store_url}'\n    # Set part number\n    api_url += f'&term=manuPartNum:{part_number}'\n\n    return api_url\n\n\ndef build_image_url(image_data: dict, supplier: str, store_url=None) -> str:\n    image_url = 'https://'\n    # Set store URL\n    if store_url:\n        image_url += store_url\n    else:\n        image_url += get_default_store_url(supplier)\n    # Append static text\n    image_url += '/productimages/standard'\n    # Append locale\n    if 'farnell' in image_data['vrntPath']:\n        image_url += '/en_GB'\n    else:\n        image_url += '/en_US'\n    # Append image filename\n    image_url += image_data['baseName']\n\n    return image_url\n\n\ndef fetch_part_info(part_number: str, supplier: str, store_url=None, silent=False) -> dict:\n    ''' Fetch part data from API '''\n\n    part_info = {}\n\n    def search_timeout(timeout=10):\n        url = build_api_url(part_number, supplier, store_url, silent)\n        response = download(url, timeout=timeout)\n        return response\n\n    # Query part number\n    try:\n        part = search_timeout()\n    except:\n        part = None\n\n    # Extract result\n    try:\n        part = part['manufacturerPartNumberSearchReturn'].get('products', [])[0]\n    except (TypeError, IndexError):\n        part = None\n\n    if not part:\n        return part_info\n\n    headers = SEARCH_HEADERS\n\n    for key in part:\n        if key in headers:\n            if key == 'displayName':\n                # String to remove\n                str_remove = part['brandName'] + ' - ' + part['translatedManufacturerPartNumber'] + ' - '\n                # Remove and limit to 100 chars\n                part_info['displayName'] = part['displayName'].replace(str_remove, '')[:100]\n            elif key == 'datasheets':\n                try:\n                    part_info['datasheet_url'] = part['datasheets'][0]['url'].replace('http', 'https')\n                except IndexError:\n                    pass\n            elif key == 'image':\n                part_info['image_url'] = build_image_url(part['image'], supplier, store_url)\n            elif key == 'attributes':\n                part_info['parameters'] = {}\n            else:\n                part_info[key] = part[key]\n\n    # Parameters\n    if 'parameters' in part_info.keys():\n        [parameter_key, name_key, value_key] = PARAMETERS_MAP\n\n        try:\n            for parameter in range(len(part[parameter_key])):\n                parameter_name = part[parameter_key][parameter][name_key]\n                parameter_value = part[parameter_key][parameter][value_key]\n                # Append to parameters dictionary\n                part_info['parameters'][parameter_name] = parameter_value\n        except TypeError:\n            # Parameter list is empty\n            pass\n\n    # Pricing\n    part_info['pricing'] = {}\n    [pricing_key, qty_key, price_key] = PRICING_MAP\n\n    for price_break in part[pricing_key]:\n        quantity = price_break[qty_key]\n        price = price_break[price_key]\n        part_info['pricing'][quantity] = price\n        \n    if not store_url:\n        store_url = get_default_store_url(supplier)\n    part_info['currency'] = CURRENCIES.get(store_url, 'USD')\n\n    # Extra search fields\n    if settings.CONFIG_ELEMENT14.get('EXTRA_FIELDS', None):\n        for extra_field in settings.CONFIG_ELEMENT14['EXTRA_FIELDS']:\n            if part.get(extra_field, None):\n                part_info['parameters'][extra_field] = part[extra_field]\n            else:\n                from ..common.tools import cprint\n                cprint(f'[INFO]\\tWarning: Extra field \"{extra_field}\" not found in search results', silent=False)\n    \n    # Append Store URL\n    # Element14 support said \"At this time our API is not structured to provide a URL to product pages in the selected storeInfo.id value.\"\n    if store_url:\n        part_info['store_url'] = f'https://{store_url}'\n    else:\n        part_info['store_url'] = f'https://{get_default_store_url(supplier)}'\n    # Append search to URL\n    part_info['store_url'] += f'/w/search?st={part[\"translatedManufacturerPartNumber\"]}'\n\n    # Append categories\n    part_info['category'] = ''\n    part_info['subcategory'] = ''\n\n    return part_info\n\n\ndef test_api(store_url=None) -> bool:\n    ''' Test method for API '''\n\n    test_success = True\n\n    search_queries = [\n        {\n            'store_url': 'uk.farnell.com',\n            'part_number': '1N4148W-7-F',\n            'expected': {\n                'displayName': 'DIODE, ULTRAFAST RECOVERY, 300mA, 75V, SOD-123-2, FULL REEL',\n                'brandName': 'MULTICOMP PRO',\n                'translatedManufacturerPartNumber': '1N4148W-7-F.',\n            }\n        },\n        {\n            'store_url': 'www.newark.com',\n            'part_number': 'BLM18AG601SN1D',\n            'expected': {\n                'displayName': 'Ferrite Bead, 0603 [1608 Metric], 600 ohm, 500 mA, EMIFIL BLM18AG Series, 0.38 ohm, ± 25%',\n                'brandName': 'MURATA',\n                'translatedManufacturerPartNumber': 'BLM18AG601SN1D',\n            }\n        },\n        {\n            'store_url': 'au.element14.com',\n            'part_number': '2N7002K-T1-GE3',\n            'expected': {\n                'displayName': 'Power MOSFET, N Channel, 60 V, 190 mA, 2 ohm, SOT-23, Surface Mount',\n                'brandName': 'VISHAY',\n                'translatedManufacturerPartNumber': '2N7002K-T1-GE3',\n            }\n        },\n    ]\n\n    if store_url:\n        # If store URL is specified, only check data is returned (eg. avoid discrepancies between stores)\n        part_number = '1N4148'\n        test_part = fetch_part_info(part_number, '', store_url, True)\n        if not test_part:\n            test_success = False\n    else:\n        for item in search_queries:\n            if not test_success:\n                break\n\n            test_part = fetch_part_info(item['part_number'], '', item['store_url'], True)\n\n            if not test_part:\n                test_success = False\n                \n            # Check content of response\n            if test_success:\n                for key, value in item['expected'].items():\n                    if test_part[key] != value:\n                        print(f'\"{test_part[key]}\" <> \"{value}\"')\n                        test_success = False\n                        break\n\n    return test_success\n"
  },
  {
    "path": "kintree/search/jameco_api.py",
    "content": "import html\nimport re\nfrom ..common.tools import download\n\nSEARCH_HEADERS = [\n    'title',\n    'name',\n    'prod_id',\n    'ss_attr_manufacturer',\n    'manufacturer_part_number',\n    'url',\n    'imageUrl',\n    'related_prod_id',\n    'category',\n]\n\n# Not really a map for Jameco.\n# Parameters are listed at same level as the search keys, not in separate list\nPARAMETERS_KEYS = [\n    'product_type_unigram',\n    'ss_attr_voltage_rating',\n    'ss_attr_multiple_order_quantity',\n]\n\n\ndef get_default_search_keys():\n    # order matters, linked with part_form[] order in inventree_interface.translate_supplier_to_form()\n    return [\n        'title',\n        'name',\n        'revision',\n        'keywords',\n        'prod_id',\n        'ss_attr_manufacturer',\n        'manufacturer_part_number',\n        'url',\n        'datasheet',\n        'imageUrl',\n    ]\n\n\ndef find_categories(part_details: str):\n    ''' Find categories '''\n    try:\n        return part_details['parentCatalogName'], part_details['catalogName']\n    except:\n        return None, None\n\n\ndef fetch_part_info(part_number: str) -> dict:\n    ''' Fetch part data from API '''\n\n    # Load Jameco settings\n    from ..config import settings, config_interface\n    jameco_api_settings = config_interface.load_file(settings.CONFIG_JAMECO_API)\n\n    part_info = {}\n\n    def search_timeout(timeout=10):\n        url = jameco_api_settings.get('JAMECO_API_URL', '') + part_number\n        response = download(url, timeout=timeout)\n        return response\n\n    # Query part number\n    try:\n        part = search_timeout()\n        # Extract results, select first in returned search List\n        part = part.get('results', None)\n        part = part[0]\n    except:\n        part = None\n\n    if not part:\n        return part_info\n\n    category, subcategory = find_categories(part)\n    try:\n        part_info['category'] = category\n        part_info['subcategory'] = subcategory\n    except:\n        part_info['category'] = ''\n        part_info['subcategory'] = ''\n\n    headers = SEARCH_HEADERS\n\n    for key in part:\n        if key in headers:\n            if key == 'imageUrl':\n                try:\n                    part_info[key] = part['imageUrl']\n                except IndexError:\n                    pass\n            elif key in ['title', 'name', 'category']:\n                # Jameco title/name is often >100 chars, which causes an error later. Check for it here.\n                if (len(part[key]) > 100):\n                    trimmed_value = str(part[key])[:100]\n                    part_info[key] = html.unescape(trimmed_value)  # Json data sometimes has HTML encoded chars, e.g. &quot;\n                else:\n                    part_info[key] = html.unescape(part[key])\n            else:\n                part_info[key] = part[key]\n\n    # Parameters\n    part_info['parameters'] = {}\n\n    for i, parameter_key in enumerate(PARAMETERS_KEYS):\n        if part.get(parameter_key, ''):\n            parameter_name = parameter_key\n            parameter_value = part[parameter_key]\n            if isinstance(parameter_value, list):\n                parameter_string = ', '.join(parameter_value)\n                part_info['parameters'][parameter_name] = parameter_string\n            else:\n                # Append to parameters dictionary\n                part_info['parameters'][parameter_name] = parameter_value\n\n    # Pricing\n    part_info['pricing'] = {}\n\n    # Jameco returns price breaks as a string of HTML text\n    # Convert pricing string pattern to List, then  dictionary for Ki-nTree\n    price_break_str = part['secondary_prices']\n    price_break_str = price_break_str.replace(',', '')  # remove comma\n    price_break_str = re.sub(r'(\\&lt;br\\s\\/&*gt)', '', price_break_str)  # remove HTML\n    price_break_str = re.sub(';', ':', price_break_str)  # remove ; char\n    price_break_str = re.sub(r'(\\:\\s+\\$)|\\;', ':', price_break_str)  # remove $ char\n    price_break_list = price_break_str.split(':')  # split on :\n    price_break_list.pop()  # remove last empty element in List\n\n    for i in range(0, len(price_break_list), 2):\n        quantity = int(price_break_list[i])\n        price = float(price_break_list[i + 1])\n        part_info['pricing'][quantity] = price\n\n    part_info['currency'] = 'USD'\n\n    # Extra search fields\n    if settings.CONFIG_JAMECO.get('EXTRA_FIELDS', None):\n        for extra_field in settings.CONFIG_JAMECO['EXTRA_FIELDS']:\n            if part.get(extra_field, None):\n                part_info['parameters'][extra_field] = part[extra_field]\n            else:\n                from ..common.tools import cprint\n                cprint(f'[INFO]\\tWarning: Extra field \"{extra_field}\" not found in search results', silent=False)\n\n    return part_info\n\n\ndef test_api() -> bool:\n    ''' Test method for API '''\n\n    test_success = True\n\n    expected = {\n        'manufacturer_part_number': 'PN2222ABU',\n        'name': 'Transistor PN2222A NPN Silicon General Purpose TO-92',\n        'prod_id': '178511',\n    }\n\n    test_part = fetch_part_info('178511')\n    if not test_part:\n        test_success = False\n        \n    # Check content of response\n    if test_success:\n        for key, value in expected.items():\n            if test_part[key] != value:\n                print(f'\"{test_part[key]}\" <> \"{value}\"')\n                test_success = False\n                break\n\n    return test_success\n"
  },
  {
    "path": "kintree/search/lcsc_api.py",
    "content": "from ..common.tools import download\n\nSEARCH_HEADERS = [\n    'productDescEn',\n    'productIntroEn',\n    'productCode',\n    'brandNameEn',\n    'productModel',\n    'pdfUrl',\n    'productImages',\n]\nPARAMETERS_MAP = [\n    'paramVOList',\n    'paramNameEn',\n    'paramValueEn',\n]\n\nPRICING_MAP = [\n    'productPriceList',\n    'ladder',\n    'usdPrice',\n]\n\n\ndef get_default_search_keys():\n    return [\n        'productIntroEn',\n        'productIntroEn',\n        'revision',\n        'keywords',\n        'productCode',\n        'brandNameEn',\n        'productModel',\n        'part_url',\n        'pdfUrl',\n        'productImages',\n    ]\n\n\ndef find_categories(part_details: str):\n    ''' Find categories '''\n    try:\n        return part_details['parentCatalogName'], part_details['catalogName']\n    except:\n        return None, None\n\n\ndef fetch_part_info(part_number: str) -> dict:\n    ''' Fetch part data from API '''\n\n    # Load LCSC settings\n    from ..config import settings, config_interface\n    lcsc_api_settings = config_interface.load_file(settings.CONFIG_LCSC_API)\n\n    part_info = {}\n\n    def search_timeout(timeout=10):\n        url = lcsc_api_settings.get('LCSC_API_URL', '') + part_number\n        response = download(url, timeout=timeout)\n        return response\n\n    # Query part number\n    try:\n        part = search_timeout()\n        # Extract result\n        part = part.get('result', None)\n    except:\n        part = {}\n\n    if not part:\n        return part_info\n\n    product_code = part.get('productCode')\n    if product_code:\n        part_info['part_url'] = f'https://www.lcsc.com/product-detail/{product_code}.html'\n\n    category, subcategory = find_categories(part)\n    try:\n        part_info['category'] = category\n        part_info['subcategory'] = subcategory\n    except:\n        part_info['category'] = ''\n        part_info['subcategory'] = ''\n\n    headers = SEARCH_HEADERS\n\n    for key in part:\n        if key in headers:\n            if key == 'productImages':\n                try:\n                    part_info[key] = part['productImages'][0]\n                except IndexError:\n                    pass\n            else:\n                part_info[key] = part[key]\n\n    # Parameters\n    part_info['parameters'] = {}\n    [parameter_key, name_key, value_key] = PARAMETERS_MAP\n\n    if part.get(parameter_key, ''):\n        for parameter in range(len(part[parameter_key])):\n            parameter_name = part[parameter_key][parameter][name_key]\n            parameter_value = part[parameter_key][parameter][value_key]\n            # Append to parameters dictionary\n            part_info['parameters'][parameter_name] = parameter_value\n\n    # Pricing\n    part_info['pricing'] = {}\n    [pricing_key, qty_key, price_key] = PRICING_MAP\n\n    for price_break in part[pricing_key]:\n        quantity = price_break[qty_key]\n        price = price_break[price_key]\n        part_info['pricing'][quantity] = price\n\n    part_info['currency'] = 'USD'\n\n    # Extra search fields\n    if settings.CONFIG_LCSC.get('EXTRA_FIELDS', None):\n        for extra_field in settings.CONFIG_LCSC['EXTRA_FIELDS']:\n            if part.get(extra_field, None):\n                part_info['parameters'][extra_field] = part[extra_field]\n            else:\n                from ..common.tools import cprint\n                cprint(f'[INFO]\\tWarning: Extra field \"{extra_field}\" not found in search results', silent=False)\n\n    return part_info\n\n\ndef test_api() -> bool:\n    ''' Test method for API '''\n\n    test_success = True\n    expected = {\n        'productIntroEn': '25V 100pF C0G ±5% 0201 Multilayer Ceramic Capacitors MLCC - SMD/SMT ROHS',\n        'productCode': 'C2181718',\n        'brandNameEn': 'TDK',\n        'productModel': 'C0603C0G1E101J030BA',\n    }\n\n    test_part = fetch_part_info('C2181718')\n    if not test_part:\n        test_success = False\n        \n    # Check content of response\n    if test_success:\n        for key, value in expected.items():\n            if test_part[key] != value:\n                print(f'\"{test_part[key]}\" <> \"{value}\"')\n                test_success = False\n                break\n\n    return test_success\n"
  },
  {
    "path": "kintree/search/mouser_api.py",
    "content": "import os\n\nfrom ..config import settings, config_interface\nfrom mouser.api import MouserPartSearchRequest\n\nSEARCH_HEADERS = [\n    'Description',\n    'productCode',\n    'MouserPartNumber',\n    'Manufacturer',\n    'ManufacturerPartNumber',\n    'DataSheetUrl',\n    'ProductDetailUrl',\n    'ImagePath',\n]\nPARAMETERS_MAP = [\n    'ProductAttributes',\n    'AttributeName',\n    'AttributeValue',\n]\n\nPRICING_MAP = [\n    'PriceBreaks',\n    'Quantity',\n    'Price',\n    'Currency',\n]\n\n\ndef get_default_search_keys():\n    return [\n        'ManufacturerPartNumber',\n        'Description',\n        'revision',\n        'keywords',\n        'MouserPartNumber',\n        'Manufacturer',\n        'ManufacturerPartNumber',\n        'ProductDetailUrl',\n        'DataSheetUrl',\n        'ImagePath',\n    ]\n\n\ndef setup_environment(force=False):\n    ''' Setup environmental variables '''\n\n    api_key = os.environ.get('MOUSER_PART_API_KEY', None)\n    if not api_key or force:\n        mouser_api_settings = config_interface.load_file(settings.CONFIG_MOUSER_API)\n        try:\n            os.environ['MOUSER_PART_API_KEY'] = mouser_api_settings['MOUSER_PART_API_KEY']\n        except TypeError:\n            pass\n\n\ndef find_categories(part_details: str):\n    ''' Find categories '''\n\n    try:\n        return part_details['Category'], None\n    except:\n        return None, None\n\n\ndef fetch_part_info(part_number: str) -> dict:\n    ''' Fetch part data from API '''\n\n    from wrapt_timeout_decorator import timeout\n\n    setup_environment()\n    part_info = {}\n\n    @timeout(dec_timeout=20)\n    def search_timeout():\n        try:\n            request = MouserPartSearchRequest('partnumber')\n            request.part_search(part_number)\n        except FileNotFoundError as e:\n            error_message = repr(e.args[0])\n            error_message = error_message.strip(\"'\")\n            from ..common.tools import cprint\n            cprint(f'[INFO] Warning: {error_message}', silent=False)\n        finally:\n            # Mouser 0.1.6 API update: single part list is returned, instead of dict\n            return request.get_clean_response()[0]\n\n    # Query part number\n    try:\n        part: dict = search_timeout()\n    except:\n        part = None\n\n    if not part:\n        return part_info\n\n    # Check for empty response\n    empty = True\n    for key, value in part.items():\n        if value:\n            empty = False\n            break\n    if empty:\n        return part_info\n\n    category, subcategory = find_categories(part)\n    try:\n        part_info['category'] = category\n        part_info['subcategory'] = subcategory\n    except:\n        part_info['category'] = ''\n        part_info['subcategory'] = ''\n\n    headers = SEARCH_HEADERS\n\n    for key in part:\n        if key in headers:\n            part_info[key] = part[key]\n\n    # Parameters\n    part_info['parameters'] = {}\n    [parameter_key, name_key, value_key] = PARAMETERS_MAP\n\n    for parameter in range(len(part[parameter_key])):\n        parameter_name = part[parameter_key][parameter][name_key]\n        parameter_value = part[parameter_key][parameter][value_key]\n        # Append to parameters dictionary\n        part_info['parameters'][parameter_name] = parameter_value\n\n    # Pricing\n    part_info['pricing'] = {}\n    [pricing_key, qty_key, price_key, currency_key] = PRICING_MAP\n\n    for price_break in part[pricing_key]:\n        quantity = price_break[qty_key]\n        price = price_break[price_key]\n        part_info['pricing'][quantity] = price\n\n    if part[pricing_key]:\n        part_info['currency'] = part[pricing_key][0][currency_key]\n    else:\n        part_info['currency'] = 'USD'\n\n    # Extra search fields\n    if settings.CONFIG_MOUSER.get('EXTRA_FIELDS', None):\n        for extra_field in settings.CONFIG_MOUSER['EXTRA_FIELDS']:\n            if part.get(extra_field, None):\n                part_info['parameters'][extra_field] = part[extra_field]\n            else:\n                from ..common.tools import cprint\n                cprint(f'[INFO]\\tWarning: Extra field \"{extra_field}\" not found in search results', silent=False)\n\n    return part_info\n\n\ndef test_api() -> bool:\n    ''' Test method for API '''\n\n    test_success = True\n    expected = {\n        'Description': 'MOSFETs P-channel 1.25W',\n        'MouserPartNumber': '621-DMP2066LSN-7',\n        'Manufacturer': 'Diodes Incorporated',\n        'ManufacturerPartNumber': 'DMP2066LSN-7',\n    }\n\n    test_part = fetch_part_info('DMP2066LSN-7')\n        \n    if not test_part:\n        # Unsucessful search\n        test_success = False\n    else:\n        # Check content of response\n        for key, value in expected.items():\n            if test_part[key] != value:\n                print(f'\"{test_part[key]}\" <> \"{value}\"')\n                test_success = False\n                break\n\n    return test_success\n"
  },
  {
    "path": "kintree/search/search_api.py",
    "content": "import os\nimport time\n\nfrom ..config import settings, config_interface\n\n\ndef load_from_file(search_file, test_mode=False) -> dict:\n    ''' Fetch part data from file '''\n    cache_valid = settings.CACHE_VALID_DAYS * 24 * 3600\n\n    # Load data from file if cache enabled\n    if settings.CACHE_ENABLED:\n        try:\n            part_data = config_interface.load_file(search_file)\n        except FileNotFoundError:\n            return None\n\n        # Check cache validity\n        try:\n            # Get timestamp\n            timestamp = int(time.time() - part_data['search_timestamp'])\n        except (KeyError, TypeError):\n            timestamp = int(time.time())\n\n        if timestamp < cache_valid or test_mode:\n            return part_data\n\n    return None\n\n\ndef save_to_file(part_info, search_file, update_ts=True):\n    ''' Save part data to file '''\n\n    # Check if search/results directory needs to be created\n    if not os.path.exists(os.path.dirname(search_file)):\n        os.mkdir(os.path.dirname(search_file))\n\n    if update_ts:\n        # Update timestamp\n        part_info['search_timestamp'] = int(time.time())\n\n    # Save data if cache enabled\n    if settings.CACHE_ENABLED:\n        config_interface.dump_file(part_info, search_file)\n"
  },
  {
    "path": "kintree/search/snapeda_api.py",
    "content": "from ..config import settings\nfrom ..common.tools import download, download_with_retry\n\nAPI_BASE_URL = 'https://snapeda.eeinte.ch/?'\nSNAPEDA_URL = 'https://www.snapeda.com'\n\n\ndef fetch_snapeda_part_info(part_number: str) -> dict:\n    ''' Fetch SnapEDA part data from API '''\n\n    api_url = API_BASE_URL + part_number.replace(' ', '%20')\n    data = download(api_url, timeout=10)\n    return data if data else {}\n\n\ndef parse_snapeda_response(response: dict) -> dict:\n    ''' Return only relevant information from SnapEDA API response '''\n\n    data = {\n        'part_number': None,\n        'has_symbol': False,\n        'has_footprint': False,\n        'symbol_image': None,\n        'footprint_image': None,\n        'package': None,\n        'part_url': None,\n        'has_single_result': False,\n    }\n\n    number_results = int(response.get('hits', 0))\n\n    # Check for single result\n    if number_results == 1:\n        try:\n            data['part_number'] = response['results'][0].get('part_number', None)\n            data['has_symbol'] = response['results'][0].get('has_symbol', False)\n            data['has_footprint'] = response['results'][0].get('has_footprint', False)\n            data['package'] = response['results'][0]['package'].get('name', None)\n            data['part_url'] = SNAPEDA_URL + response['results'][0]['_links']['self'].get('href', '')\n            data['part_url'] += '?ref=kintree'\n            data['has_single_result'] = True\n        except KeyError:\n            pass\n\n        # Separate as the 'models' key does not always exist\n        try:\n            data['symbol_image'] = response['results'][0]['models'][0]['symbol_medium'].get('url', None)\n        except KeyError:\n            pass\n        try:\n            data['footprint_image'] = response['results'][0]['models'][0]['package_medium'].get('url', None)\n        except KeyError:\n            pass\n    elif number_results > 1:\n        try:\n            data['part_url'] = SNAPEDA_URL + '/search/' + response['pages'][0].get('link', None).split('&')[0] + '&ref=kintree'\n        except:\n            pass\n    else:\n        pass\n\n    return data\n\n\ndef download_snapeda_images(snapeda_data: dict, silent=False) -> dict:\n    ''' Download symbol and footprint images from SnapEDA's server '''\n\n    images = {\n        'symbol': None,\n        'footprint': None,\n    }\n\n    try:\n        part_number = snapeda_data[\"part_number\"].replace('/', '').lower()\n    except:\n        part_number = None\n\n    if part_number:\n        try:\n            if snapeda_data['symbol_image']:\n                # Form path\n                image_name = f'{part_number}_symbol.png'\n                image_location = settings.search_images + image_name\n\n                # Download symbol image\n                symbol = download_with_retry(\n                    url=snapeda_data['symbol_image'],\n                    full_path=image_location,\n                    filetype='Image',\n                    silent=silent,\n                )\n                if symbol:\n                    images['symbol'] = image_location\n        except KeyError:\n            pass\n\n        try:\n            if snapeda_data['footprint_image']:\n                # Form path\n                image_name = f'{part_number}_footprint.png'\n                image_location = settings.search_images + image_name\n\n                # Download symbol image\n                footprint = download_with_retry(\n                    url=snapeda_data['footprint_image'],\n                    full_path=image_location,\n                    filetype='Image',\n                    silent=silent,\n                )\n                if footprint:\n                    images['footprint'] = image_location\n        except KeyError:\n            pass\n\n    return images\n\n\ndef test_snapeda_api() -> bool:\n    ''' Test method for SnapEDA API '''\n\n    result = False\n\n    # Test single result\n    response = fetch_snapeda_part_info('TPS61221DCKR')\n    data = parse_snapeda_response(response)\n    images = download_snapeda_images(data, silent=True)\n\n    if data['part_number'] and data['has_symbol'] and images['symbol']:\n        result = True\n\n    # Test multiple results\n    if result:\n        response = fetch_snapeda_part_info('1N4148W-7-F')\n        data = parse_snapeda_response(response)\n        if data['has_single_result']:\n            result = False\n\n    return result\n"
  },
  {
    "path": "kintree/search/tme_api.py",
    "content": "import base64\nimport collections\nimport hashlib\nimport hmac\nimport os\nimport urllib.parse\nimport urllib.request\nimport json\n\n# from ..common.tools import download\nfrom ..config import config_interface, settings\n\nPRICING_MAP = [\n    'PriceList',\n    'Amount',\n    'PriceValue',\n    'Currency',\n]\n\n\ndef get_default_search_keys():\n    return [\n        'Symbol',\n        'Description',\n        '',  # Revision\n        'Category',\n        'Symbol',\n        'Producer',\n        'OriginalSymbol',\n        'ProductInformationPage',\n        'Datasheet',\n        'Photo',\n    ]\n\n\ndef check_environment() -> bool:\n    TME_API_TOKEN = os.environ.get('TME_API_TOKEN', None)\n    TME_API_SECRET = os.environ.get('TME_API_SECRET', None)\n\n    if not TME_API_TOKEN or not TME_API_SECRET:\n        return False\n\n    return True\n\n\ndef setup_environment(force=False) -> bool:\n    if not check_environment() or force:\n        tme_api_settings = config_interface.load_file(settings.CONFIG_TME_API)\n        os.environ['TME_API_TOKEN'] = tme_api_settings.get('TME_API_TOKEN', None)\n        os.environ['TME_API_SECRET'] = tme_api_settings.get('TME_API_SECRET', None)\n\n    return check_environment()\n\n\n# Based on TME API snippets mentioned in API documentation: https://developers.tme.eu/documentation/download\n# https://github.com/tme-dev/TME-API/blob/master/Python/call.py\ndef tme_api_request(endpoint, tme_api_settings, params, api_host='https://api.tme.eu', format='json', **kwargs):\n    TME_API_TOKEN = tme_api_settings.get('TME_API_TOKEN', None)\n    TME_API_SECRET = tme_api_settings.get('TME_API_SECRET', None)\n\n    params['Country'] = tme_api_settings.get('TME_API_COUNTRY', 'US')\n    params['Language'] = tme_api_settings.get('TME_API_LANGUAGE', 'EN')\n    if not TME_API_TOKEN and not TME_API_SECRET:\n        TME_API_TOKEN = os.environ.get('TME_API_TOKEN', None)\n        TME_API_SECRET = os.environ.get('TME_API_SECRET', None)\n    if not TME_API_TOKEN and not TME_API_SECRET:\n        from ..common.tools import cprint\n        cprint('[INFO]\\tWarning: Value not found for TME_API_TOKEN and/or TME_API_SECRET', silent=False)\n        return None\n    params['Token'] = TME_API_TOKEN\n    params = collections.OrderedDict(sorted(params.items()))\n\n    url = api_host + endpoint + '.' + format\n    encoded_params = urllib.parse.urlencode(params, quote_via=urllib.parse.quote)\n    signature_base = 'POST' + '&' + urllib.parse.quote(url, '') + '&' + urllib.parse.quote(encoded_params, '')\n    hmac_value = hmac.new(\n        TME_API_SECRET.encode(),\n        signature_base.encode(),\n        hashlib.sha1\n    ).digest()\n    api_signature = base64.encodebytes(hmac_value).rstrip()\n    params['ApiSignature'] = api_signature\n\n    data = urllib.parse.urlencode(params).encode()\n    headers = {\n        \"Content-type\": \"application/x-www-form-urlencoded\",\n    }\n    return urllib.request.Request(url, data, headers)\n\n\ndef tme_api_query(request: urllib.request.Request) -> dict:\n    response = None\n    try:\n        data = urllib.request.urlopen(request).read().decode('utf8')\n    except urllib.error.HTTPError:\n        data = None\n    if data:\n        response = json.loads(data)\n    return response\n\n\ndef fetch_part_info(part_number: str) -> dict:\n\n    def search_product(response):\n        found = False\n        index = 0\n        for product in response['Data']['ProductList']:\n            if product['Symbol'] == part_number:\n                found = True\n                break\n            index = index + 1\n        return found, index\n\n    tme_api_settings = config_interface.load_file(settings.CONFIG_TME_API)\n    params = {'SymbolList[0]': part_number}\n    response = tme_api_query(tme_api_request('/Products/GetProducts', tme_api_settings, params))\n\n    if response is None or response['Status'] != 'OK':\n        return {}\n    # in the case if multiple parts returned\n    # (for e.g. if we looking for NE555A we could have NE555A and NE555AB in the results)\n    found = False\n    index = 0\n    for product in response['Data']['ProductList']:\n        if product['Symbol'] == part_number:\n            found = True\n            break\n        index = index + 1\n\n    if not found:\n        return {}\n    part_info = response['Data']['ProductList'][index]\n    part_info['Photo'] = \"http:\" + part_info['Photo']\n    part_info['ProductInformationPage'] = \"http:\" + part_info['ProductInformationPage']\n    part_info['category'] = part_info['Category']\n    part_info['subcategory'] = None\n\n    # query the parameters\n    params = {'SymbolList[0]': part_number}\n    response = tme_api_query(tme_api_request('/Products/GetParameters', tme_api_settings, params))\n    # check if accidentally no data returned\n    if response is None or response['Status'] != 'OK':\n        return part_info\n\n    found, index = search_product(response)\n\n    if not found:\n        return part_info\n\n    part_info['parameters'] = {}\n    for param in response['Data']['ProductList'][index][\"ParameterList\"]:\n        part_info['parameters'][param['ParameterName']] = param['ParameterValue']\n\n    # query the prices\n    params = {'SymbolList[0]': part_number, 'Curreny': 'USD'}\n    response = tme_api_query(tme_api_request('/Products/GetPrices', tme_api_settings, params))\n    # check if accidentally no data returned\n    if response is None or response['Status'] != 'OK':\n        return part_info\n\n    found, index = search_product(response)\n\n    if not found:\n        part_info['currency'] = 'USD'\n        return part_info\n\n    part_info['pricing'] = {}\n    [pricing_key, qty_key, price_key, currency_key] = PRICING_MAP\n\n    for price_break in response['Data']['ProductList'][index][pricing_key]:\n        quantity = price_break[qty_key]\n        price = price_break[price_key]\n        part_info['pricing'][quantity] = price\n\n    part_info['currency'] = response['Data'][currency_key]\n\n    # Query the files associated to the product\n    params = {'SymbolList[0]': part_number}\n    response = tme_api_query(tme_api_request('/Products/GetProductsFiles', tme_api_settings, params))\n    # check if accidentally no products returned\n    if response is None or response['Status'] != 'OK':\n        return part_info\n\n    found, index = search_product(response)\n\n    if not found:\n        return part_info\n\n    for doc in response['Data']['ProductList'][index]['Files']['DocumentList']:\n        if doc['DocumentType'] == 'DTE':\n            part_info['Datasheet'] = 'http:' + doc['DocumentUrl']\n            break\n    return part_info\n\n\ndef test_api(check_content=False) -> bool:\n    ''' Test method for API '''\n    setup_environment()\n\n    test_success = True\n    expected = {\n        'Description': 'Capacitor: ceramic; MLCC; 33pF; 50V; C0G; ±5%; SMD; 0402',\n        'Symbol': 'CL05C330JB5NNNC',\n        'Producer': 'SAMSUNG',\n        'OriginalSymbol': 'CL05C330JB5NNNC',\n        'ProductInformationPage': 'http://www.tme.eu/en/details/cl05c330jb5nnnc/mlcc-smd-capacitors/samsung/',\n        'Datasheet': 'http://www.tme.eu/Document/7da762c1dbaf553c64ad9c40d3603826/mlcc_samsung.pdf',\n        'Photo': 'http://ce8dc832c.cloudimg.io/v7/_cdn_/8D/4E/00/00/0/58584_1.jpg?width=640&height=480&wat=1&wat_url=_tme-wrk_%2Ftme_new.png&wat_scale=100p&ci_sign=be42abccf5ef8119c2a0d945a27afde3acbeb699',\n    }\n\n    test_part = fetch_part_info('CL05C330JB5NNNC')\n\n    # Check for response\n    if not test_part:\n        test_success = False\n\n    if not check_content:\n        return test_success\n\n    # Check content of response\n    if test_success:\n        for key, value in expected.items():\n            if test_part[key] != value:\n                print(f'{test_part[key]} != {value}')\n                test_success = False\n                break\n\n    return test_success\n"
  },
  {
    "path": "kintree/setup_inventree.py",
    "content": "import sys\n\nfrom .config import settings\nfrom .common.tools import cprint\nfrom .config import config_interface\nfrom .database import inventree_api, inventree_interface\n\n\ndef setup_inventree():\n    SETUP_CATEGORIES = True\n    SETUP_PARAMETERS = True\n\n    def create_categories(parent, name, categories):\n        category_pk, is_category_new = inventree_api.create_category(parent=parent, name=name)\n        if is_category_new:\n            cprint(f'[TREE]\\tSuccess: Category \"{name}\" was added to InvenTree')\n        else:\n            cprint(f'[TREE]\\tWarning: Category \"{name}\" already exists')\n\n        if categories[name]:\n            for cat in categories[name]:\n                create_categories(parent=name, name=cat, categories=categories[name])\n\n    if SETUP_CATEGORIES or SETUP_PARAMETERS:\n        cprint('\\n[MAIN]\\tStarting InvenTree setup', silent=settings.SILENT)\n        # Load category configuration file\n        categories = config_interface.load_file(settings.CONFIG_CATEGORIES)['CATEGORIES']\n\n        cprint('[MAIN]\\tConnecting to Inventree', silent=settings.SILENT)\n        inventree_connect = inventree_interface.connect_to_server()\n\n        if not inventree_connect:\n            sys.exit(-1)\n\n        # Setup database for test\n        inventree_api.set_inventree_db_test_mode()\n\n    if SETUP_CATEGORIES:\n        for category in categories.keys():\n            cprint(f'\\n[MAIN]\\tCreating categories in {category.upper()}')\n            create_categories(parent=None, name=category, categories=categories)\n\n    if SETUP_PARAMETERS:\n        # Load parameter configuration file\n        parameters = config_interface.load_file(settings.CONFIG_PARAMETERS)\n        # cprint(parameters)\n        cprint('\\n[MAIN]\\tLoading Parameters')\n        for name, unit in parameters.items():\n            pk = inventree_api.create_parameter_template(name, unit)\n            if pk > 0:\n                cprint(f'[TREE]\\tSuccess: Parameter \"{name}\" was added to InvenTree')\n            else:\n                cprint(f'[TREE]\\tWarning: Parameter \"{name}\" already exists')\n\n\nif __name__ == '__main__':\n    setup_inventree()\n"
  },
  {
    "path": "kintree_gui.py",
    "content": "import sys\nfrom kintree.kintree_gui import main\n\nif __name__ == '__main__':\n    if len(sys.argv) > 1:\n        main(view='browser')\n        sys.exit()\n    main(view='flet_app')\n"
  },
  {
    "path": "poetry.toml",
    "content": "[virtualenvs]\nin-project = true\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"kintree\"\nversion = \"1.2.1\" # placeholder\ndescription = \"Fast part creation in KiCad and InvenTree\"\nauthors = [\"eeintech <eeintech@eeinte.ch>\"]\nmaintainers = [\"eeintech <eeintech@eeinte.ch>\"]\nlicense = \"GPL-3.0-or-later\"\nreadme = \"README.md\"\nhomepage = \"https://github.com/sparkmicro/Ki-nTree\"\nrepository = \"https://github.com/sparkmicro/Ki-nTree\"\nkeywords = [\"inventree\", \"kicad\", \"digikey\", \"mouser\", \"component\", \"part\", \"create\"]\n\n[tool.poetry.dependencies]\npython = \">=3.9,<3.14\"\n# digikey-api = \"^1.0.0\"\ndigikey-api = { git = \"https://github.com/hurricaneJoef/digikey-api.git\", branch = \"master\" }\nsetuptools = \"^75.6.0\"\nflet = \"0.24.1\"\nthefuzz = \"^0.22.1\"\ninventree = \"^0.23.1\"\nkiutils = \"^1.4.8\"\nmouser = \"^0.1.6\"\nmultiprocess = \"^0.70.17\"\npyyaml = \"^6.0.2\"\nvalidators = \"^0.34.0\"\nwrapt_timeout_decorator = \"^1.5.1\"\ncloudscraper = \"^1.2.71\"\n\n[tool.poetry.dev-dependencies]\ninvoke = \"^2.0.0\"\ncoveralls = \"^3.3.1\"\n\n[tool.poetry.scripts]\nkintree = 'kintree.kintree_gui:main'\nkintree_setup_inventree = 'kintree.setup_inventree:setup_inventree'\n\n[build-system]\nrequires = [\"poetry-core>=1.4.2\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "requirements.txt",
    "content": "cloudscraper==1.2.71\nsetuptools==75.2.0\nhttps://github.com/hurricaneJoef/digikey-api/archive/refs/heads/master.zip\nFlet>=0.24.1,<=0.24.1\nthefuzz>=0.19.0,<1.0\ninventree>=0.23.1,<1.0\nkiutils>=1.4.8,<2.0\nmouser>=0.1.6,<1.0\nmultiprocess>=0.70.16,<0.71\nPyYAML>=6.0.1,<7.0\nvalidators>=0.19.0,<1.0\nwrapt_timeout_decorator>=1.5.1,<2.0\n"
  },
  {
    "path": "run_tests.py",
    "content": "import os\nimport sys\n\nimport kintree.config.settings as settings\nfrom kintree.common.tools import cprint, create_library, download_with_retry\nfrom kintree.config import config_interface\nfrom kintree.database import inventree_api, inventree_interface\nfrom kintree.kicad import kicad_interface\nfrom kintree.search import (\n    digikey_api,\n    mouser_api,\n    element14_api,\n    lcsc_api,\n    tme_api,\n    snapeda_api,\n    automationdirect_api,\n    jameco_api,\n)\nfrom kintree.setup_inventree import setup_inventree\n\n\n# SETTINGS\n# Enable API tests\ntry:\n    ENABLE_API = int(sys.argv[1])\nexcept IndexError:\n    ENABLE_API = 0\n# Enable InvenTree tests\nENABLE_INVENTREE = False\n# Enable KiCad tests\nENABLE_KICAD = True\n# Set categories to test\nPART_CATEGORIES = [\n    'Capacitors',\n    'Circuit Protections',\n    'Connectors',\n    'Crystals and Oscillators',\n    'Diodes',\n    'Inductors',\n    'Integrated Circuits',\n    'Mechanicals',\n    'Power Management',\n    'Resistors',\n    'RF',\n    'Transistors',\n]\n# Enable tests on extra methods\nENABLE_TEST_METHODS = True\n###\n\n\n# Pretty test printing\ndef pretty_test_print(message: str):\n    cprint(message.ljust(65), end='')\n\n\n# Check result\ndef check_result(status: str, new_part: bool) -> bool:\n    # Build result\n    success = False\n    if (status == 'original') or (status == 'fake_alternate'):\n        if new_part:\n            success = True\n    elif status == 'alternate_mpn':\n        if not new_part:\n            success = True\n    else:\n        pass\n\n    return success\n\n\n# --- SETUP ---\n\n# Enable test mode\nsettings.enable_test_mode()\n# Enable InvenTree and KiCad\nsettings.set_enable_flag('inventree', True)\nsettings.set_enable_flag('alternate', False)\nsettings.set_enable_flag('kicad', True)\n# Load user configuration files\nsettings.load_user_config()\n# Set path to test libraries\ntest_library_path = os.path.join(settings.PROJECT_DIR, 'tests', 'TEST.kicad_sym')\nsymbol_libraries_test_path = os.path.join(settings.PROJECT_DIR, 'tests', 'files', 'SYMBOLS')\nfootprint_libraries_test_path = os.path.join(settings.PROJECT_DIR, 'tests', 'files', 'FOOTPRINTS', '')\n\nif ENABLE_API:\n    # Disable Digi-Key API logging\n    digikey_api.disable_api_logger()\n\n    # Test Digi-Key API\n    if 'Digi-Key' in settings.SUPPORTED_SUPPLIERS_API:\n        pretty_test_print('[MAIN]\\tDigi-Key API Test')\n        if not digikey_api.test_api(check_content=True):\n            cprint('[ FAIL ]')\n            cprint('[INFO]\\tFailed to get Digi-Key API token, aborting.')\n        else:\n            cprint('[ PASS ]')\n\n    # Test Mouser API\n    if 'Mouser' in settings.SUPPORTED_SUPPLIERS_API:\n        pretty_test_print('[MAIN]\\tMouser API Test')\n        if not mouser_api.test_api():\n            cprint('[ FAIL ]')\n        else:\n            cprint('[ PASS ]')\n\n    # Test Element14 API (with retry, to avoid the few false positives)\n    if 'Element14' in settings.SUPPORTED_SUPPLIERS_API:\n        for i in range(2):\n            pretty_test_print('[MAIN]\\tElement14 API Test')\n            if not element14_api.test_api() or not element14_api.test_api(store_url='www.newark.com'):\n                cprint('[ FAIL ]')\n            else:\n                cprint('[ PASS ]')\n                break\n\n    # Test LCSC API\n    if 'LCSC' in settings.SUPPORTED_SUPPLIERS_API:\n        pretty_test_print('[MAIN]\\tLCSC API Test')\n        if not lcsc_api.test_api():\n            cprint('[ FAIL ]')\n        else:\n            cprint('[ PASS ]')\n\n    # Test TME API\n    if 'TME' in settings.SUPPORTED_SUPPLIERS_API:\n        pretty_test_print('[MAIN]\\tTME API Test')\n        if not tme_api.test_api():\n            cprint('[ FAIL ]')\n        else:\n            cprint('[ PASS ]')\n\n    # Test AutomationDirect API\n    if 'AutomationDirect' in settings.SUPPORTED_SUPPLIERS_API:\n        pretty_test_print('[MAIN]\\tAutomationDirect API Test')\n        if not automationdirect_api.test_api():\n            cprint('[ FAIL ]')\n        else:\n            cprint('[ PASS ]')\n\n    # Test Jameco API\n    if 'Jameco' in settings.SUPPORTED_SUPPLIERS_API:\n        pretty_test_print('[MAIN]\\tJameco API Test')\n        if not jameco_api.test_api():\n            cprint('[ FAIL ]')\n        else:\n            cprint('[ PASS ]')\n\n    # Test SnapEDA API methods\n    pretty_test_print('[MAIN]\\tSnapEDA API Test')\n    if not snapeda_api.test_snapeda_api():\n        cprint('[ FAIL ]')\n    else:\n        cprint('[ PASS ]')\n\n    cprint('\\n-----')\n\nif ENABLE_INVENTREE:\n    # Setup InvenTree\n    pretty_test_print('\\n[MAIN]\\tSetting up Inventree')\n    setup_inventree()\n    cprint('\\n-----')\n\n# Load test samples\nsamples = config_interface.load_file(os.path.abspath(\n    os.path.join('tests', 'test_samples.yaml')))\nPART_TEST_SAMPLES = {}\nfor category in PART_CATEGORIES:\n    PART_TEST_SAMPLES.update({category: samples[category]})\n\n# Store results\nexit_code = 0\nkicad_results = {}\ninventree_results = {}\n\n# --- TESTS ---\nif __name__ == '__main__':\n    if settings.ENABLE_TEST:\n        if ENABLE_INVENTREE:\n            pretty_test_print('\\n[MAIN]\\tConnecting to Inventree')\n            inventree_connect = inventree_interface.connect_to_server()\n            if inventree_connect:\n                cprint('[ PASS ]')\n            else:\n                cprint('[ FAIL ]')\n\n        if ENABLE_KICAD or ENABLE_INVENTREE:\n            for category in PART_TEST_SAMPLES.keys():\n                cprint(f'\\n[MAIN]\\tCategory: {category.upper()}')\n\n                # For last category, combine creation of KiCad and InvenTree parts\n                last_category = False\n                if ENABLE_KICAD and ENABLE_INVENTREE and category == list(PART_TEST_SAMPLES.keys())[-1]:\n                    last_category = True\n\n                for number, status in PART_TEST_SAMPLES[category].items():\n                    kicad_result = False\n                    inventree_result = False\n                    # Fetch supplier data\n                    supplier_info = inventree_interface.supplier_search(\n                        supplier='Digi-Key',\n                        part_number=number,\n                        test_mode=True,\n                    )\n                    # Translate to form\n                    part_info = inventree_interface.translate_supplier_to_form(\n                        supplier='Digi-Key',\n                        part_info=supplier_info,\n                    )\n                    # Stitch categories and parameters\n                    part_info.update({\n                        'category_tree': [supplier_info['category'], supplier_info['subcategory']],\n                        'parameters': supplier_info['parameters'],\n                        'Symbol': f'{category}:{number}',\n                        'IPN': supplier_info['manufacturer_product_number'],\n                    })\n                    # Update categories\n                    part_info['category_tree'] = inventree_interface.get_categories_from_supplier_data(part_info)\n                    # Needed for tests\n                    part_info['Template'] = part_info['category_tree']\n                    # Display part to be tested\n                    pretty_test_print(f'[INFO]\\tChecking \"{number}\" ({status})')\n\n                    if ENABLE_INVENTREE:\n                        # Adding part information to InvenTree\n                        categories = [None, None]\n                        new_part = False\n                        part_pk = 0\n                        part_data = {}\n\n                        # Create part in InvenTree\n                        new_part, part_pk, part_data = inventree_interface.inventree_create(\n                            part_info=part_info,\n                            kicad=last_category,\n                            symbol=part_info['Symbol'],\n                            show_progress=False,\n                            enable_upload=True if number == 'BSS84-7-F' else False,\n                        )\n\n                        inventree_result = check_result(status, new_part)\n                        pk_list = [data[0] for data in inventree_results.values()]\n\n                        if part_pk != 0 and part_pk not in pk_list:\n                            delete = True\n                        else:\n                            delete = False\n\n                        # Log results\n                        inventree_results.update({number: [part_pk, inventree_result, delete]})\n\n                    if ENABLE_KICAD:\n                        if settings.AUTO_GENERATE_LIB:\n                            create_library(\n                                os.path.dirname(test_library_path),\n                                'TEST',\n                                settings.symbol_template_lib,\n                            )\n\n                        kicad_result, kicad_new_part, kicad_part_name = kicad_interface.inventree_to_kicad(\n                            part_data=part_info,\n                            library_path=test_library_path,\n                            show_progress=False,\n                        )\n\n                        # Log result\n                        if number not in kicad_results.keys():\n                            kicad_results.update({number: kicad_result})\n\n                    # Combine KiCad and InvenTree for less verbose\n                    result = False\n                    if ENABLE_KICAD and ENABLE_INVENTREE:\n                        result = kicad_result and inventree_result\n                    else:\n                        result = kicad_result or inventree_result\n\n                    # Print live results\n                    if result:\n                        cprint('[ PASS ]')\n                    else:\n                        cprint('[ FAIL ]')\n                        exit_code = -1\n                        if ENABLE_KICAD:\n                            cprint(f'[DBUG]\\tkicad_result = {kicad_result}')\n                            cprint(f'[DBUG]\\tkicad_new_part = {kicad_new_part}')\n                            cprint(f'[DBUG]\\tkicad_part_name = {kicad_part_name}')\n                        if ENABLE_INVENTREE:\n                            cprint(f'[DBUG]\\tinventree_result = {inventree_result}')\n                            cprint(f'[DBUG]\\tnew_part = {new_part}')\n                            cprint(f'[DBUG]\\tpart_pk = {part_pk}')\n\n                    # Disable datasheet download/upload after first part (to speed up testing)\n                    # settings.DATASHEET_UPLOAD = False\n\n        if ENABLE_TEST_METHODS and ENABLE_INVENTREE:\n            methods = [\n                'Fuzzy category matching',\n                'Custom parts form',\n                'Digi-Key search missing part number',\n                'Load KiCad library paths',\n                'Add symbol library to user file',\n                'Add footprint library to user file',\n                'Add supplier category',\n                'Sync InvenTree and supplier categories',\n                'Download image/PDF method',\n                'Get category parameters',\n                'Add valid alternate supplier part using part ID',\n                'Add invalid alternate supplier part using part IPN',\n                'Save InvenTree settings',\n                'Load configuration files',\n                'Build InvenTree category tree (file, db and branch)',\n            ]\n            method_success = True\n            # Line return\n            cprint('')\n            cprint('[MAIN]\\tChecking untested methods'.ljust(65))\n\n            for method_idx, method_name in enumerate(methods):\n                pretty_test_print(method_name)\n\n                if method_idx == 0:\n                    # Fuzzy category matching\n                    part_info = {\n                        'category_tree': ['Capacitors', 'Super',],\n                    }\n                    categories = tuple(inventree_interface.get_categories_from_supplier_data(part_info))\n                    if not (categories[0] and categories[1]):\n                        method_success = False\n\n                elif method_idx == 1:\n                    # Custom part form\n                    try:\n                        inventree_interface.translate_form_to_inventree(part_info, categories)\n                        # If the above function does not fail, it's a problem\n                        method_success = False\n                    except KeyError:\n                        pass\n                    \n                    part_info = {\n                        'name': 'part_name',\n                        'description': 'part_desc',\n                        'revision': 'part_rev',\n                        'keywords': 'part_key',\n                        'supplier_name': 'part_supplier',\n                        'supplier_part_number': 'part_sku',\n                        'supplier_link': 'part_link',\n                        'manufacturer_name': 'part_man',\n                        'manufacturer_part_number': 'part_mpn',\n                        'datasheet': 'part_data',\n                        'image': 'part_image',\n                        'IPN': 'part_mpn',\n                    }\n                    if not inventree_interface.translate_form_to_inventree(part_info, categories, is_custom=True):\n                        method_success = False\n\n                elif method_idx == 2:\n                    # Digi-Key search missing part number\n                    search = inventree_interface.supplier_search(supplier='Digi-Key', part_number='')\n                    if search:\n                        method_success = False\n\n                elif method_idx == 3:\n                    # Load KiCad library paths\n                    config_interface.load_library_path(settings.KICAD_CONFIG_PATHS, silent=True)\n                    symbol_libraries_paths = config_interface.load_libraries_paths(settings.KICAD_CONFIG_CATEGORY_MAP, symbol_libraries_test_path)\n                    footprint_libraries_paths = config_interface.load_footprint_paths(settings.KICAD_CONFIG_CATEGORY_MAP, footprint_libraries_test_path)\n                    if not (symbol_libraries_paths and footprint_libraries_paths):\n                        method_success = False\n\n                elif method_idx == 4:\n                    # Add symbol library to user file\n                    add_symbol_lib = config_interface.add_library_path(user_config_path=settings.KICAD_CONFIG_CATEGORY_MAP,\n                                                                       category='category_test',\n                                                                       symbol_library='symbol_library_test')\n                    if not add_symbol_lib:\n                        method_success = False\n\n                elif method_idx == 5:\n                    # Add footprint library to user file\n                    add_footprint_lib = config_interface.add_footprint_library(user_config_path=settings.KICAD_CONFIG_CATEGORY_MAP,\n                                                                               category='category_test',\n                                                                               library_folder='footprint_folder_test')\n                    if not add_footprint_lib:\n                        method_success = False\n\n                elif method_idx == 6:\n                    # Add supplier category\n                    categories = {\n                        'Capacitors':\n                        {'Super': 'Super'}\n                    }\n                    add_category = config_interface.add_supplier_category(categories, settings.CONFIG_DIGIKEY_CATEGORIES)\n                    if not add_category:\n                        method_success = False\n\n                elif method_idx == 7:\n                    # Sync InvenTree and Supplier categories\n                    sync_categories = config_interface.sync_inventree_supplier_categories(inventree_config_path=settings.CONFIG_CATEGORIES,\n                                                                                          supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES)\n                    if not sync_categories:\n                        method_success = False\n\n                elif method_idx == 8:\n                    test_image_urllib = 'https://media.digikey.com/Renders/Diodes%20Renders/31;%20SOD-123;%20;%202.jpg'\n                    test_image_requestslib = 'https://www.newark.com/productimages/standard/en_GB/GE2SOD12307-40.jpg'\n                    test_pdf_urllib = 'https://www.seielect.com/Catalog/SEI-CF_CFM.pdf'\n                    # Test different download methods for images\n                    if not download_with_retry(test_image_urllib, './image1.jpg', silent=True, filetype='Image'):\n                        print(' [1] ')\n                        method_success = False\n                    if not download_with_retry(test_image_requestslib, './image2.jpg', silent=True, filetype='Image'):\n                        print(' [2] ')\n                        method_success = False\n                    # Test PDF\n                    if not download_with_retry(test_pdf_urllib, './datasheet.pdf', silent=True, filetype='PDF'):\n                        print(' [3] ')\n                        method_success = False\n                    # Wrong folder\n                    if download_with_retry(test_pdf_urllib, './myfolder/datasheet.pdf', silent=True, filetype='PDF'):\n                        print(' [4] ')\n                        method_success = False\n                    # Test erroneous URL\n                    if download_with_retry('http', '', silent=True):\n                        print(' [5] ')\n                        method_success = False\n                    # Test empty URL\n                    if download_with_retry('', '', silent=True):\n                        print(' [6] ')\n                        method_success = False\n\n                elif method_idx == 9:\n                    # Test InvenTree category parameters\n                    if inventree_api.get_category_parameters(1):\n                        method_success = False\n\n                elif method_idx == 10:\n                    # Test manufacturer and supplier alternates using Part ID\n                    part_info = {\n                        \"datasheet\": \"https://search.murata.co.jp/Ceramy/image/img/A01X/G101/ENG/GRM155R71C104KA88-01.pdf\",\n                        \"manufacturer_name\": \"Murata Electronics\",\n                        \"manufacturer_part_number\": \"GRM155R71C104KA88D\",\n                        \"supplier_link\": \"https://www.digikey.com/en/products/detail/murata-electronics/GRM155R71C104KA88D/675947\",\n                        \"supplier_name\": \"Digi-Key\",\n                        \"supplier_part_number\": \"490-3261-1-ND\",\n                        \"name\": \"\",\n                        \"description\": \"\",\n                        \"revision\": \"\",\n                        \"keywords\": \"\",\n                        \"IPN\": \"\",\n                        \"image\": \"\",\n                        \"parameters\": {},\n                        \"pricing\": {},\n                    }\n                    if not inventree_interface.inventree_create_alternate(part_info=part_info,\n                                                                          part_id='1',\n                                                                          show_progress=False, ):\n                        method_success = False\n\n                elif method_idx == 11:\n                    # Test manufacturer and supplier alternates using Part IPN\n                    if inventree_interface.inventree_create_alternate(part_info=part_info,\n                                                                      part_ipn='CAP-000001-00',\n                                                                      show_progress=False, ):\n                        method_success = False\n\n                elif method_idx == 12:\n                    # Save InvenTree settings\n                    if not config_interface.save_inventree_user_settings(\n                        enable=True,\n                        server='http://127.0.0.1:8000',\n                        username='admin',\n                        password='admin',\n                        enable_proxy=False,\n                        proxies={},\n                        datasheet_upload=True,\n                        user_config_path=settings.INVENTREE_CONFIG,\n                        pricing_upload=True,\n                    ):\n                        method_success = False\n\n                elif method_idx == 13:\n                    # Select one configuration file\n                    element14_config = os.path.join(\n                        settings.USER_SETTINGS['USER_FILES'],\n                        'element14_config.yaml',\n                    )\n                    # Delete the user configuration file\n                    os.remove(element14_config)\n                    # Try to load this file\n                    if config_interface.load_file(element14_config):\n                        method_success = False\n\n                    if method_success:\n                        # Load user configuration files\n                        if not settings.load_user_config():\n                            method_success = False\n\n                    if method_success:\n                        # Load configuration files with incorrect paths\n                        if config_interface.load_user_config_files('', ''):\n                            method_success = False\n\n                elif method_idx == 14:\n                    # Reload categories from file\n                    cat_from_file = inventree_interface.build_category_tree(reload=False)\n                    if isinstance(cat_from_file, type(list)):\n                        print(f'{type(cat_from_file)} != list')\n                        method_success = False\n\n                    if method_success:\n                        # Reload categories from InvenTree database\n                        cat_from_db = inventree_interface.build_category_tree(reload=True)\n                        if len(cat_from_db) != len(cat_from_file):\n                            print(f'{len(cat_from_db)} != {len(cat_from_file)}')\n                            method_success = False\n                    \n                    if method_success:\n                        # Reload category branch\n                        cat_branch = inventree_interface.build_category_tree(category='Crystals and Oscillators')\n                        if len(cat_branch) != 3:\n                            print(f'{len(cat_branch)} != 3')\n                            method_success = False\n\n                if method_success:\n                    cprint('[ PASS ]')\n                else:\n                    cprint('[ FAIL ]')\n                    exit_code = -1\n                    break\n\n            # Line return\n            cprint('')\n\n    sys.exit(exit_code)\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\nignore =\n\t# - W191 - indentation contains tab\n\t# W191,\n\t# - W293 - blank lines contain whitespace\n\tW293,\n\tW605,\n\t# - E501 - line too long (82 characters)\n\tE501, E722,\n\t# - C901 - function is too complex\n\tC901,\n\t# - N802 - function name should be lowercase\n\tN802,\n\t# - N806 - variable should be lowercase\n\tN806,\n\tN812,\n\tF824,\nexclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*,*ci_*.py*\nmax-complexity = 20\n"
  },
  {
    "path": "tasks.py",
    "content": "import webbrowser\n\nfrom kintree.common.tools import cprint\nfrom invoke import UnexpectedExit, task\n\n\n@task\ndef install(c, is_install=True):\n    \"\"\"\n    Install Ki-nTree dependencies\n    \"\"\"\n\n    if is_install:\n        cprint('[MAIN]\\tInstalling required dependencies')\n        c.run('pip install -U wheel', hide='out')\n    else:\n        cprint('[MAIN]\\tUpdating required dependencies')\n    c.run('pip install -U -r requirements.txt', hide='out')\n\n\n@task\ndef update(c):\n    \"\"\"\n    Update Ki-nTree dependencies\n    \"\"\"\n\n    install(c, is_install=False)\n\n\n@task\ndef clean(c):\n    \"\"\"\n    Clean project folder\n    \"\"\"\n\n    cprint('[MAIN]\\tCleaning project directory')\n    try:\n        c.run('find . -name __pycache__ | xargs rm -r', hide='err')\n    except UnexpectedExit:\n        pass\n    try:\n        c.run('rm .coverage', hide='err')\n    except UnexpectedExit:\n        pass\n    try:\n        c.run('rm .coverage.*', hide='err')\n    except UnexpectedExit:\n        pass\n    try:\n        c.run('rm -r dist/ build/ htmlcov', hide='err')\n    except UnexpectedExit:\n        pass\n    \n\n@task(pre=[clean])\ndef build(c):\n    \"\"\"\n    Build Ki-nTree into dist/wheel\n    \"\"\"\n\n    try:\n        c.run('pip show poetry', hide=True)\n    except UnexpectedExit:\n        c.run('pip install -U poetry', hide=True)\n\n    cprint('[MAIN]\\tBuilding Ki-nTree GUI into \"dist\" directory')\n    c.run('poetry build', hide=True)\n\n\n@task\ndef setup_inventree(c):\n    \"\"\"\n    Setup InvenTree server\n    \"\"\"\n\n    c.run('python -m kintree.setup_inventree')\n\n\n@task\ndef coverage_report(c, open_browser=True):\n    \"\"\"\n    Show coverage report\n    \"\"\"\n\n    cprint('[MAIN]\\tBuilding coverage report')\n    c.run('coverage report')\n    c.run('coverage html')\n    if open_browser:\n        webbrowser.open('htmlcov/index.html', new=2)\n\n\n@task\ndef test(c, enable_api=0):\n    \"\"\"\n    Run Ki-nTree tests\n    \"\"\"\n\n    try:\n        c.run('pip show coverage', hide=True)\n    except UnexpectedExit:\n        c.run('pip install -U coverage', hide=True)\n\n    cprint('[MAIN]\\tRunning tests using coverage\\n-----')\n    # Start InvenTree server\n    c.run('cd InvenTree/ && inv server && cd ..', asynchronous=True)\n    c.run('sleep 15')\n    # Copy test files\n    c.run('cp -r tests/ kintree/')\n    # Run Tests\n    run_tests = c.run(f'coverage run run_tests.py {enable_api}')\n    if run_tests.exited == 0:\n        coverage_report(c, open_browser=False)\n\n\n@task\ndef python_badge(c):\n    \"\"\"\n    Make badge for supported versions of Python\n    \"\"\"\n\n    cprint('[MAIN]\\tInstall pybadges')\n    c.run('pip install pybadges pip-autoremove', hide=True)\n    cprint('[MAIN]\\tCreate badge')\n    c.run('python -m pybadges --left-text=\"python\" --right-text=\"3.9 | 3.10 | 3.11 | 3.12\" '\n          '--whole-link=\"https://www.python.org/\" --browser --embed-logo '\n          '--logo=\"https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/python.svg\"')\n    cprint('[MAIN]\\tUninstall pybadges')\n    c.run('pip-autoremove pybadges -y', hide=True)\n    c.run('pip uninstall pip-autoremove -y', hide=True)\n\n\n@task\ndef style(c):\n    \"\"\"\n    Run PEP style checks against Ki-nTree sourcecode\n    \"\"\"\n\n    c.run('pip install -U flake8', hide=True)\n    print(\"Running PEP style checks...\")\n    c.run('flake8 --extend-ignore W503 \\\n        tasks.py run_tests.py kintree_gui.py kintree/kintree_gui.py kintree/setup_inventree.py \\\n        kintree/common/ kintree/config/ kintree/database/ kintree/kicad/*.py kintree/search/*.py \\\n        kintree/gui/gui.py kintree/gui/views/*.py')\n\n\n@task\ndef gui(c, browser=False):\n    \"\"\"\n    Open GUI in either app or browser mode\n    \"\"\"\n    if browser:\n        c.run('python -m kintree_gui b')\n        return\n    c.run('python -m kintree_gui')\n"
  },
  {
    "path": "tests/files/FOOTPRINTS/RF.pretty/Skyworks_SKY13575_639LF.kicad_mod",
    "content": "(module \"Skyworks_SKY13575_639LF\" (layer F.Cu) (tedit 5D6EEFCB)\n  (descr \"http://www.skyworksinc.com/uploads/documents/SKY13575_639LF_203270D.pdf\")\n  (tags \"Skyworks\")\n  (attr smd)\n  (fp_text reference \"REF**\" (at 0 -2.45) (layer F.SilkS)\n    (effects (font (size 1 1) (thickness 0.15)))\n  )\n  (fp_text value \"Skyworks_SKY13575_639LF\" (at -0.01 2.01) (layer F.Fab)\n    (effects (font (size 1 1) (thickness 0.15)))\n  )\n  (fp_line (start -0.96 -0.89) (end -0.71 -0.89) (layer F.SilkS) (width 0.12))\n  (fp_text user \"%R\" (at 0 -1.55) (layer F.Fab)\n    (effects (font (size 0.5 0.5) (thickness 0.075)))\n  )\n  (fp_line (start 1.22 -1.22) (end -1.22 -1.22) (layer F.CrtYd) (width 0.05))\n  (fp_line (start 1.22 1.22) (end 1.22 -1.22) (layer F.CrtYd) (width 0.05))\n  (fp_line (start -1.22 1.22) (end 1.22 1.22) (layer F.CrtYd) (width 0.05))\n  (fp_line (start -1.22 -1.22) (end -1.22 1.22) (layer F.CrtYd) (width 0.05))\n  (fp_line (start -0.8 -0.4) (end -0.8 0.8) (layer F.Fab) (width 0.1))\n  (fp_line (start -0.4 -0.8) (end -0.8 -0.4) (layer F.Fab) (width 0.1))\n  (fp_line (start 0.8 -0.8) (end -0.4 -0.8) (layer F.Fab) (width 0.1))\n  (fp_line (start 0.8 0.8) (end 0.8 -0.8) (layer F.Fab) (width 0.1))\n  (fp_line (start -0.8 0.8) (end 0.8 0.8) (layer F.Fab) (width 0.1))\n  (pad \"2\" smd rect (at -0.772 -0.2) (size 0.4 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"))\n  (pad \"3\" smd rect (at -0.772 0.2) (size 0.4 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"))\n  (pad \"9\" smd rect (at 0.772 0.2) (size 0.4 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"))\n  (pad \"10\" smd rect (at 0.772 -0.2) (size 0.4 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"))\n  (pad \"6\" smd rect (at 0 0.772 90) (size 0.4 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"))\n  (pad \"13\" smd rect (at 0 -0.772 90) (size 0.4 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"))\n  (pad \"4\" smd custom (at -0.866 0.602) (size 0.2 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy -0.106 -0.102) (xy 0.239 -0.102) (xy 0.239 -0.054) (xy 0.175 0.098) (xy -0.106 0.098)\n) (width 0))\n    ))\n  (pad \"1\" smd custom (at -0.866 -0.601) (size 0.2 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy -0.106 0.101) (xy 0.239 0.101) (xy 0.239 0.053) (xy 0.175 -0.099) (xy -0.106 -0.099)\n) (width 0))\n    ))\n  (pad \"5\" smd custom (at -0.4 0.871) (size 0.19 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy 0.1 -0.299) (xy 0.1 0.101) (xy -0.1 0.101) (xy -0.1 -0.119) (xy -0.025 -0.299)\n) (width 0))\n    ))\n  (pad \"7\" smd custom (at 0.4 0.87) (size 0.19 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy -0.1 -0.298) (xy -0.1 0.102) (xy 0.1 0.102) (xy 0.1 -0.118) (xy 0.025 -0.298)\n) (width 0))\n    ))\n  (pad \"12\" smd custom (at 0.398 -0.87) (size 0.19 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy -0.098 -0.102) (xy -0.098 0.298) (xy 0.027 0.298) (xy 0.102 0.118) (xy 0.102 -0.102)\n) (width 0))\n    ))\n  (pad \"14\" smd custom (at -0.4 -0.869) (size 0.19 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy 0.1 -0.103) (xy -0.1 -0.103) (xy -0.1 0.117) (xy -0.025 0.297) (xy 0.1 0.297)\n) (width 0))\n    ))\n  (pad \"8\" smd custom (at 0.871 0.6) (size 0.2 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy 0.101 -0.1) (xy 0.101 0.1) (xy -0.18 0.1) (xy -0.244 -0.052) (xy -0.244 -0.1)\n) (width 0))\n    ))\n  (pad \"11\" smd custom (at 0.872 -0.6) (size 0.2 0.2) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\")\n    (zone_connect 2)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy 0.1 -0.1) (xy 0.1 0.1) (xy -0.245 0.1) (xy -0.245 0.052) (xy -0.181 -0.1)\n) (width 0))\n    ))\n  (pad \"15\" smd custom (at 0 0) (size 0.29 0.29) (layers \"F.Cu\" \"F.Mask\")\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy -0.14 -0.36) (xy -0.36 -0.15) (xy -0.36 0.36) (xy 0.36 0.36) (xy 0.36 -0.36)\n) (width 0))\n    ))\n  (pad \"\" smd rect (at 0.185 -0.185) (size 0.29 0.29) (layers \"F.Paste\"))\n  (pad \"\" smd rect (at 0.185 0.185) (size 0.29 0.29) (layers \"F.Paste\"))\n  (pad \"\" smd rect (at -0.185 0.185) (size 0.29 0.29) (layers \"F.Paste\"))\n  (pad \"\" smd custom (at -0.17 -0.17) (size 0.1 0.1) (layers \"F.Paste\")\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy 0.13 0.13) (xy 0.13 -0.16) (xy 0.04 -0.16) (xy -0.16 0.03) (xy -0.16 0.13)\n) (width 0))\n    ))\n  (model \"${KISYS3DMOD}/RF.3dshapes/Skyworks_SKY13575_639LF.wrl\"\n    (at (xyz 0 0 0))\n    (scale (xyz 1 1 1))\n    (rotate (xyz 0 0 0))\n  )\n)\n"
  },
  {
    "path": "tests/files/FOOTPRINTS/RF.pretty/Skyworks_SKY65404-31.kicad_mod",
    "content": "(module Skyworks_SKY65404-31 (layer F.Cu) (tedit 5C4622B0)\n  (descr http://www.skyworksinc.com/uploads/documents/SKY65404_31_201512K.pdf)\n  (tags Skyworks)\n  (attr smd)\n  (fp_text reference REF** (at 0 -1.75) (layer F.SilkS)\n    (effects (font (size 1 1) (thickness 0.15)))\n  )\n  (fp_text value Skyworks_SKY65404-31 (at 0 1.85) (layer F.Fab)\n    (effects (font (size 1 1) (thickness 0.15)))\n  )\n  (fp_line (start -1.15 -1) (end 1.15 -1) (layer F.CrtYd) (width 0.05))\n  (fp_line (start -1.15 1) (end -1.15 -1) (layer F.CrtYd) (width 0.05))\n  (fp_line (start 1.15 1) (end -1.15 1) (layer F.CrtYd) (width 0.05))\n  (fp_line (start 1.15 -1) (end 1.15 1) (layer F.CrtYd) (width 0.05))\n  (fp_text user %R (at 0 0 270) (layer F.Fab)\n    (effects (font (size 0.5 0.5) (thickness 0.075)))\n  )\n  (fp_line (start -0.75 -0.375) (end -0.375 -0.75) (layer F.Fab) (width 0.1))\n  (fp_line (start -0.75 0.75) (end -0.75 -0.375) (layer F.Fab) (width 0.1))\n  (fp_line (start 0.75 0.75) (end -0.75 0.75) (layer F.Fab) (width 0.1))\n  (fp_line (start 0.75 -0.75) (end 0.75 0.75) (layer F.Fab) (width 0.1))\n  (fp_line (start -0.375 -0.75) (end 0.75 -0.75) (layer F.Fab) (width 0.1))\n  (fp_line (start -0.75 0.85) (end 0.75 0.85) (layer F.SilkS) (width 0.12))\n  (fp_line (start 0 -0.85) (end 0.75 -0.85) (layer F.SilkS) (width 0.12))\n  (pad 7 smd custom (at 0 0) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask)\n    (zone_connect 0)\n    (options (clearance outline) (anchor rect))\n    (primitives\n      (gr_poly (pts\n         (xy 0.35 -0.6) (xy 0.35 0) (xy 0.35 0.6) (xy 0.1 0.6) (xy 0.1 0.65)\n         (xy -0.1 0.65) (xy -0.1 0.6) (xy -0.35 0.6) (xy -0.35 -0.45) (xy -0.2 -0.6)\n         (xy -0.1 -0.6) (xy -0.1 -0.65) (xy 0.1 -0.65) (xy 0.1 -0.6)) (width 0))\n    ))\n  (pad 6 smd roundrect (at 0.725 -0.5) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25))\n  (pad 5 smd roundrect (at 0.725 0) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25))\n  (pad 4 smd roundrect (at 0.725 0.5) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25))\n  (pad 3 smd roundrect (at -0.725 0.5) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25))\n  (pad 2 smd roundrect (at -0.725 0) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25))\n  (pad 1 smd roundrect (at -0.725 -0.5) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25))\n  (model ${KISYS3DMOD}/RF.3dshapes/Skyworks_SKY65404-31.wrl\n    (at (xyz 0 0 0))\n    (scale (xyz 1 1 1))\n    (rotate (xyz 0 0 0))\n  )\n)\n"
  },
  {
    "path": "tests/files/SYMBOLS/TEST.kicad_sym",
    "content": "(kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor)\n  (symbol \"C0402C100J3GACTU\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"C\" (at 0 4.064 0)\n      (effects (font (size 1.524 1.524)))\n    )\n    (property \"Value\" \"C0402C100J3GACTU\" (at 0 -11.43 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Footprint\" \"Capacitors:C0402\" (at 0 -26.67 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Datasheet\" \"http://ksim.kemet.com/Plots/SpicePlots.aspx\" (at 0 -13.97 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Supplier Part Number\" \"399-7746-1-ND\" (at 0 -16.51 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Manufacturer\" \"KEMET\" (at 0 -19.05 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"C0402C100J3GACTU\" (at 0 -21.59 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Description\" \"CAP 10PF 25V C0G/NP0 0402\" (at 0 -24.13 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Capacitance (Farad)\" \"10pF\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Tolerance (%)\" \" ±5%\" (at 5.08 -3.81 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Voltage Rated (Volt)\" \"25V\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Variant\" \"dnp\" (at 0 -29.21 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"10pF 25V 0402\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"CAP CER 10PF 25V C0G/NP0 0402\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"C0402C100J3GACTU_0_1\"\n      (polyline\n        (pts\n          (xy -1.27 0)\n          (xy -1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -0.889 1.905)\n          (xy -0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 0.889 1.905)\n          (xy 0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.905 0)\n          (xy 2.54 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"C0402C100J3GACTU_1_1\"\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n  (symbol \"C0402C103K8RACTU\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"C\" (at 0 4.064 0)\n      (effects (font (size 1.524 1.524)))\n    )\n    (property \"Value\" \"C0402C103K8RACTU\" (at 0 -11.43 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Footprint\" \"Capacitors:C0402\" (at 0 -26.67 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Datasheet\" \"https://api.kemet.com/component-edge/download/datasheet/C0402C103K8RACTU.pdf\" (at 0 -13.97 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"C0402C103K8RACTU\" (at 0 -16.51 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Distributors\" \"Digi-Key\" (at 0 -19.05 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Distributors References\" \"399-7759-1-ND\" (at 0 -21.59 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Description\" \"CAP CER 10nF 10V X7R 0402\" (at 0 -24.13 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Capacitance (Farad)\" \"10nF\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Tolerance (%)\" \"±10%\" (at 7.62 -3.81 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Voltage Rated (Volt)\" \"10V\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Variant\" \"dnp\" (at 0 -29.21 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"10nF 10V 0402\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"CAP CER 10nF 10V X7R 0402\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"C0402C103K8RACTU_0_1\"\n      (polyline\n        (pts\n          (xy -1.27 0)\n          (xy -1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -0.889 1.905)\n          (xy -0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 0.889 1.905)\n          (xy 0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.905 0)\n          (xy 2.54 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"C0402C103K8RACTU_1_1\"\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n  (symbol \"GRM155R70J105KA12J\" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"C\" (at 0 4.064 0)\n      (effects (font (size 1.524 1.524)))\n    )\n    (property \"Value\" \"GRM155R70J105KA12J\" (at 0 -11.43 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Footprint\" \"Capacitors:C0402\" (at 0 -26.67 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Datasheet\" \"https://ds.murata.co.jp/simsurfing/mlcc.html?partnumbers=%5B%22GRM155R70J105KA12%22%5D&oripartnumbers=%5B%22GRM155R70J105KA12J%22%5D&rgear=jomoqke&rgearinfo=ca\" (at 0 -13.97 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Supplier Part Number\" \"490-13339-1-ND\" (at 0 -16.51 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Manufacturer\" \"Murata Electronics\" (at 0 -19.05 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"GRM155R70J105KA12J\" (at 0 -21.59 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Description\" \"CAP CER 1UF 6.3V X7R 0402\" (at 0 -24.13 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Capacitance (Farad)\" \"1µF\" (at 0 -3.81 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Tolerance (%)\" \"±10%\" (at 5.08 -3.81 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"Voltage Rated (Volt)\" \"6.3V\" (at 0 -6.35 0)\n      (effects (font (size 1.27 1.27)))\n    )\n    (property \"Variant\" \"dnp\" (at 0 -29.21 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_keywords\" \"1uF 6.3V 0402\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"CAP CER 1UF 6.3V X7R 0402\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"GRM155R70J105KA12J_0_1\"\n      (polyline\n        (pts\n          (xy -1.27 0)\n          (xy -1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy -0.889 1.905)\n          (xy -0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 0.889 1.905)\n          (xy 0.889 -1.905)\n        )\n        (stroke (width 0.254) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.27 0)\n          (xy 1.016 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n      (polyline\n        (pts\n          (xy 1.905 0)\n          (xy 2.54 0)\n        )\n        (stroke (width 0) (type default))\n        (fill (type none))\n      )\n    )\n    (symbol \"GRM155R70J105KA12J_1_1\"\n      (pin passive line (at -3.81 0 0) (length 2.54)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 3.81 0 180) (length 2.54)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n  (symbol \"HTSH-110-01-F-DV-007-K\" (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)\n    (property \"Reference\" \"J\" (at 0 15.24 0)\n      (effects (font (size 1.524 1.524)))\n    )\n    (property \"Value\" \"HTSH-110-01-F-DV-007-K\" (at 0 -29.21 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Footprint\" \"Connectors:FTSH-110-01-F-DV-007-K\" (at 0 -44.45 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Datasheet\" \"https://s3.amazonaws.com/catalogspreads-pdf/PAGE119%20.100%20SFH11%20SERIES%20FEMALE%20HDR%20ST%20RA.pdf\" (at 0 -31.75 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Supplier Part Number\" \"SAM11264-ND\" (at 0 -34.29 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Manufacturer\" \"Samtec Inc.\" (at 0 -36.83 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Manufacturer Part Number\" \"FTSH-110-01-F-DV-007-K\" (at 0 -39.37 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Description\" \"CONN HEADER MALE 20POS .05\\\" (1.27mm) Keying Shroud\" (at 0 -41.91 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"Variant\" \"dnp\" (at 0 -46.99 0)\n      (effects (font (size 1.524 1.524)) hide)\n    )\n    (property \"ki_keywords\" \"HEADER FEMALE 20POS RA\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (property \"ki_description\" \"CONN HEADER FMALE 20PS .1\\\" R/A AU\" (at 0 0 0)\n      (effects (font (size 1.27 1.27)) hide)\n    )\n    (symbol \"HTSH-110-01-F-DV-007-K_0_1\"\n      (rectangle (start -5.08 12.7) (end 5.08 -12.7)\n        (stroke (width 0) (type default))\n        (fill (type background))\n      )\n    )\n    (symbol \"HTSH-110-01-F-DV-007-K_1_1\"\n      (pin passive line (at -10.16 11.43 0) (length 5.08)\n        (name \"1\" (effects (font (size 1.27 1.27))))\n        (number \"1\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 1.27 180) (length 5.08)\n        (name \"10\" (effects (font (size 1.27 1.27))))\n        (number \"10\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 -1.27 0) (length 5.08)\n        (name \"11\" (effects (font (size 1.27 1.27))))\n        (number \"11\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 -1.27 180) (length 5.08)\n        (name \"12\" (effects (font (size 1.27 1.27))))\n        (number \"12\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 -3.81 0) (length 5.08)\n        (name \"13\" (effects (font (size 1.27 1.27))))\n        (number \"13\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 -3.81 180) (length 5.08)\n        (name \"14\" (effects (font (size 1.27 1.27))))\n        (number \"14\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 -6.35 0) (length 5.08)\n        (name \"15\" (effects (font (size 1.27 1.27))))\n        (number \"15\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 -6.35 180) (length 5.08)\n        (name \"16\" (effects (font (size 1.27 1.27))))\n        (number \"16\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 -8.89 0) (length 5.08)\n        (name \"17\" (effects (font (size 1.27 1.27))))\n        (number \"17\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 -8.89 180) (length 5.08)\n        (name \"18\" (effects (font (size 1.27 1.27))))\n        (number \"18\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 -11.43 0) (length 5.08)\n        (name \"19\" (effects (font (size 1.27 1.27))))\n        (number \"19\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 11.43 180) (length 5.08)\n        (name \"2\" (effects (font (size 1.27 1.27))))\n        (number \"2\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 -11.43 180) (length 5.08)\n        (name \"20\" (effects (font (size 1.27 1.27))))\n        (number \"20\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 8.89 0) (length 5.08)\n        (name \"3\" (effects (font (size 1.27 1.27))))\n        (number \"3\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 8.89 180) (length 5.08)\n        (name \"4\" (effects (font (size 1.27 1.27))))\n        (number \"4\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 6.35 0) (length 5.08)\n        (name \"5\" (effects (font (size 1.27 1.27))))\n        (number \"5\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 6.35 180) (length 5.08)\n        (name \"6\" (effects (font (size 1.27 1.27))))\n        (number \"6\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at 10.16 3.81 180) (length 5.08)\n        (name \"8\" (effects (font (size 1.27 1.27))))\n        (number \"8\" (effects (font (size 1.27 1.27))))\n      )\n      (pin passive line (at -10.16 1.27 0) (length 5.08)\n        (name \"9\" (effects (font (size 1.27 1.27))))\n        (number \"9\" (effects (font (size 1.27 1.27))))\n      )\n    )\n  )\n)\n"
  },
  {
    "path": "tests/files/digikey_config.yaml",
    "content": "EXTRA_FIELDS: null\nSEARCH_DATASHEET: null\nSEARCH_DESCRIPTION: null\nSEARCH_KEYWORDS: null\nSEARCH_MANUFACTURER: null\nSEARCH_MPN: null\nSEARCH_NAME: null\nSEARCH_REVISION: null\nSEARCH_SKU: null\nSEARCH_SUPPLIER_URL: null\nSUPPLIER_INVENTREE_NAME: Digi-Key"
  },
  {
    "path": "tests/files/inventree_dev.yaml",
    "content": "DATASHEET_UPLOAD: true\nENABLE: true\nENABLE_PROXY: false\nPASSWORD: !!binary |\n  WVdSdGFXND0=\nSERVER_ADDRESS: http://127.0.0.1:8000/\nUSERNAME: admin\nPROXIES: null\n"
  },
  {
    "path": "tests/files/kicad_map.yaml",
    "content": "KICAD_FOOTPRINTS:\n  Capacitors:\n  - Capacitors\n  Circuit Protections:\n  - Fuses\n  Connectors:\n  - Connectors\n  Crystals and Oscillators:\n  - Crystals\n  Diodes:\n  - Diodes\n  Inductors:\n  - Inductors\n  Integrated Circuits:\n  - Package_SO\n  - Package_DFN\n  - Package_QFP\n  - Package_QFN\n  Mechanicals:\n  - Mechanicals\n  - Switches\n  Miscellaneous:\n  - Misc\n  Power Management:\n  - Package_SO\n  - Package_GEN\n  - Package_DFN\n  RF:\n  - Antennas\n  Resistors:\n  - Resistors\n  Transistors:\n  - Package_SO\nKICAD_LIBRARIES:\n  Capacitors:\n  - Capacitors\n  Circuit Protections:\n  - Circuit_Protections\n  Connectors:\n  - Connectors\n  Crystals and Oscillators:\n  - Crystals_Oscillators\n  Diodes:\n  - Diodes\n  Inductors:\n  - Inductors\n  Integrated Circuits:\n  - Integrated_Circuits\n  Mechanicals:\n  - Mechanicals\n  Miscellaneous:\n  - Miscellaneous\n  Power Management:\n  - Power_Management\n  RF:\n  - RF\n  Resistors:\n  - Resistors\n  Transistors:\n  - Transistors\nKICAD_TEMPLATES:\n  Capacitors:\n    Aluminium: capacitor-polarized\n    Ceramic: capacitor\n    Default: capacitor\n    Polymer: capacitor-polarized\n    Super Capacitors: capacitor-polarized\n    Tantalum: capacitor-polarized\n  Circuit Protections:\n    Default: protection-unidir\n    Fuse: fuse\n    TVS: protection-unidir\n  Connectors:\n    Default: connector\n  Crystals and Oscillators:\n    Crystal 2P: crystal-2p\n    Default: crystal-2p\n    Oscillator 4P: oscillator-4p\n  Diodes:\n    Default: diode-standard\n    LED: diode-led\n    Schottky: diode-schottky\n    Standard: diode-standard\n    Zener: diode-zener\n  Inductors:\n    Default: inductor\n    Ferrite Bead: ferrite-bead\n    Power: inductor\n  Integrated Circuits:\n    Default: integrated-circuit\n    EEPROM SOT23: eeprom-sot23\n  Mechanicals:\n    Default: default\n  Power Management:\n    Default: integrated-circuit\n  RF:\n    Default: integrated-circuit\n  Resistors:\n    Default: resistor\n    Surface Mount: resistor-sm\n    Through Hole: resistor\n  Transistors:\n    Default: transistor-nfet\n    N-Channel FET: transistor-nfet\n    NPN: transistor-npn\n    P-Channel FET: transistor-pfet\n    PNP: transistor-pnp\n"
  },
  {
    "path": "tests/test_samples.yaml",
    "content": "Capacitors:\n  ## 0402 0.1u 16V X7R\n  0402B104K160CT: original\n  # Equivalent\n  CL05B104KO5NNNC: alternate_mpn\n  # 'Fake' Equivalent (Temperature Grade)\n  C0402C104K4PACTU: fake_alternate\n  # Aluminum\n  EEE-HA1A101WP: original\n  # Tantalum\n  T491A106K010AT: original\n  # Aluminum-Polymer\n  RSA0J331MCN1GS: original\n  # Tantalum-Polymer\n  10TPE68M: original\n  # Super-Capacitor\n  CPH3225A: original\n\nCircuit Protections:\n  # TVS\n  SMAJ5.0CA-13-F: original\n  # ESD\n  ESDALC14-1BF4: original\n  # Fuse\n  0603SFP150F/32-2: original\n  # PTC\n  0ZCM0005FF2G: original\n\nConnectors:\n  # Board-to-board\n  10144517-061802LF: original\n  # Interface, microSD\n  DM3CS-SF: original\n  # Interface, USB Type-C\n  USB4085-GF-A: original\n  # Interface, Audio Jack\n  SJ-3524-SMT-TR: original\n  # Interface, Ethernet\n  J3011G21DNLT: original\n  # Coaxial\n  H.FL-R-SMT(C)(10): original\n  # FPC\n  0522713069: original\n  # Header Male (Power)\n  0533980271: original\n  # Header Female\n  20021321-00010C4LF: original\n\nCrystals and Oscillators:\n  # 2-pin 32.768kHz Crystal\n  ABS07-120-32.768KHZ-T: original\n  # 4-pin 40MHz Oscillator\n  KC2520C40.0000C2YE00: original\n\nDiodes:\n  # General Purpose\n  1N4148W-7-F: original\n  # Zener\n  MMSZ5231B-7-F: original\n  # LED\n  LTST-C191KGKT: original\n  # Schottky\n  PMEG10010ELRX: original\n\nInductors:\n  # Power\n  NRS5030T4R7MMGJV: original\n  # Ferrite Bead\n  BLM18AG601SN1D: original\n\nIntegrated Circuits:\n  # Logic\n  SN74LVC2T45DCTR: original\n  # Memory\n  MX25R6435FM2IL0: original\n  # Microcontroller\n  STM32F730R8T6: original\n  # Interface\n  MAX9867ETJ+T: original\n  # Sensor\n  BME280: original\n\nMechanicals:\n  # Switch\n  TL1105SF160Q: original\n\nPower Management:\n  # Buck, Adjustable\n  AOZ1282CI: original\n  # Buck, Fixed\n  SC189CULTRT: original\n  # Boost, Fixed\n  TPS61221DCKR: original\n  # LDO, Adjustable\n  LP3982IMM-ADJ/NOPB: original\n  # LDO, Fixed\n  AP2210K-3.3TRG1: original\n\nResistors:\n  # 10K/5%\n  ### Surface Mount\n  ## 0603\n  RK73B1JTTD103J: original\n  # Equivalent\n  ERJ-3GEYJ103V: alternate_mpn\n  # 'Fake' Equivalent (Tolerance)\n  RC0603FR-0710KL: fake_alternate\n  ## 0402\n  RC0402FR-07100RL: original\n  # Equivalent\n  CRCW0402100RFKEDC: alternate_mpn\n  # 'Fake' Equivalent (Power)\n  ERJ-2GEJ101X: fake_alternate\n  ### Through Hole\n  CF14JT10K0: original\n  # Equivalent\n  CFR16J10K: alternate_mpn\n  # 'Fake' Equivalent (Temperature)\n  CBT25J10K: fake_alternate\n  ### Array\n  # ('original', 'EXB-28V103JX'),\n  # # Equivalent\n  # ('alternate_mpn', 'CAT10-103J4LF'),\n  # # 'Fake' Equivalent (Power)\n  # ('fake_alternate', 'EXB-N8V103JX'),\n  # ### Potentiometer\n  # ('original', '3590S-2-103L'),\n  # ### NTC\n  # ('original', 'NCP15XH103J03RC'),\n\nRF:\n  # Filter (Balun)\n  2450BM14E0003001T: original\n\nTransistors:\n  # NPN\n  MMBT3904-7-F: original\n  # PNP\n  MMBT3906TT1G: original\n  # N-Channel FET\n  2N7002-7-F: original\n  # P-Channel FET\n  BSS84-7-F: original\n"
  }
]